Module: CloseYourIt::Rails::ActiveJobExtension

Defined in:
lib/closeyourit/rails/active_job_extension.rb

Overview

Incluso in ActiveJob::Base (via railtie on_load(:active_job)): cattura gli errori dei job (oggi persi) con il contesto del job. La logica vive in .monitor/.report_discarded per essere testabile senza ActiveSupport/ActiveJob.

CYRB-19: around_perform gira DENTRO perform_now, mentre retry_on/discard_on (rescue_from) sono valutati FUORI, dopo i callback. Catturare nell'around_perform significa quindi segnalare OGNI tentativo — anche quelli che verranno ritentati con successo — perché il reporter vede l'errore prima che il retry possa zittirlo, e ogni tentativo solleva una nuova istanza (la deduplica interna, per-istanza, non interviene). Su ActiveJob 7.1+ deleghiamo la segnalazione ad after_discard, che Rails invoca al fallimento definitivo (retry_on esauriti o eccezione non gestita), mai sui tentativi che retry_on ritenta → una sola occorrenza per job.

Limiti noti (per costruzione di ActiveJob, non del client):

  • after_discard NON scatta per gli errori intercettati da un rescue_from custom: sono gestiti dall'app, quindi non li segnaliamo più come "non gestiti" (prima lo facevamo, impropriamente).
  • I retry a livello di ADAPTER (es. Sidekiq) SENZA retry_on risollevano l'errore non gestito: after_discard scatta a ogni esecuzione, quindi lì la de-duplicazione per tentativo non si applica (comportamento invariato rispetto a prima). Sulle versioni prive di after_discard restiamo al fallback legacy (cattura nell'around_perform).

Constant Summary collapse

STASHED_SCOPE_IVAR =

Scope arricchito durante il perform tramandato ad after_discard. Legato all'ISTANZA del job (ogni retry ne crea una nuova, deserializzata) → vive esattamente quanto serve, niente bleed tra job o thread; il reset di fine perform lo sgancia solo dallo storage, non muta l'oggetto.

:@__closeyourit_stashed_scope

Class Method Summary collapse

Class Method Details

.apply_job_scope(job) ⇒ Object



87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/closeyourit/rails/active_job_extension.rb', line 87

def self.apply_job_scope(job)
  CloseYourIt.set_tag("job.class", job.class.name)
  CloseYourIt.set_tag("job.queue", job.queue_name) if job.respond_to?(:queue_name)

  context = {}
  context["job_id"] = job.job_id if job.respond_to?(:job_id)
  context["executions"] = job.executions if job.respond_to?(:executions)
  CloseYourIt.set_context("active_job", context) unless context.empty?

  # Un trace_id stabile per esecuzione (job_id è unico per run ActiveJob): log ed errore dello
  # stesso job condividono il trace_id → correlazione log↔errore anche fuori dal ciclo richiesta.
  CloseYourIt::Scope.current.trace_id = job.job_id if job.respond_to?(:job_id)
end

.included(base) ⇒ Object



30
31
32
33
34
35
36
37
38
39
40
# File 'lib/closeyourit/rails/active_job_extension.rb', line 30

def self.included(base)
  base.around_perform do |job, block|
    CloseYourIt::Rails::ActiveJobExtension.monitor(job) { block.call }
  end

  return unless base.respond_to?(:after_discard)

  base.after_discard do |job, exception|
    CloseYourIt::Rails::ActiveJobExtension.report_discarded(job, exception)
  end
end

.monitor(job) ⇒ Object

Esegue il job arricchendo lo scope con tag/context; resetta lo scope a fine job (no bleed tra job sullo stesso thread). Su ActiveJob 7.1+ NON cattura l'errore (lo fa after_discard solo al fallimento definitivo): qui tramanda lo scope al job e ri-solleva. Sul fallback legacy cattura.



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/closeyourit/rails/active_job_extension.rb', line 45

def self.monitor(job)
  return yield unless CloseYourIt.configuration.report_active_job_errors

  begin
    apply_job_scope(job)
    yield
  rescue Exception => e # rubocop:disable Lint/RescueException
    if report_on_discard?(job)
      job.instance_variable_set(STASHED_SCOPE_IVAR, CloseYourIt::Scope.current)
    else
      CloseYourIt.capture_exception(e, handled: false)
    end
    raise
  ensure
    CloseYourIt::Scope.reset!
  end
end

.report_discarded(job, exception) ⇒ Object

Aggancio after_discard (ActiveJob 7.1+): il job è definitivamente fallito, quindi segnaliamo l'errore UNA sola volta (handled:false). Riprendiamo lo scope arricchito durante il perform (tramandato da monitor) così il report conserva breadcrumb/tag/contesti raccolti nel job; apply_job_scope rinfresca i campi standard con executions finale senza perdere i custom.



67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/closeyourit/rails/active_job_extension.rb', line 67

def self.report_discarded(job, exception)
  return unless CloseYourIt.configuration.report_active_job_errors

  begin
    stashed = job.instance_variable_get(STASHED_SCOPE_IVAR)
    CloseYourIt::Scope.current = stashed if stashed
    apply_job_scope(job)
    CloseYourIt.capture_exception(exception, handled: false)
  ensure
    job.remove_instance_variable(STASHED_SCOPE_IVAR) if job.instance_variable_defined?(STASHED_SCOPE_IVAR)
    CloseYourIt::Scope.reset!
  end
end

.report_on_discard?(job) ⇒ Boolean

true quando ActiveJob espone after_discard (7.1+): la segnalazione è delegata lì, così monitor non cattura i tentativi intermedi. false → fallback legacy (cattura nell'around_perform).

Returns:

  • (Boolean)


83
84
85
# File 'lib/closeyourit/rails/active_job_extension.rb', line 83

def self.report_on_discard?(job)
  job.class.respond_to?(:after_discard)
end