@feltdb/core 0.4.12 → 0.4.14
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 +42 -25
- package/dist/cli/index.js +1 -1
- package/dist/create/cli.js +39 -4
- package/dist/create/create.js +30 -18
- package/dist/create/docker-compose-generator.js +68 -11
- 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/ConflictExplorer.d.ts.map +1 -1
- package/dist/studio/components/index.js +1 -1
- package/dist/studio/{components-Q0Nx6gLE.js → components-9kDSWiGL.js} +163 -47
- package/dist/studio/index.js +31 -28
- package/dist/studio-app/assets/{index-BHOmLZF-.js → index-DVdvmf69.js} +9 -9
- package/dist/studio-app/index.html +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
/// Production submission layer for FeltDB
|
|
2
|
+
///
|
|
3
|
+
/// Integrates the admission contract with actual operation submission,
|
|
4
|
+
/// ensuring no silent failures and explicit rejection semantics.
|
|
5
|
+
|
|
6
|
+
use crate::admission::{ProductionRetryPolicy, RejectionReason, SubmissionOutcome};
|
|
7
|
+
use crate::transaction_api::{TransactionManager, TxError};
|
|
8
|
+
use crate::transactions::{ConsistencyContract, Operation};
|
|
9
|
+
use sha2::{Digest, Sha256};
|
|
10
|
+
use std::sync::{Arc, Mutex};
|
|
11
|
+
use std::time::{SystemTime, UNIX_EPOCH};
|
|
12
|
+
|
|
13
|
+
/// Metadata about a submission's retry history
|
|
14
|
+
#[derive(Clone, Debug)]
|
|
15
|
+
pub struct SubmissionMetrics {
|
|
16
|
+
pub outcome: SubmissionOutcome,
|
|
17
|
+
pub first_attempt_latency_us: u64,
|
|
18
|
+
pub total_latency_us: u64,
|
|
19
|
+
pub retry_attempts: usize,
|
|
20
|
+
pub eventually_accepted: bool,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// Production submission manager
|
|
24
|
+
/// Handles operation admission with explicit contract guarantees
|
|
25
|
+
pub struct SubmissionManager {
|
|
26
|
+
transaction_manager: Arc<Mutex<TransactionManager>>,
|
|
27
|
+
retry_policy: ProductionRetryPolicy,
|
|
28
|
+
deduplication_cache: Arc<Mutex<std::collections::HashMap<String, u64>>>,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
impl SubmissionManager {
|
|
32
|
+
/// Create a new submission manager with standard production retry policy
|
|
33
|
+
pub fn new(transaction_manager: Arc<Mutex<TransactionManager>>) -> Self {
|
|
34
|
+
Self {
|
|
35
|
+
transaction_manager,
|
|
36
|
+
retry_policy: ProductionRetryPolicy::standard(),
|
|
37
|
+
deduplication_cache: Arc::new(Mutex::new(std::collections::HashMap::new())),
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// Create with custom retry policy
|
|
42
|
+
pub fn with_policy(
|
|
43
|
+
transaction_manager: Arc<Mutex<TransactionManager>>,
|
|
44
|
+
retry_policy: ProductionRetryPolicy,
|
|
45
|
+
) -> Self {
|
|
46
|
+
Self {
|
|
47
|
+
transaction_manager,
|
|
48
|
+
retry_policy,
|
|
49
|
+
deduplication_cache: Arc::new(Mutex::new(std::collections::HashMap::new())),
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// Submit a single operation for processing with detailed metrics
|
|
54
|
+
/// Returns SubmissionMetrics including outcome, latencies, and retry count
|
|
55
|
+
pub fn submit_with_metrics(&self, operation: Operation) -> SubmissionMetrics {
|
|
56
|
+
let content_hash = self.hash_operation(&operation);
|
|
57
|
+
let start_time = std::time::Instant::now();
|
|
58
|
+
|
|
59
|
+
// Check deduplication cache
|
|
60
|
+
{
|
|
61
|
+
let cache = self.deduplication_cache.lock().unwrap();
|
|
62
|
+
if let Some(&original_id) = cache.get(&content_hash) {
|
|
63
|
+
let latency = start_time.elapsed().as_micros() as u64;
|
|
64
|
+
return SubmissionMetrics {
|
|
65
|
+
outcome: SubmissionOutcome::Rejected(RejectionReason::DuplicateDetected {
|
|
66
|
+
original_operation_id: original_id,
|
|
67
|
+
}),
|
|
68
|
+
first_attempt_latency_us: latency,
|
|
69
|
+
total_latency_us: latency,
|
|
70
|
+
retry_attempts: 0,
|
|
71
|
+
eventually_accepted: false,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Attempt submission with bounded retries
|
|
77
|
+
let mut first_attempt_latency_us = 0u64;
|
|
78
|
+
let mut retry_attempts = 0usize;
|
|
79
|
+
|
|
80
|
+
for attempt in 0..self.retry_policy.max_attempts {
|
|
81
|
+
let attempt_start = std::time::Instant::now();
|
|
82
|
+
match self.try_submit_once(&operation) {
|
|
83
|
+
Ok(operation_id) => {
|
|
84
|
+
let attempt_latency = attempt_start.elapsed().as_micros() as u64;
|
|
85
|
+
if attempt == 0 {
|
|
86
|
+
first_attempt_latency_us = attempt_latency;
|
|
87
|
+
}
|
|
88
|
+
// Cache this operation
|
|
89
|
+
{
|
|
90
|
+
let mut cache = self.deduplication_cache.lock().unwrap();
|
|
91
|
+
cache.insert(content_hash, operation_id);
|
|
92
|
+
}
|
|
93
|
+
return SubmissionMetrics {
|
|
94
|
+
outcome: SubmissionOutcome::Accepted { operation_id },
|
|
95
|
+
first_attempt_latency_us,
|
|
96
|
+
total_latency_us: start_time.elapsed().as_micros() as u64,
|
|
97
|
+
retry_attempts,
|
|
98
|
+
eventually_accepted: true,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
Err(rejection) => {
|
|
102
|
+
let attempt_latency = attempt_start.elapsed().as_micros() as u64;
|
|
103
|
+
if attempt == 0 {
|
|
104
|
+
first_attempt_latency_us = attempt_latency;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Determine if we should retry or reject
|
|
108
|
+
match &rejection {
|
|
109
|
+
RejectionReason::AdmissionBackpressure {
|
|
110
|
+
retry_after_us: _,
|
|
111
|
+
suggested_backoff_ms: _,
|
|
112
|
+
} => {
|
|
113
|
+
// Apply backoff if not at final attempt
|
|
114
|
+
if attempt < self.retry_policy.max_attempts - 1 {
|
|
115
|
+
let backoff_us = self.retry_policy.backoff_for_attempt(attempt + 1);
|
|
116
|
+
spin_sleep_us(backoff_us);
|
|
117
|
+
retry_attempts += 1;
|
|
118
|
+
continue;
|
|
119
|
+
} else {
|
|
120
|
+
// Final attempt exhausted
|
|
121
|
+
return SubmissionMetrics {
|
|
122
|
+
outcome: SubmissionOutcome::Rejected(rejection),
|
|
123
|
+
first_attempt_latency_us,
|
|
124
|
+
total_latency_us: start_time.elapsed().as_micros() as u64,
|
|
125
|
+
retry_attempts,
|
|
126
|
+
eventually_accepted: false,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
RejectionReason::TransientLockContention => {
|
|
131
|
+
// Immediate retry likely to succeed; retry without delay on first attempt
|
|
132
|
+
if attempt < self.retry_policy.max_attempts - 1 {
|
|
133
|
+
retry_attempts += 1;
|
|
134
|
+
continue;
|
|
135
|
+
} else {
|
|
136
|
+
return SubmissionMetrics {
|
|
137
|
+
outcome: SubmissionOutcome::Rejected(rejection),
|
|
138
|
+
first_attempt_latency_us,
|
|
139
|
+
total_latency_us: start_time.elapsed().as_micros() as u64,
|
|
140
|
+
retry_attempts,
|
|
141
|
+
eventually_accepted: false,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
RejectionReason::QueueFull | RejectionReason::DuplicateDetected { .. } => {
|
|
146
|
+
// No point retrying these
|
|
147
|
+
return SubmissionMetrics {
|
|
148
|
+
outcome: SubmissionOutcome::Rejected(rejection),
|
|
149
|
+
first_attempt_latency_us,
|
|
150
|
+
total_latency_us: start_time.elapsed().as_micros() as u64,
|
|
151
|
+
retry_attempts,
|
|
152
|
+
eventually_accepted: false,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Should not reach here, but if we do, reject due to exhausted attempts
|
|
161
|
+
SubmissionMetrics {
|
|
162
|
+
outcome: SubmissionOutcome::Rejected(RejectionReason::AdmissionBackpressure {
|
|
163
|
+
retry_after_us: 1000,
|
|
164
|
+
suggested_backoff_ms: 10,
|
|
165
|
+
}),
|
|
166
|
+
first_attempt_latency_us,
|
|
167
|
+
total_latency_us: start_time.elapsed().as_micros() as u64,
|
|
168
|
+
retry_attempts,
|
|
169
|
+
eventually_accepted: false,
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/// Submit a single operation for processing
|
|
174
|
+
/// Returns explicit SubmissionOutcome: accepted with operation_id, or rejected with reason
|
|
175
|
+
pub fn submit(&self, operation: Operation) -> SubmissionOutcome {
|
|
176
|
+
let content_hash = self.hash_operation(&operation);
|
|
177
|
+
|
|
178
|
+
// Check deduplication cache
|
|
179
|
+
{
|
|
180
|
+
let cache = self.deduplication_cache.lock().unwrap();
|
|
181
|
+
if let Some(&original_id) = cache.get(&content_hash) {
|
|
182
|
+
return SubmissionOutcome::Rejected(RejectionReason::DuplicateDetected {
|
|
183
|
+
original_operation_id: original_id,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Attempt submission with bounded retries
|
|
189
|
+
for attempt in 0..self.retry_policy.max_attempts {
|
|
190
|
+
match self.try_submit_once(&operation) {
|
|
191
|
+
Ok(operation_id) => {
|
|
192
|
+
// Cache this operation
|
|
193
|
+
{
|
|
194
|
+
let mut cache = self.deduplication_cache.lock().unwrap();
|
|
195
|
+
cache.insert(content_hash, operation_id);
|
|
196
|
+
}
|
|
197
|
+
return SubmissionOutcome::Accepted { operation_id };
|
|
198
|
+
}
|
|
199
|
+
Err(rejection) => {
|
|
200
|
+
// Determine if we should retry or reject
|
|
201
|
+
match &rejection {
|
|
202
|
+
RejectionReason::AdmissionBackpressure {
|
|
203
|
+
retry_after_us: _,
|
|
204
|
+
suggested_backoff_ms: _,
|
|
205
|
+
} => {
|
|
206
|
+
// Apply backoff if not at final attempt
|
|
207
|
+
if attempt < self.retry_policy.max_attempts - 1 {
|
|
208
|
+
let backoff_us = self.retry_policy.backoff_for_attempt(attempt + 1);
|
|
209
|
+
spin_sleep_us(backoff_us);
|
|
210
|
+
continue;
|
|
211
|
+
} else {
|
|
212
|
+
// Final attempt exhausted
|
|
213
|
+
return SubmissionOutcome::Rejected(rejection);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
RejectionReason::TransientLockContention => {
|
|
217
|
+
// Immediate retry likely to succeed; retry without delay on first attempt
|
|
218
|
+
if attempt < self.retry_policy.max_attempts - 1 {
|
|
219
|
+
continue;
|
|
220
|
+
} else {
|
|
221
|
+
return SubmissionOutcome::Rejected(rejection);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
RejectionReason::QueueFull | RejectionReason::DuplicateDetected { .. } => {
|
|
225
|
+
// No point retrying these
|
|
226
|
+
return SubmissionOutcome::Rejected(rejection);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Should not reach here, but if we do, reject due to exhausted attempts
|
|
234
|
+
SubmissionOutcome::Rejected(RejectionReason::AdmissionBackpressure {
|
|
235
|
+
retry_after_us: 1000,
|
|
236
|
+
suggested_backoff_ms: 10,
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/// Try to submit once without retry logic
|
|
241
|
+
/// Used internally; only call once per submission flow
|
|
242
|
+
fn try_submit_once(&self, operation: &Operation) -> Result<u64, RejectionReason> {
|
|
243
|
+
let mut manager = self.transaction_manager.lock().unwrap();
|
|
244
|
+
|
|
245
|
+
// Extract operation ID - this is the unique identifier
|
|
246
|
+
let operation_id = u64::from_str_radix(
|
|
247
|
+
&operation.operation_id.sequence.to_string(),
|
|
248
|
+
10,
|
|
249
|
+
)
|
|
250
|
+
.unwrap_or(0);
|
|
251
|
+
|
|
252
|
+
// Attempt to execute transaction
|
|
253
|
+
match manager.execute_and_commit(
|
|
254
|
+
format!("txn_{}", operation_id),
|
|
255
|
+
vec![operation.clone()],
|
|
256
|
+
ConsistencyContract::local(),
|
|
257
|
+
) {
|
|
258
|
+
Ok(_) => Ok(operation_id),
|
|
259
|
+
Err(err) => {
|
|
260
|
+
// Map TxError to RejectionReason
|
|
261
|
+
match err {
|
|
262
|
+
TxError::OperationDeduplication(_) => {
|
|
263
|
+
Err(RejectionReason::DuplicateDetected {
|
|
264
|
+
original_operation_id: operation_id,
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
TxError::PreconditionFailed(_) | TxError::ConflictDetected(_) => {
|
|
268
|
+
// These could indicate lock contention
|
|
269
|
+
Err(RejectionReason::TransientLockContention)
|
|
270
|
+
}
|
|
271
|
+
TxError::InvalidState(_) => Err(RejectionReason::AdmissionBackpressure {
|
|
272
|
+
retry_after_us: 100,
|
|
273
|
+
suggested_backoff_ms: 1,
|
|
274
|
+
}),
|
|
275
|
+
TxError::PersistenceFailure(_) => Err(RejectionReason::AdmissionBackpressure {
|
|
276
|
+
retry_after_us: 500,
|
|
277
|
+
suggested_backoff_ms: 5,
|
|
278
|
+
}),
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/// Compute content hash of an operation for deduplication
|
|
285
|
+
fn hash_operation(&self, operation: &Operation) -> String {
|
|
286
|
+
let mut hasher = Sha256::new();
|
|
287
|
+
// Hash the operation's complete identifying fields
|
|
288
|
+
hasher.update(operation.operation_id.node_id.as_bytes());
|
|
289
|
+
hasher.update(operation.operation_id.sequence.to_le_bytes());
|
|
290
|
+
hasher.update(operation.command.op_type.as_bytes());
|
|
291
|
+
hasher.update(operation.command.collection.as_bytes());
|
|
292
|
+
hasher.update(operation.command.record_id.as_bytes());
|
|
293
|
+
// Include field values in the hash for complete uniqueness
|
|
294
|
+
for (key, val) in &operation.command.fields {
|
|
295
|
+
hasher.update(key.as_bytes());
|
|
296
|
+
hasher.update(val.to_string().as_bytes());
|
|
297
|
+
}
|
|
298
|
+
let result = hasher.finalize();
|
|
299
|
+
format!("{:x}", result)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/// Spin-based sleep in microseconds (for tight retry loops)
|
|
304
|
+
fn spin_sleep_us(us: u64) {
|
|
305
|
+
if us == 0 {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
let start = std::time::Instant::now();
|
|
309
|
+
let duration = std::time::Duration::from_micros(us);
|
|
310
|
+
while start.elapsed() < duration {
|
|
311
|
+
// Spin
|
|
312
|
+
std::hint::spin_loop();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
#[cfg(test)]
|
|
317
|
+
mod tests {
|
|
318
|
+
use super::*;
|
|
319
|
+
use crate::convergence::VectorClock;
|
|
320
|
+
use crate::transactions::{Operation, OperationCommand, OperationId, StateVersion};
|
|
321
|
+
use std::collections::HashMap;
|
|
322
|
+
|
|
323
|
+
fn create_test_manager() -> Arc<Mutex<TransactionManager>> {
|
|
324
|
+
let mut vc = VectorClock::new();
|
|
325
|
+
vc.increment("node1");
|
|
326
|
+
let version = StateVersion::new(vc, "hash0".to_string());
|
|
327
|
+
Arc::new(Mutex::new(TransactionManager::new(
|
|
328
|
+
"node1".to_string(),
|
|
329
|
+
version,
|
|
330
|
+
)))
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
fn create_test_operation(seq: u64) -> Operation {
|
|
334
|
+
Operation::new(
|
|
335
|
+
OperationId::new("node1".to_string(), seq),
|
|
336
|
+
StateVersion::new(VectorClock::new(), "hash0".to_string()),
|
|
337
|
+
"Update".to_string(),
|
|
338
|
+
OperationCommand {
|
|
339
|
+
op_type: "UpdateField".to_string(),
|
|
340
|
+
collection: "test".to_string(),
|
|
341
|
+
record_id: "rec1".to_string(),
|
|
342
|
+
fields: {
|
|
343
|
+
let mut m = HashMap::new();
|
|
344
|
+
m.insert("field".to_string(), serde_json::json!("value"));
|
|
345
|
+
m
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
"user".to_string(),
|
|
349
|
+
)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
#[test]
|
|
353
|
+
fn submission_contract_accepted() {
|
|
354
|
+
let manager = create_test_manager();
|
|
355
|
+
let submitter = SubmissionManager::new(manager);
|
|
356
|
+
|
|
357
|
+
let op = create_test_operation(1);
|
|
358
|
+
let outcome = submitter.submit(op);
|
|
359
|
+
|
|
360
|
+
assert!(outcome.is_accepted());
|
|
361
|
+
match outcome {
|
|
362
|
+
SubmissionOutcome::Accepted { operation_id } => {
|
|
363
|
+
assert_eq!(operation_id, 1);
|
|
364
|
+
}
|
|
365
|
+
_ => panic!("Expected Accepted outcome"),
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
#[test]
|
|
370
|
+
fn submission_contract_duplicate_detection() {
|
|
371
|
+
let manager = create_test_manager();
|
|
372
|
+
let submitter = SubmissionManager::new(manager);
|
|
373
|
+
|
|
374
|
+
let op = create_test_operation(1);
|
|
375
|
+
let outcome1 = submitter.submit(op.clone());
|
|
376
|
+
assert!(outcome1.is_accepted());
|
|
377
|
+
|
|
378
|
+
// Submit same operation again
|
|
379
|
+
let outcome2 = submitter.submit(op);
|
|
380
|
+
assert!(outcome2.is_rejected());
|
|
381
|
+
|
|
382
|
+
match outcome2 {
|
|
383
|
+
SubmissionOutcome::Rejected(RejectionReason::DuplicateDetected {
|
|
384
|
+
original_operation_id,
|
|
385
|
+
}) => {
|
|
386
|
+
assert_eq!(original_operation_id, 1);
|
|
387
|
+
}
|
|
388
|
+
_ => panic!("Expected DuplicateDetected rejection"),
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
#[test]
|
|
393
|
+
fn submission_contract_never_silently_drops() {
|
|
394
|
+
// This test verifies that submit() always returns an explicit outcome
|
|
395
|
+
let manager = create_test_manager();
|
|
396
|
+
let submitter = SubmissionManager::new(manager);
|
|
397
|
+
|
|
398
|
+
for i in 1..=10 {
|
|
399
|
+
let op = create_test_operation(i);
|
|
400
|
+
let outcome = submitter.submit(op);
|
|
401
|
+
// Every submission gets an explicit outcome
|
|
402
|
+
match outcome {
|
|
403
|
+
SubmissionOutcome::Accepted { .. } => {
|
|
404
|
+
assert!(true, "Operation {} accepted", i);
|
|
405
|
+
}
|
|
406
|
+
SubmissionOutcome::Rejected(_) => {
|
|
407
|
+
assert!(true, "Operation {} explicitly rejected", i);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
#[test]
|
|
414
|
+
fn production_retry_policy_exhaustion() {
|
|
415
|
+
let manager = create_test_manager();
|
|
416
|
+
let retry_policy = ProductionRetryPolicy::standard();
|
|
417
|
+
let submitter = SubmissionManager::with_policy(manager, retry_policy);
|
|
418
|
+
|
|
419
|
+
let op = create_test_operation(1);
|
|
420
|
+
let outcome = submitter.submit(op);
|
|
421
|
+
|
|
422
|
+
// Should succeed normally
|
|
423
|
+
assert!(outcome.is_accepted());
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
#[test]
|
|
427
|
+
fn deduplication_cache_prevents_duplicates() {
|
|
428
|
+
let manager = create_test_manager();
|
|
429
|
+
let submitter = SubmissionManager::new(manager);
|
|
430
|
+
|
|
431
|
+
// Submit three different operations
|
|
432
|
+
for i in 1..=3 {
|
|
433
|
+
let op = create_test_operation(i);
|
|
434
|
+
let outcome = submitter.submit(op);
|
|
435
|
+
assert!(outcome.is_accepted());
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Duplicate should be detected
|
|
439
|
+
let op_dup = create_test_operation(2);
|
|
440
|
+
let outcome = submitter.submit(op_dup);
|
|
441
|
+
assert!(outcome.is_rejected());
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
#[test]
|
|
445
|
+
fn rejection_reason_display() {
|
|
446
|
+
let backpressure = RejectionReason::AdmissionBackpressure {
|
|
447
|
+
retry_after_us: 100,
|
|
448
|
+
suggested_backoff_ms: 1,
|
|
449
|
+
};
|
|
450
|
+
let msg = format!("{}", backpressure);
|
|
451
|
+
assert!(msg.contains("backpressure"));
|
|
452
|
+
assert!(msg.contains("100"));
|
|
453
|
+
|
|
454
|
+
let duplicate = RejectionReason::DuplicateDetected {
|
|
455
|
+
original_operation_id: 42,
|
|
456
|
+
};
|
|
457
|
+
let msg = format!("{}", duplicate);
|
|
458
|
+
assert!(msg.contains("duplicate"));
|
|
459
|
+
assert!(msg.contains("42"));
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
#[test]
|
|
463
|
+
fn acceptance_guarantees_operation_id() {
|
|
464
|
+
let manager = create_test_manager();
|
|
465
|
+
let submitter = SubmissionManager::new(manager);
|
|
466
|
+
|
|
467
|
+
let op = create_test_operation(123);
|
|
468
|
+
let outcome = submitter.submit(op);
|
|
469
|
+
|
|
470
|
+
match outcome {
|
|
471
|
+
SubmissionOutcome::Accepted { operation_id } => {
|
|
472
|
+
assert_eq!(operation_id, 123);
|
|
473
|
+
}
|
|
474
|
+
SubmissionOutcome::Rejected(_) => panic!("Expected acceptance"),
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|