Class: Hecks::Runtime::Registry

Inherits:
Object
  • Object
show all
Includes:
SagaPersistence, Verification
Defined in:
lib/hecks/runtime/registry.rb,
lib/hecks/runtime/registry/verification.rb,
lib/hecks/runtime/registry/saga_persistence.rb

Overview

The collections a boot gathers — bluebooks, hexagons, ports, adapters, worlds, the logs — and how a repository is resolved from them. The wiring gate lives in registry/verification.rb, saga persistence resolution in registry/saga_persistence.rb.

Defined Under Namespace

Modules: SagaPersistence, Verification

Constant Summary

Constants included from Verification

Verification::PER_AGGREGATE_PORTS

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from SagaPersistence

#rehydrate_sagas!, #saga_persistence

Methods included from Verification

#adapter_class, #check_answers, #check_settings, #check_verb, #port_for, #verify!, #verify_default_adapter!, #verify_singleton_port_answers!

Constructor Details

#initialize(root: nil) ⇒ Registry

Returns a new instance of Registry.



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/hecks/runtime/registry.rb', line 20

def initialize(root: nil)
  @root         = root
  @bluebooks    = {}
  @hecksagons   = {}
  @ports = {}
  @adapters     = {}
  @worlds       = {}
  @translations = []
  @event_log    = []
  @reaction_log = []
  @saga_log = []
  # ADDITIVE, RUBY-ONLY — never merged into saga_log/reaction_log.
  # rust/src/kernel/orchestrate.rs ports THOSE two arrays' exact
  # shape byte-for-byte (spec/rust_conformance_spec.rb's own
  # equality check) — a landmine found by reading that spec before
  # touching anything, not by hitting it. These carry the raw
  # inputs a dispatch's own argument binding was resolved from
  # (SagaInterpreter#deliver_saga_dispatch / PolicyInterpreter#
  # trigger_args), for Properties.dispatch_binding_fidelity's own
  # independent re-derivation — a fact neither existing log
  # records at all, so there is nothing here for Rust to have
  # matched or drifted from.
  @saga_dispatch_log   = []
  @policy_dispatch_log = []
  @saga_instances = Hash.new { |h, k| h[k] = {} }
  # GUARDS `saga_instances`' OWN mutation+checkpoint sequence
  # (`SagaInterpreter`'s 4 write points, §7) — the same shape of
  # hazard this codebase's own prior audit already flagged for
  # `Dispatcher#reenter`'s reaction-depth counter (M20: a
  # thread-shared ivar with no lock, since fixed by moving it to
  # `Thread.current`, dispatcher.rb), made meaningfully easier to
  # hit here once a persistence write sits in the same critical
  # section. Held across the in-memory
  # mutation AND the checkpoint write together, never across a
  # saga's own dispatch cascade — see `SagaInterpreter#advance_saga`'s
  # own comment for why that distinction matters (non-reentrant
  # Mutex, recursive re-entry is real).
  @saga_mutex = Mutex.new
  @repositories = {}
  @projection_repositories = {}
  @bluebook_builders = {}
  # EAGER, NOT LAZY — see `#resolved_eras`'s own comment for why. Built
  # here rather than `@resolved_eras ||= {}` on first access so there is
  # no window, post-boot, where two concurrently dispatching threads
  # could race creating this Hash (Hecks/ThreadSharedIvarMutation; the
  # same shape of hazard `Dispatcher#reenter`'s `@reaction_depth` was
  # fixed for). Every WRITE into it still only ever happens at boot,
  # single-threaded (`EraResolver.check!`, a `:pre_verify` boot gate) —
  # this only removes the race on standing up the container itself for
  # a boot with no era-plugin domain at all, whose first touch would
  # otherwise be a live dispatch's own `RepositoryFactory.build` read.
  @resolved_eras = {}
  # EAGER, NOT LAZY — see `#capability_graph`'s own comment for why.
  # `CapabilityGraph.new` only stores the registry reference; there is
  # no reason to defer it, and doing so removes the exact same
  # first-access race `#resolved_eras` above does, while preserving the
  # "same instance every call" identity `spec/runtime/capability_graph_
  # spec.rb` already requires.
  @capability_graph = CapabilityGraph.new(self)
  # `@saga_persistence` itself is eager (see `#saga_persistence`'s own
  # comment) — only the PER-DOMAIN resolution inside it is genuinely
  # expensive and lazy, guarded by this dedicated mutex. NOT the same
  # mutex as `@saga_mutex`: `checkpoint` (saga_interpreter.rb) calls
  # `saga_persistence(domain)` from INSIDE an `@saga_mutex.synchronize`
  # block, so reusing `@saga_mutex` here would deadlock the very first
  # time a saga advanced (a `Mutex` is not reentrant — the exact
  # warning `@saga_mutex`'s own comment already gives for a different
  # reason).
  @saga_persistence = {}
  @saga_persistence_mutex = Mutex.new
end

Instance Attribute Details

#adaptersObject (readonly)

Returns the value of attribute adapters.



16
17
18
# File 'lib/hecks/runtime/registry.rb', line 16

def adapters
  @adapters
end

#bluebooksObject (readonly)

Returns the value of attribute bluebooks.



16
17
18
# File 'lib/hecks/runtime/registry.rb', line 16

def bluebooks
  @bluebooks
end

#capability_graphObject (readonly)

Built eagerly in initialize (see that comment) — this is a plain reader, not a memoizer; spec/runtime/capability_graph_spec.rb asserts the SAME instance comes back every call, which this still gives, just without a lazy ||= race on standing it up.



212
213
214
# File 'lib/hecks/runtime/registry.rb', line 212

def capability_graph
  @capability_graph
end

#event_logObject (readonly)

Returns the value of attribute event_log.



16
17
18
# File 'lib/hecks/runtime/registry.rb', line 16

def event_log
  @event_log
end

#hecksagonsObject (readonly)

Returns the value of attribute hecksagons.



16
17
18
# File 'lib/hecks/runtime/registry.rb', line 16

def hecksagons
  @hecksagons
end

#policy_dispatch_logObject (readonly)

Returns the value of attribute policy_dispatch_log.



16
17
18
# File 'lib/hecks/runtime/registry.rb', line 16

def policy_dispatch_log
  @policy_dispatch_log
end

#portsObject (readonly)

Returns the value of attribute ports.



16
17
18
# File 'lib/hecks/runtime/registry.rb', line 16

def ports
  @ports
end

#reaction_logObject (readonly)

Returns the value of attribute reaction_log.



16
17
18
# File 'lib/hecks/runtime/registry.rb', line 16

def reaction_log
  @reaction_log
end

#resolved_erasObject (readonly)

name => era ordinal as resolved by the boot-time era gate. A lineage adapter writes into ITS OWN era's partition — which, for an old checkout booting a held-but-superseded shape, is not the newest one. The Hash itself is stood up in initialize (see that comment) — this is a plain reader, not a memoizer; Hecks/ThreadSharedIvarMutation is the reason there is no ||= left here to flag.



156
157
158
# File 'lib/hecks/runtime/registry.rb', line 156

def resolved_eras
  @resolved_eras
end

#rootObject (readonly)

Returns the value of attribute root.



16
17
18
# File 'lib/hecks/runtime/registry.rb', line 16

def root
  @root
end

#saga_dispatch_logObject (readonly)

Returns the value of attribute saga_dispatch_log.



16
17
18
# File 'lib/hecks/runtime/registry.rb', line 16

def saga_dispatch_log
  @saga_dispatch_log
end

#saga_instancesObject (readonly)

Returns the value of attribute saga_instances.



16
17
18
# File 'lib/hecks/runtime/registry.rb', line 16

def saga_instances
  @saga_instances
end

#saga_logObject (readonly)

Returns the value of attribute saga_log.



16
17
18
# File 'lib/hecks/runtime/registry.rb', line 16

def saga_log
  @saga_log
end

#saga_mutexObject (readonly)

Returns the value of attribute saga_mutex.



16
17
18
# File 'lib/hecks/runtime/registry.rb', line 16

def saga_mutex
  @saga_mutex
end

#translationsObject (readonly)

Returns the value of attribute translations.



16
17
18
# File 'lib/hecks/runtime/registry.rb', line 16

def translations
  @translations
end

#worldsObject (readonly)

Returns the value of attribute worlds.



16
17
18
# File 'lib/hecks/runtime/registry.rb', line 16

def worlds
  @worlds
end

Instance Method Details

#add_adapter(item) ⇒ Object



129
# File 'lib/hecks/runtime/registry.rb', line 129

def add_adapter(item) = @adapters[item.name] = item

#add_bluebook(item) ⇒ Object

BOOT-TIME-ONLY, SINGLE-THREADED — every add_* below (through add_translation) is called exclusively from Hecks.collect (hecks.rb), which is what Hecks.bluebook/.hecksagon/.port/ .adapter/.world/.translation run inside while a .bluebook/ .hecksagon/.world file is being Kernel.loaded — i.e. strictly during Loader.boot/.boot_files, before dispatcher_for ever hands this registry to a live, multi-threaded caller. Nothing downstream of boot ever calls these — verified by grepping every call site in lib/ and spec/ before writing this — so unlike #resolved_eras/#capability_graph/#saga_persistence (each reachable from live dispatch, and fixed for real above/in registry/saga_persistence.rb) there is no concurrent caller for Hecks/ThreadSharedIvarMutation to actually be warning about here. rubocop:disable Hecks/ThreadSharedIvarMutation



115
# File 'lib/hecks/runtime/registry.rb', line 115

def add_bluebook(item) = @bluebooks[item.name] = item

#add_hecksagon(item) ⇒ Object

MERGED, NOT REPLACED — RECOVERED, not new (see Runtime::Loader .boot's own comment for the provenance). A domain's hecksagon can now load in more than one block for the same domain (base file plus an environments/<name>.hecksagon overlay), and the second block should ADD to what the first declared, not silently discard it.



123
124
125
126
# File 'lib/hecks/runtime/registry.rb', line 123

def add_hecksagon(item)
  existing = @hecksagons[item.domain]
  @hecksagons[item.domain] = existing ? merge_hecksagons(existing, item) : item
end

#add_port(item) ⇒ Object



128
# File 'lib/hecks/runtime/registry.rb', line 128

def add_port(item) = @ports[item.name] = item

#add_translation(item) ⇒ Object



146
147
# File 'lib/hecks/runtime/registry.rb', line 146

def add_translation(item) = @translations << item
# rubocop:enable Hecks/ThreadSharedIvarMutation

#add_world(item) ⇒ Object

MERGED, NOT REPLACED — the same generalization for World that add_hecksagon above recovers for Hecksagon: an environments/<name>.world overlay (or a host-owned tenancy overlay world, same mechanism) can now add or override settings for a domain a base .world file already declared, without restating everything the base file said. Settings merge shallow, keyed exactly the way WorldBuilder already stores them (both the bare verb key and the "verb:adapter" qualified key point at the same resolved hash) — an overlay's key wins over the base's same key; a key only the base declares survives untouched.



141
142
143
144
# File 'lib/hecks/runtime/registry.rb', line 141

def add_world(item)
  existing = @worlds[item.domain]
  @worlds[item.domain] = existing ? merge_worlds(existing, item) : item
end

#bluebook(name) ⇒ Object



158
# File 'lib/hecks/runtime/registry.rb', line 158

def bluebook(name)  = @bluebooks[name.to_s]

#bluebook_builder(name) ⇒ Object

THE BUILDER STAYS OPEN FOR THE LIFE OF THIS REGISTRY, keyed by chapter name — see the comment on BluebookBuilder.build. A chapter split across several files (language/bluebook/*.bluebook, all Hecks.bluebook "Bluebook") needs its declarations to accumulate into ONE builder rather than each file minting its own and silently discarding the one before.



97
98
99
# File 'lib/hecks/runtime/registry.rb', line 97

def bluebook_builder(name)
  @bluebook_builders[name.to_s] ||= yield
end

#hecksagon(name) ⇒ Object



159
# File 'lib/hecks/runtime/registry.rb', line 159

def hecksagon(name) = @hecksagons[name.to_s]

#merge_hecksagons(a, b) ⇒ Object

RECOVERED — see add_hecksagon's own comment for provenance. Concatenates every list-shaped fact; binds in particular is additive because an overlay REBINDING an aggregate (a new persisted_by for the same aggregate/verb) is meant to shadow the base's own bind at resolution time, not erase it outright — Ports::Persistence::BindingPolicy.resolve's own "exactly one authoritative bind" check is what actually catches a genuine double-bind; this merge only concatenates, it does not itself decide which of two binds for the same aggregate wins.



252
253
254
255
256
257
258
259
260
# File 'lib/hecks/runtime/registry.rb', line 252

def merge_hecksagons(a, b)
  Bluebook::Hecksagon.new(
    domain:             a.domain,
    binds:              a.binds + b.binds,
    subscriptions:      a.subscriptions + b.subscriptions,
    framework_members:  a.framework_members + b.framework_members,
    vendored_bluebooks: a.vendored_bluebooks + b.vendored_bluebooks
  )
end

#merge_worlds(a, b) ⇒ Object

RECOVERED AND GENERALIZED — see add_world's own comment. realm/ latest are scalars, so the overlay's value wins when present, else the base's survives; settings is a shallow merge keyed by verb (and "verb:adapter") — an overlay entry for a key the base also declares REPLACES that key's whole resolved hash (the same all-or-nothing shape WorldBuilder#method_missing already builds each entry as), it does not deep-merge field by field within it.



269
270
271
272
273
274
275
276
# File 'lib/hecks/runtime/registry.rb', line 269

def merge_worlds(a, b)
  Bluebook::World.new(
    domain:   a.domain,
    realm:    b.realm || a.realm,
    latest:   b.latest || a.latest,
    settings: a.settings.merge(b.settings)
  )
end

#projection_current?(projection, authoritative) ⇒ Boolean

Returns:

  • (Boolean)


228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/hecks/runtime/registry.rb', line 228

def projection_current?(projection, authoritative)
  projected_entries = projection.entries
  source_entries = authoritative.entries
  return false unless projected_entries.length == source_entries.length
  return false unless projected_entries.zip(source_entries).all? do |projected, source|
    projected.operation == source.operation && projected.id == source.id && projected.state == source.state
  end

  projected_rows = projection.all.map(&:to_h).sort_by { |row| row.fetch(:id).to_s }
  source_rows = authoritative.all.map(&:to_h).sort_by { |row| row.fetch(:id).to_s }
  projected_rows == source_rows
rescue StandardError
  false
end

#read_repository(domain, aggregate) ⇒ Object



214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/hecks/runtime/registry.rb', line 214

def read_repository(domain, aggregate)
  key = [domain.to_s, aggregate.hecks_name]
  binding = Ports::Projection.binds_for(self, domain, aggregate).first
  return repository(domain, aggregate) unless binding

  projection = (@projection_repositories[key] ||= begin
    projection = Ports::Persistence::RepositoryFactory.build(self, domain, aggregate, binding,
                                                             recover: true, settings_verb: Ports::Projection::VERB)
    projection
  end)
  authoritative = repository(domain, aggregate)
  projection_current?(projection, authoritative) ? projection : authoritative
end

#repository(domain, aggregate) ⇒ Object



164
165
166
# File 'lib/hecks/runtime/registry.rb', line 164

def repository(domain, aggregate)
  @repositories[[domain.to_s, aggregate.hecks_name]] ||= Ports::Persistence.repository(self, domain, aggregate)
end

#reset_runtime_state!Object

EVERYTHING A DISPATCH WROTE, CLEARED; NOTHING A BOOT DECLARED, TOUCHED. Bluebooks, hecksagons, ports, adapters, worlds and the resolved eras are what loading the files produced and stay as they are; the logs, the saga instances and the repositories are what running commands against them produced, and go back to exactly what a fresh boot of the same files hands out. Dropping the repositories (rather than emptying each) is deliberate: a fresh boot's own repositories are new adapter instances too, so a Memory adapter starts empty and a durable one sees whatever it persisted — the same reading either way. Sagas rehydrate off that store again, the way Loader.boot_files does after verify!.

What this is for: a test runner that used to boot a runtime per test to get isolation (Behaviors::Expectations.run_one) — ~2s a boot, 76 chess behaviours = two and a half minutes of booting the same two files — can now boot once and reset between tests.

SINGLE-THREADED CALLER, THE SAME REASON THE add_* CLUSTER ABOVE IS EXEMPT — Behaviors::Expectations.run_one is this method's ONLY caller (verified by grep before writing this), and it runs one test at a time: Runner#run maps over tests sequentially, and Behaviors.rspec's generated examples run under RSpec's own single-threaded example loop. No production dispatch path calls this at all — a live Puma worker pool never resets a registry out from under itself mid-flight. rubocop:disable-next Hecks/ThreadSharedIvarMutation



195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/hecks/runtime/registry.rb', line 195

def reset_runtime_state!
  @event_log.clear
  @reaction_log.clear
  @saga_log.clear
  @saga_dispatch_log.clear
  @policy_dispatch_log.clear
  @saga_instances.clear
  @repositories = {}
  @projection_repositories = {}
  rehydrate_sagas!
  self
end

#verbsObject



162
# File 'lib/hecks/runtime/registry.rb', line 162

def verbs = @bluebooks.values.flat_map(&:verbs).sort

#world(name) ⇒ Object



160
# File 'lib/hecks/runtime/registry.rb', line 160

def world(name)     = @worlds[name.to_s]