@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.
Files changed (99) hide show
  1. package/dist/cli/application.js +55 -88
  2. package/dist/cli/commands.js +206 -95
  3. package/dist/cli/index.js +1 -1
  4. package/dist/collection.d.ts +110 -2
  5. package/dist/collection.d.ts.map +1 -1
  6. package/dist/collection.js +228 -3
  7. package/dist/create/package-versions.js +1 -1
  8. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +408 -178
  9. package/dist/create/server-source/crates/feltdb/src/lib.rs +49 -5
  10. package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +659 -0
  11. package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +285 -0
  12. package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +582 -0
  13. package/dist/create/server-source/crates/feltdb-server/src/main.rs +141 -0
  14. package/dist/db.d.ts +59 -0
  15. package/dist/db.d.ts.map +1 -1
  16. package/dist/db.js +94 -2
  17. package/dist/embedded-transaction.d.ts +77 -0
  18. package/dist/embedded-transaction.d.ts.map +1 -0
  19. package/dist/embedded-transaction.js +102 -0
  20. package/dist/feltdb.d.ts +49 -1
  21. package/dist/feltdb.d.ts.map +1 -1
  22. package/dist/file-db.d.ts +33 -0
  23. package/dist/file-db.d.ts.map +1 -1
  24. package/dist/file-db.js +128 -4
  25. package/dist/freshness.d.ts +119 -0
  26. package/dist/freshness.d.ts.map +1 -0
  27. package/dist/freshness.js +115 -0
  28. package/dist/http-db.d.ts +48 -0
  29. package/dist/http-db.d.ts.map +1 -1
  30. package/dist/http-db.js +77 -1
  31. package/dist/index-backend.d.ts +8 -0
  32. package/dist/index-backend.d.ts.map +1 -1
  33. package/dist/index-backend.js +13 -0
  34. package/dist/index-core.d.ts +78 -0
  35. package/dist/index-core.d.ts.map +1 -0
  36. package/dist/index-core.js +72 -0
  37. package/dist/index-manager.d.ts +11 -0
  38. package/dist/index-manager.d.ts.map +1 -1
  39. package/dist/index-manager.js +30 -13
  40. package/dist/index-types.d.ts +17 -0
  41. package/dist/index-types.d.ts.map +1 -1
  42. package/dist/index.browser.d.ts +14 -0
  43. package/dist/index.browser.d.ts.map +1 -0
  44. package/dist/index.browser.js +13 -0
  45. package/dist/index.d.ts +8 -59
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +11 -54
  48. package/dist/indexeddb-db.d.ts +32 -0
  49. package/dist/indexeddb-db.d.ts.map +1 -1
  50. package/dist/indexeddb-db.js +151 -1
  51. package/dist/memory-db.d.ts +26 -0
  52. package/dist/memory-db.d.ts.map +1 -1
  53. package/dist/memory-db.js +86 -0
  54. package/dist/runtime-registry.d.ts +32 -0
  55. package/dist/runtime-registry.d.ts.map +1 -0
  56. package/dist/runtime-registry.js +38 -0
  57. package/dist/studio-app/assets/{feltdb_wasm-B4wq4mqp.js → feltdb_wasm-CJv3wHzi.js} +1 -1
  58. package/dist/studio-app/assets/feltdb_wasm_bg-C8TG8r2n.wasm +0 -0
  59. package/dist/studio-app/assets/index-D3rT0SJi.js +28 -0
  60. package/dist/studio-app/index.html +1 -1
  61. package/dist/transaction.d.ts +98 -0
  62. package/dist/transaction.d.ts.map +1 -0
  63. package/dist/transaction.js +86 -0
  64. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  65. package/dist/workspace/browser.d.ts +22 -0
  66. package/dist/workspace/browser.d.ts.map +1 -0
  67. package/dist/workspace/browser.js +17 -0
  68. package/dist/workspace/git-identity.d.ts +35 -0
  69. package/dist/workspace/git-identity.d.ts.map +1 -0
  70. package/dist/workspace/git-identity.js +81 -0
  71. package/dist/workspace/index.d.ts +11 -1
  72. package/dist/workspace/index.d.ts.map +1 -1
  73. package/dist/workspace/index.js +6 -0
  74. package/dist/workspace/investigation-analysis.d.ts +47 -0
  75. package/dist/workspace/investigation-analysis.d.ts.map +1 -0
  76. package/dist/workspace/investigation-analysis.js +181 -0
  77. package/dist/workspace/investigation-lifecycle-manager.d.ts +86 -0
  78. package/dist/workspace/investigation-lifecycle-manager.d.ts.map +1 -0
  79. package/dist/workspace/investigation-lifecycle-manager.js +229 -0
  80. package/dist/workspace/investigation-supervisor.d.ts +100 -0
  81. package/dist/workspace/investigation-supervisor.d.ts.map +1 -0
  82. package/dist/workspace/investigation-supervisor.js +236 -0
  83. package/dist/workspace/runtime-investigation.d.ts +1 -1
  84. package/dist/workspace/runtime-investigation.d.ts.map +1 -1
  85. package/dist/workspace/runtime-investigation.js +46 -21
  86. package/dist/workspace/runtime-observation.d.ts +79 -0
  87. package/dist/workspace/runtime-observation.d.ts.map +1 -0
  88. package/dist/workspace/runtime-observation.js +152 -0
  89. package/dist/workspace/runtime-observer.d.ts +51 -0
  90. package/dist/workspace/runtime-observer.d.ts.map +1 -0
  91. package/dist/workspace/runtime-observer.js +268 -0
  92. package/dist/workspace/workspace-connection.d.ts.map +1 -1
  93. package/dist/workspace/workspace-connection.js +14 -5
  94. package/dist/workspace/workspace-types.d.ts +113 -0
  95. package/dist/workspace/workspace-types.d.ts.map +1 -1
  96. package/package.json +12 -4
  97. package/dist/create/server-source/crates/feltdb/src/phase_1c_three_node.rs +0 -523
  98. package/dist/studio-app/assets/feltdb_wasm_bg-Ceyi7l21.wasm +0 -0
  99. package/dist/studio-app/assets/index-BF45M757.js +0 -28
@@ -1,11 +1,45 @@
1
- /// Standalone FeltDB node for OS process boundary testing
2
- ///
3
- /// Simplified runtime that:
4
- /// 1. Accepts command via stdin (execute-tx, query-state, shutdown)
5
- /// 2. Outputs state to stdout in JSON format
6
- /// 3. Runs replication over TCP in background
7
- /// 4. Executes local transactions and replicates to peers
8
- /// 5. Receives and applies replicated transactions from peers
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 NodeState {
28
- executor: Arc<Mutex<DistributedTransactionExecutor>>,
29
- node_id: String,
30
- transport: Arc<Mutex<TcpTransport>>,
31
- tx_sequence: Arc<Mutex<u64>>,
32
- is_server: bool,
33
- should_exit: Arc<AtomicBool>,
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
- // Parse arguments
41
- let mut mode = "server";
42
- let mut replication_port = 9001u16;
43
- let mut peer_addr = String::new();
44
-
45
- let mut i = 1;
46
- while i < args.len() {
47
- match args[i].as_str() {
48
- "--mode" => {
49
- i += 1;
50
- mode = &args[i];
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
- "--port" => {
53
- i += 1;
54
- replication_port = args[i].parse()?;
138
+ "--send" => {
139
+ index += 1;
140
+ send_specs.push(parse_link(args.get(index).ok_or("--send requires PEER=ADDR")?)?);
55
141
  }
56
- "--peer" => {
57
- i += 1;
58
- peer_addr = args[i].clone();
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
- i += 1;
148
+ index += 1;
63
149
  }
64
150
 
65
- let is_server = mode == "server";
66
- let node_id = if is_server {
67
- "node_a".to_string()
68
- } else {
69
- "node_b".to_string()
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
- // Initialize executor
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::new(node_id.clone(), initial_state.clone());
76
- executor.register_replica("node_a".to_string(), initial_state.clone());
77
- executor.register_replica("node_b".to_string(), initial_state.clone());
78
-
79
- let transport = if is_server {
80
- TcpTransport::new(format!("127.0.0.1:{}", replication_port))
81
- } else {
82
- TcpTransport::new("127.0.0.1:0".to_string())
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
- // Spawn background task to receive and apply replicated messages
120
- let ns = node_state.clone();
121
- tokio::spawn(async move {
122
- loop {
123
- if ns.should_exit.load(Ordering::Relaxed) {
124
- break;
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
- let messages = {
128
- let transport = ns.transport.lock().await;
129
- match transport.receive().await {
130
- Ok(msgs) => msgs,
131
- Err(_) => {
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
- for msg in messages {
139
- let mut executor = ns.executor.lock().await;
140
- let mut parent_vc = VectorClock::new();
141
- parent_vc.increment(&ns.node_id);
142
- let parent_version = StateVersion::new(parent_vc, "hash_initial".to_string());
143
-
144
- if let Err(e) = executor.receive_replicated_transaction(msg, parent_version) {
145
- eprintln!("ERROR: apply replication failed: {}", e);
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
- sleep(Duration::from_millis(50)).await;
150
- }
151
- });
152
-
153
- // Give replication setup time to establish
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
- // Create a channel for stdin commands
157
- let (tx, mut rx) = mpsc::channel::<String>(100);
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
- // Spawn a blocking task to read stdin
293
+ let (commands, mut incoming) = mpsc::channel::<String>(100);
160
294
  tokio::task::spawn_blocking(move || {
161
- let stdin = io::stdin();
162
- let handle = stdin.lock();
163
-
164
- for line in handle.lines() {
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
- // Process commands from the channel
176
- while let Some(cmd) = rx.recv().await {
177
- match cmd.trim() {
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 s = node_state.tx_sequence.lock().await;
181
- let current = *s;
182
- *s += 1;
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("key".to_string(), json!("value"));
320
+ fields.insert("record".to_string(), json!(record.clone()));
193
321
 
194
- let op = Operation::new(
195
- OperationId::new(node_state.node_id.clone(), seq),
196
- parent_version.clone(),
197
- format!("tx_{}", seq),
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: format!("item{}", seq),
330
+ record_id: record.clone(),
202
331
  fields,
203
332
  },
204
- node_state.node_id.clone(),
333
+ node_id.clone(),
205
334
  );
206
335
 
207
- {
208
- let mut executor = node_state.executor.lock().await;
209
- match executor.execute_local_transaction(
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_{}", seq),
212
- parent_version,
213
- vec![op],
341
+ format!("tx_{record}"),
342
+ version.clone(),
343
+ vec![operation],
214
344
  ConsistencyContract::local(),
215
- ) {
216
- Ok(envelope) => {
217
- let replication_msg = ReplicationMessage::new(
218
- envelope,
219
- node_state.node_id.clone(),
220
- if node_state.node_id == "node_a" {
221
- "node_b".to_string()
222
- } else {
223
- "node_a".to_string()
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
- let transport = node_state.transport.lock().await;
229
- if let Err(e) = transport.send(replication_msg).await {
230
- println!("ERROR: send replication failed: {}", e);
231
- } else {
232
- println!("TX_EXECUTED");
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
- Err(e) => {
236
- println!("ERROR: execute transaction failed: {}", e);
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
- let _ = std::io::stdout().flush();
423
+ io::stdout().flush().ok();
241
424
  }
425
+
242
426
  "query-state" => {
243
- let executor = node_state.executor.lock().await;
244
- if let Some(replica) = executor.get_replica_state(&node_state.node_id) {
245
- println!("STATE:{},{}", replica.state_hash, replica.operations_applied);
246
- } else {
247
- println!("ERROR: node not found");
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
- let _ = std::io::stdout().flush();
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
- node_state.should_exit.store(true, Ordering::Relaxed);
481
+ should_exit.store(true, Ordering::Relaxed);
253
482
  println!("SHUTDOWN");
254
- let _ = std::io::stdout().flush();
255
- sleep(Duration::from_millis(100)).await;
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
- let _ = std::io::stdout().flush();
489
+ println!("ERROR {}", json!({ "reason": format!("unknown command {verb}") }));
490
+ io::stdout().flush().ok();
261
491
  }
262
492
  }
263
493
  }