igniter-ledger 0.5.2
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 +7 -0
- data/README.md +481 -0
- data/examples/intelligent_ledger/availability_boundary_ledger.rb +1190 -0
- data/examples/intelligent_ledger/availability_deriver.rb +150 -0
- data/examples/intelligent_ledger/availability_ledger.rb +197 -0
- data/examples/intelligent_ledger/ledger_boundary.rb +180 -0
- data/examples/store_poc.rb +45 -0
- data/exe/igniter-ledger-server +111 -0
- data/exe/igniter-store-server +6 -0
- data/ext/igniter_store_native/Cargo.toml +28 -0
- data/ext/igniter_store_native/extconf.rb +6 -0
- data/ext/igniter_store_native/src/fact.rs +303 -0
- data/ext/igniter_store_native/src/fact_log.rs +180 -0
- data/ext/igniter_store_native/src/file_backend.rs +91 -0
- data/ext/igniter_store_native/src/lib.rs +55 -0
- data/lib/igniter/ledger.rb +7 -0
- data/lib/igniter/store/access_path.rb +84 -0
- data/lib/igniter/store/change_event.rb +65 -0
- data/lib/igniter/store/changefeed_buffer.rb +585 -0
- data/lib/igniter/store/codecs.rb +253 -0
- data/lib/igniter/store/contractable_receipt_sink.rb +172 -0
- data/lib/igniter/store/fact.rb +121 -0
- data/lib/igniter/store/fact_log.rb +103 -0
- data/lib/igniter/store/file_backend.rb +269 -0
- data/lib/igniter/store/http_adapter.rb +413 -0
- data/lib/igniter/store/igniter_store.rb +838 -0
- data/lib/igniter/store/mcp_adapter.rb +403 -0
- data/lib/igniter/store/native.rb +80 -0
- data/lib/igniter/store/network_backend.rb +159 -0
- data/lib/igniter/store/protocol/handlers/access_path_handler.rb +38 -0
- data/lib/igniter/store/protocol/handlers/command_handler.rb +59 -0
- data/lib/igniter/store/protocol/handlers/derivation_handler.rb +27 -0
- data/lib/igniter/store/protocol/handlers/effect_handler.rb +65 -0
- data/lib/igniter/store/protocol/handlers/history_handler.rb +24 -0
- data/lib/igniter/store/protocol/handlers/projection_handler.rb +41 -0
- data/lib/igniter/store/protocol/handlers/relation_handler.rb +43 -0
- data/lib/igniter/store/protocol/handlers/store_handler.rb +24 -0
- data/lib/igniter/store/protocol/handlers/subscription_handler.rb +24 -0
- data/lib/igniter/store/protocol/interpreter.rb +447 -0
- data/lib/igniter/store/protocol/receipt.rb +96 -0
- data/lib/igniter/store/protocol/sync_profile.rb +53 -0
- data/lib/igniter/store/protocol/wire_envelope.rb +214 -0
- data/lib/igniter/store/protocol.rb +27 -0
- data/lib/igniter/store/read_cache.rb +163 -0
- data/lib/igniter/store/schema_graph.rb +248 -0
- data/lib/igniter/store/segmented_file_backend.rb +699 -0
- data/lib/igniter/store/server_config.rb +55 -0
- data/lib/igniter/store/server_logger.rb +64 -0
- data/lib/igniter/store/server_metrics.rb +222 -0
- data/lib/igniter/store/store_server.rb +597 -0
- data/lib/igniter/store/subscription_registry.rb +73 -0
- data/lib/igniter/store/tbackend_adapter_descriptor.rb +307 -0
- data/lib/igniter/store/tcp_adapter.rb +127 -0
- data/lib/igniter/store/wire_protocol.rb +42 -0
- data/lib/igniter/store.rb +64 -0
- data/lib/igniter-ledger.rb +4 -0
- data/lib/igniter-store.rb +5 -0
- metadata +212 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Igniter
|
|
7
|
+
module Store
|
|
8
|
+
class TBackendAdapterDescriptor
|
|
9
|
+
KIND = "ledger_tbackend_adapter_descriptor"
|
|
10
|
+
DIAGNOSTICS_KIND = "ledger_tbackend_adapter_descriptor_diagnostics"
|
|
11
|
+
ADAPTER_KIND = "ledger_open_protocol"
|
|
12
|
+
ADAPTER_VERSION = "0.1.0"
|
|
13
|
+
CONTRACT_VERSION = "tbackend.v0"
|
|
14
|
+
PROTOCOL = "igniter_store"
|
|
15
|
+
EVIDENCE_MODE = "receipt_required"
|
|
16
|
+
DEFAULT_ADAPTER_REF = "adapter:ledger-open-protocol/package-descriptor-v0"
|
|
17
|
+
|
|
18
|
+
READ_OPS = %w[read query fact_ref].freeze
|
|
19
|
+
APPEND_OPS = %w[write write_fact append].freeze
|
|
20
|
+
REPLAY_OPS = %w[replay sync_hub_profile].freeze
|
|
21
|
+
SNAPSHOT_OPS = %w[metadata_snapshot descriptor_snapshot sync_hub_profile].freeze
|
|
22
|
+
TBACKEND_OPS = {
|
|
23
|
+
"read" => READ_OPS,
|
|
24
|
+
"append" => APPEND_OPS,
|
|
25
|
+
"replay" => REPLAY_OPS,
|
|
26
|
+
"snapshot" => SNAPSHOT_OPS,
|
|
27
|
+
"compact" => %w[compact],
|
|
28
|
+
"subscribe" => %w[subscribe]
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
CURSOR_POLICY = {
|
|
32
|
+
ordered: "forward",
|
|
33
|
+
cursor_kinds: ["timestamp"],
|
|
34
|
+
truncation_reported: true,
|
|
35
|
+
tie_breaker: "timestamp_then_fact_id_required"
|
|
36
|
+
}.freeze
|
|
37
|
+
|
|
38
|
+
NON_AUTHORIZATION = {
|
|
39
|
+
runtime_binding: false,
|
|
40
|
+
ledger_reads: false,
|
|
41
|
+
ledger_writes: false,
|
|
42
|
+
ledger_append: false,
|
|
43
|
+
ledger_replay: false,
|
|
44
|
+
ledger_compact: false,
|
|
45
|
+
ledger_subscribe: false,
|
|
46
|
+
migration_execution: false
|
|
47
|
+
}.freeze
|
|
48
|
+
|
|
49
|
+
attr_reader :metadata_snapshot,
|
|
50
|
+
:descriptor_snapshot,
|
|
51
|
+
:payload
|
|
52
|
+
|
|
53
|
+
def self.build(metadata_snapshot:, descriptor_snapshot:, schema_fingerprint:, adapter_ref: nil,
|
|
54
|
+
ledger_protocol_ops: nil)
|
|
55
|
+
new(
|
|
56
|
+
metadata_snapshot: metadata_snapshot,
|
|
57
|
+
descriptor_snapshot: descriptor_snapshot,
|
|
58
|
+
schema_fingerprint: schema_fingerprint,
|
|
59
|
+
adapter_ref: adapter_ref,
|
|
60
|
+
ledger_protocol_ops: ledger_protocol_ops
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def initialize(metadata_snapshot:, descriptor_snapshot:, schema_fingerprint:, adapter_ref: nil,
|
|
65
|
+
ledger_protocol_ops: nil)
|
|
66
|
+
@metadata_snapshot = normalize_hash(metadata_snapshot, :metadata_snapshot)
|
|
67
|
+
@descriptor_snapshot = normalize_hash(descriptor_snapshot, :descriptor_snapshot)
|
|
68
|
+
@payload = build_payload(
|
|
69
|
+
schema_fingerprint: require_value(:schema_fingerprint, schema_fingerprint),
|
|
70
|
+
adapter_ref: adapter_ref || DEFAULT_ADAPTER_REF,
|
|
71
|
+
ledger_protocol_ops: normalize_ops(
|
|
72
|
+
ledger_protocol_ops || metadata_snapshot_value(:ledger_protocol_ops) || metadata_snapshot_value(:protocol_ops)
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
freeze
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def descriptor_hash
|
|
79
|
+
payload.fetch(:descriptor_hash)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def descriptor_registry_hash
|
|
83
|
+
payload.fetch(:descriptor_registry_hash)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def ledger_protocol_ops
|
|
87
|
+
payload.fetch(:ledger_protocol_ops)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def supported_tbackend_ops
|
|
91
|
+
payload.fetch(:supported_tbackend_ops)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def hook_methods
|
|
95
|
+
payload.fetch(:hook_methods)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def capabilities
|
|
99
|
+
payload.fetch(:capabilities)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def history_axes
|
|
103
|
+
payload.fetch(:history_axes)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def cursor_policy
|
|
107
|
+
payload.fetch(:cursor_policy)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def diagnostics(requirement = {})
|
|
111
|
+
requirement = normalize_hash(requirement, :requirement)
|
|
112
|
+
missing_ops = missing(:required_ops, :supported_tbackend_ops, requirement)
|
|
113
|
+
missing_hook_methods = missing(:required_hook_methods, :hook_methods, requirement)
|
|
114
|
+
missing_capabilities = missing(:required_capabilities, :capabilities, requirement)
|
|
115
|
+
missing_axes = missing(:history_axes, :history_axes, requirement)
|
|
116
|
+
schema_fingerprint_match = schema_fingerprint_match?(requirement)
|
|
117
|
+
blocked = missing_ops.any? ||
|
|
118
|
+
missing_hook_methods.any? ||
|
|
119
|
+
missing_capabilities.any? ||
|
|
120
|
+
missing_axes.any? ||
|
|
121
|
+
!schema_fingerprint_match
|
|
122
|
+
|
|
123
|
+
deep_freeze(
|
|
124
|
+
kind: DIAGNOSTICS_KIND,
|
|
125
|
+
status: blocked ? "blocked" : "ok",
|
|
126
|
+
missing_ops: missing_ops,
|
|
127
|
+
missing_hook_methods: missing_hook_methods,
|
|
128
|
+
missing_capabilities: missing_capabilities,
|
|
129
|
+
missing_axes: missing_axes,
|
|
130
|
+
schema_fingerprint_match: schema_fingerprint_match,
|
|
131
|
+
descriptor_hash: descriptor_hash,
|
|
132
|
+
descriptor_registry_hash: descriptor_registry_hash
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def to_h
|
|
137
|
+
payload
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
private
|
|
141
|
+
|
|
142
|
+
def build_payload(schema_fingerprint:, adapter_ref:, ledger_protocol_ops:)
|
|
143
|
+
supported_tbackend_ops = derive_supported_tbackend_ops(ledger_protocol_ops)
|
|
144
|
+
hook_methods = []
|
|
145
|
+
capabilities = []
|
|
146
|
+
history_axes = []
|
|
147
|
+
|
|
148
|
+
if history_read_supported?(supported_tbackend_ops)
|
|
149
|
+
hook_methods << "read_as_of"
|
|
150
|
+
capabilities << "history_read"
|
|
151
|
+
history_axes << "valid_time"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
if bihistory_supported?
|
|
155
|
+
hook_methods << "bihistory_at"
|
|
156
|
+
capabilities << "bihistory_read"
|
|
157
|
+
history_axes << "transaction_time"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
descriptor_registry_hash = self.class.canonical_hash(
|
|
161
|
+
metadata_snapshot: metadata_snapshot,
|
|
162
|
+
descriptor_snapshot: descriptor_snapshot
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
payload_without_hash = {
|
|
166
|
+
kind: KIND,
|
|
167
|
+
adapter_kind: ADAPTER_KIND,
|
|
168
|
+
adapter_ref: adapter_ref,
|
|
169
|
+
adapter_version: ADAPTER_VERSION,
|
|
170
|
+
contract_version: CONTRACT_VERSION,
|
|
171
|
+
protocol: PROTOCOL,
|
|
172
|
+
protocol_schema_version: protocol_schema_version,
|
|
173
|
+
ledger_protocol_ops: ledger_protocol_ops,
|
|
174
|
+
supported_tbackend_ops: supported_tbackend_ops,
|
|
175
|
+
hook_methods: hook_methods,
|
|
176
|
+
capabilities: capabilities,
|
|
177
|
+
history_axes: history_axes,
|
|
178
|
+
cursor_policy: CURSOR_POLICY,
|
|
179
|
+
schema_fingerprint: schema_fingerprint,
|
|
180
|
+
descriptor_registry_hash: descriptor_registry_hash,
|
|
181
|
+
evidence_mode: EVIDENCE_MODE,
|
|
182
|
+
source_snapshots: {
|
|
183
|
+
metadata_snapshot_present: true,
|
|
184
|
+
descriptor_snapshot_present: true
|
|
185
|
+
},
|
|
186
|
+
non_authorization: NON_AUTHORIZATION
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
deep_freeze(payload_without_hash.merge(
|
|
190
|
+
descriptor_hash: self.class.canonical_hash(payload_without_hash)
|
|
191
|
+
))
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def protocol_schema_version
|
|
195
|
+
metadata_snapshot[:schema_version] || descriptor_snapshot[:schema_version]
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def derive_supported_tbackend_ops(ledger_protocol_ops)
|
|
199
|
+
TBACKEND_OPS.filter_map do |tbackend_op, ledger_ops|
|
|
200
|
+
tbackend_op if (ledger_protocol_ops & ledger_ops).any?
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def history_read_supported?(supported_tbackend_ops)
|
|
205
|
+
supported_tbackend_ops.include?("read") && store_descriptors.any? do |descriptor|
|
|
206
|
+
descriptor_capabilities(descriptor).include?("as_of_read")
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def bihistory_supported?
|
|
211
|
+
history_descriptors.any?
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def store_descriptors
|
|
215
|
+
Array(metadata_snapshot[:stores]) + Array(descriptor_snapshot[:stores])
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def history_descriptors
|
|
219
|
+
Array(metadata_snapshot[:histories]) + Array(descriptor_snapshot[:histories])
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def descriptor_capabilities(descriptor)
|
|
223
|
+
Array(descriptor[:capabilities]).map(&:to_s)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def missing(requirement_key, descriptor_key, requirement)
|
|
227
|
+
required = Array(requirement[requirement_key]).map(&:to_s)
|
|
228
|
+
actual = Array(payload.fetch(descriptor_key)).map(&:to_s)
|
|
229
|
+
required - actual
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def schema_fingerprint_match?(requirement)
|
|
233
|
+
required = requirement[:schema_fingerprint]
|
|
234
|
+
required.nil? || required == payload.fetch(:schema_fingerprint)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def metadata_snapshot_value(key)
|
|
238
|
+
metadata_snapshot[key]
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def normalize_ops(value)
|
|
242
|
+
Array(value).map(&:to_s).uniq.freeze
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def require_value(name, value)
|
|
246
|
+
raise ArgumentError, "#{name} is required" if value.nil?
|
|
247
|
+
|
|
248
|
+
value
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def normalize_hash(value, name)
|
|
252
|
+
raise ArgumentError, "#{name} must be a hash" unless value.respond_to?(:to_h)
|
|
253
|
+
|
|
254
|
+
value.to_h.each_with_object({}) do |(key, entry), hash|
|
|
255
|
+
normalized_key = key.respond_to?(:to_sym) ? key.to_sym : key
|
|
256
|
+
hash[normalized_key] = normalize_value(entry)
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def normalize_value(value)
|
|
261
|
+
case value
|
|
262
|
+
when Hash
|
|
263
|
+
normalize_hash(value, :value)
|
|
264
|
+
when Array
|
|
265
|
+
value.map { |entry| normalize_value(entry) }
|
|
266
|
+
else
|
|
267
|
+
value
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def deep_freeze(value)
|
|
272
|
+
case value
|
|
273
|
+
when Array
|
|
274
|
+
value.map { |entry| deep_freeze(entry) }.freeze
|
|
275
|
+
when Hash
|
|
276
|
+
value.transform_values { |entry| deep_freeze(entry) }.freeze
|
|
277
|
+
else
|
|
278
|
+
value.freeze
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
class << self
|
|
283
|
+
def canonical_hash(value)
|
|
284
|
+
"sha256:#{Digest::SHA256.hexdigest(JSON.generate(canonical_value(value)))}"
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
private
|
|
288
|
+
|
|
289
|
+
def canonical_value(value)
|
|
290
|
+
case value
|
|
291
|
+
when Hash
|
|
292
|
+
value.keys.map(&:to_s).sort.each_with_object({}) do |key, hash|
|
|
293
|
+
original_key = value.key?(key.to_sym) ? key.to_sym : key
|
|
294
|
+
hash[key] = canonical_value(value.fetch(original_key))
|
|
295
|
+
end
|
|
296
|
+
when Array
|
|
297
|
+
value.map { |entry| canonical_value(entry) }
|
|
298
|
+
when Symbol
|
|
299
|
+
value.to_s
|
|
300
|
+
else
|
|
301
|
+
value
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
require "json"
|
|
5
|
+
require_relative "wire_protocol"
|
|
6
|
+
|
|
7
|
+
module Igniter
|
|
8
|
+
module Store
|
|
9
|
+
# TCP transport adapter for the Igniter Store Open Protocol.
|
|
10
|
+
#
|
|
11
|
+
# Exposes Protocol::Interpreter over a framed TCP (or Unix socket) connection
|
|
12
|
+
# using the same WireProtocol CRC32 framing as the legacy StoreServer path.
|
|
13
|
+
# Each request frame carries a WireEnvelope JSON object; each response frame
|
|
14
|
+
# carries the WireEnvelope response JSON object.
|
|
15
|
+
#
|
|
16
|
+
# This is the new envelope dispatch path (default port 7401). The legacy
|
|
17
|
+
# StoreServer path (port 7400) is separate and unchanged.
|
|
18
|
+
#
|
|
19
|
+
# Usage:
|
|
20
|
+
# adapter = TCPAdapter.new(interpreter: interpreter, port: 7401)
|
|
21
|
+
# adapter.start_async
|
|
22
|
+
# adapter.wait_until_ready
|
|
23
|
+
# adapter.stop
|
|
24
|
+
class TCPAdapter
|
|
25
|
+
include WireProtocol
|
|
26
|
+
|
|
27
|
+
def initialize(interpreter:, port: 7401, host: "127.0.0.1", transport: :tcp)
|
|
28
|
+
@interpreter = interpreter
|
|
29
|
+
@port = port
|
|
30
|
+
@host = host
|
|
31
|
+
@transport = transport
|
|
32
|
+
@stopped = false
|
|
33
|
+
@threads = []
|
|
34
|
+
@threads_mutex = Mutex.new
|
|
35
|
+
@ready_mutex = Mutex.new
|
|
36
|
+
@ready_cond = ConditionVariable.new
|
|
37
|
+
@ready = false
|
|
38
|
+
@server = build_server
|
|
39
|
+
# Socket is bound during initialize — signal ready immediately so that
|
|
40
|
+
# wait_until_ready is race-free even before start is called.
|
|
41
|
+
signal_ready
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Runs the accept loop in the current thread (blocks until #stop).
|
|
45
|
+
def start
|
|
46
|
+
until @stopped
|
|
47
|
+
begin
|
|
48
|
+
socket = @server.accept
|
|
49
|
+
rescue IOError, Errno::EBADF
|
|
50
|
+
break
|
|
51
|
+
end
|
|
52
|
+
t = Thread.new(socket) { |s| handle_client(s) }
|
|
53
|
+
@threads_mutex.synchronize { @threads << t }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Starts the accept loop in a background thread. Returns self.
|
|
58
|
+
def start_async
|
|
59
|
+
@thread = Thread.new do
|
|
60
|
+
Thread.current.abort_on_exception = false
|
|
61
|
+
start
|
|
62
|
+
end
|
|
63
|
+
wait_until_ready
|
|
64
|
+
self
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Blocks until the server socket is bound and ready.
|
|
68
|
+
def wait_until_ready(timeout: 2)
|
|
69
|
+
@ready_mutex.synchronize do
|
|
70
|
+
deadline = Time.now + timeout
|
|
71
|
+
until @ready
|
|
72
|
+
remaining = deadline - Time.now
|
|
73
|
+
raise "TCPAdapter did not become ready within #{timeout}s" if remaining <= 0
|
|
74
|
+
@ready_cond.wait(@ready_mutex, remaining)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
self
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def stop
|
|
81
|
+
@stopped = true
|
|
82
|
+
@server&.close rescue nil
|
|
83
|
+
@thread&.join(2) rescue nil
|
|
84
|
+
@threads_mutex.synchronize { @threads.each { |t| t.join(1) rescue nil } }
|
|
85
|
+
self
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def bind_address
|
|
89
|
+
@transport == :unix ? @host : "#{@host}:#{@port}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def build_server
|
|
95
|
+
case @transport
|
|
96
|
+
when :tcp then TCPServer.new(@host, @port)
|
|
97
|
+
when :unix then UNIXServer.new(@host)
|
|
98
|
+
else raise ArgumentError, "Unknown transport: #{@transport.inspect}. Use :tcp or :unix"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def signal_ready
|
|
103
|
+
@ready_mutex.synchronize { @ready = true; @ready_cond.broadcast }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def handle_client(socket)
|
|
107
|
+
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, true) rescue nil
|
|
108
|
+
loop do
|
|
109
|
+
body = read_frame(socket)
|
|
110
|
+
break unless body
|
|
111
|
+
|
|
112
|
+
envelope = JSON.parse(body, symbolize_names: true)
|
|
113
|
+
result = @interpreter.wire.dispatch(envelope)
|
|
114
|
+
socket.write(encode_frame(JSON.generate(result)))
|
|
115
|
+
end
|
|
116
|
+
rescue IOError, Errno::ECONNRESET, Errno::EPIPE
|
|
117
|
+
# client disconnected cleanly
|
|
118
|
+
rescue => e
|
|
119
|
+
# unexpected error — log and close
|
|
120
|
+
$stderr.puts "TCPAdapter: client error: #{e.class}: #{e.message}" rescue nil
|
|
121
|
+
ensure
|
|
122
|
+
socket.close rescue nil
|
|
123
|
+
@threads_mutex.synchronize { @threads.delete(Thread.current) }
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zlib"
|
|
4
|
+
|
|
5
|
+
module Igniter
|
|
6
|
+
module Store
|
|
7
|
+
# Shared CRC32-framed binary encoding for WAL files and network transport.
|
|
8
|
+
#
|
|
9
|
+
# Frame layout:
|
|
10
|
+
# [4 bytes BE uint32: body_len][body_len bytes: body][4 bytes BE uint32: CRC32(body)]
|
|
11
|
+
#
|
|
12
|
+
# A frame with a mismatched CRC or truncated body signals corruption /
|
|
13
|
+
# connection loss — the caller should stop reading.
|
|
14
|
+
module WireProtocol
|
|
15
|
+
FRAME_HEADER_SIZE = 4
|
|
16
|
+
FRAME_CRC_SIZE = 4
|
|
17
|
+
|
|
18
|
+
def encode_frame(body)
|
|
19
|
+
body_b = body.b
|
|
20
|
+
[body_b.bytesize].pack("N") << body_b << [Zlib.crc32(body)].pack("N")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Reads one frame from +io+. Returns the body String on success, nil on
|
|
24
|
+
# truncation or CRC mismatch.
|
|
25
|
+
def read_frame(io)
|
|
26
|
+
header = io.read(FRAME_HEADER_SIZE)
|
|
27
|
+
return nil if header.nil? || header.bytesize < FRAME_HEADER_SIZE
|
|
28
|
+
|
|
29
|
+
len = header.unpack1("N")
|
|
30
|
+
body = io.read(len)
|
|
31
|
+
return nil if body.nil? || body.bytesize < len
|
|
32
|
+
|
|
33
|
+
crc_bytes = io.read(FRAME_CRC_SIZE)
|
|
34
|
+
return nil if crc_bytes.nil? || crc_bytes.bytesize < FRAME_CRC_SIZE
|
|
35
|
+
|
|
36
|
+
return nil unless Zlib.crc32(body) == crc_bytes.unpack1("N")
|
|
37
|
+
|
|
38
|
+
body
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Store
|
|
5
|
+
NATIVE = false unless const_defined?(:NATIVE) # overwritten by native.rb when extension loads
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
require_relative "store/native" # attempt to load Rust extension
|
|
10
|
+
require_relative "store/access_path"
|
|
11
|
+
require_relative "store/fact" # Ruby Struct + Fact.from_h (always loaded)
|
|
12
|
+
require_relative "store/fact_log" # Ruby FactLog + native all_facts patch
|
|
13
|
+
require_relative "store/wire_protocol"
|
|
14
|
+
require_relative "store/file_backend" # Ruby FileBackend + native snapshot patch
|
|
15
|
+
require_relative "store/server_config"
|
|
16
|
+
require_relative "store/server_logger"
|
|
17
|
+
require_relative "store/subscription_registry"
|
|
18
|
+
require_relative "store/change_event"
|
|
19
|
+
require_relative "store/changefeed_buffer"
|
|
20
|
+
require_relative "store/network_backend"
|
|
21
|
+
require_relative "store/store_server"
|
|
22
|
+
require_relative "store/igniter_store"
|
|
23
|
+
require_relative "store/read_cache"
|
|
24
|
+
require_relative "store/schema_graph"
|
|
25
|
+
require_relative "store/protocol"
|
|
26
|
+
require_relative "store/http_adapter"
|
|
27
|
+
require_relative "store/tcp_adapter"
|
|
28
|
+
require_relative "store/codecs"
|
|
29
|
+
require_relative "store/segmented_file_backend"
|
|
30
|
+
require_relative "store/mcp_adapter"
|
|
31
|
+
require_relative "store/contractable_receipt_sink"
|
|
32
|
+
require_relative "store/tbackend_adapter_descriptor"
|
|
33
|
+
|
|
34
|
+
module Igniter
|
|
35
|
+
module Store
|
|
36
|
+
class << self
|
|
37
|
+
def memory
|
|
38
|
+
IgniterStore.new
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def open(path)
|
|
42
|
+
IgniterStore.open(path)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Open (or create) a segmented WAL store at +root_dir+.
|
|
46
|
+
# Facts from all stores are partitioned into per-store, per-time-bucket
|
|
47
|
+
# segment files under root_dir/wal/.
|
|
48
|
+
def segmented(root_dir, **opts)
|
|
49
|
+
backend = SegmentedFileBackend.new(root_dir, **opts)
|
|
50
|
+
store = IgniterStore.new(backend: backend)
|
|
51
|
+
backend.replay.each { |fact| store.__send__(:replay, fact) }
|
|
52
|
+
store
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def access_path(...)
|
|
56
|
+
AccessPath.new(...)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
LedgerStore = IgniterStore unless const_defined?(:LedgerStore)
|
|
61
|
+
LedgerServer = StoreServer unless const_defined?(:LedgerServer)
|
|
62
|
+
LedgerNetworkBackend = NetworkBackend unless const_defined?(:LedgerNetworkBackend)
|
|
63
|
+
end
|
|
64
|
+
end
|