lepus-saga 0.3.0 → 0.3.2
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 +13 -0
- data/Gemfile.lock +2 -2
- data/README.md +10 -8
- data/lib/lepus/saga/execution_behaviour.rb +10 -1
- data/lib/lepus/saga/executor.rb +4 -4
- data/lib/lepus/saga/rspec/engine_examples.rb +8 -1
- data/lib/lepus/saga/rspec/query_examples.rb +187 -15
- data/lib/lepus/saga/rspec/store_examples.rb +8 -7
- data/lib/lepus/saga/stores/base.rb +89 -18
- data/lib/lepus/saga/stores/memory.rb +35 -10
- data/lib/lepus/saga/version.rb +1 -1
- data/lib/lepus/saga/web/api.rb +53 -4
- data/lib/lepus/saga/web/serializer.rb +14 -1
- data/lib/lepus/saga.rb +1 -1
- data/web/assets/app.css +45 -1
- data/web/assets/js/api.js +1 -0
- data/web/assets/js/dom.js +1 -1
- data/web/assets/js/map.js +142 -10
- data/web/assets/js/views/execution.js +3 -3
- data/web/assets/js/views/executions.js +95 -21
- data/web/assets/js/views/overview.js +171 -7
- metadata +1 -1
|
@@ -29,6 +29,9 @@ module Lepus
|
|
|
29
29
|
QUERY_MAX_ROWS = 50_000
|
|
30
30
|
MAX_SERIES_BUCKETS = 1_000
|
|
31
31
|
GROUPABLE = %i[saga_name status partition current_step].freeze
|
|
32
|
+
SORTS = {started_at: :started_at, duration: :duration_ms, finished_at: :finished_at}.freeze
|
|
33
|
+
NULLABLE_SORTS = %i[duration_ms finished_at].freeze
|
|
34
|
+
DIRECTIONS = %i[desc asc].freeze
|
|
32
35
|
SERIES_KEYS = ["started", *ExecutionBehaviour::TERMINAL_STATUSES].freeze
|
|
33
36
|
LATENCY_STATUSES = %w[succeeded failed].freeze
|
|
34
37
|
PERCENTILES = {p50: 50, p95: 95, p99: 99}.freeze
|
|
@@ -87,7 +90,8 @@ module Lepus
|
|
|
87
90
|
raise NotImplementedError
|
|
88
91
|
end
|
|
89
92
|
|
|
90
|
-
# Executions of a resource, most recently
|
|
93
|
+
# Executions of a resource, most recently started first (ties by id,
|
|
94
|
+
# descending).
|
|
91
95
|
def executions_for_resource(resource_type, resource_id)
|
|
92
96
|
raise NotImplementedError
|
|
93
97
|
end
|
|
@@ -112,6 +116,16 @@ module Lepus
|
|
|
112
116
|
raise NotImplementedError
|
|
113
117
|
end
|
|
114
118
|
|
|
119
|
+
# Names of the steps each of `executions` ran forward (attempts in the
|
|
120
|
+
# execute direction, whatever their status), as a Hash keyed by
|
|
121
|
+
# execution id, every id present. Stores override it to read them in
|
|
122
|
+
# one query.
|
|
123
|
+
def executed_steps(executions)
|
|
124
|
+
executions.to_h do |execution|
|
|
125
|
+
[execution.id, step_attempts(execution).select { |attempt| attempt.direction == "execute" }.map(&:step_name).uniq]
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
115
129
|
# Number of attempts of `execution` for this step, direction and epoch,
|
|
116
130
|
# whatever their status.
|
|
117
131
|
def count_step_attempts(execution, step_name:, direction:, epoch:)
|
|
@@ -139,23 +153,38 @@ module Lepus
|
|
|
139
153
|
# Whether the optional query methods below are implemented.
|
|
140
154
|
def queries? = false
|
|
141
155
|
|
|
142
|
-
# Executions matching the filters,
|
|
143
|
-
#
|
|
144
|
-
#
|
|
145
|
-
# `
|
|
146
|
-
# `
|
|
147
|
-
#
|
|
148
|
-
#
|
|
149
|
-
#
|
|
150
|
-
#
|
|
151
|
-
#
|
|
152
|
-
#
|
|
153
|
-
#
|
|
154
|
-
#
|
|
156
|
+
# Executions matching the filters, at most `limit` of them, in the
|
|
157
|
+
# order of `sort` and `direction`:
|
|
158
|
+
#
|
|
159
|
+
# - `sort: :started_at` (the default), `:duration` (duration_ms) or
|
|
160
|
+
# `:finished_at`, `direction: :desc` (the default) or `:asc`. The id
|
|
161
|
+
# breaks ties in the same direction. Executions without a value
|
|
162
|
+
# (in-progress ones have no duration_ms or finished_at) come last in
|
|
163
|
+
# either direction, by id. Ids need not follow time: rows written
|
|
164
|
+
# with ids that are not time-ordered (e.g. random UUIDv4) still list
|
|
165
|
+
# in time order.
|
|
166
|
+
# - `after` is the id of the last execution of the previous page: the
|
|
167
|
+
# page holds the executions strictly after it, by (sort value, id),
|
|
168
|
+
# in the same order. An `after` that names no execution returns an
|
|
169
|
+
# empty page.
|
|
170
|
+
#
|
|
171
|
+
# Every filter is optional: `saga_name`, `status` and `current_step`
|
|
172
|
+
# take a value or a list; `partition` matches the partition column,
|
|
173
|
+
# and executions without one whose definition runs in that partition;
|
|
174
|
+
# `resource` is a case-insensitive substring of resource_type or
|
|
175
|
+
# resource_id; `from` (inclusive) and `to` (exclusive) bound
|
|
176
|
+
# started_at, `progress_from` and `progress_to` bound
|
|
177
|
+
# last_progress_at and `finished_from` and `finished_to` bound
|
|
178
|
+
# finished_at the same way (in-progress executions with
|
|
179
|
+
# `progress_to: Time.now.utc - 15 * 60` are the stalled ones).
|
|
180
|
+
# `error_class` and `error_step` take a value or a list and keep
|
|
181
|
+
# executions with a failed attempt of that error class and at that
|
|
182
|
+
# step (either filter alone matches on its own attribute);
|
|
155
183
|
# `error_from` (inclusive) and `error_to` (exclusive) bound that
|
|
156
184
|
# failed attempt's started_at, the window #failure_summary counts.
|
|
157
|
-
def list_executions(
|
|
158
|
-
|
|
185
|
+
def list_executions(sort: :started_at, direction: :desc, after: nil, limit: 50, saga_name: nil, status: nil, current_step: nil,
|
|
186
|
+
partition: nil, resource: nil, from: nil, to: nil, progress_from: nil, progress_to: nil, finished_from: nil, finished_to: nil,
|
|
187
|
+
error_class: nil, error_step: nil, error_from: nil, error_to: nil)
|
|
159
188
|
raise NotImplementedError
|
|
160
189
|
end
|
|
161
190
|
|
|
@@ -165,11 +194,37 @@ module Lepus
|
|
|
165
194
|
# resolves executions without one to their definition's partition
|
|
166
195
|
# (nil for an unknown saga).
|
|
167
196
|
def count_executions(group_by:, saga_name: nil, status: nil, current_step: nil, partition: nil, resource: nil, from: nil, to: nil,
|
|
168
|
-
progress_from: nil, progress_to: nil,
|
|
197
|
+
progress_from: nil, progress_to: nil, finished_from: nil, finished_to: nil, error_class: nil, error_step: nil, error_from: nil,
|
|
198
|
+
error_to: nil)
|
|
169
199
|
raise NotImplementedError
|
|
170
200
|
end
|
|
171
201
|
|
|
172
|
-
# Executions
|
|
202
|
+
# Executions that finished between `from` (inclusive) and `to`
|
|
203
|
+
# (exclusive), latest finish first (ties by id), at most `per_saga`
|
|
204
|
+
# of each saga when given and at most `limit` in all. `saga_name`
|
|
205
|
+
# takes a value or a list. Returns `{executions: [...], counts:
|
|
206
|
+
# {[saga_name, status] => n}}`, the counts covering every execution
|
|
207
|
+
# that finished in the window, so a caller can tell how many the
|
|
208
|
+
# limits left out. `listed_from` (at most `from`) widens the listed
|
|
209
|
+
# executions, not the counts, back to that time, so a caller polling
|
|
210
|
+
# with some overlap can catch late writes without counting them
|
|
211
|
+
# twice. Built on #list_executions and #count_executions.
|
|
212
|
+
def finished_executions(from:, to:, listed_from: nil, saga_name: nil, per_saga: nil, limit: 100)
|
|
213
|
+
counts = count_executions(group_by: %i[saga_name status], saga_name: saga_name, finished_from: from, finished_to: to)
|
|
214
|
+
window = {saga_name: saga_name, finished_from: [listed_from, from].compact.min, finished_to: to}
|
|
215
|
+
names = (per_saga && listed_from.nil?) ? counts.keys.map(&:first).uniq : nil
|
|
216
|
+
executions = if per_saga
|
|
217
|
+
(names || saga_names_finished(**window)).sort.flat_map do |name|
|
|
218
|
+
list_executions(**window, saga_name: name, sort: :finished_at, limit: [per_saga, limit].min)
|
|
219
|
+
end
|
|
220
|
+
else
|
|
221
|
+
list_executions(**window, sort: :finished_at, limit: limit)
|
|
222
|
+
end
|
|
223
|
+
ordered = executions.sort_by { |execution| [execution.finished_at.to_r, execution.id] }.reverse
|
|
224
|
+
{executions: ordered.first(limit), counts: counts}
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Executions started (by started_at) and finished in each terminal
|
|
173
228
|
# status (by finished_at) per `bucket` seconds between `from` and
|
|
174
229
|
# `to`. Returns `{rows: [[bucket_start, {"started" => n, "completed" =>
|
|
175
230
|
# n, ...}], ...], truncated:}` with every bucket of the window, bucket
|
|
@@ -226,6 +281,22 @@ module Lepus
|
|
|
226
281
|
attributes.each { |name, value| record.public_send(:"#{name}=", value) }
|
|
227
282
|
end
|
|
228
283
|
|
|
284
|
+
# The column and direction of a #list_executions order, and whether
|
|
285
|
+
# rows without a value can match: never for finished_at when a
|
|
286
|
+
# finished_at bound is among the filters.
|
|
287
|
+
def list_order(sort, direction, filters = {})
|
|
288
|
+
column = SORTS[sort.to_s.to_sym] || raise(ArgumentError, "Cannot sort executions by #{sort.inspect}")
|
|
289
|
+
descending = direction.to_s.to_sym
|
|
290
|
+
raise ArgumentError, "Unknown sort direction #{direction.inspect}" unless DIRECTIONS.include?(descending)
|
|
291
|
+
|
|
292
|
+
bounded = column == :finished_at && (filters[:finished_from] || filters[:finished_to])
|
|
293
|
+
[column, descending == :desc, NULLABLE_SORTS.include?(column) && !bounded]
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def saga_names_finished(**window)
|
|
297
|
+
count_executions(group_by: :saga_name, **window).keys
|
|
298
|
+
end
|
|
299
|
+
|
|
229
300
|
def filter_values(value)
|
|
230
301
|
values = Array(value).map(&:to_s)
|
|
231
302
|
values unless values.empty?
|
|
@@ -131,7 +131,7 @@ module Lepus
|
|
|
131
131
|
|
|
132
132
|
def executions_for_resource(resource_type, resource_id)
|
|
133
133
|
where(@executions, Execution) { |row| row[:resource_type] == resource_type && row[:resource_id] == resource_id }
|
|
134
|
-
.sort_by { |execution| [execution.
|
|
134
|
+
.sort_by { |execution| [execution.started_at, execution.id] }.reverse
|
|
135
135
|
end
|
|
136
136
|
|
|
137
137
|
def create_step_attempt(execution, attributes)
|
|
@@ -177,10 +177,15 @@ module Lepus
|
|
|
177
177
|
|
|
178
178
|
def queries? = true
|
|
179
179
|
|
|
180
|
-
def list_executions(
|
|
180
|
+
def list_executions(sort: :started_at, direction: :desc, after: nil, limit: 50, **filters)
|
|
181
|
+
column, descending, = list_order(sort, direction, filters)
|
|
182
|
+
cursor = after && find_execution(after)
|
|
183
|
+
return [] if after && !cursor
|
|
184
|
+
|
|
185
|
+
compare = list_comparator(column, descending)
|
|
181
186
|
executions = matching_executions(**filters)
|
|
182
|
-
executions = executions.select { |execution| execution.
|
|
183
|
-
executions.
|
|
187
|
+
executions = executions.select { |execution| compare.call(execution, cursor).positive? } if cursor
|
|
188
|
+
executions.sort(&compare).first(limit)
|
|
184
189
|
end
|
|
185
190
|
|
|
186
191
|
def count_executions(group_by:, **filters)
|
|
@@ -192,7 +197,7 @@ module Lepus
|
|
|
192
197
|
window = from...to
|
|
193
198
|
executions = matching_executions(saga_name: saga_name)
|
|
194
199
|
started, started_truncated = take_rows(
|
|
195
|
-
executions.map(&:
|
|
200
|
+
executions.map(&:started_at).select { |time| window.cover?(time) }.sort.reverse, max_rows
|
|
196
201
|
)
|
|
197
202
|
finished, finished_truncated = take_rows(
|
|
198
203
|
executions.select { |execution| execution.terminal? && window.cover?(execution.finished_at) }
|
|
@@ -229,7 +234,8 @@ module Lepus
|
|
|
229
234
|
private
|
|
230
235
|
|
|
231
236
|
def matching_executions(saga_name: nil, status: nil, current_step: nil, partition: nil, resource: nil, from: nil, to: nil,
|
|
232
|
-
progress_from: nil, progress_to: nil,
|
|
237
|
+
progress_from: nil, progress_to: nil, finished_from: nil, finished_to: nil, error_class: nil, error_step: nil, error_from: nil,
|
|
238
|
+
error_to: nil)
|
|
233
239
|
sagas = filter_values(saga_name)
|
|
234
240
|
statuses = filter_values(status)
|
|
235
241
|
steps = filter_values(current_step)
|
|
@@ -241,16 +247,35 @@ module Lepus
|
|
|
241
247
|
(sagas.nil? || sagas.include?(row[:saga_name])) &&
|
|
242
248
|
(statuses.nil? || statuses.include?(row[:status])) &&
|
|
243
249
|
(steps.nil? || steps.include?(row[:current_step])) &&
|
|
244
|
-
(
|
|
245
|
-
(
|
|
246
|
-
(
|
|
247
|
-
(progress_to.nil? || row[:last_progress_at] < progress_to) &&
|
|
250
|
+
within?(row[:started_at], from, to) &&
|
|
251
|
+
within?(row[:last_progress_at], progress_from, progress_to) &&
|
|
252
|
+
within?(row[:finished_at], finished_from, finished_to) &&
|
|
248
253
|
(failing.nil? || failing.include?(row[:id])) &&
|
|
249
254
|
(partition_sagas.nil? || in_partition?(row, partition.to_s, partition_sagas)) &&
|
|
250
255
|
(needle.nil? || row.values_at(:resource_type, :resource_id).any? { |value| value.to_s.downcase.include?(needle) })
|
|
251
256
|
end
|
|
252
257
|
end
|
|
253
258
|
|
|
259
|
+
# Orders by the column in the given direction with nulls last either
|
|
260
|
+
# way, then by id in the same direction.
|
|
261
|
+
def list_comparator(column, descending)
|
|
262
|
+
lambda do |left, right|
|
|
263
|
+
a = left[column]
|
|
264
|
+
b = right[column]
|
|
265
|
+
next(a.nil? ? 1 : -1) if a.nil? != b.nil?
|
|
266
|
+
|
|
267
|
+
order = (a.nil? ? 0 : a <=> b).nonzero? || left.id <=> right.id
|
|
268
|
+
descending ? -order : order
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def within?(value, from, to)
|
|
273
|
+
return true if from.nil? && to.nil?
|
|
274
|
+
return false if value.nil?
|
|
275
|
+
|
|
276
|
+
(from.nil? || value >= from) && (to.nil? || value < to)
|
|
277
|
+
end
|
|
278
|
+
|
|
254
279
|
def failing_execution_ids(error_classes, steps, from, to)
|
|
255
280
|
return if [error_classes, steps, from, to].all?(&:nil?)
|
|
256
281
|
|
data/lib/lepus/saga/version.rb
CHANGED
data/lib/lepus/saga/web/api.rb
CHANGED
|
@@ -25,6 +25,7 @@ module Lepus
|
|
|
25
25
|
["GET", %r{\A/executions\z}, :executions],
|
|
26
26
|
["GET", %r{\A/executions/(?<id>[^/]+)\z}, :execution],
|
|
27
27
|
["GET", %r{\A/moving\z}, :moving],
|
|
28
|
+
["GET", %r{\A/recent\z}, :recent],
|
|
28
29
|
["GET", %r{\A/series\z}, :series],
|
|
29
30
|
["GET", %r{\A/insights/failures\z}, :failures],
|
|
30
31
|
["GET", %r{\A/insights/latency\z}, :latency],
|
|
@@ -40,6 +41,8 @@ module Lepus
|
|
|
40
41
|
IN_PROGRESS = ExecutionBehaviour::IN_PROGRESS_STATUSES
|
|
41
42
|
STOPPED_STATUSES = %w[failed compensation_failed].freeze
|
|
42
43
|
RELATED_LIMIT = 20
|
|
44
|
+
SORTS = {"started_at" => :started_at, "duration" => :duration}.freeze
|
|
45
|
+
DIRECTIONS = %w[desc asc].freeze
|
|
43
46
|
|
|
44
47
|
def initialize(config)
|
|
45
48
|
@config = config
|
|
@@ -104,7 +107,7 @@ module Lepus
|
|
|
104
107
|
totals: totals.filter_map { |(saga, status), count| [status, count] if saga == name }.to_h
|
|
105
108
|
)
|
|
106
109
|
end
|
|
107
|
-
{from: Serializer.time(from), to: Serializer.time(to), stalled_after: config.stalled_after, sagas: sagas}
|
|
110
|
+
{now: Serializer.time(Time.now.utc), from: Serializer.time(from), to: Serializer.time(to), stalled_after: config.stalled_after, sagas: sagas}
|
|
108
111
|
end
|
|
109
112
|
|
|
110
113
|
def stations(name, counts, stalled, failures)
|
|
@@ -121,12 +124,18 @@ module Lepus
|
|
|
121
124
|
stations
|
|
122
125
|
end
|
|
123
126
|
|
|
127
|
+
# A page of executions in the order of `sort` (started_at or
|
|
128
|
+
# duration) and `dir`; `next` is the `after` of the following page.
|
|
124
129
|
def executions(request)
|
|
125
130
|
limit = integer(request, "limit", default: 50, range: 1..200)
|
|
126
|
-
|
|
131
|
+
sort = choice(request, "sort", SORTS.keys, default: "started_at")
|
|
132
|
+
direction = choice(request, "dir", DIRECTIONS, default: "desc")
|
|
133
|
+
rows = store.list_executions(**filters(request), sort: SORTS[sort], direction: direction.to_sym,
|
|
134
|
+
after: param(request, "after"), limit: limit + 1)
|
|
127
135
|
{
|
|
136
|
+
sort: sort, dir: direction,
|
|
128
137
|
executions: rows.first(limit).map { |execution| Serializer.execution(execution) },
|
|
129
|
-
|
|
138
|
+
next: (rows[limit - 1].id if rows.size > limit)
|
|
130
139
|
}
|
|
131
140
|
end
|
|
132
141
|
|
|
@@ -143,7 +152,40 @@ module Lepus
|
|
|
143
152
|
rows = store.list_executions(saga_name: saga, current_step: step, status: IN_PROGRESS, progress_from: since, limit: per_station)
|
|
144
153
|
{saga_name: saga, step_name: step, count: count, executions: rows.map { |execution| Serializer.execution(execution) }}
|
|
145
154
|
end
|
|
146
|
-
{stalled_after: config.stalled_after, stations: stations, omitted: counts.size - busiest.size}
|
|
155
|
+
{now: Serializer.time(Time.now.utc), stalled_after: config.stalled_after, stations: stations, omitted: counts.size - busiest.size}
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Executions that finished since `since` (the `now` of an earlier
|
|
159
|
+
# response), per saga line: the latest `per_saga` of them with the
|
|
160
|
+
# steps they passed, and counts by status of every one, so a live
|
|
161
|
+
# view can finish or replay them without losing any. `replay_from`
|
|
162
|
+
# (before `since`) also lists, without counting, the ones that
|
|
163
|
+
# finished from then on, for writes that landed after the last poll.
|
|
164
|
+
def recent(request)
|
|
165
|
+
since = time(request, "since") || raise(ArgumentError, "since must be an ISO 8601 time")
|
|
166
|
+
now = Time.now.utc
|
|
167
|
+
raise ArgumentError, "since must be before now" unless since < now
|
|
168
|
+
|
|
169
|
+
replay_from = time(request, "replay_from")
|
|
170
|
+
replay_from = nil if replay_from && replay_from >= since
|
|
171
|
+
per_saga = integer(request, "per_saga", default: 8, range: 1..50)
|
|
172
|
+
limit = integer(request, "limit", default: 100, range: 1..500)
|
|
173
|
+
result = store.finished_executions(
|
|
174
|
+
from: since, to: now, listed_from: replay_from, saga_name: list(request, "saga"), per_saga: per_saga, limit: limit
|
|
175
|
+
)
|
|
176
|
+
steps = store.executed_steps(result[:executions].reject { |execution| execution.status == "completed" })
|
|
177
|
+
by_saga = result[:executions].group_by(&:saga_name)
|
|
178
|
+
counts = result[:counts].group_by { |(saga, _status), _count| saga }
|
|
179
|
+
lines = (by_saga.keys | counts.keys).sort.map do |saga|
|
|
180
|
+
line_counts = counts.fetch(saga, [])
|
|
181
|
+
{
|
|
182
|
+
saga_name: saga,
|
|
183
|
+
total: line_counts.sum(&:last),
|
|
184
|
+
counts: line_counts.to_h { |(_saga, status), count| [status, count] },
|
|
185
|
+
executions: by_saga.fetch(saga, []).map { |execution| Serializer.finished(execution, steps.fetch(execution.id, [])) }
|
|
186
|
+
}
|
|
187
|
+
end
|
|
188
|
+
{now: Serializer.time(now), since: Serializer.time(since), lines: lines}
|
|
147
189
|
end
|
|
148
190
|
|
|
149
191
|
def execution(_request, id:)
|
|
@@ -288,6 +330,13 @@ module Lepus
|
|
|
288
330
|
presence(value.strip) if value.is_a?(String)
|
|
289
331
|
end
|
|
290
332
|
|
|
333
|
+
def choice(request, name, values, default:)
|
|
334
|
+
value = param(request, name) || default
|
|
335
|
+
raise ArgumentError, "#{name} must be one of #{values.join(", ")}" unless values.include?(value)
|
|
336
|
+
|
|
337
|
+
value
|
|
338
|
+
end
|
|
339
|
+
|
|
291
340
|
def flag?(request, name)
|
|
292
341
|
%w[1 true].include?(param(request, name))
|
|
293
342
|
end
|
|
@@ -6,7 +6,7 @@ module Lepus
|
|
|
6
6
|
module Serializer
|
|
7
7
|
EXECUTION_FIELDS = %i[
|
|
8
8
|
id saga_name resource_type resource_id status current_step direction
|
|
9
|
-
epoch correlation_id failure_reason resume_count
|
|
9
|
+
epoch correlation_id failure_reason resume_count duration_ms
|
|
10
10
|
].freeze
|
|
11
11
|
EXECUTION_TIMES = %i[started_at finished_at last_progress_at created_at updated_at].freeze
|
|
12
12
|
ATTEMPT_FIELDS = %i[
|
|
@@ -24,6 +24,19 @@ module Lepus
|
|
|
24
24
|
data
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# A finished execution and the steps it passed: `reached` is the
|
|
28
|
+
# furthest step it ran forward and, on the compensation path,
|
|
29
|
+
# `back_to` the last step it compensated (or failed compensating).
|
|
30
|
+
def finished(record, executed)
|
|
31
|
+
steps = Array(Lepus::Saga.definitions[record.saga_name]&.steps).map(&:name)
|
|
32
|
+
reached = executed.max_by { |name| steps.index(name) || -1 } || record.current_step
|
|
33
|
+
{
|
|
34
|
+
id: record.id, saga_name: record.saga_name, status: record.status,
|
|
35
|
+
started_at: time(record.started_at), finished_at: time(record.finished_at), duration_ms: record.duration_ms,
|
|
36
|
+
reached: reached, back_to: (record.current_step if record.compensate_direction?)
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
27
40
|
def attempt(record)
|
|
28
41
|
data = ATTEMPT_FIELDS.to_h { |name| [name, record.public_send(name)] }
|
|
29
42
|
data[:duration_ms] = data[:duration_ms]&.to_f&.round(1)
|
data/lib/lepus/saga.rb
CHANGED
|
@@ -95,7 +95,7 @@ module Lepus
|
|
|
95
95
|
epoch: execution.epoch + 1,
|
|
96
96
|
status: execution.compensate_direction? ? "compensating" : "running",
|
|
97
97
|
failure_reason: nil,
|
|
98
|
-
|
|
98
|
+
**execution.reopen_attributes)
|
|
99
99
|
end
|
|
100
100
|
Instrumentation.saga_event("saga.resumed", execution)
|
|
101
101
|
StepProducer.publish_step(execution)
|
data/web/assets/app.css
CHANGED
|
@@ -300,6 +300,21 @@ button, input, select { font: inherit; color: inherit; }
|
|
|
300
300
|
.sym-idle { fill: var(--paper); stroke: var(--token); stroke-width: 3; }
|
|
301
301
|
.sym-stop { fill: var(--stop); }
|
|
302
302
|
.sym-back { fill: var(--back); }
|
|
303
|
+
.sym-replay { fill: var(--token); fill-opacity: 0.5; stroke: var(--line); stroke-width: 2; stroke-opacity: 0.45; }
|
|
304
|
+
|
|
305
|
+
.m-key {
|
|
306
|
+
display: flex;
|
|
307
|
+
flex-wrap: wrap;
|
|
308
|
+
gap: 4px 18px;
|
|
309
|
+
list-style: none;
|
|
310
|
+
margin: -6px 0 18px;
|
|
311
|
+
padding: 0;
|
|
312
|
+
color: var(--ink-3);
|
|
313
|
+
font-size: 13px;
|
|
314
|
+
font-weight: 550;
|
|
315
|
+
}
|
|
316
|
+
.m-key li { display: inline-flex; align-items: center; gap: 6px; }
|
|
317
|
+
.m-key .sym svg { display: block; width: 14px; height: 14px; overflow: visible; }
|
|
303
318
|
|
|
304
319
|
/* The map */
|
|
305
320
|
|
|
@@ -366,6 +381,14 @@ button, input, select { font: inherit; color: inherit; }
|
|
|
366
381
|
.t-more.is-back { stroke: var(--back); }
|
|
367
382
|
.t-more-n { fill: var(--token-text); font-size: 11.5px; font-weight: 750; }
|
|
368
383
|
.t-more-n.is-back { fill: var(--back-text); }
|
|
384
|
+
.t-token.is-replay { fill-opacity: 0.5; stroke-opacity: 0.6; }
|
|
385
|
+
.t-flash { fill: var(--stop); }
|
|
386
|
+
.t-note { fill: var(--raise); stroke: var(--rule); stroke-width: 1.5; }
|
|
387
|
+
.t-note-n { font-size: 11.5px; font-weight: 750; }
|
|
388
|
+
.t-note-pale, .t-note-sep { fill: var(--ink-2); }
|
|
389
|
+
.t-note-done { fill: var(--ink-2); }
|
|
390
|
+
.t-note-back { fill: var(--back-text); }
|
|
391
|
+
.t-note-stop { fill: var(--stop-text); }
|
|
369
392
|
.g-trail-move { stop-color: var(--token); }
|
|
370
393
|
.g-trail-back { stop-color: var(--back); }
|
|
371
394
|
|
|
@@ -801,7 +824,25 @@ button, input, select { font: inherit; color: inherit; }
|
|
|
801
824
|
border-bottom: 1px solid var(--rule);
|
|
802
825
|
}
|
|
803
826
|
.rows td { padding: 12px 12px 12px 0; border-bottom: 1px solid var(--rule-soft); vertical-align: middle; }
|
|
804
|
-
.rows tr { cursor: pointer; }
|
|
827
|
+
.rows tbody tr { cursor: pointer; }
|
|
828
|
+
.rows th .sort {
|
|
829
|
+
display: inline-flex;
|
|
830
|
+
align-items: center;
|
|
831
|
+
gap: 5px;
|
|
832
|
+
margin: -4px -6px;
|
|
833
|
+
padding: 4px 6px;
|
|
834
|
+
border: 0;
|
|
835
|
+
border-radius: 6px;
|
|
836
|
+
background: none;
|
|
837
|
+
color: inherit;
|
|
838
|
+
font: inherit;
|
|
839
|
+
cursor: pointer;
|
|
840
|
+
}
|
|
841
|
+
.rows th .sort:hover { color: var(--ink); background: var(--rule-soft); }
|
|
842
|
+
.rows th .sort svg { width: 10px; height: 10px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; opacity: 0.45; }
|
|
843
|
+
.rows th[aria-sort="ascending"] .sort, .rows th[aria-sort="descending"] .sort { color: var(--ink); }
|
|
844
|
+
.rows th[aria-sort="ascending"] .sort svg, .rows th[aria-sort="descending"] .sort svg { opacity: 1; }
|
|
845
|
+
.sort-pick { display: none; }
|
|
805
846
|
.rows tbody tr:hover td { background: var(--rule-soft); }
|
|
806
847
|
.rows td:first-child, .rows th:first-child { padding-left: 4px; }
|
|
807
848
|
.rows .r-res a { font-weight: 650; text-decoration: none; overflow-wrap: anywhere; }
|
|
@@ -875,6 +916,7 @@ button, input, select { font: inherit; color: inherit; }
|
|
|
875
916
|
.h-took { grid-column: 3; padding-left: 6px; padding-top: 0; }
|
|
876
917
|
.h-error, .h-trace { grid-column: 3; }
|
|
877
918
|
.rows thead { display: none; }
|
|
919
|
+
.sort-pick { display: inline-block; }
|
|
878
920
|
.rows, .rows tbody, .rows tr, .rows td { display: block; }
|
|
879
921
|
.rows tr {
|
|
880
922
|
display: grid;
|
|
@@ -890,6 +932,8 @@ button, input, select { font: inherit; color: inherit; }
|
|
|
890
932
|
.rows .r-step { grid-column: 1; font-size: 14px; }
|
|
891
933
|
.rows .r-when { grid-column: 2; text-align: right; }
|
|
892
934
|
.rows .r-took { display: none; }
|
|
935
|
+
.rows.is-by-took .r-when { display: none; }
|
|
936
|
+
.rows.is-by-took .r-took { display: block; grid-column: 2; text-align: right; }
|
|
893
937
|
}
|
|
894
938
|
|
|
895
939
|
@media (min-width: 1700px) {
|
data/web/assets/js/api.js
CHANGED
|
@@ -41,6 +41,7 @@ export const api = {
|
|
|
41
41
|
executions: (params) => request(url("executions", params)),
|
|
42
42
|
execution: (id) => request(url(`executions/${encodeURIComponent(id)}`)),
|
|
43
43
|
moving: (params) => request(url("moving", params)),
|
|
44
|
+
recent: (params) => request(url("recent", params)),
|
|
44
45
|
series: (params) => request(url("series", params)),
|
|
45
46
|
failures: (params) => request(url("insights/failures", params)),
|
|
46
47
|
latency: (params) => request(url("insights/latency", params)),
|
data/web/assets/js/dom.js
CHANGED
|
@@ -9,7 +9,7 @@ function apply(el, attrs) {
|
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
function append(el, children) {
|
|
12
|
+
export function append(el, children) {
|
|
13
13
|
for (const child of children.flat(Infinity)) {
|
|
14
14
|
if (child === null || child === undefined || child === false) continue;
|
|
15
15
|
el.append(child instanceof Node ? child : document.createTextNode(String(child)));
|