smith-agents 0.4.3 → 0.4.5
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 +87 -0
- data/README.md +8 -2
- data/docs/PERSISTENCE.md +180 -1
- data/docs/workflow_claim.md +4 -1
- data/lib/smith/agent/lifecycle.rb +30 -10
- data/lib/smith/doctor/checks/persistence_capabilities.rb +1 -0
- data/lib/smith/persistence_adapters/active_record_connection_errors.rb +23 -0
- data/lib/smith/persistence_adapters/active_record_initial_write.rb +42 -0
- data/lib/smith/persistence_adapters/active_record_store.rb +91 -51
- data/lib/smith/persistence_adapters/cache_store.rb +3 -1
- data/lib/smith/persistence_adapters/memory.rb +29 -13
- data/lib/smith/persistence_adapters/payload_version.rb +23 -0
- data/lib/smith/persistence_adapters/redis_store.rb +11 -33
- data/lib/smith/persistence_adapters/redis_versioned_write.rb +56 -0
- data/lib/smith/persistence_adapters/version_expectation.rb +19 -0
- data/lib/smith/persistence_adapters.rb +6 -1
- data/lib/smith/version.rb +1 -1
- data/lib/smith/workflow/durability.rb +1 -0
- data/lib/smith/workflow/persistence.rb +12 -2
- data/lib/smith/workflow/split_step_persistence/boundary.rb +115 -0
- data/lib/smith/workflow/split_step_persistence/checkpoint.rb +121 -0
- data/lib/smith/workflow/split_step_persistence/checkpoint_state.rb +51 -0
- data/lib/smith/workflow/split_step_persistence/execution.rb +126 -0
- data/lib/smith/workflow/split_step_persistence/inheritance.rb +20 -0
- data/lib/smith/workflow/split_step_persistence/payloads.rb +50 -0
- data/lib/smith/workflow/split_step_persistence/preparation.rb +97 -0
- data/lib/smith/workflow/split_step_persistence/preparation_claim.rb +91 -0
- data/lib/smith/workflow/split_step_persistence/preparation_recovery.rb +46 -0
- data/lib/smith/workflow/split_step_persistence/state_snapshot.rb +84 -0
- data/lib/smith/workflow/split_step_persistence/subclass_boundary.rb +52 -0
- data/lib/smith/workflow/split_step_persistence/transition_contract.rb +48 -0
- data/lib/smith/workflow/split_step_persistence/transition_contract_freezer.rb +83 -0
- data/lib/smith/workflow/split_step_persistence/transition_contract_signature.rb +107 -0
- data/lib/smith/workflow/split_step_persistence/transition_contract_structured_values.rb +51 -0
- data/lib/smith/workflow/split_step_persistence.rb +43 -0
- data/lib/smith/workflow.rb +13 -1
- data/lib/smith.rb +1 -0
- metadata +22 -1
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module SplitStepPersistence
|
|
6
|
+
module Execution
|
|
7
|
+
def execute_prepared_step!
|
|
8
|
+
execution_started = false
|
|
9
|
+
step = nil
|
|
10
|
+
claim_split_step_execution_verification!
|
|
11
|
+
begin
|
|
12
|
+
verify_split_step_preparation_available!
|
|
13
|
+
activate_split_step_execution!
|
|
14
|
+
execution_started = true
|
|
15
|
+
step = execute_claimed_split_step_transition!
|
|
16
|
+
ensure
|
|
17
|
+
execution_started ? finish_split_step_execution!(step) : restore_unverified_execution!
|
|
18
|
+
end
|
|
19
|
+
step
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def prepared_persisted_step? = @split_step_mutex.synchronize { @split_step_phase == :prepared }
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def claim_split_step_execution_verification!
|
|
27
|
+
@split_step_mutex.synchronize do
|
|
28
|
+
raise WorkflowError, "no persisted step is prepared" unless @split_step_phase == :prepared
|
|
29
|
+
raise WorkflowError, "the prepared transition no longer matches the workflow" unless
|
|
30
|
+
prepared_split_step_transition_matches?
|
|
31
|
+
|
|
32
|
+
@split_step_phase = :verifying_execution
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def prepared_split_step_transition_matches?
|
|
37
|
+
transition = pending_split_step_transition
|
|
38
|
+
transition_name = @next_transition_name || transition&.name
|
|
39
|
+
transition_name == @split_step_transition_name &&
|
|
40
|
+
transition.equal?(@split_step_transition) &&
|
|
41
|
+
split_step_transition_signature(transition) == @split_step_transition_signature
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def activate_split_step_execution!
|
|
45
|
+
@split_step_mutex.synchronize do
|
|
46
|
+
unless @split_step_phase == :verifying_execution
|
|
47
|
+
raise WorkflowError, "the prepared execution claim is no longer active"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
@split_step_phase = :executing
|
|
51
|
+
@split_step_execution_thread = Thread.current
|
|
52
|
+
@split_step_advance_permit = true
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def restore_unverified_execution!
|
|
57
|
+
@split_step_mutex.synchronize do
|
|
58
|
+
@split_step_phase = :prepared if @split_step_phase == :verifying_execution
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def verify_split_step_preparation_available!
|
|
63
|
+
payload = @split_step_adapter.fetch(@split_step_persistence_key)
|
|
64
|
+
durable = persisted_split_step_payload?(payload, @split_step_preparation_payload)
|
|
65
|
+
live = persisted_split_step_payload?(current_split_step_preparation_payload, @split_step_preparation_payload)
|
|
66
|
+
stable = split_step_transition_signature(@split_step_transition) == @split_step_transition_signature
|
|
67
|
+
return if durable && live && stable
|
|
68
|
+
|
|
69
|
+
raise WorkflowError, "the persisted split-step preparation is no longer available"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def current_split_step_preparation_payload
|
|
73
|
+
split_step_preparation_payload(
|
|
74
|
+
JSON.generate(to_state.merge(persistence_version: @persistence_version))
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def execute_claimed_split_step_transition!
|
|
79
|
+
step = Workflow.instance_method(:advance!).bind_call(self)
|
|
80
|
+
return step if accepted_split_step_result?(step)
|
|
81
|
+
|
|
82
|
+
raise WorkflowError, "prepared execution did not return the claimed transition"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def finish_split_step_execution!(step)
|
|
86
|
+
@split_step_mutex.synchronize do
|
|
87
|
+
@split_step_execution_thread = nil
|
|
88
|
+
@split_step_advance_permit = false
|
|
89
|
+
@split_step_phase = step ? :executed : :attempted
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def resolve_split_step_advance_transition
|
|
94
|
+
@split_step_mutex.synchronize do
|
|
95
|
+
unless @split_step_phase == :executing && @split_step_execution_thread.equal?(Thread.current)
|
|
96
|
+
return NO_SPLIT_TRANSITION
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
name = @next_transition_name
|
|
100
|
+
@next_transition_name = nil if name
|
|
101
|
+
raise UnresolvedTransitionError.new(name, self.class, @state) if name && @split_step_transition.nil?
|
|
102
|
+
|
|
103
|
+
@split_step_transition
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def accepted_split_step_result?(step)
|
|
108
|
+
return false unless step.is_a?(Hash)
|
|
109
|
+
return false unless step[:transition] == @split_step_transition_name
|
|
110
|
+
return false unless step[:from] == @split_step_origin_state
|
|
111
|
+
|
|
112
|
+
step[:error] ? accepted_split_step_failure? : step.key?(:to) && @state == step[:to]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def accepted_split_step_failure?
|
|
116
|
+
failure_name = @split_step_transition&.failure_transition || :fail
|
|
117
|
+
failure_transition = self.class.find_transition(failure_name)
|
|
118
|
+
return false unless failure_transition
|
|
119
|
+
|
|
120
|
+
@state == failure_transition.to ||
|
|
121
|
+
(@state == @split_step_origin_state && @next_transition_name == failure_name)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module SplitStepPersistence
|
|
6
|
+
module Inheritance
|
|
7
|
+
def inherited(subclass)
|
|
8
|
+
super
|
|
9
|
+
subclass.prepend(SubclassBoundary)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def prepend(*features)
|
|
13
|
+
result = super
|
|
14
|
+
Module.instance_method(:prepend).bind_call(self, SubclassBoundary.guard)
|
|
15
|
+
result
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module SplitStepPersistence
|
|
6
|
+
module Payloads
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def persisted_split_step_payload?(payload, expected)
|
|
10
|
+
return false unless payload
|
|
11
|
+
|
|
12
|
+
JSON.parse(payload) == JSON.parse(expected)
|
|
13
|
+
rescue JSON::ParserError, TypeError
|
|
14
|
+
false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def persisted_split_step_checkpoint?(payload)
|
|
18
|
+
return false unless payload && @split_step_checkpoint_digest
|
|
19
|
+
|
|
20
|
+
@split_step_checkpoint_digest == Digest::SHA256.hexdigest(payload)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def validate_split_step_marker!(payload, expected:)
|
|
24
|
+
marker = JSON.parse(payload).fetch("step_in_progress")
|
|
25
|
+
return if marker == expected
|
|
26
|
+
|
|
27
|
+
raise WorkflowError, "split-step persistence serialized an invalid step_in_progress marker"
|
|
28
|
+
rescue JSON::ParserError, KeyError, TypeError
|
|
29
|
+
raise WorkflowError, "split-step persistence requires an explicit step_in_progress marker"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def split_step_checkpoint_payload(payload)
|
|
33
|
+
document = JSON.parse(payload)
|
|
34
|
+
document["step_in_progress"] = false
|
|
35
|
+
JSON.generate(document)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def split_step_preparation_payload(payload)
|
|
39
|
+
document = JSON.parse(payload)
|
|
40
|
+
document["split_step_token"] = @split_step_token
|
|
41
|
+
JSON.generate(document)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def split_step_transition_signature(transition)
|
|
45
|
+
transition && TransitionContract.signature(transition)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module SplitStepPersistence
|
|
6
|
+
module Preparation
|
|
7
|
+
def prepare_persisted_step!(key = nil, adapter: Smith.persistence_adapter)
|
|
8
|
+
ensure_strict_split_step_persistence!
|
|
9
|
+
intent_claimed = claim_split_step_preparation_intent!
|
|
10
|
+
return unless intent_claimed
|
|
11
|
+
|
|
12
|
+
preparation_finalized = false
|
|
13
|
+
prepare_claimed_persisted_step!(key, adapter)
|
|
14
|
+
preparation_finalized = true
|
|
15
|
+
persist_claimed_split_step_preparation!(@split_step_persistence_key, @split_step_adapter)
|
|
16
|
+
@split_step_transition_name
|
|
17
|
+
ensure
|
|
18
|
+
release_split_step_preparation_intent! if intent_claimed && !preparation_finalized
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def confirm_prepared_step!
|
|
22
|
+
confirmation_claimed = false
|
|
23
|
+
claim_split_step_confirmation!
|
|
24
|
+
confirmation_claimed = true
|
|
25
|
+
confirmed = false
|
|
26
|
+
payload = @split_step_adapter.fetch(@split_step_persistence_key)
|
|
27
|
+
raise WorkflowError, "the persisted split-step preparation is not committed" unless
|
|
28
|
+
persisted_split_step_payload?(payload, @split_step_preparation_payload)
|
|
29
|
+
|
|
30
|
+
@split_step_mutex.synchronize { @split_step_phase = :prepared }
|
|
31
|
+
confirmed = true
|
|
32
|
+
self
|
|
33
|
+
ensure
|
|
34
|
+
restore_unconfirmed_preparation! if confirmation_claimed && !confirmed
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def ensure_strict_split_step_persistence!
|
|
40
|
+
return if strict_idempotency?
|
|
41
|
+
|
|
42
|
+
raise WorkflowError, "split-step persistence requires idempotency_mode :strict"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def prepare_claimed_persisted_step!(key, adapter)
|
|
46
|
+
transition = pending_split_step_transition
|
|
47
|
+
transition_name = @next_transition_name || transition&.name
|
|
48
|
+
raise WorkflowError, "workflow has no pending transition" unless transition_name
|
|
49
|
+
|
|
50
|
+
ensure_transition_budget!
|
|
51
|
+
resolved_key = normalize_split_step_persistence_key(candidate_split_step_persistence_key(key))
|
|
52
|
+
store = persistence_adapter!(adapter)
|
|
53
|
+
persistence_ttl = validate_split_step_adapter!(store)
|
|
54
|
+
finalize_split_step_preparation!(transition, transition_name, resolved_key, store, persistence_ttl)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def pending_split_step_transition
|
|
58
|
+
transition = if @next_transition_name
|
|
59
|
+
self.class.find_transition(@next_transition_name)
|
|
60
|
+
else
|
|
61
|
+
self.class.transitions_from(@state).first
|
|
62
|
+
end
|
|
63
|
+
validate_transition_origin!(transition) if transition
|
|
64
|
+
transition
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def candidate_split_step_persistence_key(key)
|
|
68
|
+
return key unless key.nil? || blank_key?(key)
|
|
69
|
+
return @persistence_key unless blank_key?(@persistence_key)
|
|
70
|
+
|
|
71
|
+
self.class.send(:resolve_persistence_key!, key:, context: @context)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def normalize_split_step_persistence_key(key) = key.to_s.dup.freeze
|
|
75
|
+
|
|
76
|
+
def validate_split_step_adapter!(adapter)
|
|
77
|
+
unless Smith::PersistenceAdapters.supports?(adapter, :store_versioned)
|
|
78
|
+
raise WorkflowError, "split-step persistence requires an adapter with store_versioned"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
unless split_step_adapter_accepts_explicit_ttl?(adapter)
|
|
82
|
+
raise WorkflowError, "split-step persistence requires store_versioned to accept ttl:"
|
|
83
|
+
end
|
|
84
|
+
return unless effective_persistence_ttl
|
|
85
|
+
|
|
86
|
+
raise WorkflowError, "split-step persistence requires non-expiring workflow persistence"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def split_step_adapter_accepts_explicit_ttl?(adapter)
|
|
90
|
+
adapter.method(:store_versioned).parameters.any? do |kind, name|
|
|
91
|
+
kind == :keyrest || (%i[key keyreq].include?(kind) && name == :ttl)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module SplitStepPersistence
|
|
6
|
+
module PreparationClaim
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def claim_split_step_preparation_intent!
|
|
10
|
+
@split_step_mutex.synchronize do
|
|
11
|
+
ensure_no_split_step_boundary!
|
|
12
|
+
return false if terminal?
|
|
13
|
+
|
|
14
|
+
@split_step_phase = :claiming_preparation
|
|
15
|
+
@split_step_preparation_thread = Thread.current
|
|
16
|
+
true
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def release_split_step_preparation_intent!
|
|
21
|
+
@split_step_mutex.synchronize do
|
|
22
|
+
next unless @split_step_phase == :claiming_preparation
|
|
23
|
+
next unless @split_step_preparation_thread.equal?(Thread.current)
|
|
24
|
+
|
|
25
|
+
@split_step_phase = nil
|
|
26
|
+
@split_step_preparation_thread = nil
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def finalize_split_step_preparation!(transition, transition_name, key, adapter, persistence_ttl)
|
|
31
|
+
transition_signature = TransitionContract.capture(transition)
|
|
32
|
+
@split_step_mutex.synchronize do
|
|
33
|
+
unless active_split_step_preparation_claim?
|
|
34
|
+
raise WorkflowError, "the split-step preparation claim is no longer active"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
@split_step_phase = :preparing
|
|
38
|
+
@split_step_transition_name = transition_name
|
|
39
|
+
@split_step_transition = transition
|
|
40
|
+
@split_step_transition_signature = transition_signature
|
|
41
|
+
@split_step_origin_state = @state
|
|
42
|
+
@split_step_token = SecureRandom.uuid.freeze
|
|
43
|
+
detach_split_step_execution_state!
|
|
44
|
+
@split_step_persistence_key = key
|
|
45
|
+
@split_step_adapter = adapter
|
|
46
|
+
@split_step_persistence_ttl = persistence_ttl
|
|
47
|
+
@persistence_key = key
|
|
48
|
+
@split_step_persist_permit = true
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def active_split_step_preparation_claim?
|
|
53
|
+
@split_step_phase == :claiming_preparation &&
|
|
54
|
+
@split_step_preparation_thread.equal?(Thread.current)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def mark_split_step_prepared!(adapter)
|
|
58
|
+
@split_step_mutex.synchronize do
|
|
59
|
+
@split_step_phase = if Smith::PersistenceAdapters.supports?(adapter, :transaction_open?) &&
|
|
60
|
+
adapter.transaction_open?
|
|
61
|
+
:prepared_uncommitted
|
|
62
|
+
else
|
|
63
|
+
:prepared
|
|
64
|
+
end
|
|
65
|
+
@split_step_preparation_thread = nil
|
|
66
|
+
@split_step_persist_permit = false
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def claim_split_step_confirmation!
|
|
71
|
+
@split_step_mutex.synchronize do
|
|
72
|
+
unless @split_step_phase == :prepared_uncommitted
|
|
73
|
+
raise WorkflowError, "no uncommitted split-step preparation is awaiting confirmation"
|
|
74
|
+
end
|
|
75
|
+
if @split_step_adapter.transaction_open?
|
|
76
|
+
raise WorkflowError, "the split-step preparation transaction is still open"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
@split_step_phase = :confirming_preparation
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def restore_unconfirmed_preparation!
|
|
84
|
+
@split_step_mutex.synchronize do
|
|
85
|
+
@split_step_phase = :prepared_uncommitted if @split_step_phase == :confirming_preparation
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module SplitStepPersistence
|
|
6
|
+
module PreparationRecovery
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def persist_claimed_split_step_preparation!(resolved_key, store)
|
|
10
|
+
persist!(resolved_key, adapter: store)
|
|
11
|
+
mark_split_step_prepared!(store)
|
|
12
|
+
rescue PersistenceVersionConflict
|
|
13
|
+
handle_split_step_preparation_conflict!
|
|
14
|
+
raise
|
|
15
|
+
rescue StandardError
|
|
16
|
+
mark_split_step_preparation_unknown!
|
|
17
|
+
raise
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def reset_failed_split_step_preparation!
|
|
21
|
+
return unless @split_step_phase == :preparing
|
|
22
|
+
|
|
23
|
+
clear_step_in_progress!
|
|
24
|
+
clear_split_step_boundary!
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def handle_split_step_preparation_conflict!
|
|
28
|
+
payload = @split_step_adapter.fetch(@split_step_persistence_key)
|
|
29
|
+
if persisted_split_step_payload?(payload, @split_step_preparation_payload)
|
|
30
|
+
mark_split_step_preparation_unknown!
|
|
31
|
+
else
|
|
32
|
+
reset_failed_split_step_preparation!
|
|
33
|
+
end
|
|
34
|
+
rescue StandardError
|
|
35
|
+
mark_split_step_preparation_unknown!
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def mark_split_step_preparation_unknown!
|
|
39
|
+
@split_step_mutex.synchronize do
|
|
40
|
+
@split_step_phase = :preparation_unknown if @split_step_phase == :preparing
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module SplitStepPersistence
|
|
6
|
+
module StateSnapshot
|
|
7
|
+
def to_state
|
|
8
|
+
state = super
|
|
9
|
+
return state unless split_step_boundary_active?
|
|
10
|
+
|
|
11
|
+
snapshot_value(state)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def session_messages
|
|
15
|
+
return super unless split_step_boundary_active?
|
|
16
|
+
|
|
17
|
+
snapshot_session_messages
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def ledger
|
|
21
|
+
return super unless split_step_boundary_active?
|
|
22
|
+
return unless @ledger
|
|
23
|
+
|
|
24
|
+
snapshot_split_step_ledger
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def effective_persistence_ttl
|
|
30
|
+
return super unless split_step_boundary_active?
|
|
31
|
+
return super unless instance_variable_defined?(:@split_step_persistence_ttl)
|
|
32
|
+
|
|
33
|
+
@split_step_persistence_ttl
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def ttl_kwarg(ttl)
|
|
37
|
+
return super unless split_step_boundary_active?
|
|
38
|
+
return super unless instance_variable_defined?(:@split_step_persistence_ttl)
|
|
39
|
+
|
|
40
|
+
{ ttl: ttl }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def detach_split_step_execution_state!
|
|
44
|
+
detach_split_step_collections!
|
|
45
|
+
detach_split_step_results!
|
|
46
|
+
detach_split_step_metadata!
|
|
47
|
+
@ledger = snapshot_split_step_ledger if @ledger
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def detach_split_step_collections!
|
|
51
|
+
@context = snapshot_context
|
|
52
|
+
@session_messages = snapshot_session_messages
|
|
53
|
+
@tool_results = snapshot_tool_results
|
|
54
|
+
@outcome = snapshot_outcome
|
|
55
|
+
@usage_entries = snapshot_usage_entries
|
|
56
|
+
@persisted_keys = Set.new(snapshot_value((@persisted_keys || Set.new).to_a))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def detach_split_step_results!
|
|
60
|
+
@last_output = snapshot_value(@last_output)
|
|
61
|
+
@last_failed_step = snapshot_value(@last_failed_step)
|
|
62
|
+
@last_prepared_input = snapshot_value(@last_prepared_input)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def detach_split_step_metadata!
|
|
66
|
+
@created_at = snapshot_value(@created_at)
|
|
67
|
+
@updated_at = snapshot_value(@updated_at)
|
|
68
|
+
@execution_namespace = snapshot_value(@execution_namespace)
|
|
69
|
+
@seed_digest = snapshot_value(@seed_digest)
|
|
70
|
+
@router_next_transition = snapshot_value(@router_next_transition)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def snapshot_split_step_ledger
|
|
74
|
+
Budget::Ledger.new(
|
|
75
|
+
limits: snapshot_value(@ledger.limits),
|
|
76
|
+
consumed: snapshot_value(@ledger.consumed)
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def split_step_boundary_active? = !@split_step_phase.nil?
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module SplitStepPersistence
|
|
6
|
+
module SubclassBoundary
|
|
7
|
+
def self.guard
|
|
8
|
+
Module.new.tap do |guard|
|
|
9
|
+
instance_methods(false).each do |method_name|
|
|
10
|
+
guard.define_method(method_name, instance_method(method_name))
|
|
11
|
+
end
|
|
12
|
+
private_instance_methods(false).each do |method_name|
|
|
13
|
+
guard.define_method(method_name, instance_method(method_name))
|
|
14
|
+
guard.send(:private, method_name)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_state
|
|
20
|
+
state = super
|
|
21
|
+
split_step_boundary_active? ? snapshot_value(state) : state
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def advance!
|
|
25
|
+
SplitStepPersistence.instance_method(:guard_split_step_subclass_execution!).bind_call(self)
|
|
26
|
+
super
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def run!
|
|
30
|
+
SplitStepPersistence.instance_method(:guard_split_step_subclass_execution!).bind_call(self)
|
|
31
|
+
super
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def effective_persistence_ttl
|
|
37
|
+
return super unless split_step_boundary_active?
|
|
38
|
+
return super unless instance_variable_defined?(:@split_step_persistence_ttl)
|
|
39
|
+
|
|
40
|
+
@split_step_persistence_ttl
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def ttl_kwarg(ttl)
|
|
44
|
+
return super unless split_step_boundary_active?
|
|
45
|
+
return super unless instance_variable_defined?(:@split_step_persistence_ttl)
|
|
46
|
+
|
|
47
|
+
{ ttl: ttl }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry-initializer"
|
|
4
|
+
|
|
5
|
+
module Smith
|
|
6
|
+
class Workflow
|
|
7
|
+
module SplitStepPersistence
|
|
8
|
+
class TransitionContract
|
|
9
|
+
extend Dry::Initializer
|
|
10
|
+
|
|
11
|
+
MAX_DEPTH = 128
|
|
12
|
+
MAX_NODES = 10_000
|
|
13
|
+
MAX_BYTES = 4 * 1024 * 1024
|
|
14
|
+
|
|
15
|
+
param :transition
|
|
16
|
+
|
|
17
|
+
def self.capture(transition) = new(transition).capture
|
|
18
|
+
def self.signature(transition) = new(transition).signature
|
|
19
|
+
|
|
20
|
+
def capture
|
|
21
|
+
captured = signature
|
|
22
|
+
freeze_value(transition)
|
|
23
|
+
freeze_value(captured)
|
|
24
|
+
captured
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def signature
|
|
28
|
+
TransitionContractSignature.new(
|
|
29
|
+
value: transition,
|
|
30
|
+
max_depth: MAX_DEPTH,
|
|
31
|
+
max_nodes: MAX_NODES,
|
|
32
|
+
max_bytes: MAX_BYTES
|
|
33
|
+
).call
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def freeze_value(value)
|
|
39
|
+
TransitionContractFreezer.new(
|
|
40
|
+
value: value,
|
|
41
|
+
max_depth: MAX_DEPTH,
|
|
42
|
+
max_nodes: MAX_NODES
|
|
43
|
+
).call
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|