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.
Files changed (140) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +201 -0
  3. data/README.md +108 -4
  4. data/docs/CONFIGURATION.md +4 -1
  5. data/docs/PATTERNS.md +7 -0
  6. data/docs/PERSISTENCE.md +347 -25
  7. data/lib/smith/agent/lifecycle.rb +19 -10
  8. data/lib/smith/agent/registry/execution_binding_capture.rb +26 -0
  9. data/lib/smith/agent/registry/mutation_boundary.rb +29 -0
  10. data/lib/smith/agent/registry.rb +8 -0
  11. data/lib/smith/agent.rb +24 -2
  12. data/lib/smith/budget/amount_contract.rb +67 -0
  13. data/lib/smith/budget/amount_representation.rb +43 -0
  14. data/lib/smith/budget/decimal_context.rb +18 -0
  15. data/lib/smith/budget/ledger.rb +110 -18
  16. data/lib/smith/budget/ledger_publication.rb +60 -0
  17. data/lib/smith/budget/ledger_state.rb +21 -0
  18. data/lib/smith/budget/ledger_state_transition.rb +72 -0
  19. data/lib/smith/budget/public_state_validator.rb +25 -0
  20. data/lib/smith/budget/reservation.rb +20 -0
  21. data/lib/smith/budget/reservation_contract.rb +34 -0
  22. data/lib/smith/context/observation_masking.rb +3 -1
  23. data/lib/smith/context/state_injection.rb +1 -1
  24. data/lib/smith/doctor/checks/persistence_capabilities.rb +18 -4
  25. data/lib/smith/error.rb +5 -0
  26. data/lib/smith/errors.rb +6 -0
  27. data/lib/smith/exponential_backoff.rb +128 -0
  28. data/lib/smith/persistence_adapters/active_record_exact_predicate.rb +43 -0
  29. data/lib/smith/persistence_adapters/active_record_exact_store.rb +62 -0
  30. data/lib/smith/persistence_adapters/active_record_exact_write.rb +40 -0
  31. data/lib/smith/persistence_adapters/active_record_store.rb +7 -4
  32. data/lib/smith/persistence_adapters/cache_store.rb +1 -0
  33. data/lib/smith/persistence_adapters/memory.rb +16 -1
  34. data/lib/smith/persistence_adapters/redis_client_access.rb +38 -0
  35. data/lib/smith/persistence_adapters/redis_exact_write.rb +44 -0
  36. data/lib/smith/persistence_adapters/redis_store.rb +28 -11
  37. data/lib/smith/persistence_adapters/retry.rb +22 -10
  38. data/lib/smith/persistence_adapters.rb +21 -18
  39. data/lib/smith/persistence_payload_conflict.rb +12 -0
  40. data/lib/smith/tool/budget_enforcement.rb +2 -2
  41. data/lib/smith/types.rb +1 -0
  42. data/lib/smith/version.rb +2 -1
  43. data/lib/smith/workflow/branch_env.rb +1 -1
  44. data/lib/smith/workflow/budget_integration.rb +21 -11
  45. data/lib/smith/workflow/definition_identity/class_methods.rb +48 -0
  46. data/lib/smith/workflow/definition_identity.rb +25 -0
  47. data/lib/smith/workflow/deterministic_step.rb +2 -2
  48. data/lib/smith/workflow/dsl.rb +73 -5
  49. data/lib/smith/workflow/durability.rb +1 -1
  50. data/lib/smith/workflow/evaluator_optimizer.rb +7 -11
  51. data/lib/smith/workflow/execution.rb +16 -18
  52. data/lib/smith/workflow/execution_binding_resolution.rb +16 -0
  53. data/lib/smith/workflow/execution_result_snapshot.rb +118 -0
  54. data/lib/smith/workflow/fanout_execution.rb +1 -1
  55. data/lib/smith/workflow/graph/diagnostic.rb +27 -1
  56. data/lib/smith/workflow/graph/diagnostic_path.rb +24 -0
  57. data/lib/smith/workflow/graph/execution_successors.rb +65 -0
  58. data/lib/smith/workflow/graph/identifier_projection.rb +13 -0
  59. data/lib/smith/workflow/graph/metrics.rb +1 -3
  60. data/lib/smith/workflow/graph/nested_readiness_diagnostics.rb +15 -16
  61. data/lib/smith/workflow/graph/optimization_contract.rb +2 -2
  62. data/lib/smith/workflow/graph/orchestration_contract.rb +3 -3
  63. data/lib/smith/workflow/graph/reachability.rb +19 -31
  64. data/lib/smith/workflow/graph/reachability_diagnostics.rb +17 -1
  65. data/lib/smith/workflow/graph/retry_policy_diagnostic.rb +36 -0
  66. data/lib/smith/workflow/graph/runtime_binding_diagnostics.rb +2 -2
  67. data/lib/smith/workflow/graph/runtime_readiness.rb +3 -44
  68. data/lib/smith/workflow/graph/runtime_readiness_metrics.rb +2 -2
  69. data/lib/smith/workflow/graph/runtime_readiness_report_builder.rb +47 -0
  70. data/lib/smith/workflow/graph/runtime_readiness_traversal.rb +108 -0
  71. data/lib/smith/workflow/graph/state_diagnostics.rb +2 -2
  72. data/lib/smith/workflow/graph/transition_contract.rb +47 -0
  73. data/lib/smith/workflow/graph/transition_contract_attributes.rb +73 -0
  74. data/lib/smith/workflow/graph/transition_contract_configurations.rb +100 -0
  75. data/lib/smith/workflow/graph/transition_diagnostics.rb +54 -12
  76. data/lib/smith/workflow/graph/transition_optimization_configuration.rb +62 -0
  77. data/lib/smith/workflow/graph/transition_snapshot.rb +4 -1
  78. data/lib/smith/workflow/graph/validator.rb +1 -1
  79. data/lib/smith/workflow/graph.rb +62 -4
  80. data/lib/smith/workflow/guardrail_integration.rb +23 -9
  81. data/lib/smith/workflow/identifier.rb +24 -0
  82. data/lib/smith/workflow/message_admission.rb +40 -0
  83. data/lib/smith/workflow/message_admission_boundary.rb +22 -0
  84. data/lib/smith/workflow/message_batch.rb +60 -0
  85. data/lib/smith/workflow/message_value_normalizer.rb +133 -0
  86. data/lib/smith/workflow/nested_execution.rb +10 -0
  87. data/lib/smith/workflow/orchestrator_worker.rb +3 -7
  88. data/lib/smith/workflow/parallel/cancellation_signal.rb +14 -5
  89. data/lib/smith/workflow/parallel/execution_context.rb +113 -0
  90. data/lib/smith/workflow/parallel/nested_execution.rb +124 -0
  91. data/lib/smith/workflow/parallel/root_execution.rb +91 -0
  92. data/lib/smith/workflow/parallel.rb +19 -22
  93. data/lib/smith/workflow/parallel_agent_binding.rb +24 -0
  94. data/lib/smith/workflow/parallel_execution.rb +7 -3
  95. data/lib/smith/workflow/persistence.rb +37 -8
  96. data/lib/smith/workflow/prepared_step.rb +122 -0
  97. data/lib/smith/workflow/prepared_step_dispatch.rb +88 -0
  98. data/lib/smith/workflow/prepared_step_execution_authorization.rb +116 -0
  99. data/lib/smith/workflow/prepared_step_execution_result.rb +49 -0
  100. data/lib/smith/workflow/prepared_step_execution_scope.rb +45 -0
  101. data/lib/smith/workflow/prepared_step_recovery.rb +38 -0
  102. data/lib/smith/workflow/retry_execution.rb +20 -11
  103. data/lib/smith/workflow/split_step_persistence/boundary.rb +35 -22
  104. data/lib/smith/workflow/split_step_persistence/boundary_reset.rb +50 -0
  105. data/lib/smith/workflow/split_step_persistence/canonical_payload_digest.rb +55 -0
  106. data/lib/smith/workflow/split_step_persistence/checkpoint.rb +3 -5
  107. data/lib/smith/workflow/split_step_persistence/definition_boundary.rb +19 -0
  108. data/lib/smith/workflow/split_step_persistence/dispatch_boundary.rb +16 -0
  109. data/lib/smith/workflow/split_step_persistence/dispatch_claim.rb +118 -0
  110. data/lib/smith/workflow/split_step_persistence/dispatch_confirmation.rb +70 -0
  111. data/lib/smith/workflow/split_step_persistence/dispatch_verification.rb +35 -0
  112. data/lib/smith/workflow/split_step_persistence/execution.rb +49 -47
  113. data/lib/smith/workflow/split_step_persistence/execution_authorization.rb +89 -0
  114. data/lib/smith/workflow/split_step_persistence/execution_binding_collector.rb +90 -0
  115. data/lib/smith/workflow/split_step_persistence/execution_binding_snapshot.rb +95 -0
  116. data/lib/smith/workflow/split_step_persistence/execution_result_capture.rb +48 -0
  117. data/lib/smith/workflow/split_step_persistence/execution_verification.rb +65 -0
  118. data/lib/smith/workflow/split_step_persistence/execution_workflow_snapshot.rb +77 -0
  119. data/lib/smith/workflow/split_step_persistence/inheritance.rb +3 -1
  120. data/lib/smith/workflow/split_step_persistence/payloads.rb +7 -3
  121. data/lib/smith/workflow/split_step_persistence/preparation.rb +30 -2
  122. data/lib/smith/workflow/split_step_persistence/preparation_claim.rb +38 -12
  123. data/lib/smith/workflow/split_step_persistence/preparation_payload.rb +25 -0
  124. data/lib/smith/workflow/split_step_persistence/preparation_recovery.rb +10 -2
  125. data/lib/smith/workflow/split_step_persistence/recovery.rb +125 -0
  126. data/lib/smith/workflow/split_step_persistence/recovery_boundary.rb +56 -0
  127. data/lib/smith/workflow/split_step_persistence/recovery_class_methods.rb +14 -0
  128. data/lib/smith/workflow/split_step_persistence/replace_exact_signature.rb +46 -0
  129. data/lib/smith/workflow/split_step_persistence/restart_safe_adapter.rb +42 -0
  130. data/lib/smith/workflow/split_step_persistence/subclass_boundary.rb +50 -0
  131. data/lib/smith/workflow/split_step_persistence/transaction_identity.rb +45 -0
  132. data/lib/smith/workflow/split_step_persistence.rb +36 -0
  133. data/lib/smith/workflow/step_completion.rb +31 -0
  134. data/lib/smith/workflow/string_snapshot.rb +25 -0
  135. data/lib/smith/workflow/transition.rb +122 -133
  136. data/lib/smith/workflow/transition_actionability.rb +22 -0
  137. data/lib/smith/workflow.rb +44 -18
  138. data/lib/smith.rb +41 -2
  139. data/sig/smith.rbs +1 -0
  140. metadata +110 -19
@@ -21,19 +21,50 @@ module Smith
21
21
 
22
22
  def initialize_copy(source)
23
23
  super
24
- @split_step_mutex = Mutex.new
25
- return unless source.instance_variable_get(:@split_step_phase)
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
- private
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
- validate_split_step_marker!(payload, expected: true) if @split_step_phase == :preparing || checkpoint_claimed
95
- if @split_step_phase == :preparing
96
- payload = split_step_preparation_payload(payload)
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
- execution_started = false
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
- claim_split_step_execution_verification!
18
+ result = nil
19
+ execution_thread = Thread.current
20
+ execution_active = false
11
21
  begin
12
- verify_split_step_preparation_available!
13
- activate_split_step_execution!
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
- execution_started ? finish_split_step_execution!(step) : restore_unverified_execution!
27
+ finish_split_step_execution!(step, authorization, execution_thread) if execution_active
18
28
  end
19
- step
29
+ result
20
30
  end
21
31
 
22
- def prepared_persisted_step? = @split_step_mutex.synchronize { @split_step_phase == :prepared }
23
-
24
- private
25
-
26
- def claim_split_step_execution_verification!
32
+ def prepared_persisted_step?
27
33
  @split_step_mutex.synchronize do
28
- raise WorkflowError, "no persisted step is prepared" unless @split_step_phase == :prepared
29
- raise WorkflowError, "the prepared transition no longer matches the workflow" unless
30
- prepared_split_step_transition_matches?
31
-
32
- @split_step_phase = :verifying_execution
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
- def prepared_split_step_transition_matches?
37
- transition = pending_split_step_transition
38
- transition_name = @next_transition_name || transition&.name
39
- transition_name == @split_step_transition_name &&
40
- transition.equal?(@split_step_transition) &&
41
- split_step_transition_signature(transition) == @split_step_transition_signature
42
- end
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 @split_step_phase == :verifying_execution
47
- raise WorkflowError, "the prepared execution claim is no longer active"
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
- @split_step_execution_thread = Thread.current
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
- @split_step_phase = :prepared if @split_step_phase == :verifying_execution
59
- end
60
- end
61
-
62
- def verify_split_step_preparation_available!
63
- payload = @split_step_adapter.fetch(@split_step_persistence_key)
64
- durable = persisted_split_step_payload?(payload, @split_step_preparation_payload)
65
- live = persisted_split_step_payload?(current_split_step_preparation_payload, @split_step_preparation_payload)
66
- stable = split_step_transition_signature(@split_step_transition) == @split_step_transition_signature
67
- return if durable && live && stable
63
+ next unless active_split_step_execution_verification?(verification_token)
68
64
 
69
- raise WorkflowError, "the persisted split-step preparation is no longer available"
70
- end
71
-
72
- def current_split_step_preparation_payload
73
- split_step_preparation_payload(
74
- JSON.generate(to_state.merge(persistence_version: @persistence_version))
75
- )
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