hecks 1.0.2 → 1.1.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/lib/hecks/adapters/driven/claude_code.rb +3 -3
- data/lib/hecks/adapters/driven/d1.rb +48 -28
- data/lib/hecks/adapters/driven/folder.rb +10 -3
- data/lib/hecks/adapters/driven/google_authentication.rb +2 -4
- data/lib/hecks/adapters/driven/heki/journal.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +3 -5
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +4 -1
- data/lib/hecks/adapters/driven/lambda.rb +2 -2
- data/lib/hecks/adapters/driven/local_storage.adapter +3 -0
- data/lib/hecks/adapters/driven/local_storage.rb +120 -0
- data/lib/hecks/adapters/driven/memory.rb +48 -3
- data/lib/hecks/adapters/driven/postgres/codec.rb +3 -1
- data/lib/hecks/adapters/driven/postgres/outbox.rb +97 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +3 -2
- data/lib/hecks/adapters/driven/postgres.rb +25 -14
- data/lib/hecks/adapters/driven/prism.rb +11 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +45 -20
- data/lib/hecks/adapters/driven/sqlite/codec.rb +3 -1
- data/lib/hecks/adapters/driven/sqlite/projection.rb +17 -1
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +23 -1
- data/lib/hecks/adapters/driven/sqlite.rb +87 -8
- data/lib/hecks/adapters/driven.rb +4 -0
- data/lib/hecks/adapters.rb +4 -0
- data/lib/hecks/behaviors/dsl.rb +7 -0
- data/lib/hecks/behaviors/expectations.rb +32 -4
- data/lib/hecks/behaviors/rspec.rb +2 -0
- data/lib/hecks/behaviors/runner.rb +4 -0
- data/lib/hecks/bluebook/aggregate.rb +6 -5
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +4 -4
- data/lib/hecks/bluebook/assembly/build.rb +6 -3
- data/lib/hecks/bluebook/assembly/contract.rb +5 -1
- data/lib/hecks/bluebook/assembly/contracts.rb +4 -0
- data/lib/hecks/bluebook/assembly/marks.rb +2 -1
- data/lib/hecks/bluebook/assembly.rb +1 -3
- data/lib/hecks/bluebook/attribute.rb +6 -0
- data/lib/hecks/bluebook/behaviour/policy.rb +18 -0
- data/lib/hecks/bluebook/behaviour/process_manager.rb +17 -1
- data/lib/hecks/bluebook/behaviour/traits.rb +1 -1
- data/lib/hecks/bluebook/command.rb +6 -5
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +4 -1
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +524 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +19 -472
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +14 -6
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +5 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +848 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +25 -784
- data/lib/hecks/bluebook/dsl/command_builder.rb +53 -13
- data/lib/hecks/bluebook/dsl/const_shim.rb +6 -0
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +20 -7
- data/lib/hecks/bluebook/dsl/entity_builder.rb +27 -6
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +12 -2
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +8 -1
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +24 -12
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +40 -1
- data/lib/hecks/bluebook/dsl/policy_builder.rb +10 -1
- data/lib/hecks/bluebook/dsl/port_builder.rb +6 -1
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +33 -12
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +56 -12
- data/lib/hecks/bluebook/dsl/query_builder.rb +7 -1
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +11 -3
- data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -1
- data/lib/hecks/bluebook/dsl/translation_builder.rb +13 -2
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +8 -1
- data/lib/hecks/bluebook/dsl/word_gate.rb +5 -0
- data/lib/hecks/bluebook/dsl/world_builder.rb +13 -3
- data/lib/hecks/bluebook/entity.rb +5 -3
- data/lib/hecks/bluebook/expression/ast_json.rb +88 -9
- data/lib/hecks/bluebook/expression/ast_reader.rb +107 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +7 -0
- data/lib/hecks/bluebook/expression/evaluator.rb +64 -11
- data/lib/hecks/bluebook/expression/projection.json +12 -0
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +4 -3
- data/lib/hecks/bluebook/expression/resolver.rb +83 -11
- data/lib/hecks/bluebook/expression.rb +1 -0
- data/lib/hecks/bluebook/hexagon.rb +10 -0
- data/lib/hecks/bluebook/lifecycle.rb +9 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +10 -4
- data/lib/hecks/bluebook/meta_validator/plan.rb +4 -0
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/shapes.rb +3 -3
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +7 -4
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +42 -3
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -1
- data/lib/hecks/bluebook/meta_validator.rb +5 -5
- data/lib/hecks/bluebook/model_check.rb +119 -82
- data/lib/hecks/bluebook/pattern_subset.rb +9 -0
- data/lib/hecks/bluebook/policy.rb +2 -1
- data/lib/hecks/bluebook/process_manager.rb +6 -0
- data/lib/hecks/bluebook/project_register.rb +45 -25
- data/lib/hecks/bluebook/query.rb +4 -0
- data/lib/hecks/bluebook/read_model.rb +2 -2
- data/lib/hecks/bluebook/smoke_test.rb +5 -0
- data/lib/hecks/bluebook/synthesizer.rb +2 -1
- data/lib/hecks/bluebook/translation.rb +3 -0
- data/lib/hecks/bluebook/value_object.rb +2 -2
- data/lib/hecks/codemod.rb +13 -0
- data/lib/hecks/doc/reference.rb +17 -11
- data/lib/hecks/facade/cli_runner.rb +2 -2
- data/lib/hecks/facade/command_request.rb +11 -11
- data/lib/hecks/facade/handle.rb +3 -3
- data/lib/hecks/facade/surface/aggregate_door.rb +52 -38
- data/lib/hecks/facade.rb +4 -0
- data/lib/hecks/forms/app.rb +10 -6
- data/lib/hecks/forms/command_form_renderer.rb +3 -3
- data/lib/hecks/forms/field_renderer.rb +14 -9
- data/lib/hecks/forms/field_shape.rb +7 -2
- data/lib/hecks/forms/html.rb +5 -2
- data/lib/hecks/forms/index_renderer.rb +1 -1
- data/lib/hecks/forms/page.rb +7 -3
- data/lib/hecks/forms/query_form_renderer.rb +3 -2
- data/lib/hecks/forms/record_renderer.rb +22 -16
- data/lib/hecks/forms.rb +4 -0
- data/lib/hecks/fqn.rb +21 -9
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +30 -8
- data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -1
- data/lib/hecks/fuzzing/isolated_boot.rb +14 -6
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +264 -0
- data/lib/hecks/fuzzing/properties/guards.rb +251 -0
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +351 -0
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +99 -0
- data/lib/hecks/fuzzing/properties/querying.rb +206 -0
- data/lib/hecks/fuzzing/properties.rb +41 -1065
- data/lib/hecks/fuzzing/replay.rb +60 -16
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +6 -2
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +2 -1
- data/lib/hecks/fuzzing/sequence_generator.rb +0 -1
- data/lib/hecks/fuzzing/value_generator.rb +2 -2
- data/lib/hecks/grammar/evolve.rb +34 -10
- data/lib/hecks/grammar/expression_operators.json +124 -0
- data/lib/hecks/grammar.rb +15 -5
- data/lib/hecks/ir.rb +9 -0
- data/lib/hecks/language/bluebook/aggregate.bluebook +4 -2
- data/lib/hecks/language/bluebook/command.bluebook +2 -1
- data/lib/hecks/language/bluebook/entity.bluebook +2 -1
- data/lib/hecks/language/bluebook/projection.bluebook +2 -1
- data/lib/hecks/language/bluebook/query.bluebook +4 -2
- data/lib/hecks/language/bluebook/shape.bluebook +2 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -0
- data/lib/hecks/literal.rb +18 -8
- data/lib/hecks/naming.rb +6 -3
- data/lib/hecks/ports/access_control.rb +1 -1
- data/lib/hecks/ports/agent.rb +5 -3
- data/lib/hecks/ports/authentication.rb +3 -2
- data/lib/hecks/ports/authorization.rb +1 -1
- data/lib/hecks/ports/clock.rb +1 -1
- data/lib/hecks/ports/extraction.rb +5 -1
- data/lib/hecks/ports/identity_assignment.rb +1 -1
- data/lib/hecks/ports/identity_generation.rb +1 -1
- data/lib/hecks/ports/identity_resolution.rb +1 -1
- data/lib/hecks/ports/loading.rb +4 -1
- data/lib/hecks/ports/persistence/append_only.rb +40 -4
- data/lib/hecks/ports/persistence/binding_policy.rb +1 -1
- data/lib/hecks/ports/persistence/plugin.rb +3 -0
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -1
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +5 -5
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +14 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +4 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +80 -16
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +23 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +43 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +25 -4
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +49 -9
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +10 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +49 -38
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +23 -13
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +19 -21
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +27 -9
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +9 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +25 -10
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +2 -6
- data/lib/hecks/ports/persistence/plugins/era.rb +3 -1
- data/lib/hecks/ports/persistence/remote_runtime.rb +1 -1
- data/lib/hecks/ports/persistence.rb +7 -3
- data/lib/hecks/ports/projection.rb +13 -5
- data/lib/hecks/ports/query/in_memory.rb +8 -2
- data/lib/hecks/ports/query.rb +1 -3
- data/lib/hecks/ports.rb +2 -0
- data/lib/hecks/projections/diagrams.rb +11 -3
- data/lib/hecks/projections/ir.rb +2 -0
- data/lib/hecks/projections/model/deviations.rb +2 -1
- data/lib/hecks/projections/model.rb +15 -10
- data/lib/hecks/projections/vocabulary.rb +1 -3
- data/lib/hecks/projector/cli_projector.rb +49 -29
- data/lib/hecks/projector/docs_projector.rb +21 -12
- data/lib/hecks/projector/exporter.rb +10 -1
- data/lib/hecks/projector/narrate_projector.rb +83 -25
- data/lib/hecks/projector.rb +2 -2
- data/lib/hecks/query_ir.rb +60 -36
- data/lib/hecks/query_specification/common/authorization_spec.rb +1 -1
- data/lib/hecks/query_specification/common/comparators.rb +4 -0
- data/lib/hecks/query_specification/common/comparison.rb +8 -1
- data/lib/hecks/query_specification/common/dsl.rb +5 -0
- data/lib/hecks/query_specification/common/null_policy.rb +6 -2
- data/lib/hecks/query_specification/common/options.rb +17 -5
- data/lib/hecks/query_specification/field_path.rb +4 -4
- data/lib/hecks/query_specification/read_model/specification.rb +5 -2
- data/lib/hecks/router/namespace_installer.rb +22 -10
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +57 -22
- data/lib/hecks/runtime/command_interpreter.rb +104 -65
- data/lib/hecks/runtime/command_rules/admissibility.rb +26 -8
- data/lib/hecks/runtime/command_rules/arithmetic.rb +35 -14
- data/lib/hecks/runtime/dependency_planning.rb +33 -21
- data/lib/hecks/runtime/dispatcher.rb +41 -7
- data/lib/hecks/runtime/entity_element.rb +25 -6
- data/lib/hecks/runtime/entity_interpreter.rb +28 -15
- data/lib/hecks/runtime/errors.rb +28 -2
- data/lib/hecks/runtime/instance.rb +9 -3
- data/lib/hecks/runtime/interpreting.rb +29 -2
- data/lib/hecks/runtime/loader.rb +21 -0
- data/lib/hecks/runtime/outbox.rb +365 -0
- data/lib/hecks/runtime/policy_interpreter.rb +32 -17
- data/lib/hecks/runtime/query_interpreter.rb +36 -16
- data/lib/hecks/runtime/reaction_invocation.rb +35 -22
- data/lib/hecks/runtime/read_model_interpreter.rb +26 -3
- data/lib/hecks/runtime/reference_hop.rb +1 -1
- data/lib/hecks/runtime/refusal_wording.rb +55 -49
- data/lib/hecks/runtime/registry/saga_persistence.rb +3 -1
- data/lib/hecks/runtime/registry/verification.rb +39 -2
- data/lib/hecks/runtime/registry.rb +21 -11
- data/lib/hecks/runtime/remote_dispatcher.rb +3 -1
- data/lib/hecks/runtime/routing.rb +52 -39
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +7 -7
- data/lib/hecks/runtime/saga_interpreter.rb +119 -90
- data/lib/hecks/runtime/saga_pending_dispatch.rb +6 -3
- data/lib/hecks/runtime/tenant_scope.rb +7 -0
- data/lib/hecks/runtime/value/coercion.rb +207 -48
- data/lib/hecks/runtime/value.rb +1 -1
- data/lib/hecks/runtime.rb +7 -2
- data/lib/hecks/storehouse.rb +15 -8
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +1 -0
- data/lib/hecks.rb +17 -13
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +1 -1
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +1 -1
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +1 -1
- metadata +14 -2
|
@@ -8,6 +8,11 @@ require_relative "saga_pending_dispatch"
|
|
|
8
8
|
|
|
9
9
|
module Hecks
|
|
10
10
|
module Runtime
|
|
11
|
+
# Runs one domain's `process_manager` (saga) declarations against a
|
|
12
|
+
# just-emitted event: begins a new correlated instance on its
|
|
13
|
+
# starts_on event, advances a matching handler's state and dispatches
|
|
14
|
+
# its declared commands, and ends/deletes the instance on ends_on —
|
|
15
|
+
# checkpointing each transition durably before its dispatches run.
|
|
11
16
|
class SagaInterpreter
|
|
12
17
|
include Correlation
|
|
13
18
|
|
|
@@ -28,14 +33,17 @@ module Hecks
|
|
|
28
33
|
@door = door
|
|
29
34
|
end
|
|
30
35
|
|
|
31
|
-
|
|
36
|
+
# `only:` — one process manager, the outbox relay's way of running
|
|
37
|
+
# exactly the consumer a row names (`Runtime::Outbox::Relay#
|
|
38
|
+
# run_consumer`); nil advances every manager the domain declares.
|
|
39
|
+
def advance(event, domain, only: nil)
|
|
32
40
|
bluebook = @registry.bluebook(domain)
|
|
33
41
|
return unless bluebook
|
|
34
42
|
|
|
35
|
-
bluebook.process_managers.each do |
|
|
36
|
-
begin_saga(
|
|
37
|
-
advance_saga(
|
|
38
|
-
end_saga(
|
|
43
|
+
(only ? [only] : bluebook.process_managers).each do |process_manager|
|
|
44
|
+
begin_saga(process_manager, event, domain)
|
|
45
|
+
advance_saga(process_manager, event, domain)
|
|
46
|
+
end_saga(process_manager, event, domain)
|
|
39
47
|
end
|
|
40
48
|
end
|
|
41
49
|
|
|
@@ -63,11 +71,11 @@ module Hecks
|
|
|
63
71
|
# what it always did. The marker exists ONLY in the persisted
|
|
64
72
|
# blob, and only for as long as a dispatch cascade is genuinely
|
|
65
73
|
# in flight for this instance.
|
|
66
|
-
def checkpoint(
|
|
74
|
+
def checkpoint(process_manager, correlation, instance, domain, pending: nil)
|
|
67
75
|
memory = deep_copy(instance[:memory])
|
|
68
76
|
memory[SAGA_PENDING_DISPATCH_KEY] = pending if pending
|
|
69
77
|
@registry.saga_persistence(domain).save_saga(
|
|
70
|
-
process_manager:
|
|
78
|
+
process_manager: process_manager.name, correlation: correlation,
|
|
71
79
|
state: instance[:state], memory: memory,
|
|
72
80
|
completed_compensations: deep_copy_array(instance[:completed_compensations])
|
|
73
81
|
)
|
|
@@ -84,18 +92,18 @@ module Hecks
|
|
|
84
92
|
|
|
85
93
|
def deep_copy(hash) = JSON.parse(JSON.generate(hash), symbolize_names: true)
|
|
86
94
|
|
|
87
|
-
def begin_saga(
|
|
88
|
-
return unless event.name ==
|
|
95
|
+
def begin_saga(process_manager, event, domain)
|
|
96
|
+
return unless event.name == process_manager.starts_on
|
|
89
97
|
|
|
90
|
-
correlation = saga_correlation(
|
|
98
|
+
correlation = saga_correlation(process_manager, event)
|
|
91
99
|
if correlation.to_s.empty?
|
|
92
|
-
@registry.saga_log << { process_manager:
|
|
93
|
-
born: false, reason: "no #{
|
|
100
|
+
@registry.saga_log << { process_manager: process_manager.name, on: event.name,
|
|
101
|
+
born: false, reason: "no #{process_manager.correlates_by} in the payload" }
|
|
94
102
|
return
|
|
95
103
|
end
|
|
96
104
|
|
|
97
105
|
created = @registry.saga_mutex.synchronize do
|
|
98
|
-
next false if @registry.saga_instances[
|
|
106
|
+
next false if @registry.saga_instances[process_manager.name].key?(correlation)
|
|
99
107
|
|
|
100
108
|
# `.dup`, NOT THE SAME OBJECT — a fresh saga's own memory starts
|
|
101
109
|
# as a COPY of the starting event's own payload, never the
|
|
@@ -113,15 +121,15 @@ module Hecks
|
|
|
113
121
|
# still matters: it is what makes the saga's own memory a
|
|
114
122
|
# normal, writable Hash of its own, rather than one write away
|
|
115
123
|
# from crashing every future in-place `remember`.
|
|
116
|
-
instance = { state:
|
|
117
|
-
@registry.saga_instances[
|
|
118
|
-
checkpoint(
|
|
124
|
+
instance = { state: process_manager.states.first, memory: event.payload.dup, completed_compensations: [] }
|
|
125
|
+
@registry.saga_instances[process_manager.name][correlation] = instance
|
|
126
|
+
checkpoint(process_manager, correlation, instance, domain)
|
|
119
127
|
true
|
|
120
128
|
end
|
|
121
129
|
return unless created
|
|
122
130
|
|
|
123
|
-
@registry.saga_log << { process_manager:
|
|
124
|
-
instance: correlation, born: true, state:
|
|
131
|
+
@registry.saga_log << { process_manager: process_manager.name, on: event.name,
|
|
132
|
+
instance: correlation, born: true, state: process_manager.states.first }
|
|
125
133
|
end
|
|
126
134
|
|
|
127
135
|
# THE MUTEX COVERS ONLY THE STATE-CHECK-AND-MUTATE-AND-CHECKPOINT
|
|
@@ -131,38 +139,50 @@ module Hecks
|
|
|
131
139
|
# or itself again) on the SAME thread, and `Mutex` is not
|
|
132
140
|
# reentrant: holding it across that call would deadlock the
|
|
133
141
|
# thread against itself the moment any real chain did that.
|
|
134
|
-
def advance_saga(
|
|
135
|
-
|
|
136
|
-
return unless handler
|
|
142
|
+
def advance_saga(process_manager, event, domain)
|
|
143
|
+
return unless process_manager.handles?(event.name)
|
|
137
144
|
|
|
138
|
-
correlation = saga_correlation(
|
|
145
|
+
correlation = saga_correlation(process_manager, event)
|
|
139
146
|
return if correlation.to_s.empty?
|
|
140
147
|
|
|
141
|
-
record = { process_manager:
|
|
148
|
+
record = { process_manager: process_manager.name, on: event.name, instance: correlation }
|
|
142
149
|
instance = nil
|
|
150
|
+
handler = nil
|
|
143
151
|
pre_state = nil
|
|
144
152
|
|
|
145
153
|
advanced = @registry.saga_mutex.synchronize do
|
|
146
|
-
instance = @registry.saga_instances[
|
|
154
|
+
instance = @registry.saga_instances[process_manager.name][correlation]
|
|
147
155
|
unless instance
|
|
148
156
|
@registry.saga_log << record.merge(advanced: false, reason: "no conversation remembers #{correlation.inspect}")
|
|
149
157
|
next false
|
|
150
158
|
end
|
|
151
|
-
|
|
159
|
+
# THE LEG IS CHOSEN BY (EVENT, CURRENT STATE) — C10.3. Read
|
|
160
|
+
# under the mutex, against the state this instance holds right
|
|
161
|
+
# now, so two legs on the same event from different states each
|
|
162
|
+
# answer exactly when their own state is current.
|
|
163
|
+
handler = process_manager.handler_for(event.name, instance[:state])
|
|
164
|
+
unless handler
|
|
152
165
|
@registry.saga_log << record.merge(advanced: false,
|
|
153
|
-
reason:
|
|
166
|
+
reason: leg_mismatch(process_manager, event.name, instance[:state]))
|
|
154
167
|
next false
|
|
155
168
|
end
|
|
156
169
|
|
|
157
170
|
pre_state = instance[:state]
|
|
158
171
|
instance[:state] = handler.to_state
|
|
159
|
-
checkpoint(
|
|
172
|
+
checkpoint(process_manager, correlation, instance, domain,
|
|
160
173
|
pending: pending_marker(event, handler, pre_state, instance[:state]))
|
|
161
174
|
true
|
|
162
175
|
end
|
|
163
176
|
return unless advanced
|
|
164
177
|
|
|
165
|
-
settle_transition(
|
|
178
|
+
settle_transition(process_manager, event, handler, instance, correlation, domain, record, pre_state)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# "in X, not Y" — the same wording as before C10.3 when one leg
|
|
182
|
+
# answers the event; every leg's own from: listed when several do.
|
|
183
|
+
def leg_mismatch(process_manager, event_name, state)
|
|
184
|
+
expected = process_manager.handlers_for(event_name).map { |h| h.from_state.inspect }.uniq
|
|
185
|
+
"in #{state.inspect}, not #{expected.join(' or ')}"
|
|
166
186
|
end
|
|
167
187
|
|
|
168
188
|
def pending_marker(event, handler, from_state, to_state)
|
|
@@ -175,13 +195,14 @@ module Hecks
|
|
|
175
195
|
# instance at all", `unwind`'s doesn't), then this: log the real
|
|
176
196
|
# observed transition, run the leg's dispatches, and clear the
|
|
177
197
|
# pending marker once that cascade — however it ended — is done.
|
|
178
|
-
def settle_transition(
|
|
198
|
+
def settle_transition(process_manager, event, handler, instance, correlation, domain, record, pre_state,
|
|
199
|
+
drain_compensations: false)
|
|
179
200
|
# `from:`/`to:` are the INSTANCE'S OWN real pre/post state — read
|
|
180
201
|
# back from `instance` itself, never re-derived from `handler.
|
|
181
202
|
# from_state`/`handler.to_state` a second time. `Properties.saga_
|
|
182
203
|
# advances_follow_declared_handlers` (fuzzing/properties.rb) builds
|
|
183
204
|
# its OWN "declared edges" list from this SAME handler object (via
|
|
184
|
-
# `
|
|
205
|
+
# `process_manager.handlers`), so a log entry that just echoed `handler.
|
|
185
206
|
# from_state`/`handler.to_state` back could never disagree with
|
|
186
207
|
# that list no matter what the runtime actually did — the entry
|
|
187
208
|
# and the thing it's checked against would be the identical fact,
|
|
@@ -205,12 +226,12 @@ module Hecks
|
|
|
205
226
|
# construction too, not only by the state guard.
|
|
206
227
|
if drain_compensations
|
|
207
228
|
compensations = instance[:completed_compensations] || []
|
|
208
|
-
deliver_derived_compensation(
|
|
209
|
-
checkpoint(
|
|
229
|
+
deliver_derived_compensation(process_manager, compensations.pop, correlation, domain) until compensations.empty?
|
|
230
|
+
checkpoint(process_manager, correlation, instance, domain)
|
|
210
231
|
end
|
|
211
232
|
|
|
212
233
|
handler.dispatches.each do |spec|
|
|
213
|
-
deliver_saga_dispatch(
|
|
234
|
+
deliver_saga_dispatch(process_manager, spec, event, instance, correlation, domain)
|
|
214
235
|
end
|
|
215
236
|
|
|
216
237
|
# THE CLEAR — guarded by the SAME identity check `end_saga`'s own
|
|
@@ -231,15 +252,17 @@ module Hecks
|
|
|
231
252
|
# now, so this is not the reentrancy hazard `advance_saga`'s own
|
|
232
253
|
# comment warns about.
|
|
233
254
|
@registry.saga_mutex.synchronize do
|
|
234
|
-
next unless @registry.saga_instances[
|
|
255
|
+
next unless @registry.saga_instances[process_manager.name][correlation].equal?(instance)
|
|
235
256
|
|
|
236
|
-
checkpoint(
|
|
257
|
+
checkpoint(process_manager, correlation, instance, domain, pending: nil)
|
|
237
258
|
end
|
|
238
259
|
end
|
|
239
260
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
261
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength -- the checkpoint/
|
|
262
|
+
# dispatch/compensation ledger protocol reads best as one sequence.
|
|
263
|
+
def deliver_saga_dispatch(process_manager, spec, event, instance, correlation, domain)
|
|
264
|
+
args = dispatch_args(process_manager, spec, event, instance, correlation)
|
|
265
|
+
record = { process_manager: process_manager.name, instance: correlation, dispatch: spec.command_name }
|
|
243
266
|
|
|
244
267
|
# THE RAW INPUTS `args` WAS RESOLVED FROM, captured alongside the
|
|
245
268
|
# result — never re-derived from history[:saga_instances] later
|
|
@@ -251,8 +274,9 @@ module Hecks
|
|
|
251
274
|
# no givens/from is skipped by lifecycle_guard_and_given_
|
|
252
275
|
# violations_are_refused.
|
|
253
276
|
unless spec.with_spec.to_a.empty?
|
|
254
|
-
@registry.saga_dispatch_log << { process_manager:
|
|
255
|
-
|
|
277
|
+
@registry.saga_dispatch_log << { process_manager: process_manager.name, instance: correlation,
|
|
278
|
+
dispatch: spec.command_name,
|
|
279
|
+
on: event.name, correlation_head: process_manager.correlation_head,
|
|
256
280
|
event_payload: event.payload, memory: Value.materialize(instance[:memory]),
|
|
257
281
|
with_spec: spec.with_spec, args: args }
|
|
258
282
|
end
|
|
@@ -270,7 +294,7 @@ module Hecks
|
|
|
270
294
|
# past `from_state`.
|
|
271
295
|
@registry.saga_log << record.merge(delivered: false,
|
|
272
296
|
reason: "reaction depth #{@door.max_reaction_depth} reached")
|
|
273
|
-
unwind(
|
|
297
|
+
unwind(process_manager, event, instance, correlation, domain)
|
|
274
298
|
return
|
|
275
299
|
end
|
|
276
300
|
|
|
@@ -292,9 +316,9 @@ module Hecks
|
|
|
292
316
|
# one that failed — never left recorded for a refusal that
|
|
293
317
|
# was never this leg's own to compensate.
|
|
294
318
|
if spec.compensates && !compensation_recorded
|
|
295
|
-
resolved = dispatch_args(
|
|
319
|
+
resolved = dispatch_args(process_manager, spec.compensates, event, instance, correlation)
|
|
296
320
|
instance[:completed_compensations] << { command_name: spec.compensates.command_name, args: resolved }
|
|
297
|
-
checkpoint(
|
|
321
|
+
checkpoint(process_manager, correlation, instance, domain)
|
|
298
322
|
compensation_recorded = true
|
|
299
323
|
end
|
|
300
324
|
|
|
@@ -303,23 +327,23 @@ module Hecks
|
|
|
303
327
|
verb: qualified(spec.command_name, domain),
|
|
304
328
|
projected: args,
|
|
305
329
|
explicit: ReactionInvocation.projection_declared?(spec),
|
|
306
|
-
passthrough: [
|
|
330
|
+
passthrough: [process_manager.correlation_head],
|
|
307
331
|
source_receiver: { aggregate: event.aggregate, identity: event.id }
|
|
308
332
|
)
|
|
309
333
|
@door.reenter(qualified(spec.command_name, domain),
|
|
310
|
-
saga_correlation: {
|
|
334
|
+
saga_correlation: { process_manager.correlation_head.to_s => correlation }, **invocation)
|
|
311
335
|
@registry.saga_log << record.merge(delivered: true)
|
|
312
|
-
rescue *DOMAIN_REFUSALS =>
|
|
313
|
-
unrecord_compensation(instance, correlation, domain,
|
|
336
|
+
rescue *DOMAIN_REFUSALS => e
|
|
337
|
+
unrecord_compensation(instance, correlation, domain, process_manager) if compensation_recorded
|
|
314
338
|
# Same rule as the policy interpreter : a refusal by the target is
|
|
315
339
|
# a recorded outcome, and the leg that raised it UNWINDS — see
|
|
316
340
|
# `unwind`'s own comment for why the procedure runs its
|
|
317
341
|
# compensation here rather than leaving the money (or whatever
|
|
318
342
|
# else a leg moved) sitting out.
|
|
319
|
-
@registry.saga_log << record.merge(delivered: false, reason:
|
|
320
|
-
unwind(
|
|
321
|
-
rescue StandardError =>
|
|
322
|
-
unrecord_compensation(instance, correlation, domain,
|
|
343
|
+
@registry.saga_log << record.merge(delivered: false, reason: e.message)
|
|
344
|
+
unwind(process_manager, event, instance, correlation, domain)
|
|
345
|
+
rescue StandardError => e
|
|
346
|
+
unrecord_compensation(instance, correlation, domain, process_manager) if compensation_recorded
|
|
323
347
|
compensation_recorded = false
|
|
324
348
|
# A DEFECT, not a refusal — see PolicyInterpreter#deliver's own
|
|
325
349
|
# comment for the full reasoning: the same DOMAIN_REFUSALS split,
|
|
@@ -342,19 +366,20 @@ module Hecks
|
|
|
342
366
|
# tag is for.
|
|
343
367
|
attempt += 1
|
|
344
368
|
if attempt <= MAX_DEFECT_RETRIES
|
|
345
|
-
@registry.saga_log << record.merge(delivered: false, reason:
|
|
346
|
-
defect: true, error_class:
|
|
369
|
+
@registry.saga_log << record.merge(delivered: false, reason: e.message,
|
|
370
|
+
defect: true, error_class: e.class.name,
|
|
347
371
|
attempt: attempt, retrying: true)
|
|
348
372
|
retry
|
|
349
373
|
end
|
|
350
374
|
|
|
351
|
-
warn "[hecks] defect in saga #{
|
|
352
|
-
"dispatching #{spec.command_name} after #{attempt} attempts: #{
|
|
353
|
-
@registry.saga_log << record.merge(delivered: false, reason:
|
|
354
|
-
error_class:
|
|
355
|
-
unwind(
|
|
375
|
+
warn "[hecks] defect in saga #{process_manager.name} — instance #{correlation.inspect} " \
|
|
376
|
+
"dispatching #{spec.command_name} after #{attempt} attempts: #{e.class}: #{e.message}"
|
|
377
|
+
@registry.saga_log << record.merge(delivered: false, reason: e.message, defect: true,
|
|
378
|
+
error_class: e.class.name, defect_compensated: true)
|
|
379
|
+
unwind(process_manager, event, instance, correlation, domain)
|
|
356
380
|
end
|
|
357
381
|
end
|
|
382
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
358
383
|
|
|
359
384
|
# THE ROLLBACK HALF of `deliver_saga_dispatch`'s own speculative
|
|
360
385
|
# pre-record (that method's own comment for why it has to be
|
|
@@ -367,9 +392,9 @@ module Hecks
|
|
|
367
392
|
# push and this leg's own return, and a nested refusal that
|
|
368
393
|
# consumed it already popped it itself — this rollback only ever
|
|
369
394
|
# runs for THIS leg's own, still-present entry).
|
|
370
|
-
def unrecord_compensation(instance, correlation, domain,
|
|
395
|
+
def unrecord_compensation(instance, correlation, domain, process_manager)
|
|
371
396
|
instance[:completed_compensations].pop
|
|
372
|
-
checkpoint(
|
|
397
|
+
checkpoint(process_manager, correlation, instance, domain)
|
|
373
398
|
end
|
|
374
399
|
|
|
375
400
|
# A refused leg UNWINDS — the procedure runs the leg declared `on :refused`,
|
|
@@ -388,25 +413,28 @@ module Hecks
|
|
|
388
413
|
# flag to stop it: the state moves to the compensating leg's to_state BEFORE
|
|
389
414
|
# its dispatches run, so a second refusal finds the instance no longer in
|
|
390
415
|
# from_state and records that instead. The check is the guard.
|
|
391
|
-
def unwind(
|
|
392
|
-
|
|
393
|
-
return unless handler && instance
|
|
416
|
+
def unwind(process_manager, event, instance, correlation, domain)
|
|
417
|
+
return unless instance && process_manager.handles?(REFUSED)
|
|
394
418
|
|
|
395
|
-
record = { process_manager:
|
|
419
|
+
record = { process_manager: process_manager.name, on: REFUSED, instance: correlation }
|
|
420
|
+
handler = nil
|
|
396
421
|
pre_state = nil
|
|
397
422
|
|
|
398
423
|
# Same non-reentrancy reasoning as `advance_saga`'s own comment —
|
|
399
424
|
# the mutex covers only the check-and-mutate-and-checkpoint step.
|
|
400
425
|
advanced = @registry.saga_mutex.synchronize do
|
|
401
|
-
|
|
426
|
+
# The compensating leg is selected by (REFUSED, current state)
|
|
427
|
+
# too — C10.3, one rule for every leg.
|
|
428
|
+
handler = process_manager.handler_for(REFUSED, instance[:state])
|
|
429
|
+
unless handler
|
|
402
430
|
@registry.saga_log << record.merge(advanced: false,
|
|
403
|
-
reason:
|
|
431
|
+
reason: leg_mismatch(process_manager, REFUSED, instance[:state]))
|
|
404
432
|
next false
|
|
405
433
|
end
|
|
406
434
|
|
|
407
435
|
pre_state = instance[:state]
|
|
408
436
|
instance[:state] = handler.to_state
|
|
409
|
-
checkpoint(
|
|
437
|
+
checkpoint(process_manager, correlation, instance, domain,
|
|
410
438
|
pending: pending_marker(event, handler, pre_state, instance[:state]))
|
|
411
439
|
true
|
|
412
440
|
end
|
|
@@ -419,7 +447,7 @@ module Hecks
|
|
|
419
447
|
# `drain_compensations: true` — only `unwind`'s own call site
|
|
420
448
|
# fires derived compensation; `advance_saga`'s own call never
|
|
421
449
|
# does.
|
|
422
|
-
settle_transition(
|
|
450
|
+
settle_transition(process_manager, event, handler, instance, correlation, domain, record, pre_state,
|
|
423
451
|
drain_compensations: true)
|
|
424
452
|
end
|
|
425
453
|
|
|
@@ -434,8 +462,8 @@ module Hecks
|
|
|
434
462
|
# distinctly in the log instead of recording it identically to an
|
|
435
463
|
# ordinary failed delivery, and every OTHER completed compensation
|
|
436
464
|
# still queued still gets its own attempt.
|
|
437
|
-
def deliver_derived_compensation(
|
|
438
|
-
record = { process_manager:
|
|
465
|
+
def deliver_derived_compensation(process_manager, entry, correlation, domain)
|
|
466
|
+
record = { process_manager: process_manager.name, instance: correlation, dispatch: entry[:command_name] }
|
|
439
467
|
|
|
440
468
|
attempt = 0
|
|
441
469
|
begin
|
|
@@ -444,36 +472,37 @@ module Hecks
|
|
|
444
472
|
verb: qualified(entry[:command_name], domain),
|
|
445
473
|
projected: entry[:args],
|
|
446
474
|
explicit: true,
|
|
447
|
-
passthrough: [
|
|
475
|
+
passthrough: [process_manager.correlation_head],
|
|
448
476
|
source_receiver: nil
|
|
449
477
|
)
|
|
450
478
|
@door.reenter(qualified(entry[:command_name], domain),
|
|
451
|
-
saga_correlation: {
|
|
479
|
+
saga_correlation: { process_manager.correlation_head.to_s => correlation }, **invocation)
|
|
452
480
|
@registry.saga_log << record.merge(delivered: true, compensation: true)
|
|
453
|
-
rescue *DOMAIN_REFUSALS =>
|
|
454
|
-
@registry.saga_log << record.merge(delivered: false, reason:
|
|
455
|
-
|
|
481
|
+
rescue *DOMAIN_REFUSALS => e
|
|
482
|
+
@registry.saga_log << record.merge(delivered: false, reason: e.message, compensation: true,
|
|
483
|
+
compensation_failed: true)
|
|
484
|
+
rescue StandardError => e
|
|
456
485
|
attempt += 1
|
|
457
486
|
if attempt <= MAX_DEFECT_RETRIES
|
|
458
|
-
@registry.saga_log << record.merge(delivered: false, reason:
|
|
459
|
-
defect: true, error_class:
|
|
487
|
+
@registry.saga_log << record.merge(delivered: false, reason: e.message, compensation: true,
|
|
488
|
+
defect: true, error_class: e.class.name,
|
|
460
489
|
attempt: attempt, retrying: true)
|
|
461
490
|
retry
|
|
462
491
|
end
|
|
463
492
|
|
|
464
|
-
warn "[hecks] defect compensating saga #{
|
|
465
|
-
"dispatching #{entry[:command_name]} after #{attempt} attempts: #{
|
|
466
|
-
@registry.saga_log << record.merge(delivered: false, reason:
|
|
467
|
-
defect: true, error_class:
|
|
493
|
+
warn "[hecks] defect compensating saga #{process_manager.name} — instance #{correlation.inspect} " \
|
|
494
|
+
"dispatching #{entry[:command_name]} after #{attempt} attempts: #{e.class}: #{e.message}"
|
|
495
|
+
@registry.saga_log << record.merge(delivered: false, reason: e.message, compensation: true,
|
|
496
|
+
defect: true, error_class: e.class.name, compensation_failed: true)
|
|
468
497
|
end
|
|
469
498
|
end
|
|
470
499
|
|
|
471
|
-
def dispatch_args(
|
|
500
|
+
def dispatch_args(process_manager, spec, event, instance, correlation)
|
|
472
501
|
ReactionInvocation.resolve_mapping(
|
|
473
502
|
with_spec: spec.with_spec,
|
|
474
503
|
scopes: [["current event payload", event.payload], ["opening event memory", instance[:memory]]],
|
|
475
|
-
bindings: {
|
|
476
|
-
label: "#{
|
|
504
|
+
bindings: { process_manager.correlation_head => correlation },
|
|
505
|
+
label: "#{process_manager.name}'s dispatch #{spec.command_name}"
|
|
477
506
|
)
|
|
478
507
|
end
|
|
479
508
|
|
|
@@ -481,21 +510,21 @@ module Hecks
|
|
|
481
510
|
command_name.include?("::") ? command_name : "#{domain}::#{command_name}"
|
|
482
511
|
end
|
|
483
512
|
|
|
484
|
-
def end_saga(
|
|
485
|
-
return unless event.name ==
|
|
513
|
+
def end_saga(process_manager, event, domain)
|
|
514
|
+
return unless event.name == process_manager.ends_on
|
|
486
515
|
|
|
487
|
-
correlation = saga_correlation(
|
|
516
|
+
correlation = saga_correlation(process_manager, event)
|
|
488
517
|
return if correlation.to_s.empty?
|
|
489
518
|
|
|
490
519
|
ended = @registry.saga_mutex.synchronize do
|
|
491
|
-
next false unless @registry.saga_instances[
|
|
520
|
+
next false unless @registry.saga_instances[process_manager.name].delete(correlation)
|
|
492
521
|
|
|
493
|
-
@registry.saga_persistence(domain).delete_saga(process_manager:
|
|
522
|
+
@registry.saga_persistence(domain).delete_saga(process_manager: process_manager.name, correlation: correlation)
|
|
494
523
|
true
|
|
495
524
|
end
|
|
496
525
|
return unless ended
|
|
497
526
|
|
|
498
|
-
@registry.saga_log << { process_manager:
|
|
527
|
+
@registry.saga_log << { process_manager: process_manager.name, on: event.name,
|
|
499
528
|
instance: correlation, ended: true }
|
|
500
529
|
end
|
|
501
530
|
end
|
|
@@ -37,9 +37,12 @@ module Hecks
|
|
|
37
37
|
# worse defect than the stall it would replace. So this is real,
|
|
38
38
|
# durable, crash-surviving VISIBILITY into exactly what a stalled
|
|
39
39
|
# saga was doing when the process died — the missing half of "no
|
|
40
|
-
# reconciliation pass exists"
|
|
41
|
-
# delivered outbox
|
|
42
|
-
# not a
|
|
40
|
+
# reconciliation pass exists". The full pending → claimed →
|
|
41
|
+
# delivered outbox now exists too (`Runtime::Outbox`, ADR 0053) and
|
|
42
|
+
# is COMPLEMENTARY, not a replacement: the outbox row names the
|
|
43
|
+
# EVENT owed to this saga (and redrives it if the saga never got to
|
|
44
|
+
# claim it); this marker names the saga's own LEG mid-flight after
|
|
45
|
+
# the event was delivered. A crash can leave either standing.
|
|
43
46
|
SAGA_PENDING_DISPATCH_KEY = :__hecks_saga_pending_dispatch__
|
|
44
47
|
end
|
|
45
48
|
end
|
|
@@ -42,6 +42,13 @@ module Hecks
|
|
|
42
42
|
Scoped.new(declared, QuerySpecification::Common::WhereClause.new(field: tenant, op: "eq", value: tenant))
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
# A SimpleDelegator wrapping one declared query/read-model spec
|
|
46
|
+
# with its tenant `eq` where-clause appended to #wheres — the
|
|
47
|
+
# object every query engine actually reads, so nothing beyond
|
|
48
|
+
# TenantScope.apply itself has to know the boundary exists. Never
|
|
49
|
+
# handed back to a caller that might call an IR-level method whose
|
|
50
|
+
# own internal `.wheres` read would bypass this override (see this
|
|
51
|
+
# module's own header for why).
|
|
45
52
|
class Scoped < SimpleDelegator
|
|
46
53
|
def initialize(declared, clause)
|
|
47
54
|
super(declared)
|