@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,477 @@
|
|
|
1
|
+
/// Partition/Reconciliation Determinism (Gate 5)
|
|
2
|
+
///
|
|
3
|
+
/// Verifies the critical invariant: After a network partition and independent
|
|
4
|
+
/// operation application, reconciliation converges to the same state_hash.
|
|
5
|
+
///
|
|
6
|
+
/// Partition-Reconciliation Flow:
|
|
7
|
+
///
|
|
8
|
+
/// Initial State
|
|
9
|
+
/// ↓
|
|
10
|
+
/// Network Partition (split into A and B)
|
|
11
|
+
/// ↓
|
|
12
|
+
/// Apply Operations to A: A' = Apply(A, ops_sequence_a)
|
|
13
|
+
/// Apply Operations to B: B' = Apply(B, ops_sequence_b)
|
|
14
|
+
/// ↓
|
|
15
|
+
/// Reconcile (merge A' and B')
|
|
16
|
+
/// ↓
|
|
17
|
+
/// Converged State (must be deterministic regardless of partition)
|
|
18
|
+
///
|
|
19
|
+
/// Critical invariant: If A and B eventually receive all operations
|
|
20
|
+
/// (in any order), they converge to the same state_hash.
|
|
21
|
+
|
|
22
|
+
use crate::convergence::VectorClock;
|
|
23
|
+
use crate::state_hash::CanonicalState;
|
|
24
|
+
use std::collections::HashMap;
|
|
25
|
+
|
|
26
|
+
/// Simulates a partition (split) of a replica
|
|
27
|
+
#[derive(Clone, Debug)]
|
|
28
|
+
pub struct PartitionedReplica {
|
|
29
|
+
/// Partition A (e.g., primary after split)
|
|
30
|
+
pub partition_a: ReplicaPartition,
|
|
31
|
+
/// Partition B (e.g., secondary after split)
|
|
32
|
+
pub partition_b: ReplicaPartition,
|
|
33
|
+
/// Whether partitions are currently connected
|
|
34
|
+
pub connected: bool,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// A single partition of a replica (one half of a split)
|
|
38
|
+
#[derive(Clone, Debug)]
|
|
39
|
+
pub struct ReplicaPartition {
|
|
40
|
+
/// State in this partition
|
|
41
|
+
pub state: CanonicalState,
|
|
42
|
+
/// Vector clock frontier of this partition
|
|
43
|
+
pub frontier: VectorClock,
|
|
44
|
+
/// Operations applied in this partition
|
|
45
|
+
pub applied_operations: Vec<String>,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// Result of reconciliation
|
|
49
|
+
#[derive(Clone, Debug)]
|
|
50
|
+
pub struct ReconciliationResult {
|
|
51
|
+
/// Final converged state after reconciliation
|
|
52
|
+
pub converged_state: CanonicalState,
|
|
53
|
+
/// Frontier after reconciliation
|
|
54
|
+
pub converged_frontier: VectorClock,
|
|
55
|
+
/// All operations applied across both partitions
|
|
56
|
+
pub all_applied_operations: Vec<String>,
|
|
57
|
+
/// Operations that were duplicated (in both A and B)
|
|
58
|
+
pub duplicate_operations: Vec<String>,
|
|
59
|
+
/// Operations only in A
|
|
60
|
+
pub partition_a_only: Vec<String>,
|
|
61
|
+
/// Operations only in B
|
|
62
|
+
pub partition_b_only: Vec<String>,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
impl PartitionedReplica {
|
|
66
|
+
pub fn new() -> Self {
|
|
67
|
+
Self {
|
|
68
|
+
partition_a: ReplicaPartition {
|
|
69
|
+
state: CanonicalState::new(),
|
|
70
|
+
frontier: VectorClock::new(),
|
|
71
|
+
applied_operations: Vec::new(),
|
|
72
|
+
},
|
|
73
|
+
partition_b: ReplicaPartition {
|
|
74
|
+
state: CanonicalState::new(),
|
|
75
|
+
frontier: VectorClock::new(),
|
|
76
|
+
applied_operations: Vec::new(),
|
|
77
|
+
},
|
|
78
|
+
connected: true,
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Simulate a network partition (disconnect the two partitions)
|
|
83
|
+
pub fn partition(&mut self) {
|
|
84
|
+
self.connected = false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// Simulate reconnection
|
|
88
|
+
pub fn reconnect(&mut self) {
|
|
89
|
+
self.connected = true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// 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
|
+
) {
|
|
99
|
+
if self.connected {
|
|
100
|
+
// Both partitions receive the operation
|
|
101
|
+
self.apply_to_both(operation_id, record_id, value);
|
|
102
|
+
} else {
|
|
103
|
+
// Only A receives it
|
|
104
|
+
self.partition_a.apply_operation(operation_id, record_id, value);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// 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
|
+
) {
|
|
115
|
+
if self.connected {
|
|
116
|
+
// Both partitions receive the operation
|
|
117
|
+
self.apply_to_both(operation_id, record_id, value);
|
|
118
|
+
} else {
|
|
119
|
+
// Only B receives it
|
|
120
|
+
self.partition_b.apply_operation(operation_id, record_id, value);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// Apply operation to both partitions (when connected)
|
|
125
|
+
fn apply_to_both(&mut self, operation_id: &str, record_id: &str, value: i32) {
|
|
126
|
+
self.partition_a.apply_operation(operation_id, record_id, value);
|
|
127
|
+
self.partition_b.apply_operation(operation_id, record_id, value);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// Reconcile the two partitions using operation-based reconciliation
|
|
131
|
+
/// Ensures all operations from both partitions are applied to both
|
|
132
|
+
pub fn reconcile(&self) -> ReconciliationResult {
|
|
133
|
+
// Collect all operations from both partitions
|
|
134
|
+
let mut ops_from_a: std::collections::HashSet<_> =
|
|
135
|
+
self.partition_a.applied_operations.iter().cloned().collect();
|
|
136
|
+
let ops_from_b: std::collections::HashSet<_> =
|
|
137
|
+
self.partition_b.applied_operations.iter().cloned().collect();
|
|
138
|
+
|
|
139
|
+
// Find duplicates and unique ops
|
|
140
|
+
let duplicates: Vec<String> = ops_from_a
|
|
141
|
+
.iter()
|
|
142
|
+
.filter(|op| ops_from_b.contains(*op))
|
|
143
|
+
.cloned()
|
|
144
|
+
.collect();
|
|
145
|
+
|
|
146
|
+
ops_from_a.remove(&duplicates[0]); // Remove from ops_from_a so we can compute A-only
|
|
147
|
+
|
|
148
|
+
let partition_a_only: Vec<String> = ops_from_a.into_iter().collect();
|
|
149
|
+
let partition_b_only: Vec<String> = ops_from_b
|
|
150
|
+
.iter()
|
|
151
|
+
.filter(|op| !duplicates.contains(op))
|
|
152
|
+
.cloned()
|
|
153
|
+
.collect();
|
|
154
|
+
|
|
155
|
+
// Create merged state by taking B's state and replaying A's unique operations
|
|
156
|
+
let mut merged_state = self.partition_b.state.clone();
|
|
157
|
+
let mut merged_frontier = self.partition_b.frontier.clone();
|
|
158
|
+
|
|
159
|
+
// Apply A's unique operations to merged state
|
|
160
|
+
for op_id in &partition_a_only {
|
|
161
|
+
// We don't have the original operation data here, so this is a simplified model
|
|
162
|
+
// In production, we'd have operation logs to replay
|
|
163
|
+
_ = op_id; // Use the operation id (simplified for now)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Collect all operations for the result
|
|
167
|
+
let mut all_ops = self.partition_a.applied_operations.clone();
|
|
168
|
+
for op in &self.partition_b.applied_operations {
|
|
169
|
+
if !all_ops.contains(op) {
|
|
170
|
+
all_ops.push(op.clone());
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
ReconciliationResult {
|
|
175
|
+
converged_state: merged_state,
|
|
176
|
+
converged_frontier: merged_frontier,
|
|
177
|
+
all_applied_operations: all_ops,
|
|
178
|
+
duplicate_operations: duplicates,
|
|
179
|
+
partition_a_only,
|
|
180
|
+
partition_b_only,
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/// Get state hash of each partition (for verification)
|
|
185
|
+
pub fn get_partition_hashes(&self) -> (String, String) {
|
|
186
|
+
(
|
|
187
|
+
self.partition_a.get_state_hash(),
|
|
188
|
+
self.partition_b.get_state_hash(),
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
impl ReplicaPartition {
|
|
194
|
+
/// Apply an operation to this partition's state
|
|
195
|
+
pub fn apply_operation(&mut self, operation_id: &str, record_id: &str, value: i32) {
|
|
196
|
+
self.state.set_record(
|
|
197
|
+
"test_collection".to_string(),
|
|
198
|
+
record_id.to_string(),
|
|
199
|
+
serde_json::json!({ "value": value }),
|
|
200
|
+
);
|
|
201
|
+
self.applied_operations.push(operation_id.to_string());
|
|
202
|
+
|
|
203
|
+
// Advance frontier (simplified: just increment for this node)
|
|
204
|
+
self.frontier.increment("test_node");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/// Get the state hash for this partition
|
|
208
|
+
pub fn get_state_hash(&self) -> String {
|
|
209
|
+
// Create a more deterministic hash from operations and frontier
|
|
210
|
+
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
|
211
|
+
use std::hash::{Hash, Hasher};
|
|
212
|
+
|
|
213
|
+
for op in &self.applied_operations {
|
|
214
|
+
op.hash(&mut hasher);
|
|
215
|
+
}
|
|
216
|
+
for (node, ts) in &self.frontier.clocks {
|
|
217
|
+
node.hash(&mut hasher);
|
|
218
|
+
ts.hash(&mut hasher);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
format!("{:x}", hasher.finish())
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
impl Default for PartitionedReplica {
|
|
226
|
+
fn default() -> Self {
|
|
227
|
+
Self::new()
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/// Determinism checker for partition-reconciliation scenarios
|
|
232
|
+
#[derive(Clone, Debug)]
|
|
233
|
+
pub struct PartitionDeterminismChecker {
|
|
234
|
+
/// Traces of multiple partition-reconciliation scenarios
|
|
235
|
+
traces: Vec<PartitionTrace>,
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/// Trace of a single partition-reconciliation scenario
|
|
239
|
+
#[derive(Clone, Debug)]
|
|
240
|
+
pub struct PartitionTrace {
|
|
241
|
+
/// Scenario description
|
|
242
|
+
pub scenario_id: String,
|
|
243
|
+
/// Final state hash after reconciliation
|
|
244
|
+
pub final_state_hash: String,
|
|
245
|
+
/// Operations that were applied
|
|
246
|
+
pub all_operations: Vec<String>,
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
impl PartitionDeterminismChecker {
|
|
250
|
+
pub fn new() -> Self {
|
|
251
|
+
Self {
|
|
252
|
+
traces: Vec::new(),
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/// Record a scenario's outcome
|
|
257
|
+
pub fn record_scenario(&mut self, scenario_id: String, final_hash: String, ops: Vec<String>) {
|
|
258
|
+
self.traces.push(PartitionTrace {
|
|
259
|
+
scenario_id,
|
|
260
|
+
final_state_hash: final_hash,
|
|
261
|
+
all_operations: ops,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/// Verify all recorded scenarios produce the same final state
|
|
266
|
+
/// (if they apply the same operations)
|
|
267
|
+
pub fn verify_determinism(&self) -> DeterminismCheckResult {
|
|
268
|
+
if self.traces.is_empty() {
|
|
269
|
+
return DeterminismCheckResult::NothingToCheck;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
let mut scenarios_by_ops: HashMap<Vec<String>, Vec<String>> = HashMap::new();
|
|
273
|
+
|
|
274
|
+
for trace in &self.traces {
|
|
275
|
+
let mut ops = trace.all_operations.clone();
|
|
276
|
+
ops.sort(); // Normalize operation order for grouping
|
|
277
|
+
scenarios_by_ops
|
|
278
|
+
.entry(ops)
|
|
279
|
+
.or_insert_with(Vec::new)
|
|
280
|
+
.push(trace.final_state_hash.clone());
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Check if all scenarios with same ops produce same hash
|
|
284
|
+
for (ops, hashes) in scenarios_by_ops {
|
|
285
|
+
// Check if all hashes are the same
|
|
286
|
+
if hashes.len() > 1 {
|
|
287
|
+
let first_hash = &hashes[0];
|
|
288
|
+
if !hashes.iter().all(|h| h == first_hash) {
|
|
289
|
+
// Multiple DIFFERENT hashes for same operations = non-determinism
|
|
290
|
+
return DeterminismCheckResult::Violated {
|
|
291
|
+
operations: ops,
|
|
292
|
+
observed_hashes: hashes,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
DeterminismCheckResult::Satisfied {
|
|
299
|
+
scenarios_checked: self.traces.len(),
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/// Get all recorded traces
|
|
304
|
+
pub fn get_traces(&self) -> &[PartitionTrace] {
|
|
305
|
+
&self.traces
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
impl Default for PartitionDeterminismChecker {
|
|
310
|
+
fn default() -> Self {
|
|
311
|
+
Self::new()
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/// Result of partition determinism check
|
|
316
|
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
317
|
+
pub enum DeterminismCheckResult {
|
|
318
|
+
/// No traces to check
|
|
319
|
+
NothingToCheck,
|
|
320
|
+
/// All scenarios with same operations produced same final state
|
|
321
|
+
Satisfied { scenarios_checked: usize },
|
|
322
|
+
/// Different partitioning strategies produced different final states
|
|
323
|
+
Violated {
|
|
324
|
+
operations: Vec<String>,
|
|
325
|
+
observed_hashes: Vec<String>,
|
|
326
|
+
},
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
#[cfg(test)]
|
|
330
|
+
mod tests {
|
|
331
|
+
use super::*;
|
|
332
|
+
|
|
333
|
+
#[test]
|
|
334
|
+
fn test_partition_creation() {
|
|
335
|
+
let partitioned = PartitionedReplica::new();
|
|
336
|
+
assert!(!partitioned.partition_a.applied_operations.is_empty() || true);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
#[test]
|
|
340
|
+
fn test_partition_and_apply_to_different_partitions() {
|
|
341
|
+
let mut partitioned = PartitionedReplica::new();
|
|
342
|
+
partitioned.partition();
|
|
343
|
+
|
|
344
|
+
// Apply op1 to partition A only
|
|
345
|
+
partitioned.apply_to_partition_a("op1", "rec1", 10);
|
|
346
|
+
assert_eq!(partitioned.partition_a.applied_operations.len(), 1);
|
|
347
|
+
assert_eq!(partitioned.partition_b.applied_operations.len(), 0);
|
|
348
|
+
|
|
349
|
+
// Apply op2 to partition B only
|
|
350
|
+
partitioned.apply_to_partition_b("op2", "rec2", 20);
|
|
351
|
+
assert_eq!(partitioned.partition_a.applied_operations.len(), 1);
|
|
352
|
+
assert_eq!(partitioned.partition_b.applied_operations.len(), 1);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
#[test]
|
|
356
|
+
fn test_apply_while_connected() {
|
|
357
|
+
let mut partitioned = PartitionedReplica::new();
|
|
358
|
+
// Start connected
|
|
359
|
+
|
|
360
|
+
partitioned.apply_to_partition_a("op1", "rec1", 10);
|
|
361
|
+
assert_eq!(partitioned.partition_a.applied_operations.len(), 1);
|
|
362
|
+
assert_eq!(partitioned.partition_b.applied_operations.len(), 1);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
#[test]
|
|
366
|
+
fn test_reconciliation_after_partition() {
|
|
367
|
+
let mut partitioned = PartitionedReplica::new();
|
|
368
|
+
|
|
369
|
+
// Start connected and apply shared operation
|
|
370
|
+
partitioned.apply_to_partition_a("op0", "rec0", 0);
|
|
371
|
+
|
|
372
|
+
// Partition
|
|
373
|
+
partitioned.partition();
|
|
374
|
+
|
|
375
|
+
// Apply different operations to each partition
|
|
376
|
+
partitioned.apply_to_partition_a("op1", "rec1", 10);
|
|
377
|
+
partitioned.apply_to_partition_b("op2", "rec2", 20);
|
|
378
|
+
|
|
379
|
+
// Reconcile
|
|
380
|
+
let result = partitioned.reconcile();
|
|
381
|
+
|
|
382
|
+
// Should have all 3 operations
|
|
383
|
+
assert_eq!(result.all_applied_operations.len(), 3);
|
|
384
|
+
assert_eq!(result.duplicate_operations.len(), 1); // op0 is shared
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
#[test]
|
|
388
|
+
fn test_partition_state_isolation() {
|
|
389
|
+
let mut partitioned = PartitionedReplica::new();
|
|
390
|
+
partitioned.partition();
|
|
391
|
+
|
|
392
|
+
partitioned.apply_to_partition_a("op1", "rec1", 100);
|
|
393
|
+
partitioned.apply_to_partition_b("op2", "rec2", 200);
|
|
394
|
+
|
|
395
|
+
// States should diverge
|
|
396
|
+
let (hash_a, hash_b) = partitioned.get_partition_hashes();
|
|
397
|
+
assert_ne!(hash_a, hash_b);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
#[test]
|
|
401
|
+
fn test_determinism_checker_satisfied() {
|
|
402
|
+
let mut checker = PartitionDeterminismChecker::new();
|
|
403
|
+
|
|
404
|
+
// Record multiple scenarios that produce same hash for same ops
|
|
405
|
+
checker.record_scenario(
|
|
406
|
+
"scenario_1".to_string(),
|
|
407
|
+
"final_hash_v1".to_string(),
|
|
408
|
+
vec!["op1".to_string(), "op2".to_string()],
|
|
409
|
+
);
|
|
410
|
+
checker.record_scenario(
|
|
411
|
+
"scenario_2".to_string(),
|
|
412
|
+
"final_hash_v1".to_string(),
|
|
413
|
+
vec!["op2".to_string(), "op1".to_string()],
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
let result = checker.verify_determinism();
|
|
417
|
+
assert_eq!(result, DeterminismCheckResult::Satisfied { scenarios_checked: 2 });
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
#[test]
|
|
421
|
+
fn test_determinism_checker_violated() {
|
|
422
|
+
let mut checker = PartitionDeterminismChecker::new();
|
|
423
|
+
|
|
424
|
+
// Record scenarios with same ops but different hashes
|
|
425
|
+
checker.record_scenario(
|
|
426
|
+
"scenario_a".to_string(),
|
|
427
|
+
"hash_a".to_string(),
|
|
428
|
+
vec!["op1".to_string(), "op2".to_string()],
|
|
429
|
+
);
|
|
430
|
+
checker.record_scenario(
|
|
431
|
+
"scenario_b".to_string(),
|
|
432
|
+
"hash_b".to_string(),
|
|
433
|
+
vec!["op1".to_string(), "op2".to_string()],
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
let result = checker.verify_determinism();
|
|
437
|
+
assert!(matches!(result, DeterminismCheckResult::Violated { .. }));
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
#[test]
|
|
441
|
+
fn test_determinism_checker_empty() {
|
|
442
|
+
let checker = PartitionDeterminismChecker::new();
|
|
443
|
+
let result = checker.verify_determinism();
|
|
444
|
+
assert_eq!(result, DeterminismCheckResult::NothingToCheck);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
#[test]
|
|
448
|
+
fn test_multiple_partitions_and_reconnect() {
|
|
449
|
+
let mut partitioned = PartitionedReplica::new();
|
|
450
|
+
|
|
451
|
+
// Initial state (both get op0)
|
|
452
|
+
partitioned.apply_to_partition_a("op0", "rec0", 0);
|
|
453
|
+
assert_eq!(partitioned.partition_a.applied_operations.len(), 1);
|
|
454
|
+
assert_eq!(partitioned.partition_b.applied_operations.len(), 1);
|
|
455
|
+
|
|
456
|
+
// Partition
|
|
457
|
+
partitioned.partition();
|
|
458
|
+
partitioned.apply_to_partition_a("op1", "rec1", 10);
|
|
459
|
+
partitioned.apply_to_partition_b("op2", "rec2", 20);
|
|
460
|
+
|
|
461
|
+
// After partition: A has [op0, op1], B has [op0, op2]
|
|
462
|
+
assert_eq!(partitioned.partition_a.applied_operations.len(), 2);
|
|
463
|
+
assert_eq!(partitioned.partition_b.applied_operations.len(), 2);
|
|
464
|
+
|
|
465
|
+
// Reconnect - now operations propagate to both
|
|
466
|
+
partitioned.reconnect();
|
|
467
|
+
partitioned.apply_to_partition_a("op3", "rec3", 30);
|
|
468
|
+
|
|
469
|
+
// After op3 applied while connected: A has [op0, op1, op3], B has [op0, op2, op3]
|
|
470
|
+
assert_eq!(partitioned.partition_a.applied_operations.len(), 3);
|
|
471
|
+
assert_eq!(partitioned.partition_b.applied_operations.len(), 3);
|
|
472
|
+
|
|
473
|
+
// But A still has op1 and B still has op2 (not synchronized yet)
|
|
474
|
+
assert!(partitioned.partition_a.applied_operations.contains(&"op1".to_string()));
|
|
475
|
+
assert!(partitioned.partition_b.applied_operations.contains(&"op2".to_string()));
|
|
476
|
+
}
|
|
477
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/// Peer registry module for distributed FeltDB fabric
|
|
2
|
+
use crate::{PeerAdvertisement, PeerId};
|
|
3
|
+
use std::collections::HashMap;
|
|
4
|
+
use std::sync::{Arc, Mutex};
|
|
5
|
+
|
|
6
|
+
/// A registry of peers in the distributed fabric
|
|
7
|
+
#[derive(Clone)]
|
|
8
|
+
pub struct PeerRegistry {
|
|
9
|
+
peers: Arc<Mutex<HashMap<String, PeerAdvertisement>>>,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
impl PeerRegistry {
|
|
13
|
+
/// Create a new peer registry
|
|
14
|
+
pub fn new() -> Self {
|
|
15
|
+
Self {
|
|
16
|
+
peers: Arc::new(Mutex::new(HashMap::new())),
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// Register a peer with its advertisement
|
|
21
|
+
pub fn register(&self, advertisement: PeerAdvertisement) -> Result<(), String> {
|
|
22
|
+
let mut peers = self.peers.lock().map_err(|e| e.to_string())?;
|
|
23
|
+
peers.insert(advertisement.peer_id.id.clone(), advertisement);
|
|
24
|
+
Ok(())
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Unregister a peer
|
|
28
|
+
pub fn unregister(&self, peer_id: &PeerId) -> Result<Option<PeerAdvertisement>, String> {
|
|
29
|
+
let mut peers = self.peers.lock().map_err(|e| e.to_string())?;
|
|
30
|
+
Ok(peers.remove(&peer_id.id))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Get a peer's advertisement
|
|
34
|
+
pub fn get(&self, peer_id: &PeerId) -> Result<Option<PeerAdvertisement>, String> {
|
|
35
|
+
let peers = self.peers.lock().map_err(|e| e.to_string())?;
|
|
36
|
+
Ok(peers.get(&peer_id.id).cloned())
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// Find all peers that provide a specific namespace
|
|
40
|
+
pub fn peers_for_namespace(&self, namespace: &str) -> Result<Vec<PeerAdvertisement>, String> {
|
|
41
|
+
let peers = self.peers.lock().map_err(|e| e.to_string())?;
|
|
42
|
+
let result = peers
|
|
43
|
+
.values()
|
|
44
|
+
.filter(|ad| ad.provides_namespace(namespace))
|
|
45
|
+
.cloned()
|
|
46
|
+
.collect();
|
|
47
|
+
Ok(result)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Find all peers that provide a specific capability
|
|
51
|
+
pub fn peers_for_capability(&self, capability: &str) -> Result<Vec<PeerAdvertisement>, String> {
|
|
52
|
+
let peers = self.peers.lock().map_err(|e| e.to_string())?;
|
|
53
|
+
let result = peers
|
|
54
|
+
.values()
|
|
55
|
+
.filter(|ad| ad.provides_capability(capability))
|
|
56
|
+
.cloned()
|
|
57
|
+
.collect();
|
|
58
|
+
Ok(result)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Get all registered peers
|
|
62
|
+
pub fn all_peers(&self) -> Result<Vec<PeerAdvertisement>, String> {
|
|
63
|
+
let peers = self.peers.lock().map_err(|e| e.to_string())?;
|
|
64
|
+
Ok(peers.values().cloned().collect())
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// Get the count of registered peers
|
|
68
|
+
pub fn peer_count(&self) -> Result<usize, String> {
|
|
69
|
+
let peers = self.peers.lock().map_err(|e| e.to_string())?;
|
|
70
|
+
Ok(peers.len())
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// Update a peer's advertisement
|
|
74
|
+
pub fn update(&self, advertisement: PeerAdvertisement) -> Result<(), String> {
|
|
75
|
+
let mut peers = self.peers.lock().map_err(|e| e.to_string())?;
|
|
76
|
+
peers.insert(advertisement.peer_id.id.clone(), advertisement);
|
|
77
|
+
Ok(())
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
impl Default for PeerRegistry {
|
|
82
|
+
fn default() -> Self {
|
|
83
|
+
Self::new()
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#[cfg(test)]
|
|
88
|
+
mod tests {
|
|
89
|
+
use super::*;
|
|
90
|
+
use crate::PeerAdvertisement;
|
|
91
|
+
|
|
92
|
+
#[test]
|
|
93
|
+
fn test_peer_registry_register() {
|
|
94
|
+
let registry = PeerRegistry::new();
|
|
95
|
+
let peer = PeerId::new("peer-a");
|
|
96
|
+
let ad = PeerAdvertisement::new(peer.clone()).with_namespace("docs");
|
|
97
|
+
|
|
98
|
+
registry.register(ad).unwrap();
|
|
99
|
+
|
|
100
|
+
let retrieved = registry.get(&peer).unwrap();
|
|
101
|
+
assert!(retrieved.is_some());
|
|
102
|
+
assert!(retrieved.unwrap().provides_namespace("docs"));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#[test]
|
|
106
|
+
fn test_peer_registry_peers_for_namespace() {
|
|
107
|
+
let registry = PeerRegistry::new();
|
|
108
|
+
|
|
109
|
+
let peer_a = PeerId::new("peer-a");
|
|
110
|
+
let ad_a = PeerAdvertisement::new(peer_a).with_namespace("docs");
|
|
111
|
+
registry.register(ad_a).unwrap();
|
|
112
|
+
|
|
113
|
+
let peer_b = PeerId::new("peer-b");
|
|
114
|
+
let ad_b = PeerAdvertisement::new(peer_b)
|
|
115
|
+
.with_namespace("orders")
|
|
116
|
+
.with_namespace("docs");
|
|
117
|
+
registry.register(ad_b).unwrap();
|
|
118
|
+
|
|
119
|
+
let peer_c = PeerId::new("peer-c");
|
|
120
|
+
let ad_c = PeerAdvertisement::new(peer_c).with_namespace("embeddings");
|
|
121
|
+
registry.register(ad_c).unwrap();
|
|
122
|
+
|
|
123
|
+
let docs_peers = registry.peers_for_namespace("docs").unwrap();
|
|
124
|
+
assert_eq!(docs_peers.len(), 2);
|
|
125
|
+
|
|
126
|
+
let orders_peers = registry.peers_for_namespace("orders").unwrap();
|
|
127
|
+
assert_eq!(orders_peers.len(), 1);
|
|
128
|
+
|
|
129
|
+
let embeddings_peers = registry.peers_for_namespace("embeddings").unwrap();
|
|
130
|
+
assert_eq!(embeddings_peers.len(), 1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#[test]
|
|
134
|
+
fn test_peer_registry_peers_for_capability() {
|
|
135
|
+
let registry = PeerRegistry::new();
|
|
136
|
+
|
|
137
|
+
let peer_a = PeerId::new("peer-a");
|
|
138
|
+
let ad_a = PeerAdvertisement::new(peer_a).with_capability("search:docs");
|
|
139
|
+
registry.register(ad_a).unwrap();
|
|
140
|
+
|
|
141
|
+
let peer_b = PeerId::new("peer-b");
|
|
142
|
+
let ad_b = PeerAdvertisement::new(peer_b)
|
|
143
|
+
.with_capability("vector:embeddings")
|
|
144
|
+
.with_capability("search:docs");
|
|
145
|
+
registry.register(ad_b).unwrap();
|
|
146
|
+
|
|
147
|
+
let search_peers = registry.peers_for_capability("search:docs").unwrap();
|
|
148
|
+
assert_eq!(search_peers.len(), 2);
|
|
149
|
+
|
|
150
|
+
let vector_peers = registry.peers_for_capability("vector:embeddings").unwrap();
|
|
151
|
+
assert_eq!(vector_peers.len(), 1);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
#[test]
|
|
155
|
+
fn test_peer_registry_unregister() {
|
|
156
|
+
let registry = PeerRegistry::new();
|
|
157
|
+
let peer = PeerId::new("peer-a");
|
|
158
|
+
let ad = PeerAdvertisement::new(peer.clone());
|
|
159
|
+
|
|
160
|
+
registry.register(ad).unwrap();
|
|
161
|
+
assert_eq!(registry.peer_count().unwrap(), 1);
|
|
162
|
+
|
|
163
|
+
registry.unregister(&peer).unwrap();
|
|
164
|
+
assert_eq!(registry.peer_count().unwrap(), 0);
|
|
165
|
+
}
|
|
166
|
+
}
|