@feltdb/core 0.8.4 → 0.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/Cargo.lock +165 -0
- package/dist/create/server-source/Cargo.toml +9 -0
- package/dist/create/server-source/crates/feltdb/Cargo.toml +3 -0
- package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +44 -44
- package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +12 -24
- package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +7 -13
- package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +13 -10
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +12 -9
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +78 -25
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +16 -13
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +13 -5
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +41 -22
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +33 -15
- package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +100 -33
- package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +56 -20
- package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +116 -41
- package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +186 -55
- package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +64 -26
- package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +46 -15
- package/dist/create/server-source/crates/feltdb/src/admission.rs +8 -15
- package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +43 -13
- package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +15 -42
- package/dist/create/server-source/crates/feltdb/src/analytics.rs +65 -19
- package/dist/create/server-source/crates/feltdb/src/application.rs +113 -30
- package/dist/create/server-source/crates/feltdb/src/authorization_security_tests.rs +475 -140
- package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +17 -15
- package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +0 -1
- package/dist/create/server-source/crates/feltdb/src/causal_backlog_bound.rs +59 -15
- package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +266 -114
- package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +25 -7
- package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +10 -15
- package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +3 -11
- package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +14 -5
- package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +21 -25
- package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +14 -11
- package/dist/create/server-source/crates/feltdb/src/dedup_bound_investigation.rs +103 -22
- package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +18 -15
- package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +12 -8
- package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +1 -5
- package/dist/create/server-source/crates/feltdb/src/durable_operation_identity.rs +87 -23
- package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +3 -7
- package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +10 -9
- package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +1 -6
- package/dist/create/server-source/crates/feltdb/src/indexing.rs +35 -38
- package/dist/create/server-source/crates/feltdb/src/lib.rs +507 -6
- package/dist/create/server-source/crates/feltdb/src/managed_cas_tests.rs +4 -1
- package/dist/create/server-source/crates/feltdb/src/metrics.rs +0 -1
- package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +1 -2
- package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +107 -30
- package/dist/create/server-source/crates/feltdb/src/observability.rs +19 -6
- package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +12 -11
- package/dist/create/server-source/crates/feltdb/src/operation_log.rs +9 -4
- package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +65 -18
- package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +193 -57
- package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +37 -27
- package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +38 -10
- package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +20 -14
- package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +394 -229
- package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +8 -6
- package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +11 -13
- package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +79 -70
- package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +3 -3
- package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +33 -11
- package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +6 -6
- package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +14 -56
- package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +29 -27
- package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +35 -17
- package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +8 -2
- package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +59 -15
- package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +70 -17
- package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +23 -6
- package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +12 -3
- package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +41 -11
- package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +56 -25
- package/dist/create/server-source/crates/feltdb/src/policy_evaluation.rs +701 -245
- package/dist/create/server-source/crates/feltdb/src/production_api.rs +31 -13
- package/dist/create/server-source/crates/feltdb/src/query_performance.rs +6 -8
- package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +5 -5
- package/dist/create/server-source/crates/feltdb/src/replica_acknowledgements.rs +48 -18
- package/dist/create/server-source/crates/feltdb/src/replica_membership.rs +30 -11
- package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +6 -3
- package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +4 -3
- package/dist/create/server-source/crates/feltdb/src/sharding.rs +36 -10
- package/dist/create/server-source/crates/feltdb/src/state_conflict_contract.rs +516 -0
- package/dist/create/server-source/crates/feltdb/src/state_contract.rs +13 -4
- package/dist/create/server-source/crates/feltdb/src/state_diff_contract.rs +222 -0
- package/dist/create/server-source/crates/feltdb/src/state_facade.rs +82 -54
- package/dist/create/server-source/crates/feltdb/src/state_hash.rs +2 -2
- package/dist/create/server-source/crates/feltdb/src/state_model.rs +1514 -537
- package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +6 -3
- package/dist/create/server-source/crates/feltdb/src/state_trigger.rs +672 -0
- package/dist/create/server-source/crates/feltdb/src/submission.rs +5 -11
- package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +6 -8
- package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +24 -35
- package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +24 -8
- package/dist/create/server-source/crates/feltdb/src/transaction_preconditions.rs +248 -59
- package/dist/create/server-source/crates/feltdb/src/transactions.rs +17 -20
- package/dist/create/server-source/crates/feltdb/src/trigger_contract.rs +749 -0
- package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +1 -0
- package/dist/create/server-source/crates/feltdb/src/workload.rs +512 -4
- package/dist/create/server-source/crates/feltdb/src/workload_diagnostics.rs +3 -4
- package/dist/create/server-source/crates/feltdb/tests/branching_evidence.rs +299 -0
- package/dist/create/server-source/crates/feltdb/tests/current_revision_authority_evidence.rs +288 -0
- package/dist/create/server-source/crates/feltdb/tests/durable_format_compatibility.rs +392 -0
- package/dist/create/server-source/crates/feltdb/tests/feltdb_state_boundary_tests.rs +436 -220
- package/dist/create/server-source/crates/feltdb/tests/fixtures/state_conflict_contract_corpus.json +1916 -0
- package/dist/create/server-source/crates/feltdb/tests/fixtures/state_diff_contract_corpus.json +1878 -0
- package/dist/create/server-source/crates/feltdb/tests/fixtures/trigger_contract_corpus.json +1862 -0
- package/dist/create/server-source/crates/feltdb/tests/pr34_query_collection.rs +2 -1
- package/dist/create/server-source/crates/feltdb/tests/pr35_equality_index.rs +80 -25
- package/dist/create/server-source/crates/feltdb/tests/pr7_self_authorization_proof.rs +5 -8
- package/dist/create/server-source/crates/feltdb/tests/pr8_vocabulary_assessment.rs +52 -44
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase2_boundary_tests.rs +33 -16
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3a_path_a_tests.rs +22 -7
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_authorized_mutations.rs +41 -22
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_role_based_authorization.rs +25 -8
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_simple_auth_delete.rs +9 -6
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_team_delete_role_authorization.rs +120 -69
- package/dist/create/server-source/crates/feltdb/tests/pr9_teams_role_based_access.rs +21 -10
- package/dist/create/server-source/crates/feltdb/tests/production_readiness_contract.rs +1032 -0
- package/dist/create/server-source/crates/feltdb/tests/reconciliation_application.rs +868 -0
- package/dist/create/server-source/crates/feltdb/tests/reconciliation_wire_format_evidence.rs +221 -0
- package/dist/create/server-source/crates/feltdb/tests/resource_scoped_revisions.rs +338 -0
- package/dist/create/server-source/crates/feltdb/tests/revision_identity_contract.rs +1039 -0
- package/dist/create/server-source/crates/feltdb/tests/revision_model_decision.rs +739 -0
- package/dist/create/server-source/crates/feltdb/tests/revision_retention_boundary_evidence.rs +427 -0
- package/dist/create/server-source/crates/feltdb/tests/saas_authorization_integration.rs +3 -3
- package/dist/create/server-source/crates/feltdb/tests/saas_invitation_lifecycle.rs +25 -22
- package/dist/create/server-source/crates/feltdb/tests/state_conflict_contract_conformance.rs +1799 -0
- package/dist/create/server-source/crates/feltdb/tests/state_diff_contract_conformance.rs +1316 -0
- package/dist/create/server-source/crates/feltdb/tests/state_model_integration.rs +53 -61
- package/dist/create/server-source/crates/feltdb/tests/state_persistence_integration.rs +156 -61
- package/dist/create/server-source/crates/feltdb/tests/state_store_boundary_evidence.rs +299 -0
- package/dist/create/server-source/crates/feltdb/tests/sync_divergence_evidence.rs +255 -0
- package/dist/create/server-source/crates/feltdb/tests/three_way_input_boundary_evidence.rs +249 -0
- package/dist/create/server-source/crates/feltdb/tests/trigger_contract_conformance.rs +994 -0
- package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +2 -1
- package/dist/create/server-source/crates/feltdb-server/src/auth.rs +164 -13
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +326 -6
- package/dist/studio-app/assets/{feltdb_wasm-CVQWgXO-.js → feltdb_wasm-DaNwCLRX.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-DnsHNv6g.wasm +0 -0
- package/dist/studio-app/assets/{index-DwgNAIIX.js → index-j8IlhNqJ.js} +1 -1
- package/dist/studio-app/index.html +1 -1
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/workload.d.ts +2 -1
- package/dist/workload.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-CNVpvaZV.wasm +0 -0
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
/// - Relationship access patterns
|
|
8
8
|
/// - Materialized views and projections
|
|
9
9
|
/// - Multi-key operations
|
|
10
|
-
|
|
11
10
|
use serde::{Deserialize, Serialize};
|
|
12
11
|
use std::collections::HashMap;
|
|
13
12
|
|
|
@@ -142,7 +141,11 @@ impl ProductionAPI {
|
|
|
142
141
|
.ok_or_else(|| format!("Resource not found: {}", id))
|
|
143
142
|
}
|
|
144
143
|
|
|
145
|
-
pub fn read_many(
|
|
144
|
+
pub fn read_many(
|
|
145
|
+
&self,
|
|
146
|
+
ids: &[&str],
|
|
147
|
+
_options: ReadOptions,
|
|
148
|
+
) -> Result<Vec<StateValue>, String> {
|
|
146
149
|
let mut results = Vec::new();
|
|
147
150
|
for id in ids {
|
|
148
151
|
if let Some(value) = self.state_store.get(*id) {
|
|
@@ -152,7 +155,11 @@ impl ProductionAPI {
|
|
|
152
155
|
Ok(results)
|
|
153
156
|
}
|
|
154
157
|
|
|
155
|
-
pub fn write(
|
|
158
|
+
pub fn write(
|
|
159
|
+
&mut self,
|
|
160
|
+
mutation: Mutation,
|
|
161
|
+
options: WriteOptions,
|
|
162
|
+
) -> Result<MutationResult, String> {
|
|
156
163
|
if let Some(ref key) = options.idempotency_key {
|
|
157
164
|
if let Some(existing) = self.state_store.get(&mutation.resource_id) {
|
|
158
165
|
return Ok(MutationResult {
|
|
@@ -177,7 +184,8 @@ impl ProductionAPI {
|
|
|
177
184
|
updated_at: now,
|
|
178
185
|
};
|
|
179
186
|
|
|
180
|
-
self.state_store
|
|
187
|
+
self.state_store
|
|
188
|
+
.insert(mutation.resource_id.clone(), value.clone());
|
|
181
189
|
|
|
182
190
|
Ok(MutationResult {
|
|
183
191
|
success: true,
|
|
@@ -209,7 +217,10 @@ impl ProductionAPI {
|
|
|
209
217
|
Ok(results)
|
|
210
218
|
}
|
|
211
219
|
|
|
212
|
-
pub fn transaction(
|
|
220
|
+
pub fn transaction(
|
|
221
|
+
&mut self,
|
|
222
|
+
request: TransactionRequest,
|
|
223
|
+
) -> Result<TransactionResult, String> {
|
|
213
224
|
self.transaction_id_counter += 1;
|
|
214
225
|
let transaction_id = format!("tx-{}", self.transaction_id_counter);
|
|
215
226
|
|
|
@@ -246,13 +257,12 @@ impl ProductionAPI {
|
|
|
246
257
|
.push(target);
|
|
247
258
|
}
|
|
248
259
|
|
|
249
|
-
pub fn get_relationships(
|
|
260
|
+
pub fn get_relationships(
|
|
261
|
+
&self,
|
|
262
|
+
query: RelationshipQuery,
|
|
263
|
+
) -> Result<RelationshipResult, String> {
|
|
250
264
|
let key = format!("{}:{}", query.source_id, query.relationship);
|
|
251
|
-
let targets = self
|
|
252
|
-
.relationships
|
|
253
|
-
.get(&key)
|
|
254
|
-
.cloned()
|
|
255
|
-
.unwrap_or_default();
|
|
265
|
+
let targets = self.relationships.get(&key).cloned().unwrap_or_default();
|
|
256
266
|
|
|
257
267
|
Ok(RelationshipResult {
|
|
258
268
|
source_id: query.source_id,
|
|
@@ -359,8 +369,16 @@ mod tests {
|
|
|
359
369
|
#[test]
|
|
360
370
|
fn test_relationships() {
|
|
361
371
|
let mut api = ProductionAPI::new();
|
|
362
|
-
api.add_relationship(
|
|
363
|
-
|
|
372
|
+
api.add_relationship(
|
|
373
|
+
"user-1".to_string(),
|
|
374
|
+
"follows".to_string(),
|
|
375
|
+
"user-2".to_string(),
|
|
376
|
+
);
|
|
377
|
+
api.add_relationship(
|
|
378
|
+
"user-1".to_string(),
|
|
379
|
+
"follows".to_string(),
|
|
380
|
+
"user-3".to_string(),
|
|
381
|
+
);
|
|
364
382
|
|
|
365
383
|
let result = api
|
|
366
384
|
.get_relationships(RelationshipQuery {
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
/// - Latency contract enforcement
|
|
6
6
|
/// - Performance degradation detection
|
|
7
7
|
/// - Query optimization recommendations
|
|
8
|
-
|
|
9
8
|
use serde::{Deserialize, Serialize};
|
|
10
9
|
use std::collections::HashMap;
|
|
11
10
|
use std::time::{Duration, Instant};
|
|
@@ -156,7 +155,11 @@ impl QueryPerformanceTracker {
|
|
|
156
155
|
for metric in &self.metrics {
|
|
157
156
|
csv.push_str(&format!(
|
|
158
157
|
"{},{},{},{},{}\n",
|
|
159
|
-
metric.timestamp,
|
|
158
|
+
metric.timestamp,
|
|
159
|
+
metric.query_type,
|
|
160
|
+
metric.duration_us,
|
|
161
|
+
metric.index_name,
|
|
162
|
+
metric.result_count
|
|
160
163
|
));
|
|
161
164
|
}
|
|
162
165
|
csv
|
|
@@ -187,12 +190,7 @@ mod tests {
|
|
|
187
190
|
std::thread::sleep(Duration::from_micros(50));
|
|
188
191
|
let elapsed = timer.elapsed_us();
|
|
189
192
|
|
|
190
|
-
tracker.record_query(
|
|
191
|
-
"index_lookup".to_string(),
|
|
192
|
-
"idx_1".to_string(),
|
|
193
|
-
10,
|
|
194
|
-
elapsed,
|
|
195
|
-
);
|
|
193
|
+
tracker.record_query("index_lookup".to_string(), "idx_1".to_string(), 10, elapsed);
|
|
196
194
|
|
|
197
195
|
let stats = tracker.get_stats("index_lookup").unwrap();
|
|
198
196
|
assert_eq!(stats.count, 1);
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
/// - After index deletion (rebuild index from scratch)
|
|
9
9
|
///
|
|
10
10
|
/// All paths must converge to identical state_hash.
|
|
11
|
-
|
|
12
11
|
use crate::operation_log::{MemoryOperationLog, OperationLogEntry};
|
|
13
12
|
use crate::state_hash::{CanonicalState, StateHash};
|
|
14
13
|
use crate::state_transition_store::MemoryStateTransitionStore;
|
|
@@ -46,7 +45,10 @@ pub struct ReplayFuzzer {
|
|
|
46
45
|
}
|
|
47
46
|
|
|
48
47
|
impl ReplayFuzzer {
|
|
49
|
-
pub fn new(
|
|
48
|
+
pub fn new(
|
|
49
|
+
operation_log: MemoryOperationLog,
|
|
50
|
+
transition_store: MemoryStateTransitionStore,
|
|
51
|
+
) -> Self {
|
|
50
52
|
Self {
|
|
51
53
|
operation_log,
|
|
52
54
|
transition_store,
|
|
@@ -99,9 +101,7 @@ impl ReplayFuzzer {
|
|
|
99
101
|
let log_size = self.operation_log.size();
|
|
100
102
|
if snapshot_at >= log_size {
|
|
101
103
|
return ReplayPath {
|
|
102
|
-
scenario: ReplayScenario::AfterSnapshot {
|
|
103
|
-
snapshot_at,
|
|
104
|
-
},
|
|
104
|
+
scenario: ReplayScenario::AfterSnapshot { snapshot_at },
|
|
105
105
|
final_state_hash: StateHash::from_hex("snapshot_beyond_log".to_string()),
|
|
106
106
|
operations_replayed: 0,
|
|
107
107
|
transitions_created: 0,
|
|
@@ -137,12 +137,16 @@ impl AcknowledgementStore {
|
|
|
137
137
|
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, AcknowledgementError> {
|
|
138
138
|
let path = path.as_ref().to_path_buf();
|
|
139
139
|
let acknowledgements = if path.exists() {
|
|
140
|
-
let bytes =
|
|
140
|
+
let bytes =
|
|
141
|
+
std::fs::read(&path).map_err(|e| AcknowledgementError::Io(e.to_string()))?;
|
|
141
142
|
serde_json::from_slice(&bytes).map_err(|e| AcknowledgementError::Io(e.to_string()))?
|
|
142
143
|
} else {
|
|
143
144
|
BTreeMap::new()
|
|
144
145
|
};
|
|
145
|
-
Ok(Self {
|
|
146
|
+
Ok(Self {
|
|
147
|
+
path,
|
|
148
|
+
acknowledgements,
|
|
149
|
+
})
|
|
146
150
|
}
|
|
147
151
|
|
|
148
152
|
fn persist(&self) -> Result<(), AcknowledgementError> {
|
|
@@ -154,10 +158,12 @@ impl AcknowledgementStore {
|
|
|
154
158
|
.map_err(|e| AcknowledgementError::Io(e.to_string()))?;
|
|
155
159
|
{
|
|
156
160
|
use std::io::Write;
|
|
157
|
-
let mut file =
|
|
158
|
-
|
|
159
|
-
file.write_all(&bytes)
|
|
160
|
-
|
|
161
|
+
let mut file = std::fs::File::create(&temp)
|
|
162
|
+
.map_err(|e| AcknowledgementError::Io(e.to_string()))?;
|
|
163
|
+
file.write_all(&bytes)
|
|
164
|
+
.map_err(|e| AcknowledgementError::Io(e.to_string()))?;
|
|
165
|
+
file.sync_all()
|
|
166
|
+
.map_err(|e| AcknowledgementError::Io(e.to_string()))?;
|
|
161
167
|
}
|
|
162
168
|
std::fs::rename(&temp, &self.path).map_err(|e| AcknowledgementError::Io(e.to_string()))?;
|
|
163
169
|
if let Some(parent) = self.path.parent() {
|
|
@@ -227,7 +233,9 @@ impl AcknowledgementStore {
|
|
|
227
233
|
// No required replicas at all. Still not a licence: a node with no
|
|
228
234
|
// membership has not established that it is alone, it has
|
|
229
235
|
// established nothing.
|
|
230
|
-
return ReclamationEvidence::Blocked {
|
|
236
|
+
return ReclamationEvidence::Blocked {
|
|
237
|
+
waiting_on: Vec::new(),
|
|
238
|
+
};
|
|
231
239
|
}
|
|
232
240
|
|
|
233
241
|
let missing: Vec<String> = required
|
|
@@ -236,7 +244,9 @@ impl AcknowledgementStore {
|
|
|
236
244
|
.cloned()
|
|
237
245
|
.collect();
|
|
238
246
|
if !missing.is_empty() {
|
|
239
|
-
return ReclamationEvidence::Blocked {
|
|
247
|
+
return ReclamationEvidence::Blocked {
|
|
248
|
+
waiting_on: missing,
|
|
249
|
+
};
|
|
240
250
|
}
|
|
241
251
|
|
|
242
252
|
// The minimum over every required replica, component-wise. A frontier
|
|
@@ -259,7 +269,10 @@ impl AcknowledgementStore {
|
|
|
259
269
|
frontier.clocks.insert(origin, lowest);
|
|
260
270
|
}
|
|
261
271
|
}
|
|
262
|
-
ReclamationEvidence::SafeThrough {
|
|
272
|
+
ReclamationEvidence::SafeThrough {
|
|
273
|
+
frontier,
|
|
274
|
+
covered: required,
|
|
275
|
+
}
|
|
263
276
|
}
|
|
264
277
|
}
|
|
265
278
|
|
|
@@ -328,12 +341,17 @@ mod tests {
|
|
|
328
341
|
fn a_required_replica_with_no_acknowledgement_blocks() {
|
|
329
342
|
let dir = tempfile::tempdir().unwrap();
|
|
330
343
|
let mut acks = store(&dir);
|
|
331
|
-
acks.record(ack("rb", &[("A", 3)], &["ra", "rb", "rc"]))
|
|
344
|
+
acks.record(ack("rb", &[("A", 3)], &["ra", "rb", "rc"]))
|
|
345
|
+
.unwrap();
|
|
332
346
|
|
|
333
347
|
let evidence = acks.evidence(&["ra".into(), "rb".into(), "rc".into()]);
|
|
334
348
|
match evidence {
|
|
335
349
|
ReclamationEvidence::Blocked { waiting_on } => {
|
|
336
|
-
assert_eq!(
|
|
350
|
+
assert_eq!(
|
|
351
|
+
waiting_on,
|
|
352
|
+
vec!["ra", "rc"],
|
|
353
|
+
"and it names who it waits for"
|
|
354
|
+
);
|
|
337
355
|
}
|
|
338
356
|
other => panic!("missing acknowledgement must block, got {other:?}"),
|
|
339
357
|
}
|
|
@@ -347,7 +365,8 @@ mod tests {
|
|
|
347
365
|
let dir = tempfile::tempdir().unwrap();
|
|
348
366
|
let mut acks = store(&dir);
|
|
349
367
|
acks.record(ack("ra", &[("A", 3)], &["ra", "rb"])).unwrap();
|
|
350
|
-
acks.record(ack("rb", &[("A", 3)], &["ra", "rb", "rc"]))
|
|
368
|
+
acks.record(ack("rb", &[("A", 3)], &["ra", "rb", "rc"]))
|
|
369
|
+
.unwrap();
|
|
351
370
|
|
|
352
371
|
let local = vec!["ra".to_string(), "rb".to_string()];
|
|
353
372
|
assert_eq!(acks.required_union(&local), vec!["ra", "rb", "rc"]);
|
|
@@ -363,7 +382,10 @@ mod tests {
|
|
|
363
382
|
// alone. It has established nothing, and nothing is what it may act on.
|
|
364
383
|
let dir = tempfile::tempdir().unwrap();
|
|
365
384
|
let acks = store(&dir);
|
|
366
|
-
assert!(matches!(
|
|
385
|
+
assert!(matches!(
|
|
386
|
+
acks.evidence(&[]),
|
|
387
|
+
ReclamationEvidence::Blocked { .. }
|
|
388
|
+
));
|
|
367
389
|
}
|
|
368
390
|
|
|
369
391
|
#[test]
|
|
@@ -395,8 +417,10 @@ mod tests {
|
|
|
395
417
|
fn the_safe_frontier_is_the_minimum_over_every_required_replica() {
|
|
396
418
|
let dir = tempfile::tempdir().unwrap();
|
|
397
419
|
let mut acks = store(&dir);
|
|
398
|
-
acks.record(ack("ra", &[("A", 9), ("B", 4)], &["ra", "rb"]))
|
|
399
|
-
|
|
420
|
+
acks.record(ack("ra", &[("A", 9), ("B", 4)], &["ra", "rb"]))
|
|
421
|
+
.unwrap();
|
|
422
|
+
acks.record(ack("rb", &[("A", 5), ("B", 6)], &["ra", "rb"]))
|
|
423
|
+
.unwrap();
|
|
400
424
|
|
|
401
425
|
match acks.evidence(&["ra".into(), "rb".into()]) {
|
|
402
426
|
ReclamationEvidence::SafeThrough { frontier, covered } => {
|
|
@@ -415,7 +439,8 @@ mod tests {
|
|
|
415
439
|
// anything is safe through.
|
|
416
440
|
let dir = tempfile::tempdir().unwrap();
|
|
417
441
|
let mut acks = store(&dir);
|
|
418
|
-
acks.record(ack("ra", &[("A", 4), ("C", 9)], &["ra", "rb"]))
|
|
442
|
+
acks.record(ack("ra", &[("A", 4), ("C", 9)], &["ra", "rb"]))
|
|
443
|
+
.unwrap();
|
|
419
444
|
acks.record(ack("rb", &[("A", 4)], &["ra", "rb"])).unwrap();
|
|
420
445
|
|
|
421
446
|
match acks.evidence(&["ra".into(), "rb".into()]) {
|
|
@@ -437,7 +462,8 @@ mod tests {
|
|
|
437
462
|
let dir = tempfile::tempdir().unwrap();
|
|
438
463
|
{
|
|
439
464
|
let mut acks = store(&dir);
|
|
440
|
-
acks.record(ack("rb", &[("A", 3)], &["ra", "rb", "rc"]))
|
|
465
|
+
acks.record(ack("rb", &[("A", 3)], &["ra", "rb", "rc"]))
|
|
466
|
+
.unwrap();
|
|
441
467
|
}
|
|
442
468
|
let recovered = store(&dir);
|
|
443
469
|
match recovered.evidence(&["ra".into(), "rb".into()]) {
|
|
@@ -464,7 +490,11 @@ mod tests {
|
|
|
464
490
|
|
|
465
491
|
let recovered = store(&dir);
|
|
466
492
|
assert_eq!(
|
|
467
|
-
recovered
|
|
493
|
+
recovered
|
|
494
|
+
.get("rb")
|
|
495
|
+
.unwrap()
|
|
496
|
+
.origin_sequence_high_water
|
|
497
|
+
.get("A"),
|
|
468
498
|
Some(&7),
|
|
469
499
|
);
|
|
470
500
|
}
|
|
@@ -146,19 +146,23 @@ impl MembershipStore {
|
|
|
146
146
|
/// directory. Membership is what will later authorise deleting history, so
|
|
147
147
|
/// it is written with the same care as the log it will authorise deleting.
|
|
148
148
|
fn persist(&self) -> Result<(), MembershipError> {
|
|
149
|
-
let Some(cluster) = &self.cluster else {
|
|
149
|
+
let Some(cluster) = &self.cluster else {
|
|
150
|
+
return Ok(());
|
|
151
|
+
};
|
|
150
152
|
if let Some(parent) = self.path.parent() {
|
|
151
153
|
std::fs::create_dir_all(parent).map_err(|e| MembershipError::Io(e.to_string()))?;
|
|
152
154
|
}
|
|
153
155
|
let temp = self.path.with_extension("tmp");
|
|
154
|
-
let bytes =
|
|
155
|
-
.map_err(|e| MembershipError::Io(e.to_string()))?;
|
|
156
|
+
let bytes =
|
|
157
|
+
serde_json::to_vec_pretty(cluster).map_err(|e| MembershipError::Io(e.to_string()))?;
|
|
156
158
|
{
|
|
157
159
|
use std::io::Write;
|
|
158
|
-
let mut file =
|
|
160
|
+
let mut file =
|
|
161
|
+
std::fs::File::create(&temp).map_err(|e| MembershipError::Io(e.to_string()))?;
|
|
162
|
+
file.write_all(&bytes)
|
|
163
|
+
.map_err(|e| MembershipError::Io(e.to_string()))?;
|
|
164
|
+
file.sync_all()
|
|
159
165
|
.map_err(|e| MembershipError::Io(e.to_string()))?;
|
|
160
|
-
file.write_all(&bytes).map_err(|e| MembershipError::Io(e.to_string()))?;
|
|
161
|
-
file.sync_all().map_err(|e| MembershipError::Io(e.to_string()))?;
|
|
162
166
|
}
|
|
163
167
|
std::fs::rename(&temp, &self.path).map_err(|e| MembershipError::Io(e.to_string()))?;
|
|
164
168
|
if let Some(parent) = self.path.parent() {
|
|
@@ -435,7 +439,11 @@ mod tests {
|
|
|
435
439
|
cluster.suspect_replica("rc").unwrap();
|
|
436
440
|
cluster.mark_seen("rc").unwrap();
|
|
437
441
|
let seen = cluster.get_members().into_iter().next().unwrap();
|
|
438
|
-
assert_eq!(
|
|
442
|
+
assert_eq!(
|
|
443
|
+
seen.status,
|
|
444
|
+
ReplicaStatus::Active,
|
|
445
|
+
"contact reinstates a suspected replica"
|
|
446
|
+
);
|
|
439
447
|
assert!(seen.last_seen_at_ms.is_some());
|
|
440
448
|
|
|
441
449
|
cluster.remove_replica("rc").unwrap();
|
|
@@ -503,7 +511,10 @@ mod tests {
|
|
|
503
511
|
fn membership_operations_need_a_cluster() {
|
|
504
512
|
let dir = tempfile::tempdir().unwrap();
|
|
505
513
|
let mut cluster = store(&dir);
|
|
506
|
-
assert!(matches!(
|
|
514
|
+
assert!(matches!(
|
|
515
|
+
cluster.join_replica("ra", "A"),
|
|
516
|
+
Err(MembershipError::NoCluster)
|
|
517
|
+
));
|
|
507
518
|
cluster.create_cluster("c1").unwrap();
|
|
508
519
|
assert!(matches!(
|
|
509
520
|
cluster.create_cluster("c2"),
|
|
@@ -523,7 +534,10 @@ mod tests {
|
|
|
523
534
|
cluster.join_replica("ra", "A").unwrap();
|
|
524
535
|
cluster.approve_replica("ra").unwrap();
|
|
525
536
|
assert!(
|
|
526
|
-
matches!(
|
|
537
|
+
matches!(
|
|
538
|
+
cluster.approve_replica("ra"),
|
|
539
|
+
Err(MembershipError::NotJoining(_))
|
|
540
|
+
),
|
|
527
541
|
"approving twice is not a silent no-op"
|
|
528
542
|
);
|
|
529
543
|
}
|
|
@@ -648,7 +662,9 @@ mod tests {
|
|
|
648
662
|
let mut cluster = MembershipStore::open(&path).unwrap();
|
|
649
663
|
cluster.create_cluster("c1").unwrap();
|
|
650
664
|
for index in 0..20 {
|
|
651
|
-
cluster
|
|
665
|
+
cluster
|
|
666
|
+
.join_replica(&format!("r{index}"), &format!("N{index}"))
|
|
667
|
+
.unwrap();
|
|
652
668
|
let observed = MembershipStore::open(&path).unwrap();
|
|
653
669
|
assert_eq!(
|
|
654
670
|
observed.get_members().len(),
|
|
@@ -656,6 +672,9 @@ mod tests {
|
|
|
656
672
|
"every observation is of a complete cluster"
|
|
657
673
|
);
|
|
658
674
|
}
|
|
659
|
-
assert!(
|
|
675
|
+
assert!(
|
|
676
|
+
!path.with_extension("tmp").exists(),
|
|
677
|
+
"no temp file is left at rest"
|
|
678
|
+
);
|
|
660
679
|
}
|
|
661
680
|
}
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
/// - Routing messages from leader to followers
|
|
7
7
|
/// - Acknowledgment tracking
|
|
8
8
|
/// - Retry logic for lost messages
|
|
9
|
-
|
|
10
9
|
use crate::distributed_transactions::ReplicationMessage;
|
|
11
10
|
use std::collections::{HashMap, VecDeque};
|
|
12
11
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
@@ -67,7 +66,10 @@ impl PeerReplicationQueue {
|
|
|
67
66
|
}
|
|
68
67
|
|
|
69
68
|
pub fn pending_count(&self) -> usize {
|
|
70
|
-
self.messages
|
|
69
|
+
self.messages
|
|
70
|
+
.iter()
|
|
71
|
+
.filter(|m| m.status == DeliveryStatus::Pending)
|
|
72
|
+
.count()
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
pub fn next_pending(&mut self) -> Option<TrackedMessage> {
|
|
@@ -103,7 +105,8 @@ impl ReplicationManager {
|
|
|
103
105
|
|
|
104
106
|
/// Register a peer to receive replication messages
|
|
105
107
|
pub fn register_peer(&mut self, peer_id: String) {
|
|
106
|
-
self.peer_queues
|
|
108
|
+
self.peer_queues
|
|
109
|
+
.insert(peer_id.clone(), PeerReplicationQueue::new(peer_id));
|
|
107
110
|
}
|
|
108
111
|
|
|
109
112
|
/// Enqueue a message for replication to all peers
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
///
|
|
3
3
|
/// Decouples ReplicationMessage handling from transport details.
|
|
4
4
|
/// Works with typed messages (ReplicationMessage) rather than raw bytes.
|
|
5
|
-
|
|
6
5
|
use crate::distributed_transactions::ReplicationMessage;
|
|
7
6
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
8
7
|
use std::sync::Arc;
|
|
@@ -71,7 +70,8 @@ impl ProtocolMetrics {
|
|
|
71
70
|
|
|
72
71
|
pub fn record_receive(&self, bytes: usize) {
|
|
73
72
|
self.messages_received.fetch_add(1, Ordering::Relaxed);
|
|
74
|
-
self.bytes_received
|
|
73
|
+
self.bytes_received
|
|
74
|
+
.fetch_add(bytes as u64, Ordering::Relaxed);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
pub fn record_error(&self) {
|
|
@@ -79,7 +79,8 @@ impl ProtocolMetrics {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
pub fn record_connection_state_change(&self) {
|
|
82
|
-
self.connection_state_changes
|
|
82
|
+
self.connection_state_changes
|
|
83
|
+
.fetch_add(1, Ordering::Relaxed);
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
pub fn get_messages_sent(&self) -> u64 {
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
* - Query routing to appropriate shards
|
|
9
9
|
* - Data migration between shards
|
|
10
10
|
*/
|
|
11
|
-
|
|
12
11
|
use serde::{Deserialize, Serialize};
|
|
13
12
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
|
14
13
|
|
|
@@ -189,7 +188,9 @@ impl ShardManager {
|
|
|
189
188
|
}
|
|
190
189
|
"delete" => {
|
|
191
190
|
metrics.entry_count = metrics.entry_count.saturating_sub(records_affected);
|
|
192
|
-
metrics.size_bytes = metrics
|
|
191
|
+
metrics.size_bytes = metrics
|
|
192
|
+
.size_bytes
|
|
193
|
+
.saturating_sub(size_bytes.min(metrics.size_bytes));
|
|
193
194
|
}
|
|
194
195
|
_ => {}
|
|
195
196
|
}
|
|
@@ -253,7 +254,9 @@ impl ShardManager {
|
|
|
253
254
|
}
|
|
254
255
|
|
|
255
256
|
// High write load
|
|
256
|
-
if write_std_dev > 0.0
|
|
257
|
+
if write_std_dev > 0.0
|
|
258
|
+
&& (metrics.writes as f64 - avg_writes).abs() > write_std_dev * 2.0
|
|
259
|
+
{
|
|
257
260
|
alerts.push(HotspotAlert {
|
|
258
261
|
shard_id: metrics.shard_id.clone(),
|
|
259
262
|
reason: "High write load".to_string(),
|
|
@@ -269,7 +272,9 @@ impl ShardManager {
|
|
|
269
272
|
}
|
|
270
273
|
|
|
271
274
|
// Data imbalance
|
|
272
|
-
if entry_std_dev > 0.0
|
|
275
|
+
if entry_std_dev > 0.0
|
|
276
|
+
&& (metrics.entry_count as f64 - avg_entries).abs() > entry_std_dev * 3.0
|
|
277
|
+
{
|
|
273
278
|
alerts.push(HotspotAlert {
|
|
274
279
|
shard_id: metrics.shard_id.clone(),
|
|
275
280
|
reason: "Data imbalance".to_string(),
|
|
@@ -297,13 +302,22 @@ impl ShardManager {
|
|
|
297
302
|
for hotspot in hotspots {
|
|
298
303
|
if hotspot.severity == "high" {
|
|
299
304
|
// Find the overloaded shard
|
|
300
|
-
if let Some(shard_range) = self
|
|
305
|
+
if let Some(shard_range) = self
|
|
306
|
+
.shard_ranges
|
|
307
|
+
.iter()
|
|
308
|
+
.find(|r| r.shard_id == hotspot.shard_id)
|
|
309
|
+
{
|
|
301
310
|
// Split the hash range in half
|
|
302
|
-
let mid_hash =
|
|
311
|
+
let mid_hash =
|
|
312
|
+
(shard_range.start_hash as u128 + shard_range.end_hash as u128) / 2;
|
|
303
313
|
let mid_hash = mid_hash as u64;
|
|
304
314
|
|
|
305
315
|
// Create a new shard ID for the split
|
|
306
|
-
let new_shard_id = ShardId(format!(
|
|
316
|
+
let new_shard_id = ShardId(format!(
|
|
317
|
+
"{}-split-{}",
|
|
318
|
+
hotspot.shard_id.0,
|
|
319
|
+
current_time_ms()
|
|
320
|
+
));
|
|
307
321
|
|
|
308
322
|
operations.push(RebalanceOperation {
|
|
309
323
|
source_shard: hotspot.shard_id.clone(),
|
|
@@ -415,7 +429,10 @@ mod tests {
|
|
|
415
429
|
ShardingStrategy::ConsistentHash,
|
|
416
430
|
);
|
|
417
431
|
|
|
418
|
-
let shards = vec![
|
|
432
|
+
let shards = vec![
|
|
433
|
+
ShardId("shard-0".to_string()),
|
|
434
|
+
ShardId("shard-1".to_string()),
|
|
435
|
+
];
|
|
419
436
|
let mut replicas = HashMap::new();
|
|
420
437
|
replicas.insert("shard-0".to_string(), vec!["node-1".to_string()]);
|
|
421
438
|
replicas.insert("shard-1".to_string(), vec!["node-2".to_string()]);
|
|
@@ -435,7 +452,10 @@ mod tests {
|
|
|
435
452
|
ShardingStrategy::ConsistentHash,
|
|
436
453
|
);
|
|
437
454
|
|
|
438
|
-
let shards = vec![
|
|
455
|
+
let shards = vec![
|
|
456
|
+
ShardId("shard-0".to_string()),
|
|
457
|
+
ShardId("shard-1".to_string()),
|
|
458
|
+
];
|
|
439
459
|
manager.initialize_shards(shards, HashMap::new());
|
|
440
460
|
|
|
441
461
|
let shard = manager.get_shard_for_key("user123");
|
|
@@ -510,7 +530,13 @@ mod tests {
|
|
|
510
530
|
manager.initialize_shards(shards, HashMap::new());
|
|
511
531
|
|
|
512
532
|
// Create severe imbalanced load across 3 shards
|
|
513
|
-
manager.record_shard_operation(
|
|
533
|
+
manager.record_shard_operation(
|
|
534
|
+
&ShardId("shard-0".to_string()),
|
|
535
|
+
"write",
|
|
536
|
+
5.0,
|
|
537
|
+
100000,
|
|
538
|
+
50000000,
|
|
539
|
+
);
|
|
514
540
|
manager.record_shard_operation(&ShardId("shard-1".to_string()), "write", 5.0, 1000, 500000);
|
|
515
541
|
manager.record_shard_operation(&ShardId("shard-2".to_string()), "write", 5.0, 100, 50000);
|
|
516
542
|
|