@feltdb/core 0.7.3 → 0.8.0
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/README.md +25 -0
- package/dist/application-development.d.ts +78 -0
- package/dist/application-development.d.ts.map +1 -0
- package/dist/application-development.js +153 -0
- package/dist/application-manifest.d.ts +6 -1
- package/dist/application-manifest.d.ts.map +1 -1
- package/dist/application-manifest.js +10 -3
- package/dist/canonical-application.d.ts +27 -0
- package/dist/canonical-application.d.ts.map +1 -0
- package/dist/canonical-application.js +46 -0
- package/dist/cli/ai-model-config.js +97 -0
- package/dist/cli/cli.js +1 -1
- package/dist/cli/commands.js +664 -120
- package/dist/cli/index.js +1 -1
- package/dist/cli/managed-environment.js +6 -0
- package/dist/cli/managed-project.js +6 -0
- package/dist/cli/proposal-client.js +63 -0
- package/dist/cli/publish-engine.js +165 -0
- package/dist/cli/source-sync.js +183 -0
- package/dist/create/cli.js +11 -2
- package/dist/create/create.js +38 -16
- package/dist/create/managed-account.js +82 -11
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/crates/feltdb/src/application.rs +41 -1
- package/dist/create/server-source/crates/feltdb/src/authority_failover.rs +84 -6
- package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +126 -38
- package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +72 -34
- package/dist/create/server-source/crates/feltdb/src/lib.rs +20 -2
- package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +12 -24
- package/dist/create/server-source/crates/feltdb-server/src/application_contract.rs +11 -3
- package/dist/create/server-source/crates/feltdb-server/src/authenticated_principal.rs +0 -1
- package/dist/create/server-source/crates/feltdb-server/src/certification_harness.rs +23 -22
- package/dist/create/server-source/crates/feltdb-server/src/delegation_token.rs +5 -15
- package/dist/create/server-source/crates/feltdb-server/src/durable_operations.rs +13 -28
- package/dist/create/server-source/crates/feltdb-server/src/identity.rs +111 -3
- package/dist/create/server-source/crates/feltdb-server/src/key_management.rs +28 -7
- package/dist/create/server-source/crates/feltdb-server/src/lib.rs +5 -5
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +1960 -143
- package/dist/create/server-source/crates/feltdb-server/src/managed_diagnostics.rs +10 -30
- package/dist/create/server-source/crates/feltdb-server/src/membership_policy.rs +12 -4
- package/dist/create/server-source/crates/feltdb-server/src/request_telemetry.rs +8 -6
- package/dist/create/server-source/crates/feltdb-server/src/snapshot_cursor.rs +10 -14
- package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +195 -13
- package/dist/create/server-source/crates/feltdb-server/src/tenant_policies.rs +9 -21
- package/dist/create/server-source/crates/feltdb-server/src/transaction_idempotency.rs +5 -3
- package/dist/create/server-source/crates/feltdb-server/src/transaction_recovery.rs +9 -8
- package/dist/create/server-source/crates/feltdb-server/tests/revision_recovery_integration_test.rs +1 -4
- package/dist/create/template/default-project/README.md +57 -0
- package/dist/create/template/default-project/agents/activity-assistant.ts +18 -0
- package/dist/create/template/default-project/agents/project-assistant.ts +19 -0
- package/dist/create/template/default-project/capabilities/activity-summary.ts +14 -0
- package/dist/create/template/default-project/capabilities/project-search.ts +16 -0
- package/dist/create/template/default-project/capabilities/project-summary.ts +15 -0
- package/dist/create/template/default-project/feltdb.flow +174 -0
- package/dist/create/template/default-project/src/App.tsx +20 -0
- package/dist/create/template/default-project/src/context/AuthContext.tsx +27 -0
- package/dist/create/template/default-project/src/feltdb.ts +87 -0
- package/dist/create/template/default-project/src/index.tsx +14 -0
- package/dist/create/template/default-project/src/pages/Activity.tsx +7 -0
- package/dist/create/template/default-project/src/pages/Agents.tsx +15 -0
- package/dist/create/template/default-project/src/pages/Dashboard.tsx +17 -0
- package/dist/create/template/default-project/src/pages/Invitations.tsx +11 -0
- package/dist/create/template/default-project/src/pages/Projects.tsx +11 -0
- package/dist/create/template/default-project/src/pages/SignIn.tsx +8 -0
- package/dist/create/template/default-project/src/pages/SignUp.tsx +8 -0
- package/dist/create/template/default-project/src/styles-application.css +21 -0
- package/dist/create/template/default-project/src/styles.css +1 -0
- package/dist/create/template/default-project/workflows/agent-assisted-summary.ts +1 -0
- package/dist/create/template/default-project/workflows/invitation.ts +30 -0
- package/dist/create/template/default-project/workflows/project-created.ts +2 -0
- package/dist/db.d.ts +49 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +77 -0
- package/dist/error-codes.d.ts +20 -1
- package/dist/error-codes.d.ts.map +1 -1
- package/dist/error-codes.js +25 -0
- package/dist/flowspec.d.ts +1 -0
- package/dist/flowspec.d.ts.map +1 -1
- package/dist/flowspec.js +66 -11
- package/dist/http-client.d.ts +135 -14
- package/dist/http-client.d.ts.map +1 -1
- package/dist/http-client.js +79 -31
- package/dist/http-db.d.ts +15 -2
- package/dist/http-db.d.ts.map +1 -1
- package/dist/http-db.js +113 -5
- package/dist/index-core.d.ts +5 -0
- package/dist/index-core.d.ts.map +1 -1
- package/dist/index-core.js +5 -0
- package/dist/module-intent.d.ts +37 -0
- package/dist/module-intent.d.ts.map +1 -0
- package/dist/module-intent.js +81 -0
- package/dist/module.d.ts +76 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +61 -0
- package/dist/proposal.d.ts +161 -0
- package/dist/proposal.d.ts.map +1 -0
- package/dist/proposal.js +232 -0
- package/dist/studio/app.d.ts +5 -1
- package/dist/studio/app.d.ts.map +1 -1
- package/dist/studio/components/ApplicationDesigner.d.ts +2 -1
- package/dist/studio/components/ApplicationDesigner.d.ts.map +1 -1
- package/dist/studio/components/AskFeltDB.d.ts +21 -0
- package/dist/studio/components/AskFeltDB.d.ts.map +1 -0
- package/dist/studio/components/ContractInspector.d.ts +7 -0
- package/dist/studio/components/ContractInspector.d.ts.map +1 -0
- package/dist/studio/components/ManagedInstancePanel.d.ts.map +1 -1
- package/dist/studio/components/ManagedInstancePanel.js +1 -5
- package/dist/studio/components/ModuleExplorer.d.ts +6 -0
- package/dist/studio/components/ModuleExplorer.d.ts.map +1 -0
- package/dist/studio/components/ProposalBrowser.d.ts +12 -0
- package/dist/studio/components/ProposalBrowser.d.ts.map +1 -0
- package/dist/studio/components/StateExplorer.d.ts +6 -7
- package/dist/studio/components/StateExplorer.d.ts.map +1 -1
- package/dist/studio/components/index.d.ts +4 -0
- package/dist/studio/components/index.d.ts.map +1 -1
- package/dist/studio/components/index.js +4 -4
- package/dist/studio/components-CNSNrmA9.js +2787 -0
- package/dist/studio/index.js +214 -157
- package/dist/studio/studio.css +1 -1
- package/dist/studio/utils/index.d.ts +2 -0
- package/dist/studio/utils/index.d.ts.map +1 -1
- package/dist/studio/utils/index.js +14 -12
- package/dist/studio/utils/managed-instance.d.ts +0 -4
- package/dist/studio/utils/managed-instance.d.ts.map +1 -1
- package/dist/studio/utils/managed-instance.js +0 -4
- package/dist/studio/utils/proposal-api.d.ts +53 -0
- package/dist/studio/utils/proposal-api.d.ts.map +1 -0
- package/dist/studio/utils/proposal-api.js +91 -0
- package/dist/studio/utils/service-api.d.ts +81 -0
- package/dist/studio/utils/service-api.d.ts.map +1 -0
- package/dist/studio/utils/service-api.js +51 -0
- package/dist/studio-app/assets/dist-CCOk39Uc.js +1 -0
- package/dist/studio-app/assets/{feltdb_wasm-DVKsw75S.js → feltdb_wasm-CSB6KVgw.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-Dd1fnO9U.wasm +0 -0
- package/dist/studio-app/assets/index-Bncji3aN.js +43 -0
- package/dist/studio-app/assets/{index-C1GWyazR.css → index-yazaWMUN.css} +1 -1
- package/dist/studio-app/assets/lib-B2_7fcn7.js +69 -0
- package/dist/studio-app/assets/worker-CARZ5g7K.js +69 -0
- package/dist/studio-app/index.html +2 -2
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/workspace/local-development-authority.d.ts +3 -0
- package/dist/workspace/local-development-authority.d.ts.map +1 -1
- package/dist/workspace/local-development-authority.js +124 -2
- package/package.json +1 -1
- package/dist/studio/components-Dxuhrv8_.js +0 -1832
- package/dist/studio-app/assets/feltdb_wasm_bg-DNyNf0yy.wasm +0 -0
- package/dist/studio-app/assets/index-B5tnmvSD.js +0 -28
|
@@ -51,9 +51,11 @@ use feltdb::distributed_transactions::{
|
|
|
51
51
|
CausalCapacity, DistributedTransactionExecutor, ReceiveOutcome, ReplicationMessage,
|
|
52
52
|
SubmitOutcome,
|
|
53
53
|
};
|
|
54
|
-
use feltdb::
|
|
55
|
-
|
|
54
|
+
use feltdb::replica_acknowledgements::{
|
|
55
|
+
AcknowledgementStore, ReclamationEvidence, ReplicaAcknowledgement,
|
|
56
|
+
};
|
|
56
57
|
use feltdb::replica_membership::MembershipStore;
|
|
58
|
+
use feltdb::replication_protocol::ProtocolTransport;
|
|
57
59
|
use feltdb::state_hash::StateHash;
|
|
58
60
|
use feltdb::tcp_transport::TcpTransport;
|
|
59
61
|
use feltdb::transactions::{
|
|
@@ -127,7 +129,11 @@ fn deliver(
|
|
|
127
129
|
Ok(ReceiveOutcome::AlreadyKnown) => {
|
|
128
130
|
json!({ "envelope": key, "outcome": "already_known" })
|
|
129
131
|
}
|
|
130
|
-
Ok(ReceiveOutcome::CapacityDeferred {
|
|
132
|
+
Ok(ReceiveOutcome::CapacityDeferred {
|
|
133
|
+
missing,
|
|
134
|
+
pending_entries,
|
|
135
|
+
pending_bytes,
|
|
136
|
+
}) => {
|
|
131
137
|
// Durable, but not buffered. The caller's obligation is to stop
|
|
132
138
|
// reading from this peer; the envelope is admitted from the log
|
|
133
139
|
// once the backlog drains.
|
|
@@ -178,16 +184,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
178
184
|
let mut max_pending_entries: Option<usize> = None;
|
|
179
185
|
let mut max_pending_bytes: Option<usize> = None;
|
|
180
186
|
let mut authority_priority: u32 = 0;
|
|
187
|
+
let mut heartbeat_interval_ms: u64 = 1_000;
|
|
188
|
+
let mut heartbeat_failure_threshold: u32 = 5;
|
|
181
189
|
|
|
182
190
|
let mut index = 1;
|
|
183
191
|
while index < args.len() {
|
|
184
192
|
match args[index].as_str() {
|
|
185
193
|
"--node-id" => {
|
|
186
194
|
index += 1;
|
|
187
|
-
node_id = args
|
|
188
|
-
.get(index)
|
|
189
|
-
.ok_or("--node-id requires a value")?
|
|
190
|
-
.clone();
|
|
195
|
+
node_id = args.get(index).ok_or("--node-id requires a value")?.clone();
|
|
191
196
|
}
|
|
192
197
|
"--data" => {
|
|
193
198
|
index += 1;
|
|
@@ -197,11 +202,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
197
202
|
}
|
|
198
203
|
"--send" => {
|
|
199
204
|
index += 1;
|
|
200
|
-
send_specs.push(parse_link(
|
|
205
|
+
send_specs.push(parse_link(
|
|
206
|
+
args.get(index).ok_or("--send requires PEER=ADDR")?,
|
|
207
|
+
)?);
|
|
201
208
|
}
|
|
202
209
|
"--recv" => {
|
|
203
210
|
index += 1;
|
|
204
|
-
recv_specs.push(parse_link(
|
|
211
|
+
recv_specs.push(parse_link(
|
|
212
|
+
args.get(index).ok_or("--recv requires PEER=ADDR")?,
|
|
213
|
+
)?);
|
|
205
214
|
}
|
|
206
215
|
// The bound is settable so an acceptance test can reach it through
|
|
207
216
|
// the production path. Filling the shipping default would mean
|
|
@@ -237,6 +246,22 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
237
246
|
.parse::<u32>()
|
|
238
247
|
.map_err(|e| format!("--authority-priority: {e}"))?;
|
|
239
248
|
}
|
|
249
|
+
"--heartbeat-interval-ms" => {
|
|
250
|
+
index += 1;
|
|
251
|
+
heartbeat_interval_ms = args
|
|
252
|
+
.get(index)
|
|
253
|
+
.ok_or("--heartbeat-interval-ms requires a value")?
|
|
254
|
+
.parse::<u64>()
|
|
255
|
+
.map_err(|e| format!("--heartbeat-interval-ms: {e}"))?;
|
|
256
|
+
}
|
|
257
|
+
"--heartbeat-failure-threshold" => {
|
|
258
|
+
index += 1;
|
|
259
|
+
heartbeat_failure_threshold = args
|
|
260
|
+
.get(index)
|
|
261
|
+
.ok_or("--heartbeat-failure-threshold requires a value")?
|
|
262
|
+
.parse::<u32>()
|
|
263
|
+
.map_err(|e| format!("--heartbeat-failure-threshold: {e}"))?;
|
|
264
|
+
}
|
|
240
265
|
other => return Err(format!("unknown argument {other}").into()),
|
|
241
266
|
}
|
|
242
267
|
index += 1;
|
|
@@ -282,7 +307,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
282
307
|
// recover its previous role. The cluster ID is set by the first `authority-claim`
|
|
283
308
|
// command, so it starts as the node's own ID.
|
|
284
309
|
let authority = Arc::new(Mutex::new(
|
|
285
|
-
AuthorityStore::
|
|
310
|
+
AuthorityStore::open_with_failure_detector(
|
|
286
311
|
data_dir
|
|
287
312
|
.as_ref()
|
|
288
313
|
.map(|dir| dir.join("authority.json"))
|
|
@@ -290,13 +315,18 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
290
315
|
&node_id, // cluster_id defaults to node_id until set
|
|
291
316
|
&node_id,
|
|
292
317
|
authority_priority,
|
|
318
|
+
heartbeat_interval_ms,
|
|
319
|
+
heartbeat_failure_threshold,
|
|
293
320
|
)
|
|
294
321
|
.map_err(|e| e.to_string())?,
|
|
295
322
|
));
|
|
296
323
|
|
|
297
324
|
let initial_state = StateHash::from_hex("0".repeat(64));
|
|
298
|
-
let mut executor =
|
|
299
|
-
|
|
325
|
+
let mut executor = DistributedTransactionExecutor::with_log(
|
|
326
|
+
node_id.clone(),
|
|
327
|
+
initial_state.clone(),
|
|
328
|
+
log_path.clone(),
|
|
329
|
+
)?;
|
|
300
330
|
|
|
301
331
|
// The bound is set before recovery, not after, because recovery restores
|
|
302
332
|
// the backlog and has to respect the same limit the live path does.
|
|
@@ -338,7 +368,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
338
368
|
// missed an operation observably divergent. It lives beside the executor
|
|
339
369
|
// under one lock so a local commit and an arriving replication cannot
|
|
340
370
|
// interleave and chain onto a parent that is already stale.
|
|
341
|
-
let
|
|
371
|
+
let recovered_version = executor
|
|
372
|
+
.get_replica_state(&node_id)
|
|
373
|
+
.map(|replica| StateVersion::new(replica.vector_clock, replica.state_hash.to_string()))
|
|
374
|
+
.unwrap_or_else(|| StateVersion::new(VectorClock::new(), "0".repeat(64)));
|
|
375
|
+
let core = Arc::new(Mutex::new((executor, recovered_version)));
|
|
342
376
|
let should_exit = Arc::new(AtomicBool::new(false));
|
|
343
377
|
// Per-peer delivery hold.
|
|
344
378
|
//
|
|
@@ -367,7 +401,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
367
401
|
.into_iter()
|
|
368
402
|
.map(|(peer, addr)| {
|
|
369
403
|
let transport = TcpTransport::new("127.0.0.1:0".to_string());
|
|
370
|
-
SendLink {
|
|
404
|
+
SendLink {
|
|
405
|
+
peer,
|
|
406
|
+
addr,
|
|
407
|
+
control: Arc::new(Mutex::new(transport.clone())),
|
|
408
|
+
io: transport,
|
|
409
|
+
}
|
|
371
410
|
})
|
|
372
411
|
.collect();
|
|
373
412
|
|
|
@@ -375,7 +414,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
375
414
|
.into_iter()
|
|
376
415
|
.map(|(peer, addr)| {
|
|
377
416
|
let transport = TcpTransport::new(addr.clone());
|
|
378
|
-
RecvLink {
|
|
417
|
+
RecvLink {
|
|
418
|
+
peer,
|
|
419
|
+
addr,
|
|
420
|
+
control: Arc::new(Mutex::new(transport.clone())),
|
|
421
|
+
io: transport,
|
|
422
|
+
}
|
|
379
423
|
})
|
|
380
424
|
.collect();
|
|
381
425
|
|
|
@@ -509,8 +553,35 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
509
553
|
};
|
|
510
554
|
|
|
511
555
|
match verb {
|
|
512
|
-
"execute-tx" => {
|
|
513
|
-
let record = if
|
|
556
|
+
"execute-tx" | "authority-write" => {
|
|
557
|
+
let record = if verb == "authority-write" {
|
|
558
|
+
let mut parts = argument.splitn(2, ' ');
|
|
559
|
+
let token = parts.next().and_then(|value| value.parse::<u64>().ok());
|
|
560
|
+
let record = parts.next().unwrap_or("item").to_string();
|
|
561
|
+
let validation = match token {
|
|
562
|
+
Some(token) => authority
|
|
563
|
+
.lock()
|
|
564
|
+
.await
|
|
565
|
+
.validate_quorum_write(FencingToken(token), 2),
|
|
566
|
+
None => Err(feltdb::authority_failover::AuthorityError::StaleToken {
|
|
567
|
+
provided: FencingToken(0),
|
|
568
|
+
current: authority.lock().await.state().current_token(),
|
|
569
|
+
}),
|
|
570
|
+
};
|
|
571
|
+
if let Err(error) = validation {
|
|
572
|
+
println!(
|
|
573
|
+
"AUTHORITY_WRITE_REJECTED {}",
|
|
574
|
+
json!({ "record": record, "reason": error.to_string() })
|
|
575
|
+
);
|
|
576
|
+
io::stdout().flush().ok();
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
record
|
|
580
|
+
} else if argument.is_empty() {
|
|
581
|
+
"item".to_string()
|
|
582
|
+
} else {
|
|
583
|
+
argument
|
|
584
|
+
};
|
|
514
585
|
|
|
515
586
|
let mut fields = HashMap::new();
|
|
516
587
|
fields.insert("record".to_string(), json!(record.clone()));
|
|
@@ -854,7 +925,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
854
925
|
let (frontier, marks) = {
|
|
855
926
|
let guard = core.lock().await;
|
|
856
927
|
let (executor, _) = &*guard;
|
|
857
|
-
(
|
|
928
|
+
(
|
|
929
|
+
executor.causal_frontier().clone(),
|
|
930
|
+
executor.origin_sequence_high_water(),
|
|
931
|
+
)
|
|
858
932
|
};
|
|
859
933
|
let view: Vec<String> = membership
|
|
860
934
|
.get_members()
|
|
@@ -1083,27 +1157,30 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
1083
1157
|
"list-operations" => {
|
|
1084
1158
|
let guard = core.lock().await;
|
|
1085
1159
|
let (executor, _) = &*guard;
|
|
1086
|
-
|
|
1160
|
+
|
|
1087
1161
|
// Get all operations from the log
|
|
1088
1162
|
let mut all_ops = Vec::new();
|
|
1089
1163
|
let mut applied_ops = Vec::new();
|
|
1090
1164
|
let pending_ops = executor.pending_causal_keys();
|
|
1091
1165
|
let deferred_ops = executor.deferred_causal_keys();
|
|
1092
|
-
|
|
1166
|
+
|
|
1093
1167
|
if let Some(ref log) = executor.operation_log {
|
|
1094
1168
|
if let Ok(envelopes) = log.load_all() {
|
|
1095
1169
|
for env in &envelopes {
|
|
1096
|
-
let key = format!(
|
|
1170
|
+
let key = format!(
|
|
1171
|
+
"{}:{}",
|
|
1172
|
+
env.envelope_id.originating_node, env.envelope_id.sequence
|
|
1173
|
+
);
|
|
1097
1174
|
all_ops.push(key.clone());
|
|
1098
1175
|
}
|
|
1099
1176
|
}
|
|
1100
1177
|
}
|
|
1101
|
-
|
|
1178
|
+
|
|
1102
1179
|
// Get applied operations from the barrier
|
|
1103
1180
|
for key in executor.causal_barrier.applied_keys() {
|
|
1104
1181
|
applied_ops.push(key.to_string());
|
|
1105
1182
|
}
|
|
1106
|
-
|
|
1183
|
+
|
|
1107
1184
|
println!(
|
|
1108
1185
|
"OPERATIONS {}",
|
|
1109
1186
|
json!({
|
|
@@ -1120,23 +1197,26 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
1120
1197
|
"operation-inventory" => {
|
|
1121
1198
|
let guard = core.lock().await;
|
|
1122
1199
|
let (executor, _) = &*guard;
|
|
1123
|
-
|
|
1200
|
+
|
|
1124
1201
|
// Get all operations from the log
|
|
1125
1202
|
let mut operations = json!({
|
|
1126
1203
|
"applied": [],
|
|
1127
1204
|
"pending": [],
|
|
1128
1205
|
"deferred": [],
|
|
1129
1206
|
});
|
|
1130
|
-
|
|
1207
|
+
|
|
1131
1208
|
if let Some(ref log) = executor.operation_log {
|
|
1132
1209
|
if let Ok(envelopes) = log.load_all() {
|
|
1133
1210
|
let applied_keys = executor.causal_barrier.applied_keys();
|
|
1134
1211
|
let pending_keys = executor.pending_causal_keys();
|
|
1135
1212
|
let deferred_keys = executor.deferred_causal_keys();
|
|
1136
|
-
|
|
1213
|
+
|
|
1137
1214
|
for env in &envelopes {
|
|
1138
|
-
let key = format!(
|
|
1139
|
-
|
|
1215
|
+
let key = format!(
|
|
1216
|
+
"{}:{}",
|
|
1217
|
+
env.envelope_id.originating_node, env.envelope_id.sequence
|
|
1218
|
+
);
|
|
1219
|
+
|
|
1140
1220
|
if applied_keys.contains(&key) {
|
|
1141
1221
|
operations["applied"].as_array_mut().unwrap().push(json!({
|
|
1142
1222
|
"id": key,
|
|
@@ -1159,7 +1239,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
1159
1239
|
}
|
|
1160
1240
|
}
|
|
1161
1241
|
}
|
|
1162
|
-
|
|
1242
|
+
|
|
1163
1243
|
println!("INVENTORY {}", operations);
|
|
1164
1244
|
io::stdout().flush().ok();
|
|
1165
1245
|
}
|
|
@@ -1198,7 +1278,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
1198
1278
|
})
|
|
1199
1279
|
);
|
|
1200
1280
|
}
|
|
1201
|
-
Ok(ClaimResult::Rejected {
|
|
1281
|
+
Ok(ClaimResult::Rejected {
|
|
1282
|
+
current_authority,
|
|
1283
|
+
current_token,
|
|
1284
|
+
}) => {
|
|
1202
1285
|
println!(
|
|
1203
1286
|
"AUTHORITY_REJECTED {}",
|
|
1204
1287
|
json!({
|
|
@@ -1230,11 +1313,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
1230
1313
|
"authority-accept" => {
|
|
1231
1314
|
let mut parts = argument.split_whitespace();
|
|
1232
1315
|
let authority_node = parts.next().unwrap_or_default().to_string();
|
|
1233
|
-
let token_value: u64 = parts
|
|
1234
|
-
.next()
|
|
1235
|
-
.unwrap_or("0")
|
|
1236
|
-
.parse()
|
|
1237
|
-
.unwrap_or(0);
|
|
1316
|
+
let token_value: u64 = parts.next().unwrap_or("0").parse().unwrap_or(0);
|
|
1238
1317
|
let token = FencingToken(token_value);
|
|
1239
1318
|
|
|
1240
1319
|
let mut guard = authority.lock().await;
|
|
@@ -1265,7 +1344,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
1265
1344
|
let operations_applied = {
|
|
1266
1345
|
let core_guard = core.lock().await;
|
|
1267
1346
|
let (executor, _) = &*core_guard;
|
|
1268
|
-
executor
|
|
1347
|
+
executor
|
|
1348
|
+
.get_replica_state(&node_id)
|
|
1349
|
+
.map(|r| r.operations_applied)
|
|
1350
|
+
.unwrap_or(0)
|
|
1269
1351
|
};
|
|
1270
1352
|
println!(
|
|
1271
1353
|
"AUTHORITY {}",
|
|
@@ -1291,7 +1373,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
1291
1373
|
let from_node = parts.next().unwrap_or_default().to_string();
|
|
1292
1374
|
|
|
1293
1375
|
if from_node.is_empty() {
|
|
1294
|
-
println!(
|
|
1376
|
+
println!(
|
|
1377
|
+
"ERROR {}",
|
|
1378
|
+
json!({ "reason": "heartbeat requires NODE_ID" })
|
|
1379
|
+
);
|
|
1295
1380
|
} else {
|
|
1296
1381
|
let mut guard = authority.lock().await;
|
|
1297
1382
|
guard.heartbeat(&from_node);
|
|
@@ -1377,7 +1462,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
1377
1462
|
}
|
|
1378
1463
|
|
|
1379
1464
|
_ => {
|
|
1380
|
-
println!(
|
|
1465
|
+
println!(
|
|
1466
|
+
"ERROR {}",
|
|
1467
|
+
json!({ "reason": format!("unknown command {verb}") })
|
|
1468
|
+
);
|
|
1381
1469
|
io::stdout().flush().ok();
|
|
1382
1470
|
}
|
|
1383
1471
|
}
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
/// - DistributedExecutor: Orchestrates consistency semantics
|
|
8
8
|
///
|
|
9
9
|
/// Invariant: All replicas converge to identical state_hash for same operations
|
|
10
|
-
|
|
11
10
|
use crate::causal_dependency_barrier::CausalDependencyBarrier;
|
|
12
11
|
use crate::convergence::VectorClock;
|
|
13
12
|
use crate::durable_operation_log::OperationLog;
|
|
@@ -136,11 +135,7 @@ impl ReplicaState {
|
|
|
136
135
|
}
|
|
137
136
|
|
|
138
137
|
/// Update replica state after transaction
|
|
139
|
-
pub fn apply_transaction(
|
|
140
|
-
&mut self,
|
|
141
|
-
envelope: &TransactionEnvelope,
|
|
142
|
-
new_state_hash: StateHash,
|
|
143
|
-
) {
|
|
138
|
+
pub fn apply_transaction(&mut self, envelope: &TransactionEnvelope, new_state_hash: StateHash) {
|
|
144
139
|
self.vector_clock.merge(&envelope.vector_clock);
|
|
145
140
|
self.vector_clock.increment(&self.node_id);
|
|
146
141
|
self.state_hash = new_state_hash;
|
|
@@ -276,7 +271,10 @@ impl CausalCapacity {
|
|
|
276
271
|
|
|
277
272
|
/// A bound small enough to be reached by a test through the real path.
|
|
278
273
|
pub fn new(max_pending_entries: usize, max_pending_bytes: usize) -> Self {
|
|
279
|
-
Self {
|
|
274
|
+
Self {
|
|
275
|
+
max_pending_entries,
|
|
276
|
+
max_pending_bytes,
|
|
277
|
+
}
|
|
280
278
|
}
|
|
281
279
|
}
|
|
282
280
|
|
|
@@ -393,8 +391,12 @@ impl DistributedTransactionExecutor {
|
|
|
393
391
|
/// source `next_origin_sequence` is recovered from.
|
|
394
392
|
pub fn origin_sequence_high_water(&self) -> std::collections::BTreeMap<String, u64> {
|
|
395
393
|
let mut marks = std::collections::BTreeMap::new();
|
|
396
|
-
let Some(ref log) = self.operation_log else {
|
|
397
|
-
|
|
394
|
+
let Some(ref log) = self.operation_log else {
|
|
395
|
+
return marks;
|
|
396
|
+
};
|
|
397
|
+
let Ok(envelopes) = log.load_all() else {
|
|
398
|
+
return marks;
|
|
399
|
+
};
|
|
398
400
|
for envelope in envelopes {
|
|
399
401
|
let entry = marks
|
|
400
402
|
.entry(envelope.envelope_id.originating_node.clone())
|
|
@@ -516,7 +518,9 @@ impl DistributedTransactionExecutor {
|
|
|
516
518
|
|
|
517
519
|
/// The size local work would contribute, measured before it is executed.
|
|
518
520
|
fn operations_bytes(operations: &[Operation]) -> usize {
|
|
519
|
-
serde_json::to_string(operations)
|
|
521
|
+
serde_json::to_string(operations)
|
|
522
|
+
.map(|json| json.len())
|
|
523
|
+
.unwrap_or(0)
|
|
520
524
|
}
|
|
521
525
|
|
|
522
526
|
/// Execute local work unconditionally.
|
|
@@ -582,8 +586,10 @@ impl DistributedTransactionExecutor {
|
|
|
582
586
|
// Step 6: A locally executed operation is applied, so it advances the
|
|
583
587
|
// frontier exactly as a replicated one does. Own writes are always
|
|
584
588
|
// causally eligible: they are built on the frontier they advance.
|
|
585
|
-
self.causal_barrier
|
|
586
|
-
|
|
589
|
+
self.causal_barrier.record_applied(
|
|
590
|
+
&Self::barrier_key(&envelope.envelope_id),
|
|
591
|
+
&envelope.vector_clock,
|
|
592
|
+
);
|
|
587
593
|
|
|
588
594
|
Ok(envelope)
|
|
589
595
|
}
|
|
@@ -712,7 +718,9 @@ impl DistributedTransactionExecutor {
|
|
|
712
718
|
/// per-operation constant, so a large envelope consumes its actual byte
|
|
713
719
|
/// count.
|
|
714
720
|
fn envelope_bytes(envelope: &TransactionEnvelope) -> usize {
|
|
715
|
-
serde_json::to_string(envelope)
|
|
721
|
+
serde_json::to_string(envelope)
|
|
722
|
+
.map(|json| json.len())
|
|
723
|
+
.unwrap_or(0)
|
|
716
724
|
}
|
|
717
725
|
|
|
718
726
|
/// Apply one envelope and advance the applied frontier.
|
|
@@ -741,8 +749,10 @@ impl DistributedTransactionExecutor {
|
|
|
741
749
|
|
|
742
750
|
// The frontier advances here and only here, so it describes what has
|
|
743
751
|
// been successfully applied rather than what was claimed or received.
|
|
744
|
-
self.causal_barrier
|
|
745
|
-
|
|
752
|
+
self.causal_barrier.record_applied(
|
|
753
|
+
&Self::barrier_key(&envelope.envelope_id),
|
|
754
|
+
&envelope.vector_clock,
|
|
755
|
+
);
|
|
746
756
|
|
|
747
757
|
Ok(state_hash)
|
|
748
758
|
}
|
|
@@ -816,16 +826,14 @@ impl DistributedTransactionExecutor {
|
|
|
816
826
|
return Ok(0);
|
|
817
827
|
}
|
|
818
828
|
let by_key = self.log_by_key()?;
|
|
819
|
-
let mut keys: Vec<String> = self
|
|
820
|
-
.deferred
|
|
821
|
-
.iter()
|
|
822
|
-
.map(Self::barrier_key)
|
|
823
|
-
.collect();
|
|
829
|
+
let mut keys: Vec<String> = self.deferred.iter().map(Self::barrier_key).collect();
|
|
824
830
|
keys.sort();
|
|
825
831
|
|
|
826
832
|
let mut admitted = 0;
|
|
827
833
|
for key in keys {
|
|
828
|
-
let Some(envelope) = by_key.get(&key).cloned() else {
|
|
834
|
+
let Some(envelope) = by_key.get(&key).cloned() else {
|
|
835
|
+
continue;
|
|
836
|
+
};
|
|
829
837
|
let dependencies = Self::dependencies_of(&envelope);
|
|
830
838
|
let payload_bytes = Self::envelope_bytes(&envelope);
|
|
831
839
|
|
|
@@ -888,9 +896,7 @@ impl DistributedTransactionExecutor {
|
|
|
888
896
|
.clocks
|
|
889
897
|
.iter()
|
|
890
898
|
.filter(|(node, required)| frontier.get(node) < **required)
|
|
891
|
-
.map(|(node, required)| {
|
|
892
|
-
format!("{}@{} (have {})", node, required, frontier.get(node))
|
|
893
|
-
})
|
|
899
|
+
.map(|(node, required)| format!("{}@{} (have {})", node, required, frontier.get(node)))
|
|
894
900
|
.collect();
|
|
895
901
|
missing.sort();
|
|
896
902
|
missing
|
|
@@ -1021,11 +1027,7 @@ impl DistributedTransactionExecutor {
|
|
|
1021
1027
|
return true;
|
|
1022
1028
|
}
|
|
1023
1029
|
|
|
1024
|
-
let first_hash = self
|
|
1025
|
-
.replicas
|
|
1026
|
-
.values()
|
|
1027
|
-
.next()
|
|
1028
|
-
.map(|r| r.state_hash.clone());
|
|
1030
|
+
let first_hash = self.replicas.values().next().map(|r| r.state_hash.clone());
|
|
1029
1031
|
|
|
1030
1032
|
if let Some(first) = first_hash {
|
|
1031
1033
|
self.replicas.values().all(|r| r.state_hash == first)
|
|
@@ -1122,6 +1124,27 @@ impl DistributedTransactionExecutor {
|
|
|
1122
1124
|
);
|
|
1123
1125
|
}
|
|
1124
1126
|
|
|
1127
|
+
// Restore the observable local replica state from the same applied
|
|
1128
|
+
// subset. Recovery previously rebuilt the barrier and canonical data
|
|
1129
|
+
// but left this counter and clock at their constructor values, making
|
|
1130
|
+
// a fully recovered node report that it had applied zero operations.
|
|
1131
|
+
let applied_keys = self.causal_barrier.applied_keys();
|
|
1132
|
+
let operations_applied = envelopes
|
|
1133
|
+
.iter()
|
|
1134
|
+
.filter(|envelope| applied_keys.contains(&Self::barrier_key(&envelope.envelope_id)))
|
|
1135
|
+
.map(|envelope| envelope.operations.len() as u64)
|
|
1136
|
+
.sum();
|
|
1137
|
+
let recovered_clock = self.causal_barrier.get_frontier().clone();
|
|
1138
|
+
let recovered_hash = self
|
|
1139
|
+
.canonical_state()
|
|
1140
|
+
.map(|state| state.hash())
|
|
1141
|
+
.unwrap_or_else(|_| StateHash::from_hex("0".repeat(64)));
|
|
1142
|
+
if let Some(local) = self.replicas.get_mut(&self.local_node_id) {
|
|
1143
|
+
local.vector_clock = recovered_clock;
|
|
1144
|
+
local.operations_applied = operations_applied;
|
|
1145
|
+
local.state_hash = recovered_hash;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1125
1148
|
// Identity is recovered from the same log, and this is the half that
|
|
1126
1149
|
// was missing: the causal frontier was already rebuilt here, so a
|
|
1127
1150
|
// restarted origin came back knowing its causal position and not its
|
|
@@ -1201,7 +1224,10 @@ mod tests {
|
|
|
1201
1224
|
|
|
1202
1225
|
#[test]
|
|
1203
1226
|
fn test_replica_state_creation() {
|
|
1204
|
-
let state = ReplicaState::new(
|
|
1227
|
+
let state = ReplicaState::new(
|
|
1228
|
+
"node1".to_string(),
|
|
1229
|
+
StateHash::from_hex("hash0".to_string()),
|
|
1230
|
+
);
|
|
1205
1231
|
|
|
1206
1232
|
assert_eq!(state.node_id, "node1");
|
|
1207
1233
|
assert_eq!(state.operations_applied, 0);
|
|
@@ -1224,7 +1250,10 @@ mod tests {
|
|
|
1224
1250
|
StateHash::from_hex("hash0".to_string()),
|
|
1225
1251
|
);
|
|
1226
1252
|
|
|
1227
|
-
executor.register_replica(
|
|
1253
|
+
executor.register_replica(
|
|
1254
|
+
"node2".to_string(),
|
|
1255
|
+
StateHash::from_hex("hash0".to_string()),
|
|
1256
|
+
);
|
|
1228
1257
|
|
|
1229
1258
|
assert!(executor.replicas.contains_key("node2"));
|
|
1230
1259
|
}
|
|
@@ -1247,8 +1276,14 @@ mod tests {
|
|
|
1247
1276
|
StateHash::from_hex("hash0".to_string()),
|
|
1248
1277
|
);
|
|
1249
1278
|
|
|
1250
|
-
executor.register_replica(
|
|
1251
|
-
|
|
1279
|
+
executor.register_replica(
|
|
1280
|
+
"node2".to_string(),
|
|
1281
|
+
StateHash::from_hex("hash0".to_string()),
|
|
1282
|
+
);
|
|
1283
|
+
executor.register_replica(
|
|
1284
|
+
"node3".to_string(),
|
|
1285
|
+
StateHash::from_hex("hash0".to_string()),
|
|
1286
|
+
);
|
|
1252
1287
|
|
|
1253
1288
|
assert!(executor.check_convergence());
|
|
1254
1289
|
}
|
|
@@ -1260,7 +1295,10 @@ mod tests {
|
|
|
1260
1295
|
StateHash::from_hex("hash0".to_string()),
|
|
1261
1296
|
);
|
|
1262
1297
|
|
|
1263
|
-
executor.register_replica(
|
|
1298
|
+
executor.register_replica(
|
|
1299
|
+
"node2".to_string(),
|
|
1300
|
+
StateHash::from_hex("hash1".to_string()),
|
|
1301
|
+
);
|
|
1264
1302
|
|
|
1265
1303
|
assert!(!executor.check_convergence());
|
|
1266
1304
|
}
|
|
@@ -246,7 +246,8 @@ use serde_json::Value;
|
|
|
246
246
|
use sha2::{Digest, Sha256};
|
|
247
247
|
use std::any::type_name;
|
|
248
248
|
use std::collections::hash_map::DefaultHasher;
|
|
249
|
-
use std::collections::{HashMap, HashSet};
|
|
249
|
+
use std::collections::{BTreeMap, HashMap, HashSet};
|
|
250
|
+
use std::ops::Bound::{Excluded, Unbounded};
|
|
250
251
|
use std::fmt::{Display, Formatter};
|
|
251
252
|
use std::fs::{self, OpenOptions};
|
|
252
253
|
use std::hash::{Hash, Hasher};
|
|
@@ -315,7 +316,7 @@ pub struct FeltDb {
|
|
|
315
316
|
#[allow(dead_code)] // Reserved runtime subsystems are initialized before their public APIs land.
|
|
316
317
|
struct Inner {
|
|
317
318
|
path: PathBuf,
|
|
318
|
-
rows: HashMap<String,
|
|
319
|
+
rows: HashMap<String, BTreeMap<String, StoredRow>>,
|
|
319
320
|
key_counters: HashMap<String, u64>,
|
|
320
321
|
query_count_by_type: HashMap<String, usize>,
|
|
321
322
|
adaptive_indexes: HashSet<String>,
|
|
@@ -1721,6 +1722,23 @@ impl FeltDb {
|
|
|
1721
1722
|
.unwrap_or_default())
|
|
1722
1723
|
}
|
|
1723
1724
|
|
|
1725
|
+
/// Traverse one collection in immutable record-key order without materializing it.
|
|
1726
|
+
pub fn list_collection_page(&self, capability: &str, after: Option<&str>, limit: usize) -> Result<Vec<StoredRow>> {
|
|
1727
|
+
let inner = self.inner.lock().expect("lock poisoned");
|
|
1728
|
+
let Some(rows) = inner.rows.get(capability) else { return Ok(vec![]) };
|
|
1729
|
+
let values: Box<dyn Iterator<Item = &StoredRow>> = match after {
|
|
1730
|
+
Some(key) => Box::new(rows.range::<str, _>((Excluded(key), Unbounded)).map(|(_, row)| row)),
|
|
1731
|
+
None => Box::new(rows.values()),
|
|
1732
|
+
};
|
|
1733
|
+
Ok(values.filter(|row| !row.deleted).take(limit).cloned().collect())
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
/// Fetch one live collection record without materializing collection state.
|
|
1737
|
+
pub fn get_collection_record(&self, capability: &str, key: &str) -> Result<Option<StoredRow>> {
|
|
1738
|
+
let inner = self.inner.lock().expect("lock poisoned");
|
|
1739
|
+
Ok(inner.rows.get(capability).and_then(|rows| rows.get(key)).filter(|row| !row.deleted).cloned())
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1724
1742
|
/// Subscribe to canonical mutation events.
|
|
1725
1743
|
pub fn subscribe_changes(&self) -> broadcast::Receiver<ChangeEvent> {
|
|
1726
1744
|
self.event_tx.subscribe()
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
use serde_json::Value;
|
|
1
2
|
use std::path::PathBuf;
|
|
2
3
|
use std::{
|
|
3
4
|
collections::HashMap,
|
|
4
5
|
sync::{atomic::AtomicU64, Arc},
|
|
5
6
|
time::Instant,
|
|
6
7
|
};
|
|
7
|
-
use serde_json::Value;
|
|
8
8
|
|
|
9
9
|
#[derive(Clone)]
|
|
10
10
|
pub struct BoundedQueryCursor {
|
|
@@ -16,34 +16,22 @@ pub struct BoundedQueryCursor {
|
|
|
16
16
|
pub created_at: u64,
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
use crate::{
|
|
20
|
+
application_contract::ApplicationContractStore, artifacts::ArtifactStore, audit::AuditLog,
|
|
21
|
+
auth::KeyStore, causal::CausalStore, clock::LeaseClock, cluster::ClusterStore,
|
|
22
|
+
connections::ConnectionStore, content::ContentStore, identity::IdentityStore,
|
|
23
|
+
leases::LeaseStore, metrics::Metrics, portable_bundle::BundleStore, principals::PrincipalStore,
|
|
24
|
+
providers::ProviderStore, releases::ReleaseStore, sessions::SessionVerifier,
|
|
25
|
+
tenancy::TenancyStore,
|
|
26
|
+
};
|
|
19
27
|
use feltdb::{
|
|
20
|
-
FeltDb,
|
|
21
28
|
application::ApplicationStore,
|
|
29
|
+
application_runtime::RuntimeStore,
|
|
22
30
|
authorization::{GrantSigner, GrantStore},
|
|
23
31
|
sync_contract::SyncStore,
|
|
24
|
-
workload::WorkloadStore,
|
|
25
32
|
worker_mesh::WorkerMeshStore,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
use crate::{
|
|
29
|
-
artifacts::ArtifactStore,
|
|
30
|
-
audit::AuditLog,
|
|
31
|
-
auth::KeyStore,
|
|
32
|
-
causal::CausalStore,
|
|
33
|
-
cluster::ClusterStore,
|
|
34
|
-
connections::ConnectionStore,
|
|
35
|
-
content::ContentStore,
|
|
36
|
-
identity::IdentityStore,
|
|
37
|
-
leases::LeaseStore,
|
|
38
|
-
metrics::Metrics,
|
|
39
|
-
portable_bundle::BundleStore,
|
|
40
|
-
principals::PrincipalStore,
|
|
41
|
-
providers::ProviderStore,
|
|
42
|
-
releases::ReleaseStore,
|
|
43
|
-
sessions::SessionVerifier,
|
|
44
|
-
tenancy::TenancyStore,
|
|
45
|
-
application_contract::ApplicationContractStore,
|
|
46
|
-
clock::LeaseClock,
|
|
33
|
+
workload::WorkloadStore,
|
|
34
|
+
FeltDb,
|
|
47
35
|
};
|
|
48
36
|
|
|
49
37
|
#[derive(Clone)]
|