@feltdb/core 0.4.12 → 0.4.13
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/commands.js +5 -2
- package/dist/cli/index.js +1 -1
- package/dist/create/cli.js +16 -1
- package/dist/create/create.js +30 -16
- package/dist/create/docker-compose-generator.js +48 -9
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/Cargo.lock +2238 -0
- package/dist/create/server-source/Cargo.toml +7 -0
- package/dist/create/server-source/crates/feltdb/Cargo.lock +175 -0
- package/dist/create/server-source/crates/feltdb/Cargo.toml +79 -0
- package/dist/create/server-source/crates/feltdb/benches/baselines/gate-13-redux.json +370 -0
- package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +589 -0
- package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +259 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +446 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +272 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +378 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +414 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +486 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +273 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +587 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +513 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +395 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +469 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +409 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +627 -0
- package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +383 -0
- package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +298 -0
- package/dist/create/server-source/crates/feltdb/src/acceptance_tests.rs +1698 -0
- package/dist/create/server-source/crates/feltdb/src/acquisition.rs +286 -0
- package/dist/create/server-source/crates/feltdb/src/admission.rs +192 -0
- package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +477 -0
- package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +566 -0
- package/dist/create/server-source/crates/feltdb/src/analytics.rs +475 -0
- package/dist/create/server-source/crates/feltdb/src/application.rs +2244 -0
- package/dist/create/server-source/crates/feltdb/src/application_runtime.rs +1070 -0
- package/dist/create/server-source/crates/feltdb/src/authorization.rs +1030 -0
- package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +266 -0
- package/dist/create/server-source/crates/feltdb/src/capabilities/mod.rs +8 -0
- package/dist/create/server-source/crates/feltdb/src/capabilities/search.rs +418 -0
- package/dist/create/server-source/crates/feltdb/src/capabilities/vector.rs +482 -0
- package/dist/create/server-source/crates/feltdb/src/capability.rs +903 -0
- package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +261 -0
- package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +78 -0
- package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +2214 -0
- package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +194 -0
- package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +427 -0
- package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +453 -0
- package/dist/create/server-source/crates/feltdb/src/content_distribution.rs +465 -0
- package/dist/create/server-source/crates/feltdb/src/convergence.rs +618 -0
- package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +418 -0
- package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +380 -0
- package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +362 -0
- package/dist/create/server-source/crates/feltdb/src/cron.rs +294 -0
- package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +474 -0
- package/dist/create/server-source/crates/feltdb/src/distributed_tests.rs +1003 -0
- package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +536 -0
- package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +364 -0
- package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +235 -0
- package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +207 -0
- package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +316 -0
- package/dist/create/server-source/crates/feltdb/src/execution.rs +426 -0
- package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +219 -0
- package/dist/create/server-source/crates/feltdb/src/indexing.rs +779 -0
- package/dist/create/server-source/crates/feltdb/src/lib.rs +2838 -0
- package/dist/create/server-source/crates/feltdb/src/materialization.rs +184 -0
- package/dist/create/server-source/crates/feltdb/src/metrics.rs +267 -0
- package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +311 -0
- package/dist/create/server-source/crates/feltdb/src/observability.rs +366 -0
- package/dist/create/server-source/crates/feltdb/src/operation.rs +251 -0
- package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +438 -0
- package/dist/create/server-source/crates/feltdb/src/operation_log.rs +344 -0
- package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +477 -0
- package/dist/create/server-source/crates/feltdb/src/peer_registry.rs +166 -0
- package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +261 -0
- package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +560 -0
- package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +3226 -0
- package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +201 -0
- package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +263 -0
- package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +484 -0
- package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +216 -0
- package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +281 -0
- package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +323 -0
- package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +573 -0
- package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +404 -0
- package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +418 -0
- package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +381 -0
- package/dist/create/server-source/crates/feltdb/src/phase_1c_three_node.rs +523 -0
- package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +334 -0
- package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +306 -0
- package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +355 -0
- package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +395 -0
- package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +346 -0
- package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +430 -0
- package/dist/create/server-source/crates/feltdb/src/production_api.rs +400 -0
- package/dist/create/server-source/crates/feltdb/src/provenance.rs +207 -0
- package/dist/create/server-source/crates/feltdb/src/query_performance.rs +217 -0
- package/dist/create/server-source/crates/feltdb/src/references.rs +404 -0
- package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +401 -0
- package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +160 -0
- package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +132 -0
- package/dist/create/server-source/crates/feltdb/src/routing.rs +409 -0
- package/dist/create/server-source/crates/feltdb/src/sharding.rs +523 -0
- package/dist/create/server-source/crates/feltdb/src/state_contract.rs +3118 -0
- package/dist/create/server-source/crates/feltdb/src/state_hash.rs +291 -0
- package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +376 -0
- package/dist/create/server-source/crates/feltdb/src/storage.rs +267 -0
- package/dist/create/server-source/crates/feltdb/src/submission.rs +477 -0
- package/dist/create/server-source/crates/feltdb/src/sync.rs +483 -0
- package/dist/create/server-source/crates/feltdb/src/sync_contract.rs +822 -0
- package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +366 -0
- package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +473 -0
- package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +949 -0
- package/dist/create/server-source/crates/feltdb/src/transactions.rs +646 -0
- package/dist/create/server-source/crates/feltdb/src/trigger.rs +390 -0
- package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +928 -0
- package/dist/create/server-source/crates/feltdb/src/workflow.rs +713 -0
- package/dist/create/server-source/crates/feltdb/src/workflow_acceptance_tests.rs +510 -0
- package/dist/create/server-source/crates/feltdb/src/workflow_integration.rs +354 -0
- package/dist/create/server-source/crates/feltdb/src/workflow_runtime.rs +594 -0
- package/dist/create/server-source/crates/feltdb/src/workload.rs +1310 -0
- package/dist/create/server-source/crates/feltdb-server/Cargo.toml +23 -0
- package/dist/create/server-source/crates/feltdb-server/README.md +79 -0
- package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +73 -0
- package/dist/create/server-source/crates/feltdb-server/src/application_contract.rs +425 -0
- package/dist/create/server-source/crates/feltdb-server/src/artifacts.rs +449 -0
- package/dist/create/server-source/crates/feltdb-server/src/audit.rs +59 -0
- package/dist/create/server-source/crates/feltdb-server/src/auth.rs +308 -0
- package/dist/create/server-source/crates/feltdb-server/src/authorization.rs +228 -0
- package/dist/create/server-source/crates/feltdb-server/src/backup.rs +416 -0
- package/dist/create/server-source/crates/feltdb-server/src/causal.rs +218 -0
- package/dist/create/server-source/crates/feltdb-server/src/certification.rs +223 -0
- package/dist/create/server-source/crates/feltdb-server/src/clock.rs +132 -0
- package/dist/create/server-source/crates/feltdb-server/src/cluster.rs +165 -0
- package/dist/create/server-source/crates/feltdb-server/src/connections.rs +1044 -0
- package/dist/create/server-source/crates/feltdb-server/src/content.rs +111 -0
- package/dist/create/server-source/crates/feltdb-server/src/identity.rs +250 -0
- package/dist/create/server-source/crates/feltdb-server/src/key_management.rs +78 -0
- package/dist/create/server-source/crates/feltdb-server/src/key_provider.rs +247 -0
- package/dist/create/server-source/crates/feltdb-server/src/leases.rs +123 -0
- package/dist/create/server-source/crates/feltdb-server/src/lib.rs +25 -0
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +8715 -0
- package/dist/create/server-source/crates/feltdb-server/src/metrics.rs +97 -0
- package/dist/create/server-source/crates/feltdb-server/src/portable_bundle.rs +350 -0
- package/dist/create/server-source/crates/feltdb-server/src/principals.rs +510 -0
- package/dist/create/server-source/crates/feltdb-server/src/providers.rs +269 -0
- package/dist/create/server-source/crates/feltdb-server/src/releases.rs +2563 -0
- package/dist/create/server-source/crates/feltdb-server/src/sessions.rs +156 -0
- package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +1097 -0
- package/dist/create/server-source/crates/feltdb-server/src/versions.rs +264 -0
- package/dist/create/server-source/crates/feltdb-wasm/Cargo.toml +31 -0
- package/dist/create/server-source/crates/feltdb-wasm/src/lib.rs +1086 -0
- package/dist/create/server-source/crates/feltdb-wasm/test.db +0 -0
- package/dist/flowspec.d.ts +2 -0
- package/dist/flowspec.d.ts.map +1 -1
- package/dist/flowspec.js +28 -4
- package/dist/state-contract.d.ts +7 -1
- package/dist/state-contract.d.ts.map +1 -1
- package/dist/studio/components/ApplicationDesigner.d.ts.map +1 -1
- package/dist/studio/components/index.js +1 -1
- package/dist/studio/{components-Q0Nx6gLE.js → components-bHTARBin.js} +156 -46
- package/dist/studio/index.js +31 -28
- package/dist/studio-app/assets/{index-BHOmLZF-.js → index-BqKquLuU.js} +8 -8
- package/dist/studio-app/index.html +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/// Crash-Atomic Boundary Testing (Gate 3)
|
|
2
|
+
///
|
|
3
|
+
/// Verifies the critical invariant: State mutation and OperationId persistence
|
|
4
|
+
/// are crash-atomic with respect to recovery.
|
|
5
|
+
///
|
|
6
|
+
/// The dangerous boundary:
|
|
7
|
+
///
|
|
8
|
+
/// apply state mutation
|
|
9
|
+
/// ↓
|
|
10
|
+
/// CRASH 💥
|
|
11
|
+
/// ↓
|
|
12
|
+
/// persist OperationId to dedup set
|
|
13
|
+
///
|
|
14
|
+
/// After recovery, MUST have either:
|
|
15
|
+
/// - state reflects operation AND OperationId in dedup set, OR
|
|
16
|
+
/// - state doesn't reflect operation AND OperationId not in dedup set
|
|
17
|
+
///
|
|
18
|
+
/// Never:
|
|
19
|
+
/// - state reflects op but dedup missing (double-apply on retry)
|
|
20
|
+
/// - dedup has op but state wasn't mutated (phantom operation)
|
|
21
|
+
|
|
22
|
+
use crate::state_hash::CanonicalState;
|
|
23
|
+
use std::collections::HashSet;
|
|
24
|
+
|
|
25
|
+
/// Simulates crash points in state/dedup boundary
|
|
26
|
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
27
|
+
pub enum CrashBoundaryPoint {
|
|
28
|
+
/// Crash before state mutation begins
|
|
29
|
+
BeforeStateMutation,
|
|
30
|
+
/// Crash after state mutation, before dedup persistence
|
|
31
|
+
AfterStateMutationBeforeDedupPersist,
|
|
32
|
+
/// Crash after dedup persistence complete
|
|
33
|
+
AfterDedupPersist,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// Simulates operation application with recovery points
|
|
37
|
+
#[derive(Clone, Debug)]
|
|
38
|
+
pub struct CrashAtomicExecutor {
|
|
39
|
+
state: CanonicalState,
|
|
40
|
+
dedup_set: HashSet<String>, // OperationId set
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
impl CrashAtomicExecutor {
|
|
44
|
+
pub fn new() -> Self {
|
|
45
|
+
Self {
|
|
46
|
+
state: CanonicalState::new(),
|
|
47
|
+
dedup_set: HashSet::new(),
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Apply operation with potential crash at specified point
|
|
52
|
+
pub fn apply_with_crash(
|
|
53
|
+
&mut self,
|
|
54
|
+
operation_id: &str,
|
|
55
|
+
record_id: &str,
|
|
56
|
+
value: i32,
|
|
57
|
+
) -> Result<(), String> {
|
|
58
|
+
// Check for duplicate before applying
|
|
59
|
+
if self.dedup_set.contains(operation_id) {
|
|
60
|
+
return Err(format!("Operation {} already applied (dedup hit)", operation_id));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Simulate state mutation
|
|
64
|
+
self.mutate_state(record_id, value);
|
|
65
|
+
|
|
66
|
+
// Simulate dedup persistence
|
|
67
|
+
self.dedup_set.insert(operation_id.to_string());
|
|
68
|
+
|
|
69
|
+
Ok(())
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Apply operation and simulate crash at specified point
|
|
73
|
+
/// Returns the state at the time of crash (for recovery testing)
|
|
74
|
+
pub fn apply_with_simulated_crash(
|
|
75
|
+
&mut self,
|
|
76
|
+
operation_id: &str,
|
|
77
|
+
record_id: &str,
|
|
78
|
+
value: i32,
|
|
79
|
+
crash_point: CrashBoundaryPoint,
|
|
80
|
+
) -> CrashRecoveryState {
|
|
81
|
+
match crash_point {
|
|
82
|
+
CrashBoundaryPoint::BeforeStateMutation => {
|
|
83
|
+
// Crash before anything happens
|
|
84
|
+
CrashRecoveryState {
|
|
85
|
+
state_mutated: false,
|
|
86
|
+
dedup_persisted: false,
|
|
87
|
+
operation_id: operation_id.to_string(),
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
CrashBoundaryPoint::AfterStateMutationBeforeDedupPersist => {
|
|
91
|
+
// Mutate state but crash before dedup
|
|
92
|
+
self.mutate_state(record_id, value);
|
|
93
|
+
CrashRecoveryState {
|
|
94
|
+
state_mutated: true,
|
|
95
|
+
dedup_persisted: false,
|
|
96
|
+
operation_id: operation_id.to_string(),
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
CrashBoundaryPoint::AfterDedupPersist => {
|
|
100
|
+
// Mutate state and persist dedup
|
|
101
|
+
self.mutate_state(record_id, value);
|
|
102
|
+
self.dedup_set.insert(operation_id.to_string());
|
|
103
|
+
CrashRecoveryState {
|
|
104
|
+
state_mutated: true,
|
|
105
|
+
dedup_persisted: true,
|
|
106
|
+
operation_id: operation_id.to_string(),
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// Recover from crash state
|
|
113
|
+
/// Returns whether the operation should be reapplied
|
|
114
|
+
pub fn recover(&self, crash_state: &CrashRecoveryState) -> RecoveryDecision {
|
|
115
|
+
let in_dedup = self.dedup_set.contains(&crash_state.operation_id);
|
|
116
|
+
|
|
117
|
+
match (crash_state.state_mutated, crash_state.dedup_persisted, in_dedup) {
|
|
118
|
+
// State mutated, dedup persisted → fully applied, don't retry
|
|
119
|
+
(true, true, true) => RecoveryDecision::AlreadyApplied,
|
|
120
|
+
|
|
121
|
+
// State mutated, dedup not persisted, not in dedup set → partially applied, needs recovery
|
|
122
|
+
(true, false, false) => RecoveryDecision::PartiallyApplied {
|
|
123
|
+
reason: "State mutated but dedup lost in crash".to_string(),
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
// State mutated, dedup not persisted, but in dedup set → inconsistent
|
|
127
|
+
(true, false, true) => RecoveryDecision::InconsistentState {
|
|
128
|
+
reason: "State mutated but dedup signal false, yet in dedup set".to_string(),
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
// State not mutated, dedup persisted → phantom state, needs investigation
|
|
132
|
+
(false, true, true) => RecoveryDecision::PhantomOperation {
|
|
133
|
+
reason: "Dedup persisted but state mutation didn't complete".to_string(),
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
// Nothing persisted → safe to retry
|
|
137
|
+
(false, false, false) => RecoveryDecision::SafeToRetry,
|
|
138
|
+
|
|
139
|
+
// Inconsistent states
|
|
140
|
+
(true, true, false) => RecoveryDecision::InconsistentState {
|
|
141
|
+
reason: "State shows applied but dedup missing".to_string(),
|
|
142
|
+
},
|
|
143
|
+
(false, false, true) => RecoveryDecision::InconsistentState {
|
|
144
|
+
reason: "Dedup has operation but state not applied".to_string(),
|
|
145
|
+
},
|
|
146
|
+
(false, true, false) => RecoveryDecision::InconsistentState {
|
|
147
|
+
reason: "Dedup persisted signal true but not in set".to_string(),
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// Verify the crash-atomic invariant
|
|
153
|
+
/// After recovery, either (state AND dedup) or (neither)
|
|
154
|
+
pub fn verify_invariant(&self, crash_state: &CrashRecoveryState) -> InvariantCheckResult {
|
|
155
|
+
let in_dedup = self.dedup_set.contains(&crash_state.operation_id);
|
|
156
|
+
|
|
157
|
+
// Both true or both false → invariant satisfied
|
|
158
|
+
if crash_state.state_mutated == in_dedup {
|
|
159
|
+
InvariantCheckResult::Satisfied
|
|
160
|
+
} else {
|
|
161
|
+
InvariantCheckResult::Violated {
|
|
162
|
+
state_mutated: crash_state.state_mutated,
|
|
163
|
+
dedup_persisted: in_dedup,
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
fn mutate_state(&mut self, record_id: &str, value: i32) {
|
|
169
|
+
self.state.set_record(
|
|
170
|
+
"test_collection".to_string(),
|
|
171
|
+
record_id.to_string(),
|
|
172
|
+
serde_json::json!({ "value": value }),
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
pub fn state(&self) -> &CanonicalState {
|
|
177
|
+
&self.state
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
pub fn dedup_set(&self) -> &HashSet<String> {
|
|
181
|
+
&self.dedup_set
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/// State at time of crash (what we recover from)
|
|
186
|
+
#[derive(Clone, Debug)]
|
|
187
|
+
pub struct CrashRecoveryState {
|
|
188
|
+
pub state_mutated: bool,
|
|
189
|
+
pub dedup_persisted: bool,
|
|
190
|
+
pub operation_id: String,
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/// Decision about what to do after recovery
|
|
194
|
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
195
|
+
pub enum RecoveryDecision {
|
|
196
|
+
/// Operation fully applied, don't retry
|
|
197
|
+
AlreadyApplied,
|
|
198
|
+
/// Operation partially applied, needs recovery action
|
|
199
|
+
PartiallyApplied { reason: String },
|
|
200
|
+
/// Operation marked as applied but state missing (data corruption)
|
|
201
|
+
PhantomOperation { reason: String },
|
|
202
|
+
/// Safe to retry the operation
|
|
203
|
+
SafeToRetry,
|
|
204
|
+
/// Inconsistent state detected
|
|
205
|
+
InconsistentState { reason: String },
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/// Result of verifying the crash-atomic invariant
|
|
209
|
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
210
|
+
pub enum InvariantCheckResult {
|
|
211
|
+
/// Invariant satisfied: state and dedup agree (both true or both false)
|
|
212
|
+
Satisfied,
|
|
213
|
+
/// Invariant violated: state and dedup disagree
|
|
214
|
+
Violated {
|
|
215
|
+
state_mutated: bool,
|
|
216
|
+
dedup_persisted: bool,
|
|
217
|
+
},
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
#[cfg(test)]
|
|
221
|
+
mod tests {
|
|
222
|
+
use super::*;
|
|
223
|
+
|
|
224
|
+
#[test]
|
|
225
|
+
fn test_crash_before_state_mutation() {
|
|
226
|
+
let mut executor = CrashAtomicExecutor::new();
|
|
227
|
+
let crash_state = executor.apply_with_simulated_crash(
|
|
228
|
+
"op1",
|
|
229
|
+
"rec1",
|
|
230
|
+
10,
|
|
231
|
+
CrashBoundaryPoint::BeforeStateMutation,
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
assert!(!crash_state.state_mutated);
|
|
235
|
+
assert!(!crash_state.dedup_persisted);
|
|
236
|
+
|
|
237
|
+
let decision = executor.recover(&crash_state);
|
|
238
|
+
assert_eq!(decision, RecoveryDecision::SafeToRetry);
|
|
239
|
+
|
|
240
|
+
// Invariant must be satisfied
|
|
241
|
+
assert_eq!(
|
|
242
|
+
executor.verify_invariant(&crash_state),
|
|
243
|
+
InvariantCheckResult::Satisfied
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
#[test]
|
|
248
|
+
fn test_crash_after_state_mutation_before_dedup() {
|
|
249
|
+
let mut executor = CrashAtomicExecutor::new();
|
|
250
|
+
let crash_state = executor.apply_with_simulated_crash(
|
|
251
|
+
"op1",
|
|
252
|
+
"rec1",
|
|
253
|
+
10,
|
|
254
|
+
CrashBoundaryPoint::AfterStateMutationBeforeDedupPersist,
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
assert!(crash_state.state_mutated);
|
|
258
|
+
assert!(!crash_state.dedup_persisted);
|
|
259
|
+
|
|
260
|
+
// State was mutated, but dedup wasn't persisted
|
|
261
|
+
// This violates the invariant!
|
|
262
|
+
let invariant_result = executor.verify_invariant(&crash_state);
|
|
263
|
+
assert!(matches!(
|
|
264
|
+
invariant_result,
|
|
265
|
+
InvariantCheckResult::Violated { .. }
|
|
266
|
+
));
|
|
267
|
+
|
|
268
|
+
let decision = executor.recover(&crash_state);
|
|
269
|
+
assert!(matches!(decision, RecoveryDecision::PartiallyApplied { .. }));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
#[test]
|
|
273
|
+
fn test_crash_after_dedup_persist() {
|
|
274
|
+
let mut executor = CrashAtomicExecutor::new();
|
|
275
|
+
let crash_state = executor.apply_with_simulated_crash(
|
|
276
|
+
"op1",
|
|
277
|
+
"rec1",
|
|
278
|
+
10,
|
|
279
|
+
CrashBoundaryPoint::AfterDedupPersist,
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
assert!(crash_state.state_mutated);
|
|
283
|
+
assert!(crash_state.dedup_persisted);
|
|
284
|
+
|
|
285
|
+
let decision = executor.recover(&crash_state);
|
|
286
|
+
assert_eq!(decision, RecoveryDecision::AlreadyApplied);
|
|
287
|
+
|
|
288
|
+
// Invariant satisfied
|
|
289
|
+
assert_eq!(
|
|
290
|
+
executor.verify_invariant(&crash_state),
|
|
291
|
+
InvariantCheckResult::Satisfied
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
#[test]
|
|
296
|
+
fn test_duplicate_prevention_after_recovery() {
|
|
297
|
+
let mut executor = CrashAtomicExecutor::new();
|
|
298
|
+
|
|
299
|
+
// First application with crash after dedup
|
|
300
|
+
let crash_state = executor.apply_with_simulated_crash(
|
|
301
|
+
"op1",
|
|
302
|
+
"rec1",
|
|
303
|
+
10,
|
|
304
|
+
CrashBoundaryPoint::AfterDedupPersist,
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
// After recovery, operation is in dedup
|
|
308
|
+
assert!(executor.dedup_set().contains("op1"));
|
|
309
|
+
|
|
310
|
+
// Try to apply the same operation again
|
|
311
|
+
let result = executor.apply_with_crash("op1", "rec1", 20);
|
|
312
|
+
assert!(result.is_err());
|
|
313
|
+
assert!(result.unwrap_err().contains("already applied"));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
#[test]
|
|
317
|
+
fn test_exactly_once_under_all_crash_points() {
|
|
318
|
+
let crash_points = vec![
|
|
319
|
+
CrashBoundaryPoint::BeforeStateMutation,
|
|
320
|
+
CrashBoundaryPoint::AfterStateMutationBeforeDedupPersist,
|
|
321
|
+
CrashBoundaryPoint::AfterDedupPersist,
|
|
322
|
+
];
|
|
323
|
+
|
|
324
|
+
for crash_point in crash_points {
|
|
325
|
+
let mut executor = CrashAtomicExecutor::new();
|
|
326
|
+
let crash_state =
|
|
327
|
+
executor.apply_with_simulated_crash("op1", "rec1", 10, crash_point.clone());
|
|
328
|
+
|
|
329
|
+
let decision = executor.recover(&crash_state);
|
|
330
|
+
|
|
331
|
+
// The decision should allow recovery without double-apply
|
|
332
|
+
match decision {
|
|
333
|
+
RecoveryDecision::AlreadyApplied => {
|
|
334
|
+
// Safe: already fully applied
|
|
335
|
+
}
|
|
336
|
+
RecoveryDecision::SafeToRetry => {
|
|
337
|
+
// Safe: not yet applied, can retry
|
|
338
|
+
}
|
|
339
|
+
RecoveryDecision::PartiallyApplied { .. } => {
|
|
340
|
+
// This is the dangerous case - but we detected it
|
|
341
|
+
}
|
|
342
|
+
_ => panic!("Unexpected recovery decision: {:?}", decision),
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
#[test]
|
|
348
|
+
fn test_invariant_satisfaction_across_crash_points() {
|
|
349
|
+
let crash_points = vec![
|
|
350
|
+
CrashBoundaryPoint::BeforeStateMutation,
|
|
351
|
+
CrashBoundaryPoint::AfterStateMutationBeforeDedupPersist,
|
|
352
|
+
CrashBoundaryPoint::AfterDedupPersist,
|
|
353
|
+
];
|
|
354
|
+
|
|
355
|
+
for crash_point in crash_points {
|
|
356
|
+
let mut executor = CrashAtomicExecutor::new();
|
|
357
|
+
let crash_state =
|
|
358
|
+
executor.apply_with_simulated_crash("op1", "rec1", 10, crash_point.clone());
|
|
359
|
+
|
|
360
|
+
// After crash, verify what we find
|
|
361
|
+
let in_dedup = executor.dedup_set().contains("op1");
|
|
362
|
+
|
|
363
|
+
// For consistency: state_mutated should equal in_dedup
|
|
364
|
+
// (they're either both true or both false, never mixed)
|
|
365
|
+
if crash_point == CrashBoundaryPoint::AfterStateMutationBeforeDedupPersist {
|
|
366
|
+
// This is the violation case: state true, dedup false
|
|
367
|
+
assert_ne!(crash_state.state_mutated, in_dedup);
|
|
368
|
+
} else {
|
|
369
|
+
// All other cases should agree
|
|
370
|
+
assert_eq!(crash_state.state_mutated, in_dedup);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
#[test]
|
|
376
|
+
fn test_recovery_state_tracking() {
|
|
377
|
+
let mut executor = CrashAtomicExecutor::new();
|
|
378
|
+
|
|
379
|
+
// Apply operation normally
|
|
380
|
+
let _ = executor.apply_with_crash("op1", "rec1", 10);
|
|
381
|
+
assert!(executor.dedup_set().contains("op1"));
|
|
382
|
+
|
|
383
|
+
// Simulate crash recovery state
|
|
384
|
+
let crash_state = CrashRecoveryState {
|
|
385
|
+
state_mutated: true,
|
|
386
|
+
dedup_persisted: true,
|
|
387
|
+
operation_id: "op1".to_string(),
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
let decision = executor.recover(&crash_state);
|
|
391
|
+
assert_eq!(decision, RecoveryDecision::AlreadyApplied);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
#[test]
|
|
395
|
+
fn test_multiple_operations_crash_atomic() {
|
|
396
|
+
let mut executor = CrashAtomicExecutor::new();
|
|
397
|
+
|
|
398
|
+
// Apply first operation fully
|
|
399
|
+
let _ = executor.apply_with_crash("op1", "rec1", 10);
|
|
400
|
+
assert_eq!(executor.dedup_set().len(), 1);
|
|
401
|
+
|
|
402
|
+
// Apply second operation with crash after state mutation
|
|
403
|
+
let crash_state = executor.apply_with_simulated_crash(
|
|
404
|
+
"op2",
|
|
405
|
+
"rec2",
|
|
406
|
+
20,
|
|
407
|
+
CrashBoundaryPoint::AfterStateMutationBeforeDedupPersist,
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
// op1 is still in dedup, op2 is not
|
|
411
|
+
assert!(executor.dedup_set().contains("op1"));
|
|
412
|
+
assert!(!executor.dedup_set().contains("op2"));
|
|
413
|
+
|
|
414
|
+
// op2's crash state shows state mutated but not persisted to dedup
|
|
415
|
+
let invariant = executor.verify_invariant(&crash_state);
|
|
416
|
+
assert!(matches!(invariant, InvariantCheckResult::Violated { .. }));
|
|
417
|
+
}
|
|
418
|
+
}
|