@feltdb/core 0.6.8 → 0.6.10
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/dist/cli/application.js +55 -88
- package/dist/cli/commands.js +206 -95
- package/dist/cli/index.js +1 -1
- package/dist/collection.d.ts +110 -2
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +228 -3
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +408 -178
- package/dist/create/server-source/crates/feltdb/src/lib.rs +49 -5
- package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +659 -0
- package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +285 -0
- package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +582 -0
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +141 -0
- package/dist/db.d.ts +59 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +94 -2
- package/dist/embedded-transaction.d.ts +77 -0
- package/dist/embedded-transaction.d.ts.map +1 -0
- package/dist/embedded-transaction.js +102 -0
- package/dist/feltdb.d.ts +49 -1
- package/dist/feltdb.d.ts.map +1 -1
- package/dist/file-db.d.ts +33 -0
- package/dist/file-db.d.ts.map +1 -1
- package/dist/file-db.js +128 -4
- package/dist/freshness.d.ts +119 -0
- package/dist/freshness.d.ts.map +1 -0
- package/dist/freshness.js +115 -0
- package/dist/http-db.d.ts +48 -0
- package/dist/http-db.d.ts.map +1 -1
- package/dist/http-db.js +77 -1
- package/dist/index-backend.d.ts +8 -0
- package/dist/index-backend.d.ts.map +1 -1
- package/dist/index-backend.js +13 -0
- package/dist/index-core.d.ts +78 -0
- package/dist/index-core.d.ts.map +1 -0
- package/dist/index-core.js +72 -0
- package/dist/index-manager.d.ts +11 -0
- package/dist/index-manager.d.ts.map +1 -1
- package/dist/index-manager.js +30 -13
- package/dist/index-types.d.ts +17 -0
- package/dist/index-types.d.ts.map +1 -1
- package/dist/index.browser.d.ts +14 -0
- package/dist/index.browser.d.ts.map +1 -0
- package/dist/index.browser.js +13 -0
- package/dist/index.d.ts +8 -59
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -54
- package/dist/indexeddb-db.d.ts +32 -0
- package/dist/indexeddb-db.d.ts.map +1 -1
- package/dist/indexeddb-db.js +151 -1
- package/dist/memory-db.d.ts +26 -0
- package/dist/memory-db.d.ts.map +1 -1
- package/dist/memory-db.js +86 -0
- package/dist/runtime-registry.d.ts +32 -0
- package/dist/runtime-registry.d.ts.map +1 -0
- package/dist/runtime-registry.js +38 -0
- package/dist/studio-app/assets/{feltdb_wasm-B4wq4mqp.js → feltdb_wasm-CJv3wHzi.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-C8TG8r2n.wasm +0 -0
- package/dist/studio-app/assets/index-D3rT0SJi.js +28 -0
- package/dist/studio-app/index.html +1 -1
- package/dist/transaction.d.ts +98 -0
- package/dist/transaction.d.ts.map +1 -0
- package/dist/transaction.js +86 -0
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/workspace/browser.d.ts +22 -0
- package/dist/workspace/browser.d.ts.map +1 -0
- package/dist/workspace/browser.js +17 -0
- package/dist/workspace/git-identity.d.ts +35 -0
- package/dist/workspace/git-identity.d.ts.map +1 -0
- package/dist/workspace/git-identity.js +81 -0
- package/dist/workspace/index.d.ts +11 -1
- package/dist/workspace/index.d.ts.map +1 -1
- package/dist/workspace/index.js +6 -0
- package/dist/workspace/investigation-analysis.d.ts +47 -0
- package/dist/workspace/investigation-analysis.d.ts.map +1 -0
- package/dist/workspace/investigation-analysis.js +181 -0
- package/dist/workspace/investigation-lifecycle-manager.d.ts +86 -0
- package/dist/workspace/investigation-lifecycle-manager.d.ts.map +1 -0
- package/dist/workspace/investigation-lifecycle-manager.js +229 -0
- package/dist/workspace/investigation-supervisor.d.ts +100 -0
- package/dist/workspace/investigation-supervisor.d.ts.map +1 -0
- package/dist/workspace/investigation-supervisor.js +236 -0
- package/dist/workspace/runtime-investigation.d.ts +1 -1
- package/dist/workspace/runtime-investigation.d.ts.map +1 -1
- package/dist/workspace/runtime-investigation.js +46 -21
- package/dist/workspace/runtime-observation.d.ts +79 -0
- package/dist/workspace/runtime-observation.d.ts.map +1 -0
- package/dist/workspace/runtime-observation.js +152 -0
- package/dist/workspace/runtime-observer.d.ts +51 -0
- package/dist/workspace/runtime-observer.d.ts.map +1 -0
- package/dist/workspace/runtime-observer.js +268 -0
- package/dist/workspace/workspace-connection.d.ts.map +1 -1
- package/dist/workspace/workspace-connection.js +14 -5
- package/dist/workspace/workspace-types.d.ts +113 -0
- package/dist/workspace/workspace-types.d.ts.map +1 -1
- package/package.json +12 -4
- package/dist/create/server-source/crates/feltdb/src/phase_1c_three_node.rs +0 -523
- package/dist/studio-app/assets/feltdb_wasm_bg-Ceyi7l21.wasm +0 -0
- package/dist/studio-app/assets/index-BF45M757.js +0 -28
|
@@ -1,11 +1,45 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
//! A FeltDB replication node, as an operating-system process.
|
|
2
|
+
//!
|
|
3
|
+
//! This exists so replication can be proved across a real process and network
|
|
4
|
+
//! boundary rather than between executors sharing a heap. It is a harness, not
|
|
5
|
+
//! a product surface: it speaks a line protocol on stdin/stdout so a test can
|
|
6
|
+
//! drive it, and it implements no replication semantics of its own. Every
|
|
7
|
+
//! decision about ordering, deduplication, durability, and convergence is made
|
|
8
|
+
//! by `DistributedTransactionExecutor`; this binary only moves bytes between
|
|
9
|
+
//! processes and reports what happened.
|
|
10
|
+
//!
|
|
11
|
+
//! ## Links are directed, one TCP connection each
|
|
12
|
+
//!
|
|
13
|
+
//! `TcpTransport` holds a single stream, and its `receive()` holds the stream
|
|
14
|
+
//! lock while awaiting the next message. A bidirectional link would therefore
|
|
15
|
+
//! deadlock the first `send()` behind the receive that is parked on the socket.
|
|
16
|
+
//!
|
|
17
|
+
//! So each direction gets its own connection: for a link X -> Y, Y binds a port
|
|
18
|
+
//! dedicated to X and only ever receives on it, and X connects to that port and
|
|
19
|
+
//! only ever sends. Three nodes in a full mesh is six directed links.
|
|
20
|
+
//!
|
|
21
|
+
//! That is a topology choice made here in the harness. It changes nothing about
|
|
22
|
+
//! the transport or the protocol.
|
|
23
|
+
//!
|
|
24
|
+
//! ## Usage
|
|
25
|
+
//!
|
|
26
|
+
//! ```text
|
|
27
|
+
//! feltdb_node --node-id A --data DIR --recv B=127.0.0.1:9111 --send B=127.0.0.1:9121
|
|
28
|
+
//! ```
|
|
29
|
+
//!
|
|
30
|
+
//! `--recv PEER=ADDR` binds ADDR and accepts the connection PEER makes to it.
|
|
31
|
+
//! `--send PEER=ADDR` connects to the address PEER is listening on for us.
|
|
32
|
+
//!
|
|
33
|
+
//! Commands on stdin, one per line:
|
|
34
|
+
//!
|
|
35
|
+
//! ```text
|
|
36
|
+
//! execute-tx RECORD commit a local transaction and replicate it to every peer
|
|
37
|
+
//! resend-last put the identical envelope back on the wire, to test dedup
|
|
38
|
+
//! query-state the local replica's state hash and applied-operation count
|
|
39
|
+
//! metrics per-link byte and message counters, straight from the transport
|
|
40
|
+
//! peers per-link connection state
|
|
41
|
+
//! shutdown exit cleanly
|
|
42
|
+
//! ```
|
|
9
43
|
|
|
10
44
|
use feltdb::convergence::VectorClock;
|
|
11
45
|
use feltdb::distributed_transactions::{DistributedTransactionExecutor, ReplicationMessage};
|
|
@@ -18,246 +52,442 @@ use feltdb::transactions::{
|
|
|
18
52
|
use serde_json::json;
|
|
19
53
|
use std::collections::HashMap;
|
|
20
54
|
use std::io::{self, BufRead, Write};
|
|
55
|
+
use std::path::PathBuf;
|
|
21
56
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
22
57
|
use std::sync::Arc;
|
|
23
58
|
use tokio::sync::{mpsc, Mutex};
|
|
24
59
|
use tokio::time::{sleep, Duration};
|
|
25
60
|
|
|
61
|
+
/// One outbound connection to a peer, used only for sending.
|
|
62
|
+
///
|
|
63
|
+
/// `control` exists because `connect` needs `&mut self`. Everything else goes
|
|
64
|
+
/// through `io`, a clone: `TcpTransport` holds its stream and metrics behind
|
|
65
|
+
/// `Arc`s, so a clone is the same connection, and reading counters or sending
|
|
66
|
+
/// does not queue behind whatever else holds the mutex.
|
|
67
|
+
#[derive(Clone)]
|
|
68
|
+
struct SendLink {
|
|
69
|
+
peer: String,
|
|
70
|
+
addr: String,
|
|
71
|
+
control: Arc<Mutex<TcpTransport>>,
|
|
72
|
+
io: TcpTransport,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// One inbound connection from a peer, used only for receiving.
|
|
76
|
+
///
|
|
77
|
+
/// The distinction matters more here: `receive()` parks on the socket holding
|
|
78
|
+
/// the transport's internal stream lock, so anything sharing an outer mutex
|
|
79
|
+
/// with it would block until a message happened to arrive.
|
|
26
80
|
#[derive(Clone)]
|
|
27
|
-
struct
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
81
|
+
struct RecvLink {
|
|
82
|
+
peer: String,
|
|
83
|
+
#[allow(dead_code)]
|
|
84
|
+
addr: String,
|
|
85
|
+
control: Arc<Mutex<TcpTransport>>,
|
|
86
|
+
io: TcpTransport,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
fn parse_link(value: &str) -> Result<(String, String), String> {
|
|
90
|
+
let (peer, addr) = value
|
|
91
|
+
.split_once('=')
|
|
92
|
+
.ok_or_else(|| format!("expected PEER=ADDR, got {value}"))?;
|
|
93
|
+
if peer.is_empty() || addr.is_empty() {
|
|
94
|
+
return Err(format!("expected PEER=ADDR, got {value}"));
|
|
95
|
+
}
|
|
96
|
+
Ok((peer.to_string(), addr.to_string()))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// Connect with retries: the peer's listener may not be bound yet, and after a
|
|
100
|
+
/// peer restarts its old socket is gone and this is how the link comes back.
|
|
101
|
+
async fn connect_with_retry(link: &SendLink, attempts: u32) -> bool {
|
|
102
|
+
for attempt in 0..attempts {
|
|
103
|
+
let mut control = link.control.lock().await;
|
|
104
|
+
if control.connect(&link.addr).await.is_ok() {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
drop(control);
|
|
108
|
+
sleep(Duration::from_millis(25 * (attempt as u64 + 1))).await;
|
|
109
|
+
}
|
|
110
|
+
false
|
|
34
111
|
}
|
|
35
112
|
|
|
36
113
|
#[tokio::main]
|
|
37
114
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
38
115
|
let args: Vec<String> = std::env::args().collect();
|
|
39
116
|
|
|
40
|
-
|
|
41
|
-
let mut
|
|
42
|
-
let mut
|
|
43
|
-
let mut
|
|
44
|
-
|
|
45
|
-
let mut
|
|
46
|
-
while
|
|
47
|
-
match args[
|
|
48
|
-
"--
|
|
49
|
-
|
|
50
|
-
|
|
117
|
+
let mut node_id = String::new();
|
|
118
|
+
let mut data_dir: Option<PathBuf> = None;
|
|
119
|
+
let mut send_specs: Vec<(String, String)> = Vec::new();
|
|
120
|
+
let mut recv_specs: Vec<(String, String)> = Vec::new();
|
|
121
|
+
|
|
122
|
+
let mut index = 1;
|
|
123
|
+
while index < args.len() {
|
|
124
|
+
match args[index].as_str() {
|
|
125
|
+
"--node-id" => {
|
|
126
|
+
index += 1;
|
|
127
|
+
node_id = args
|
|
128
|
+
.get(index)
|
|
129
|
+
.ok_or("--node-id requires a value")?
|
|
130
|
+
.clone();
|
|
131
|
+
}
|
|
132
|
+
"--data" => {
|
|
133
|
+
index += 1;
|
|
134
|
+
data_dir = Some(PathBuf::from(
|
|
135
|
+
args.get(index).ok_or("--data requires a value")?,
|
|
136
|
+
));
|
|
51
137
|
}
|
|
52
|
-
"--
|
|
53
|
-
|
|
54
|
-
|
|
138
|
+
"--send" => {
|
|
139
|
+
index += 1;
|
|
140
|
+
send_specs.push(parse_link(args.get(index).ok_or("--send requires PEER=ADDR")?)?);
|
|
55
141
|
}
|
|
56
|
-
"--
|
|
57
|
-
|
|
58
|
-
|
|
142
|
+
"--recv" => {
|
|
143
|
+
index += 1;
|
|
144
|
+
recv_specs.push(parse_link(args.get(index).ok_or("--recv requires PEER=ADDR")?)?);
|
|
59
145
|
}
|
|
60
|
-
|
|
146
|
+
other => return Err(format!("unknown argument {other}").into()),
|
|
61
147
|
}
|
|
62
|
-
|
|
148
|
+
index += 1;
|
|
63
149
|
}
|
|
64
150
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
151
|
+
if node_id.is_empty() {
|
|
152
|
+
return Err("--node-id is required".into());
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Durability is the executor's, not this binary's. The log path is per
|
|
156
|
+
// node, so each process owns its own directory on disk.
|
|
157
|
+
let log_path = data_dir.as_ref().map(|dir| {
|
|
158
|
+
std::fs::create_dir_all(dir).ok();
|
|
159
|
+
dir.join("operations.jsonl")
|
|
160
|
+
});
|
|
71
161
|
|
|
72
|
-
|
|
73
|
-
let initial_state = StateHash::from_hex("hash_initial".to_string());
|
|
162
|
+
let initial_state = StateHash::from_hex("0".repeat(64));
|
|
74
163
|
let mut executor =
|
|
75
|
-
DistributedTransactionExecutor::
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
let
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
let node_state = NodeState {
|
|
86
|
-
executor: Arc::new(Mutex::new(executor)),
|
|
87
|
-
node_id: node_id.clone(),
|
|
88
|
-
transport: Arc::new(Mutex::new(transport)),
|
|
89
|
-
tx_sequence: Arc::new(Mutex::new(1)),
|
|
90
|
-
is_server,
|
|
91
|
-
should_exit: Arc::new(AtomicBool::new(false)),
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
// Start TCP replication in background
|
|
95
|
-
if is_server {
|
|
96
|
-
let replication_addr = format!("127.0.0.1:{}", replication_port);
|
|
97
|
-
let ns = node_state.clone();
|
|
98
|
-
tokio::spawn(async move {
|
|
99
|
-
let mut transport = ns.transport.lock().await;
|
|
100
|
-
if let Err(e) = transport.listen().await {
|
|
101
|
-
eprintln!("ERROR: listen failed: {}", e);
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
println!("STARTED server on port {}", replication_port);
|
|
105
|
-
let _ = std::io::stdout().flush();
|
|
106
|
-
} else {
|
|
107
|
-
let ns = node_state.clone();
|
|
108
|
-
let peer = peer_addr.clone();
|
|
109
|
-
tokio::spawn(async move {
|
|
110
|
-
let mut transport = ns.transport.lock().await;
|
|
111
|
-
if let Err(e) = transport.connect(&peer).await {
|
|
112
|
-
eprintln!("ERROR: connect failed: {}", e);
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
println!("STARTED client connecting to {}", peer_addr);
|
|
116
|
-
let _ = std::io::stdout().flush();
|
|
164
|
+
DistributedTransactionExecutor::with_log(node_id.clone(), initial_state.clone(), log_path.clone())?;
|
|
165
|
+
|
|
166
|
+
// Replaying the log restores the deduplication set, so a restarted node
|
|
167
|
+
// does not re-apply operations it already durably recorded.
|
|
168
|
+
let mut replayed = 0usize;
|
|
169
|
+
if let Some(path) = &log_path {
|
|
170
|
+
if path.exists() {
|
|
171
|
+
replayed = executor.load_from_disk(path.clone())?;
|
|
172
|
+
}
|
|
117
173
|
}
|
|
118
174
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
175
|
+
for (peer, _) in send_specs.iter().chain(recv_specs.iter()) {
|
|
176
|
+
if peer != &node_id {
|
|
177
|
+
executor.register_replica(peer.clone(), initial_state.clone());
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// The running state version, advanced by every applied transaction.
|
|
182
|
+
//
|
|
183
|
+
// This matters more than it looks. `execute_transaction` derives the next
|
|
184
|
+
// hash as H(parent_hash, command), so passing a fresh zero-hash parent each
|
|
185
|
+
// time — as this binary and the old in-process tests both did — makes the
|
|
186
|
+
// resulting "state hash" a function of the last command alone, carrying no
|
|
187
|
+
// history. Two nodes that applied completely different sets of operations
|
|
188
|
+
// then report identical hashes as long as the most recent one matches, and
|
|
189
|
+
// a convergence check built on that comparison cannot fail.
|
|
190
|
+
//
|
|
191
|
+
// Chaining it makes the hash mean what its name says, and makes a node that
|
|
192
|
+
// missed an operation observably divergent. It lives beside the executor
|
|
193
|
+
// under one lock so a local commit and an arriving replication cannot
|
|
194
|
+
// interleave and chain onto a parent that is already stale.
|
|
195
|
+
let core = Arc::new(Mutex::new((executor, StateVersion::new(VectorClock::new(), "0".repeat(64)))));
|
|
196
|
+
let should_exit = Arc::new(AtomicBool::new(false));
|
|
197
|
+
let sequence = Arc::new(Mutex::new(1u64));
|
|
198
|
+
// Kept so `resend-last` can put the identical envelope back on the wire.
|
|
199
|
+
let last_envelope: Arc<Mutex<Option<feltdb::distributed_transactions::TransactionEnvelope>>> =
|
|
200
|
+
Arc::new(Mutex::new(None));
|
|
201
|
+
|
|
202
|
+
let send_links: Vec<SendLink> = send_specs
|
|
203
|
+
.into_iter()
|
|
204
|
+
.map(|(peer, addr)| {
|
|
205
|
+
let transport = TcpTransport::new("127.0.0.1:0".to_string());
|
|
206
|
+
SendLink { peer, addr, control: Arc::new(Mutex::new(transport.clone())), io: transport }
|
|
207
|
+
})
|
|
208
|
+
.collect();
|
|
209
|
+
|
|
210
|
+
let recv_links: Vec<RecvLink> = recv_specs
|
|
211
|
+
.into_iter()
|
|
212
|
+
.map(|(peer, addr)| {
|
|
213
|
+
let transport = TcpTransport::new(addr.clone());
|
|
214
|
+
RecvLink { peer, addr, control: Arc::new(Mutex::new(transport.clone())), io: transport }
|
|
215
|
+
})
|
|
216
|
+
.collect();
|
|
126
217
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
218
|
+
// Accept, serve, and accept again. One task per link, because each parks
|
|
219
|
+
// on its own socket and must not block the others.
|
|
220
|
+
//
|
|
221
|
+
// The re-listen is what makes a peer restart survivable. `TcpTransport`'s
|
|
222
|
+
// `listen()` binds, accepts exactly one connection, and drops the listener,
|
|
223
|
+
// so after that first accept the port is closed and a peer that reconnects
|
|
224
|
+
// is refused. Looping back to `listen()` once the connection ends rebinds
|
|
225
|
+
// the port for the next one. That is a harness-level fix: the transport
|
|
226
|
+
// keeps its single-connection shape and the protocol is untouched.
|
|
227
|
+
for link in &recv_links {
|
|
228
|
+
let control = link.control.clone();
|
|
229
|
+
let transport = link.io.clone();
|
|
230
|
+
let core = core.clone();
|
|
231
|
+
let exit = should_exit.clone();
|
|
232
|
+
let peer = link.peer.clone();
|
|
233
|
+
tokio::spawn(async move {
|
|
234
|
+
while !exit.load(Ordering::Relaxed) {
|
|
235
|
+
{
|
|
236
|
+
let mut control = control.lock().await;
|
|
237
|
+
if let Err(error) = control.listen().await {
|
|
238
|
+
eprintln!("listen for {peer} failed: {error}");
|
|
239
|
+
drop(control);
|
|
132
240
|
sleep(Duration::from_millis(100)).await;
|
|
133
241
|
continue;
|
|
134
242
|
}
|
|
135
243
|
}
|
|
136
|
-
};
|
|
137
244
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
245
|
+
// Serve this connection until it ends, then rebind for the next.
|
|
246
|
+
loop {
|
|
247
|
+
if exit.load(Ordering::Relaxed) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
match transport.receive().await {
|
|
251
|
+
Ok(messages) => {
|
|
252
|
+
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
|
+
{
|
|
264
|
+
version.state_hash = applied.to_string();
|
|
265
|
+
version.vector_clock.increment(&origin);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
Err(_) => break,
|
|
270
|
+
}
|
|
146
271
|
}
|
|
147
272
|
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
148
275
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
sleep(Duration::from_millis(500)).await;
|
|
276
|
+
// Establish outbound links before announcing readiness, so a test that
|
|
277
|
+
// writes immediately is not racing the topology.
|
|
278
|
+
for link in &send_links {
|
|
279
|
+
connect_with_retry(link, 20).await;
|
|
280
|
+
}
|
|
155
281
|
|
|
156
|
-
|
|
157
|
-
|
|
282
|
+
println!(
|
|
283
|
+
"STARTED {}",
|
|
284
|
+
json!({
|
|
285
|
+
"node": node_id,
|
|
286
|
+
"replayed": replayed,
|
|
287
|
+
"send": send_links.iter().map(|l| l.peer.clone()).collect::<Vec<_>>(),
|
|
288
|
+
"recv": recv_links.iter().map(|l| l.peer.clone()).collect::<Vec<_>>(),
|
|
289
|
+
})
|
|
290
|
+
);
|
|
291
|
+
io::stdout().flush().ok();
|
|
158
292
|
|
|
159
|
-
|
|
293
|
+
let (commands, mut incoming) = mpsc::channel::<String>(100);
|
|
160
294
|
tokio::task::spawn_blocking(move || {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if let Ok(cmd) = line {
|
|
166
|
-
let cmd_str = cmd.trim().to_string();
|
|
167
|
-
if !cmd_str.is_empty() {
|
|
168
|
-
// Non-blocking send, ignore if receiver is gone
|
|
169
|
-
let _ = tx.blocking_send(cmd_str);
|
|
170
|
-
}
|
|
295
|
+
for line in io::stdin().lock().lines().map_while(Result::ok) {
|
|
296
|
+
let line = line.trim().to_string();
|
|
297
|
+
if !line.is_empty() && commands.blocking_send(line).is_err() {
|
|
298
|
+
break;
|
|
171
299
|
}
|
|
172
300
|
}
|
|
173
301
|
});
|
|
174
302
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
303
|
+
while let Some(command) = incoming.recv().await {
|
|
304
|
+
let (verb, argument) = match command.split_once(' ') {
|
|
305
|
+
Some((verb, rest)) => (verb, rest.trim().to_string()),
|
|
306
|
+
None => (command.as_str(), String::new()),
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
match verb {
|
|
178
310
|
"execute-tx" => {
|
|
311
|
+
let record = if argument.is_empty() { "item".to_string() } else { argument };
|
|
179
312
|
let seq = {
|
|
180
|
-
let mut
|
|
181
|
-
let current = *
|
|
182
|
-
*
|
|
313
|
+
let mut guard = sequence.lock().await;
|
|
314
|
+
let current = *guard;
|
|
315
|
+
*guard += 1;
|
|
183
316
|
current
|
|
184
317
|
};
|
|
185
318
|
|
|
186
|
-
// Create a test operation
|
|
187
|
-
let mut parent_vc = VectorClock::new();
|
|
188
|
-
parent_vc.increment(&node_state.node_id);
|
|
189
|
-
let parent_version = StateVersion::new(parent_vc, "hash_initial".to_string());
|
|
190
|
-
|
|
191
319
|
let mut fields = HashMap::new();
|
|
192
|
-
fields.insert("
|
|
320
|
+
fields.insert("record".to_string(), json!(record.clone()));
|
|
193
321
|
|
|
194
|
-
let
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
322
|
+
let parent = { core.lock().await.1.clone() };
|
|
323
|
+
let operation = Operation::new(
|
|
324
|
+
OperationId::new(node_id.clone(), seq),
|
|
325
|
+
parent.clone(),
|
|
326
|
+
format!("tx_{record}"),
|
|
198
327
|
OperationCommand {
|
|
199
328
|
op_type: "set".to_string(),
|
|
200
329
|
collection: "items".to_string(),
|
|
201
|
-
record_id:
|
|
330
|
+
record_id: record.clone(),
|
|
202
331
|
fields,
|
|
203
332
|
},
|
|
204
|
-
|
|
333
|
+
node_id.clone(),
|
|
205
334
|
);
|
|
206
335
|
|
|
207
|
-
{
|
|
208
|
-
let mut
|
|
209
|
-
|
|
336
|
+
let envelope = {
|
|
337
|
+
let mut guard = core.lock().await;
|
|
338
|
+
let (executor, version) = &mut *guard;
|
|
339
|
+
let result = executor.execute_local_transaction(
|
|
210
340
|
seq,
|
|
211
|
-
format!("tx_{}"
|
|
212
|
-
|
|
213
|
-
vec![
|
|
341
|
+
format!("tx_{record}"),
|
|
342
|
+
version.clone(),
|
|
343
|
+
vec![operation],
|
|
214
344
|
ConsistencyContract::local(),
|
|
215
|
-
)
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
345
|
+
);
|
|
346
|
+
if result.is_ok() {
|
|
347
|
+
if let Some(replica) = executor.get_replica_state(&node_id) {
|
|
348
|
+
version.state_hash = replica.state_hash.to_string();
|
|
349
|
+
version.vector_clock.increment(&node_id);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
result
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
match envelope {
|
|
356
|
+
Ok(envelope) => {
|
|
357
|
+
*last_envelope.lock().await = Some(envelope.clone());
|
|
358
|
+
let mut delivered = Vec::new();
|
|
359
|
+
let mut failed = Vec::new();
|
|
360
|
+
for link in &send_links {
|
|
361
|
+
let message = ReplicationMessage::new(
|
|
362
|
+
envelope.clone(),
|
|
363
|
+
node_id.clone(),
|
|
364
|
+
link.peer.clone(),
|
|
225
365
|
seq,
|
|
226
366
|
);
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
367
|
+
let sent = link.io.send(message.clone()).await;
|
|
368
|
+
match sent {
|
|
369
|
+
Ok(()) => delivered.push(link.peer.clone()),
|
|
370
|
+
Err(_) => {
|
|
371
|
+
// The peer may have restarted, which drops
|
|
372
|
+
// our end of the socket. Reconnect and try
|
|
373
|
+
// once more before reporting a failure.
|
|
374
|
+
if connect_with_retry(link, 3).await {
|
|
375
|
+
if link.io.send(message).await.is_ok() {
|
|
376
|
+
delivered.push(link.peer.clone());
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
failed.push(link.peer.clone());
|
|
381
|
+
}
|
|
233
382
|
}
|
|
234
383
|
}
|
|
235
|
-
|
|
236
|
-
|
|
384
|
+
println!(
|
|
385
|
+
"TX_EXECUTED {}",
|
|
386
|
+
json!({ "record": record, "sequence": seq, "delivered": delivered, "failed": failed })
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
Err(error) => println!("ERROR {}", json!({ "reason": error })),
|
|
390
|
+
}
|
|
391
|
+
io::stdout().flush().ok();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Re-send the most recent envelope, unchanged, to every peer.
|
|
395
|
+
//
|
|
396
|
+
// Deduplication is a claimed property of the executor, and it is
|
|
397
|
+
// only interesting if the duplicate actually crosses the wire.
|
|
398
|
+
// Issuing another `execute-tx` would not test it: that produces a
|
|
399
|
+
// new envelope id and is a different transaction.
|
|
400
|
+
"resend-last" => {
|
|
401
|
+
let envelope = last_envelope.lock().await.clone();
|
|
402
|
+
match envelope {
|
|
403
|
+
Some(envelope) => {
|
|
404
|
+
let mut delivered = Vec::new();
|
|
405
|
+
for link in &send_links {
|
|
406
|
+
let message = ReplicationMessage::new(
|
|
407
|
+
envelope.clone(),
|
|
408
|
+
node_id.clone(),
|
|
409
|
+
link.peer.clone(),
|
|
410
|
+
envelope.envelope_id.sequence,
|
|
411
|
+
);
|
|
412
|
+
if link.io.send(message).await.is_ok() {
|
|
413
|
+
delivered.push(link.peer.clone());
|
|
414
|
+
}
|
|
237
415
|
}
|
|
416
|
+
println!(
|
|
417
|
+
"RESENT {}",
|
|
418
|
+
json!({ "envelope": envelope.envelope_id, "delivered": delivered })
|
|
419
|
+
);
|
|
238
420
|
}
|
|
421
|
+
None => println!("ERROR {}", json!({ "reason": "nothing sent yet" })),
|
|
239
422
|
}
|
|
240
|
-
|
|
423
|
+
io::stdout().flush().ok();
|
|
241
424
|
}
|
|
425
|
+
|
|
242
426
|
"query-state" => {
|
|
243
|
-
let
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
427
|
+
let guard = core.lock().await;
|
|
428
|
+
let (executor, _) = &*guard;
|
|
429
|
+
match executor.get_replica_state(&node_id) {
|
|
430
|
+
Some(replica) => println!(
|
|
431
|
+
"STATE {}",
|
|
432
|
+
json!({
|
|
433
|
+
"hash": replica.state_hash.to_string(),
|
|
434
|
+
"operations_applied": replica.operations_applied,
|
|
435
|
+
})
|
|
436
|
+
),
|
|
437
|
+
None => println!("ERROR {}", json!({ "reason": "no local replica" })),
|
|
438
|
+
}
|
|
439
|
+
io::stdout().flush().ok();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// The proof that the network carried the work. Counters come from
|
|
443
|
+
// the transport itself, so a test can assert bytes actually moved
|
|
444
|
+
// rather than inferring it from converged state.
|
|
445
|
+
"metrics" => {
|
|
446
|
+
let mut sent = json!({});
|
|
447
|
+
let mut received = json!({});
|
|
448
|
+
for link in &send_links {
|
|
449
|
+
let metrics = link.io.metrics();
|
|
450
|
+
sent[&link.peer] = json!({
|
|
451
|
+
"bytes": metrics.get_bytes_sent(),
|
|
452
|
+
"messages": metrics.get_messages_sent(),
|
|
453
|
+
"connection_changes": metrics.get_connection_state_changes(),
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
for link in &recv_links {
|
|
457
|
+
let metrics = link.io.metrics();
|
|
458
|
+
received[&link.peer] = json!({
|
|
459
|
+
"bytes": metrics.get_bytes_received(),
|
|
460
|
+
"messages": metrics.get_messages_received(),
|
|
461
|
+
"connection_changes": metrics.get_connection_state_changes(),
|
|
462
|
+
});
|
|
248
463
|
}
|
|
249
|
-
|
|
464
|
+
println!("METRICS {}", json!({ "sent": sent, "received": received }));
|
|
465
|
+
io::stdout().flush().ok();
|
|
250
466
|
}
|
|
467
|
+
|
|
468
|
+
"peers" => {
|
|
469
|
+
let mut state = json!({});
|
|
470
|
+
for link in &send_links {
|
|
471
|
+
state[format!("send:{}", link.peer)] = json!(link.io.is_connected());
|
|
472
|
+
}
|
|
473
|
+
for link in &recv_links {
|
|
474
|
+
state[format!("recv:{}", link.peer)] = json!(link.io.is_connected());
|
|
475
|
+
}
|
|
476
|
+
println!("PEERS {state}");
|
|
477
|
+
io::stdout().flush().ok();
|
|
478
|
+
}
|
|
479
|
+
|
|
251
480
|
"shutdown" => {
|
|
252
|
-
|
|
481
|
+
should_exit.store(true, Ordering::Relaxed);
|
|
253
482
|
println!("SHUTDOWN");
|
|
254
|
-
|
|
255
|
-
sleep(Duration::from_millis(
|
|
483
|
+
io::stdout().flush().ok();
|
|
484
|
+
sleep(Duration::from_millis(50)).await;
|
|
256
485
|
std::process::exit(0);
|
|
257
486
|
}
|
|
487
|
+
|
|
258
488
|
_ => {
|
|
259
|
-
println!("ERROR: unknown command");
|
|
260
|
-
|
|
489
|
+
println!("ERROR {}", json!({ "reason": format!("unknown command {verb}") }));
|
|
490
|
+
io::stdout().flush().ok();
|
|
261
491
|
}
|
|
262
492
|
}
|
|
263
493
|
}
|