Class: KairosMcp::KairosChain::Chain
- Inherits:
-
Object
- Object
- KairosMcp::KairosChain::Chain
- Defined in:
- lib/kairos_mcp/kairos_chain/chain.rb
Overview
INV-D: an append may only add a block to the end of the sequence that is on disk at that moment. The in-memory sequence never replaces the one on disk. When the sequence on disk cannot be determined, nothing is appended. INV-E: the ledger's state is carried by one entrance that every reader passes through (#load_state), not by the block sequence. INV-G: under a backend that does not declare its contract, Chain refuses to append and reads the ledger as unreadable.
Constant Summary collapse
- LOAD_STATES =
The five values #load_state can take. Only :readable and :absent permit an append; only :readable makes #chain / #latest_block meaningful.
%i[absent unreadable empty corrupt readable].freeze
- APPEND_FLAG =
Re-entrancy flag. Thread-level (not fiber-level) on purpose: an append may cross fibers, and a nested append would deadlock on its own flock.
:kairos_chain_append_in_progress- EMPTY_CHAIN =
[].freeze
Instance Attribute Summary collapse
-
#load_state ⇒ Object
readonly
Returns the value of attribute load_state.
Instance Method Summary collapse
-
#add_block(data) ⇒ Block
Append one block to whatever is on disk right now.
-
#chain ⇒ Object
The block sequence, but only when the ledger was readable.
-
#initialize(chain_file: nil, storage_backend: nil) ⇒ Chain
constructor
Constructing the default backend can itself fail — a malformed config scalar, a data directory that cannot be created on a read-only or full mount.
- #latest_block ⇒ Object
-
#storage_type ⇒ Symbol
:file, :sqlite, ...
-
#valid? ⇒ Boolean
An alias for "the ledger was readable".
Constructor Details
#initialize(chain_file: nil, storage_backend: nil) ⇒ Chain
Constructing the default backend can itself fail — a malformed config scalar, a data directory that cannot be created on a read-only or full mount. That construction is therefore deferred into the protected region of classify_disk; doing it here would put it outside every rescue and break "Chain.new never raises".
54 55 56 57 58 59 60 61 62 63 64 65 66 |
# File 'lib/kairos_mcp/kairos_chain/chain.rb', line 54 def initialize(chain_file: nil, storage_backend: nil) # FIX B — NEW CLAIM: Chain.new resolves no paths of its own, so it # returns an object for every input. KairosMcp.blockchain_path here ran # outside every rescue and raised for an unresolvable data directory # (KAIROS_DATA_DIR='~nosuchuser/…' → ArgumentError; deleted CWD → # Errno::ENOENT from Dir.pwd — both measured). @chain_file is dead # (assigned, never read); the argument is kept for call-site # compatibility only. Path resolution now happens solely inside # classify_disk's protected region, where it degrades to :unreadable. @chain_file = chain_file @storage_backend = storage_backend @load_state, @chain = classify_disk end |
Instance Attribute Details
#load_state ⇒ Object (readonly)
Returns the value of attribute load_state.
45 46 47 |
# File 'lib/kairos_mcp/kairos_chain/chain.rb', line 45 def load_state @load_state end |
Instance Method Details
#add_block(data) ⇒ Block
Append one block to whatever is on disk right now.
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/kairos_mcp/kairos_chain/chain.rb', line 89 def add_block(data) # ⓪ flag first, before the lock: a nested append would deadlock on its own # flock. The path question is asked here too, so a backend that answers # a relative path is refused before any file is created. if Thread.current.thread_variable_get(APPEND_FLAG) raise ChainStateError.new( 'append already in progress on this thread (nested append is forbidden)', state: @load_state ) end path = ledger_path Thread.current.thread_variable_set(APPEND_FLAG, true) begin # ① take the key (a file beside the ledger, never the ledger itself) with_lock(path) do # ② read disk and classify state, disk_blocks = classify_disk # ③ refuse anything that is neither readable nor absent unless %i[readable absent].include?(state) raise ChainStateError.new("cannot append: ledger is #{state}", state: state) end # ④ the base is the disk sequence, or a genesis-only sequence base = state == :readable ? disk_blocks : [Block.genesis] # ⑤ build on the base's tail and write base + new new_block = build_block(base.last, data) appended = base + [new_block] storage_backend.save_all_blocks(appended.map(&:to_h)) # ⑥ only a successful write advances this instance's sequence and state @chain = appended @load_state = :readable new_block end ensure # ⑦ lower the flag — reached only by the call that raised it, because ⓪ # raises before this begin block Thread.current.thread_variable_set(APPEND_FLAG, nil) end end |
#chain ⇒ Object
The block sequence, but only when the ledger was readable. Every other state yields a frozen empty array: callers must consult #load_state first.
70 71 72 |
# File 'lib/kairos_mcp/kairos_chain/chain.rb', line 70 def chain @load_state == :readable ? @chain : EMPTY_CHAIN end |
#latest_block ⇒ Object
74 75 76 |
# File 'lib/kairos_mcp/kairos_chain/chain.rb', line 74 def latest_block @load_state == :readable ? @chain.last : nil end |
#storage_type ⇒ Symbol
Returns :file, :sqlite, ... or :unavailable when the backend could not be constructed.
136 137 138 139 140 |
# File 'lib/kairos_mcp/kairos_chain/chain.rb', line 136 def storage_type storage_backend.backend_type rescue StandardError :unavailable end |
#valid? ⇒ Boolean
An alias for "the ledger was readable". Not a separate integrity pass: the four predicates of §3 already ran during classification.
80 81 82 |
# File 'lib/kairos_mcp/kairos_chain/chain.rb', line 80 def valid? @load_state == :readable end |