Module: RoundhouseUi::Runbooks
- Defined in:
- lib/roundhouse_ui/runbooks.rb
Overview
Resolves a runbook for a job class at read time, the same way Tags resolves owners (ADR 0002) — no middleware, no storage, applies retroactively to jobs already in the sets.
RoundhouseUi.job_runbooks = RoundhouseUi::Runbooks.from_constant(:RUNBOOK)
RoundhouseUi.job_runbooks = { "Billing::SyncWorker" => "https://wiki/…" }
RoundhouseUi.job_runbooks = ->(klass:, item:) { "https://wiki/#{klass}" }
Whoever wrote the job knows what to do when it fails; the person paged at 3am usually does not. This is the cheapest possible bridge between them.
Class Method Summary collapse
-
.for(klass, item = nil, cache: nil) ⇒ Object
A runbook URL for one job, or nil.
-
.from_constant(const_name = :RUNBOOK) ⇒ Object
The class-constant convention, matching Tags.from_constant.
-
.resolve(source, klass, item) ⇒ Object
-- internals ----------------------------------------------------------.
- .safe_url(raw) ⇒ Object
- .warn_once(message) ⇒ Object
Class Method Details
.for(klass, item = nil, cache: nil) ⇒ Object
A runbook URL for one job, or nil. Resolvers always see the real job class — the ActiveJob wrapper is unwrapped first, so a runbook declared on a mailer is found when that mailer fails.
Only http(s) URLs are returned. A runbook lands in an href, so javascript:
and data: are refused rather than escaped: a host that misconfigures this
should get no link, not a link that runs.
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
# File 'lib/roundhouse_ui/runbooks.rb', line 22 def for(klass, item = nil, cache: nil) source = RoundhouseUi.job_runbooks return nil unless source effective = RoundhouseUi.unwrapped_class(klass, item) return nil unless effective.present? return resolve(source, effective, item) unless cache # `item` is withheld once we are caching by class — the same rule Tags # follows, and for the same reason: a resolver that reads the payload but # is cached per class would hand every job of that class the *first* job's # runbook. Withholding makes that misuse resolve to nil deterministically # instead of returning something plausible and wrong. A runbook describes a # job class, so reading the payload is a misuse rather than a use case. cache.key?(effective) ? cache[effective] : cache[effective] = resolve(source, effective, nil) end |
.from_constant(const_name = :RUNBOOK) ⇒ Object
The class-constant convention, matching Tags.from_constant. Inherited constants count, so a base class can carry the runbook for a whole family.
41 42 43 44 45 46 47 |
# File 'lib/roundhouse_ui/runbooks.rb', line 41 def from_constant(const_name = :RUNBOOK) lambda do |klass:, item:| _ = item k = RoundhouseUi.job_class(klass) k.const_get(const_name) if k&.const_defined?(const_name) end end |
.resolve(source, klass, item) ⇒ Object
-- internals ----------------------------------------------------------
51 52 53 54 55 56 57 58 59 60 61 |
# File 'lib/roundhouse_ui/runbooks.rb', line 51 def resolve(source, klass, item) raw = if source.respond_to?(:call) source.call(klass: klass, item: item) elsif source.respond_to?(:[]) source[klass] end safe_url(raw) rescue StandardError => e warn_once("job_runbooks resolver failed for #{klass}: #{e.}") nil end |
.safe_url(raw) ⇒ Object
63 64 65 66 67 68 69 70 71 |
# File 'lib/roundhouse_ui/runbooks.rb', line 63 def safe_url(raw) url = raw.to_s.strip # Capped like theme values are: this is interpolated into markup on every # matching row, and a runaway string from a misconfigured resolver should # not become the page. return nil if url.empty? || url.length > 2_000 url if url.match?(%r{\Ahttps?://\S+\z}i) end |
.warn_once(message) ⇒ Object
73 |
# File 'lib/roundhouse_ui/runbooks.rb', line 73 def warn_once() = RoundhouseUi.warn_once() |