smith-agents 0.12.6 → 0.12.7
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 +27 -0
- data/README.md +6 -3
- data/docs/PERSISTENCE.md +37 -8
- data/lib/smith/version.rb +1 -1
- data/lib/smith/workflow/child_run_execution.rb +2 -1
- data/lib/smith/workflow/dsl.rb +123 -0
- data/lib/smith/workflow/durability.rb +4 -3
- data/lib/smith/workflow/graph/rename_diagnostics.rb +69 -0
- data/lib/smith/workflow/graph/validator.rb +1 -0
- data/lib/smith/workflow/graph.rb +1 -0
- data/lib/smith/workflow/persistence.rb +69 -1
- data/lib/smith/workflow/step_context.rb +2 -1
- data/lib/smith/workflow/wait_execution.rb +2 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8158004e28c8bec8f58a83727cbfe9d91a7fff89dd24cc3435aefd06f87d826f
|
|
4
|
+
data.tar.gz: 6b7088fd850554492cbe98edb93c76b2d1958024206eec9a497fd423f7d2b794
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 442ebb9375a3e912f6d98c08da04e67d3aeec1afa8aa1b0f1a45f936e4e33ef9b9dd477d847b113510545a34327451566bd264b3ca2970a26762d61291c5d5f4
|
|
7
|
+
data.tar.gz: dc3fb7433a59f12a2e947e8eeec20670f4430e5c86b654ad5e1795c4f6e75da753450905de141b409e19162c2f034f070685bf40ef83aa3a93b370abd21fcab6
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,33 @@ All notable changes to Smith are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Smith is pre-1.0 and under active development; expect occasional contract tightening between minor versions until 1.0.
|
|
6
6
|
|
|
7
|
+
## [0.12.7] - 2026-10-13
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- A workflow can rename a state or a transition and keep its persisted runs:
|
|
12
|
+
`renamed_state :old, to: :new` and `renamed_transition :old, to: :new`.
|
|
13
|
+
On restore, a run persisted under an old name is mapped to the new one
|
|
14
|
+
before anything checks it: its state, the transition it takes next, the
|
|
15
|
+
transition its wait, pending retry or pending batch is in, and its last
|
|
16
|
+
failed step's names, each kept the type it was persisted as. A run waiting
|
|
17
|
+
in a renamed step resumes there under its wait's own token; one in a
|
|
18
|
+
renamed state carries on from the new one. A renamed transition keeps its
|
|
19
|
+
keys: its steps' idempotency keys and wait tokens are made under the first
|
|
20
|
+
name in its chain (`Workflow.transition_key_name`), so a step cut short, a
|
|
21
|
+
retry or an early delivery meets the keys its earlier attempt used. Renames
|
|
22
|
+
chain (a to b, then b to c); a name renamed twice or back to a name it had,
|
|
23
|
+
and two names renamed to one transition, are refused where declared. A name
|
|
24
|
+
the class still declares is never taken for an old one, a rename to a name
|
|
25
|
+
the class does not know is still refused on restore
|
|
26
|
+
(`Smith::UnknownStateOnRestore`, `UnknownTransitionOnRestore`), and
|
|
27
|
+
`validate_graph` reports both (`:rename_shadowed`,
|
|
28
|
+
`:renamed_to_unknown_state`, `:renamed_to_unknown_transition`). Subclasses
|
|
29
|
+
inherit their parent's renames, and `stuck_for?` follows them. Renames apply
|
|
30
|
+
after `migrate_from`; no `persistence_schema_version` is needed for one.
|
|
31
|
+
`Workflow.current_state_name` and `current_transition_name` give what a
|
|
32
|
+
persisted name stands for now, renames followed.
|
|
33
|
+
|
|
7
34
|
## [0.12.6] - 2026-10-13
|
|
8
35
|
|
|
9
36
|
### Fixed
|
data/README.md
CHANGED
|
@@ -30,7 +30,7 @@ What you get:
|
|
|
30
30
|
|
|
31
31
|
```ruby
|
|
32
32
|
# Gemfile
|
|
33
|
-
gem "smith-agents", "0.12.
|
|
33
|
+
gem "smith-agents", "0.12.7", require: "smith"
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
In a Rails app:
|
|
@@ -137,8 +137,11 @@ Your app delivers events (`resume!`), schedules the timeout
|
|
|
137
137
|
(`expire_wait!` once `deadline_at` passes), and runs the next step from a
|
|
138
138
|
job. Smith stores runs through an adapter (`:active_record`, `:redis`,
|
|
139
139
|
`:rails_cache`, `:memory` or your own) and refuses a state past
|
|
140
|
-
`persistence_max_bytes`.
|
|
141
|
-
|
|
140
|
+
`persistence_max_bytes`. A later version can rename a state or a step
|
|
141
|
+
(`renamed_state :drafted, to: :written`, `renamed_transition :approve, to:
|
|
142
|
+
:review`), and runs persisted under the old names carry on under the new ones,
|
|
143
|
+
waits included, under the keys they had. Waits, child runs, durable retries, replay safety, renames and
|
|
144
|
+
storage: [`docs/PERSISTENCE.md`](docs/PERSISTENCE.md).
|
|
142
145
|
|
|
143
146
|
## Agent outputs
|
|
144
147
|
|
data/docs/PERSISTENCE.md
CHANGED
|
@@ -72,12 +72,40 @@ What Smith does for you:
|
|
|
72
72
|
`migrate_from` and after the `definition_digest` check, so a digest
|
|
73
73
|
mismatch is reported as one and a migration can map an old name: bump
|
|
74
74
|
`persistence_schema_version` and declare `migrate_from(<previous version>)`,
|
|
75
|
-
since migrations run only when the stored version is behind.
|
|
76
|
-
|
|
77
|
-
`
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
75
|
+
since migrations run only when the stored version is behind. A rename needs
|
|
76
|
+
neither: `renamed_state :old, to: :new` and `renamed_transition :old, to:
|
|
77
|
+
:new` map a run persisted under the old name to the new one after
|
|
78
|
+
`migrate_from` (a migration sees the names as they were persisted) and
|
|
79
|
+
before anything checks it (its state, the transition it takes next, the
|
|
80
|
+
transition its wait, pending retry or pending batch is in, and its last
|
|
81
|
+
failed step's names, each kept the type it was persisted as), so a run
|
|
82
|
+
waiting in a renamed step resumes there under its wait's own token. A
|
|
83
|
+
renamed transition keeps its keys: its steps' idempotency keys and wait
|
|
84
|
+
tokens are made under the first name in its chain
|
|
85
|
+
(`Workflow.transition_key_name`), so a step a deploy cut short runs again, a
|
|
86
|
+
retry makes its next attempt, and a delivery that came before its wait was
|
|
87
|
+
stored meets the block's token, all under the keys they had before the
|
|
88
|
+
rename. Renames chain (a to b, then b to c); a name is renamed at most once,
|
|
89
|
+
never back to a name it had, and at most one name is renamed to a given
|
|
90
|
+
transition, each refused with `ArgumentError` where it is declared. A name
|
|
91
|
+
the class still declares is never taken for an old one, and a rename to a
|
|
92
|
+
name the class does not know is refused as above when a run restores
|
|
93
|
+
through it; `validate_graph` reports both before then (`:rename_shadowed`,
|
|
94
|
+
a warning; `:renamed_to_unknown_state` and `:renamed_to_unknown_transition`,
|
|
95
|
+
errors). A subclass inherits its parent's renames. A state's rename stays
|
|
96
|
+
declared while runs persisted under its old name may remain; a
|
|
97
|
+
transition's, for good, since dropping it moves the transition's keys to
|
|
98
|
+
its new name.
|
|
99
|
+
`Workflow.current_state_name` and `current_transition_name` give what a
|
|
100
|
+
persisted name stands for now, renames followed. In strict mode a payload
|
|
101
|
+
caught mid-step is refused before renames apply, so
|
|
102
|
+
`Smith::StepInProgressOnRestore` names its transition as persisted. Finished
|
|
103
|
+
runs are refused too, by `restore`, `restore_or_initialize`,
|
|
104
|
+
`restorable_billing_state?` and the class-level `run_persisted!`: a finished
|
|
105
|
+
run whose terminal state was renamed restores while the rename is declared;
|
|
106
|
+
to read one whose terminal state was removed, migrate it or read the stored
|
|
107
|
+
JSON directly. `stuck_for?` reports a stored run in an unknown state
|
|
108
|
+
(renames followed) as stuck, since it can neither advance nor restore
|
|
81
109
|
|
|
82
110
|
`preserve_seed: true` is intended for workflows whose seed is durable external
|
|
83
111
|
conversation and whose later accepted outputs are internal stage handoffs. Smith
|
|
@@ -860,8 +888,9 @@ end
|
|
|
860
888
|
transition raises `Smith::SerializationError`; one whose transition the
|
|
861
889
|
class no longer declares exactly (kind, event, `into:`) or that does not
|
|
862
890
|
start from the run's state raises `Smith::UnknownTransitionOnRestore`. A
|
|
863
|
-
|
|
864
|
-
`
|
|
891
|
+
rename declared with `renamed_transition` (or `renamed_state`) maps the
|
|
892
|
+
wait's transition first; a `migrate_from` that renames such things itself
|
|
893
|
+
must rewrite `awaiting` too, and `pending_retry` for a retry wait.
|
|
865
894
|
- A wait that does not end within the store's persistence TTL is refused when
|
|
866
895
|
it is entered, before the block runs. `stuck_for?` reports a waiting run as stuck
|
|
867
896
|
only once its deadline has passed by the threshold.
|
data/lib/smith/version.rb
CHANGED
|
@@ -141,7 +141,8 @@ module Smith
|
|
|
141
141
|
check_deadline!
|
|
142
142
|
namespace = ArtifactIntegration.instance_method(:execution_namespace).bind_call(self)
|
|
143
143
|
token = IdempotencyKey.__send__(:root_scope, namespace)
|
|
144
|
-
.within(:step, @step_count + 1, transition.name
|
|
144
|
+
.within(:step, @step_count + 1, self.class.transition_key_name(transition.name))
|
|
145
|
+
.within(:spawn).key
|
|
145
146
|
launch_child_runs(transition, token)
|
|
146
147
|
nil
|
|
147
148
|
end
|
data/lib/smith/workflow/dsl.rb
CHANGED
|
@@ -33,6 +33,8 @@ module Smith
|
|
|
33
33
|
subclass.instance_variable_set(:@persistence_key_builder, @persistence_key_builder)
|
|
34
34
|
subclass.instance_variable_set(:@persistence_schema_version, @persistence_schema_version)
|
|
35
35
|
subclass.instance_variable_set(:@migrations, (@migrations || {}).dup)
|
|
36
|
+
subclass.instance_variable_set(:@renamed_states, @renamed_states)
|
|
37
|
+
subclass.instance_variable_set(:@renamed_transitions, @renamed_transitions)
|
|
36
38
|
subclass.instance_variable_set(:@seed_validation_mode, @seed_validation_mode)
|
|
37
39
|
subclass.instance_variable_set(:@idempotency_mode, @idempotency_mode)
|
|
38
40
|
subclass.instance_variable_set(:@session_mode, @session_mode)
|
|
@@ -165,6 +167,77 @@ module Smith
|
|
|
165
167
|
@migrations || {}
|
|
166
168
|
end
|
|
167
169
|
|
|
170
|
+
# A state a later version renamed: a run persisted in `old`
|
|
171
|
+
# restores in `to` and carries on from there. Renames chain (a to
|
|
172
|
+
# b, then b to c); a name is renamed at most once and never back
|
|
173
|
+
# to a name it had. A name the class still declares is never taken
|
|
174
|
+
# for an old one (validate_graph warns of such a rename). The
|
|
175
|
+
# persisted run is mapped after migrate_from, before anything
|
|
176
|
+
# checks its names, so a rename needs no persistence_schema_version.
|
|
177
|
+
# A rename stays declared while runs persisted under its old name
|
|
178
|
+
# may remain.
|
|
179
|
+
def renamed_state(old, to:)
|
|
180
|
+
@renamed_states = renamed_name_map(renamed_states, old, to, kind: "state")
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# A transition a later version renamed: a run persisted about to
|
|
184
|
+
# take `old`, or waiting in it (its wait, pending retry or batch),
|
|
185
|
+
# restores at `to`, as renamed_state. A renamed transition keeps
|
|
186
|
+
# its keys: its steps' idempotency keys and wait tokens are made
|
|
187
|
+
# under the first name in its chain (transition_key_name), so a
|
|
188
|
+
# step a deploy cut short, a retry or an early delivery meets the
|
|
189
|
+
# keys its earlier attempt used. Hence at most one name is renamed
|
|
190
|
+
# to a transition, and the rename stays declared for good: dropping
|
|
191
|
+
# it moves the transition's keys to its new name.
|
|
192
|
+
def renamed_transition(old, to:)
|
|
193
|
+
renames = renamed_name_map(renamed_transitions, old, to, kind: "transition")
|
|
194
|
+
earlier = renamed_transitions.key(renames.fetch(old.to_s))
|
|
195
|
+
if earlier
|
|
196
|
+
raise ArgumentError, "transition #{to.inspect} is already the new name of #{earlier.inspect}; a " \
|
|
197
|
+
"transition keeps the keys of one first name, so only one name is renamed to it"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
@renamed_transitions = renames
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def renamed_states
|
|
204
|
+
@renamed_states || {}.freeze
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def renamed_transitions
|
|
208
|
+
@renamed_transitions || {}.freeze
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# The state a persisted state name stands for now, renames
|
|
212
|
+
# followed, or nil when the class knows none. `O(R·(S + T))` for
|
|
213
|
+
# `R` renamed states.
|
|
214
|
+
def current_state_name(name)
|
|
215
|
+
return if name.nil?
|
|
216
|
+
|
|
217
|
+
resolve_state_name(follow_renames(name, renamed_states) { |candidate| resolve_state_name(candidate) })
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# The transition a persisted transition name stands for now,
|
|
221
|
+
# renames followed, or nil when the class knows none. `O(R·T)` for
|
|
222
|
+
# `R` renamed transitions.
|
|
223
|
+
def current_transition_name(name)
|
|
224
|
+
return if name.nil?
|
|
225
|
+
|
|
226
|
+
transition_named(follow_renames(name, renamed_transitions) { |candidate| transition_named(candidate) })&.name
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# The name a transition's steps are keyed under (their idempotency
|
|
230
|
+
# keys and wait tokens): the first name in its rename chain, so a
|
|
231
|
+
# renamed transition keeps its keys. Renames have no cycles and at
|
|
232
|
+
# most one name renamed to each, so the chain back is a line.
|
|
233
|
+
# `O(R)` for `R` renamed transitions.
|
|
234
|
+
def transition_key_name(name)
|
|
235
|
+
earlier = renamed_transitions.invert
|
|
236
|
+
current = name.to_s
|
|
237
|
+
current = earlier[current] while earlier.key?(current)
|
|
238
|
+
current
|
|
239
|
+
end
|
|
240
|
+
|
|
168
241
|
# Controls whether restore validates that the seed_messages
|
|
169
242
|
# builder still produces the same digest as when this workflow
|
|
170
243
|
# was originally persisted.
|
|
@@ -313,6 +386,56 @@ module Smith
|
|
|
313
386
|
Identifier.normalize(identifier, label: "workflow identifier")
|
|
314
387
|
end
|
|
315
388
|
|
|
389
|
+
# The renames with `old` renamed to `to`: refused when `old` is
|
|
390
|
+
# already renamed, or when the chain from `to` leads back to `old`
|
|
391
|
+
# (a rename back), so every chain is a line. `O(R)`.
|
|
392
|
+
def renamed_name_map(renames, old, to, kind:)
|
|
393
|
+
old_name = renamed_identifier(old, kind:)
|
|
394
|
+
new_name = renamed_identifier(to, kind:)
|
|
395
|
+
check_rename!(renames, old_name, new_name, kind:)
|
|
396
|
+
renames.merge(old_name => new_name).freeze
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def check_rename!(renames, old_name, new_name, kind:)
|
|
400
|
+
if old_name == new_name
|
|
401
|
+
raise ArgumentError, "a renamed #{kind} must differ from its new name, got #{old_name.inspect}"
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
if renames.key?(old_name)
|
|
405
|
+
raise ArgumentError, "#{kind} #{old_name.inspect} is already renamed to #{renames[old_name].inspect}; " \
|
|
406
|
+
"rename #{renames[old_name].inspect} instead"
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
return unless renamed_back?(renames, old_name, new_name)
|
|
410
|
+
|
|
411
|
+
raise ArgumentError, "#{kind} #{new_name.inspect} was renamed from #{old_name.inspect}; a #{kind} is " \
|
|
412
|
+
"never renamed back to a name it had"
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def renamed_back?(renames, old_name, new_name)
|
|
416
|
+
current = new_name
|
|
417
|
+
current = renames[current] while renames.key?(current) && current != old_name
|
|
418
|
+
current == old_name
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def renamed_identifier(name, kind:)
|
|
422
|
+
unless (name.is_a?(Symbol) || name.is_a?(String)) && !name.to_s.strip.empty?
|
|
423
|
+
raise ArgumentError, "a renamed #{kind} must be a non-blank Symbol or String, got #{name.inspect}"
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
name.to_s
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# The name `name` stands for now: itself while the class declares
|
|
430
|
+
# it (`declared` says so), else where its renames lead. Chains are
|
|
431
|
+
# lines (renamed_name_map), so the walk ends. `O(R)` calls of
|
|
432
|
+
# `declared`.
|
|
433
|
+
def follow_renames(name, renames, &declared)
|
|
434
|
+
current = name.to_s
|
|
435
|
+
current = renames[current] until declared.call(current) || !renames.key?(current)
|
|
436
|
+
current
|
|
437
|
+
end
|
|
438
|
+
|
|
316
439
|
def transitions_by_state
|
|
317
440
|
@transitions_by_state ||= Hash.new { |index, state| index[state] = [] }
|
|
318
441
|
end
|
|
@@ -244,10 +244,11 @@ module Smith
|
|
|
244
244
|
false
|
|
245
245
|
end
|
|
246
246
|
|
|
247
|
-
# A state the class no longer knows can never
|
|
248
|
-
# it is not terminal: an old one reports as
|
|
247
|
+
# A state the class no longer knows, renames followed, can never
|
|
248
|
+
# advance or restore, so it is not terminal: an old one reports as
|
|
249
|
+
# stuck.
|
|
249
250
|
def terminal_payload_state?(klass, state_name, next_transition)
|
|
250
|
-
state = klass.
|
|
251
|
+
state = klass.current_state_name(state_name)
|
|
251
252
|
!state.nil? && !klass.transition_from?(state) && next_transition.nil?
|
|
252
253
|
end
|
|
253
254
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
class Graph
|
|
6
|
+
# Renames (renamed_state, renamed_transition) found wanting before a
|
|
7
|
+
# run restores through one: a rename leading to no name the workflow
|
|
8
|
+
# declares is an error, as a run persisted under its old name cannot
|
|
9
|
+
# restore; one whose old name the workflow still declares is a
|
|
10
|
+
# warning, as a run persisted under that name stays there.
|
|
11
|
+
# `O(R·(S + T))` for `R` renames.
|
|
12
|
+
class RenameDiagnostics
|
|
13
|
+
attr_reader :graph
|
|
14
|
+
|
|
15
|
+
def initialize(graph)
|
|
16
|
+
@graph = graph
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_a
|
|
20
|
+
workflow = graph.workflow_class
|
|
21
|
+
|
|
22
|
+
[
|
|
23
|
+
*diagnostics_for(:state, workflow.renamed_states) do |name|
|
|
24
|
+
[workflow.resolve_state_name(name), workflow.current_state_name(name)]
|
|
25
|
+
end,
|
|
26
|
+
*diagnostics_for(:transition, workflow.renamed_transitions) do |name|
|
|
27
|
+
[workflow.transition_named(name), workflow.current_transition_name(name)]
|
|
28
|
+
end
|
|
29
|
+
]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def diagnostics_for(kind, renames)
|
|
35
|
+
renames.filter_map do |old, new|
|
|
36
|
+
declared, current = yield(old)
|
|
37
|
+
if declared
|
|
38
|
+
shadowed(kind, old, new)
|
|
39
|
+
elsif current.nil?
|
|
40
|
+
leads_nowhere(kind, old, new)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def shadowed(kind, old, new)
|
|
46
|
+
Diagnostic.new(
|
|
47
|
+
severity: :warning,
|
|
48
|
+
code: :rename_shadowed,
|
|
49
|
+
kind => old,
|
|
50
|
+
message: "Renamed #{kind} #{old.inspect} is still declared, so a run persisted in it stays there " \
|
|
51
|
+
"rather than moving to #{new.inspect}.",
|
|
52
|
+
suggestion: "Drop the rename, or move such runs with persistence_schema_version and migrate_from."
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def leads_nowhere(kind, old, new)
|
|
57
|
+
Diagnostic.new(
|
|
58
|
+
severity: :error,
|
|
59
|
+
code: :"renamed_to_unknown_#{kind}",
|
|
60
|
+
kind => old,
|
|
61
|
+
message: "Renamed #{kind} #{old.inspect} leads through #{new.inspect} to no #{kind} this workflow " \
|
|
62
|
+
"declares, so a run persisted in it cannot restore.",
|
|
63
|
+
suggestion: "Declare #{kind} #{new.inspect} or rename #{old.inspect} to a declared #{kind}."
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
data/lib/smith/workflow/graph.rb
CHANGED
|
@@ -88,6 +88,7 @@ require_relative "graph/identifier_projection"
|
|
|
88
88
|
require_relative "graph/transition_contract"
|
|
89
89
|
require_relative "graph/diagnostic"
|
|
90
90
|
require_relative "graph/state_diagnostics"
|
|
91
|
+
require_relative "graph/rename_diagnostics"
|
|
91
92
|
require_relative "graph/targets"
|
|
92
93
|
require_relative "graph/execution_successors"
|
|
93
94
|
require_relative "graph/reachability"
|
|
@@ -87,7 +87,7 @@ module Smith
|
|
|
87
87
|
def restore_state(hash, allow_step_in_progress: false)
|
|
88
88
|
validate_raw_step_in_progress!(hash) if
|
|
89
89
|
self.class.idempotency_mode == :strict && !allow_step_in_progress
|
|
90
|
-
migrated = migrate_if_needed(hash)
|
|
90
|
+
migrated = apply_renames(migrate_if_needed(hash))
|
|
91
91
|
normalized = normalize_persisted_state(migrated)
|
|
92
92
|
persistence_version = validated_persistence_version(normalized)
|
|
93
93
|
restore_persisted_keys(normalized)
|
|
@@ -447,6 +447,74 @@ module Smith
|
|
|
447
447
|
payload
|
|
448
448
|
end
|
|
449
449
|
|
|
450
|
+
# A run persisted under names a later version renamed
|
|
451
|
+
# (renamed_state, renamed_transition) carries on under the new
|
|
452
|
+
# ones: its state, the transition it takes next, the transition its
|
|
453
|
+
# wait, pending retry and pending batch are in, and its last failed
|
|
454
|
+
# step's names, each kept the type it was persisted as. A name the
|
|
455
|
+
# class still declares stays as it is.
|
|
456
|
+
RENAMED_RECORDS = {
|
|
457
|
+
awaiting: { transition: %w[transition] },
|
|
458
|
+
pending_retry: { transition: %w[transition] },
|
|
459
|
+
pending_batch: { transition: %w[transition] },
|
|
460
|
+
last_failed_step: { transition: %w[transition], state: %w[from to] }
|
|
461
|
+
}.freeze
|
|
462
|
+
private_constant :RENAMED_RECORDS
|
|
463
|
+
|
|
464
|
+
def apply_renames(payload)
|
|
465
|
+
return payload unless renames?
|
|
466
|
+
|
|
467
|
+
renamed = payload.dup
|
|
468
|
+
renamed[:state] = renamed_state_name(payload[:state])
|
|
469
|
+
renamed[:next_transition_name] = renamed_transition_name(payload[:next_transition_name]) if
|
|
470
|
+
payload.key?(:next_transition_name)
|
|
471
|
+
RENAMED_RECORDS.each_key { |key| renamed[key] = renamed_record(payload[key], RENAMED_RECORDS[key]) }
|
|
472
|
+
renamed
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def renames? = !(self.class.renamed_states.empty? && self.class.renamed_transitions.empty?)
|
|
476
|
+
|
|
477
|
+
def renamed_state_name(name)
|
|
478
|
+
renamed_value(name) do |text|
|
|
479
|
+
self.class.send(:follow_renames, text, self.class.renamed_states) do |candidate|
|
|
480
|
+
self.class.resolve_state_name(candidate)
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def renamed_transition_name(name)
|
|
486
|
+
renamed_value(name) do |text|
|
|
487
|
+
self.class.send(:follow_renames, text, self.class.renamed_transitions) do |candidate|
|
|
488
|
+
self.class.transition_named(candidate)
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
# A persisted name renamed, kept the type it was persisted as.
|
|
494
|
+
def renamed_value(name)
|
|
495
|
+
return name unless name.is_a?(String) || name.is_a?(Symbol)
|
|
496
|
+
|
|
497
|
+
renamed = yield(name.to_s)
|
|
498
|
+
name.is_a?(Symbol) ? renamed.to_sym : renamed
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
# A persisted record's name fields renamed, each by its kind
|
|
502
|
+
# (:state or :transition), under String or Symbol keys alike.
|
|
503
|
+
def renamed_record(record, fields_by_kind)
|
|
504
|
+
return record unless record.is_a?(Hash)
|
|
505
|
+
|
|
506
|
+
renamed = record.dup
|
|
507
|
+
fields_by_kind.each do |kind, fields|
|
|
508
|
+
fields.each do |field|
|
|
509
|
+
key = [field, field.to_sym].find { |candidate| renamed.key?(candidate) }
|
|
510
|
+
next if key.nil?
|
|
511
|
+
|
|
512
|
+
renamed[key] = kind == :state ? renamed_state_name(renamed[key]) : renamed_transition_name(renamed[key])
|
|
513
|
+
end
|
|
514
|
+
end
|
|
515
|
+
renamed
|
|
516
|
+
end
|
|
517
|
+
|
|
450
518
|
def normalize_persisted_state(hash)
|
|
451
519
|
normalized = snapshot_value(hash).transform_keys { |k| k.is_a?(String) ? k.to_sym : k }
|
|
452
520
|
normalized[:outcome] = symbolize_value(normalized[:outcome]) if normalized[:outcome].is_a?(Hash)
|
|
@@ -99,7 +99,8 @@ module Smith
|
|
|
99
99
|
namespace = ArtifactIntegration.instance_method(:execution_namespace).bind_call(self)
|
|
100
100
|
outer = IdempotencyKey.__send__(:current_scope)
|
|
101
101
|
base = outer && outer.run == namespace ? outer : IdempotencyKey.__send__(:root_scope, namespace)
|
|
102
|
-
|
|
102
|
+
position = base.within(:step, @step_count + 1, self.class.transition_key_name(transition.name))
|
|
103
|
+
IdempotencyKey.__send__(:install, position)
|
|
103
104
|
end
|
|
104
105
|
|
|
105
106
|
def teardown_step_context
|
|
@@ -140,7 +140,8 @@ module Smith
|
|
|
140
140
|
|
|
141
141
|
def wait_token_for(transition, scope)
|
|
142
142
|
IdempotencyKey.__send__(:root_scope, ArtifactIntegration.instance_method(:execution_namespace).bind_call(self))
|
|
143
|
-
.within(:step, @step_count + 1, transition.name
|
|
143
|
+
.within(:step, @step_count + 1, self.class.transition_key_name(transition.name))
|
|
144
|
+
.within(:wait).key(scope)
|
|
144
145
|
end
|
|
145
146
|
|
|
146
147
|
def check_wait_admissible!(transition, seconds)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: smith-agents
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.12.
|
|
4
|
+
version: 0.12.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Ralak
|
|
@@ -485,6 +485,7 @@ files:
|
|
|
485
485
|
- lib/smith/workflow/graph/reachability.rb
|
|
486
486
|
- lib/smith/workflow/graph/reachability_diagnostics.rb
|
|
487
487
|
- lib/smith/workflow/graph/reference.rb
|
|
488
|
+
- lib/smith/workflow/graph/rename_diagnostics.rb
|
|
488
489
|
- lib/smith/workflow/graph/report.rb
|
|
489
490
|
- lib/smith/workflow/graph/retry_policy_diagnostic.rb
|
|
490
491
|
- lib/smith/workflow/graph/run_launcher_diagnostic.rb
|