smith-agents 0.4.5 → 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 +201 -0
- data/README.md +108 -4
- data/docs/CONFIGURATION.md +4 -1
- data/docs/PATTERNS.md +7 -0
- data/docs/PERSISTENCE.md +347 -25
- 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 -4
- 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_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_store.rb +7 -4
- data/lib/smith/persistence_adapters/cache_store.rb +1 -0
- data/lib/smith/persistence_adapters/memory.rb +16 -1
- 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 +28 -11
- data/lib/smith/persistence_adapters/retry.rb +22 -10
- data/lib/smith/persistence_adapters.rb +21 -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 +1 -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 +37 -8
- 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 +35 -22
- 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 +3 -5
- 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 +49 -47
- 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 +3 -1
- data/lib/smith/workflow/split_step_persistence/payloads.rb +7 -3
- data/lib/smith/workflow/split_step_persistence/preparation.rb +30 -2
- data/lib/smith/workflow/split_step_persistence/preparation_claim.rb +38 -12
- data/lib/smith/workflow/split_step_persistence/preparation_payload.rb +25 -0
- data/lib/smith/workflow/split_step_persistence/preparation_recovery.rb +10 -2
- 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/subclass_boundary.rb +50 -0
- data/lib/smith/workflow/split_step_persistence/transaction_identity.rb +45 -0
- data/lib/smith/workflow/split_step_persistence.rb +36 -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 +44 -18
- data/lib/smith.rb +41 -2
- data/sig/smith.rbs +1 -0
- metadata +110 -19
|
@@ -21,19 +21,50 @@ module Smith
|
|
|
21
21
|
|
|
22
22
|
def initialize_copy(source)
|
|
23
23
|
super
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
copied_split_step_phase = @split_step_phase
|
|
25
|
+
detach_copied_runtime_state!(source)
|
|
26
|
+
return unless copied_split_step_phase
|
|
26
27
|
|
|
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!
|
|
28
37
|
@split_step_token = nil
|
|
38
|
+
@split_step_prepared_descriptor = nil
|
|
39
|
+
@split_step_transaction_identity = nil
|
|
29
40
|
@split_step_execution_thread = nil
|
|
41
|
+
@split_step_active_execution_authorization = nil
|
|
42
|
+
@split_step_execution_result = nil
|
|
30
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
|
|
31
55
|
@split_step_preparation_thread = nil
|
|
32
56
|
@split_step_persist_permit = false
|
|
33
57
|
end
|
|
34
|
-
private :initialize_copy
|
|
35
58
|
|
|
36
|
-
|
|
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
|
|
37
68
|
|
|
38
69
|
def ensure_split_step_execution_allowed!
|
|
39
70
|
@split_step_mutex.synchronize do
|
|
@@ -91,24 +122,6 @@ module Smith
|
|
|
91
122
|
@split_step_execution_thread.equal?(Thread.current) &&
|
|
92
123
|
@split_step_advance_permit
|
|
93
124
|
end
|
|
94
|
-
|
|
95
|
-
def clear_split_step_boundary!
|
|
96
|
-
@split_step_phase = nil
|
|
97
|
-
@split_step_transition_name = nil
|
|
98
|
-
@split_step_transition = nil
|
|
99
|
-
@split_step_transition_signature = nil
|
|
100
|
-
@split_step_origin_state = nil
|
|
101
|
-
@split_step_token = nil
|
|
102
|
-
@split_step_persistence_key = nil
|
|
103
|
-
@split_step_adapter = nil
|
|
104
|
-
remove_instance_variable(:@split_step_persistence_ttl) if
|
|
105
|
-
instance_variable_defined?(:@split_step_persistence_ttl)
|
|
106
|
-
@split_step_preparation_payload = nil
|
|
107
|
-
@split_step_checkpoint_digest = nil
|
|
108
|
-
@split_step_checkpoint_version = nil
|
|
109
|
-
@split_step_preparation_thread = nil
|
|
110
|
-
@split_step_persist_permit = false
|
|
111
|
-
end
|
|
112
125
|
end
|
|
113
126
|
end
|
|
114
127
|
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
|
|
@@ -91,11 +91,9 @@ module Smith
|
|
|
91
91
|
end
|
|
92
92
|
|
|
93
93
|
def capture_split_step_payload!(payload, next_version, checkpoint_claimed:)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
@split_step_preparation_payload = payload
|
|
98
|
-
end
|
|
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
|
|
99
97
|
return payload unless checkpoint_claimed
|
|
100
98
|
|
|
101
99
|
checkpoint_payload = split_step_checkpoint_payload(payload)
|
|
@@ -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
|
|
@@ -5,74 +5,67 @@ module Smith
|
|
|
5
5
|
module SplitStepPersistence
|
|
6
6
|
module Execution
|
|
7
7
|
def execute_prepared_step!
|
|
8
|
-
|
|
8
|
+
authorization = ExecutionAuthorization
|
|
9
|
+
.instance_method(:authorize_prepared_step_execution!)
|
|
10
|
+
.bind_call(self)
|
|
11
|
+
result = Execution.instance_method(:execute_authorized_prepared_step!).bind_call(self, authorization)
|
|
12
|
+
result.step_snapshot
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def execute_authorized_prepared_step!(authorization)
|
|
16
|
+
authorization = validate_split_step_execution_authorization!(authorization)
|
|
9
17
|
step = nil
|
|
10
|
-
|
|
18
|
+
result = nil
|
|
19
|
+
execution_thread = Thread.current
|
|
20
|
+
execution_active = false
|
|
11
21
|
begin
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
execution_started = true
|
|
22
|
+
activate_split_step_execution!(authorization, execution_thread)
|
|
23
|
+
execution_active = true
|
|
15
24
|
step = execute_claimed_split_step_transition!
|
|
25
|
+
result = consume_split_step_execution_result!(execution_thread)
|
|
16
26
|
ensure
|
|
17
|
-
|
|
27
|
+
finish_split_step_execution!(step, authorization, execution_thread) if execution_active
|
|
18
28
|
end
|
|
19
|
-
|
|
29
|
+
result
|
|
20
30
|
end
|
|
21
31
|
|
|
22
|
-
def prepared_persisted_step?
|
|
23
|
-
|
|
24
|
-
private
|
|
25
|
-
|
|
26
|
-
def claim_split_step_execution_verification!
|
|
32
|
+
def prepared_persisted_step?
|
|
27
33
|
@split_step_mutex.synchronize do
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
prepared_split_step_transition_matches?
|
|
31
|
-
|
|
32
|
-
@split_step_phase = :verifying_execution
|
|
34
|
+
expected = restart_safe_split_step? ? :dispatch_claimed : :prepared
|
|
35
|
+
[expected, :execution_authorized].include?(@split_step_phase)
|
|
33
36
|
end
|
|
34
37
|
end
|
|
35
38
|
|
|
36
|
-
|
|
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
|
|
39
|
+
private
|
|
43
40
|
|
|
44
|
-
def activate_split_step_execution!
|
|
41
|
+
def activate_split_step_execution!(authorization, execution_thread)
|
|
45
42
|
@split_step_mutex.synchronize do
|
|
46
|
-
unless
|
|
47
|
-
raise WorkflowError, "the prepared execution
|
|
43
|
+
unless active_split_step_execution_authorization?(authorization)
|
|
44
|
+
raise WorkflowError, "the prepared-step execution authorization is no longer active"
|
|
48
45
|
end
|
|
49
46
|
|
|
47
|
+
ensure_split_step_definition_current!
|
|
48
|
+
ensure_prepared_split_step_transition_matches!
|
|
49
|
+
PreparedStepExecutionAuthorization.instance_method(:activate_execution!)
|
|
50
|
+
.bind_call(authorization, execution_thread)
|
|
51
|
+
|
|
52
|
+
@split_step_active_execution_authorization = authorization
|
|
53
|
+
@split_step_execution_result = nil
|
|
50
54
|
@split_step_phase = :executing
|
|
51
|
-
|
|
55
|
+
clear_split_step_execution_authorization!
|
|
56
|
+
@split_step_execution_thread = execution_thread
|
|
52
57
|
@split_step_advance_permit = true
|
|
53
58
|
end
|
|
54
59
|
end
|
|
55
60
|
|
|
56
|
-
def restore_unverified_execution!
|
|
61
|
+
def restore_unverified_execution!(verification_token)
|
|
57
62
|
@split_step_mutex.synchronize do
|
|
58
|
-
|
|
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
|
|
63
|
+
next unless active_split_step_execution_verification?(verification_token)
|
|
68
64
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
split_step_preparation_payload(
|
|
74
|
-
JSON.generate(to_state.merge(persistence_version: @persistence_version))
|
|
75
|
-
)
|
|
65
|
+
@split_step_phase = @split_step_execution_previous_phase
|
|
66
|
+
@split_step_execution_previous_phase = nil
|
|
67
|
+
clear_split_step_execution_verification!
|
|
68
|
+
end
|
|
76
69
|
end
|
|
77
70
|
|
|
78
71
|
def execute_claimed_split_step_transition!
|
|
@@ -82,12 +75,21 @@ module Smith
|
|
|
82
75
|
raise WorkflowError, "prepared execution did not return the claimed transition"
|
|
83
76
|
end
|
|
84
77
|
|
|
85
|
-
def finish_split_step_execution!(step)
|
|
78
|
+
def finish_split_step_execution!(step, authorization, execution_thread)
|
|
86
79
|
@split_step_mutex.synchronize do
|
|
80
|
+
next unless @split_step_phase == :executing &&
|
|
81
|
+
@split_step_execution_thread.equal?(execution_thread)
|
|
82
|
+
|
|
87
83
|
@split_step_execution_thread = nil
|
|
88
84
|
@split_step_advance_permit = false
|
|
85
|
+
@split_step_execution_previous_phase = nil
|
|
86
|
+
@split_step_active_execution_authorization = nil
|
|
87
|
+
@split_step_execution_result = nil
|
|
89
88
|
@split_step_phase = step ? :executed : :attempted
|
|
90
89
|
end
|
|
90
|
+
ensure
|
|
91
|
+
PreparedStepExecutionAuthorization.instance_method(:close_execution!)
|
|
92
|
+
.bind_call(authorization, execution_thread)
|
|
91
93
|
end
|
|
92
94
|
|
|
93
95
|
def resolve_split_step_advance_transition
|