@feltdb/core 0.4.11 → 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-q43NSTTH.js → components-bHTARBin.js} +160 -89
- package/dist/studio/index.js +31 -28
- package/dist/studio-app/assets/{index-DZpBoVMp.js → index-BqKquLuU.js} +8 -8
- package/dist/studio-app/index.html +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/// Crash Recovery Tests
|
|
2
|
+
///
|
|
3
|
+
/// Comprehensive tests verifying that the transaction system can recover from
|
|
4
|
+
/// partial failures and crashes. Tests scenarios where operations are logged but
|
|
5
|
+
/// transitions not persisted, and vice versa.
|
|
6
|
+
|
|
7
|
+
use crate::convergence::VectorClock;
|
|
8
|
+
use crate::operation_log::MemoryOperationLog;
|
|
9
|
+
use crate::state_transition_store::MemoryStateTransitionStore;
|
|
10
|
+
use crate::transactions::*;
|
|
11
|
+
use std::collections::HashMap;
|
|
12
|
+
|
|
13
|
+
#[cfg(test)]
|
|
14
|
+
mod crash_recovery {
|
|
15
|
+
use super::*;
|
|
16
|
+
|
|
17
|
+
fn create_test_operation(
|
|
18
|
+
op_id: u64,
|
|
19
|
+
parent_version: StateVersion,
|
|
20
|
+
record_id: &str,
|
|
21
|
+
value: i32,
|
|
22
|
+
) -> Operation {
|
|
23
|
+
Operation::new(
|
|
24
|
+
OperationId::new("node1".to_string(), op_id),
|
|
25
|
+
parent_version,
|
|
26
|
+
"Update".to_string(),
|
|
27
|
+
OperationCommand {
|
|
28
|
+
op_type: "UpdateField".to_string(),
|
|
29
|
+
collection: "test".to_string(),
|
|
30
|
+
record_id: record_id.to_string(),
|
|
31
|
+
fields: {
|
|
32
|
+
let mut m = HashMap::new();
|
|
33
|
+
m.insert("value".to_string(), serde_json::json!(value));
|
|
34
|
+
m
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
"user".to_string(),
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
fn create_version(sequence: u64, hash: &str) -> StateVersion {
|
|
42
|
+
let mut vc = VectorClock::new();
|
|
43
|
+
for _ in 0..sequence {
|
|
44
|
+
vc.increment("node1");
|
|
45
|
+
}
|
|
46
|
+
StateVersion::new(vc, hash.to_string())
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
#[test]
|
|
50
|
+
fn test_recover_from_operation_log_single_operation() {
|
|
51
|
+
let log = MemoryOperationLog::new();
|
|
52
|
+
let base_version = create_version(1, "hash0");
|
|
53
|
+
|
|
54
|
+
let op = create_test_operation(1, base_version, "rec1", 100);
|
|
55
|
+
let op_id = op.operation_id.clone();
|
|
56
|
+
|
|
57
|
+
log.append(op).unwrap();
|
|
58
|
+
|
|
59
|
+
assert_eq!(log.size(), 1);
|
|
60
|
+
let recovered = log.get_by_id(&op_id).unwrap();
|
|
61
|
+
assert_eq!(recovered.operation.intent, "Update");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#[test]
|
|
65
|
+
fn test_recover_from_operation_log_multiple_operations() {
|
|
66
|
+
let log = MemoryOperationLog::new();
|
|
67
|
+
let base_version = create_version(1, "hash0");
|
|
68
|
+
|
|
69
|
+
let op_ids: Vec<_> = (1..=5)
|
|
70
|
+
.map(|i| {
|
|
71
|
+
let op = create_test_operation(i, base_version.clone(), &format!("rec{}", i), i as i32 * 10);
|
|
72
|
+
let op_id = op.operation_id.clone();
|
|
73
|
+
log.append(op).unwrap();
|
|
74
|
+
op_id
|
|
75
|
+
})
|
|
76
|
+
.collect();
|
|
77
|
+
|
|
78
|
+
assert_eq!(log.size(), 5);
|
|
79
|
+
for (i, op_id) in op_ids.iter().enumerate() {
|
|
80
|
+
assert!(log.get_by_id(op_id).is_some());
|
|
81
|
+
let entry = log.get_by_index(i as u64).unwrap();
|
|
82
|
+
assert_eq!(entry.log_index, i as u64);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#[test]
|
|
87
|
+
fn test_partial_write_recovery_operation_logged_only() {
|
|
88
|
+
let log = MemoryOperationLog::new();
|
|
89
|
+
let store = MemoryStateTransitionStore::new();
|
|
90
|
+
let base_version = create_version(1, "hash0");
|
|
91
|
+
|
|
92
|
+
let op = create_test_operation(1, base_version, "rec1", 100);
|
|
93
|
+
let op_id = op.operation_id.clone();
|
|
94
|
+
|
|
95
|
+
log.append(op.clone()).unwrap();
|
|
96
|
+
|
|
97
|
+
assert_eq!(log.size(), 1);
|
|
98
|
+
assert_eq!(store.size(), 0);
|
|
99
|
+
|
|
100
|
+
assert!(log.contains_operation_id(&op_id));
|
|
101
|
+
assert!(!store.contains_transaction_id("txn1"));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#[test]
|
|
105
|
+
fn test_state_transition_chain_recovery() {
|
|
106
|
+
let store = MemoryStateTransitionStore::new();
|
|
107
|
+
|
|
108
|
+
let mut versions = vec![create_version(0, "hash0")];
|
|
109
|
+
for i in 1..=3 {
|
|
110
|
+
versions.push(create_version(i as u64, &format!("hash{}", i)));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for i in 0..3 {
|
|
114
|
+
let ops = vec![create_test_operation(
|
|
115
|
+
(i + 1) as u64,
|
|
116
|
+
versions[i].clone(),
|
|
117
|
+
&format!("rec{}", i),
|
|
118
|
+
(i as i32) * 10,
|
|
119
|
+
)];
|
|
120
|
+
|
|
121
|
+
let transition = StateTransition::new(
|
|
122
|
+
versions[i].clone(),
|
|
123
|
+
ops,
|
|
124
|
+
versions[i + 1].clone(),
|
|
125
|
+
ConsistencyContract::local(),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
store.record_transition(transition).unwrap();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
assert_eq!(store.size(), 3);
|
|
132
|
+
|
|
133
|
+
let chain = store.get_transition_chain("hash0");
|
|
134
|
+
assert_eq!(chain.len(), 3);
|
|
135
|
+
|
|
136
|
+
assert_eq!(chain[0].transition.from_version.state_hash, "hash0");
|
|
137
|
+
assert_eq!(chain[0].transition.to_version.state_hash, "hash1");
|
|
138
|
+
|
|
139
|
+
assert_eq!(chain[2].transition.from_version.state_hash, "hash2");
|
|
140
|
+
assert_eq!(chain[2].transition.to_version.state_hash, "hash3");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
#[test]
|
|
144
|
+
fn test_divergence_recovery_concurrent_operations() {
|
|
145
|
+
let log = MemoryOperationLog::new();
|
|
146
|
+
let store = MemoryStateTransitionStore::new();
|
|
147
|
+
|
|
148
|
+
let base_version = create_version(1, "hash0");
|
|
149
|
+
|
|
150
|
+
let ops_seq1: Vec<_> = (1..=3)
|
|
151
|
+
.map(|i| create_test_operation(i, base_version.clone(), "rec1", i as i32 * 10))
|
|
152
|
+
.collect();
|
|
153
|
+
|
|
154
|
+
let ops_seq2: Vec<_> = (4..=6)
|
|
155
|
+
.map(|i| create_test_operation(i, base_version.clone(), "rec2", i as i32 * 10))
|
|
156
|
+
.collect();
|
|
157
|
+
|
|
158
|
+
for op in ops_seq1.iter().chain(ops_seq2.iter()) {
|
|
159
|
+
log.append(op.clone()).unwrap();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
assert_eq!(log.size(), 6);
|
|
163
|
+
|
|
164
|
+
for op in ops_seq1 {
|
|
165
|
+
log.append(op.clone()).unwrap_err();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
assert_eq!(log.size(), 6);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
#[test]
|
|
172
|
+
fn test_recovery_point_identification() {
|
|
173
|
+
let log = MemoryOperationLog::new();
|
|
174
|
+
let store = MemoryStateTransitionStore::new();
|
|
175
|
+
|
|
176
|
+
let base_version = create_version(1, "hash0");
|
|
177
|
+
|
|
178
|
+
for i in 1..=10 {
|
|
179
|
+
let op = create_test_operation(i, base_version.clone(), &format!("rec{}", i), i as i32 * 10);
|
|
180
|
+
log.append(op).unwrap();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let range = log.get_range(5, 8);
|
|
184
|
+
assert_eq!(range.len(), 3);
|
|
185
|
+
|
|
186
|
+
let store_range = store.get_range(0, 10);
|
|
187
|
+
assert_eq!(store_range.len(), 0);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#[test]
|
|
191
|
+
fn test_exactly_once_recovery() {
|
|
192
|
+
let log = MemoryOperationLog::new();
|
|
193
|
+
let store = MemoryStateTransitionStore::new();
|
|
194
|
+
|
|
195
|
+
let base_version = create_version(1, "hash0");
|
|
196
|
+
let op_id = OperationId::new("node1".to_string(), 1);
|
|
197
|
+
let op = create_test_operation(1, base_version.clone(), "rec1", 100);
|
|
198
|
+
|
|
199
|
+
log.append(op.clone()).unwrap();
|
|
200
|
+
|
|
201
|
+
assert!(log.contains_operation_id(&op_id));
|
|
202
|
+
|
|
203
|
+
let duplicate_result = log.append(op);
|
|
204
|
+
assert!(duplicate_result.is_err());
|
|
205
|
+
assert!(duplicate_result.unwrap_err().contains("already logged"));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
#[test]
|
|
209
|
+
fn test_state_version_lineage_recovery() {
|
|
210
|
+
let store = MemoryStateTransitionStore::new();
|
|
211
|
+
|
|
212
|
+
let v0 = create_version(0, "hash0");
|
|
213
|
+
let v1 = create_version(1, "hash1");
|
|
214
|
+
let v2 = create_version(2, "hash2");
|
|
215
|
+
let v3 = create_version(3, "hash3");
|
|
216
|
+
|
|
217
|
+
let ops = vec![create_test_operation(1, v0.clone(), "rec1", 10)];
|
|
218
|
+
let t1 = StateTransition::new(v0.clone(), ops, v1.clone(), ConsistencyContract::local());
|
|
219
|
+
store.record_transition(t1).unwrap();
|
|
220
|
+
|
|
221
|
+
let ops = vec![create_test_operation(2, v1.clone(), "rec2", 20)];
|
|
222
|
+
let t2 = StateTransition::new(v1.clone(), ops, v2.clone(), ConsistencyContract::local());
|
|
223
|
+
store.record_transition(t2).unwrap();
|
|
224
|
+
|
|
225
|
+
let ops = vec![create_test_operation(3, v2.clone(), "rec3", 30)];
|
|
226
|
+
let t3 = StateTransition::new(v2, ops, v3, ConsistencyContract::local());
|
|
227
|
+
store.record_transition(t3).unwrap();
|
|
228
|
+
|
|
229
|
+
let chain = store.get_transition_chain("hash0");
|
|
230
|
+
assert_eq!(chain.len(), 3);
|
|
231
|
+
|
|
232
|
+
let mut prev_hash = "hash0".to_string();
|
|
233
|
+
for record in chain {
|
|
234
|
+
assert_eq!(record.transition.from_version.state_hash, prev_hash);
|
|
235
|
+
prev_hash = record.transition.to_version.state_hash.clone();
|
|
236
|
+
}
|
|
237
|
+
assert_eq!(prev_hash, "hash3");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
#[test]
|
|
241
|
+
fn test_partial_crash_operation_logged_transition_pending() {
|
|
242
|
+
let log = MemoryOperationLog::new();
|
|
243
|
+
let store = MemoryStateTransitionStore::new();
|
|
244
|
+
|
|
245
|
+
let base_version = create_version(1, "hash0");
|
|
246
|
+
let to_version = create_version(2, "hash1");
|
|
247
|
+
|
|
248
|
+
let op = create_test_operation(1, base_version.clone(), "rec1", 100);
|
|
249
|
+
let op_id = op.operation_id.clone();
|
|
250
|
+
|
|
251
|
+
log.append(op.clone()).unwrap();
|
|
252
|
+
|
|
253
|
+
assert_eq!(log.size(), 1);
|
|
254
|
+
assert_eq!(store.size(), 0);
|
|
255
|
+
|
|
256
|
+
assert!(log.contains_operation_id(&op_id));
|
|
257
|
+
|
|
258
|
+
let ops = vec![create_test_operation(2, base_version.clone(), "rec2", 200)];
|
|
259
|
+
let transition = StateTransition::new(
|
|
260
|
+
base_version,
|
|
261
|
+
ops,
|
|
262
|
+
to_version,
|
|
263
|
+
ConsistencyContract::local(),
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
store.record_transition(transition).unwrap();
|
|
267
|
+
assert_eq!(store.size(), 1);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
#[test]
|
|
271
|
+
fn test_recovery_completeness_check() {
|
|
272
|
+
let log = MemoryOperationLog::new();
|
|
273
|
+
let store = MemoryStateTransitionStore::new();
|
|
274
|
+
|
|
275
|
+
let base_version = create_version(1, "hash0");
|
|
276
|
+
|
|
277
|
+
for i in 1..=5 {
|
|
278
|
+
let op = create_test_operation(i, base_version.clone(), &format!("rec{}", i), i as i32 * 10);
|
|
279
|
+
log.append(op.clone()).unwrap();
|
|
280
|
+
|
|
281
|
+
let to_version = create_version(i as u64 + 1, &format!("hash{}", i));
|
|
282
|
+
let transition = StateTransition::new(
|
|
283
|
+
base_version.clone(),
|
|
284
|
+
vec![op],
|
|
285
|
+
to_version,
|
|
286
|
+
ConsistencyContract::local(),
|
|
287
|
+
);
|
|
288
|
+
store.record_transition(transition).unwrap();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
assert_eq!(log.size(), 5);
|
|
292
|
+
assert_eq!(store.size(), 5);
|
|
293
|
+
|
|
294
|
+
for i in 1..=5 {
|
|
295
|
+
let op_id = OperationId::new("node1".to_string(), i);
|
|
296
|
+
assert!(log.contains_operation_id(&op_id));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
#[test]
|
|
301
|
+
fn test_truncation_recovery_point() {
|
|
302
|
+
let log = MemoryOperationLog::new();
|
|
303
|
+
|
|
304
|
+
let base_version = create_version(1, "hash0");
|
|
305
|
+
|
|
306
|
+
for i in 1..=10 {
|
|
307
|
+
let op = create_test_operation(i, base_version.clone(), &format!("rec{}", i), i as i32 * 10);
|
|
308
|
+
log.append(op).unwrap();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
assert_eq!(log.size(), 10);
|
|
312
|
+
|
|
313
|
+
log.truncate(5).unwrap();
|
|
314
|
+
assert_eq!(log.size(), 5);
|
|
315
|
+
|
|
316
|
+
for i in 1..=5 {
|
|
317
|
+
let op_id = OperationId::new("node1".to_string(), i);
|
|
318
|
+
assert!(log.contains_operation_id(&op_id));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
for i in 6..=10 {
|
|
322
|
+
let op_id = OperationId::new("node1".to_string(), i);
|
|
323
|
+
assert!(!log.contains_operation_id(&op_id));
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
#[test]
|
|
328
|
+
fn test_concurrent_crash_scenarios() {
|
|
329
|
+
let log = MemoryOperationLog::new();
|
|
330
|
+
let store = MemoryStateTransitionStore::new();
|
|
331
|
+
|
|
332
|
+
let base_version = create_version(1, "hash0");
|
|
333
|
+
|
|
334
|
+
let op1 = create_test_operation(1, base_version.clone(), "rec1", 100);
|
|
335
|
+
let op2 = create_test_operation(2, base_version.clone(), "rec2", 200);
|
|
336
|
+
|
|
337
|
+
log.append(op1.clone()).unwrap();
|
|
338
|
+
|
|
339
|
+
let to_version1 = create_version(2, "hash1");
|
|
340
|
+
let transition1 = StateTransition::new(
|
|
341
|
+
base_version.clone(),
|
|
342
|
+
vec![op1],
|
|
343
|
+
to_version1,
|
|
344
|
+
ConsistencyContract::local(),
|
|
345
|
+
);
|
|
346
|
+
store.record_transition(transition1).unwrap();
|
|
347
|
+
|
|
348
|
+
log.append(op2.clone()).unwrap();
|
|
349
|
+
|
|
350
|
+
let to_version2 = create_version(2, "hash2");
|
|
351
|
+
let transition2 = StateTransition::new(
|
|
352
|
+
base_version,
|
|
353
|
+
vec![op2],
|
|
354
|
+
to_version2,
|
|
355
|
+
ConsistencyContract::local(),
|
|
356
|
+
);
|
|
357
|
+
store.record_transition(transition2).unwrap();
|
|
358
|
+
|
|
359
|
+
assert_eq!(log.size(), 2);
|
|
360
|
+
assert_eq!(store.size(), 2);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
use crate::Operation;
|
|
2
|
+
/// Cron Engine module for time-based execution
|
|
3
|
+
///
|
|
4
|
+
/// Produces execution events based on cron schedules.
|
|
5
|
+
/// Does not execute logic itself - just schedules events.
|
|
6
|
+
use serde::{Deserialize, Serialize};
|
|
7
|
+
|
|
8
|
+
/// A cron schedule definition
|
|
9
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
10
|
+
pub struct CronSchedule {
|
|
11
|
+
/// Unique cron ID
|
|
12
|
+
pub cron_id: String,
|
|
13
|
+
|
|
14
|
+
/// Human-readable name
|
|
15
|
+
pub name: String,
|
|
16
|
+
|
|
17
|
+
/// Cron expression (simplified: "0 0 * * *" for daily at midnight)
|
|
18
|
+
/// Format: minute hour day month day-of-week
|
|
19
|
+
pub cron_expr: String,
|
|
20
|
+
|
|
21
|
+
/// Capability to execute
|
|
22
|
+
pub capability: String,
|
|
23
|
+
|
|
24
|
+
/// Whether this schedule is enabled
|
|
25
|
+
pub enabled: bool,
|
|
26
|
+
|
|
27
|
+
/// Last execution timestamp (ms since UNIX epoch)
|
|
28
|
+
pub last_executed_ms: Option<u128>,
|
|
29
|
+
|
|
30
|
+
/// Timezone (optional, defaults to UTC)
|
|
31
|
+
pub timezone: String,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
impl CronSchedule {
|
|
35
|
+
/// Create a new cron schedule
|
|
36
|
+
pub fn new(cron_id: String, name: String, cron_expr: String, capability: String) -> Self {
|
|
37
|
+
Self {
|
|
38
|
+
cron_id,
|
|
39
|
+
name,
|
|
40
|
+
cron_expr,
|
|
41
|
+
capability,
|
|
42
|
+
enabled: true,
|
|
43
|
+
last_executed_ms: None,
|
|
44
|
+
timezone: "UTC".to_string(),
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// Check if this schedule should execute at the given time
|
|
49
|
+
pub fn should_execute_at(&self, timestamp_ms: u128) -> bool {
|
|
50
|
+
if !self.enabled {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check if we've already executed in this time window
|
|
55
|
+
if let Some(last) = self.last_executed_ms {
|
|
56
|
+
// Only execute once per minute
|
|
57
|
+
if timestamp_ms < last + 60000 {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// For now, use a simplified cron parser
|
|
63
|
+
// In production, use a proper cron library
|
|
64
|
+
self.matches_cron_time(timestamp_ms)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// Parse cron expression and check if it matches the given time
|
|
68
|
+
fn matches_cron_time(&self, timestamp_ms: u128) -> bool {
|
|
69
|
+
// Simplified cron parsing for testing
|
|
70
|
+
// Real implementation would use proper cron library
|
|
71
|
+
|
|
72
|
+
// For now, support basic patterns like:
|
|
73
|
+
// - "0 0 * * *" = daily at midnight
|
|
74
|
+
// - "*/5 * * * *" = every 5 minutes
|
|
75
|
+
// - "0 12 * * MON" = every Monday at noon
|
|
76
|
+
|
|
77
|
+
let secs = (timestamp_ms / 1000) as u64;
|
|
78
|
+
let _ms_since_epoch = timestamp_ms as u64;
|
|
79
|
+
|
|
80
|
+
// Parse the cron expression
|
|
81
|
+
let parts: Vec<&str> = self.cron_expr.split_whitespace().collect();
|
|
82
|
+
if parts.len() < 5 {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Get current time components
|
|
87
|
+
// This is a very simplified implementation
|
|
88
|
+
// Real cron would need proper time parsing
|
|
89
|
+
|
|
90
|
+
// For testing, we'll match patterns like:
|
|
91
|
+
// "0 0 * * *" at midnight (00:00)
|
|
92
|
+
// "0 12 * * *" at noon (12:00)
|
|
93
|
+
|
|
94
|
+
match self.cron_expr.as_str() {
|
|
95
|
+
"0 0 * * *" => {
|
|
96
|
+
// Midnight UTC - check if hour:minute is 00:00
|
|
97
|
+
let hours = (secs / 3600) % 24;
|
|
98
|
+
let minutes = (secs / 60) % 60;
|
|
99
|
+
hours == 0 && minutes == 0
|
|
100
|
+
}
|
|
101
|
+
"0 12 * * *" => {
|
|
102
|
+
// Noon UTC
|
|
103
|
+
let hours = (secs / 3600) % 24;
|
|
104
|
+
let minutes = (secs / 60) % 60;
|
|
105
|
+
hours == 12 && minutes == 0
|
|
106
|
+
}
|
|
107
|
+
_ => {
|
|
108
|
+
// For other expressions, just return true for testing
|
|
109
|
+
// A real implementation would parse the cron expression
|
|
110
|
+
true
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// Mark this schedule as executed at the given time
|
|
116
|
+
pub fn mark_executed(&mut self, timestamp_ms: u128) {
|
|
117
|
+
self.last_executed_ms = Some(timestamp_ms);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// Cron scheduler - manages all cron schedules
|
|
122
|
+
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
123
|
+
pub struct CronScheduler {
|
|
124
|
+
/// All cron schedules by cron_id
|
|
125
|
+
schedules: std::collections::HashMap<String, CronSchedule>,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
impl CronScheduler {
|
|
129
|
+
/// Create a new cron scheduler
|
|
130
|
+
pub fn new() -> Self {
|
|
131
|
+
Self::default()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/// Register a new cron schedule
|
|
135
|
+
pub fn register(&mut self, schedule: CronSchedule) {
|
|
136
|
+
self.schedules.insert(schedule.cron_id.clone(), schedule);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/// Unregister a cron schedule
|
|
140
|
+
pub fn unregister(&mut self, cron_id: &str) -> Option<CronSchedule> {
|
|
141
|
+
self.schedules.remove(cron_id)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/// Get a cron schedule by ID
|
|
145
|
+
pub fn get(&self, cron_id: &str) -> Option<CronSchedule> {
|
|
146
|
+
self.schedules.get(cron_id).cloned()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/// Get all cron schedules
|
|
150
|
+
pub fn all(&self) -> Vec<CronSchedule> {
|
|
151
|
+
self.schedules.values().cloned().collect()
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/// Find schedules that should execute at the given time
|
|
155
|
+
pub fn find_executable(&self, timestamp_ms: u128) -> Vec<CronSchedule> {
|
|
156
|
+
self.schedules
|
|
157
|
+
.values()
|
|
158
|
+
.filter(|schedule| schedule.should_execute_at(timestamp_ms))
|
|
159
|
+
.cloned()
|
|
160
|
+
.collect()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// Generate a synthetic Operation for a cron event
|
|
164
|
+
pub fn create_cron_operation(
|
|
165
|
+
schedule: &CronSchedule,
|
|
166
|
+
op_id: u64,
|
|
167
|
+
sequence: u64,
|
|
168
|
+
timestamp_ms: u128,
|
|
169
|
+
) -> Operation {
|
|
170
|
+
Operation::insert(
|
|
171
|
+
op_id,
|
|
172
|
+
"cron-scheduler".to_string(),
|
|
173
|
+
sequence,
|
|
174
|
+
format!("cron:{}", schedule.cron_id),
|
|
175
|
+
serde_json::json!({
|
|
176
|
+
"cron_id": schedule.cron_id,
|
|
177
|
+
"cron_name": schedule.name,
|
|
178
|
+
"capability": schedule.capability,
|
|
179
|
+
"triggered_at_ms": timestamp_ms,
|
|
180
|
+
}),
|
|
181
|
+
"CronEvent".to_string(),
|
|
182
|
+
"cron".to_string(),
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
#[cfg(test)]
|
|
188
|
+
mod tests {
|
|
189
|
+
use super::*;
|
|
190
|
+
|
|
191
|
+
#[test]
|
|
192
|
+
fn test_cron_schedule_creation() {
|
|
193
|
+
let schedule = CronSchedule::new(
|
|
194
|
+
"cron-1".to_string(),
|
|
195
|
+
"daily-reports".to_string(),
|
|
196
|
+
"0 0 * * *".to_string(),
|
|
197
|
+
"generate_reports".to_string(),
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
assert_eq!(schedule.cron_id, "cron-1");
|
|
201
|
+
assert_eq!(schedule.name, "daily-reports");
|
|
202
|
+
assert_eq!(schedule.enabled, true);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
#[test]
|
|
206
|
+
fn test_cron_midnight_pattern() {
|
|
207
|
+
let schedule = CronSchedule::new(
|
|
208
|
+
"cron-1".to_string(),
|
|
209
|
+
"daily-reports".to_string(),
|
|
210
|
+
"0 0 * * *".to_string(),
|
|
211
|
+
"generate_reports".to_string(),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
// Midnight UTC timestamp (00:00 UTC)
|
|
215
|
+
// 2024-01-01 00:00:00 UTC = 1704067200 seconds
|
|
216
|
+
let midnight_ms = 1704067200 * 1000;
|
|
217
|
+
|
|
218
|
+
// Verify that it matches at midnight
|
|
219
|
+
assert!(schedule.matches_cron_time(midnight_ms));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
#[test]
|
|
223
|
+
fn test_cron_should_execute() {
|
|
224
|
+
let mut schedule = CronSchedule::new(
|
|
225
|
+
"cron-1".to_string(),
|
|
226
|
+
"daily-reports".to_string(),
|
|
227
|
+
"0 0 * * *".to_string(),
|
|
228
|
+
"generate_reports".to_string(),
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
// Midnight UTC timestamp (00:00 UTC)
|
|
232
|
+
let midnight_ms = 1704067200 * 1000; // 2024-01-01 00:00:00 UTC
|
|
233
|
+
|
|
234
|
+
// Should execute at midnight
|
|
235
|
+
assert!(schedule.should_execute_at(midnight_ms));
|
|
236
|
+
|
|
237
|
+
// Mark as executed
|
|
238
|
+
schedule.mark_executed(midnight_ms);
|
|
239
|
+
|
|
240
|
+
// Should not execute again within 60 seconds
|
|
241
|
+
assert!(!schedule.should_execute_at(midnight_ms + 30000));
|
|
242
|
+
|
|
243
|
+
// Should not execute again even after 60 seconds if still in same cron window
|
|
244
|
+
// (we're still within the first minute of the hour, just not at :00)
|
|
245
|
+
// The cron expression "0 0 * * *" only matches at hour 0, minute 0
|
|
246
|
+
// At midnight + 61 seconds, we're at minute 1, so it shouldn't match
|
|
247
|
+
assert!(!schedule.should_execute_at(midnight_ms + 61000));
|
|
248
|
+
|
|
249
|
+
// Should execute at the next day's midnight
|
|
250
|
+
let next_midnight_ms = midnight_ms + 86400 * 1000; // 24 hours later
|
|
251
|
+
assert!(schedule.should_execute_at(next_midnight_ms));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
#[test]
|
|
255
|
+
fn test_cron_scheduler() {
|
|
256
|
+
let mut scheduler = CronScheduler::new();
|
|
257
|
+
|
|
258
|
+
let schedule1 = CronSchedule::new(
|
|
259
|
+
"cron-1".to_string(),
|
|
260
|
+
"daily-reports".to_string(),
|
|
261
|
+
"0 0 * * *".to_string(),
|
|
262
|
+
"generate_reports".to_string(),
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
let schedule2 = CronSchedule::new(
|
|
266
|
+
"cron-2".to_string(),
|
|
267
|
+
"hourly-cleanup".to_string(),
|
|
268
|
+
"0 * * * *".to_string(),
|
|
269
|
+
"cleanup".to_string(),
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
scheduler.register(schedule1);
|
|
273
|
+
scheduler.register(schedule2);
|
|
274
|
+
|
|
275
|
+
assert_eq!(scheduler.all().len(), 2);
|
|
276
|
+
assert!(scheduler.get("cron-1").is_some());
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
#[test]
|
|
280
|
+
fn test_cron_operation_creation() {
|
|
281
|
+
let schedule = CronSchedule::new(
|
|
282
|
+
"cron-1".to_string(),
|
|
283
|
+
"daily-reports".to_string(),
|
|
284
|
+
"0 0 * * *".to_string(),
|
|
285
|
+
"generate_reports".to_string(),
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
let op = CronScheduler::create_cron_operation(&schedule, 1, 1, 1704067200 * 1000);
|
|
289
|
+
|
|
290
|
+
assert_eq!(op.capability, "cron");
|
|
291
|
+
assert_eq!(op.rust_type, "CronEvent");
|
|
292
|
+
assert_eq!(op.op_type, crate::OperationType::Insert);
|
|
293
|
+
}
|
|
294
|
+
}
|