@feltdb/core 0.8.4 → 0.8.6
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/create/package-versions.js +1 -1
- package/dist/create/server-source/Cargo.lock +165 -0
- package/dist/create/server-source/Cargo.toml +9 -0
- package/dist/create/server-source/crates/feltdb/Cargo.toml +3 -0
- package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +44 -44
- package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +12 -24
- package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +7 -13
- package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +13 -10
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +12 -9
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +78 -25
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +16 -13
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +13 -5
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +41 -22
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +33 -15
- package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +100 -33
- package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +56 -20
- package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +116 -41
- package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +186 -55
- package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +64 -26
- package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +46 -15
- package/dist/create/server-source/crates/feltdb/src/admission.rs +8 -15
- package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +43 -13
- package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +15 -42
- package/dist/create/server-source/crates/feltdb/src/analytics.rs +65 -19
- package/dist/create/server-source/crates/feltdb/src/application.rs +113 -30
- package/dist/create/server-source/crates/feltdb/src/authorization_security_tests.rs +475 -140
- package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +17 -15
- package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +0 -1
- package/dist/create/server-source/crates/feltdb/src/causal_backlog_bound.rs +59 -15
- package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +266 -114
- package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +25 -7
- package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +10 -15
- package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +3 -11
- package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +14 -5
- package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +21 -25
- package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +14 -11
- package/dist/create/server-source/crates/feltdb/src/dedup_bound_investigation.rs +103 -22
- package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +18 -15
- package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +12 -8
- package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +1 -5
- package/dist/create/server-source/crates/feltdb/src/durable_operation_identity.rs +87 -23
- package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +3 -7
- package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +10 -9
- package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +1 -6
- package/dist/create/server-source/crates/feltdb/src/indexing.rs +35 -38
- package/dist/create/server-source/crates/feltdb/src/lib.rs +1648 -46
- package/dist/create/server-source/crates/feltdb/src/managed_cas_tests.rs +4 -1
- package/dist/create/server-source/crates/feltdb/src/metrics.rs +0 -1
- package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +1 -2
- package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +107 -30
- package/dist/create/server-source/crates/feltdb/src/observability.rs +19 -6
- package/dist/create/server-source/crates/feltdb/src/operation.rs +39 -0
- package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +12 -11
- package/dist/create/server-source/crates/feltdb/src/operation_log.rs +9 -4
- package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +65 -18
- package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +193 -57
- package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +37 -27
- package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +38 -10
- package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +20 -14
- package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +394 -229
- package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +8 -6
- package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +11 -13
- package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +79 -70
- package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +3 -3
- package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +33 -11
- package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +6 -6
- package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +14 -56
- package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +29 -27
- package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +35 -17
- package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +8 -2
- package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +59 -15
- package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +70 -17
- package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +23 -6
- package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +12 -3
- package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +41 -11
- package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +56 -25
- package/dist/create/server-source/crates/feltdb/src/policy_evaluation.rs +701 -245
- package/dist/create/server-source/crates/feltdb/src/production_api.rs +31 -13
- package/dist/create/server-source/crates/feltdb/src/query_performance.rs +6 -8
- package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +5 -5
- package/dist/create/server-source/crates/feltdb/src/replica_acknowledgements.rs +48 -18
- package/dist/create/server-source/crates/feltdb/src/replica_membership.rs +30 -11
- package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +6 -3
- package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +4 -3
- package/dist/create/server-source/crates/feltdb/src/sharding.rs +36 -10
- package/dist/create/server-source/crates/feltdb/src/state_conflict_contract.rs +516 -0
- package/dist/create/server-source/crates/feltdb/src/state_contract.rs +13 -4
- package/dist/create/server-source/crates/feltdb/src/state_diff_contract.rs +222 -0
- package/dist/create/server-source/crates/feltdb/src/state_facade.rs +82 -54
- package/dist/create/server-source/crates/feltdb/src/state_hash.rs +2 -2
- package/dist/create/server-source/crates/feltdb/src/state_model.rs +1565 -536
- package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +6 -3
- package/dist/create/server-source/crates/feltdb/src/state_trigger.rs +672 -0
- package/dist/create/server-source/crates/feltdb/src/storage.rs +9 -3
- package/dist/create/server-source/crates/feltdb/src/submission.rs +5 -11
- package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +6 -8
- package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +24 -35
- package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +24 -8
- package/dist/create/server-source/crates/feltdb/src/transaction_preconditions.rs +248 -59
- package/dist/create/server-source/crates/feltdb/src/transactions.rs +17 -20
- package/dist/create/server-source/crates/feltdb/src/trigger_contract.rs +749 -0
- package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +1 -0
- package/dist/create/server-source/crates/feltdb/src/workload.rs +512 -4
- package/dist/create/server-source/crates/feltdb/src/workload_diagnostics.rs +3 -4
- package/dist/create/server-source/crates/feltdb/tests/bounded_read_contract.rs +132 -0
- package/dist/create/server-source/crates/feltdb/tests/branching_evidence.rs +299 -0
- package/dist/create/server-source/crates/feltdb/tests/compaction_stall_contract.rs +272 -0
- package/dist/create/server-source/crates/feltdb/tests/crash_durability_contract.rs +467 -0
- package/dist/create/server-source/crates/feltdb/tests/current_revision_authority_evidence.rs +309 -0
- package/dist/create/server-source/crates/feltdb/tests/durable_backup_contract.rs +445 -0
- package/dist/create/server-source/crates/feltdb/tests/durable_corruption_contract.rs +518 -0
- package/dist/create/server-source/crates/feltdb/tests/durable_format_compatibility.rs +392 -0
- package/dist/create/server-source/crates/feltdb/tests/feltdb_state_boundary_tests.rs +436 -220
- package/dist/create/server-source/crates/feltdb/tests/fixtures/state_conflict_contract_corpus.json +1916 -0
- package/dist/create/server-source/crates/feltdb/tests/fixtures/state_diff_contract_corpus.json +1878 -0
- package/dist/create/server-source/crates/feltdb/tests/fixtures/trigger_contract_corpus.json +1862 -0
- package/dist/create/server-source/crates/feltdb/tests/operational_health_contract.rs +278 -0
- package/dist/create/server-source/crates/feltdb/tests/pr34_query_collection.rs +2 -1
- package/dist/create/server-source/crates/feltdb/tests/pr35_equality_index.rs +80 -25
- package/dist/create/server-source/crates/feltdb/tests/pr7_self_authorization_proof.rs +5 -8
- package/dist/create/server-source/crates/feltdb/tests/pr8_vocabulary_assessment.rs +52 -44
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase2_boundary_tests.rs +33 -16
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3a_path_a_tests.rs +22 -7
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_authorized_mutations.rs +41 -22
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_role_based_authorization.rs +25 -8
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_simple_auth_delete.rs +9 -6
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_team_delete_role_authorization.rs +120 -69
- package/dist/create/server-source/crates/feltdb/tests/pr9_teams_role_based_access.rs +21 -10
- package/dist/create/server-source/crates/feltdb/tests/production_readiness_contract.rs +1365 -0
- package/dist/create/server-source/crates/feltdb/tests/reconciliation_application.rs +868 -0
- package/dist/create/server-source/crates/feltdb/tests/reconciliation_wire_format_evidence.rs +221 -0
- package/dist/create/server-source/crates/feltdb/tests/replicated_history_contract.rs +417 -0
- package/dist/create/server-source/crates/feltdb/tests/resource_scoped_revisions.rs +338 -0
- package/dist/create/server-source/crates/feltdb/tests/revision_identity_contract.rs +1039 -0
- package/dist/create/server-source/crates/feltdb/tests/revision_model_decision.rs +739 -0
- package/dist/create/server-source/crates/feltdb/tests/revision_retention_boundary_evidence.rs +427 -0
- package/dist/create/server-source/crates/feltdb/tests/saas_authorization_integration.rs +3 -3
- package/dist/create/server-source/crates/feltdb/tests/saas_invitation_lifecycle.rs +25 -22
- package/dist/create/server-source/crates/feltdb/tests/state_conflict_contract_conformance.rs +1799 -0
- package/dist/create/server-source/crates/feltdb/tests/state_diff_contract_conformance.rs +1316 -0
- package/dist/create/server-source/crates/feltdb/tests/state_model_integration.rs +53 -61
- package/dist/create/server-source/crates/feltdb/tests/state_persistence_integration.rs +156 -61
- package/dist/create/server-source/crates/feltdb/tests/state_store_boundary_evidence.rs +299 -0
- package/dist/create/server-source/crates/feltdb/tests/sync_divergence_evidence.rs +255 -0
- package/dist/create/server-source/crates/feltdb/tests/three_way_input_boundary_evidence.rs +249 -0
- package/dist/create/server-source/crates/feltdb/tests/trigger_contract_conformance.rs +994 -0
- package/dist/create/server-source/crates/feltdb/tests/workload_envelope_contract.rs +442 -0
- package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +16 -1
- package/dist/create/server-source/crates/feltdb-server/src/auth.rs +164 -13
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +695 -47
- package/dist/create/server-source/crates/feltdb-server/src/metrics.rs +21 -0
- package/dist/studio-app/assets/{feltdb_wasm-CVQWgXO-.js → feltdb_wasm-C1VhI-U5.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-C8HXbAXb.wasm +0 -0
- package/dist/studio-app/assets/{index-DwgNAIIX.js → index-Bbos1m2U.js} +1 -1
- package/dist/studio-app/index.html +1 -1
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/workload.d.ts +2 -1
- package/dist/workload.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-CNVpvaZV.wasm +0 -0
|
@@ -84,7 +84,9 @@ mod tests {
|
|
|
84
84
|
let record = node.build("tx-reject", operations);
|
|
85
85
|
|
|
86
86
|
match node.commit(&record)? {
|
|
87
|
-
ApplyOutcome::Rejected(RejectionReason::PreconditionFailed {
|
|
87
|
+
ApplyOutcome::Rejected(RejectionReason::PreconditionFailed {
|
|
88
|
+
operation_index, ..
|
|
89
|
+
}) => {
|
|
88
90
|
assert_eq!(operation_index, 1);
|
|
89
91
|
}
|
|
90
92
|
other => panic!("expected a precondition rejection, got {:?}", other),
|
|
@@ -93,18 +95,28 @@ mod tests {
|
|
|
93
95
|
// A rejected transaction must leave nothing behind, including the
|
|
94
96
|
// operations that were legal on their own.
|
|
95
97
|
assert_eq!(visible(&node, &record), 0, "no operation may be visible");
|
|
96
|
-
assert!(
|
|
97
|
-
|
|
98
|
+
assert!(
|
|
99
|
+
node.state().get_record("accounts", "A").is_none(),
|
|
100
|
+
"A must not be applied"
|
|
101
|
+
);
|
|
102
|
+
assert!(
|
|
103
|
+
node.state().get_record("accounts", "C").is_none(),
|
|
104
|
+
"C must not be applied"
|
|
105
|
+
);
|
|
98
106
|
|
|
99
107
|
drop(node);
|
|
100
108
|
let (restarted, report) = TransactionalNode::open(&path, "node1")?;
|
|
101
|
-
assert!(
|
|
109
|
+
assert!(
|
|
110
|
+
report.committed.is_empty(),
|
|
111
|
+
"a rejected transaction must never be logged"
|
|
112
|
+
);
|
|
102
113
|
assert!(restarted.state().get_record("accounts", "A").is_none());
|
|
103
114
|
Ok(())
|
|
104
115
|
}
|
|
105
116
|
|
|
106
117
|
#[test]
|
|
107
|
-
fn a_transaction_writing_one_record_twice_is_refused() -> Result<(), Box<dyn std::error::Error>>
|
|
118
|
+
fn a_transaction_writing_one_record_twice_is_refused() -> Result<(), Box<dyn std::error::Error>>
|
|
119
|
+
{
|
|
108
120
|
let dir = TempDir::new()?;
|
|
109
121
|
let (mut node, _) = TransactionalNode::open(dir.path().join("tx.log"), "node1")?;
|
|
110
122
|
|
|
@@ -137,8 +149,12 @@ mod tests {
|
|
|
137
149
|
CrashPoint::DuringWrite { keep_percent: 90 },
|
|
138
150
|
CrashPoint::AfterWriteBeforeFsync,
|
|
139
151
|
CrashPoint::AfterFsyncBeforeApply,
|
|
140
|
-
CrashPoint::DuringApply {
|
|
141
|
-
|
|
152
|
+
CrashPoint::DuringApply {
|
|
153
|
+
after_operations: 1,
|
|
154
|
+
},
|
|
155
|
+
CrashPoint::DuringApply {
|
|
156
|
+
after_operations: 2,
|
|
157
|
+
},
|
|
142
158
|
CrashPoint::AfterApply,
|
|
143
159
|
];
|
|
144
160
|
|
|
@@ -162,14 +178,22 @@ mod tests {
|
|
|
162
178
|
// Nothing durable was written, so nothing may be visible.
|
|
163
179
|
CrashPoint::BeforeWrite | CrashPoint::DuringWrite { .. } => {
|
|
164
180
|
assert!(!fully_visible, "{}: nothing should have committed", context);
|
|
165
|
-
assert!(
|
|
181
|
+
assert!(
|
|
182
|
+
report.committed.is_empty(),
|
|
183
|
+
"{}: log must hold no record",
|
|
184
|
+
context
|
|
185
|
+
);
|
|
166
186
|
}
|
|
167
187
|
// The record was fsynced, so recovery must replay all of it —
|
|
168
188
|
// including when the crash happened part-way through applying.
|
|
169
189
|
CrashPoint::AfterFsyncBeforeApply
|
|
170
190
|
| CrashPoint::DuringApply { .. }
|
|
171
191
|
| CrashPoint::AfterApply => {
|
|
172
|
-
assert!(
|
|
192
|
+
assert!(
|
|
193
|
+
fully_visible,
|
|
194
|
+
"{}: a committed transaction must fully recover",
|
|
195
|
+
context
|
|
196
|
+
);
|
|
173
197
|
assert_eq!(report.committed.len(), 1, "{}", context);
|
|
174
198
|
}
|
|
175
199
|
// Written but not fsynced: the operating system decides. Either
|
|
@@ -182,8 +206,8 @@ mod tests {
|
|
|
182
206
|
}
|
|
183
207
|
|
|
184
208
|
#[test]
|
|
185
|
-
fn a_torn_final_record_is_discarded_and_the_log_stays_writable(
|
|
186
|
-
-> Result<(), Box<dyn std::error::Error>> {
|
|
209
|
+
fn a_torn_final_record_is_discarded_and_the_log_stays_writable(
|
|
210
|
+
) -> Result<(), Box<dyn std::error::Error>> {
|
|
187
211
|
let dir = TempDir::new()?;
|
|
188
212
|
let path = dir.path().join("tx.log");
|
|
189
213
|
|
|
@@ -200,7 +224,10 @@ mod tests {
|
|
|
200
224
|
// Recovery keeps the committed transaction and discards the torn one.
|
|
201
225
|
let (mut node, report) = TransactionalNode::open(&path, "node1")?;
|
|
202
226
|
assert_eq!(report.committed.len(), 1);
|
|
203
|
-
assert!(matches!(
|
|
227
|
+
assert!(matches!(
|
|
228
|
+
report.discarded_tail,
|
|
229
|
+
Some(LogDefect::TornTail { .. })
|
|
230
|
+
));
|
|
204
231
|
assert_eq!(visible(&node, &first), 3);
|
|
205
232
|
|
|
206
233
|
// The torn bytes were truncated, so a later write cannot splice onto
|
|
@@ -210,7 +237,10 @@ mod tests {
|
|
|
210
237
|
drop(node);
|
|
211
238
|
|
|
212
239
|
let (again, report) = TransactionalNode::open(&path, "node1")?;
|
|
213
|
-
assert!(
|
|
240
|
+
assert!(
|
|
241
|
+
report.discarded_tail.is_none(),
|
|
242
|
+
"the log must be clean after truncation"
|
|
243
|
+
);
|
|
214
244
|
assert_eq!(report.committed.len(), 2);
|
|
215
245
|
assert_eq!(visible(&again, &first), 3);
|
|
216
246
|
assert_eq!(visible(&again, &third), 3);
|
|
@@ -218,7 +248,8 @@ mod tests {
|
|
|
218
248
|
}
|
|
219
249
|
|
|
220
250
|
#[test]
|
|
221
|
-
fn interior_corruption_is_refused_rather_than_guessed() -> Result<(), Box<dyn std::error::Error>>
|
|
251
|
+
fn interior_corruption_is_refused_rather_than_guessed() -> Result<(), Box<dyn std::error::Error>>
|
|
252
|
+
{
|
|
222
253
|
let dir = TempDir::new()?;
|
|
223
254
|
let path = dir.path().join("tx.log");
|
|
224
255
|
|
|
@@ -234,9 +265,19 @@ mod tests {
|
|
|
234
265
|
drop(node);
|
|
235
266
|
|
|
236
267
|
let outcome = TransactionalNode::open(&path, "node1");
|
|
237
|
-
assert!(
|
|
238
|
-
|
|
239
|
-
|
|
268
|
+
assert!(
|
|
269
|
+
outcome.is_err(),
|
|
270
|
+
"recovery must refuse a corrupted interior record"
|
|
271
|
+
);
|
|
272
|
+
let message = outcome
|
|
273
|
+
.err()
|
|
274
|
+
.map(|error| error.to_string())
|
|
275
|
+
.unwrap_or_default();
|
|
276
|
+
assert!(
|
|
277
|
+
message.contains("corrupted"),
|
|
278
|
+
"unexpected error: {}",
|
|
279
|
+
message
|
|
280
|
+
);
|
|
240
281
|
Ok(())
|
|
241
282
|
}
|
|
242
283
|
|
|
@@ -247,15 +288,23 @@ mod tests {
|
|
|
247
288
|
|
|
248
289
|
// Alter the body without recomputing the checksum, as a partial write
|
|
249
290
|
// or a bit flip would.
|
|
250
|
-
record.operations.push(TransactionOperation::set(
|
|
251
|
-
|
|
291
|
+
record.operations.push(TransactionOperation::set(
|
|
292
|
+
"accounts",
|
|
293
|
+
"D",
|
|
294
|
+
json!({ "balance": 9 }),
|
|
295
|
+
));
|
|
296
|
+
assert!(
|
|
297
|
+
!record.verify_checksum(),
|
|
298
|
+
"a mutated record must fail its integrity check"
|
|
299
|
+
);
|
|
252
300
|
Ok(())
|
|
253
301
|
}
|
|
254
302
|
|
|
255
303
|
// ---------------------------------------------------------------- replay
|
|
256
304
|
|
|
257
305
|
#[test]
|
|
258
|
-
fn replaying_a_committed_transaction_changes_nothing() -> Result<(), Box<dyn std::error::Error>>
|
|
306
|
+
fn replaying_a_committed_transaction_changes_nothing() -> Result<(), Box<dyn std::error::Error>>
|
|
307
|
+
{
|
|
259
308
|
let dir = TempDir::new()?;
|
|
260
309
|
let (mut node, _) = TransactionalNode::open(dir.path().join("tx.log"), "node1")?;
|
|
261
310
|
|
|
@@ -265,10 +314,17 @@ mod tests {
|
|
|
265
314
|
|
|
266
315
|
// Deliver the same transaction repeatedly.
|
|
267
316
|
for _ in 0..5 {
|
|
268
|
-
assert_eq!(
|
|
317
|
+
assert_eq!(
|
|
318
|
+
node.accept_replicated(&record)?,
|
|
319
|
+
ApplyOutcome::AlreadyApplied
|
|
320
|
+
);
|
|
269
321
|
}
|
|
270
322
|
|
|
271
|
-
assert_eq!(
|
|
323
|
+
assert_eq!(
|
|
324
|
+
node.state().hash().as_hex(),
|
|
325
|
+
hash_after_first,
|
|
326
|
+
"replay must not change state"
|
|
327
|
+
);
|
|
272
328
|
Ok(())
|
|
273
329
|
}
|
|
274
330
|
|
|
@@ -292,8 +348,10 @@ mod tests {
|
|
|
292
348
|
hashes.push(node.state().hash().as_hex().to_string());
|
|
293
349
|
}
|
|
294
350
|
|
|
295
|
-
assert!(
|
|
296
|
-
|
|
351
|
+
assert!(
|
|
352
|
+
hashes.windows(2).all(|pair| pair[0] == pair[1]),
|
|
353
|
+
"every restart must recover identical state"
|
|
354
|
+
);
|
|
297
355
|
Ok(())
|
|
298
356
|
}
|
|
299
357
|
|
|
@@ -320,7 +378,11 @@ mod tests {
|
|
|
320
378
|
assert_eq!(report.committed.len(), 3);
|
|
321
379
|
for index in 1..=3 {
|
|
322
380
|
let key = format!("A-tx{}", index);
|
|
323
|
-
assert!(
|
|
381
|
+
assert!(
|
|
382
|
+
node.state().get_record("accounts", &key).is_some(),
|
|
383
|
+
"tx{} lost",
|
|
384
|
+
index
|
|
385
|
+
);
|
|
324
386
|
}
|
|
325
387
|
Ok(())
|
|
326
388
|
}
|
|
@@ -353,15 +415,23 @@ mod tests {
|
|
|
353
415
|
}
|
|
354
416
|
|
|
355
417
|
// Sequences are unique and ordered, so the log defines one history.
|
|
356
|
-
let sequences: Vec<u64> = report
|
|
418
|
+
let sequences: Vec<u64> = report
|
|
419
|
+
.committed
|
|
420
|
+
.iter()
|
|
421
|
+
.map(|record| record.sequence)
|
|
422
|
+
.collect();
|
|
357
423
|
let unique: HashSet<u64> = sequences.iter().copied().collect();
|
|
358
424
|
assert_eq!(unique.len(), sequences.len(), "sequences must be unique");
|
|
359
|
-
assert!(
|
|
425
|
+
assert!(
|
|
426
|
+
sequences.windows(2).all(|pair| pair[0] < pair[1]),
|
|
427
|
+
"log must be ordered"
|
|
428
|
+
);
|
|
360
429
|
Ok(())
|
|
361
430
|
}
|
|
362
431
|
|
|
363
432
|
#[test]
|
|
364
|
-
fn a_losing_writer_is_rejected_without_partial_effect() -> Result<(), Box<dyn std::error::Error>>
|
|
433
|
+
fn a_losing_writer_is_rejected_without_partial_effect() -> Result<(), Box<dyn std::error::Error>>
|
|
434
|
+
{
|
|
365
435
|
let dir = TempDir::new()?;
|
|
366
436
|
let path = dir.path().join("tx.log");
|
|
367
437
|
let (mut node, _) = TransactionalNode::open(&path, "node1")?;
|
|
@@ -389,9 +459,14 @@ mod tests {
|
|
|
389
459
|
assert!(matches!(node.commit(&second)?, ApplyOutcome::Rejected(_)));
|
|
390
460
|
|
|
391
461
|
// The loser must leave nothing behind, including its uncontended write.
|
|
392
|
-
assert!(
|
|
393
|
-
"
|
|
394
|
-
|
|
462
|
+
assert!(
|
|
463
|
+
node.state().get_record("accounts", "only-b").is_none(),
|
|
464
|
+
"a rejected transaction must not apply its other operations"
|
|
465
|
+
);
|
|
466
|
+
assert_eq!(
|
|
467
|
+
node.state().get_record("accounts", "shared"),
|
|
468
|
+
Some(&json!({ "owner": "a" }))
|
|
469
|
+
);
|
|
395
470
|
drop(node);
|
|
396
471
|
|
|
397
472
|
let (recovered, report) = TransactionalNode::open(&path, "node1")?;
|
|
@@ -412,16 +487,36 @@ mod tests {
|
|
|
412
487
|
let record = leader.build("tx1", abc("tx1"));
|
|
413
488
|
assert_eq!(leader.commit(&record)?, ApplyOutcome::Applied);
|
|
414
489
|
|
|
415
|
-
assert_eq!(
|
|
416
|
-
|
|
490
|
+
assert_eq!(
|
|
491
|
+
follower_b.accept_replicated(&record)?,
|
|
492
|
+
ApplyOutcome::Applied
|
|
493
|
+
);
|
|
494
|
+
assert_eq!(
|
|
495
|
+
follower_c.accept_replicated(&record)?,
|
|
496
|
+
ApplyOutcome::Applied
|
|
497
|
+
);
|
|
417
498
|
|
|
418
499
|
// Duplicate delivery is safe.
|
|
419
|
-
assert_eq!(
|
|
420
|
-
|
|
500
|
+
assert_eq!(
|
|
501
|
+
follower_b.accept_replicated(&record)?,
|
|
502
|
+
ApplyOutcome::AlreadyApplied
|
|
503
|
+
);
|
|
504
|
+
assert_eq!(
|
|
505
|
+
follower_c.accept_replicated(&record)?,
|
|
506
|
+
ApplyOutcome::AlreadyApplied
|
|
507
|
+
);
|
|
421
508
|
|
|
422
509
|
let leader_hash = leader.state().hash().as_hex().to_string();
|
|
423
|
-
assert_eq!(
|
|
424
|
-
|
|
510
|
+
assert_eq!(
|
|
511
|
+
follower_b.state().hash().as_hex(),
|
|
512
|
+
leader_hash,
|
|
513
|
+
"B diverged"
|
|
514
|
+
);
|
|
515
|
+
assert_eq!(
|
|
516
|
+
follower_c.state().hash().as_hex(),
|
|
517
|
+
leader_hash,
|
|
518
|
+
"C diverged"
|
|
519
|
+
);
|
|
425
520
|
|
|
426
521
|
for node in [&follower_b, &follower_c] {
|
|
427
522
|
assert_atomic(node, &record, "replicated follower");
|
|
@@ -430,8 +525,8 @@ mod tests {
|
|
|
430
525
|
}
|
|
431
526
|
|
|
432
527
|
#[test]
|
|
433
|
-
fn a_follower_that_crashes_mid_replication_recovers_atomically(
|
|
434
|
-
-> Result<(), Box<dyn std::error::Error>> {
|
|
528
|
+
fn a_follower_that_crashes_mid_replication_recovers_atomically(
|
|
529
|
+
) -> Result<(), Box<dyn std::error::Error>> {
|
|
435
530
|
let dir = TempDir::new()?;
|
|
436
531
|
let leader_path = dir.path().join("a.log");
|
|
437
532
|
let follower_path = dir.path().join("b.log");
|
|
@@ -450,27 +545,40 @@ mod tests {
|
|
|
450
545
|
}
|
|
451
546
|
|
|
452
547
|
let (mut follower, report) = TransactionalNode::open(&follower_path, "nodeB")?;
|
|
453
|
-
assert!(
|
|
548
|
+
assert!(
|
|
549
|
+
report.committed.is_empty(),
|
|
550
|
+
"a torn replicated record must not commit"
|
|
551
|
+
);
|
|
454
552
|
assert_atomic(&follower, &record, "follower after torn write");
|
|
455
553
|
assert_eq!(follower.executor().visible_operations(&record), 0);
|
|
456
554
|
|
|
457
555
|
// Retransmission after recovery succeeds and converges.
|
|
458
556
|
assert_eq!(follower.accept_replicated(&record)?, ApplyOutcome::Applied);
|
|
459
557
|
let (leader, _) = TransactionalNode::open(&leader_path, "nodeA")?;
|
|
460
|
-
assert_eq!(
|
|
558
|
+
assert_eq!(
|
|
559
|
+
follower.state().hash().as_hex(),
|
|
560
|
+
leader.state().hash().as_hex()
|
|
561
|
+
);
|
|
461
562
|
Ok(())
|
|
462
563
|
}
|
|
463
564
|
|
|
464
565
|
#[test]
|
|
465
|
-
fn a_replicated_record_that_fails_integrity_is_refused(
|
|
566
|
+
fn a_replicated_record_that_fails_integrity_is_refused(
|
|
567
|
+
) -> Result<(), Box<dyn std::error::Error>> {
|
|
466
568
|
let dir = TempDir::new()?;
|
|
467
569
|
let (mut follower, _) = TransactionalNode::open(dir.path().join("b.log"), "nodeB")?;
|
|
468
570
|
|
|
469
571
|
let mut record = TransactionRecord::new("tx1", "nodeA", 1, abc("tx1"));
|
|
470
572
|
record.operations[0].record_id = "tampered".to_string();
|
|
471
573
|
|
|
472
|
-
assert!(matches!(
|
|
473
|
-
|
|
574
|
+
assert!(matches!(
|
|
575
|
+
follower.accept_replicated(&record)?,
|
|
576
|
+
ApplyOutcome::Rejected(_)
|
|
577
|
+
));
|
|
578
|
+
assert!(
|
|
579
|
+
follower.state().records.is_empty(),
|
|
580
|
+
"a tampered record must not apply"
|
|
581
|
+
);
|
|
474
582
|
Ok(())
|
|
475
583
|
}
|
|
476
584
|
|
|
@@ -490,11 +598,16 @@ mod tests {
|
|
|
490
598
|
|
|
491
599
|
let (mut node, report) = TransactionalNode::open(&path, "node1")?;
|
|
492
600
|
assert_eq!(report.committed[0].transaction_id, "stable-id");
|
|
493
|
-
assert!(
|
|
494
|
-
"
|
|
601
|
+
assert!(
|
|
602
|
+
node.executor().applied_transactions().contains("stable-id"),
|
|
603
|
+
"identity must be restored so replay stays idempotent"
|
|
604
|
+
);
|
|
495
605
|
|
|
496
606
|
// Redelivery after restart is still a no-op.
|
|
497
|
-
assert_eq!(
|
|
607
|
+
assert_eq!(
|
|
608
|
+
node.accept_replicated(&record)?,
|
|
609
|
+
ApplyOutcome::AlreadyApplied
|
|
610
|
+
);
|
|
498
611
|
Ok(())
|
|
499
612
|
}
|
|
500
613
|
|
|
@@ -526,23 +639,34 @@ mod tests {
|
|
|
526
639
|
drop(node);
|
|
527
640
|
|
|
528
641
|
let (recovered, _) = TransactionalNode::open(&path, "node1")?;
|
|
529
|
-
assert!(
|
|
530
|
-
|
|
531
|
-
|
|
642
|
+
assert!(
|
|
643
|
+
recovered.state().get_record("accounts", "old").is_none(),
|
|
644
|
+
"delete must persist"
|
|
645
|
+
);
|
|
646
|
+
assert!(
|
|
647
|
+
recovered.state().get_record("accounts", "new").is_some(),
|
|
648
|
+
"insert must persist"
|
|
649
|
+
);
|
|
650
|
+
assert!(
|
|
651
|
+
recovered.state().get_record("accounts", "keep").is_some(),
|
|
652
|
+
"untouched record kept"
|
|
653
|
+
);
|
|
532
654
|
Ok(())
|
|
533
655
|
}
|
|
534
656
|
|
|
535
657
|
/// A long mixed history under repeated crashes: no transaction may ever be
|
|
536
658
|
/// partially visible, at any point, under any interleaving.
|
|
537
659
|
#[test]
|
|
538
|
-
fn a_long_crash_ridden_history_never_exposes_a_partial_transaction(
|
|
539
|
-
-> Result<(), Box<dyn std::error::Error>> {
|
|
660
|
+
fn a_long_crash_ridden_history_never_exposes_a_partial_transaction(
|
|
661
|
+
) -> Result<(), Box<dyn std::error::Error>> {
|
|
540
662
|
let dir = TempDir::new()?;
|
|
541
663
|
let path = dir.path().join("tx.log");
|
|
542
664
|
let crashes = [
|
|
543
665
|
CrashPoint::AfterApply,
|
|
544
666
|
CrashPoint::DuringWrite { keep_percent: 30 },
|
|
545
|
-
CrashPoint::DuringApply {
|
|
667
|
+
CrashPoint::DuringApply {
|
|
668
|
+
after_operations: 2,
|
|
669
|
+
},
|
|
546
670
|
CrashPoint::BeforeWrite,
|
|
547
671
|
CrashPoint::AfterFsyncBeforeApply,
|
|
548
672
|
CrashPoint::DuringWrite { keep_percent: 80 },
|
|
@@ -567,14 +691,26 @@ mod tests {
|
|
|
567
691
|
// Final state: every transaction is wholly present or wholly absent,
|
|
568
692
|
// and the committed count matches what the log actually holds.
|
|
569
693
|
let (recovered, report) = TransactionalNode::open(&path, "node1")?;
|
|
570
|
-
let committed_ids: HashSet<String> =
|
|
571
|
-
|
|
694
|
+
let committed_ids: HashSet<String> = report
|
|
695
|
+
.committed
|
|
696
|
+
.iter()
|
|
697
|
+
.map(|record| record.transaction_id.clone())
|
|
698
|
+
.collect();
|
|
572
699
|
for record in &issued {
|
|
573
700
|
let seen = recovered.executor().visible_operations(record);
|
|
574
701
|
if committed_ids.contains(&record.transaction_id) {
|
|
575
|
-
assert_eq!(
|
|
702
|
+
assert_eq!(
|
|
703
|
+
seen,
|
|
704
|
+
record.operations.len(),
|
|
705
|
+
"{} committed but not whole",
|
|
706
|
+
record.transaction_id
|
|
707
|
+
);
|
|
576
708
|
} else {
|
|
577
|
-
assert_eq!(
|
|
709
|
+
assert_eq!(
|
|
710
|
+
seen, 0,
|
|
711
|
+
"{} not committed but visible",
|
|
712
|
+
record.transaction_id
|
|
713
|
+
);
|
|
578
714
|
}
|
|
579
715
|
}
|
|
580
716
|
Ok(())
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
///
|
|
19
19
|
/// Critical invariant: If A and B eventually receive all operations
|
|
20
20
|
/// (in any order), they converge to the same state_hash.
|
|
21
|
-
|
|
22
21
|
use crate::convergence::VectorClock;
|
|
23
22
|
use crate::state_hash::CanonicalState;
|
|
24
23
|
use std::collections::HashMap;
|
|
@@ -90,51 +89,53 @@ impl PartitionedReplica {
|
|
|
90
89
|
}
|
|
91
90
|
|
|
92
91
|
/// Apply an operation to partition A
|
|
93
|
-
pub fn apply_to_partition_a(
|
|
94
|
-
&mut self,
|
|
95
|
-
operation_id: &str,
|
|
96
|
-
record_id: &str,
|
|
97
|
-
value: i32,
|
|
98
|
-
) {
|
|
92
|
+
pub fn apply_to_partition_a(&mut self, operation_id: &str, record_id: &str, value: i32) {
|
|
99
93
|
if self.connected {
|
|
100
94
|
// Both partitions receive the operation
|
|
101
95
|
self.apply_to_both(operation_id, record_id, value);
|
|
102
96
|
} else {
|
|
103
97
|
// Only A receives it
|
|
104
|
-
self.partition_a
|
|
98
|
+
self.partition_a
|
|
99
|
+
.apply_operation(operation_id, record_id, value);
|
|
105
100
|
}
|
|
106
101
|
}
|
|
107
102
|
|
|
108
103
|
/// Apply an operation to partition B
|
|
109
|
-
pub fn apply_to_partition_b(
|
|
110
|
-
&mut self,
|
|
111
|
-
operation_id: &str,
|
|
112
|
-
record_id: &str,
|
|
113
|
-
value: i32,
|
|
114
|
-
) {
|
|
104
|
+
pub fn apply_to_partition_b(&mut self, operation_id: &str, record_id: &str, value: i32) {
|
|
115
105
|
if self.connected {
|
|
116
106
|
// Both partitions receive the operation
|
|
117
107
|
self.apply_to_both(operation_id, record_id, value);
|
|
118
108
|
} else {
|
|
119
109
|
// Only B receives it
|
|
120
|
-
self.partition_b
|
|
110
|
+
self.partition_b
|
|
111
|
+
.apply_operation(operation_id, record_id, value);
|
|
121
112
|
}
|
|
122
113
|
}
|
|
123
114
|
|
|
124
115
|
/// Apply operation to both partitions (when connected)
|
|
125
116
|
fn apply_to_both(&mut self, operation_id: &str, record_id: &str, value: i32) {
|
|
126
|
-
self.partition_a
|
|
127
|
-
|
|
117
|
+
self.partition_a
|
|
118
|
+
.apply_operation(operation_id, record_id, value);
|
|
119
|
+
self.partition_b
|
|
120
|
+
.apply_operation(operation_id, record_id, value);
|
|
128
121
|
}
|
|
129
122
|
|
|
130
123
|
/// Reconcile the two partitions using operation-based reconciliation
|
|
131
124
|
/// Ensures all operations from both partitions are applied to both
|
|
132
125
|
pub fn reconcile(&self) -> ReconciliationResult {
|
|
133
126
|
// Collect all operations from both partitions
|
|
134
|
-
let mut ops_from_a: std::collections::HashSet<_> =
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
127
|
+
let mut ops_from_a: std::collections::HashSet<_> = self
|
|
128
|
+
.partition_a
|
|
129
|
+
.applied_operations
|
|
130
|
+
.iter()
|
|
131
|
+
.cloned()
|
|
132
|
+
.collect();
|
|
133
|
+
let ops_from_b: std::collections::HashSet<_> = self
|
|
134
|
+
.partition_b
|
|
135
|
+
.applied_operations
|
|
136
|
+
.iter()
|
|
137
|
+
.cloned()
|
|
138
|
+
.collect();
|
|
138
139
|
|
|
139
140
|
// Find duplicates and unique ops
|
|
140
141
|
let duplicates: Vec<String> = ops_from_a
|
|
@@ -248,9 +249,7 @@ pub struct PartitionTrace {
|
|
|
248
249
|
|
|
249
250
|
impl PartitionDeterminismChecker {
|
|
250
251
|
pub fn new() -> Self {
|
|
251
|
-
Self {
|
|
252
|
-
traces: Vec::new(),
|
|
253
|
-
}
|
|
252
|
+
Self { traces: Vec::new() }
|
|
254
253
|
}
|
|
255
254
|
|
|
256
255
|
/// Record a scenario's outcome
|
|
@@ -414,7 +413,12 @@ mod tests {
|
|
|
414
413
|
);
|
|
415
414
|
|
|
416
415
|
let result = checker.verify_determinism();
|
|
417
|
-
assert_eq!(
|
|
416
|
+
assert_eq!(
|
|
417
|
+
result,
|
|
418
|
+
DeterminismCheckResult::Satisfied {
|
|
419
|
+
scenarios_checked: 2
|
|
420
|
+
}
|
|
421
|
+
);
|
|
418
422
|
}
|
|
419
423
|
|
|
420
424
|
#[test]
|
|
@@ -471,7 +475,13 @@ mod tests {
|
|
|
471
475
|
assert_eq!(partitioned.partition_b.applied_operations.len(), 3);
|
|
472
476
|
|
|
473
477
|
// But A still has op1 and B still has op2 (not synchronized yet)
|
|
474
|
-
assert!(partitioned
|
|
475
|
-
|
|
478
|
+
assert!(partitioned
|
|
479
|
+
.partition_a
|
|
480
|
+
.applied_operations
|
|
481
|
+
.contains(&"op1".to_string()));
|
|
482
|
+
assert!(partitioned
|
|
483
|
+
.partition_b
|
|
484
|
+
.applied_operations
|
|
485
|
+
.contains(&"op2".to_string()));
|
|
476
486
|
}
|
|
477
487
|
}
|