@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.
Files changed (83) hide show
  1. package/README.md +32 -0
  2. package/dist/agent-registry.js +1 -3
  3. package/dist/agent-runtime.js +8 -7
  4. package/dist/analytics-backend.js +3 -1
  5. package/dist/application-contract.js +1 -0
  6. package/dist/application-manifest.js +1 -0
  7. package/dist/artifact.js +2 -0
  8. package/dist/authorization.js +2 -0
  9. package/dist/bundle.js +2 -0
  10. package/dist/capability.js +1 -3
  11. package/dist/cell.js +9 -4
  12. package/dist/cli/commands.js +26 -2
  13. package/dist/cli/index.js +1 -1
  14. package/dist/collection.js +39 -31
  15. package/dist/create/package-versions.js +1 -1
  16. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +613 -27
  17. package/dist/create/server-source/crates/feltdb/src/causal_backlog_bound.rs +452 -0
  18. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +208 -0
  19. package/dist/create/server-source/crates/feltdb/src/convergence.rs +16 -0
  20. package/dist/create/server-source/crates/feltdb/src/dedup_bound_investigation.rs +402 -0
  21. package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +784 -24
  22. package/dist/create/server-source/crates/feltdb/src/durable_operation_identity.rs +418 -0
  23. package/dist/create/server-source/crates/feltdb/src/lib.rs +284 -0
  24. package/dist/create/server-source/crates/feltdb/src/replica_acknowledgements.rs +471 -0
  25. package/dist/create/server-source/crates/feltdb/src/replica_membership.rs +661 -0
  26. package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +72 -3
  27. package/dist/create/server-source/crates/feltdb/src/transaction_preconditions.rs +899 -0
  28. package/dist/create/server-source/crates/feltdb-server/src/main.rs +116 -4
  29. package/dist/db.d.ts +2 -2
  30. package/dist/db.d.ts.map +1 -1
  31. package/dist/db.js +48 -15
  32. package/dist/development-runtime-bridge.js +1 -1
  33. package/dist/distributed-indexing.js +7 -5
  34. package/dist/embedded-transaction.d.ts +9 -0
  35. package/dist/embedded-transaction.d.ts.map +1 -1
  36. package/dist/embedded-transaction.js +95 -3
  37. package/dist/feltdb.d.ts +16 -0
  38. package/dist/feltdb.d.ts.map +1 -1
  39. package/dist/file-db.d.ts.map +1 -1
  40. package/dist/file-db.js +9 -3
  41. package/dist/flowspec.js +2 -1
  42. package/dist/http-client.js +2 -0
  43. package/dist/http-db.d.ts +11 -0
  44. package/dist/http-db.d.ts.map +1 -1
  45. package/dist/http-db.js +32 -3
  46. package/dist/identity.js +1 -0
  47. package/dist/index-analytics.js +6 -7
  48. package/dist/index-backend.js +3 -3
  49. package/dist/index-dashboard.js +10 -13
  50. package/dist/index-manager.js +12 -11
  51. package/dist/index-monitoring.js +9 -4
  52. package/dist/index-store.js +2 -0
  53. package/dist/indexeddb-db.d.ts.map +1 -1
  54. package/dist/indexeddb-db.js +32 -23
  55. package/dist/memory-db.d.ts.map +1 -1
  56. package/dist/memory-db.js +8 -4
  57. package/dist/observe.js +2 -0
  58. package/dist/provider.js +2 -0
  59. package/dist/query-planner.js +2 -4
  60. package/dist/reactive-graph.js +6 -8
  61. package/dist/release.js +2 -0
  62. package/dist/sharding.js +11 -6
  63. package/dist/state-contract.js +3 -3
  64. package/dist/studio-app/assets/{feltdb_wasm-CJv3wHzi.js → feltdb_wasm-CD744e5D.js} +1 -1
  65. package/dist/studio-app/assets/feltdb_wasm_bg-CiIXhOLi.wasm +0 -0
  66. package/dist/studio-app/assets/index-DoROs8yx.js +28 -0
  67. package/dist/studio-app/index.html +1 -1
  68. package/dist/sync-contract.js +9 -2
  69. package/dist/telemetry.d.ts.map +1 -1
  70. package/dist/telemetry.js +32 -12
  71. package/dist/transaction.d.ts +127 -9
  72. package/dist/transaction.d.ts.map +1 -1
  73. package/dist/transaction.js +91 -5
  74. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  75. package/dist/worker.js +2 -0
  76. package/dist/workload.js +2 -0
  77. package/dist/workspace/development-node.js +11 -10
  78. package/dist/workspace/investigation-lifecycle-manager.js +2 -0
  79. package/dist/workspace/investigation-supervisor.js +5 -3
  80. package/dist/workspace/workspace-connection.js +14 -5
  81. package/package.json +1 -1
  82. package/dist/studio-app/assets/feltdb_wasm_bg-C8TG8r2n.wasm +0 -0
  83. package/dist/studio-app/assets/index-DospFFYE.js +0 -28
@@ -0,0 +1,452 @@
1
+ //! The causal backlog bound, exercised through the production receive path.
2
+ //!
3
+ //! Every test here drives `DistributedTransactionExecutor::receive_replicated`
4
+ //! and `try_execute_local_transaction` -- the entry points the shipping node
5
+ //! binary calls. None of them constructs a `CausalDependencyBarrier` and fills
6
+ //! it directly.
7
+ //!
8
+ //! That restriction is the point, and it is written down because the previous
9
+ //! evidence for this property did exactly the opposite: Gate 14B built a
10
+ //! barrier, filled it, computed `backlog_depth * 1500 / 1024`, and reported the
11
+ //! result as a memory measurement. It found linear growth because linear growth
12
+ //! was the arithmetic it had been given. A new harness that also constructed a
13
+ //! barrier would repeat that defect in a fresh file, so these tests reach the
14
+ //! backlog the way an operation reaches it.
15
+ //!
16
+ //! What is asserted is behaviour at the bound, not a predicted footprint. Where
17
+ //! a number is reported it is one that was observed.
18
+ //!
19
+ //! The contract under test is `docs/architecture/causal-backlog-bound.md` §4.
20
+
21
+ #[cfg(test)]
22
+ mod tests {
23
+ use crate::convergence::VectorClock;
24
+ use crate::distributed_transactions::{
25
+ CausalCapacity, DistributedTransactionExecutor, ReceiveOutcome, ReplicationMessage,
26
+ SubmitOutcome, TransactionEnvelope,
27
+ };
28
+ use crate::state_hash::StateHash;
29
+ use crate::transactions::{
30
+ ConsistencyContract, Operation, OperationCommand, OperationId, StateVersion,
31
+ };
32
+ use std::collections::HashMap;
33
+
34
+ fn zero_version() -> StateVersion {
35
+ StateVersion::new(VectorClock::new(), "0".repeat(64))
36
+ }
37
+
38
+ /// An envelope from `origin` at `sequence`, carrying the causal history
39
+ /// `{origin: sequence}` -- so its dependency is `{origin: sequence - 1}`,
40
+ /// exactly as the production stamping produces.
41
+ fn envelope_at(origin: &str, sequence: u64, payload: &str) -> TransactionEnvelope {
42
+ let mut fields = HashMap::new();
43
+ fields.insert("record".to_string(), serde_json::json!(payload));
44
+ let operation = Operation::new(
45
+ OperationId::new(origin.to_string(), sequence),
46
+ zero_version(),
47
+ format!("tx_{payload}"),
48
+ OperationCommand {
49
+ op_type: "set".to_string(),
50
+ collection: "items".to_string(),
51
+ record_id: payload.to_string(),
52
+ fields,
53
+ },
54
+ origin.to_string(),
55
+ );
56
+
57
+ let mut envelope = TransactionEnvelope::new(
58
+ origin.to_string(),
59
+ sequence,
60
+ format!("tx_{payload}"),
61
+ vec![operation],
62
+ ConsistencyContract::local(),
63
+ );
64
+ let mut clock = VectorClock::new();
65
+ clock.clocks.insert(origin.to_string(), sequence);
66
+ envelope.vector_clock = clock;
67
+ envelope
68
+ }
69
+
70
+ fn message(envelope: TransactionEnvelope) -> ReplicationMessage {
71
+ let origin = envelope.originating_node.clone();
72
+ let sequence = envelope.envelope_id.sequence;
73
+ ReplicationMessage::new(envelope, origin, "receiver".to_string(), sequence)
74
+ }
75
+
76
+ /// A receiver with a durable log, which every one of these tests needs:
77
+ /// a deferred or pending envelope is replayed from the log, not from RAM.
78
+ fn receiver(capacity: CausalCapacity) -> (DistributedTransactionExecutor, tempfile::TempDir) {
79
+ let dir = tempfile::tempdir().expect("tempdir");
80
+ let mut executor = DistributedTransactionExecutor::with_log(
81
+ "receiver".to_string(),
82
+ StateHash::from_hex("0".repeat(64)),
83
+ Some(dir.path().join("operations.jsonl")),
84
+ )
85
+ .expect("executor with log");
86
+ executor.set_causal_capacity(capacity);
87
+ (executor, dir)
88
+ }
89
+
90
+ /// Deliver `count` envelopes that can never become eligible, because the
91
+ /// dependency they name is never sent. This is how the backlog is filled
92
+ /// through the real path.
93
+ fn fill_with_blocked(
94
+ executor: &mut DistributedTransactionExecutor,
95
+ origin: &str,
96
+ count: usize,
97
+ ) -> Vec<ReceiveOutcome> {
98
+ (0..count)
99
+ .map(|index| {
100
+ // Sequence starts at 2 so the dependency {origin: 1} is never
101
+ // satisfied: envelope 1 is deliberately never delivered.
102
+ let sequence = (index as u64) + 2;
103
+ executor
104
+ .receive_replicated(
105
+ message(envelope_at(origin, sequence, &format!("r{sequence}"))),
106
+ zero_version(),
107
+ )
108
+ .expect("receive")
109
+ })
110
+ .collect()
111
+ }
112
+
113
+ #[test]
114
+ fn causal_backlog_fills_to_just_under_the_entry_bound() {
115
+ let (mut executor, _dir) = receiver(CausalCapacity::new(8, usize::MAX));
116
+ let outcomes = fill_with_blocked(&mut executor, "A", 7);
117
+
118
+ assert!(
119
+ outcomes
120
+ .iter()
121
+ .all(|outcome| matches!(outcome, ReceiveOutcome::PendingDependencies { .. })),
122
+ "everything below the bound is buffered"
123
+ );
124
+ assert_eq!(executor.pending_causal_count(), 7);
125
+ assert_eq!(executor.deferred_causal_count(), 0);
126
+ }
127
+
128
+ #[test]
129
+ fn causal_backlog_accepts_the_exact_entry_bound() {
130
+ // The bound is inclusive: `<=`, not `<`. Filling exactly to it is
131
+ // accepted, and the breach is the step past it.
132
+ let (mut executor, _dir) = receiver(CausalCapacity::new(8, usize::MAX));
133
+ let outcomes = fill_with_blocked(&mut executor, "A", 8);
134
+
135
+ assert!(matches!(
136
+ outcomes.last().expect("an outcome"),
137
+ ReceiveOutcome::PendingDependencies { .. }
138
+ ));
139
+ assert_eq!(executor.pending_causal_count(), 8);
140
+ assert_eq!(executor.deferred_causal_count(), 0);
141
+ }
142
+
143
+ #[test]
144
+ fn causal_backlog_defers_one_entry_past_the_bound() {
145
+ let (mut executor, _dir) = receiver(CausalCapacity::new(8, usize::MAX));
146
+ let outcomes = fill_with_blocked(&mut executor, "A", 9);
147
+
148
+ assert!(
149
+ matches!(outcomes[8], ReceiveOutcome::CapacityDeferred { .. }),
150
+ "the ninth entry breaches an eight-entry bound, got {:?}",
151
+ outcomes[8]
152
+ );
153
+ assert_eq!(executor.pending_causal_count(), 8, "the bound holds");
154
+ assert_eq!(executor.deferred_causal_count(), 1);
155
+ }
156
+
157
+ #[test]
158
+ fn causal_backlog_defers_one_byte_past_the_byte_bound() {
159
+ // Size the byte bound from a real envelope rather than a guessed
160
+ // constant: measure what two envelopes actually occupy, then set the
161
+ // bound one byte below it.
162
+ let probe = envelope_at("A", 2, "r2");
163
+ let probe_bytes = serde_json::to_string(&probe).expect("serialize").len();
164
+
165
+ let (mut executor, _dir) =
166
+ receiver(CausalCapacity::new(usize::MAX, probe_bytes * 2 - 1));
167
+ let outcomes = fill_with_blocked(&mut executor, "A", 3);
168
+
169
+ assert!(matches!(outcomes[0], ReceiveOutcome::PendingDependencies { .. }));
170
+ assert!(
171
+ matches!(outcomes[1], ReceiveOutcome::CapacityDeferred { .. }),
172
+ "the second envelope breaches a bound one byte short of two, got {:?}",
173
+ outcomes[1]
174
+ );
175
+ assert_eq!(executor.pending_causal_count(), 1);
176
+ assert!(executor.pending_causal_payload_bytes() <= probe_bytes * 2 - 1);
177
+ }
178
+
179
+ #[test]
180
+ fn causal_backlog_charges_a_large_envelope_its_actual_bytes() {
181
+ // A byte bound is only a bound if a big operation costs more than a
182
+ // small one. Two envelopes of very different sizes, against a bound
183
+ // that admits the small one and not the large one.
184
+ let small = envelope_at("A", 2, "s");
185
+ let large = envelope_at("A", 3, &"L".repeat(4096));
186
+ let small_bytes = serde_json::to_string(&small).expect("serialize").len();
187
+ let large_bytes = serde_json::to_string(&large).expect("serialize").len();
188
+ assert!(large_bytes > small_bytes * 2, "the fixture must actually differ in size");
189
+
190
+ let (mut executor, _dir) = receiver(CausalCapacity::new(usize::MAX, small_bytes + 16));
191
+
192
+ let first = executor
193
+ .receive_replicated(message(small), zero_version())
194
+ .expect("receive");
195
+ assert!(matches!(first, ReceiveOutcome::PendingDependencies { .. }));
196
+
197
+ let second = executor
198
+ .receive_replicated(message(large), zero_version())
199
+ .expect("receive");
200
+ assert!(
201
+ matches!(second, ReceiveOutcome::CapacityDeferred { .. }),
202
+ "a large envelope must consume its actual byte count, got {second:?}"
203
+ );
204
+ }
205
+
206
+ #[test]
207
+ fn causal_backlog_refuses_local_work_at_the_bound_without_admitting_it() {
208
+ let (mut executor, _dir) = receiver(CausalCapacity::new(4, usize::MAX));
209
+ fill_with_blocked(&mut executor, "A", 4);
210
+ assert_eq!(executor.pending_causal_count(), 4);
211
+
212
+ let applied_before = executor
213
+ .get_replica_state("receiver")
214
+ .map(|replica| replica.operations_applied)
215
+ .unwrap_or(0);
216
+
217
+ let mut fields = HashMap::new();
218
+ fields.insert("record".to_string(), serde_json::json!("local"));
219
+ let operation = Operation::new(
220
+ OperationId::new("receiver".to_string(), 1),
221
+ zero_version(),
222
+ "tx_local".to_string(),
223
+ OperationCommand {
224
+ op_type: "set".to_string(),
225
+ collection: "items".to_string(),
226
+ record_id: "local".to_string(),
227
+ fields,
228
+ },
229
+ "receiver".to_string(),
230
+ );
231
+
232
+ let outcome = executor
233
+ .try_execute_local_transaction(
234
+ 1,
235
+ "tx_local".to_string(),
236
+ zero_version(),
237
+ vec![operation],
238
+ ConsistencyContract::local(),
239
+ )
240
+ .expect("submit");
241
+
242
+ match outcome {
243
+ SubmitOutcome::CapacityExceeded { limit_entries, pending_entries, .. } => {
244
+ assert_eq!(limit_entries, 4);
245
+ assert_eq!(pending_entries, 4);
246
+ }
247
+ SubmitOutcome::Admitted(_) => panic!("local work must be refused at the bound"),
248
+ }
249
+
250
+ // Refused work never appears as applied, and never reaches the log.
251
+ let applied_after = executor
252
+ .get_replica_state("receiver")
253
+ .map(|replica| replica.operations_applied)
254
+ .unwrap_or(0);
255
+ assert_eq!(applied_after, applied_before, "a refusal applies nothing");
256
+ assert!(
257
+ !executor
258
+ .causal_barrier
259
+ .pending_keys()
260
+ .iter()
261
+ .any(|key| key.starts_with("receiver:")),
262
+ "a refusal buffers nothing"
263
+ );
264
+ }
265
+
266
+ #[test]
267
+ fn causal_backlog_never_evicts_and_admits_deferred_work_when_it_drains() {
268
+ // The whole point of deferring rather than refusing: nothing is lost.
269
+ // Once the dependency arrives and the backlog drains, the deferred
270
+ // envelope is admitted from the log and applied.
271
+ let (mut executor, _dir) = receiver(CausalCapacity::new(2, usize::MAX));
272
+
273
+ let outcomes = fill_with_blocked(&mut executor, "A", 3);
274
+ assert!(matches!(outcomes[2], ReceiveOutcome::CapacityDeferred { .. }));
275
+ assert_eq!(executor.deferred_causal_count(), 1);
276
+
277
+ // Deliver the missing dependency A:1, which releases the whole chain.
278
+ let released = executor
279
+ .receive_replicated(message(envelope_at("A", 1, "r1")), zero_version())
280
+ .expect("receive");
281
+ assert!(matches!(released, ReceiveOutcome::Applied { .. }));
282
+
283
+ assert_eq!(executor.pending_causal_count(), 0, "nothing is left waiting");
284
+ assert_eq!(executor.deferred_causal_count(), 0, "nothing is left deferred");
285
+ let applied = executor
286
+ .get_replica_state("receiver")
287
+ .map(|replica| replica.operations_applied)
288
+ .unwrap_or(0);
289
+ assert_eq!(applied, 4, "all four envelopes applied, none evicted");
290
+ }
291
+
292
+ #[test]
293
+ fn causal_backlog_holds_order_under_pressure() {
294
+ // Causal correctness is not traded away at capacity: nothing is applied
295
+ // before its dependencies, whatever the bound is doing.
296
+ let (mut executor, _dir) = receiver(CausalCapacity::new(2, usize::MAX));
297
+ fill_with_blocked(&mut executor, "A", 4);
298
+
299
+ let applied = executor
300
+ .get_replica_state("receiver")
301
+ .map(|replica| replica.operations_applied)
302
+ .unwrap_or(0);
303
+ assert_eq!(applied, 0, "nothing may be applied while its dependency is missing");
304
+ assert_eq!(executor.causal_frontier().get("A"), 0);
305
+ }
306
+
307
+ #[test]
308
+ fn causal_backlog_deduplicates_a_redelivered_blocked_entry() {
309
+ let (mut executor, _dir) = receiver(CausalCapacity::new(8, usize::MAX));
310
+
311
+ let first = executor
312
+ .receive_replicated(message(envelope_at("A", 2, "r2")), zero_version())
313
+ .expect("receive");
314
+ assert!(matches!(first, ReceiveOutcome::PendingDependencies { .. }));
315
+
316
+ let again = executor
317
+ .receive_replicated(message(envelope_at("A", 2, "r2")), zero_version())
318
+ .expect("receive");
319
+ assert_eq!(again, ReceiveOutcome::AlreadyKnown);
320
+ assert_eq!(executor.pending_causal_count(), 1, "one logical operation");
321
+ }
322
+
323
+ #[test]
324
+ fn causal_backlog_deduplicates_a_redelivered_deferred_entry() {
325
+ // A deferred envelope is durable but unbuffered, so it needs its own
326
+ // dedup answer: without one, a retransmission would append it to the
327
+ // log a second time.
328
+ let (mut executor, _dir) = receiver(CausalCapacity::new(1, usize::MAX));
329
+ let outcomes = fill_with_blocked(&mut executor, "A", 2);
330
+ assert!(matches!(outcomes[1], ReceiveOutcome::CapacityDeferred { .. }));
331
+
332
+ let again = executor
333
+ .receive_replicated(message(envelope_at("A", 3, "r3")), zero_version())
334
+ .expect("receive");
335
+ assert_eq!(again, ReceiveOutcome::AlreadyKnown);
336
+ assert_eq!(executor.deferred_causal_count(), 1, "still one deferred entry");
337
+ }
338
+
339
+ #[test]
340
+ fn causal_backlog_reports_current_and_maximum_occupancy() {
341
+ let (mut executor, _dir) = receiver(CausalCapacity::new(8, usize::MAX));
342
+ fill_with_blocked(&mut executor, "A", 5);
343
+
344
+ let (max_entries, max_bytes) = executor.max_causal_occupancy();
345
+ assert_eq!(executor.pending_causal_count(), 5);
346
+ assert_eq!(max_entries, 5);
347
+ assert!(max_bytes > 0, "payload bytes are reported, not assumed");
348
+
349
+ // Drain it, and the current gauge falls while the maximum does not --
350
+ // which is the reason the maximum is reported at all.
351
+ executor
352
+ .receive_replicated(message(envelope_at("A", 1, "r1")), zero_version())
353
+ .expect("receive");
354
+ assert_eq!(executor.pending_causal_count(), 0);
355
+ assert_eq!(
356
+ executor.max_causal_occupancy().0,
357
+ 5,
358
+ "a backlog that filled and drained between two reads must still be visible"
359
+ );
360
+ }
361
+
362
+ #[test]
363
+ fn causal_backlog_survives_restart_with_deferred_and_pending_entries() {
364
+ // Durability is the property the bound must not cost. A blocked
365
+ // envelope is fsynced before it is buffered, and a deferred one is
366
+ // fsynced before it is deferred, so a restart rebuilds both from the
367
+ // log rather than losing whichever the bound had excluded.
368
+ let dir = tempfile::tempdir().expect("tempdir");
369
+ let log_path = dir.path().join("operations.jsonl");
370
+
371
+ {
372
+ let mut executor = DistributedTransactionExecutor::with_log(
373
+ "receiver".to_string(),
374
+ StateHash::from_hex("0".repeat(64)),
375
+ Some(log_path.clone()),
376
+ )
377
+ .expect("executor");
378
+ executor.set_causal_capacity(CausalCapacity::new(2, usize::MAX));
379
+ let outcomes = fill_with_blocked(&mut executor, "A", 3);
380
+ assert!(matches!(outcomes[2], ReceiveOutcome::CapacityDeferred { .. }));
381
+ assert_eq!(executor.pending_causal_count(), 2);
382
+ assert_eq!(executor.deferred_causal_count(), 1);
383
+ }
384
+
385
+ // Restart: a fresh executor over the same log.
386
+ let mut recovered = DistributedTransactionExecutor::with_log(
387
+ "receiver".to_string(),
388
+ StateHash::from_hex("0".repeat(64)),
389
+ Some(log_path.clone()),
390
+ )
391
+ .expect("executor");
392
+ recovered.set_causal_capacity(CausalCapacity::new(2, usize::MAX));
393
+ let replayed = recovered.load_from_disk(log_path).expect("recover");
394
+
395
+ assert_eq!(replayed, 3, "all three envelopes were durable before the restart");
396
+ assert_eq!(
397
+ recovered.pending_causal_count(),
398
+ 2,
399
+ "recovery honours the same bound the live path does"
400
+ );
401
+ assert_eq!(recovered.deferred_causal_count(), 1, "the excess is deferred, not lost");
402
+
403
+ // And the dependency still releases everything after the restart.
404
+ recovered
405
+ .receive_replicated(message(envelope_at("A", 1, "r1")), zero_version())
406
+ .expect("receive");
407
+ assert_eq!(recovered.pending_causal_count(), 0);
408
+ assert_eq!(recovered.deferred_causal_count(), 0);
409
+ assert_eq!(
410
+ recovered
411
+ .get_replica_state("receiver")
412
+ .map(|replica| replica.operations_applied)
413
+ .unwrap_or(0),
414
+ 4,
415
+ "every envelope that was durable before the crash is applied after it"
416
+ );
417
+ }
418
+
419
+ #[test]
420
+ fn causal_backlog_recovery_is_deterministic() {
421
+ // Behaviour at capacity must not depend on iteration order. The same
422
+ // log and the same bound must produce the same split twice.
423
+ let dir = tempfile::tempdir().expect("tempdir");
424
+ let log_path = dir.path().join("operations.jsonl");
425
+
426
+ {
427
+ let mut executor = DistributedTransactionExecutor::with_log(
428
+ "receiver".to_string(),
429
+ StateHash::from_hex("0".repeat(64)),
430
+ Some(log_path.clone()),
431
+ )
432
+ .expect("executor");
433
+ executor.set_causal_capacity(CausalCapacity::new(10, usize::MAX));
434
+ fill_with_blocked(&mut executor, "A", 6);
435
+ }
436
+
437
+ let split = |bound: usize| {
438
+ let mut executor = DistributedTransactionExecutor::with_log(
439
+ "receiver".to_string(),
440
+ StateHash::from_hex("0".repeat(64)),
441
+ Some(log_path.clone()),
442
+ )
443
+ .expect("executor");
444
+ executor.set_causal_capacity(CausalCapacity::new(bound, usize::MAX));
445
+ executor.load_from_disk(log_path.clone()).expect("recover");
446
+ executor.causal_barrier.pending_keys()
447
+ };
448
+
449
+ assert_eq!(split(3), split(3), "the same log and bound recover identically");
450
+ assert_eq!(split(3).len(), 3);
451
+ }
452
+ }