@feltdb/core 0.6.13 → 0.7.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.
- package/README.md +32 -0
- package/dist/agent-registry.js +1 -3
- package/dist/agent-runtime.js +8 -7
- package/dist/analytics-backend.js +3 -1
- package/dist/application-contract.js +1 -0
- package/dist/application-manifest.js +1 -0
- package/dist/artifact.js +2 -0
- package/dist/authorization.js +2 -0
- package/dist/bundle.js +2 -0
- package/dist/capability.js +1 -3
- package/dist/cell.js +9 -4
- package/dist/cli/commands.js +26 -2
- package/dist/cli/index.js +1 -1
- package/dist/collection.js +39 -31
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +613 -27
- package/dist/create/server-source/crates/feltdb/src/causal_backlog_bound.rs +452 -0
- package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +208 -0
- package/dist/create/server-source/crates/feltdb/src/convergence.rs +16 -0
- package/dist/create/server-source/crates/feltdb/src/dedup_bound_investigation.rs +402 -0
- package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +784 -24
- package/dist/create/server-source/crates/feltdb/src/durable_operation_identity.rs +418 -0
- package/dist/create/server-source/crates/feltdb/src/lib.rs +284 -0
- package/dist/create/server-source/crates/feltdb/src/replica_acknowledgements.rs +471 -0
- package/dist/create/server-source/crates/feltdb/src/replica_membership.rs +661 -0
- package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +72 -3
- package/dist/create/server-source/crates/feltdb/src/transaction_preconditions.rs +899 -0
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +116 -4
- package/dist/db.d.ts +2 -2
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +48 -15
- package/dist/development-runtime-bridge.js +1 -1
- package/dist/distributed-indexing.js +7 -5
- package/dist/embedded-transaction.d.ts +9 -0
- package/dist/embedded-transaction.d.ts.map +1 -1
- package/dist/embedded-transaction.js +95 -3
- package/dist/feltdb.d.ts +16 -0
- package/dist/feltdb.d.ts.map +1 -1
- package/dist/file-db.d.ts.map +1 -1
- package/dist/file-db.js +9 -3
- package/dist/flowspec.js +2 -1
- package/dist/http-client.js +2 -0
- package/dist/http-db.d.ts +11 -0
- package/dist/http-db.d.ts.map +1 -1
- package/dist/http-db.js +32 -3
- package/dist/identity.js +1 -0
- package/dist/index-analytics.js +6 -7
- package/dist/index-backend.js +3 -3
- package/dist/index-dashboard.js +10 -13
- package/dist/index-manager.js +12 -11
- package/dist/index-monitoring.js +9 -4
- package/dist/index-store.js +2 -0
- package/dist/indexeddb-db.d.ts.map +1 -1
- package/dist/indexeddb-db.js +32 -23
- package/dist/memory-db.d.ts.map +1 -1
- package/dist/memory-db.js +8 -4
- package/dist/observe.js +2 -0
- package/dist/provider.js +2 -0
- package/dist/query-planner.js +2 -4
- package/dist/reactive-graph.js +6 -8
- package/dist/release.js +2 -0
- package/dist/sharding.js +11 -6
- package/dist/state-contract.js +3 -3
- package/dist/studio-app/assets/{feltdb_wasm-CJv3wHzi.js → feltdb_wasm-CD744e5D.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-CiIXhOLi.wasm +0 -0
- package/dist/studio-app/assets/index-DoROs8yx.js +28 -0
- package/dist/studio-app/index.html +1 -1
- package/dist/sync-contract.js +9 -2
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +32 -12
- package/dist/transaction.d.ts +127 -9
- package/dist/transaction.d.ts.map +1 -1
- package/dist/transaction.js +91 -5
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/worker.js +2 -0
- package/dist/workload.js +2 -0
- package/dist/workspace/development-node.js +11 -10
- package/dist/workspace/investigation-lifecycle-manager.js +2 -0
- package/dist/workspace/investigation-supervisor.js +5 -3
- package/dist/workspace/workspace-connection.js +14 -5
- package/package.json +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-C8TG8r2n.wasm +0 -0
- package/dist/studio-app/assets/index-DospFFYE.js +0 -28
|
@@ -37,20 +37,29 @@
|
|
|
37
37
|
//! resend-last put the identical envelope back on the wire, to test dedup
|
|
38
38
|
//! query-state the local replica's state hash and applied-operation count
|
|
39
39
|
//! metrics per-link byte and message counters, straight from the transport
|
|
40
|
+
//! causal-state applied frontier, and what is durable but held back
|
|
41
|
+
//! hold-inbound PEER park messages from PEER instead of delivering them
|
|
42
|
+
//! release-inbound PEER [reverse]
|
|
43
|
+
//! deliver what was parked, optionally newest-first
|
|
40
44
|
//! peers per-link connection state
|
|
41
45
|
//! shutdown exit cleanly
|
|
42
46
|
//! ```
|
|
43
47
|
|
|
44
48
|
use feltdb::convergence::VectorClock;
|
|
45
|
-
use feltdb::distributed_transactions::{
|
|
49
|
+
use feltdb::distributed_transactions::{
|
|
50
|
+
CausalCapacity, DistributedTransactionExecutor, ReceiveOutcome, ReplicationMessage,
|
|
51
|
+
SubmitOutcome,
|
|
52
|
+
};
|
|
46
53
|
use feltdb::replication_protocol::ProtocolTransport;
|
|
54
|
+
use feltdb::replica_acknowledgements::{AcknowledgementStore, ReclamationEvidence, ReplicaAcknowledgement};
|
|
55
|
+
use feltdb::replica_membership::MembershipStore;
|
|
47
56
|
use feltdb::state_hash::StateHash;
|
|
48
57
|
use feltdb::tcp_transport::TcpTransport;
|
|
49
58
|
use feltdb::transactions::{
|
|
50
59
|
ConsistencyContract, Operation, OperationCommand, OperationId, StateVersion,
|
|
51
60
|
};
|
|
52
61
|
use serde_json::json;
|
|
53
|
-
use std::collections::HashMap;
|
|
62
|
+
use std::collections::{HashMap, HashSet};
|
|
54
63
|
use std::io::{self, BufRead, Write};
|
|
55
64
|
use std::path::PathBuf;
|
|
56
65
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
@@ -86,6 +95,53 @@ struct RecvLink {
|
|
|
86
95
|
io: TcpTransport,
|
|
87
96
|
}
|
|
88
97
|
|
|
98
|
+
/// Hand one replicated message to the executor and record what it decided.
|
|
99
|
+
///
|
|
100
|
+
/// The harness makes no ordering decision of its own here. It calls the same
|
|
101
|
+
/// `receive_replicated` the shipping receive path calls, and reports the typed
|
|
102
|
+
/// outcome back so a test can assert on "applied" versus "held back" rather
|
|
103
|
+
/// than inferring it from state.
|
|
104
|
+
fn deliver(
|
|
105
|
+
executor: &mut DistributedTransactionExecutor,
|
|
106
|
+
version: &mut StateVersion,
|
|
107
|
+
message: ReplicationMessage,
|
|
108
|
+
) -> serde_json::Value {
|
|
109
|
+
let origin = message.envelope.originating_node.clone();
|
|
110
|
+
let key = format!(
|
|
111
|
+
"{}:{}",
|
|
112
|
+
message.envelope.envelope_id.originating_node, message.envelope.envelope_id.sequence
|
|
113
|
+
);
|
|
114
|
+
let parent = version.clone();
|
|
115
|
+
match executor.receive_replicated(message, parent) {
|
|
116
|
+
Ok(ReceiveOutcome::Applied { state_hash }) => {
|
|
117
|
+
// Only a real application advances the chained version, so a held
|
|
118
|
+
// or duplicated message cannot make the node look like it moved.
|
|
119
|
+
version.state_hash = state_hash.to_string();
|
|
120
|
+
version.vector_clock.increment(&origin);
|
|
121
|
+
json!({ "envelope": key, "outcome": "applied" })
|
|
122
|
+
}
|
|
123
|
+
Ok(ReceiveOutcome::PendingDependencies { missing }) => {
|
|
124
|
+
json!({ "envelope": key, "outcome": "pending", "missing": missing })
|
|
125
|
+
}
|
|
126
|
+
Ok(ReceiveOutcome::AlreadyKnown) => {
|
|
127
|
+
json!({ "envelope": key, "outcome": "already_known" })
|
|
128
|
+
}
|
|
129
|
+
Ok(ReceiveOutcome::CapacityDeferred { missing, pending_entries, pending_bytes }) => {
|
|
130
|
+
// Durable, but not buffered. The caller's obligation is to stop
|
|
131
|
+
// reading from this peer; the envelope is admitted from the log
|
|
132
|
+
// once the backlog drains.
|
|
133
|
+
json!({
|
|
134
|
+
"envelope": key,
|
|
135
|
+
"outcome": "capacity_deferred",
|
|
136
|
+
"missing": missing,
|
|
137
|
+
"pending_entries": pending_entries,
|
|
138
|
+
"pending_bytes": pending_bytes,
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
Err(error) => json!({ "envelope": key, "outcome": "error", "error": error }),
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
89
145
|
fn parse_link(value: &str) -> Result<(String, String), String> {
|
|
90
146
|
let (peer, addr) = value
|
|
91
147
|
.split_once('=')
|
|
@@ -118,6 +174,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
118
174
|
let mut data_dir: Option<PathBuf> = None;
|
|
119
175
|
let mut send_specs: Vec<(String, String)> = Vec::new();
|
|
120
176
|
let mut recv_specs: Vec<(String, String)> = Vec::new();
|
|
177
|
+
let mut max_pending_entries: Option<usize> = None;
|
|
178
|
+
let mut max_pending_bytes: Option<usize> = None;
|
|
121
179
|
|
|
122
180
|
let mut index = 1;
|
|
123
181
|
while index < args.len() {
|
|
@@ -143,6 +201,30 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
143
201
|
index += 1;
|
|
144
202
|
recv_specs.push(parse_link(args.get(index).ok_or("--recv requires PEER=ADDR")?)?);
|
|
145
203
|
}
|
|
204
|
+
// The bound is settable so an acceptance test can reach it through
|
|
205
|
+
// the production path. Filling the shipping default would mean
|
|
206
|
+
// sending ten thousand blocked envelopes over TCP; a test that
|
|
207
|
+
// instead constructed a barrier and filled it directly would be
|
|
208
|
+
// measuring a component nothing runs, which is the defect this
|
|
209
|
+
// whole line of work exists to stop repeating.
|
|
210
|
+
"--max-pending-entries" => {
|
|
211
|
+
index += 1;
|
|
212
|
+
max_pending_entries = Some(
|
|
213
|
+
args.get(index)
|
|
214
|
+
.ok_or("--max-pending-entries requires a value")?
|
|
215
|
+
.parse::<usize>()
|
|
216
|
+
.map_err(|e| format!("--max-pending-entries: {e}"))?,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
"--max-pending-bytes" => {
|
|
220
|
+
index += 1;
|
|
221
|
+
max_pending_bytes = Some(
|
|
222
|
+
args.get(index)
|
|
223
|
+
.ok_or("--max-pending-bytes requires a value")?
|
|
224
|
+
.parse::<usize>()
|
|
225
|
+
.map_err(|e| format!("--max-pending-bytes: {e}"))?,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
146
228
|
other => return Err(format!("unknown argument {other}").into()),
|
|
147
229
|
}
|
|
148
230
|
index += 1;
|
|
@@ -159,10 +241,45 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
159
241
|
dir.join("operations.jsonl")
|
|
160
242
|
});
|
|
161
243
|
|
|
244
|
+
// Membership lives beside the log and is loaded from disk, never rebuilt
|
|
245
|
+
// from --send/--recv. A node's connection flags say who it talks to; they
|
|
246
|
+
// say nothing about who is owed history, and conflating the two is what
|
|
247
|
+
// this store exists to stop.
|
|
248
|
+
let mut membership = MembershipStore::open(
|
|
249
|
+
data_dir
|
|
250
|
+
.as_ref()
|
|
251
|
+
.map(|dir| dir.join("membership.json"))
|
|
252
|
+
.unwrap_or_else(|| PathBuf::from("membership.json")),
|
|
253
|
+
)
|
|
254
|
+
.map_err(|e| e.to_string())?;
|
|
255
|
+
|
|
256
|
+
// Acknowledgements are durable and shared with the receive tasks, because
|
|
257
|
+
// they arrive on the socket rather than through a command.
|
|
258
|
+
let acknowledgements = Arc::new(Mutex::new(
|
|
259
|
+
AcknowledgementStore::open(
|
|
260
|
+
data_dir
|
|
261
|
+
.as_ref()
|
|
262
|
+
.map(|dir| dir.join("acknowledgements.json"))
|
|
263
|
+
.unwrap_or_else(|| PathBuf::from("acknowledgements.json")),
|
|
264
|
+
)
|
|
265
|
+
.map_err(|e| e.to_string())?,
|
|
266
|
+
));
|
|
267
|
+
|
|
162
268
|
let initial_state = StateHash::from_hex("0".repeat(64));
|
|
163
269
|
let mut executor =
|
|
164
270
|
DistributedTransactionExecutor::with_log(node_id.clone(), initial_state.clone(), log_path.clone())?;
|
|
165
271
|
|
|
272
|
+
// The bound is set before recovery, not after, because recovery restores
|
|
273
|
+
// the backlog and has to respect the same limit the live path does.
|
|
274
|
+
// Otherwise a node could restart into a backlog it would never have
|
|
275
|
+
// accepted while running, and the bound would hold only until the first
|
|
276
|
+
// crash.
|
|
277
|
+
let default_capacity = CausalCapacity::default();
|
|
278
|
+
executor.set_causal_capacity(CausalCapacity::new(
|
|
279
|
+
max_pending_entries.unwrap_or(default_capacity.max_pending_entries),
|
|
280
|
+
max_pending_bytes.unwrap_or(default_capacity.max_pending_bytes),
|
|
281
|
+
));
|
|
282
|
+
|
|
166
283
|
// Replaying the log restores the deduplication set, so a restarted node
|
|
167
284
|
// does not re-apply operations it already durably recorded.
|
|
168
285
|
let mut replayed = 0usize;
|
|
@@ -194,7 +311,25 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
194
311
|
// interleave and chain onto a parent that is already stale.
|
|
195
312
|
let core = Arc::new(Mutex::new((executor, StateVersion::new(VectorClock::new(), "0".repeat(64)))));
|
|
196
313
|
let should_exit = Arc::new(AtomicBool::new(false));
|
|
197
|
-
|
|
314
|
+
// Per-peer delivery hold.
|
|
315
|
+
//
|
|
316
|
+
// `Some(queue)` means messages arriving from that peer are received off the
|
|
317
|
+
// socket as normal and parked instead of being handed to the executor.
|
|
318
|
+
// This is how a test produces out-of-causal-order arrival without faking
|
|
319
|
+
// the network: the bytes really do cross TCP, and only the order in which
|
|
320
|
+
// the receiver is offered them is controlled. A real network reorders
|
|
321
|
+
// delivery this way; the harness just does it on demand.
|
|
322
|
+
let held: Arc<Mutex<HashMap<String, Vec<ReplicationMessage>>>> =
|
|
323
|
+
Arc::new(Mutex::new(HashMap::new()));
|
|
324
|
+
// Peers this node has stopped reading from because the causal backlog is
|
|
325
|
+
// at its bound.
|
|
326
|
+
//
|
|
327
|
+
// This is the backpressure the bounding decision commits to, and not
|
|
328
|
+
// reading is the whole mechanism: the socket's receive buffer fills, the
|
|
329
|
+
// TCP window closes, and the sender blocks. Nothing is dropped and nothing
|
|
330
|
+
// is refused, so no retransmission protocol is needed -- which is good,
|
|
331
|
+
// because there isn't one.
|
|
332
|
+
let backpressured: Arc<Mutex<HashSet<String>>> = Arc::new(Mutex::new(HashSet::new()));
|
|
198
333
|
// Kept so `resend-last` can put the identical envelope back on the wire.
|
|
199
334
|
let last_envelope: Arc<Mutex<Option<feltdb::distributed_transactions::TransactionEnvelope>>> =
|
|
200
335
|
Arc::new(Mutex::new(None));
|
|
@@ -230,6 +365,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
230
365
|
let core = core.clone();
|
|
231
366
|
let exit = should_exit.clone();
|
|
232
367
|
let peer = link.peer.clone();
|
|
368
|
+
let held = held.clone();
|
|
369
|
+
let backpressured = backpressured.clone();
|
|
370
|
+
let acknowledgements = acknowledgements.clone();
|
|
233
371
|
tokio::spawn(async move {
|
|
234
372
|
while !exit.load(Ordering::Relaxed) {
|
|
235
373
|
{
|
|
@@ -247,22 +385,57 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
247
385
|
if exit.load(Ordering::Relaxed) {
|
|
248
386
|
return;
|
|
249
387
|
}
|
|
388
|
+
|
|
389
|
+
// Do not read from a backpressured peer. While paused, keep
|
|
390
|
+
// trying to drain: the work that frees capacity may arrive
|
|
391
|
+
// on another connection, and a deferred envelope is already
|
|
392
|
+
// durable, so draining needs nothing from this socket.
|
|
393
|
+
while backpressured.lock().await.contains(&peer) {
|
|
394
|
+
if exit.load(Ordering::Relaxed) {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
{
|
|
398
|
+
let mut guard = core.lock().await;
|
|
399
|
+
let (executor, version) = &mut *guard;
|
|
400
|
+
let parent = version.clone();
|
|
401
|
+
let _ = executor.drain_backlog(parent);
|
|
402
|
+
if executor.pending_causal_count()
|
|
403
|
+
< executor.causal_capacity().max_pending_entries
|
|
404
|
+
{
|
|
405
|
+
backpressured.lock().await.remove(&peer);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
sleep(Duration::from_millis(25)).await;
|
|
409
|
+
}
|
|
410
|
+
|
|
250
411
|
match transport.receive().await {
|
|
251
412
|
Ok(messages) => {
|
|
413
|
+
// Acknowledgements arrive interleaved with
|
|
414
|
+
// envelopes on the same connection. They are
|
|
415
|
+
// durable before they count as evidence.
|
|
416
|
+
for ack in transport.take_acknowledgements().await {
|
|
417
|
+
let mut store = acknowledgements.lock().await;
|
|
418
|
+
if let Err(error) = store.record(ack) {
|
|
419
|
+
eprintln!("acknowledgement not recorded: {error}");
|
|
420
|
+
}
|
|
421
|
+
}
|
|
252
422
|
for message in messages {
|
|
253
|
-
let mut guard = core.lock().await;
|
|
254
|
-
let (executor, version) = &mut *guard;
|
|
255
|
-
let origin = message.envelope.originating_node.clone();
|
|
256
|
-
let parent = version.clone();
|
|
257
|
-
// A duplicate is refused by the executor, which
|
|
258
|
-
// is the behaviour under test, not an error
|
|
259
|
-
// here — and a refused message must not advance
|
|
260
|
-
// the version.
|
|
261
|
-
if let Ok(applied) =
|
|
262
|
-
executor.receive_replicated_transaction(message, parent)
|
|
263
423
|
{
|
|
264
|
-
|
|
265
|
-
|
|
424
|
+
let mut hold = held.lock().await;
|
|
425
|
+
if let Some(queue) = hold.get_mut(&peer) {
|
|
426
|
+
queue.push(message);
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
let outcome = {
|
|
431
|
+
let mut guard = core.lock().await;
|
|
432
|
+
let (executor, version) = &mut *guard;
|
|
433
|
+
deliver(executor, version, message)
|
|
434
|
+
};
|
|
435
|
+
if outcome.get("outcome").and_then(|value| value.as_str())
|
|
436
|
+
== Some("capacity_deferred")
|
|
437
|
+
{
|
|
438
|
+
backpressured.lock().await.insert(peer.clone());
|
|
266
439
|
}
|
|
267
440
|
}
|
|
268
441
|
}
|
|
@@ -309,17 +482,18 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
309
482
|
match verb {
|
|
310
483
|
"execute-tx" => {
|
|
311
484
|
let record = if argument.is_empty() { "item".to_string() } else { argument };
|
|
312
|
-
let seq = {
|
|
313
|
-
let mut guard = sequence.lock().await;
|
|
314
|
-
let current = *guard;
|
|
315
|
-
*guard += 1;
|
|
316
|
-
current
|
|
317
|
-
};
|
|
318
485
|
|
|
319
486
|
let mut fields = HashMap::new();
|
|
320
487
|
fields.insert("record".to_string(), json!(record.clone()));
|
|
321
488
|
|
|
322
|
-
|
|
489
|
+
// The identity comes from the executor, which derives it from the
|
|
490
|
+
// durable log. This binary used to keep its own counter,
|
|
491
|
+
// starting at 1 on every launch, so a restarted node re-issued
|
|
492
|
+
// identities its own earlier operations already held.
|
|
493
|
+
let (parent, seq) = {
|
|
494
|
+
let guard = core.lock().await;
|
|
495
|
+
(guard.1.clone(), guard.0.next_origin_sequence())
|
|
496
|
+
};
|
|
323
497
|
let operation = Operation::new(
|
|
324
498
|
OperationId::new(node_id.clone(), seq),
|
|
325
499
|
parent.clone(),
|
|
@@ -336,14 +510,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
336
510
|
let envelope = {
|
|
337
511
|
let mut guard = core.lock().await;
|
|
338
512
|
let (executor, version) = &mut *guard;
|
|
339
|
-
|
|
340
|
-
|
|
513
|
+
// The admitting entry point: local work is refused when the
|
|
514
|
+
// causal backlog is at its bound, before anything is
|
|
515
|
+
// executed or persisted, so a refusal leaves nothing to
|
|
516
|
+
// undo.
|
|
517
|
+
let result = executor.submit_local_transaction(
|
|
341
518
|
format!("tx_{record}"),
|
|
342
519
|
version.clone(),
|
|
343
520
|
vec![operation],
|
|
344
521
|
ConsistencyContract::local(),
|
|
345
522
|
);
|
|
346
|
-
if result
|
|
523
|
+
if matches!(result, Ok(SubmitOutcome::Admitted(_))) {
|
|
347
524
|
if let Some(replica) = executor.get_replica_state(&node_id) {
|
|
348
525
|
version.state_hash = replica.state_hash.to_string();
|
|
349
526
|
version.vector_clock.increment(&node_id);
|
|
@@ -353,7 +530,27 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
353
530
|
};
|
|
354
531
|
|
|
355
532
|
match envelope {
|
|
356
|
-
Ok(
|
|
533
|
+
Ok(SubmitOutcome::CapacityExceeded {
|
|
534
|
+
pending_entries,
|
|
535
|
+
pending_bytes,
|
|
536
|
+
limit_entries,
|
|
537
|
+
limit_bytes,
|
|
538
|
+
}) => {
|
|
539
|
+
// A distinct outcome, not an error: the caller should
|
|
540
|
+
// retry, and nothing was admitted or lost.
|
|
541
|
+
println!(
|
|
542
|
+
"TX_REFUSED {}",
|
|
543
|
+
json!({
|
|
544
|
+
"record": record,
|
|
545
|
+
"reason": "CAPACITY_EXCEEDED",
|
|
546
|
+
"pending_entries": pending_entries,
|
|
547
|
+
"pending_bytes": pending_bytes,
|
|
548
|
+
"limit_entries": limit_entries,
|
|
549
|
+
"limit_bytes": limit_bytes,
|
|
550
|
+
})
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
Ok(SubmitOutcome::Admitted(envelope)) => {
|
|
357
554
|
*last_envelope.lock().await = Some(envelope.clone());
|
|
358
555
|
let mut delivered = Vec::new();
|
|
359
556
|
let mut failed = Vec::new();
|
|
@@ -383,7 +580,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
383
580
|
}
|
|
384
581
|
println!(
|
|
385
582
|
"TX_EXECUTED {}",
|
|
386
|
-
json!({
|
|
583
|
+
json!({
|
|
584
|
+
"record": record,
|
|
585
|
+
// The identity the executor actually issued.
|
|
586
|
+
"sequence": envelope.envelope_id.sequence,
|
|
587
|
+
"delivered": delivered,
|
|
588
|
+
"failed": failed,
|
|
589
|
+
})
|
|
387
590
|
);
|
|
388
591
|
}
|
|
389
592
|
Err(error) => println!("ERROR {}", json!({ "reason": error })),
|
|
@@ -397,6 +600,127 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
397
600
|
// only interesting if the duplicate actually crosses the wire.
|
|
398
601
|
// Issuing another `execute-tx` would not test it: that produces a
|
|
399
602
|
// new envelope id and is a different transaction.
|
|
603
|
+
// `resend SEQUENCE` — put one specific envelope from this node's
|
|
604
|
+
// durable log back on the wire.
|
|
605
|
+
//
|
|
606
|
+
// A peer re-sending an operation another node is missing is the
|
|
607
|
+
// primitive catch-up is built from. This is not catch-up: nothing
|
|
608
|
+
// decides *what* to send, a test does. It exists so the receiver's
|
|
609
|
+
// behaviour can be exercised on a gap that a restart left behind.
|
|
610
|
+
// `forward ORIGIN SEQUENCE` -- put an envelope this node holds, but
|
|
611
|
+
// did not originate, back on the wire.
|
|
612
|
+
//
|
|
613
|
+
// `resend` deliberately only replays this node's own envelopes. A
|
|
614
|
+
// peer relaying an operation that a *third* node is missing is a
|
|
615
|
+
// different primitive, and it is the one a backpressured receiver
|
|
616
|
+
// needs: when a node stops reading from the peer whose envelopes it
|
|
617
|
+
// deferred, the dependency it is waiting for cannot arrive on that
|
|
618
|
+
// link. It has to come from somewhere else, which is precisely the
|
|
619
|
+
// case the read loop's keep-draining-while-paused behaviour exists
|
|
620
|
+
// to serve.
|
|
621
|
+
"forward" => {
|
|
622
|
+
let mut parts = argument.split_whitespace();
|
|
623
|
+
let origin = parts.next().unwrap_or_default().to_string();
|
|
624
|
+
let wanted: Option<u64> = parts.next().and_then(|value| value.parse().ok());
|
|
625
|
+
let envelope = {
|
|
626
|
+
let guard = core.lock().await;
|
|
627
|
+
let (executor, _) = &*guard;
|
|
628
|
+
executor.operation_log.as_ref().and_then(|log| {
|
|
629
|
+
log.load_all().ok().and_then(|envelopes| {
|
|
630
|
+
envelopes.into_iter().find(|candidate| {
|
|
631
|
+
candidate.envelope_id.originating_node == origin
|
|
632
|
+
&& Some(candidate.envelope_id.sequence) == wanted
|
|
633
|
+
})
|
|
634
|
+
})
|
|
635
|
+
})
|
|
636
|
+
};
|
|
637
|
+
match envelope {
|
|
638
|
+
Some(envelope) => {
|
|
639
|
+
let mut delivered = Vec::new();
|
|
640
|
+
for link in &send_links {
|
|
641
|
+
let message = ReplicationMessage::new(
|
|
642
|
+
envelope.clone(),
|
|
643
|
+
node_id.clone(),
|
|
644
|
+
link.peer.clone(),
|
|
645
|
+
envelope.envelope_id.sequence,
|
|
646
|
+
);
|
|
647
|
+
if link.io.send(message.clone()).await.is_ok() {
|
|
648
|
+
delivered.push(link.peer.clone());
|
|
649
|
+
} else if connect_with_retry(link, 3).await
|
|
650
|
+
&& link.io.send(message).await.is_ok()
|
|
651
|
+
{
|
|
652
|
+
delivered.push(link.peer.clone());
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
println!(
|
|
656
|
+
"FORWARDED {}",
|
|
657
|
+
json!({
|
|
658
|
+
"origin": origin,
|
|
659
|
+
"sequence": wanted,
|
|
660
|
+
"delivered": delivered,
|
|
661
|
+
})
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
None => println!(
|
|
665
|
+
"FORWARDED {}",
|
|
666
|
+
json!({ "origin": origin, "sequence": wanted, "delivered": [], "found": false })
|
|
667
|
+
),
|
|
668
|
+
}
|
|
669
|
+
io::stdout().flush().ok();
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
"resend" => {
|
|
673
|
+
let wanted: Option<u64> = argument.trim().parse().ok();
|
|
674
|
+
let envelope = {
|
|
675
|
+
let guard = core.lock().await;
|
|
676
|
+
let (executor, _) = &*guard;
|
|
677
|
+
executor.operation_log.as_ref().and_then(|log| {
|
|
678
|
+
log.load_all().ok().and_then(|envelopes| {
|
|
679
|
+
envelopes.into_iter().find(|candidate| {
|
|
680
|
+
candidate.envelope_id.originating_node == node_id
|
|
681
|
+
&& Some(candidate.envelope_id.sequence) == wanted
|
|
682
|
+
})
|
|
683
|
+
})
|
|
684
|
+
})
|
|
685
|
+
};
|
|
686
|
+
match envelope {
|
|
687
|
+
Some(envelope) => {
|
|
688
|
+
let mut delivered = Vec::new();
|
|
689
|
+
for link in &send_links {
|
|
690
|
+
let message = ReplicationMessage::new(
|
|
691
|
+
envelope.clone(),
|
|
692
|
+
node_id.clone(),
|
|
693
|
+
link.peer.clone(),
|
|
694
|
+
envelope.envelope_id.sequence,
|
|
695
|
+
);
|
|
696
|
+
// The peer this is aimed at may have restarted
|
|
697
|
+
// since the link was established, which is the
|
|
698
|
+
// whole point of re-sending. Reconnect and retry,
|
|
699
|
+
// as `execute-tx` does.
|
|
700
|
+
match link.io.send(message.clone()).await {
|
|
701
|
+
Ok(()) => delivered.push(link.peer.clone()),
|
|
702
|
+
Err(_) => {
|
|
703
|
+
if connect_with_retry(link, 3).await
|
|
704
|
+
&& link.io.send(message).await.is_ok()
|
|
705
|
+
{
|
|
706
|
+
delivered.push(link.peer.clone());
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
println!(
|
|
712
|
+
"RESENT {}",
|
|
713
|
+
json!({ "envelope": envelope.envelope_id, "delivered": delivered })
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
None => println!(
|
|
717
|
+
"ERROR {}",
|
|
718
|
+
json!({ "reason": format!("no envelope {argument} in this node's log") })
|
|
719
|
+
),
|
|
720
|
+
}
|
|
721
|
+
io::stdout().flush().ok();
|
|
722
|
+
}
|
|
723
|
+
|
|
400
724
|
"resend-last" => {
|
|
401
725
|
let envelope = last_envelope.lock().await.clone();
|
|
402
726
|
match envelope {
|
|
@@ -426,12 +750,23 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
426
750
|
"query-state" => {
|
|
427
751
|
let guard = core.lock().await;
|
|
428
752
|
let (executor, _) = &*guard;
|
|
753
|
+
let canonical = executor
|
|
754
|
+
.canonical_state()
|
|
755
|
+
.map(|state| state.hash().to_string())
|
|
756
|
+
.unwrap_or_else(|error| format!("error: {error}"));
|
|
429
757
|
match executor.get_replica_state(&node_id) {
|
|
430
758
|
Some(replica) => println!(
|
|
431
759
|
"STATE {}",
|
|
432
760
|
json!({
|
|
433
761
|
"hash": replica.state_hash.to_string(),
|
|
434
762
|
"operations_applied": replica.operations_applied,
|
|
763
|
+
// Order-independent, and therefore the one to
|
|
764
|
+
// compare across replicas. The chained hash above
|
|
765
|
+
// detects a missing operation in an ordered
|
|
766
|
+
// history; it calls two nodes that applied the same
|
|
767
|
+
// concurrent operations in different orders
|
|
768
|
+
// divergent, which they are not.
|
|
769
|
+
"canonical_hash": canonical,
|
|
435
770
|
})
|
|
436
771
|
),
|
|
437
772
|
None => println!("ERROR {}", json!({ "reason": "no local replica" })),
|
|
@@ -439,6 +774,257 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
439
774
|
io::stdout().flush().ok();
|
|
440
775
|
}
|
|
441
776
|
|
|
777
|
+
// What the causal receiver is holding, and why.
|
|
778
|
+
// Membership commands. Deliberately no compaction frontier and no
|
|
779
|
+
// acknowledgement tracking: this establishes who exists and who is
|
|
780
|
+
// required, and reclaims nothing.
|
|
781
|
+
"cluster-create" => {
|
|
782
|
+
let result = membership.create_cluster(argument.trim());
|
|
783
|
+
println!(
|
|
784
|
+
"CLUSTER {}",
|
|
785
|
+
match result {
|
|
786
|
+
Ok(()) => json!({ "cluster_id": argument.trim(), "created": true }),
|
|
787
|
+
Err(error) => json!({ "created": false, "error": error.to_string() }),
|
|
788
|
+
}
|
|
789
|
+
);
|
|
790
|
+
io::stdout().flush().ok();
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
"replica-join" | "replica-approve" | "replica-suspect" | "replica-seen"
|
|
794
|
+
| "replica-remove" => {
|
|
795
|
+
let mut parts = argument.split_whitespace();
|
|
796
|
+
let replica = parts.next().unwrap_or_default().to_string();
|
|
797
|
+
let result = match verb {
|
|
798
|
+
"replica-join" => {
|
|
799
|
+
let node = parts.next().unwrap_or(&replica).to_string();
|
|
800
|
+
membership.join_replica(&replica, &node)
|
|
801
|
+
}
|
|
802
|
+
"replica-approve" => membership.approve_replica(&replica),
|
|
803
|
+
"replica-suspect" => membership.suspect_replica(&replica),
|
|
804
|
+
"replica-seen" => membership.mark_seen(&replica),
|
|
805
|
+
_ => membership.remove_replica(&replica),
|
|
806
|
+
};
|
|
807
|
+
println!(
|
|
808
|
+
"REPLICA {}",
|
|
809
|
+
match result {
|
|
810
|
+
Ok(()) => json!({ "replica_id": replica, "action": verb, "ok": true }),
|
|
811
|
+
Err(error) =>
|
|
812
|
+
json!({ "replica_id": replica, "action": verb, "ok": false, "error": error.to_string() }),
|
|
813
|
+
}
|
|
814
|
+
);
|
|
815
|
+
io::stdout().flush().ok();
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Report what this node has durably applied, to every peer.
|
|
819
|
+
//
|
|
820
|
+
// The frontier is read from the executor, where it advances only in
|
|
821
|
+
// record_applied -- after an fsynced envelope is applied. So the
|
|
822
|
+
// statement "durably applied through this frontier" is true by
|
|
823
|
+
// construction rather than by assertion.
|
|
824
|
+
"ack-broadcast" => {
|
|
825
|
+
let (frontier, marks) = {
|
|
826
|
+
let guard = core.lock().await;
|
|
827
|
+
let (executor, _) = &*guard;
|
|
828
|
+
(executor.causal_frontier().clone(), executor.origin_sequence_high_water())
|
|
829
|
+
};
|
|
830
|
+
let view: Vec<String> = membership
|
|
831
|
+
.get_members()
|
|
832
|
+
.into_iter()
|
|
833
|
+
.map(|replica| replica.replica_id)
|
|
834
|
+
.collect();
|
|
835
|
+
let replica_id = membership
|
|
836
|
+
.get_members()
|
|
837
|
+
.into_iter()
|
|
838
|
+
.find(|replica| replica.node_id == node_id)
|
|
839
|
+
.map(|replica| replica.replica_id)
|
|
840
|
+
.unwrap_or_else(|| node_id.clone());
|
|
841
|
+
|
|
842
|
+
let ack = ReplicaAcknowledgement::new(
|
|
843
|
+
replica_id.clone(),
|
|
844
|
+
node_id.clone(),
|
|
845
|
+
frontier,
|
|
846
|
+
marks,
|
|
847
|
+
view,
|
|
848
|
+
);
|
|
849
|
+
|
|
850
|
+
// A node's own acknowledgement is evidence it holds first-hand:
|
|
851
|
+
// it knows its own durable frontier without being told. Without
|
|
852
|
+
// this a node would be permanently blocked on itself, since
|
|
853
|
+
// nothing else can report on its behalf.
|
|
854
|
+
{
|
|
855
|
+
let mut store = acknowledgements.lock().await;
|
|
856
|
+
let _ = store.record(ack.clone());
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
let mut delivered = Vec::new();
|
|
860
|
+
for link in &send_links {
|
|
861
|
+
if link.io.send_acknowledgement(ack.clone()).await.is_ok() {
|
|
862
|
+
delivered.push(link.peer.clone());
|
|
863
|
+
} else if connect_with_retry(link, 3).await
|
|
864
|
+
&& link.io.send_acknowledgement(ack.clone()).await.is_ok()
|
|
865
|
+
{
|
|
866
|
+
delivered.push(link.peer.clone());
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
println!(
|
|
870
|
+
"ACK_SENT {}",
|
|
871
|
+
json!({
|
|
872
|
+
"replica_id": replica_id,
|
|
873
|
+
"frontier": ack.applied_frontier.clocks,
|
|
874
|
+
"delivered": delivered,
|
|
875
|
+
})
|
|
876
|
+
);
|
|
877
|
+
io::stdout().flush().ok();
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
"acknowledgements" => {
|
|
881
|
+
let store = acknowledgements.lock().await;
|
|
882
|
+
println!("ACKS {}", json!({ "acknowledgements": store.all() }));
|
|
883
|
+
io::stdout().flush().ok();
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// What this node's evidence permits. Computes; reclaims nothing.
|
|
887
|
+
"reclamation-evidence" => {
|
|
888
|
+
let local: Vec<String> = membership
|
|
889
|
+
.get_required_replicas()
|
|
890
|
+
.into_iter()
|
|
891
|
+
.map(|replica| replica.replica_id)
|
|
892
|
+
.collect();
|
|
893
|
+
let store = acknowledgements.lock().await;
|
|
894
|
+
let report = match store.evidence(&local) {
|
|
895
|
+
ReclamationEvidence::Blocked { waiting_on } => json!({
|
|
896
|
+
"state": "BLOCKED",
|
|
897
|
+
"waiting_on": waiting_on,
|
|
898
|
+
"required_union": store.required_union(&local),
|
|
899
|
+
}),
|
|
900
|
+
ReclamationEvidence::SafeThrough { frontier, covered } => json!({
|
|
901
|
+
"state": "SAFE_THROUGH",
|
|
902
|
+
"frontier": frontier.clocks,
|
|
903
|
+
"covered": covered,
|
|
904
|
+
"required_union": store.required_union(&local),
|
|
905
|
+
}),
|
|
906
|
+
};
|
|
907
|
+
println!("EVIDENCE {}", report);
|
|
908
|
+
io::stdout().flush().ok();
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
"members" => {
|
|
912
|
+
println!(
|
|
913
|
+
"MEMBERS {}",
|
|
914
|
+
json!({
|
|
915
|
+
"cluster_id": membership.cluster().map(|c| c.cluster_id.clone()),
|
|
916
|
+
"members": membership.get_members(),
|
|
917
|
+
"all": membership.all_replicas(),
|
|
918
|
+
})
|
|
919
|
+
);
|
|
920
|
+
io::stdout().flush().ok();
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
"required-replicas" => {
|
|
924
|
+
println!(
|
|
925
|
+
"REQUIRED {}",
|
|
926
|
+
json!({ "required": membership.get_required_replicas() })
|
|
927
|
+
);
|
|
928
|
+
io::stdout().flush().ok();
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
"causal-state" => {
|
|
932
|
+
let guard = core.lock().await;
|
|
933
|
+
let (executor, _) = &*guard;
|
|
934
|
+
let paused = {
|
|
935
|
+
let mut peers: Vec<String> =
|
|
936
|
+
backpressured.lock().await.iter().cloned().collect();
|
|
937
|
+
peers.sort();
|
|
938
|
+
peers
|
|
939
|
+
};
|
|
940
|
+
println!(
|
|
941
|
+
"CAUSAL {}",
|
|
942
|
+
json!({
|
|
943
|
+
"frontier": executor.causal_frontier().clocks,
|
|
944
|
+
"pending": executor.pending_causal_keys(),
|
|
945
|
+
"pending_count": executor.pending_causal_count(),
|
|
946
|
+
"pending_metadata_bytes": executor.pending_causal_metadata_bytes(),
|
|
947
|
+
// Occupancy against the bound that is now enforced.
|
|
948
|
+
// Current and maximum, for both dimensions, because a
|
|
949
|
+
// backlog that filled and drained between two reads is
|
|
950
|
+
// invisible to a current-value gauge.
|
|
951
|
+
"pending_payload_bytes": executor.pending_causal_payload_bytes(),
|
|
952
|
+
"deferred_count": executor.deferred_causal_count(),
|
|
953
|
+
"deferred": executor.deferred_causal_keys(),
|
|
954
|
+
"max_pending_entries_seen": executor.max_causal_occupancy().0,
|
|
955
|
+
"max_pending_payload_bytes_seen": executor.max_causal_occupancy().1,
|
|
956
|
+
"limit_entries": executor.causal_capacity().max_pending_entries,
|
|
957
|
+
"limit_bytes": executor.causal_capacity().max_pending_bytes,
|
|
958
|
+
// Peers this node has stopped reading from. This is the
|
|
959
|
+
// observable for backpressure: the pause lives in the
|
|
960
|
+
// socket read loop, so without it a test can only infer
|
|
961
|
+
// the behaviour from timing.
|
|
962
|
+
//
|
|
963
|
+
// Locked after `core`, matching the order the read loop
|
|
964
|
+
// takes them in.
|
|
965
|
+
"backpressured": paused,
|
|
966
|
+
})
|
|
967
|
+
);
|
|
968
|
+
io::stdout().flush().ok();
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// Park messages from a peer instead of delivering them, so a test
|
|
972
|
+
// can choose the order the receiver is offered them in.
|
|
973
|
+
"hold-inbound" => {
|
|
974
|
+
let mut hold = held.lock().await;
|
|
975
|
+
hold.entry(argument.clone()).or_default();
|
|
976
|
+
println!("HELD {}", json!({ "peer": argument }));
|
|
977
|
+
io::stdout().flush().ok();
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Deliver what was parked, optionally newest-first, then resume
|
|
981
|
+
// normal delivery. Each message goes through the same production
|
|
982
|
+
// receiver as an unheld one.
|
|
983
|
+
// `release-inbound PEER [reverse] [COUNT]`
|
|
984
|
+
//
|
|
985
|
+
// COUNT releases only that many and keeps holding the rest, which
|
|
986
|
+
// is what lets a test offer an operation while deliberately
|
|
987
|
+
// withholding the dependency it needs.
|
|
988
|
+
"release-inbound" => {
|
|
989
|
+
let mut parts = argument.split_whitespace();
|
|
990
|
+
let peer = parts.next().unwrap_or_default().to_string();
|
|
991
|
+
let mut reverse = false;
|
|
992
|
+
let mut limit: Option<usize> = None;
|
|
993
|
+
for token in parts {
|
|
994
|
+
if token == "reverse" {
|
|
995
|
+
reverse = true;
|
|
996
|
+
} else if let Ok(value) = token.parse::<usize>() {
|
|
997
|
+
limit = Some(value);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
let mut queued = { held.lock().await.remove(&peer).unwrap_or_default() };
|
|
1002
|
+
if reverse {
|
|
1003
|
+
queued.reverse();
|
|
1004
|
+
}
|
|
1005
|
+
let take = limit.unwrap_or(queued.len()).min(queued.len());
|
|
1006
|
+
let remainder = queued.split_off(take);
|
|
1007
|
+
|
|
1008
|
+
let mut outcomes = Vec::new();
|
|
1009
|
+
for message in queued {
|
|
1010
|
+
let mut guard = core.lock().await;
|
|
1011
|
+
let (executor, version) = &mut *guard;
|
|
1012
|
+
outcomes.push(deliver(executor, version, message));
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// Anything not released stays held, so the hold is still in
|
|
1016
|
+
// force for messages that arrive afterwards.
|
|
1017
|
+
if !remainder.is_empty() || limit.is_some() {
|
|
1018
|
+
held.lock().await.insert(peer.clone(), remainder);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
println!(
|
|
1022
|
+
"RELEASED {}",
|
|
1023
|
+
json!({ "peer": peer, "reverse": reverse, "outcomes": outcomes })
|
|
1024
|
+
);
|
|
1025
|
+
io::stdout().flush().ok();
|
|
1026
|
+
}
|
|
1027
|
+
|
|
442
1028
|
// The proof that the network carried the work. Counters come from
|
|
443
1029
|
// the transport itself, so a test can assert bytes actually moved
|
|
444
1030
|
// rather than inferring it from converged state.
|