smith-agents 0.4.5 → 0.6.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 +264 -0
- data/README.md +191 -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 +37 -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 +10 -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/composite/branch.rb +93 -0
- data/lib/smith/workflow/composite/branch_budget_contract.rb +41 -0
- data/lib/smith/workflow/composite/branch_contract.rb +102 -0
- data/lib/smith/workflow/composite/branch_execution.rb +124 -0
- data/lib/smith/workflow/composite/branch_failure.rb +91 -0
- data/lib/smith/workflow/composite/branch_outcome.rb +108 -0
- data/lib/smith/workflow/composite/budget_allocator.rb +78 -0
- data/lib/smith/workflow/composite/budget_math.rb +51 -0
- data/lib/smith/workflow/composite/contract.rb +100 -0
- data/lib/smith/workflow/composite/effects.rb +140 -0
- data/lib/smith/workflow/composite/effects_application.rb +36 -0
- data/lib/smith/workflow/composite/effects_baseline.rb +36 -0
- data/lib/smith/workflow/composite/effects_preflight.rb +102 -0
- data/lib/smith/workflow/composite/encoded_value_budget.rb +36 -0
- data/lib/smith/workflow/composite/enums.rb +27 -0
- data/lib/smith/workflow/composite/error.rb +54 -0
- data/lib/smith/workflow/composite/error_evidence.rb +61 -0
- data/lib/smith/workflow/composite/execution_contract.rb +73 -0
- data/lib/smith/workflow/composite/fanout_branch_contract.rb +61 -0
- data/lib/smith/workflow/composite/input.rb +53 -0
- data/lib/smith/workflow/composite/outcome_accumulator.rb +131 -0
- data/lib/smith/workflow/composite/outcome_set.rb +23 -0
- data/lib/smith/workflow/composite/payload.rb +128 -0
- data/lib/smith/workflow/composite/payload_digest.rb +28 -0
- data/lib/smith/workflow/composite/plan.rb +130 -0
- data/lib/smith/workflow/composite/plan_integrity.rb +52 -0
- data/lib/smith/workflow/composite/planner.rb +53 -0
- data/lib/smith/workflow/composite/preparation.rb +27 -0
- data/lib/smith/workflow/composite/reducer.rb +133 -0
- data/lib/smith/workflow/composite/reduction.rb +31 -0
- data/lib/smith/workflow/composite/value_budget.rb +90 -0
- data/lib/smith/workflow/composite_branch_execution_authorization.rb +52 -0
- data/lib/smith/workflow/deadline_enforcement.rb +22 -5
- 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 +28 -30
- 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 +20 -19
- 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 +134 -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 +46 -14
- data/lib/smith/workflow/persistence.rb +37 -8
- data/lib/smith/workflow/prepared_branch_execution.rb +31 -0
- 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 +101 -0
- data/lib/smith/workflow/prepared_step_execution_result.rb +49 -0
- data/lib/smith/workflow/prepared_step_execution_scope.rb +111 -0
- data/lib/smith/workflow/prepared_step_recovery.rb +38 -0
- data/lib/smith/workflow/process_local.rb +33 -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/composite_branch_authorization.rb +99 -0
- data/lib/smith/workflow/split_step_persistence/composite_branch_effects.rb +41 -0
- data/lib/smith/workflow/split_step_persistence/composite_branch_execution.rb +95 -0
- data/lib/smith/workflow/split_step_persistence/composite_branch_outcome.rb +36 -0
- data/lib/smith/workflow/split_step_persistence/composite_execution.rb +39 -0
- data/lib/smith/workflow/split_step_persistence/composite_preparation.rb +57 -0
- data/lib/smith/workflow/split_step_persistence/composite_reduction_execution.rb +122 -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 +20 -64
- data/lib/smith/workflow/split_step_persistence/execution_authorization.rb +109 -0
- data/lib/smith/workflow/split_step_persistence/execution_authorization_issuance.rb +57 -0
- data/lib/smith/workflow/split_step_persistence/execution_binding_collector.rb +95 -0
- data/lib/smith/workflow/split_step_persistence/execution_binding_snapshot.rb +109 -0
- data/lib/smith/workflow/split_step_persistence/execution_lifecycle.rb +96 -0
- data/lib/smith/workflow/split_step_persistence/execution_result_capture.rb +48 -0
- data/lib/smith/workflow/split_step_persistence/execution_verification.rb +77 -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 +66 -0
- data/lib/smith/workflow/split_step_persistence/transaction_identity.rb +45 -0
- data/lib/smith/workflow/split_step_persistence.rb +50 -0
- data/lib/smith/workflow/step_completion.rb +31 -0
- data/lib/smith/workflow/step_context.rb +46 -0
- data/lib/smith/workflow/string_snapshot.rb +25 -0
- data/lib/smith/workflow/thread_context_snapshot.rb +103 -0
- data/lib/smith/workflow/transition.rb +137 -133
- data/lib/smith/workflow/transition_actionability.rb +22 -0
- data/lib/smith/workflow.rb +64 -18
- data/lib/smith.rb +44 -2
- data/sig/smith.rbs +1 -0
- metadata +155 -19
|
@@ -7,10 +7,10 @@ module Smith
|
|
|
7
7
|
private
|
|
8
8
|
|
|
9
9
|
def persisted_split_step_payload?(payload, expected)
|
|
10
|
-
return false unless payload
|
|
10
|
+
return false unless payload.is_a?(String) && expected.is_a?(String)
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
rescue JSON::ParserError, TypeError
|
|
12
|
+
CanonicalPayloadDigest.call(payload) == CanonicalPayloadDigest.call(expected)
|
|
13
|
+
rescue WorkflowError, JSON::ParserError, TypeError
|
|
14
14
|
false
|
|
15
15
|
end
|
|
16
16
|
|
|
@@ -38,6 +38,10 @@ module Smith
|
|
|
38
38
|
def split_step_preparation_payload(payload)
|
|
39
39
|
document = JSON.parse(payload)
|
|
40
40
|
document["split_step_token"] = @split_step_token
|
|
41
|
+
if restart_safe_split_step?
|
|
42
|
+
document["split_step_phase"] = "prepared"
|
|
43
|
+
document["split_step_persistence_identity"] = @split_step_adapter.persistence_identity
|
|
44
|
+
end
|
|
41
45
|
JSON.generate(document)
|
|
42
46
|
end
|
|
43
47
|
|
|
@@ -4,6 +4,14 @@ module Smith
|
|
|
4
4
|
class Workflow
|
|
5
5
|
module SplitStepPersistence
|
|
6
6
|
module Preparation
|
|
7
|
+
DESCRIPTOR_PHASES = %i[
|
|
8
|
+
prepared_uncommitted prepared verifying_execution execution_authorized executing executed attempted
|
|
9
|
+
dispatch_claimed_uncommitted dispatch_claimed dispatch_rejected dispatch_unknown
|
|
10
|
+
confirming_dispatch checkpointing checkpoint_retryable checkpoint_unknown
|
|
11
|
+
checkpointed confirming_checkpoint
|
|
12
|
+
].freeze
|
|
13
|
+
private_constant :DESCRIPTOR_PHASES
|
|
14
|
+
|
|
7
15
|
def prepare_persisted_step!(key = nil, adapter: Smith.persistence_adapter)
|
|
8
16
|
ensure_strict_split_step_persistence!
|
|
9
17
|
intent_claimed = claim_split_step_preparation_intent!
|
|
@@ -27,13 +35,32 @@ module Smith
|
|
|
27
35
|
raise WorkflowError, "the persisted split-step preparation is not committed" unless
|
|
28
36
|
persisted_split_step_payload?(payload, @split_step_preparation_payload)
|
|
29
37
|
|
|
30
|
-
@split_step_mutex.synchronize
|
|
38
|
+
@split_step_mutex.synchronize do
|
|
39
|
+
@split_step_phase = :prepared
|
|
40
|
+
@split_step_transaction_thread = nil
|
|
41
|
+
@split_step_transaction_identity = nil
|
|
42
|
+
end
|
|
31
43
|
confirmed = true
|
|
32
44
|
self
|
|
33
45
|
ensure
|
|
34
46
|
restore_unconfirmed_preparation! if confirmation_claimed && !confirmed
|
|
35
47
|
end
|
|
36
48
|
|
|
49
|
+
def prepared_persisted_step
|
|
50
|
+
phase, descriptor, adapter, transaction_identity = @split_step_mutex.synchronize do
|
|
51
|
+
[
|
|
52
|
+
@split_step_phase,
|
|
53
|
+
@split_step_prepared_descriptor,
|
|
54
|
+
@split_step_adapter,
|
|
55
|
+
@split_step_transaction_identity
|
|
56
|
+
]
|
|
57
|
+
end
|
|
58
|
+
return unless DESCRIPTOR_PHASES.include?(phase) && descriptor
|
|
59
|
+
return descriptor unless phase == :prepared_uncommitted
|
|
60
|
+
|
|
61
|
+
descriptor if TransactionIdentity.matches?(adapter, transaction_identity)
|
|
62
|
+
end
|
|
63
|
+
|
|
37
64
|
private
|
|
38
65
|
|
|
39
66
|
def ensure_strict_split_step_persistence!
|
|
@@ -58,7 +85,7 @@ module Smith
|
|
|
58
85
|
transition = if @next_transition_name
|
|
59
86
|
self.class.find_transition(@next_transition_name)
|
|
60
87
|
else
|
|
61
|
-
self.class.
|
|
88
|
+
self.class.first_transition_from(@state)
|
|
62
89
|
end
|
|
63
90
|
validate_transition_origin!(transition) if transition
|
|
64
91
|
transition
|
|
@@ -78,6 +105,7 @@ module Smith
|
|
|
78
105
|
raise WorkflowError, "split-step persistence requires an adapter with store_versioned"
|
|
79
106
|
end
|
|
80
107
|
|
|
108
|
+
validate_restart_safe_adapter!(adapter)
|
|
81
109
|
unless split_step_adapter_accepts_explicit_ttl?(adapter)
|
|
82
110
|
raise WorkflowError, "split-step persistence requires store_versioned to accept ttl:"
|
|
83
111
|
end
|
|
@@ -13,6 +13,7 @@ module Smith
|
|
|
13
13
|
|
|
14
14
|
@split_step_phase = :claiming_preparation
|
|
15
15
|
@split_step_preparation_thread = Thread.current
|
|
16
|
+
@split_step_definition_digest = self.class.send(:seal_definition_identity!)
|
|
16
17
|
true
|
|
17
18
|
end
|
|
18
19
|
end
|
|
@@ -24,10 +25,13 @@ module Smith
|
|
|
24
25
|
|
|
25
26
|
@split_step_phase = nil
|
|
26
27
|
@split_step_preparation_thread = nil
|
|
28
|
+
remove_instance_variable(:@split_step_definition_digest) if
|
|
29
|
+
instance_variable_defined?(:@split_step_definition_digest)
|
|
27
30
|
end
|
|
28
31
|
end
|
|
29
32
|
|
|
30
33
|
def finalize_split_step_preparation!(transition, transition_name, key, adapter, persistence_ttl)
|
|
34
|
+
transaction_identity = TransactionIdentity.capture(adapter)
|
|
31
35
|
transition_signature = TransitionContract.capture(transition)
|
|
32
36
|
@split_step_mutex.synchronize do
|
|
33
37
|
unless active_split_step_preparation_claim?
|
|
@@ -41,32 +45,54 @@ module Smith
|
|
|
41
45
|
@split_step_origin_state = @state
|
|
42
46
|
@split_step_token = SecureRandom.uuid.freeze
|
|
43
47
|
detach_split_step_execution_state!
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
@split_step_persistence_ttl = persistence_ttl
|
|
47
|
-
@persistence_key = key
|
|
48
|
-
@split_step_persist_permit = true
|
|
48
|
+
target_writer = PreparationClaim.instance_method(:assign_split_step_persistence_target!)
|
|
49
|
+
target_writer.bind_call(self, key, adapter, persistence_ttl, transaction_identity)
|
|
49
50
|
end
|
|
50
51
|
end
|
|
51
52
|
|
|
53
|
+
def assign_split_step_persistence_target!(key, adapter, persistence_ttl, transaction_identity)
|
|
54
|
+
@split_step_persistence_key = key
|
|
55
|
+
@split_step_adapter = adapter
|
|
56
|
+
@split_step_persistence_ttl = persistence_ttl
|
|
57
|
+
@split_step_transaction_identity = transaction_identity
|
|
58
|
+
@split_step_dispatch_started = false
|
|
59
|
+
@persistence_key = key
|
|
60
|
+
@split_step_persist_permit = true
|
|
61
|
+
end
|
|
62
|
+
|
|
52
63
|
def active_split_step_preparation_claim?
|
|
53
64
|
@split_step_phase == :claiming_preparation &&
|
|
54
65
|
@split_step_preparation_thread.equal?(Thread.current)
|
|
55
66
|
end
|
|
56
67
|
|
|
57
|
-
def mark_split_step_prepared!(
|
|
68
|
+
def mark_split_step_prepared!(_adapter)
|
|
58
69
|
@split_step_mutex.synchronize do
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
70
|
+
phase = @split_step_transaction_identity ? :prepared_uncommitted : :prepared
|
|
71
|
+
unless @split_step_pending_descriptor
|
|
72
|
+
raise WorkflowError, "split-step preparation descriptor was not captured"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
@split_step_prepared_descriptor = @split_step_pending_descriptor
|
|
76
|
+
@split_step_pending_descriptor = nil
|
|
77
|
+
@split_step_phase = phase
|
|
65
78
|
@split_step_preparation_thread = nil
|
|
66
79
|
@split_step_persist_permit = false
|
|
67
80
|
end
|
|
68
81
|
end
|
|
69
82
|
|
|
83
|
+
def build_split_step_prepared_descriptor(persistence_version)
|
|
84
|
+
PreparedStep.new(
|
|
85
|
+
token: @split_step_token,
|
|
86
|
+
transition: @split_step_transition_name.to_s,
|
|
87
|
+
from: @split_step_origin_state.to_s,
|
|
88
|
+
persistence_key: @split_step_persistence_key,
|
|
89
|
+
persistence_version: persistence_version,
|
|
90
|
+
step_number: @step_count + 1,
|
|
91
|
+
preparation_digest: @split_step_preparation_digest,
|
|
92
|
+
definition_digest: effective_definition_digest
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
|
|
70
96
|
def claim_split_step_confirmation!
|
|
71
97
|
@split_step_mutex.synchronize do
|
|
72
98
|
unless @split_step_phase == :prepared_uncommitted
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module SplitStepPersistence
|
|
6
|
+
module PreparationPayload
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def capture_split_step_preparation_payload!(payload, next_version)
|
|
10
|
+
validate_split_step_marker!(payload, expected: true)
|
|
11
|
+
prepared_payload = split_step_preparation_payload(payload)
|
|
12
|
+
@split_step_preparation_payload = prepared_payload
|
|
13
|
+
@split_step_preparation_digest = CanonicalPayloadDigest.call(prepared_payload)
|
|
14
|
+
descriptor_builder = PreparationClaim.instance_method(:build_split_step_prepared_descriptor)
|
|
15
|
+
@split_step_pending_descriptor = descriptor_builder.bind_call(self, next_version)
|
|
16
|
+
prepared_payload
|
|
17
|
+
rescue StandardError
|
|
18
|
+
resetter = PreparationRecovery.instance_method(:reset_failed_split_step_preparation!)
|
|
19
|
+
resetter.bind_call(self)
|
|
20
|
+
raise
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -13,7 +13,11 @@ module Smith
|
|
|
13
13
|
handle_split_step_preparation_conflict!
|
|
14
14
|
raise
|
|
15
15
|
rescue StandardError
|
|
16
|
-
|
|
16
|
+
if @split_step_dispatch_started
|
|
17
|
+
mark_split_step_preparation_unknown!
|
|
18
|
+
else
|
|
19
|
+
reset_failed_split_step_preparation!
|
|
20
|
+
end
|
|
17
21
|
raise
|
|
18
22
|
end
|
|
19
23
|
|
|
@@ -37,7 +41,11 @@ module Smith
|
|
|
37
41
|
|
|
38
42
|
def mark_split_step_preparation_unknown!
|
|
39
43
|
@split_step_mutex.synchronize do
|
|
40
|
-
|
|
44
|
+
if @split_step_phase == :preparing
|
|
45
|
+
@split_step_phase = :preparation_unknown
|
|
46
|
+
@split_step_prepared_descriptor = nil
|
|
47
|
+
@split_step_transaction_identity = nil
|
|
48
|
+
end
|
|
41
49
|
end
|
|
42
50
|
end
|
|
43
51
|
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module SplitStepPersistence
|
|
6
|
+
module Recovery
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def recover_prepared_step(recovery, adapter)
|
|
10
|
+
decision = validate_prepared_step_recovery!(recovery)
|
|
11
|
+
descriptor = decision.prepared_step
|
|
12
|
+
store = persistence_adapter!(adapter)
|
|
13
|
+
ensure_strict_split_step_persistence!
|
|
14
|
+
validate_split_step_adapter!(store)
|
|
15
|
+
ensure_recovery_outside_transaction!(store)
|
|
16
|
+
payload = fetch_recovery_payload!(store, descriptor.persistence_key)
|
|
17
|
+
document = validate_recovery_payload!(payload, decision, store)
|
|
18
|
+
restore_state(document, allow_step_in_progress: true)
|
|
19
|
+
attach_recovered_split_step!(descriptor, payload, store, dispatch_claim: decision.dispatch_claim)
|
|
20
|
+
self
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def validate_prepared_step_recovery!(recovery)
|
|
24
|
+
return recovery if recovery.is_a?(PreparedStepRecovery)
|
|
25
|
+
|
|
26
|
+
raise ArgumentError, "recovery must be a Smith::Workflow::PreparedStepRecovery"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def ensure_recovery_outside_transaction!(adapter)
|
|
30
|
+
return unless Smith::PersistenceAdapters.supports?(adapter, :transaction_open?)
|
|
31
|
+
return unless adapter.transaction_open?
|
|
32
|
+
|
|
33
|
+
raise WorkflowError, "prepared-step recovery requires committed host authority outside a transaction"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def fetch_recovery_payload!(adapter, key)
|
|
37
|
+
payload = adapter.fetch(key)
|
|
38
|
+
return payload if payload.is_a?(String)
|
|
39
|
+
|
|
40
|
+
raise WorkflowError, "the persisted split-step preparation is not available"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def validate_recovery_payload!(payload, decision, adapter)
|
|
44
|
+
if payload.bytesize > CanonicalPayloadDigest::MAX_BYTES
|
|
45
|
+
raise WorkflowError,
|
|
46
|
+
"persisted split-step preparation exceeds maximum bytes #{CanonicalPayloadDigest::MAX_BYTES}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
document = JSON.parse(payload)
|
|
50
|
+
raise WorkflowError, "the persisted split-step preparation must be an object" unless document.is_a?(Hash)
|
|
51
|
+
|
|
52
|
+
validate_recovery_definition!(document, decision.prepared_step)
|
|
53
|
+
validate_recovery_identity!(document, decision, adapter)
|
|
54
|
+
validate_recovery_digest!(payload, decision)
|
|
55
|
+
document
|
|
56
|
+
rescue JSON::ParserError, TypeError
|
|
57
|
+
raise WorkflowError, "the persisted split-step preparation is invalid"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def validate_recovery_definition!(document, descriptor)
|
|
61
|
+
current_digest = self.class.definition_digest
|
|
62
|
+
return if recovery_digests_match?(document, descriptor, current_digest) &&
|
|
63
|
+
recovery_class_matches?(document) && recovery_schema_matches?(document)
|
|
64
|
+
|
|
65
|
+
raise WorkflowError, "the prepared step does not match the current workflow definition"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def recovery_digests_match?(document, descriptor, current_digest)
|
|
69
|
+
current_digest &&
|
|
70
|
+
descriptor.definition_digest == current_digest &&
|
|
71
|
+
document["definition_digest"] == current_digest
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def recovery_class_matches?(document)
|
|
75
|
+
!self.class.name.to_s.empty? && document["class"] == self.class.name
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def recovery_schema_matches?(document)
|
|
79
|
+
document["schema_version"] == self.class.persistence_schema_version
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def validate_recovery_identity!(document, decision, adapter)
|
|
83
|
+
descriptor = decision.prepared_step
|
|
84
|
+
return if recovery_marker_matches?(document, decision, adapter) &&
|
|
85
|
+
recovery_position_matches?(document, descriptor)
|
|
86
|
+
|
|
87
|
+
raise WorkflowError, "the persisted split-step preparation does not match the recovery descriptor"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def recovery_marker_matches?(document, decision, adapter)
|
|
91
|
+
descriptor = decision.prepared_step
|
|
92
|
+
document["step_in_progress"] == true &&
|
|
93
|
+
recovery_phase_matches?(document, decision.dispatch_claim) &&
|
|
94
|
+
document["split_step_persistence_identity"] == adapter.persistence_identity &&
|
|
95
|
+
document["split_step_token"] == descriptor.token &&
|
|
96
|
+
document["persistence_key"] == descriptor.persistence_key &&
|
|
97
|
+
document["persistence_version"] == descriptor.persistence_version
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def recovery_phase_matches?(document, dispatch_claim)
|
|
101
|
+
if dispatch_claim
|
|
102
|
+
document["split_step_phase"] == "dispatching" &&
|
|
103
|
+
document["split_step_dispatch_token"] == dispatch_claim.token
|
|
104
|
+
else
|
|
105
|
+
document["split_step_phase"] == "prepared" && !document.key?("split_step_dispatch_token")
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def recovery_position_matches?(document, descriptor)
|
|
110
|
+
step_count = document["step_count"]
|
|
111
|
+
step_count.is_a?(Integer) &&
|
|
112
|
+
step_count + 1 == descriptor.step_number &&
|
|
113
|
+
document["state"].to_s == descriptor.from
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def validate_recovery_digest!(payload, decision)
|
|
117
|
+
expected = decision.dispatch_claim&.dispatch_digest || decision.prepared_step.preparation_digest
|
|
118
|
+
return if CanonicalPayloadDigest.call(payload) == expected
|
|
119
|
+
|
|
120
|
+
raise WorkflowError, "the persisted split-step recovery payload digest does not match"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module SplitStepPersistence
|
|
6
|
+
module RecoveryBoundary
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def attach_recovered_split_step!(descriptor, payload, adapter, dispatch_claim:)
|
|
10
|
+
transition = pending_split_step_transition
|
|
11
|
+
transition_name = @next_transition_name || transition&.name
|
|
12
|
+
unless transition && transition_name.to_s == descriptor.transition && @state.to_s == descriptor.from
|
|
13
|
+
raise WorkflowError, "the recovered transition does not match the current workflow position"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
detach_split_step_execution_state!
|
|
17
|
+
install_recovered_split_step!(descriptor, payload, adapter, transition, dispatch_claim)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def install_recovered_split_step!(descriptor, payload, adapter, transition, dispatch_claim)
|
|
21
|
+
signature = TransitionContract.capture(transition)
|
|
22
|
+
@split_step_mutex.synchronize do
|
|
23
|
+
install_recovered_transition!(transition, signature)
|
|
24
|
+
install_recovered_persistence!(descriptor, payload, adapter)
|
|
25
|
+
install_recovered_dispatch!(dispatch_claim)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def install_recovered_transition!(transition, signature)
|
|
30
|
+
@split_step_transition_name = transition.name
|
|
31
|
+
@split_step_transition = transition
|
|
32
|
+
@split_step_transition_signature = signature
|
|
33
|
+
@split_step_origin_state = @state
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def install_recovered_persistence!(descriptor, payload, adapter)
|
|
37
|
+
@split_step_token = descriptor.token
|
|
38
|
+
@split_step_prepared_descriptor = descriptor
|
|
39
|
+
@split_step_persistence_key = descriptor.persistence_key
|
|
40
|
+
@split_step_adapter = adapter
|
|
41
|
+
@split_step_persistence_ttl = nil
|
|
42
|
+
@split_step_preparation_payload = payload.dup.freeze
|
|
43
|
+
@split_step_preparation_digest = descriptor.preparation_digest
|
|
44
|
+
@split_step_definition_digest = descriptor.definition_digest
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def install_recovered_dispatch!(dispatch_claim)
|
|
48
|
+
@split_step_dispatch_started = false
|
|
49
|
+
@split_step_dispatch_token = dispatch_claim&.token
|
|
50
|
+
@split_step_dispatch_descriptor = dispatch_claim
|
|
51
|
+
@split_step_phase = dispatch_claim ? :dispatch_claimed : :prepared
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module SplitStepPersistence
|
|
6
|
+
module RecoveryClassMethods
|
|
7
|
+
def recover_prepared_step(recovery, adapter: Smith.persistence_adapter)
|
|
8
|
+
seal_definition_identity!
|
|
9
|
+
allocate.send(:recover_prepared_step, recovery, adapter)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry-initializer"
|
|
4
|
+
|
|
5
|
+
module Smith
|
|
6
|
+
class Workflow
|
|
7
|
+
module SplitStepPersistence
|
|
8
|
+
class ReplaceExactSignature
|
|
9
|
+
extend Dry::Initializer
|
|
10
|
+
|
|
11
|
+
CONTRACT_KEYWORDS = %i[expected_payload ttl].freeze
|
|
12
|
+
|
|
13
|
+
param :callable
|
|
14
|
+
|
|
15
|
+
def valid? = accepts_two_values? && accepts_keywords?
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def parameters = callable.parameters
|
|
20
|
+
|
|
21
|
+
def accepts_two_values?
|
|
22
|
+
required = parameters.count { |kind, _| kind == :req }
|
|
23
|
+
positional = parameters.count { |kind, _| %i[req opt].include?(kind) }
|
|
24
|
+
rest = parameters.any? { |kind, _| kind == :rest }
|
|
25
|
+
required <= 2 && (positional >= 2 || rest)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def accepts_keywords?
|
|
29
|
+
required_keywords_supported? && contract_keywords_accepted?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def required_keywords_supported?
|
|
33
|
+
required = parameters.filter_map { |kind, name| name if kind == :keyreq }
|
|
34
|
+
(required - CONTRACT_KEYWORDS).empty?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def contract_keywords_accepted?
|
|
38
|
+
return true if parameters.any? { |kind, _| kind == :keyrest }
|
|
39
|
+
|
|
40
|
+
keywords = parameters.filter_map { |kind, name| name if %i[key keyreq].include?(kind) }
|
|
41
|
+
CONTRACT_KEYWORDS.all? { |name| keywords.include?(name) }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "replace_exact_signature"
|
|
4
|
+
|
|
5
|
+
module Smith
|
|
6
|
+
class Workflow
|
|
7
|
+
module SplitStepPersistence
|
|
8
|
+
module RestartSafeAdapter
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def validate_restart_safe_adapter!(adapter)
|
|
12
|
+
return unless restart_safe_split_step?
|
|
13
|
+
|
|
14
|
+
ensure_replace_exact_capability!(adapter)
|
|
15
|
+
ensure_replace_exact_signature!(adapter)
|
|
16
|
+
ensure_persistence_identity!(adapter)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def ensure_replace_exact_capability!(adapter)
|
|
20
|
+
return if Smith::PersistenceAdapters.supports?(adapter, :replace_exact)
|
|
21
|
+
|
|
22
|
+
raise WorkflowError, "restart-safe split-step persistence requires replace_exact"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def ensure_replace_exact_signature!(adapter)
|
|
26
|
+
return if ReplaceExactSignature.new(adapter.method(:replace_exact)).valid?
|
|
27
|
+
|
|
28
|
+
raise WorkflowError,
|
|
29
|
+
"restart-safe split-step persistence requires replace_exact(key, payload, expected_payload:, ttl:)"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def ensure_persistence_identity!(adapter)
|
|
33
|
+
identity = adapter.persistence_identity if
|
|
34
|
+
Smith::PersistenceAdapters.supports?(adapter, :persistence_identity)
|
|
35
|
+
return if identity.is_a?(String) && !identity.empty? && identity.bytesize <= 256
|
|
36
|
+
|
|
37
|
+
raise WorkflowError, "restart-safe split-step persistence requires a bounded persistence_identity"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -4,6 +4,43 @@ module Smith
|
|
|
4
4
|
class Workflow
|
|
5
5
|
module SplitStepPersistence
|
|
6
6
|
module SubclassBoundary
|
|
7
|
+
class << self
|
|
8
|
+
def protect_execution_path!(workflow_class, *owners)
|
|
9
|
+
protected_method_names(owners).each do |method_name|
|
|
10
|
+
protect_method!(workflow_class, method_name)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def protected_method_names(owners)
|
|
17
|
+
owners.flat_map do |owner|
|
|
18
|
+
owner.private_instance_methods(false) + owner.protected_instance_methods(false)
|
|
19
|
+
end.uniq
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def protect_method!(workflow_class, method_name)
|
|
23
|
+
return if boundary_method_names.include?(method_name)
|
|
24
|
+
|
|
25
|
+
implementation = workflow_class.instance_method(method_name)
|
|
26
|
+
define_method(method_name) do |*arguments, **keywords, &block|
|
|
27
|
+
active = SubclassBoundary
|
|
28
|
+
.instance_method(:smith_prepared_execution_active?)
|
|
29
|
+
.bind_call(self)
|
|
30
|
+
if active
|
|
31
|
+
implementation.bind_call(self, *arguments, **keywords, &block)
|
|
32
|
+
else
|
|
33
|
+
super(*arguments, **keywords, &block)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
private method_name
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def boundary_method_names
|
|
40
|
+
instance_methods(false) + private_instance_methods(false) + protected_instance_methods(false)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
7
44
|
def self.guard
|
|
8
45
|
Module.new.tap do |guard|
|
|
9
46
|
instance_methods(false).each do |method_name|
|
|
@@ -31,8 +68,33 @@ module Smith
|
|
|
31
68
|
super
|
|
32
69
|
end
|
|
33
70
|
|
|
71
|
+
def authorize_prepared_step_execution!(...)
|
|
72
|
+
ExecutionAuthorizationIssuance.instance_method(:authorize_prepared_step_execution!).bind_call(self, ...)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def prepare_composite_step!(...)
|
|
76
|
+
CompositeExecution.instance_method(:prepare_composite_step!).bind_call(self, ...)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def execute_prepared_composite_branch!(...)
|
|
80
|
+
CompositeExecution.instance_method(:execute_prepared_composite_branch!).bind_call(self, ...)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def reduce_prepared_composite_step!(...)
|
|
84
|
+
CompositeExecution.instance_method(:reduce_prepared_composite_step!).bind_call(self, ...)
|
|
85
|
+
end
|
|
86
|
+
|
|
34
87
|
private
|
|
35
88
|
|
|
89
|
+
def smith_prepared_execution_active?
|
|
90
|
+
authorization = @split_step_active_execution_authorization
|
|
91
|
+
return true if authorization&.active_in_current_execution? == true
|
|
92
|
+
|
|
93
|
+
authorization = @split_step_execution_authorization
|
|
94
|
+
@split_step_phase == :execution_authorized &&
|
|
95
|
+
authorization&.issued_in_current_process? == true
|
|
96
|
+
end
|
|
97
|
+
|
|
36
98
|
def effective_persistence_ttl
|
|
37
99
|
return super unless split_step_boundary_active?
|
|
38
100
|
return super unless instance_variable_defined?(:@split_step_persistence_ttl)
|
|
@@ -46,6 +108,10 @@ module Smith
|
|
|
46
108
|
|
|
47
109
|
{ ttl: ttl }
|
|
48
110
|
end
|
|
111
|
+
|
|
112
|
+
def dispatch_store!(...)
|
|
113
|
+
DispatchBoundary.instance_method(:dispatch_store!).bind_call(self, ...)
|
|
114
|
+
end
|
|
49
115
|
end
|
|
50
116
|
end
|
|
51
117
|
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry-initializer"
|
|
4
|
+
|
|
5
|
+
module Smith
|
|
6
|
+
class Workflow
|
|
7
|
+
module SplitStepPersistence
|
|
8
|
+
class TransactionIdentity
|
|
9
|
+
extend Dry::Initializer
|
|
10
|
+
|
|
11
|
+
param :adapter
|
|
12
|
+
|
|
13
|
+
def self.capture(adapter) = new(adapter).capture
|
|
14
|
+
def self.matches?(adapter, expected) = new(adapter).matches?(expected)
|
|
15
|
+
|
|
16
|
+
def capture
|
|
17
|
+
return unless PersistenceAdapters.supports?(adapter, :transaction_open?)
|
|
18
|
+
return unless adapter.transaction_open?
|
|
19
|
+
|
|
20
|
+
ensure_identity_capability!
|
|
21
|
+
identity = adapter.transaction_identity
|
|
22
|
+
if !identity || identity.to_s.empty?
|
|
23
|
+
raise WorkflowError, "transactional split-step persistence requires an active transaction identity"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
identity.to_s.dup.freeze
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def matches?(expected)
|
|
30
|
+
capture == expected
|
|
31
|
+
rescue StandardError
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def ensure_identity_capability!
|
|
38
|
+
return if PersistenceAdapters.supports?(adapter, :transaction_identity)
|
|
39
|
+
|
|
40
|
+
raise WorkflowError, "transactional split-step persistence requires transaction_identity"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|