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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +3 -0
  3. data/README.md +3 -2
  4. data/app/frontend/pgbus/modules/charts.js +18 -0
  5. data/app/frontend/pgbus/modules/charts.min.js +1 -1
  6. data/app/frontend/pgbus/modules/charts.min.js.map +1 -1
  7. data/app/helpers/pgbus/workflows_helper.rb +2 -2
  8. data/app/models/pgbus/blocked_execution.rb +12 -0
  9. data/app/models/pgbus/uniqueness_key.rb +88 -3
  10. data/app/models/pgbus/workflow_step.rb +7 -0
  11. data/app/views/pgbus/workflows/_timeline.html.erb +11 -1
  12. data/config/locales/da.yml +4 -1
  13. data/config/locales/de.yml +4 -1
  14. data/config/locales/en.yml +4 -1
  15. data/config/locales/es.yml +4 -1
  16. data/config/locales/fi.yml +4 -1
  17. data/config/locales/fr.yml +4 -1
  18. data/config/locales/it.yml +4 -1
  19. data/config/locales/ja.yml +4 -1
  20. data/config/locales/nb.yml +4 -1
  21. data/config/locales/nl.yml +4 -1
  22. data/config/locales/pt.yml +4 -1
  23. data/config/locales/sv.yml +4 -1
  24. data/lib/pgbus/active_job/adapter.rb +81 -3
  25. data/lib/pgbus/active_job/delivery.rb +20 -6
  26. data/lib/pgbus/active_job/executor.rb +18 -8
  27. data/lib/pgbus/concurrency/blocked_execution.rb +14 -6
  28. data/lib/pgbus/configuration.rb +43 -1
  29. data/lib/pgbus/dead_letter_header.rb +9 -1
  30. data/lib/pgbus/instrumentation.rb +3 -2
  31. data/lib/pgbus/integrations/appsignal/dashboard.json +30 -1
  32. data/lib/pgbus/metrics/workflow_metrics.rb +11 -1
  33. data/lib/pgbus/process/consumer.rb +2 -1
  34. data/lib/pgbus/process/dispatcher.rb +13 -3
  35. data/lib/pgbus/process/worker.rb +10 -1
  36. data/lib/pgbus/recurring/schedule.rb +1 -1
  37. data/lib/pgbus/streams/presence.rb +65 -36
  38. data/lib/pgbus/uniqueness.rb +23 -6
  39. data/lib/pgbus/version.rb +1 -1
  40. data/lib/pgbus/web/data_source/workflow_list.rb +50 -7
  41. data/lib/pgbus/web/data_source.rb +123 -15
  42. data/lib/pgbus/web/workflow_state.rb +16 -3
  43. data/lib/pgbus/web/workflow_timeline.rb +23 -6
  44. data/lib/pgbus/workflow/step_definition.rb +4 -2
  45. data/lib/pgbus/workflow/wait_timeout.rb +25 -4
  46. data/sig/pgbus/configuration.rbs +1 -0
  47. 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 a past moment and :time a
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
- row[:timeout_at] ? ["waiting", "waiting_signal_timeout", args.merge(time: row[:timeout_at])] : ["waiting", "waiting_signal", args]
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. The show page's step table
15
- # prints durations; the range-bar chart (charts.js renderTimeline) draws
16
- # chart. Pure: rows in, hashes out.
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) and the paused
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 == "run" ? :run : :"#{value.to_s.delete_suffix("!")}!"
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. A paused run's wait does not time out until
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}")
@@ -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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pgbus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.20.0
4
+ version: 0.20.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson