hecks 7.0.0 → 7.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/postgres_era/lineage/chain_sql.rb +2 -1
- data/lib/hecks/adapters/driven/postgres_era/lineage/provisioning.rb +4 -2
- data/lib/hecks/adapters/driven/postgres_era/lineage/split_sql.rb +16 -8
- data/lib/hecks/adapters/driven/postgres_era/lineage/transform_sql.rb +7 -3
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/era_resolver.rb +2 -1
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/mint_request.rb +2 -1
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/refusals.rb +2 -1
- data/lib/hecks/adapters/driving/cli_runner/bang_rule.rb +3 -0
- data/lib/hecks/adapters/driving/cli_runner/completion_words.rb +2 -0
- data/lib/hecks/adapters/driving/cli_runner/read_context.rb +6 -3
- data/lib/hecks/adapters/driving/cli_runner/renamed_names.rb +2 -1
- data/lib/hecks/adapters/driving/cli_runner/resolution.rb +2 -2
- data/lib/hecks/adapters/driving/cli_runner/resume_words.rb +1 -0
- data/lib/hecks/adapters/driving/cli_runner/run_reference.rb +6 -3
- data/lib/hecks/adapters/driving/cli_runner/search_words.rb +2 -0
- data/lib/hecks/adapters/driving/cli_runner/tour_words.rb +1 -0
- data/lib/hecks/adapters/driving/cli_runner/verb_names.rb +2 -1
- data/lib/hecks/adapters/driving/cli_runner/verbosity.rb +2 -0
- data/lib/hecks/adapters/driving/cli_runner/word_help.rb +40 -0
- data/lib/hecks/adapters/driving/cli_runner.rb +2 -0
- data/lib/hecks/adapters/driving/mcp/progress_sink.rb +4 -2
- data/lib/hecks/adapters/driving/mcp.rb +2 -1
- data/lib/hecks/adapters/driving/mcp_scope/argument_policy.rb +2 -1
- data/lib/hecks/adapters/driving/ruby/aggregate_module/hooks.rb +28 -14
- data/lib/hecks/adapters/driving/ruby/aggregate_module.rb +4 -2
- data/lib/hecks/assembly.rb +18 -4
- data/lib/hecks/behaviors/expectations/adapter_selection.rb +82 -0
- data/lib/hecks/behaviors/expectations/boot.rb +12 -5
- data/lib/hecks/bluebook/acknowledgements.rb +1 -1
- data/lib/hecks/bluebook/command.rb +3 -2
- data/lib/hecks/bluebook/declaration.rb +2 -1
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +9 -1
- data/lib/hecks/bluebook/dsl/bluebook_builder/metadata.rb +6 -4
- data/lib/hecks/bluebook/dsl/bluebook_builder/modules.rb +6 -3
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/correlation_keys.rb +2 -1
- data/lib/hecks/bluebook/dsl/command_builder/folded_sets.rb +4 -2
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +2 -1
- data/lib/hecks/bluebook/hecksagon.rb +6 -3
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +9 -2
- data/lib/hecks/bluebook/meta_validator.rb +1 -1
- data/lib/hecks/bluebook/model_check/aggregate_checks.rb +2 -1
- data/lib/hecks/bluebook/model_check/classification_checks.rb +51 -0
- data/lib/hecks/bluebook/model_check/context_checks.rb +2 -0
- data/lib/hecks/bluebook/model_check/redundant_relationships.rb +2 -1
- data/lib/hecks/ci_speed/adapters/shard_partitioner.adapter +7 -0
- data/lib/hecks/ci_speed/adapters/shard_partitioner.rb +66 -0
- data/lib/hecks/ci_speed/adapters/timings_source.adapter +7 -0
- data/lib/hecks/ci_speed/adapters/timings_source.rb +68 -0
- data/lib/hecks/ci_speed/ci_speed.bluebook +111 -0
- data/lib/hecks/ci_speed/ci_speed.ports.hecksagon +22 -0
- data/lib/hecks/ci_workflows/ci_workflows.bluebook +50 -6
- data/lib/hecks/coder/coder.bluebook +3 -1
- data/lib/hecks/command_names/index.rb +1 -1
- data/lib/hecks/command_names/resolution.rb +15 -5
- data/lib/hecks/composer/toolbox.rb +2 -1
- data/lib/hecks/corpus.rb +4 -0
- data/lib/hecks/doc/reference/readme.rb +1 -1
- data/lib/hecks/framework_bluebooks.rb +3 -2
- data/lib/hecks/fuzzing/differential.rb +1 -1
- data/lib/hecks/fuzzing/era_boundary.rb +1 -1
- data/lib/hecks/fuzzing/isolated_boot/postgres_scratch.rb +1 -1
- data/lib/hecks/fuzzing/persistence_parity.rb +1 -1
- data/lib/hecks/fuzzing/target_capabilities.rb +1 -1
- data/lib/hecks/hecks/adapters/ci_runs.adapter +7 -0
- data/lib/hecks/hecks/adapters/ci_runs.rb +80 -0
- data/lib/hecks/hecks/adapters/claude_agent_list.rb +43 -0
- data/lib/hecks/hecks/adapters/claude_agents.rb +50 -19
- data/lib/hecks/hecks/adapters/claude_jobs.rb +36 -0
- data/lib/hecks/hecks/adapters/codebase/corpus_queries.rb +2 -1
- data/lib/hecks/hecks/adapters/codebase/minitest_runs.rb +4 -2
- data/lib/hecks/hecks/adapters/codebase/run_options.rb +2 -1
- data/lib/hecks/hecks/adapters/codebase/test_suite.rb +3 -19
- data/lib/hecks/hecks/adapters/dispatching/brief_text.rb +5 -3
- data/lib/hecks/hecks/adapters/dispatching/launcher.rb +2 -1
- data/lib/hecks/hecks/adapters/dispatching/plan.rb +8 -4
- data/lib/hecks/hecks/adapters/dispatching/settings.rb +2 -1
- data/lib/hecks/hecks/adapters/dispatching/settler.rb +4 -2
- data/lib/hecks/hecks/adapters/dispatching.rb +6 -3
- data/lib/hecks/hecks/adapters/doctor_reading.rb +4 -2
- data/lib/hecks/hecks/adapters/git_landing/carried.rb +42 -0
- data/lib/hecks/hecks/adapters/git_landing/finisher.rb +2 -1
- data/lib/hecks/hecks/adapters/git_landing/flagged.rb +32 -0
- data/lib/hecks/hecks/adapters/git_landing/tidy.rb +27 -14
- data/lib/hecks/hecks/adapters/git_landing.rb +30 -13
- data/lib/hecks/hecks/adapters/git_leftovers.adapter +7 -0
- data/lib/hecks/hecks/adapters/git_leftovers.rb +49 -0
- data/lib/hecks/hecks/adapters/git_main_pull.rb +2 -2
- data/lib/hecks/hecks/adapters/git_train.rb +9 -5
- data/lib/hecks/hecks/adapters/git_worktree_add.rb +8 -4
- data/lib/hecks/hecks/adapters/github_pull_requests.rb +2 -1
- data/lib/hecks/hecks/adapters/in_process_operations.rb +2 -1
- data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +1 -1
- data/lib/hecks/hecks/adapters/journal_store/readings/composition.rb +2 -1
- data/lib/hecks/hecks/adapters/journal_store/translation_write/shape_edge.rb +4 -2
- data/lib/hecks/hecks/adapters/journal_store/translation_write.rb +4 -2
- data/lib/hecks/hecks/adapters/landed_branches.rb +38 -0
- data/lib/hecks/hecks/adapters/main_boarding.adapter +7 -0
- data/lib/hecks/hecks/adapters/main_boarding.rb +85 -0
- data/lib/hecks/hecks/adapters/main_integration/checks.rb +63 -0
- data/lib/hecks/hecks/adapters/main_integration/command_runner.rb +32 -0
- data/lib/hecks/hecks/adapters/main_integration/range_guard.rb +49 -0
- data/lib/hecks/hecks/adapters/main_integration/repository.rb +83 -0
- data/lib/hecks/hecks/adapters/main_integration/runs.rb +51 -0
- data/lib/hecks/hecks/adapters/main_integration/train.rb +74 -0
- data/lib/hecks/hecks/adapters/main_integration.adapter +7 -0
- data/lib/hecks/hecks/adapters/main_integration.rb +77 -0
- data/lib/hecks/hecks/adapters/main_verification.adapter +7 -0
- data/lib/hecks/hecks/adapters/main_verification.rb +89 -0
- data/lib/hecks/hecks/adapters/push_recorder.adapter +7 -0
- data/lib/hecks/hecks/adapters/push_recorder.rb +78 -0
- data/lib/hecks/hecks/adapters/red_flag_guard.rb +62 -0
- data/lib/hecks/hecks/adapters/red_flag_marker.rb +101 -0
- data/lib/hecks/hecks/adapters/red_flag_report.rb +59 -0
- data/lib/hecks/hecks/adapters/red_flag_survey.rb +65 -0
- data/lib/hecks/hecks/adapters/red_flagging.adapter +7 -0
- data/lib/hecks/hecks/adapters/red_flagging.rb +98 -0
- data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +6 -1
- data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +6 -3
- data/lib/hecks/hecks/adapters/settings_reading/event_tap_row.rb +33 -0
- data/lib/hecks/hecks/adapters/settings_reading.rb +3 -1
- data/lib/hecks/hecks/codebase.behaviors +0 -6
- data/lib/hecks/hecks/custodian.behaviors +22 -1
- data/lib/hecks/hecks/delegate/delegate.bluebook +128 -15
- data/lib/hecks/hecks/deliver/deliver.bluebook +450 -2
- data/lib/hecks/hecks/environments/memory.world +5 -0
- data/lib/hecks/hecks/hecks.contextmap +8 -0
- data/lib/hecks/hecks/hecks.hecksagon +106 -6
- data/lib/hecks/hecks/hecks.world +9 -1
- data/lib/hecks/hecks/migrate/migrate.bluebook +6 -1
- data/lib/hecks/hecks/redflag/redflag.bluebook +249 -0
- data/lib/hecks/hecks/test/test.bluebook +0 -19
- data/lib/hecks/language/adapter.bluebook +13 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +8 -9
- data/lib/hecks/language/sentence/sentence.bluebook +2 -0
- data/lib/hecks/pipeline/pipeline.bluebook +251 -0
- data/lib/hecks/pipeline/pipeline.hecksagon +17 -0
- data/lib/hecks/pipeline_board/adapters/cache.rb +43 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_fill.rb +56 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_git.rb +82 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_github/runs.rb +66 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_github.adapter +7 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_github.rb +70 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_jobs.adapter +7 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_jobs.rb +87 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_journal/chain.rb +40 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_journal/tap.rb +75 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_journal.adapter +7 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_journal.rb +83 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_stages/ci_tiles.rb +85 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_stages/fanout.rb +73 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_stages/gather.rb +57 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_stages/metrics.rb +36 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_stages/strip.rb +102 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_stages/tile_kit.rb +57 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_stages/work_tiles.rb +59 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_stages.adapter +7 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_stages.rb +84 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_tickets.adapter +7 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_tickets.rb +77 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_web/page.html +292 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_web/sequence.html +190 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_web/server.rb +105 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_web/snapshot.rb +57 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_web/stream.rb +59 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_web/wiring.html +536 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_web.adapter +7 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_web.rb +80 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_wiring/details.rb +85 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_wiring/graph.rb +120 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_wiring.adapter +7 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_wiring.rb +36 -0
- data/lib/hecks/pipeline_board/adapters/probe.rb +48 -0
- data/lib/hecks/pipeline_board/pipeline_board.bluebook +144 -0
- data/lib/hecks/pipeline_board/pipeline_board.hecksagon +12 -0
- data/lib/hecks/pipeline_board/pipeline_board.ports.hecksagon +47 -0
- data/lib/hecks/ports/persistence/binding_policy.rb +2 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/compose.rb +4 -3
- data/lib/hecks/ports/persistence/repository_factory.rb +2 -1
- data/lib/hecks/projections/ci_pipeline/table.rb +2 -1
- data/lib/hecks/projections/deploy/lambda/stack/recipes.rb +2 -2
- data/lib/hecks/projections/undo.rb +2 -1
- data/lib/hecks/projector/cli_projector/command_help.rb +5 -4
- data/lib/hecks/projector/cli_projector/renames.rb +4 -2
- data/lib/hecks/projector/cli_projector/usage.rb +4 -3
- data/lib/hecks/projector/exporter/adapters.rb +3 -2
- data/lib/hecks/projector/narrate_projector/use_cases.rb +74 -0
- data/lib/hecks/projector/narrate_projector.rb +10 -3
- data/lib/hecks/quality_control/adapters/main_maintenance.rb +9 -5
- data/lib/hecks/quality_control/adapters/qa_auto_mint.rb +21 -2
- data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +2 -1
- data/lib/hecks/quality_control/cli/qa_fix_main_bug/publishing.rb +8 -4
- data/lib/hecks/quality_control/cli/qa_fix_main_bug.rb +10 -8
- data/lib/hecks/quality_control/cli/qa_generated_domains/promotion.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_main_gate/gate_run.rb +6 -3
- data/lib/hecks/quality_control/cli/qa_main_gate/logging.rb +8 -4
- data/lib/hecks/quality_control/cli/qa_main_gate/shell.rb +4 -2
- data/lib/hecks/quality_control/cli/qa_main_gate/sweeping.rb +8 -4
- data/lib/hecks/quality_control/cli/qa_main_gate/worktree.rb +3 -2
- data/lib/hecks/quality_control/cli/qa_main_gate.rb +10 -5
- data/lib/hecks/quality_control/cli/qa_sweep/all_mode/children.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_sweep.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_tick.rb +6 -3
- data/lib/hecks/quality_control/quality_control.bluebook +11 -11
- data/lib/hecks/quality_control/quality_control.ports.hecksagon +6 -3
- data/lib/hecks/query_ir/touchpoints.rb +2 -1
- data/lib/hecks/refused.rb +2 -1
- data/lib/hecks/release/approval.rb +4 -2
- data/lib/hecks/runtime/event_log.rb +27 -0
- data/lib/hecks/runtime/event_tap.rb +106 -0
- data/lib/hecks/runtime/loader/boot_steps.rb +1 -1
- data/lib/hecks/runtime/loader.rb +35 -19
- data/lib/hecks/runtime/registry/durability_warnings.rb +12 -5
- data/lib/hecks/runtime/registry/runtime_state.rb +4 -1
- data/lib/hecks/runtime/registry/saga_anchor.rb +54 -0
- data/lib/hecks/runtime/registry/saga_durability_rule.rb +66 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +58 -12
- data/lib/hecks/runtime/registry/verification.rb +7 -3
- data/lib/hecks/runtime/saga_interpreter/lifecycle.rb +2 -1
- data/lib/hecks/runtime/saga_interpreter.rb +1 -1
- data/lib/hecks/rust_format.rb +2 -1
- data/lib/hecks/shape_ledger/edge_hygiene.rb +10 -5
- data/lib/hecks/shape_ledger.rb +1 -1
- data/lib/hecks/storehouse/audit.rb +2 -1
- data/lib/hecks/storehouse/identity.rb +6 -3
- data/lib/hecks/storehouse.rb +2 -1
- data/lib/hecks/tickets/bluebook/tickets.bluebook +5 -4
- data/lib/hecks/tools/ci_workflows.rb +2 -1
- data/lib/hecks/tools/document.rb +12 -1
- data/lib/hecks/tools/downstream_model_check.rb +4 -2
- data/lib/hecks/tools/fuzz_sweep/shards.rb +9 -30
- data/lib/hecks/tools/model_check/examination.rb +25 -2
- data/lib/hecks/tools/move_disables.rb +4 -2
- data/lib/hecks/tools/regeneration_run/scratch.rb +2 -1
- data/lib/hecks/tools/rspec_shard_files.rb +12 -15
- data/lib/hecks/tools/sequence_script.rb +1 -1
- data/lib/hecks/tools/shard_planner.rb +65 -0
- data/lib/hecks/tools/stage_progress.rb +2 -1
- data/lib/hecks/tools/stress_concurrency_specs.rb +2 -1
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +8 -9
- data/lib/hecks.rb +5 -2
- data/prebuilt/{13c04bdc5970295b310f0a980d397ba722232c8da99cc1a0cdb383249921e871.marshal → 4b81646f769ae5b1e7457f9b11c48d8bc1093e96a1dd6fb61864065121ca6f01.marshal} +0 -0
- data/prebuilt/{e3857b77f47d9ecc1787b2e1f5136364f3cb3444ef4aea5479f3b98167affea2.marshal → 681890632e3f4a99646623dcae44af36b1bad66c515f5abf67a2460372b9c6fd.marshal} +0 -0
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/parser/src/emit.rs +5 -1
- data/rust/parser/src/ir.rs +2 -0
- data/rust/parser/src/keywords.rs +12 -0
- data/rust/parser/src/main.rs +1 -0
- data/rust/parser/src/parse/adapter.rs +1 -0
- data/rust/parser/src/phrases.rs +16 -0
- data/rust/parser/src/sentence/examples.rs +93 -0
- data/rust/parser/src/sentence/mod.rs +1 -0
- metadata +89 -5
- data/lib/hecks/tools/refresh_rspec_runtime_baseline.rb +0 -166
|
@@ -58,8 +58,10 @@ module Hecks
|
|
|
58
58
|
|
|
59
59
|
# Reads the decision's inputs from the environment `release.yml` gives it.
|
|
60
60
|
#
|
|
61
|
-
# @param env [#[]] `COMMIT_MESSAGE`, or `COMMIT` (the first-parent commit that set the
|
|
62
|
-
#
|
|
61
|
+
# @param env [#[]] `COMMIT_MESSAGE`, or `COMMIT` (the first-parent commit that set the
|
|
62
|
+
# version,
|
|
63
|
+
# whose message is read as {VersionCommit} reads it), `TAG_EXISTS` (true/false),
|
|
64
|
+
# `APPROVED_BY`
|
|
63
65
|
# @param out [#puts] where the words go
|
|
64
66
|
# @return [Integer] the exit status: 0 to go ahead, 1 to refuse
|
|
65
67
|
def check(env: ENV, out: $stdout)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Runtime
|
|
5
|
+
# The registry's list of emitted events. It is an Array in every respect; the addition is two
|
|
6
|
+
# observers, told of each event as it is appended and of each policy reaction as it is logged,
|
|
7
|
+
# which is how an opt-in tap sees what the runtime dispatched.
|
|
8
|
+
class EventLog < Array
|
|
9
|
+
# @return [#call, nil] told of each event appended; nil when nothing watches
|
|
10
|
+
attr_accessor :observer
|
|
11
|
+
|
|
12
|
+
# @return [#call, nil] told of each policy reaction with its record and the identity of the
|
|
13
|
+
# event it answered; nil when nothing watches
|
|
14
|
+
attr_accessor :reaction_observer
|
|
15
|
+
|
|
16
|
+
# Appends an event and tells the observer of it.
|
|
17
|
+
#
|
|
18
|
+
# @param event [Runtime::Event] the emitted event
|
|
19
|
+
# @return [EventLog] self
|
|
20
|
+
def <<(event)
|
|
21
|
+
super
|
|
22
|
+
observer&.call(event)
|
|
23
|
+
self
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "time"
|
|
6
|
+
require_relative "event"
|
|
7
|
+
|
|
8
|
+
module Hecks
|
|
9
|
+
module Runtime
|
|
10
|
+
# An opt-in, append-only local log of what the runtime dispatched: one JSON line per event and
|
|
11
|
+
# one per policy delivery. It exists for the domains that persist nothing (Memory keeps no
|
|
12
|
+
# journal), so a reader such as the PipelineBoard can still see them.
|
|
13
|
+
#
|
|
14
|
+
# It is off unless a world declares it: `event_log "EventTap", path: "...", max_bytes: N`. A
|
|
15
|
+
# client's world declares nothing, so a client writes no file. The boot may hand in an
|
|
16
|
+
# override for a process no hecks command runs (a spec run, and the children it starts): a
|
|
17
|
+
# path replaces the world's, and an empty value switches the tap off, so a spec never writes
|
|
18
|
+
# the owner's log. The log holds names, identities and outcomes, never payloads. A write
|
|
19
|
+
# that fails is dropped: logging never refuses a command.
|
|
20
|
+
class EventTap
|
|
21
|
+
# The world verb that switches the tap on.
|
|
22
|
+
VERB = "event_log"
|
|
23
|
+
|
|
24
|
+
# Bytes after which the log moves to `<path>.1` and starts again.
|
|
25
|
+
DEFAULT_MAX_BYTES = 5_000_000
|
|
26
|
+
|
|
27
|
+
# Switches the tap on for a registry whose Hecks world declares it.
|
|
28
|
+
#
|
|
29
|
+
# @param registry [Runtime::Registry] the registry whose events and reactions are tapped
|
|
30
|
+
# @param override [String, nil] a path in place of the world's; empty switches the tap off
|
|
31
|
+
# @return [EventTap, nil] the tap, nil when no world asks for one
|
|
32
|
+
def self.install(registry, override: nil)
|
|
33
|
+
settings = registry.world("Hecks")&.for_verb(VERB).to_h
|
|
34
|
+
path = path_for(settings, override)
|
|
35
|
+
return nil unless path
|
|
36
|
+
|
|
37
|
+
tap = new(path: path, max_bytes: settings.fetch(:max_bytes, DEFAULT_MAX_BYTES))
|
|
38
|
+
registry.event_log.observer = tap.method(:event)
|
|
39
|
+
registry.event_log.reaction_observer = tap.method(:reaction)
|
|
40
|
+
tap
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Where a world's tap writes: the override when one is given, else the world's path.
|
|
44
|
+
#
|
|
45
|
+
# @param settings [Hash] the Hecks world's `event_log` settings
|
|
46
|
+
# @param override [String, nil] a path in place of the world's; empty switches the tap off
|
|
47
|
+
# @return [String, nil] the log file; nil for no declared tap or an empty override
|
|
48
|
+
def self.path_for(settings, override = nil)
|
|
49
|
+
return nil if settings.to_h[:path].to_s.empty?
|
|
50
|
+
|
|
51
|
+
path = override.nil? ? settings[:path] : override
|
|
52
|
+
path.to_s.empty? ? nil : path
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @param path [String] the log file; `~` is the home directory
|
|
56
|
+
# @param max_bytes [Integer] the size at which the log rotates
|
|
57
|
+
def initialize(path:, max_bytes: DEFAULT_MAX_BYTES)
|
|
58
|
+
@path = File.expand_path(path.to_s)
|
|
59
|
+
@max_bytes = max_bytes.to_i
|
|
60
|
+
@lock = Mutex.new
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Appends one line for an emitted event.
|
|
64
|
+
#
|
|
65
|
+
# @param event [Runtime::Event] the event
|
|
66
|
+
# @return [void]
|
|
67
|
+
def event(event)
|
|
68
|
+
write("kind" => "event", "at" => event.occurred_at, "aggregate" => event.aggregate,
|
|
69
|
+
"id" => event.id.to_s, "event" => event.name)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Appends one line for a policy delivery: which policy reacted to which event, the command it
|
|
73
|
+
# reached and what became of the delivery.
|
|
74
|
+
#
|
|
75
|
+
# @param record [Hash{Symbol => Object}] the reaction's outcome
|
|
76
|
+
# @param identity [String, Integer, nil] the identity of the event instance it answered
|
|
77
|
+
# @return [void]
|
|
78
|
+
def reaction(record, identity)
|
|
79
|
+
write("kind" => "reaction", "at" => Event.stamp, "id" => identity.to_s, "event" => record[:on].to_s,
|
|
80
|
+
"policy" => record[:policy].to_s, "command" => record[:trigger].to_s, "outcome" => outcome(record))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def outcome(record)
|
|
86
|
+
return "delivered" if record[:delivered]
|
|
87
|
+
|
|
88
|
+
"refused: #{record[:reason].to_s.slice(0, 160)}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def write(line)
|
|
92
|
+
@lock.synchronize do
|
|
93
|
+
FileUtils.mkdir_p(File.dirname(@path))
|
|
94
|
+
rotate
|
|
95
|
+
File.open(@path, "a") { |file| file.puts(JSON.generate(line)) }
|
|
96
|
+
end
|
|
97
|
+
rescue SystemCallError, IOError
|
|
98
|
+
nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def rotate
|
|
102
|
+
File.rename(@path, "#{@path}.1") if File.exist?(@path) && File.size(@path) >= @max_bytes
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -49,7 +49,7 @@ module Hecks
|
|
|
49
49
|
registry.verify!
|
|
50
50
|
|
|
51
51
|
gates.register(:saga_rehydration, ->(reg, _dir) { reg.rehydrate_sagas! }, phase: :post_verify) if
|
|
52
|
-
registry.saga_domains.any? { |domain| registry.
|
|
52
|
+
registry.saga_domains.any? { |domain| registry.saga_durable?(domain) }
|
|
53
53
|
gates.run!(:post_verify, registry, directory)
|
|
54
54
|
gates
|
|
55
55
|
end
|
data/lib/hecks/runtime/loader.rb
CHANGED
|
@@ -2,6 +2,7 @@ require_relative "../ports/loading"
|
|
|
2
2
|
require_relative "../ports/persistence"
|
|
3
3
|
require_relative "../ports/dispatch"
|
|
4
4
|
require_relative "dispatcher"
|
|
5
|
+
require_relative "event_tap"
|
|
5
6
|
require_relative "boot_gates"
|
|
6
7
|
require_relative "registry"
|
|
7
8
|
require_relative "loader/boot_steps"
|
|
@@ -23,23 +24,26 @@ module Hecks
|
|
|
23
24
|
# @param path [String] a domain directory to boot
|
|
24
25
|
# @param shared [String, nil] shared ports/adapters root; nil walks up from `path`
|
|
25
26
|
# @param environment [String, nil] environment overlay loaded after the domain; nil loads none
|
|
27
|
+
# @param event_tap [String, nil] a path in place of the Hecks world's event tap; empty
|
|
28
|
+
# switches the tap off, nil leaves the world's where it is
|
|
26
29
|
# @return [Runtime::Dispatcher, Adapters::Lambda::RemoteDispatcher] the booted dispatcher
|
|
27
30
|
# @raise [Errno::ENOENT] if `path` names no domain directory
|
|
28
31
|
# @raise [Runtime::WiringError] if a boot gate finds a wiring problem
|
|
29
|
-
def self.boot(path, shared: nil, environment: nil)
|
|
30
|
-
boot_described(describe(path, shared: shared, environment: environment))
|
|
32
|
+
def self.boot(path, shared: nil, environment: nil, event_tap: nil)
|
|
33
|
+
boot_described(describe(path, shared: shared, environment: environment), event_tap: event_tap)
|
|
31
34
|
end
|
|
32
35
|
|
|
33
36
|
# Finishes a boot from declarations `describe` already loaded: runs every boot gate and builds
|
|
34
37
|
# the dispatcher, without reading the domain's files again.
|
|
35
38
|
#
|
|
36
39
|
# @param described [Described] what `describe` answered for the domain to boot
|
|
40
|
+
# @param event_tap [String, nil] a path in place of the Hecks world's event tap; see `boot`
|
|
37
41
|
# @return [Runtime::Dispatcher, Adapters::Lambda::RemoteDispatcher] the booted dispatcher
|
|
38
42
|
# @raise [Runtime::WiringError] if a boot gate finds a wiring problem
|
|
39
|
-
def self.boot_described(described)
|
|
43
|
+
def self.boot_described(described, event_tap: nil)
|
|
40
44
|
registry = described.registry
|
|
41
45
|
run_boot_gates!(registry, described.directory)
|
|
42
|
-
dispatcher = dispatcher_for(registry)
|
|
46
|
+
dispatcher = dispatcher_for(registry, event_tap)
|
|
43
47
|
redrive_outbox!(dispatcher)
|
|
44
48
|
redrive_sagas!(dispatcher)
|
|
45
49
|
seed_privacy_markings!(dispatcher, registry)
|
|
@@ -95,39 +99,41 @@ module Hecks
|
|
|
95
99
|
dispatcher.redrive_sagas!
|
|
96
100
|
end
|
|
97
101
|
|
|
98
|
-
# Boots the exact bluebook/hecksagon/world files in `paths`, in place
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
# not a copy's).
|
|
102
|
+
# Boots the exact bluebook/hecksagon/world files in `paths`, in place: unlike `boot`, no
|
|
103
|
+
# directory globbing and no copying into a temp dir (a `persisted_by` path must resolve
|
|
104
|
+
# against the real project root, not a copy's).
|
|
102
105
|
#
|
|
103
106
|
# @param paths [Array<String>, String] the exact file paths to boot, in load order
|
|
104
107
|
# @param shared [String, nil] shared ports/adapters root; nil walks up from the first path
|
|
105
|
-
# @param environment [String, nil]
|
|
106
|
-
#
|
|
108
|
+
# @param environment [String, nil] overlay loaded after the named files; nil loads none
|
|
109
|
+
# @param adapters [Array<String>, nil] exactly the `.adapter` files to load, the project's own
|
|
110
|
+
# left unloaded even when this is empty; nil leaves the choice to `load_files`
|
|
111
|
+
# @param event_tap [String, nil] a path in place of the Hecks world's event tap; see `boot`
|
|
107
112
|
# @return [Runtime::Dispatcher, Adapters::Lambda::RemoteDispatcher] the booted dispatcher
|
|
108
113
|
# @raise [Runtime::WiringError] if a boot gate finds a wiring problem
|
|
109
|
-
def self.boot_files(paths, shared: nil, environment: nil)
|
|
114
|
+
def self.boot_files(paths, shared: nil, environment: nil, adapters: nil, event_tap: nil)
|
|
110
115
|
loading = Ports::Loading.bootstrap
|
|
111
116
|
files = Array(paths).map { |path| File.expand_path(path) }
|
|
112
117
|
directory = File.dirname(files.first)
|
|
113
|
-
registry = load_files(loading, files, shared, environment)
|
|
118
|
+
registry = load_files(loading, files + Array(adapters), shared, environment, chosen: !adapters.nil?)
|
|
114
119
|
|
|
115
120
|
run_boot_gates!(registry, directory)
|
|
116
|
-
dispatcher_for(registry)
|
|
121
|
+
dispatcher_for(registry, event_tap)
|
|
117
122
|
end
|
|
118
123
|
|
|
119
124
|
# Loads exactly `files` into a fresh Registry rooted beside their directory, answering it. An
|
|
120
125
|
# `.adapter` among them stands in for the shared root's adapters, which are then left
|
|
121
|
-
# unloaded, so a
|
|
122
|
-
#
|
|
123
|
-
|
|
126
|
+
# unloaded, so a test can answer a port with its own adapter and never construct the domain's
|
|
127
|
+
# real one. `chosen` says the caller chose the adapters itself (a behaviors suite, ADR 0171),
|
|
128
|
+
# so the shared root's stay unloaded even when it chose none.
|
|
129
|
+
def self.load_files(loading, files, shared, overlay, chosen: false)
|
|
124
130
|
directory = File.dirname(files.first)
|
|
125
131
|
root = loading.shared_root(shared, directory)
|
|
126
132
|
registry = Registry.new(root: File.dirname(directory))
|
|
127
133
|
|
|
128
134
|
Hecks.with_registry(registry) do
|
|
129
135
|
loading.load_library
|
|
130
|
-
loading.load_project(root, adapters: files.none? { |file| file.end_with?(".adapter") })
|
|
136
|
+
loading.load_project(root, adapters: !chosen && files.none? { |file| file.end_with?(".adapter") })
|
|
131
137
|
loading.load_selected(files, environment: overlay)
|
|
132
138
|
end
|
|
133
139
|
registry
|
|
@@ -139,15 +145,25 @@ module Hecks
|
|
|
139
145
|
# means a deploy target exists.
|
|
140
146
|
#
|
|
141
147
|
# @param registry [Runtime::Registry] the booted registry
|
|
148
|
+
# @param event_tap [String, nil] a path in place of the Hecks world's event tap; see `boot`
|
|
142
149
|
# @return [Runtime::Dispatcher, Object] the dispatcher the boot hands back
|
|
143
150
|
# @raise [Runtime::WiringError] if the bound adapter cannot build a dispatcher
|
|
144
|
-
def self.dispatcher_for(registry)
|
|
151
|
+
def self.dispatcher_for(registry, event_tap = nil)
|
|
145
152
|
settings = registry.world(registry.bluebooks.keys.first)&.for_verb(Ports::Dispatch::VERB) || {}
|
|
146
|
-
return
|
|
153
|
+
return local_dispatcher(registry, event_tap) unless settings[:adapter]
|
|
147
154
|
|
|
148
155
|
Ports::Dispatch.dispatcher(registry, settings)
|
|
149
156
|
end
|
|
150
157
|
|
|
158
|
+
# The local `Dispatcher`, with the opt-in event log tapped when the Hecks world asks for it.
|
|
159
|
+
#
|
|
160
|
+
# @param registry [Runtime::Registry] the booted registry
|
|
161
|
+
# @param event_tap [String, nil] a path in place of the Hecks world's event tap; see `boot`
|
|
162
|
+
# @return [Runtime::Dispatcher]
|
|
163
|
+
def self.local_dispatcher(registry, event_tap = nil)
|
|
164
|
+
Dispatcher.new(registry).tap { EventTap.install(registry, override: event_tap) }
|
|
165
|
+
end
|
|
166
|
+
|
|
151
167
|
# Installs the Ruby facade over a dispatcher built by hand. See `Hecks.bind_runtime`.
|
|
152
168
|
#
|
|
153
169
|
# @param dispatcher [Runtime::Dispatcher] a dispatcher over a verified registry
|
|
@@ -69,18 +69,25 @@ module Hecks
|
|
|
69
69
|
# A warning, not a refusal: running sagas on a store with no `save_saga` is
|
|
70
70
|
# legitimate on purpose in a fast in-memory test/dev boot.
|
|
71
71
|
def warn_undurable_sagas!(hecksagon)
|
|
72
|
-
|
|
73
|
-
return
|
|
74
|
-
return if bluebook_ir.process_managers.empty?
|
|
75
|
-
return unless saga_persistence(hecksagon.domain).equal?(Ports::Persistence::NULL_SAGA_STORE)
|
|
72
|
+
undurable = undurable_process_managers(hecksagon.domain)
|
|
73
|
+
return if undurable.empty?
|
|
76
74
|
|
|
77
|
-
names =
|
|
75
|
+
names = undurable.map(&:name).join(", ")
|
|
78
76
|
warn "[hecks] #{hecksagon.domain} declares process_manager(s) #{names} but its resolved " \
|
|
79
77
|
"persistence adapter has no save_saga — saga state advances correctly in-process " \
|
|
80
78
|
"and is LOST on restart (no checkpoint, no rehydration, no compensation replay). " \
|
|
81
79
|
"Bind this domain to an adapter that implements save_saga if this process_manager " \
|
|
82
80
|
"must survive a crash."
|
|
83
81
|
end
|
|
82
|
+
|
|
83
|
+
# The process managers whose checkpoints no store keeps: each is held by its starting
|
|
84
|
+
# aggregate's store or by the domain's, and either may be the null store.
|
|
85
|
+
def undurable_process_managers(domain)
|
|
86
|
+
process_managers = bluebook(domain)&.process_managers || []
|
|
87
|
+
process_managers.select do |process_manager|
|
|
88
|
+
saga_persistence(domain, process_manager).equal?(Ports::Persistence::NULL_SAGA_STORE)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
84
91
|
end
|
|
85
92
|
end
|
|
86
93
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require_relative "../event_log"
|
|
2
|
+
|
|
1
3
|
module Hecks
|
|
2
4
|
module Runtime
|
|
3
5
|
class Registry
|
|
@@ -13,7 +15,7 @@ module Hecks
|
|
|
13
15
|
:saga_dispatch_log, :policy_dispatch_log, :cross_domain_log, :saga_instances) do
|
|
14
16
|
# @return [RuntimeState] every log empty and no saga instances
|
|
15
17
|
def self.fresh
|
|
16
|
-
new(
|
|
18
|
+
new(EventLog.new, [], {}.compare_by_identity, [], [], [], [], Hash.new { |hash, key| hash[key] = {} })
|
|
17
19
|
end
|
|
18
20
|
|
|
19
21
|
# Empties every log and drops every saga instance.
|
|
@@ -31,6 +33,7 @@ module Hecks
|
|
|
31
33
|
def log_reaction(record, event)
|
|
32
34
|
reaction_log << record
|
|
33
35
|
reaction_events[record] = event if event
|
|
36
|
+
event_log.reaction_observer&.call(record, event)
|
|
34
37
|
end
|
|
35
38
|
end
|
|
36
39
|
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Runtime
|
|
3
|
+
class Registry
|
|
4
|
+
# Which aggregate's store holds a process manager's checkpoints: the one aggregate whose
|
|
5
|
+
# command emits the event the process manager starts on, when the domain's hecksagon binds
|
|
6
|
+
# that aggregate on its own. A process manager with no such aggregate is held by the
|
|
7
|
+
# domain's store. Mixed into {SagaPersistence}.
|
|
8
|
+
module SagaAnchor
|
|
9
|
+
# @param domain [String, Symbol] the domain declaring the process manager
|
|
10
|
+
# @param process_manager [Bluebook::ProcessManager, String] the process manager or its name
|
|
11
|
+
# @return [Bluebook::Aggregate, nil] the starting aggregate with a bind of its own, or nil
|
|
12
|
+
def saga_anchor(domain, process_manager)
|
|
13
|
+
declared = declared_process_manager(domain, process_manager) or return nil
|
|
14
|
+
starters = starting_aggregates(domain, declared.starts_on.to_s)
|
|
15
|
+
return nil unless starters.size == 1 && bound_on_its_own?(domain, starters.first)
|
|
16
|
+
|
|
17
|
+
starters.first
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
# Every process manager of the domain whose store is not the domain's own, by name.
|
|
23
|
+
def routed_sagas(domain)
|
|
24
|
+
home = saga_persistence(domain)
|
|
25
|
+
stores = process_managers_of(domain).to_h { |declared| [declared.name, saga_persistence(domain, declared)] }
|
|
26
|
+
stores.reject { |_name, store| store.equal?(home) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def process_managers_of(domain) = bluebook(domain)&.process_managers || []
|
|
30
|
+
|
|
31
|
+
def declared_process_manager(domain, process_manager)
|
|
32
|
+
return process_manager if process_manager.respond_to?(:starts_on)
|
|
33
|
+
|
|
34
|
+
process_managers_of(domain).find { |declared| declared.name == process_manager.to_s }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def starting_aggregates(domain, event_name)
|
|
38
|
+
return [] if event_name.empty?
|
|
39
|
+
|
|
40
|
+
bluebook(domain).aggregates.select do |aggregate|
|
|
41
|
+
aggregate.commands.any? { |command| command.emits.map(&:to_s).include?(event_name) }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def bound_on_its_own?(domain, aggregate)
|
|
46
|
+
binds = hecksagon(domain)&.binds || []
|
|
47
|
+
binds.any? do |bind|
|
|
48
|
+
bind.aggregate_name.to_s == aggregate.hecks_name.to_s && bind.verb.to_s == Ports::Persistence::VERB
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Runtime
|
|
3
|
+
class Registry
|
|
4
|
+
# The refusal that keeps a saga as durable as the aggregates it moves (ADR 0170): a process
|
|
5
|
+
# manager whose checkpoints no store keeps may not dispatch to an aggregate a store keeps
|
|
6
|
+
# across a restart. All-Memory stays allowed and keeps its warning. Mixed into {Verification}.
|
|
7
|
+
module SagaDurabilityRule
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
# Reads every declared bluebook, not each hecksagon: a domain kept by its world's
|
|
11
|
+
# `default_adapter` or by an attaching chapter has process managers too.
|
|
12
|
+
def refuse_forgetful_sagas!
|
|
13
|
+
@declared.bluebooks.each_key do |domain|
|
|
14
|
+
process_managers_of(domain).each { |process_manager| refuse_forgetful_saga!(domain, process_manager) }
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def refuse_forgetful_saga!(domain, process_manager)
|
|
19
|
+
return unless saga_persistence(domain, process_manager).equal?(Ports::Persistence::NULL_SAGA_STORE)
|
|
20
|
+
|
|
21
|
+
kept = dispatched_aggregates(domain, process_manager).find { |aggregate| kept_across_restart(domain, aggregate) }
|
|
22
|
+
return unless kept
|
|
23
|
+
|
|
24
|
+
raise WiringError, forgetful_saga_wording(domain, process_manager, kept)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# The domain's own aggregates a process manager's legs and compensations dispatch to.
|
|
28
|
+
def dispatched_aggregates(domain, process_manager)
|
|
29
|
+
names = dispatch_specs(process_manager).map { |spec| aggregate_of_command(spec.command_name) }
|
|
30
|
+
bluebook(domain).aggregates.select { |aggregate| names.include?(aggregate.name.to_s) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def dispatch_specs(process_manager)
|
|
34
|
+
process_manager.handlers.flat_map(&:dispatches).flat_map { |spec| [spec, spec.compensates].compact }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# `"Drawer.Take"` or `"Wire::Drawer.Take"` names the aggregate `Drawer`.
|
|
38
|
+
def aggregate_of_command(command_name)
|
|
39
|
+
path = command_name.to_s.split(".", 2).first
|
|
40
|
+
path.to_s.split("::").last
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The adapter that keeps the aggregate across a restart, or nil. Memory keeps nothing, and
|
|
44
|
+
# a RemoteRuntime's far side keeps the saga as well as the record. An aggregate whose bind
|
|
45
|
+
# cannot be resolved is left to the refusal that resolution raises at dispatch.
|
|
46
|
+
def kept_across_restart(domain, aggregate)
|
|
47
|
+
bind = Ports::Persistence::BindingPolicy.resolve(self, domain, aggregate)
|
|
48
|
+
return nil if bind.adapter == Ports::Persistence::DEFAULT_ADAPTER
|
|
49
|
+
return nil if adapter_class(bind.adapter) <= Ports::Persistence::RemoteRuntime
|
|
50
|
+
|
|
51
|
+
bind.adapter
|
|
52
|
+
rescue WiringError
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def forgetful_saga_wording(domain, process_manager, aggregate)
|
|
57
|
+
"#{domain}'s process_manager #{process_manager.name} dispatches to #{aggregate.name}, which " \
|
|
58
|
+
"#{kept_across_restart(domain, aggregate)} keeps across a restart, but its own checkpoints are kept " \
|
|
59
|
+
"nowhere — after a restart the records stay moved and the saga that would finish or undo the move is " \
|
|
60
|
+
"gone. A forgotten decision: persist the aggregate it starts from (or the domain's first aggregate) " \
|
|
61
|
+
"with an adapter that keeps sagas, or keep the whole domain in Memory on purpose (ADR 0170)."
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -1,37 +1,70 @@
|
|
|
1
1
|
require_relative "../saga_pending_dispatch"
|
|
2
|
+
require_relative "saga_anchor"
|
|
2
3
|
|
|
3
4
|
module Hecks
|
|
4
5
|
module Runtime
|
|
5
6
|
class Registry
|
|
6
7
|
# A domain's sagas persist through whatever adapter its aggregates already use;
|
|
7
8
|
# `save_saga`/`delete_saga`/`each_saga` are optional, `respond_to?`-checked capabilities.
|
|
9
|
+
# A process manager started by an aggregate the hecksagon binds on its own persists with
|
|
10
|
+
# that aggregate's records instead (see {SagaAnchor}), so a saga lives where the record
|
|
11
|
+
# that starts it lives.
|
|
8
12
|
module SagaPersistence
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
include SagaAnchor
|
|
14
|
+
|
|
15
|
+
# The store one domain's sagas, or one of its process managers, checkpoint to. Memoized
|
|
16
|
+
# per store via double-checked locking; never reuse @saga_mutex, see #initialize.
|
|
17
|
+
#
|
|
18
|
+
# @param domain [String, Symbol] the domain whose sagas are checkpointed
|
|
19
|
+
# @param process_manager [Bluebook::ProcessManager, String, nil] one process manager,
|
|
20
|
+
# whose starting aggregate may hold it; the domain's own store when nil
|
|
21
|
+
# @return [#save_saga] the adapter, or the null store when nothing can checkpoint
|
|
22
|
+
def saga_persistence(domain, process_manager = nil)
|
|
23
|
+
anchor = process_manager && saga_anchor(domain, process_manager)
|
|
24
|
+
key = anchor ? "#{domain}::#{anchor.hecks_name}" : domain.to_s
|
|
12
25
|
@saga_persistence[key] || @saga_persistence_mutex.synchronize do
|
|
13
|
-
@saga_persistence[key] ||= resolve_saga_persistence(key)
|
|
26
|
+
@saga_persistence[key] ||= anchor ? anchored_saga_store(domain, anchor) : resolve_saga_persistence(key)
|
|
14
27
|
end
|
|
15
28
|
end
|
|
16
29
|
|
|
17
30
|
def saga_domains = @declared.hecksagons.keys | @declared.bluebooks.keys.select { |domain| default_adapter_for(domain) }
|
|
18
31
|
|
|
32
|
+
# Whether any saga of the domain checkpoints somewhere that survives a restart.
|
|
33
|
+
#
|
|
34
|
+
# @param domain [String, Symbol] the domain whose sagas are asked about
|
|
35
|
+
# @return [Boolean]
|
|
36
|
+
def saga_durable?(domain)
|
|
37
|
+
stores = [saga_persistence(domain), *routed_sagas(domain).values]
|
|
38
|
+
stores.any? { |store| !store.equal?(Ports::Persistence::NULL_SAGA_STORE) }
|
|
39
|
+
end
|
|
40
|
+
|
|
19
41
|
# Boot-time-only (Loader.run_boot_gates!, before dispatch exists); no concurrent
|
|
20
|
-
# writer to race here, unlike saga_interpreter.rb's writes under @saga_mutex.
|
|
42
|
+
# writer to race here, unlike saga_interpreter.rb's writes under @saga_mutex. Each store
|
|
43
|
+
# restores only the process managers that checkpoint to it; the domain's own store
|
|
44
|
+
# restores every other one, including any the bluebook does not declare.
|
|
21
45
|
def rehydrate_sagas!
|
|
22
46
|
saga_domains.each do |domain|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
{ state: state, memory: memory, completed_compensations: completed_compensations || [] }
|
|
27
|
-
stall_saga(process_manager, correlation, pending) if pending
|
|
28
|
-
end
|
|
47
|
+
routed = routed_sagas(domain)
|
|
48
|
+
restore_sagas(saga_persistence(domain)) { |name| !routed.key?(name) }
|
|
49
|
+
routed.values.uniq.each { |store| restore_sagas(store) { |name| routed[name].equal?(store) } }
|
|
29
50
|
end
|
|
30
51
|
self
|
|
31
52
|
end
|
|
32
53
|
|
|
33
54
|
private
|
|
34
55
|
|
|
56
|
+
# Restores the instances a store holds whose process manager the block accepts.
|
|
57
|
+
def restore_sagas(store)
|
|
58
|
+
store.each_saga do |process_manager, correlation, state, memory, completed_compensations = []|
|
|
59
|
+
next unless yield(process_manager.to_s)
|
|
60
|
+
|
|
61
|
+
pending = memory.delete(SAGA_PENDING_DISPATCH_KEY)
|
|
62
|
+
@state.saga_instances[process_manager][correlation] =
|
|
63
|
+
{ state: state, memory: memory, completed_compensations: completed_compensations || [] }
|
|
64
|
+
stall_saga(process_manager, correlation, pending) if pending
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
35
68
|
# A pending marker left by a crash: state was checkpointed but the leg's dispatches did
|
|
36
69
|
# not all settle. Held on the instance for `SagaInterpreter#redrive_stalled`, which runs
|
|
37
70
|
# once the dispatcher exists; Heki's each_saga only symbolizes memory's top-level keys,
|
|
@@ -40,8 +73,10 @@ module Hecks
|
|
|
40
73
|
@state.saga_instances[process_manager][correlation][:stalled] = pending.transform_keys(&:to_sym)
|
|
41
74
|
end
|
|
42
75
|
|
|
76
|
+
# A domain with no hecksagon resolves the same way: its world's `default_adapter` may keep
|
|
77
|
+
# its records, and then it keeps its sagas too (ADR 0170).
|
|
43
78
|
def resolve_saga_persistence(domain)
|
|
44
|
-
anchor =
|
|
79
|
+
anchor = bluebook(domain)&.aggregates&.first
|
|
45
80
|
return Ports::Persistence::NULL_SAGA_STORE unless anchor
|
|
46
81
|
|
|
47
82
|
persisting_adapter(domain, anchor)
|
|
@@ -50,6 +85,17 @@ module Hecks
|
|
|
50
85
|
Ports::Persistence::NULL_SAGA_STORE
|
|
51
86
|
end
|
|
52
87
|
|
|
88
|
+
# The starting aggregate's store when it can checkpoint sagas, else the domain's own.
|
|
89
|
+
# Called under the resolution mutex, so the domain's store is memoized here directly.
|
|
90
|
+
def anchored_saga_store(domain, anchor)
|
|
91
|
+
store = persisting_adapter(domain, anchor)
|
|
92
|
+
store.equal?(Ports::Persistence::NULL_SAGA_STORE) ? domain_saga_store(domain) : store
|
|
93
|
+
rescue WiringError
|
|
94
|
+
domain_saga_store(domain)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def domain_saga_store(domain) = (@saga_persistence[domain.to_s] ||= resolve_saga_persistence(domain.to_s))
|
|
98
|
+
|
|
53
99
|
# The anchor aggregate's own adapter when it can checkpoint sagas, else the null store.
|
|
54
100
|
def persisting_adapter(domain, anchor)
|
|
55
101
|
bind = Ports::Persistence::BindingPolicy.resolve(self, domain, anchor)
|
|
@@ -6,6 +6,7 @@ require_relative "query_verification"
|
|
|
6
6
|
require_relative "plugin_query_verification"
|
|
7
7
|
require_relative "bluebook_verification"
|
|
8
8
|
require_relative "durability_warnings"
|
|
9
|
+
require_relative "saga_durability_rule"
|
|
9
10
|
|
|
10
11
|
module Hecks
|
|
11
12
|
module Runtime
|
|
@@ -19,6 +20,7 @@ module Hecks
|
|
|
19
20
|
include PluginQueryVerification
|
|
20
21
|
include BluebookVerification
|
|
21
22
|
include DurabilityWarnings
|
|
23
|
+
include SagaDurabilityRule
|
|
22
24
|
|
|
23
25
|
# Runs every wiring check against this registry's loaded bluebooks, hecksagons, ports
|
|
24
26
|
# and adapters.
|
|
@@ -31,9 +33,8 @@ module Hecks
|
|
|
31
33
|
refuse_membership_without_identity!
|
|
32
34
|
refuse_unresolved_ports!
|
|
33
35
|
refuse_unanswerable_queries!
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
self
|
|
36
|
+
refuse_forgetful_sagas!
|
|
37
|
+
verify_hecksagons!
|
|
37
38
|
end
|
|
38
39
|
|
|
39
40
|
# Checks that the framework-wide default persistence adapter is itself wired correctly.
|
|
@@ -127,6 +128,9 @@ module Hecks
|
|
|
127
128
|
|
|
128
129
|
private
|
|
129
130
|
|
|
131
|
+
# Every loaded hecksagon's checks, answering the registry.
|
|
132
|
+
def verify_hecksagons! = tap { @declared.hecksagons.each_value { |hecksagon| verify_hecksagon!(hecksagon) } }
|
|
133
|
+
|
|
130
134
|
# One hecksagon's checks: its roles, attachments and ACL, every bind, and the durability
|
|
131
135
|
# warnings for its bluebook.
|
|
132
136
|
def verify_hecksagon!(hecksagon)
|
|
@@ -53,7 +53,8 @@ module Hecks
|
|
|
53
53
|
@registry.saga_mutex.synchronize do
|
|
54
54
|
next false unless @registry.saga_instances[process_manager.name].delete(correlation)
|
|
55
55
|
|
|
56
|
-
@registry.saga_persistence(domain
|
|
56
|
+
@registry.saga_persistence(domain, process_manager)
|
|
57
|
+
.delete_saga(process_manager: process_manager.name, correlation: correlation)
|
|
57
58
|
true
|
|
58
59
|
end
|
|
59
60
|
end
|
|
@@ -168,7 +168,7 @@ module Hecks
|
|
|
168
168
|
def checkpoint(process_manager, correlation, instance, domain, pending: nil)
|
|
169
169
|
memory = deep_copy(instance[:memory])
|
|
170
170
|
memory[SAGA_PENDING_DISPATCH_KEY] = pending if pending
|
|
171
|
-
@registry.saga_persistence(domain).save_saga(
|
|
171
|
+
@registry.saga_persistence(domain, process_manager).save_saga(
|
|
172
172
|
process_manager: process_manager.name, correlation: correlation,
|
|
173
173
|
state: instance[:state], memory: memory,
|
|
174
174
|
completed_compensations: deep_copy_array(instance[:completed_compensations])
|
data/lib/hecks/rust_format.rb
CHANGED
|
@@ -22,7 +22,8 @@ module Hecks
|
|
|
22
22
|
# The most passes `call` makes; a text still changing after them keeps the last result.
|
|
23
23
|
PASSES = 3
|
|
24
24
|
|
|
25
|
-
# Serializes the rustfmt children of this process. In a whole-suite run, worker threads
|
|
25
|
+
# Serializes the rustfmt children of this process. In a whole-suite run, worker threads
|
|
26
|
+
# capturing
|
|
26
27
|
# at once end with `Errno::EBADF` in `Open3.popen_run`, so each capture waits for this lock.
|
|
27
28
|
SPAWN_LOCK = Mutex.new
|
|
28
29
|
|