smith-agents 0.4.4 → 0.5.0
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 +262 -0
- data/README.md +116 -6
- data/docs/CONFIGURATION.md +4 -1
- data/docs/PATTERNS.md +7 -0
- data/docs/PERSISTENCE.md +502 -1
- data/docs/workflow_claim.md +4 -1
- data/lib/smith/agent/lifecycle.rb +19 -10
- data/lib/smith/agent/registry/execution_binding_capture.rb +26 -0
- data/lib/smith/agent/registry/mutation_boundary.rb +29 -0
- data/lib/smith/agent/registry.rb +8 -0
- data/lib/smith/agent.rb +24 -2
- data/lib/smith/budget/amount_contract.rb +67 -0
- data/lib/smith/budget/amount_representation.rb +43 -0
- data/lib/smith/budget/decimal_context.rb +18 -0
- data/lib/smith/budget/ledger.rb +110 -18
- data/lib/smith/budget/ledger_publication.rb +60 -0
- data/lib/smith/budget/ledger_state.rb +21 -0
- data/lib/smith/budget/ledger_state_transition.rb +72 -0
- data/lib/smith/budget/public_state_validator.rb +25 -0
- data/lib/smith/budget/reservation.rb +20 -0
- data/lib/smith/budget/reservation_contract.rb +34 -0
- data/lib/smith/context/observation_masking.rb +3 -1
- data/lib/smith/context/state_injection.rb +1 -1
- data/lib/smith/doctor/checks/persistence_capabilities.rb +18 -3
- data/lib/smith/error.rb +5 -0
- data/lib/smith/errors.rb +6 -0
- data/lib/smith/exponential_backoff.rb +128 -0
- data/lib/smith/persistence_adapters/active_record_connection_errors.rb +23 -0
- data/lib/smith/persistence_adapters/active_record_exact_predicate.rb +43 -0
- data/lib/smith/persistence_adapters/active_record_exact_store.rb +62 -0
- data/lib/smith/persistence_adapters/active_record_exact_write.rb +40 -0
- data/lib/smith/persistence_adapters/active_record_initial_write.rb +42 -0
- data/lib/smith/persistence_adapters/active_record_store.rb +95 -52
- data/lib/smith/persistence_adapters/cache_store.rb +4 -1
- data/lib/smith/persistence_adapters/memory.rb +45 -14
- data/lib/smith/persistence_adapters/payload_version.rb +23 -0
- data/lib/smith/persistence_adapters/redis_client_access.rb +38 -0
- data/lib/smith/persistence_adapters/redis_exact_write.rb +44 -0
- data/lib/smith/persistence_adapters/redis_store.rb +38 -43
- data/lib/smith/persistence_adapters/redis_versioned_write.rb +56 -0
- data/lib/smith/persistence_adapters/retry.rb +22 -10
- data/lib/smith/persistence_adapters/version_expectation.rb +19 -0
- data/lib/smith/persistence_adapters.rb +26 -18
- data/lib/smith/persistence_payload_conflict.rb +12 -0
- data/lib/smith/tool/budget_enforcement.rb +2 -2
- data/lib/smith/types.rb +1 -0
- data/lib/smith/version.rb +2 -1
- data/lib/smith/workflow/branch_env.rb +1 -1
- data/lib/smith/workflow/budget_integration.rb +21 -11
- data/lib/smith/workflow/definition_identity/class_methods.rb +48 -0
- data/lib/smith/workflow/definition_identity.rb +25 -0
- data/lib/smith/workflow/deterministic_step.rb +2 -2
- data/lib/smith/workflow/dsl.rb +73 -5
- data/lib/smith/workflow/durability.rb +2 -1
- data/lib/smith/workflow/evaluator_optimizer.rb +7 -11
- data/lib/smith/workflow/execution.rb +16 -18
- data/lib/smith/workflow/execution_binding_resolution.rb +16 -0
- data/lib/smith/workflow/execution_result_snapshot.rb +118 -0
- data/lib/smith/workflow/fanout_execution.rb +1 -1
- data/lib/smith/workflow/graph/diagnostic.rb +27 -1
- data/lib/smith/workflow/graph/diagnostic_path.rb +24 -0
- data/lib/smith/workflow/graph/execution_successors.rb +65 -0
- data/lib/smith/workflow/graph/identifier_projection.rb +13 -0
- data/lib/smith/workflow/graph/metrics.rb +1 -3
- data/lib/smith/workflow/graph/nested_readiness_diagnostics.rb +15 -16
- data/lib/smith/workflow/graph/optimization_contract.rb +2 -2
- data/lib/smith/workflow/graph/orchestration_contract.rb +3 -3
- data/lib/smith/workflow/graph/reachability.rb +19 -31
- data/lib/smith/workflow/graph/reachability_diagnostics.rb +17 -1
- data/lib/smith/workflow/graph/retry_policy_diagnostic.rb +36 -0
- data/lib/smith/workflow/graph/runtime_binding_diagnostics.rb +2 -2
- data/lib/smith/workflow/graph/runtime_readiness.rb +3 -44
- data/lib/smith/workflow/graph/runtime_readiness_metrics.rb +2 -2
- data/lib/smith/workflow/graph/runtime_readiness_report_builder.rb +47 -0
- data/lib/smith/workflow/graph/runtime_readiness_traversal.rb +108 -0
- data/lib/smith/workflow/graph/state_diagnostics.rb +2 -2
- data/lib/smith/workflow/graph/transition_contract.rb +47 -0
- data/lib/smith/workflow/graph/transition_contract_attributes.rb +73 -0
- data/lib/smith/workflow/graph/transition_contract_configurations.rb +100 -0
- data/lib/smith/workflow/graph/transition_diagnostics.rb +54 -12
- data/lib/smith/workflow/graph/transition_optimization_configuration.rb +62 -0
- data/lib/smith/workflow/graph/transition_snapshot.rb +4 -1
- data/lib/smith/workflow/graph/validator.rb +1 -1
- data/lib/smith/workflow/graph.rb +62 -4
- data/lib/smith/workflow/guardrail_integration.rb +23 -9
- data/lib/smith/workflow/identifier.rb +24 -0
- data/lib/smith/workflow/message_admission.rb +40 -0
- data/lib/smith/workflow/message_admission_boundary.rb +22 -0
- data/lib/smith/workflow/message_batch.rb +60 -0
- data/lib/smith/workflow/message_value_normalizer.rb +133 -0
- data/lib/smith/workflow/nested_execution.rb +10 -0
- data/lib/smith/workflow/orchestrator_worker.rb +3 -7
- data/lib/smith/workflow/parallel/cancellation_signal.rb +14 -5
- data/lib/smith/workflow/parallel/execution_context.rb +113 -0
- data/lib/smith/workflow/parallel/nested_execution.rb +124 -0
- data/lib/smith/workflow/parallel/root_execution.rb +91 -0
- data/lib/smith/workflow/parallel.rb +19 -22
- data/lib/smith/workflow/parallel_agent_binding.rb +24 -0
- data/lib/smith/workflow/parallel_execution.rb +7 -3
- data/lib/smith/workflow/persistence.rb +49 -10
- data/lib/smith/workflow/prepared_step.rb +122 -0
- data/lib/smith/workflow/prepared_step_dispatch.rb +88 -0
- data/lib/smith/workflow/prepared_step_execution_authorization.rb +116 -0
- data/lib/smith/workflow/prepared_step_execution_result.rb +49 -0
- data/lib/smith/workflow/prepared_step_execution_scope.rb +45 -0
- data/lib/smith/workflow/prepared_step_recovery.rb +38 -0
- data/lib/smith/workflow/retry_execution.rb +20 -11
- data/lib/smith/workflow/split_step_persistence/boundary.rb +128 -0
- data/lib/smith/workflow/split_step_persistence/boundary_reset.rb +50 -0
- data/lib/smith/workflow/split_step_persistence/canonical_payload_digest.rb +55 -0
- data/lib/smith/workflow/split_step_persistence/checkpoint.rb +119 -0
- data/lib/smith/workflow/split_step_persistence/checkpoint_state.rb +51 -0
- data/lib/smith/workflow/split_step_persistence/definition_boundary.rb +19 -0
- data/lib/smith/workflow/split_step_persistence/dispatch_boundary.rb +16 -0
- data/lib/smith/workflow/split_step_persistence/dispatch_claim.rb +118 -0
- data/lib/smith/workflow/split_step_persistence/dispatch_confirmation.rb +70 -0
- data/lib/smith/workflow/split_step_persistence/dispatch_verification.rb +35 -0
- data/lib/smith/workflow/split_step_persistence/execution.rb +128 -0
- data/lib/smith/workflow/split_step_persistence/execution_authorization.rb +89 -0
- data/lib/smith/workflow/split_step_persistence/execution_binding_collector.rb +90 -0
- data/lib/smith/workflow/split_step_persistence/execution_binding_snapshot.rb +95 -0
- data/lib/smith/workflow/split_step_persistence/execution_result_capture.rb +48 -0
- data/lib/smith/workflow/split_step_persistence/execution_verification.rb +65 -0
- data/lib/smith/workflow/split_step_persistence/execution_workflow_snapshot.rb +77 -0
- data/lib/smith/workflow/split_step_persistence/inheritance.rb +22 -0
- data/lib/smith/workflow/split_step_persistence/payloads.rb +54 -0
- data/lib/smith/workflow/split_step_persistence/preparation.rb +125 -0
- data/lib/smith/workflow/split_step_persistence/preparation_claim.rb +117 -0
- data/lib/smith/workflow/split_step_persistence/preparation_payload.rb +25 -0
- data/lib/smith/workflow/split_step_persistence/preparation_recovery.rb +54 -0
- data/lib/smith/workflow/split_step_persistence/recovery.rb +125 -0
- data/lib/smith/workflow/split_step_persistence/recovery_boundary.rb +56 -0
- data/lib/smith/workflow/split_step_persistence/recovery_class_methods.rb +14 -0
- data/lib/smith/workflow/split_step_persistence/replace_exact_signature.rb +46 -0
- data/lib/smith/workflow/split_step_persistence/restart_safe_adapter.rb +42 -0
- data/lib/smith/workflow/split_step_persistence/state_snapshot.rb +84 -0
- data/lib/smith/workflow/split_step_persistence/subclass_boundary.rb +102 -0
- data/lib/smith/workflow/split_step_persistence/transaction_identity.rb +45 -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 +79 -0
- data/lib/smith/workflow/step_completion.rb +31 -0
- data/lib/smith/workflow/string_snapshot.rb +25 -0
- data/lib/smith/workflow/transition.rb +122 -133
- data/lib/smith/workflow/transition_actionability.rb +22 -0
- data/lib/smith/workflow.rb +57 -19
- data/lib/smith.rb +42 -2
- data/sig/smith.rbs +1 -0
- metadata +131 -19
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module SplitStepPersistence
|
|
6
|
+
module Boundary
|
|
7
|
+
def run_persisted!(...)
|
|
8
|
+
ensure_no_split_step_boundary!
|
|
9
|
+
super
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def advance_persisted!(...)
|
|
13
|
+
ensure_no_split_step_boundary!
|
|
14
|
+
super
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def clear_persisted!(...)
|
|
18
|
+
ensure_no_split_step_boundary!
|
|
19
|
+
super
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize_copy(source)
|
|
23
|
+
super
|
|
24
|
+
copied_split_step_phase = @split_step_phase
|
|
25
|
+
detach_copied_runtime_state!(source)
|
|
26
|
+
return unless copied_split_step_phase
|
|
27
|
+
|
|
28
|
+
@split_step_phase = :copied_boundary
|
|
29
|
+
reset_copied_execution_state!
|
|
30
|
+
reset_copied_dispatch_state!
|
|
31
|
+
end
|
|
32
|
+
private :initialize_copy
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def reset_copied_execution_state!
|
|
37
|
+
@split_step_token = nil
|
|
38
|
+
@split_step_prepared_descriptor = nil
|
|
39
|
+
@split_step_transaction_identity = nil
|
|
40
|
+
@split_step_execution_thread = nil
|
|
41
|
+
@split_step_active_execution_authorization = nil
|
|
42
|
+
@split_step_execution_result = nil
|
|
43
|
+
@split_step_advance_permit = false
|
|
44
|
+
clear_split_step_execution_verification!
|
|
45
|
+
clear_split_step_execution_authorization!
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def reset_copied_dispatch_state!
|
|
49
|
+
@split_step_dispatch_token = nil
|
|
50
|
+
@split_step_dispatch_descriptor = nil
|
|
51
|
+
@split_step_attempted_dispatch_descriptor = nil
|
|
52
|
+
@split_step_dispatch_thread = nil
|
|
53
|
+
@split_step_pre_dispatch_payload = nil
|
|
54
|
+
@split_step_attempted_dispatch_payload = nil
|
|
55
|
+
@split_step_preparation_thread = nil
|
|
56
|
+
@split_step_persist_permit = false
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def detach_copied_runtime_state!(source)
|
|
60
|
+
source_messages = source.instance_variable_get(:@session_messages)
|
|
61
|
+
@split_step_mutex = Mutex.new
|
|
62
|
+
@persisted_keys_mutex = Mutex.new
|
|
63
|
+
@tool_results_mutex = Mutex.new
|
|
64
|
+
@usage_mutex = Mutex.new
|
|
65
|
+
detach_split_step_execution_state!
|
|
66
|
+
@session_messages = snapshot_value(source_messages || [])
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def ensure_split_step_execution_allowed!
|
|
70
|
+
@split_step_mutex.synchronize do
|
|
71
|
+
return unless @split_step_phase
|
|
72
|
+
|
|
73
|
+
if split_step_advance_permitted?
|
|
74
|
+
@split_step_advance_permit = false
|
|
75
|
+
return
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
raise WorkflowError, "use execute_prepared_step! for the active split-step boundary"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def claim_split_step_advance!
|
|
83
|
+
@split_step_mutex.synchronize do
|
|
84
|
+
unless @split_step_phase
|
|
85
|
+
@split_step_phase = :ordinary_execution
|
|
86
|
+
return :ordinary
|
|
87
|
+
end
|
|
88
|
+
if split_step_advance_permitted?
|
|
89
|
+
@split_step_advance_permit = false
|
|
90
|
+
return :split_step
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
raise WorkflowError, "use execute_prepared_step! for the active split-step boundary"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def release_split_step_advance!(claim)
|
|
98
|
+
return unless claim == :ordinary
|
|
99
|
+
|
|
100
|
+
@split_step_mutex.synchronize do
|
|
101
|
+
@split_step_phase = nil if @split_step_phase == :ordinary_execution
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def ensure_no_split_step_boundary!
|
|
106
|
+
return unless @split_step_phase
|
|
107
|
+
|
|
108
|
+
raise WorkflowError, "a split-step persistence boundary is already active"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def guard_split_step_subclass_execution!
|
|
112
|
+
@split_step_mutex.synchronize do
|
|
113
|
+
return unless @split_step_phase
|
|
114
|
+
return if split_step_advance_permitted?
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
raise WorkflowError, "use execute_prepared_step! for the active split-step boundary"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def split_step_advance_permitted?
|
|
121
|
+
@split_step_phase == :executing &&
|
|
122
|
+
@split_step_execution_thread.equal?(Thread.current) &&
|
|
123
|
+
@split_step_advance_permit
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module SplitStepPersistence
|
|
6
|
+
module BoundaryReset
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def clear_split_step_boundary!
|
|
10
|
+
@split_step_phase = nil
|
|
11
|
+
@split_step_transition_name = nil
|
|
12
|
+
@split_step_transition = nil
|
|
13
|
+
@split_step_transition_signature = nil
|
|
14
|
+
@split_step_origin_state = nil
|
|
15
|
+
@split_step_token = nil
|
|
16
|
+
@split_step_active_execution_authorization = nil
|
|
17
|
+
@split_step_execution_result = nil
|
|
18
|
+
clear_split_step_descriptor_state!
|
|
19
|
+
@split_step_persistence_key = nil
|
|
20
|
+
@split_step_adapter = nil
|
|
21
|
+
remove_instance_variable(:@split_step_persistence_ttl) if
|
|
22
|
+
instance_variable_defined?(:@split_step_persistence_ttl)
|
|
23
|
+
@split_step_preparation_payload = nil
|
|
24
|
+
@split_step_checkpoint_digest = nil
|
|
25
|
+
@split_step_checkpoint_version = nil
|
|
26
|
+
@split_step_preparation_thread = nil
|
|
27
|
+
@split_step_persist_permit = false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def clear_split_step_descriptor_state!
|
|
31
|
+
@split_step_prepared_descriptor = nil
|
|
32
|
+
@split_step_transaction_identity = nil
|
|
33
|
+
@split_step_preparation_digest = nil
|
|
34
|
+
@split_step_pending_descriptor = nil
|
|
35
|
+
@split_step_dispatch_started = false
|
|
36
|
+
@split_step_dispatch_token = nil
|
|
37
|
+
@split_step_dispatch_descriptor = nil
|
|
38
|
+
@split_step_attempted_dispatch_descriptor = nil
|
|
39
|
+
@split_step_dispatch_thread = nil
|
|
40
|
+
@split_step_pre_dispatch_payload = nil
|
|
41
|
+
@split_step_attempted_dispatch_payload = nil
|
|
42
|
+
clear_split_step_execution_verification!
|
|
43
|
+
clear_split_step_execution_authorization!
|
|
44
|
+
remove_instance_variable(:@split_step_definition_digest) if
|
|
45
|
+
instance_variable_defined?(:@split_step_definition_digest)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "dry-initializer"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module Smith
|
|
8
|
+
class Workflow
|
|
9
|
+
module SplitStepPersistence
|
|
10
|
+
class CanonicalPayloadDigest
|
|
11
|
+
extend Dry::Initializer
|
|
12
|
+
|
|
13
|
+
MAX_BYTES = 4 * 1024 * 1024
|
|
14
|
+
MAX_NODES = 100_000
|
|
15
|
+
|
|
16
|
+
param :payload
|
|
17
|
+
|
|
18
|
+
def self.call(payload) = new(payload).call
|
|
19
|
+
|
|
20
|
+
def call
|
|
21
|
+
if payload.bytesize > MAX_BYTES
|
|
22
|
+
raise WorkflowError, "split-step preparation payload exceeds maximum bytes #{MAX_BYTES}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
@nodes = 0
|
|
26
|
+
canonical = canonicalize(JSON.parse(payload))
|
|
27
|
+
Digest::SHA256.hexdigest(JSON.generate(canonical))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def canonicalize(value)
|
|
33
|
+
visit!
|
|
34
|
+
case value
|
|
35
|
+
when Hash then canonical_hash(value)
|
|
36
|
+
when Array then value.map { |item| canonicalize(item) }
|
|
37
|
+
when Float then value == value.to_i ? value.to_i : value
|
|
38
|
+
else value
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def canonical_hash(value)
|
|
43
|
+
value.keys.sort.to_h { |key| [key, canonicalize(value.fetch(key))] }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def visit!
|
|
47
|
+
@nodes += 1
|
|
48
|
+
return if @nodes <= MAX_NODES
|
|
49
|
+
|
|
50
|
+
raise WorkflowError, "split-step preparation payload exceeds maximum size #{MAX_NODES}"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module SplitStepPersistence
|
|
6
|
+
module Checkpoint
|
|
7
|
+
def complete_persisted_step!
|
|
8
|
+
completion_claimed = false
|
|
9
|
+
previous_phase = claim_split_step_completion!
|
|
10
|
+
completion_claimed = true
|
|
11
|
+
completed = false
|
|
12
|
+
payload = @split_step_adapter.fetch(@split_step_persistence_key)
|
|
13
|
+
unless persisted_split_step_checkpoint?(payload)
|
|
14
|
+
recover_rolled_back_split_step_checkpoint!(payload)
|
|
15
|
+
raise WorkflowError, "the persisted split-step checkpoint is not committed"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
@split_step_mutex.synchronize do
|
|
19
|
+
@persistence_version = @split_step_checkpoint_version
|
|
20
|
+
clear_step_in_progress!
|
|
21
|
+
clear_split_step_boundary!
|
|
22
|
+
end
|
|
23
|
+
completed = true
|
|
24
|
+
self
|
|
25
|
+
ensure
|
|
26
|
+
restore_incomplete_checkpoint!(previous_phase) if completion_claimed && !completed
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def persist!(key = nil, adapter: Smith.persistence_adapter)
|
|
30
|
+
checkpoint_claimed = false
|
|
31
|
+
resolved_key, store = resolve_split_step_target(key, adapter)
|
|
32
|
+
checkpoint_claimed = claim_split_step_checkpoint!
|
|
33
|
+
result = super(resolved_key, adapter: store) do |payload, next_version|
|
|
34
|
+
capture_split_step_payload!(payload, next_version, checkpoint_claimed:)
|
|
35
|
+
end
|
|
36
|
+
@split_step_mutex.synchronize { @split_step_phase = :checkpointed } if checkpoint_claimed
|
|
37
|
+
result
|
|
38
|
+
rescue StandardError
|
|
39
|
+
mark_split_step_checkpoint_unknown! if checkpoint_claimed
|
|
40
|
+
raise
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def resolve_split_step_target(key, adapter)
|
|
46
|
+
resolved_key = if @split_step_persistence_key
|
|
47
|
+
candidate = candidate_split_step_persistence_key(key)
|
|
48
|
+
if candidate.equal?(@split_step_persistence_key)
|
|
49
|
+
candidate
|
|
50
|
+
else
|
|
51
|
+
normalize_split_step_persistence_key(candidate)
|
|
52
|
+
end
|
|
53
|
+
else
|
|
54
|
+
resolve_persistence_key!(key)
|
|
55
|
+
end
|
|
56
|
+
store = persistence_adapter!(adapter)
|
|
57
|
+
validate_split_step_target!(resolved_key, store)
|
|
58
|
+
resolved_key = @split_step_persistence_key if @split_step_persistence_key
|
|
59
|
+
@persistence_key = resolved_key
|
|
60
|
+
[resolved_key, store]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def claim_split_step_checkpoint!
|
|
64
|
+
@split_step_mutex.synchronize do
|
|
65
|
+
if split_step_preparation_persist_permitted?
|
|
66
|
+
@split_step_persist_permit = false
|
|
67
|
+
return false
|
|
68
|
+
end
|
|
69
|
+
return false unless @split_step_phase
|
|
70
|
+
|
|
71
|
+
if %i[executed checkpoint_retryable].include?(@split_step_phase)
|
|
72
|
+
@split_step_phase = :checkpointing
|
|
73
|
+
return true
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
raise WorkflowError, "the active split-step boundary cannot be checkpointed"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def split_step_preparation_persist_permitted?
|
|
81
|
+
@split_step_phase == :preparing &&
|
|
82
|
+
@split_step_preparation_thread.equal?(Thread.current) &&
|
|
83
|
+
@split_step_persist_permit
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def validate_split_step_target!(key, adapter)
|
|
87
|
+
return unless @split_step_persistence_key
|
|
88
|
+
return if key == @split_step_persistence_key && adapter.equal?(@split_step_adapter)
|
|
89
|
+
|
|
90
|
+
raise WorkflowError, "the split-step persistence target cannot change"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def capture_split_step_payload!(payload, next_version, checkpoint_claimed:)
|
|
94
|
+
return capture_split_step_preparation_payload!(payload, next_version) if @split_step_phase == :preparing
|
|
95
|
+
|
|
96
|
+
validate_split_step_marker!(payload, expected: true) if checkpoint_claimed
|
|
97
|
+
return payload unless checkpoint_claimed
|
|
98
|
+
|
|
99
|
+
checkpoint_payload = split_step_checkpoint_payload(payload)
|
|
100
|
+
checkpoint_digest = Digest::SHA256.hexdigest(checkpoint_payload)
|
|
101
|
+
validate_split_step_retry_payload!(checkpoint_digest)
|
|
102
|
+
@split_step_checkpoint_digest = checkpoint_digest
|
|
103
|
+
@split_step_checkpoint_version = next_version
|
|
104
|
+
checkpoint_payload
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def validate_split_step_retry_payload!(checkpoint_digest)
|
|
108
|
+
return unless @split_step_checkpoint_digest
|
|
109
|
+
return if @split_step_checkpoint_digest == checkpoint_digest
|
|
110
|
+
|
|
111
|
+
@split_step_mutex.synchronize do
|
|
112
|
+
@split_step_phase = :checkpoint_retryable if @split_step_phase == :checkpointing
|
|
113
|
+
end
|
|
114
|
+
raise WorkflowError, "the reconciled split-step checkpoint payload changed"
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module SplitStepPersistence
|
|
6
|
+
module CheckpointState
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def claim_split_step_completion!
|
|
10
|
+
@split_step_mutex.synchronize do
|
|
11
|
+
unless %i[checkpoint_unknown checkpointed].include?(@split_step_phase)
|
|
12
|
+
raise WorkflowError, "no persisted split-step checkpoint is awaiting completion"
|
|
13
|
+
end
|
|
14
|
+
if Smith::PersistenceAdapters.supports?(@split_step_adapter, :transaction_open?) &&
|
|
15
|
+
@split_step_adapter.transaction_open?
|
|
16
|
+
raise WorkflowError, "the split-step checkpoint transaction is still open"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
previous_phase = @split_step_phase
|
|
20
|
+
@split_step_phase = :confirming_checkpoint
|
|
21
|
+
previous_phase
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def restore_incomplete_checkpoint!(previous_phase)
|
|
26
|
+
@split_step_mutex.synchronize do
|
|
27
|
+
@split_step_phase = previous_phase if @split_step_phase == :confirming_checkpoint
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def recover_rolled_back_split_step_checkpoint!(payload)
|
|
32
|
+
return unless persisted_split_step_payload?(payload, @split_step_preparation_payload)
|
|
33
|
+
|
|
34
|
+
preparation_version = JSON.parse(payload).fetch("persistence_version")
|
|
35
|
+
@split_step_mutex.synchronize do
|
|
36
|
+
return unless @split_step_phase == :confirming_checkpoint
|
|
37
|
+
|
|
38
|
+
@persistence_version = preparation_version
|
|
39
|
+
@split_step_phase = :checkpoint_retryable
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def mark_split_step_checkpoint_unknown!
|
|
44
|
+
@split_step_mutex.synchronize do
|
|
45
|
+
@split_step_phase = :checkpoint_unknown if @split_step_phase == :checkpointing
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module SplitStepPersistence
|
|
6
|
+
module DefinitionBoundary
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def restart_safe_split_step? = !effective_definition_digest.nil?
|
|
10
|
+
|
|
11
|
+
def ensure_split_step_definition_current!
|
|
12
|
+
return if effective_definition_digest == self.class.definition_digest
|
|
13
|
+
|
|
14
|
+
raise WorkflowError, "the prepared workflow definition has changed"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module SplitStepPersistence
|
|
6
|
+
module DispatchBoundary
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def dispatch_store!(...)
|
|
10
|
+
@split_step_dispatch_started = true if @split_step_phase == :preparing
|
|
11
|
+
super
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module SplitStepPersistence
|
|
6
|
+
module DispatchClaim
|
|
7
|
+
def claim_prepared_step_dispatch!
|
|
8
|
+
ensure_restart_safe_dispatch_claim!
|
|
9
|
+
ensure_split_step_definition_current!
|
|
10
|
+
transaction_identity = TransactionIdentity.capture(@split_step_adapter)
|
|
11
|
+
claim_dispatch_intent!
|
|
12
|
+
payload, descriptor = build_split_step_dispatch_claim
|
|
13
|
+
dispatch_started = true
|
|
14
|
+
replace_exact_dispatch!(payload)
|
|
15
|
+
accept_split_step_dispatch!(payload, descriptor, transaction_identity)
|
|
16
|
+
descriptor
|
|
17
|
+
rescue PersistencePayloadConflict
|
|
18
|
+
reject_split_step_dispatch!
|
|
19
|
+
raise
|
|
20
|
+
rescue StandardError
|
|
21
|
+
if dispatch_started
|
|
22
|
+
mark_split_step_dispatch_unknown!(payload, descriptor)
|
|
23
|
+
else
|
|
24
|
+
restore_unclaimed_dispatch!
|
|
25
|
+
end
|
|
26
|
+
raise
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def ensure_restart_safe_dispatch_claim!
|
|
32
|
+
return if restart_safe_split_step?
|
|
33
|
+
|
|
34
|
+
raise WorkflowError, "restart-safe dispatch claiming requires a definition_digest"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def split_step_dispatch_payload(token)
|
|
38
|
+
document = JSON.parse(@split_step_preparation_payload)
|
|
39
|
+
document["split_step_phase"] = "dispatching"
|
|
40
|
+
document["split_step_dispatch_token"] = token
|
|
41
|
+
JSON.generate(document)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def build_split_step_dispatch_claim
|
|
45
|
+
token = SecureRandom.uuid.freeze
|
|
46
|
+
payload = split_step_dispatch_payload(token)
|
|
47
|
+
[payload, build_split_step_dispatch(token, payload)]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def replace_exact_dispatch!(payload)
|
|
51
|
+
@split_step_adapter.replace_exact(
|
|
52
|
+
@split_step_persistence_key,
|
|
53
|
+
payload,
|
|
54
|
+
expected_payload: @split_step_preparation_payload,
|
|
55
|
+
ttl: nil
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def build_split_step_dispatch(token, payload)
|
|
60
|
+
PreparedStepDispatch.new(
|
|
61
|
+
prepared_step: @split_step_prepared_descriptor,
|
|
62
|
+
token:,
|
|
63
|
+
dispatch_digest: CanonicalPayloadDigest.call(payload)
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def accept_split_step_dispatch!(payload, descriptor, transaction_identity)
|
|
68
|
+
@split_step_mutex.synchronize do
|
|
69
|
+
unless active_split_step_dispatch_claim?
|
|
70
|
+
raise WorkflowError, "the prepared dispatch claim is no longer active"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
@split_step_pre_dispatch_payload = @split_step_preparation_payload
|
|
74
|
+
@split_step_preparation_payload = payload.freeze
|
|
75
|
+
@split_step_dispatch_token = descriptor.token
|
|
76
|
+
@split_step_dispatch_descriptor = descriptor
|
|
77
|
+
@split_step_dispatch_thread = nil
|
|
78
|
+
@split_step_phase = transaction_identity ? :dispatch_claimed_uncommitted : :dispatch_claimed
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def reject_split_step_dispatch!
|
|
83
|
+
@split_step_mutex.synchronize do
|
|
84
|
+
next unless active_split_step_dispatch_claim?
|
|
85
|
+
|
|
86
|
+
@split_step_dispatch_thread = nil
|
|
87
|
+
@split_step_phase = :dispatch_rejected
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def restore_unclaimed_dispatch!
|
|
92
|
+
@split_step_mutex.synchronize do
|
|
93
|
+
next unless active_split_step_dispatch_claim?
|
|
94
|
+
|
|
95
|
+
@split_step_dispatch_thread = nil
|
|
96
|
+
@split_step_phase = :prepared
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def mark_split_step_dispatch_unknown!(payload, descriptor)
|
|
101
|
+
@split_step_mutex.synchronize do
|
|
102
|
+
next unless active_split_step_dispatch_claim?
|
|
103
|
+
|
|
104
|
+
@split_step_attempted_dispatch_payload = payload&.freeze
|
|
105
|
+
@split_step_dispatch_token = descriptor&.token
|
|
106
|
+
@split_step_attempted_dispatch_descriptor = descriptor
|
|
107
|
+
@split_step_dispatch_thread = nil
|
|
108
|
+
@split_step_phase = :dispatch_unknown
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def active_split_step_dispatch_claim?
|
|
113
|
+
@split_step_phase == :claiming_dispatch && @split_step_dispatch_thread.equal?(Thread.current)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module SplitStepPersistence
|
|
6
|
+
module DispatchConfirmation
|
|
7
|
+
def confirm_prepared_step_dispatch!
|
|
8
|
+
confirmation_claimed = false
|
|
9
|
+
rolled_back = false
|
|
10
|
+
claim_dispatch_confirmation!
|
|
11
|
+
confirmation_claimed = true
|
|
12
|
+
payload = @split_step_adapter.fetch(@split_step_persistence_key)
|
|
13
|
+
return confirm_committed_dispatch! if persisted_split_step_payload?(payload, @split_step_preparation_payload)
|
|
14
|
+
|
|
15
|
+
rolled_back = recover_rolled_back_dispatch!(payload)
|
|
16
|
+
raise WorkflowError, "the persisted split-step dispatch claim is not committed"
|
|
17
|
+
rescue StandardError
|
|
18
|
+
mark_unconfirmed_dispatch_unknown! if confirmation_claimed && !rolled_back
|
|
19
|
+
raise
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def claim_dispatch_confirmation!
|
|
25
|
+
@split_step_mutex.synchronize do
|
|
26
|
+
unless @split_step_phase == :dispatch_claimed_uncommitted
|
|
27
|
+
raise WorkflowError, "no uncommitted split-step dispatch claim is awaiting confirmation"
|
|
28
|
+
end
|
|
29
|
+
if @split_step_adapter.transaction_open?
|
|
30
|
+
raise WorkflowError, "the split-step dispatch transaction is still open"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
@split_step_phase = :confirming_dispatch
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def confirm_committed_dispatch!
|
|
38
|
+
@split_step_mutex.synchronize do
|
|
39
|
+
@split_step_phase = :dispatch_claimed
|
|
40
|
+
clear_split_step_dispatch_transaction!
|
|
41
|
+
end
|
|
42
|
+
self
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def recover_rolled_back_dispatch!(payload)
|
|
46
|
+
return false unless persisted_split_step_payload?(payload, @split_step_pre_dispatch_payload)
|
|
47
|
+
|
|
48
|
+
@split_step_mutex.synchronize do
|
|
49
|
+
return false unless @split_step_phase == :confirming_dispatch
|
|
50
|
+
|
|
51
|
+
@split_step_preparation_payload = @split_step_pre_dispatch_payload
|
|
52
|
+
@split_step_phase = :prepared
|
|
53
|
+
clear_split_step_dispatch_transaction!
|
|
54
|
+
true
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def mark_unconfirmed_dispatch_unknown!
|
|
59
|
+
@split_step_mutex.synchronize do
|
|
60
|
+
@split_step_phase = :dispatch_unknown if @split_step_phase == :confirming_dispatch
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def clear_split_step_dispatch_transaction!
|
|
65
|
+
@split_step_pre_dispatch_payload = nil
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module SplitStepPersistence
|
|
6
|
+
module DispatchVerification
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def claim_dispatch_intent!
|
|
10
|
+
@split_step_mutex.synchronize do
|
|
11
|
+
raise WorkflowError, "no persisted step is prepared" unless @split_step_phase == :prepared
|
|
12
|
+
|
|
13
|
+
ensure_prepared_split_step_transition_matches!
|
|
14
|
+
@split_step_phase = :claiming_dispatch
|
|
15
|
+
@split_step_dispatch_thread = Thread.current
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def verify_split_step_dispatch_available!(verification_token)
|
|
20
|
+
payload = @split_step_adapter.fetch(@split_step_persistence_key)
|
|
21
|
+
return if persisted_split_step_payload?(payload, @split_step_preparation_payload)
|
|
22
|
+
|
|
23
|
+
@split_step_mutex.synchronize do
|
|
24
|
+
if active_split_step_execution_verification?(verification_token)
|
|
25
|
+
@split_step_phase = :dispatch_unknown
|
|
26
|
+
@split_step_execution_previous_phase = nil
|
|
27
|
+
clear_split_step_execution_verification!
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
raise PersistencePayloadConflict.new(key: @split_step_persistence_key)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|