pgbus 0.20.0 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +3 -0
- data/README.md +3 -2
- data/app/frontend/pgbus/modules/charts.js +18 -0
- data/app/frontend/pgbus/modules/charts.min.js +1 -1
- data/app/frontend/pgbus/modules/charts.min.js.map +1 -1
- data/app/helpers/pgbus/workflows_helper.rb +2 -2
- data/app/models/pgbus/blocked_execution.rb +12 -0
- data/app/models/pgbus/uniqueness_key.rb +88 -3
- data/app/models/pgbus/workflow_step.rb +7 -0
- data/app/views/pgbus/workflows/_timeline.html.erb +11 -1
- data/config/locales/da.yml +4 -1
- data/config/locales/de.yml +4 -1
- data/config/locales/en.yml +4 -1
- data/config/locales/es.yml +4 -1
- data/config/locales/fi.yml +4 -1
- data/config/locales/fr.yml +4 -1
- data/config/locales/it.yml +4 -1
- data/config/locales/ja.yml +4 -1
- data/config/locales/nb.yml +4 -1
- data/config/locales/nl.yml +4 -1
- data/config/locales/pt.yml +4 -1
- data/config/locales/sv.yml +4 -1
- data/lib/pgbus/active_job/adapter.rb +81 -3
- data/lib/pgbus/active_job/delivery.rb +20 -6
- data/lib/pgbus/active_job/executor.rb +18 -8
- data/lib/pgbus/concurrency/blocked_execution.rb +14 -6
- data/lib/pgbus/configuration.rb +43 -1
- data/lib/pgbus/dead_letter_header.rb +9 -1
- data/lib/pgbus/instrumentation.rb +3 -2
- data/lib/pgbus/integrations/appsignal/dashboard.json +30 -1
- data/lib/pgbus/metrics/workflow_metrics.rb +11 -1
- data/lib/pgbus/process/consumer.rb +2 -1
- data/lib/pgbus/process/dispatcher.rb +13 -3
- data/lib/pgbus/process/worker.rb +10 -1
- data/lib/pgbus/recurring/schedule.rb +1 -1
- data/lib/pgbus/streams/presence.rb +65 -36
- data/lib/pgbus/uniqueness.rb +23 -6
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source/workflow_list.rb +50 -7
- data/lib/pgbus/web/data_source.rb +123 -15
- data/lib/pgbus/web/workflow_state.rb +16 -3
- data/lib/pgbus/web/workflow_timeline.rb +23 -6
- data/lib/pgbus/workflow/step_definition.rb +4 -2
- data/lib/pgbus/workflow/wait_timeout.rb +25 -4
- data/sig/pgbus/configuration.rbs +1 -0
- metadata +1 -1
|
@@ -6,8 +6,8 @@ module Pgbus
|
|
|
6
6
|
# (its status, or "scheduled" for a ready run whose step waits for a
|
|
7
7
|
# time still ahead) and what it is doing and what happens next, as a
|
|
8
8
|
# pgbus.workflows.next.<key>_html key and its arguments. Arguments named
|
|
9
|
-
# :step, :signal and :batch are names, :ago
|
|
10
|
-
# future one; the helper renders each. Pure.
|
|
9
|
+
# :step, :signal and :batch are names, :ago and :late past moments and
|
|
10
|
+
# :time a future one; the helper renders each. Pure.
|
|
11
11
|
module WorkflowState
|
|
12
12
|
BADGE_TONES = { "ready" => :gray, "scheduled" => :gray, "performing" => :blue, "waiting" => :yellow,
|
|
13
13
|
"paused" => :yellow, "finished" => :green, "cancelled" => :red }.freeze
|
|
@@ -51,7 +51,20 @@ module Pgbus
|
|
|
51
51
|
return ["waiting", "waiting_step", { step: step }] unless row[:waiting_for]
|
|
52
52
|
|
|
53
53
|
args = { signal: row[:waiting_for], ago: row[:waiting_since] }
|
|
54
|
-
|
|
54
|
+
return ["waiting", "waiting_signal", args] unless row[:timeout_at]
|
|
55
|
+
|
|
56
|
+
signal_deadline(row, args.merge(time: row[:timeout_at]))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# An on_timeout: :notify deadline sends a late notice and the wait goes
|
|
60
|
+
# on (issue #565); late_since is the first deadline the wait missed.
|
|
61
|
+
def signal_deadline(row, args)
|
|
62
|
+
# A deploy that changed on_timeout since the last notice: the next
|
|
63
|
+
# deadline does something else (nil: the class is not loaded here).
|
|
64
|
+
late = row[:late_since] && [nil, "notify"].include?(row[:on_timeout]&.to_s)
|
|
65
|
+
return ["waiting", "waiting_signal_late", args.merge(late: row[:late_since])] if late
|
|
66
|
+
|
|
67
|
+
["waiting", row[:on_timeout].to_s == "notify" ? "waiting_signal_notify" : "waiting_signal_timeout", args]
|
|
55
68
|
end
|
|
56
69
|
|
|
57
70
|
def paused(row, step)
|
|
@@ -11,13 +11,15 @@ module Pgbus
|
|
|
11
11
|
# waiting waiting_since → … parked on a signal or its fan-out batch
|
|
12
12
|
#
|
|
13
13
|
# and a paused run's pauses (paused → resumed, from the events) are their
|
|
14
|
-
# own segments, so a long gap explains itself
|
|
15
|
-
#
|
|
16
|
-
# chart. Pure: rows in,
|
|
14
|
+
# own segments, so a long gap explains itself; each wait_timed_out event
|
|
15
|
+
# is a marker. The show page's step table prints durations; the
|
|
16
|
+
# range-bar chart (charts.js renderTimeline) draws chart. Pure: rows in,
|
|
17
|
+
# hashes out.
|
|
17
18
|
module WorkflowTimeline
|
|
18
19
|
OPEN = %w[scheduled waiting in_progress].freeze
|
|
19
20
|
PAUSE_ENDS = %w[resumed cancelled finished].freeze
|
|
20
21
|
PAUSE_EVENTS = (%w[paused] + PAUSE_ENDS).freeze
|
|
22
|
+
WAIT_TIMED_OUT = "wait_timed_out"
|
|
21
23
|
RAN_SERIES = { "completed" => "ran_ok", "skipped" => "ran_ok", "failed" => "ran_failed",
|
|
22
24
|
"in_progress" => "ran_open", "waiting" => "ran_open" }.freeze
|
|
23
25
|
|
|
@@ -69,8 +71,8 @@ module Pgbus
|
|
|
69
71
|
periods
|
|
70
72
|
end
|
|
71
73
|
|
|
72
|
-
# The chart payload: one row per attempt (oldest first)
|
|
73
|
-
# periods. Times are epoch milliseconds; each segment carries its
|
|
74
|
+
# The chart payload: one row per attempt (oldest first), the paused
|
|
75
|
+
# periods and the wait_timed_out marks (wait_timeouts). Times are epoch milliseconds; each segment carries its
|
|
74
76
|
# series (its colour: ran is coloured by how the attempt ended) and its
|
|
75
77
|
# duration in words for the tooltip.
|
|
76
78
|
def chart(steps, events, status:, now:)
|
|
@@ -82,7 +84,20 @@ module Pgbus
|
|
|
82
84
|
paused = paused_periods(events, status: status, now: now).map do |period|
|
|
83
85
|
chart_segment(period.merge(kind: :paused), "paused")
|
|
84
86
|
end
|
|
85
|
-
{ rows: rows, paused: paused }
|
|
87
|
+
{ rows: rows, paused: paused, wait_timeouts: wait_timeouts(events) }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# [{ at:, step:, action: }] per wait_timed_out event, oldest first, at
|
|
91
|
+
# the deadline it reports (when it went out, without one): the chart
|
|
92
|
+
# draws each as a line across the rows, the page lists them. An
|
|
93
|
+
# on_timeout: :notify wait has one per late notice (issue #565).
|
|
94
|
+
def wait_timeouts(events)
|
|
95
|
+
timed_out = events.select { |event| event[:event] == WAIT_TIMED_OUT }.sort_by { |event| event[:created_at] }
|
|
96
|
+
timed_out.map do |event|
|
|
97
|
+
details = event[:details].to_h
|
|
98
|
+
{ at: millis(deadline(details["timeout_at"]) || event[:created_at]),
|
|
99
|
+
step: "#{event[:step_name]} ##{event[:attempt]}", action: details["action"] }
|
|
100
|
+
end
|
|
86
101
|
end
|
|
87
102
|
|
|
88
103
|
def series_for(kind, step)
|
|
@@ -98,6 +113,8 @@ module Pgbus
|
|
|
98
113
|
|
|
99
114
|
def millis(time) = (time.to_f * 1000).round
|
|
100
115
|
|
|
116
|
+
def deadline(iso) = iso && Time.iso8601(iso)
|
|
117
|
+
|
|
101
118
|
# Drops a piece without both ends or of no length, and ends every piece
|
|
102
119
|
# at the attempt's finished_at (a step cancelled before its scheduled_at).
|
|
103
120
|
def clip(pieces, finished)
|
|
@@ -8,7 +8,9 @@ module Pgbus
|
|
|
8
8
|
class StepDefinition
|
|
9
9
|
OPTIONS = %i[wait skip_if job_options timeout on_exception wait_for wait_timeout on_timeout].freeze
|
|
10
10
|
# What a wait_for: step does when its wait_timeout: passes first.
|
|
11
|
-
ON_TIMEOUT = %i[pause! cancel! skip! run].freeze
|
|
11
|
+
ON_TIMEOUT = %i[pause! cancel! skip! run notify].freeze
|
|
12
|
+
# The values written without a bang (they do not end the wait as a verb would).
|
|
13
|
+
PLAIN_ON_TIMEOUT = %w[run notify].freeze
|
|
12
14
|
# The StepJob options a class (set_step_job_options) or a step
|
|
13
15
|
# (job_options:) may set. Anything else raises at class load.
|
|
14
16
|
JOB_OPTIONS = %i[queue priority wait wait_until].freeze
|
|
@@ -115,7 +117,7 @@ module Pgbus
|
|
|
115
117
|
end
|
|
116
118
|
|
|
117
119
|
def normalize_on_timeout(value)
|
|
118
|
-
verb = value.to_s
|
|
120
|
+
verb = PLAIN_ON_TIMEOUT.include?(value.to_s) ? value.to_sym : :"#{value.to_s.delete_suffix("!")}!"
|
|
119
121
|
return verb if ON_TIMEOUT.include?(verb)
|
|
120
122
|
|
|
121
123
|
raise ArgumentError, "on_timeout: must be one of #{ON_TIMEOUT.map(&:inspect).join(", ")}, got #{value.inspect}"
|
|
@@ -12,10 +12,13 @@ module Pgbus
|
|
|
12
12
|
# :skip! the step is skipped and the run moves on
|
|
13
13
|
# :run the step is scheduled again; its body runs with
|
|
14
14
|
# received_signal nil and timed_out? true
|
|
15
|
+
# :notify only the event and the instrumentation (issue #565): the
|
|
16
|
+
# same attempt keeps waiting and timeout_at moves to its next
|
|
17
|
+
# deadline, one wait_timeout on (see next_deadline)
|
|
15
18
|
#
|
|
16
19
|
# A signal that arrives after the timeout fired stays pending, like any
|
|
17
|
-
# signal no step waits for
|
|
18
|
-
# it is resumed.
|
|
20
|
+
# signal no step waits for, except under :notify, whose step still waits
|
|
21
|
+
# for it. A paused run's wait does not time out until it is resumed.
|
|
19
22
|
module WaitTimeout
|
|
20
23
|
module_function
|
|
21
24
|
|
|
@@ -53,17 +56,35 @@ module Pgbus
|
|
|
53
56
|
timeout_at: step.timeout_at&.utc&.iso8601(3) }.compact)
|
|
54
57
|
notes.instrument("pgbus.workflow.wait_timed_out",
|
|
55
58
|
Runner.payload_for(record, step_name: step.step_name, attempt: step.attempt, action: action.to_s))
|
|
56
|
-
apply(record, step, action, notes: notes, now: now)
|
|
59
|
+
apply(record, step, action, notes: notes, now: now, wait_timeout: definition&.wait_timeout)
|
|
57
60
|
action
|
|
58
61
|
end
|
|
59
62
|
|
|
60
|
-
def apply(record, step, action, notes:, now:)
|
|
63
|
+
def apply(record, step, action, notes:, now:, wait_timeout: nil)
|
|
64
|
+
return notify!(step, wait_timeout, now: now) if action == :notify
|
|
61
65
|
return Waiting.wake!(record, step, now: now, timeout_at: [step.timeout_at, now].compact.min) if action == :run
|
|
62
66
|
|
|
63
67
|
error = action == :pause! ? timed_out_error(step, now) : nil
|
|
64
68
|
Waiting.resolve!(record, step, FlowControl::Outcome.for(action), notes: notes, error: error)
|
|
65
69
|
end
|
|
66
70
|
|
|
71
|
+
# The run and the step row stay as they are (no outcome, no StepJob);
|
|
72
|
+
# only timeout_at moves.
|
|
73
|
+
def notify!(step, wait_timeout, now:)
|
|
74
|
+
timeout_at = next_deadline(step.timeout_at, wait_timeout, now)
|
|
75
|
+
step.timeout_at = timeout_at if WorkflowStep.move_deadline!(step.id, timeout_at: timeout_at).positive?
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# The first deadline after +now+ on the wait's wait_timeout grid: one
|
|
79
|
+
# wait_timeout on when the sweep is within a period of the deadline
|
|
80
|
+
# (and for expire_wait ahead of it). A sweep that missed several
|
|
81
|
+
# deadlines (the dispatcher was down) reports them once: a deadline
|
|
82
|
+
# still behind now would be selected again by the next batch.
|
|
83
|
+
def next_deadline(timeout_at, wait_timeout, now)
|
|
84
|
+
missed = timeout_at > now ? 0 : ((now - timeout_at) / wait_timeout).floor
|
|
85
|
+
timeout_at + ((missed + 1) * wait_timeout)
|
|
86
|
+
end
|
|
87
|
+
|
|
67
88
|
def timed_out_error(step, now)
|
|
68
89
|
WaitTimedOut.new("#{step.step_name} waited for #{step.wait_for} since #{step.waiting_since&.utc&.iso8601} " \
|
|
69
90
|
"and timed out at #{(step.timeout_at || now).utc.iso8601}")
|
data/sig/pgbus/configuration.rbs
CHANGED
|
@@ -293,6 +293,7 @@ module Pgbus
|
|
|
293
293
|
def visibility_heartbeat_interval=: (untyped value) -> untyped
|
|
294
294
|
def visibility_timeout: () -> Numeric?
|
|
295
295
|
def visibility_timeout=: (untyped value) -> untyped
|
|
296
|
+
def warn_queue_prefix_underscores: () -> untyped
|
|
296
297
|
def warn_read_ahead_without_heartbeat: (untyped settings) -> untyped
|
|
297
298
|
def web_actor: () -> untyped
|
|
298
299
|
def web_actor=: (untyped) -> untyped
|