Module: Axn::Async::ExceptionReporting
- Defined in:
- lib/axn/async/exception_reporting.rb
Overview
Shared utilities for async exception reporting across adapters. Used by both Sidekiq (death handler) and ActiveJob (after_discard) to build context and trigger on_exception consistently.
Defined Under Namespace
Classes: DiscardedJobAction, DiscardedJobResult
Class Method Summary collapse
-
.trigger_on_exception(exception:, action_class:, retry_context:, job_args:, extra_context: {}, log_prefix: "async") ⇒ Object
Triggers on_exception for an async job that has been discarded/exhausted.
Class Method Details
.trigger_on_exception(exception:, action_class:, retry_context:, job_args:, extra_context: {}, log_prefix: "async") ⇒ Object
Triggers on_exception for an async job that has been discarded/exhausted.
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
# File 'lib/axn/async/exception_reporting.rb', line 21 def trigger_on_exception(exception:, action_class:, retry_context:, job_args:, extra_context: {}, log_prefix: "async") Axn::Extensions.best_effort("in #{log_prefix}") do # NOTE: deliberately NOT guarded by `_fails_on?`. This is the discard/death-handler path, # which only fires after a job exhausts retries or is discarded. A `fails_on` exception # settles as `outcome.failure?` and is never re-raised by the adapter (see the # `raise … if result.outcome.exception?` gate in the Sidekiq/ActiveJob `perform`), so it # never reaches here. Anything that does reach here either was a genuine `exception` # outcome (so `_fails_on?` is necessarily false) or bypassed the executor entirely (job # deserialization / proxy errors) — and a broad declaration like `fails_on StandardError` # must NOT suppress the only global report for those. # Filter sensitive values using the action class's internal _context_slice filtered_context = action_class._context_slice(data: job_args, direction: :inbound) # Build final context with async info (avoid mutating extra_context) async_extra = extra_context[:async] || {} context = filtered_context.merge( async: retry_context.to_h.merge(async_extra), ).merge(extra_context.except(:async)) # Attach declared observability facets (PRO-2853) so an exhausted/discarded-job report # carries the same context[:tags]/context[:dimensions] as the synchronous executor path. # There's no settled action instance here (the run died in a prior attempt), so facets are # resolved best-effort against an instance reconstructed from the (deserialized) job_args: # input-derived facets (company_id, record ids) resolve; output-derived ones find no exposes # and are skipped per-facet — the same partial-resolution contract as the failure path. # # Reserve the facet keys first: unlike the sync report (where inputs nest under :inputs and # RESERVED_EXECUTION_CONTEXT_KEYS guards the top level), this path merges the job-arg slice # in at the top level, so an action with an input literally named `tags`/`dimensions` would # otherwise expose that user value under the framework key whenever no facet overwrites it. # Strip, then assign only the resolved facets (when any) — framework owns these keys. %i[tags dimensions].each { |key| context.delete(key) } facets = resolve_facets(action_class:, job_args:) context[:tags] = facets[:tags] if facets[:tags].any? context[:dimensions] = facets[:dimensions] if facets[:dimensions].any? # Create proxy action for the on_exception interface proxy_action = DiscardedJobAction.new(action_class, exception) # Trigger on_exception Axn.config.on_exception(exception, action: proxy_action, context:) end end |