@feltdb/core 0.7.2 → 0.7.4
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/application-development.d.ts +76 -0
- package/dist/application-development.d.ts.map +1 -0
- package/dist/application-development.js +137 -0
- package/dist/application-manifest.d.ts +5 -1
- package/dist/application-manifest.d.ts.map +1 -1
- package/dist/application-manifest.js +10 -3
- package/dist/canonical-application.d.ts +26 -0
- package/dist/canonical-application.d.ts.map +1 -0
- package/dist/canonical-application.js +45 -0
- package/dist/cli/ai-model-config.js +97 -0
- package/dist/cli/commands.js +485 -30
- package/dist/cli/index.js +1 -1
- package/dist/cli/proposal-client.js +54 -0
- package/dist/cli/publish-engine.js +162 -0
- package/dist/cli/source-sync.js +139 -0
- package/dist/create/cli.js +1 -1
- package/dist/create/create.js +30 -6
- package/dist/create/managed-account.js +14 -7
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/crates/feltdb/src/application.rs +31 -1
- package/dist/create/server-source/crates/feltdb/src/authority_failover.rs +847 -0
- package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +338 -29
- package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +72 -34
- package/dist/create/server-source/crates/feltdb/src/lib.rs +1 -0
- package/dist/create/server-source/crates/feltdb-server/src/identity.rs +46 -3
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +315 -16
- package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +20 -0
- 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 +169 -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.map +1 -1
- package/dist/flowspec.js +27 -6
- 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 +3 -0
- package/dist/index-core.d.ts.map +1 -1
- package/dist/index-core.js +3 -0
- package/dist/proposal.d.ts +85 -0
- package/dist/proposal.d.ts.map +1 -0
- package/dist/proposal.js +105 -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 +10 -1
- package/dist/studio/components/ApplicationDesigner.d.ts.map +1 -1
- package/dist/studio/components/AskFeltDB.d.ts +20 -0
- package/dist/studio/components/AskFeltDB.d.ts.map +1 -0
- package/dist/studio/components/ProposalBrowser.d.ts +3 -0
- package/dist/studio/components/ProposalBrowser.d.ts.map +1 -0
- package/dist/studio/components/index.d.ts +2 -0
- package/dist/studio/components/index.d.ts.map +1 -1
- package/dist/studio/components/index.js +4 -4
- package/dist/studio/{components-C5p2TfIU.js → components-Bm4sjhcX.js} +531 -192
- package/dist/studio/index.js +184 -172
- package/dist/studio/studio.css +1 -1
- package/dist/studio-app/assets/dist-DA1MYSzp.js +1 -0
- package/dist/studio-app/assets/{feltdb_wasm-DVKsw75S.js → feltdb_wasm-B-Ccgccb.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-wH3enJIA.wasm +0 -0
- package/dist/studio-app/assets/{index-C1GWyazR.css → index-BRrmvaKw.css} +1 -1
- package/dist/studio-app/assets/index-CEIqowCh.js +42 -0
- 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 +62 -1
- package/package.json +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-DNyNf0yy.wasm +0 -0
- package/dist/studio-app/assets/index-C71X92EK.js +0 -28
|
@@ -45,14 +45,17 @@
|
|
|
45
45
|
//! shutdown exit cleanly
|
|
46
46
|
//! ```
|
|
47
47
|
|
|
48
|
+
use feltdb::authority_failover::{AuthorityStore, ClaimResult, FencingToken, Heartbeat};
|
|
48
49
|
use feltdb::convergence::VectorClock;
|
|
49
50
|
use feltdb::distributed_transactions::{
|
|
50
51
|
CausalCapacity, DistributedTransactionExecutor, ReceiveOutcome, ReplicationMessage,
|
|
51
52
|
SubmitOutcome,
|
|
52
53
|
};
|
|
53
|
-
use feltdb::
|
|
54
|
-
|
|
54
|
+
use feltdb::replica_acknowledgements::{
|
|
55
|
+
AcknowledgementStore, ReclamationEvidence, ReplicaAcknowledgement,
|
|
56
|
+
};
|
|
55
57
|
use feltdb::replica_membership::MembershipStore;
|
|
58
|
+
use feltdb::replication_protocol::ProtocolTransport;
|
|
56
59
|
use feltdb::state_hash::StateHash;
|
|
57
60
|
use feltdb::tcp_transport::TcpTransport;
|
|
58
61
|
use feltdb::transactions::{
|
|
@@ -126,7 +129,11 @@ fn deliver(
|
|
|
126
129
|
Ok(ReceiveOutcome::AlreadyKnown) => {
|
|
127
130
|
json!({ "envelope": key, "outcome": "already_known" })
|
|
128
131
|
}
|
|
129
|
-
Ok(ReceiveOutcome::CapacityDeferred {
|
|
132
|
+
Ok(ReceiveOutcome::CapacityDeferred {
|
|
133
|
+
missing,
|
|
134
|
+
pending_entries,
|
|
135
|
+
pending_bytes,
|
|
136
|
+
}) => {
|
|
130
137
|
// Durable, but not buffered. The caller's obligation is to stop
|
|
131
138
|
// reading from this peer; the envelope is admitted from the log
|
|
132
139
|
// once the backlog drains.
|
|
@@ -176,16 +183,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
176
183
|
let mut recv_specs: Vec<(String, String)> = Vec::new();
|
|
177
184
|
let mut max_pending_entries: Option<usize> = None;
|
|
178
185
|
let mut max_pending_bytes: Option<usize> = None;
|
|
186
|
+
let mut authority_priority: u32 = 0;
|
|
187
|
+
let mut heartbeat_interval_ms: u64 = 1_000;
|
|
188
|
+
let mut heartbeat_failure_threshold: u32 = 5;
|
|
179
189
|
|
|
180
190
|
let mut index = 1;
|
|
181
191
|
while index < args.len() {
|
|
182
192
|
match args[index].as_str() {
|
|
183
193
|
"--node-id" => {
|
|
184
194
|
index += 1;
|
|
185
|
-
node_id = args
|
|
186
|
-
.get(index)
|
|
187
|
-
.ok_or("--node-id requires a value")?
|
|
188
|
-
.clone();
|
|
195
|
+
node_id = args.get(index).ok_or("--node-id requires a value")?.clone();
|
|
189
196
|
}
|
|
190
197
|
"--data" => {
|
|
191
198
|
index += 1;
|
|
@@ -195,11 +202,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
195
202
|
}
|
|
196
203
|
"--send" => {
|
|
197
204
|
index += 1;
|
|
198
|
-
send_specs.push(parse_link(
|
|
205
|
+
send_specs.push(parse_link(
|
|
206
|
+
args.get(index).ok_or("--send requires PEER=ADDR")?,
|
|
207
|
+
)?);
|
|
199
208
|
}
|
|
200
209
|
"--recv" => {
|
|
201
210
|
index += 1;
|
|
202
|
-
recv_specs.push(parse_link(
|
|
211
|
+
recv_specs.push(parse_link(
|
|
212
|
+
args.get(index).ok_or("--recv requires PEER=ADDR")?,
|
|
213
|
+
)?);
|
|
203
214
|
}
|
|
204
215
|
// The bound is settable so an acceptance test can reach it through
|
|
205
216
|
// the production path. Filling the shipping default would mean
|
|
@@ -225,6 +236,32 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
225
236
|
.map_err(|e| format!("--max-pending-bytes: {e}"))?,
|
|
226
237
|
);
|
|
227
238
|
}
|
|
239
|
+
// Static priority for authority election. Higher priority nodes are
|
|
240
|
+
// preferred during failover when multiple candidates have equal state.
|
|
241
|
+
"--authority-priority" => {
|
|
242
|
+
index += 1;
|
|
243
|
+
authority_priority = args
|
|
244
|
+
.get(index)
|
|
245
|
+
.ok_or("--authority-priority requires a value")?
|
|
246
|
+
.parse::<u32>()
|
|
247
|
+
.map_err(|e| format!("--authority-priority: {e}"))?;
|
|
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
|
+
}
|
|
228
265
|
other => return Err(format!("unknown argument {other}").into()),
|
|
229
266
|
}
|
|
230
267
|
index += 1;
|
|
@@ -265,9 +302,31 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
265
302
|
.map_err(|e| e.to_string())?,
|
|
266
303
|
));
|
|
267
304
|
|
|
305
|
+
// Authority state tracks who holds write authority in the cluster. The store
|
|
306
|
+
// is durable and survives restarts, so a node that crashes can rejoin and
|
|
307
|
+
// recover its previous role. The cluster ID is set by the first `authority-claim`
|
|
308
|
+
// command, so it starts as the node's own ID.
|
|
309
|
+
let authority = Arc::new(Mutex::new(
|
|
310
|
+
AuthorityStore::open_with_failure_detector(
|
|
311
|
+
data_dir
|
|
312
|
+
.as_ref()
|
|
313
|
+
.map(|dir| dir.join("authority.json"))
|
|
314
|
+
.unwrap_or_else(|| PathBuf::from("authority.json")),
|
|
315
|
+
&node_id, // cluster_id defaults to node_id until set
|
|
316
|
+
&node_id,
|
|
317
|
+
authority_priority,
|
|
318
|
+
heartbeat_interval_ms,
|
|
319
|
+
heartbeat_failure_threshold,
|
|
320
|
+
)
|
|
321
|
+
.map_err(|e| e.to_string())?,
|
|
322
|
+
));
|
|
323
|
+
|
|
268
324
|
let initial_state = StateHash::from_hex("0".repeat(64));
|
|
269
|
-
let mut executor =
|
|
270
|
-
|
|
325
|
+
let mut executor = DistributedTransactionExecutor::with_log(
|
|
326
|
+
node_id.clone(),
|
|
327
|
+
initial_state.clone(),
|
|
328
|
+
log_path.clone(),
|
|
329
|
+
)?;
|
|
271
330
|
|
|
272
331
|
// The bound is set before recovery, not after, because recovery restores
|
|
273
332
|
// the backlog and has to respect the same limit the live path does.
|
|
@@ -309,7 +368,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
309
368
|
// missed an operation observably divergent. It lives beside the executor
|
|
310
369
|
// under one lock so a local commit and an arriving replication cannot
|
|
311
370
|
// interleave and chain onto a parent that is already stale.
|
|
312
|
-
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)));
|
|
313
376
|
let should_exit = Arc::new(AtomicBool::new(false));
|
|
314
377
|
// Per-peer delivery hold.
|
|
315
378
|
//
|
|
@@ -338,7 +401,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
338
401
|
.into_iter()
|
|
339
402
|
.map(|(peer, addr)| {
|
|
340
403
|
let transport = TcpTransport::new("127.0.0.1:0".to_string());
|
|
341
|
-
SendLink {
|
|
404
|
+
SendLink {
|
|
405
|
+
peer,
|
|
406
|
+
addr,
|
|
407
|
+
control: Arc::new(Mutex::new(transport.clone())),
|
|
408
|
+
io: transport,
|
|
409
|
+
}
|
|
342
410
|
})
|
|
343
411
|
.collect();
|
|
344
412
|
|
|
@@ -346,7 +414,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
346
414
|
.into_iter()
|
|
347
415
|
.map(|(peer, addr)| {
|
|
348
416
|
let transport = TcpTransport::new(addr.clone());
|
|
349
|
-
RecvLink {
|
|
417
|
+
RecvLink {
|
|
418
|
+
peer,
|
|
419
|
+
addr,
|
|
420
|
+
control: Arc::new(Mutex::new(transport.clone())),
|
|
421
|
+
io: transport,
|
|
422
|
+
}
|
|
350
423
|
})
|
|
351
424
|
.collect();
|
|
352
425
|
|
|
@@ -480,8 +553,35 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
480
553
|
};
|
|
481
554
|
|
|
482
555
|
match verb {
|
|
483
|
-
"execute-tx" => {
|
|
484
|
-
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
|
+
};
|
|
485
585
|
|
|
486
586
|
let mut fields = HashMap::new();
|
|
487
587
|
fields.insert("record".to_string(), json!(record.clone()));
|
|
@@ -825,7 +925,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
825
925
|
let (frontier, marks) = {
|
|
826
926
|
let guard = core.lock().await;
|
|
827
927
|
let (executor, _) = &*guard;
|
|
828
|
-
(
|
|
928
|
+
(
|
|
929
|
+
executor.causal_frontier().clone(),
|
|
930
|
+
executor.origin_sequence_high_water(),
|
|
931
|
+
)
|
|
829
932
|
};
|
|
830
933
|
let view: Vec<String> = membership
|
|
831
934
|
.get_members()
|
|
@@ -1054,27 +1157,30 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
1054
1157
|
"list-operations" => {
|
|
1055
1158
|
let guard = core.lock().await;
|
|
1056
1159
|
let (executor, _) = &*guard;
|
|
1057
|
-
|
|
1160
|
+
|
|
1058
1161
|
// Get all operations from the log
|
|
1059
1162
|
let mut all_ops = Vec::new();
|
|
1060
1163
|
let mut applied_ops = Vec::new();
|
|
1061
1164
|
let pending_ops = executor.pending_causal_keys();
|
|
1062
1165
|
let deferred_ops = executor.deferred_causal_keys();
|
|
1063
|
-
|
|
1166
|
+
|
|
1064
1167
|
if let Some(ref log) = executor.operation_log {
|
|
1065
1168
|
if let Ok(envelopes) = log.load_all() {
|
|
1066
1169
|
for env in &envelopes {
|
|
1067
|
-
let key = format!(
|
|
1170
|
+
let key = format!(
|
|
1171
|
+
"{}:{}",
|
|
1172
|
+
env.envelope_id.originating_node, env.envelope_id.sequence
|
|
1173
|
+
);
|
|
1068
1174
|
all_ops.push(key.clone());
|
|
1069
1175
|
}
|
|
1070
1176
|
}
|
|
1071
1177
|
}
|
|
1072
|
-
|
|
1178
|
+
|
|
1073
1179
|
// Get applied operations from the barrier
|
|
1074
1180
|
for key in executor.causal_barrier.applied_keys() {
|
|
1075
1181
|
applied_ops.push(key.to_string());
|
|
1076
1182
|
}
|
|
1077
|
-
|
|
1183
|
+
|
|
1078
1184
|
println!(
|
|
1079
1185
|
"OPERATIONS {}",
|
|
1080
1186
|
json!({
|
|
@@ -1091,23 +1197,26 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
1091
1197
|
"operation-inventory" => {
|
|
1092
1198
|
let guard = core.lock().await;
|
|
1093
1199
|
let (executor, _) = &*guard;
|
|
1094
|
-
|
|
1200
|
+
|
|
1095
1201
|
// Get all operations from the log
|
|
1096
1202
|
let mut operations = json!({
|
|
1097
1203
|
"applied": [],
|
|
1098
1204
|
"pending": [],
|
|
1099
1205
|
"deferred": [],
|
|
1100
1206
|
});
|
|
1101
|
-
|
|
1207
|
+
|
|
1102
1208
|
if let Some(ref log) = executor.operation_log {
|
|
1103
1209
|
if let Ok(envelopes) = log.load_all() {
|
|
1104
1210
|
let applied_keys = executor.causal_barrier.applied_keys();
|
|
1105
1211
|
let pending_keys = executor.pending_causal_keys();
|
|
1106
1212
|
let deferred_keys = executor.deferred_causal_keys();
|
|
1107
|
-
|
|
1213
|
+
|
|
1108
1214
|
for env in &envelopes {
|
|
1109
|
-
let key = format!(
|
|
1110
|
-
|
|
1215
|
+
let key = format!(
|
|
1216
|
+
"{}:{}",
|
|
1217
|
+
env.envelope_id.originating_node, env.envelope_id.sequence
|
|
1218
|
+
);
|
|
1219
|
+
|
|
1111
1220
|
if applied_keys.contains(&key) {
|
|
1112
1221
|
operations["applied"].as_array_mut().unwrap().push(json!({
|
|
1113
1222
|
"id": key,
|
|
@@ -1130,7 +1239,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
1130
1239
|
}
|
|
1131
1240
|
}
|
|
1132
1241
|
}
|
|
1133
|
-
|
|
1242
|
+
|
|
1134
1243
|
println!("INVENTORY {}", operations);
|
|
1135
1244
|
io::stdout().flush().ok();
|
|
1136
1245
|
}
|
|
@@ -1147,6 +1256,203 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
1147
1256
|
io::stdout().flush().ok();
|
|
1148
1257
|
}
|
|
1149
1258
|
|
|
1259
|
+
// Authority failover commands.
|
|
1260
|
+
//
|
|
1261
|
+
// These commands implement the distributed authority protocol that prevents
|
|
1262
|
+
// split-brain and allows automatic failover when the authority node fails.
|
|
1263
|
+
|
|
1264
|
+
// Claim authority for this node. This generates a new fencing token that
|
|
1265
|
+
// supersedes any previous authority. The claim succeeds only if no other
|
|
1266
|
+
// node has a higher token and appears healthy.
|
|
1267
|
+
"authority-claim" => {
|
|
1268
|
+
let mut guard = authority.lock().await;
|
|
1269
|
+
let result = guard.claim_authority();
|
|
1270
|
+
match result {
|
|
1271
|
+
Ok(ClaimResult::Granted { token }) => {
|
|
1272
|
+
println!(
|
|
1273
|
+
"AUTHORITY_CLAIMED {}",
|
|
1274
|
+
json!({
|
|
1275
|
+
"node": node_id,
|
|
1276
|
+
"fencing_token": token.0,
|
|
1277
|
+
"role": "AUTHORITY",
|
|
1278
|
+
})
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
Ok(ClaimResult::Rejected {
|
|
1282
|
+
current_authority,
|
|
1283
|
+
current_token,
|
|
1284
|
+
}) => {
|
|
1285
|
+
println!(
|
|
1286
|
+
"AUTHORITY_REJECTED {}",
|
|
1287
|
+
json!({
|
|
1288
|
+
"current_authority": current_authority,
|
|
1289
|
+
"current_token": current_token.0,
|
|
1290
|
+
"reason": "another authority is still active",
|
|
1291
|
+
})
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
1294
|
+
Ok(ClaimResult::Fenced { token }) => {
|
|
1295
|
+
println!(
|
|
1296
|
+
"AUTHORITY_FENCED {}",
|
|
1297
|
+
json!({
|
|
1298
|
+
"node": node_id,
|
|
1299
|
+
"fencing_token": token.0,
|
|
1300
|
+
"reason": "this node is fenced",
|
|
1301
|
+
})
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
Err(e) => {
|
|
1305
|
+
println!("ERROR {}", json!({ "reason": e.to_string() }));
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
io::stdout().flush().ok();
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// Accept a new authority announced by another node.
|
|
1312
|
+
// Format: authority-accept NODE_ID TOKEN
|
|
1313
|
+
"authority-accept" => {
|
|
1314
|
+
let mut parts = argument.split_whitespace();
|
|
1315
|
+
let authority_node = parts.next().unwrap_or_default().to_string();
|
|
1316
|
+
let token_value: u64 = parts.next().unwrap_or("0").parse().unwrap_or(0);
|
|
1317
|
+
let token = FencingToken(token_value);
|
|
1318
|
+
|
|
1319
|
+
let mut guard = authority.lock().await;
|
|
1320
|
+
let result = guard.accept_authority(&authority_node, token);
|
|
1321
|
+
match result {
|
|
1322
|
+
Ok(()) => {
|
|
1323
|
+
let state = guard.state();
|
|
1324
|
+
println!(
|
|
1325
|
+
"AUTHORITY_ACCEPTED {}",
|
|
1326
|
+
json!({
|
|
1327
|
+
"authority_node": authority_node,
|
|
1328
|
+
"fencing_token": token.0,
|
|
1329
|
+
"local_role": format!("{:?}", state.local_role),
|
|
1330
|
+
})
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
Err(e) => {
|
|
1334
|
+
println!("ERROR {}", json!({ "reason": e.to_string() }));
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
io::stdout().flush().ok();
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// Query the current authority state.
|
|
1341
|
+
"authority-state" => {
|
|
1342
|
+
let guard = authority.lock().await;
|
|
1343
|
+
let state = guard.state();
|
|
1344
|
+
let operations_applied = {
|
|
1345
|
+
let core_guard = core.lock().await;
|
|
1346
|
+
let (executor, _) = &*core_guard;
|
|
1347
|
+
executor
|
|
1348
|
+
.get_replica_state(&node_id)
|
|
1349
|
+
.map(|r| r.operations_applied)
|
|
1350
|
+
.unwrap_or(0)
|
|
1351
|
+
};
|
|
1352
|
+
println!(
|
|
1353
|
+
"AUTHORITY {}",
|
|
1354
|
+
json!({
|
|
1355
|
+
"cluster_id": state.cluster_id,
|
|
1356
|
+
"local_node": state.local_node_id,
|
|
1357
|
+
"local_role": format!("{:?}", state.local_role),
|
|
1358
|
+
"is_authority": state.is_authority(),
|
|
1359
|
+
"is_fenced": state.is_fenced(),
|
|
1360
|
+
"authority_node": state.authority_node(),
|
|
1361
|
+
"current_token": state.current_token().0,
|
|
1362
|
+
"highest_seen_token": state.highest_seen_token.0,
|
|
1363
|
+
"operations_applied": operations_applied,
|
|
1364
|
+
})
|
|
1365
|
+
);
|
|
1366
|
+
io::stdout().flush().ok();
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// Record a heartbeat from a peer. This is used for failure detection.
|
|
1370
|
+
// Format: heartbeat NODE_ID [TOKEN] [OPS_APPLIED] [IS_AUTHORITY]
|
|
1371
|
+
"heartbeat" => {
|
|
1372
|
+
let mut parts = argument.split_whitespace();
|
|
1373
|
+
let from_node = parts.next().unwrap_or_default().to_string();
|
|
1374
|
+
|
|
1375
|
+
if from_node.is_empty() {
|
|
1376
|
+
println!(
|
|
1377
|
+
"ERROR {}",
|
|
1378
|
+
json!({ "reason": "heartbeat requires NODE_ID" })
|
|
1379
|
+
);
|
|
1380
|
+
} else {
|
|
1381
|
+
let mut guard = authority.lock().await;
|
|
1382
|
+
guard.heartbeat(&from_node);
|
|
1383
|
+
println!(
|
|
1384
|
+
"HEARTBEAT_RECORDED {}",
|
|
1385
|
+
json!({
|
|
1386
|
+
"from_node": from_node,
|
|
1387
|
+
})
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
io::stdout().flush().ok();
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// Check if the current authority has failed based on heartbeat timeout.
|
|
1394
|
+
"authority-failed" => {
|
|
1395
|
+
let guard = authority.lock().await;
|
|
1396
|
+
let failed = guard.authority_failed();
|
|
1397
|
+
let failed_nodes = guard.failed_nodes();
|
|
1398
|
+
let state = guard.state();
|
|
1399
|
+
println!(
|
|
1400
|
+
"AUTHORITY_CHECK {}",
|
|
1401
|
+
json!({
|
|
1402
|
+
"authority_failed": failed,
|
|
1403
|
+
"current_authority": state.authority_node(),
|
|
1404
|
+
"failed_nodes": failed_nodes,
|
|
1405
|
+
})
|
|
1406
|
+
);
|
|
1407
|
+
io::stdout().flush().ok();
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
// Step down from authority role voluntarily.
|
|
1411
|
+
"authority-stepdown" => {
|
|
1412
|
+
let mut guard = authority.lock().await;
|
|
1413
|
+
let was_authority = guard.state().is_authority();
|
|
1414
|
+
let result = guard.step_down();
|
|
1415
|
+
match result {
|
|
1416
|
+
Ok(()) => {
|
|
1417
|
+
println!(
|
|
1418
|
+
"AUTHORITY_STEPDOWN {}",
|
|
1419
|
+
json!({
|
|
1420
|
+
"node": node_id,
|
|
1421
|
+
"was_authority": was_authority,
|
|
1422
|
+
"now_role": format!("{:?}", guard.state().local_role),
|
|
1423
|
+
})
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1426
|
+
Err(e) => {
|
|
1427
|
+
println!("ERROR {}", json!({ "reason": e.to_string() }));
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
io::stdout().flush().ok();
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
// Recover from fenced state after catching up.
|
|
1434
|
+
"authority-recover" => {
|
|
1435
|
+
let mut guard = authority.lock().await;
|
|
1436
|
+
let was_fenced = guard.state().is_fenced();
|
|
1437
|
+
let result = guard.recover_from_fenced();
|
|
1438
|
+
match result {
|
|
1439
|
+
Ok(()) => {
|
|
1440
|
+
println!(
|
|
1441
|
+
"AUTHORITY_RECOVERED {}",
|
|
1442
|
+
json!({
|
|
1443
|
+
"node": node_id,
|
|
1444
|
+
"was_fenced": was_fenced,
|
|
1445
|
+
"now_role": format!("{:?}", guard.state().local_role),
|
|
1446
|
+
})
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1449
|
+
Err(e) => {
|
|
1450
|
+
println!("ERROR {}", json!({ "reason": e.to_string() }));
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
io::stdout().flush().ok();
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1150
1456
|
"shutdown" => {
|
|
1151
1457
|
should_exit.store(true, Ordering::Relaxed);
|
|
1152
1458
|
println!("SHUTDOWN");
|
|
@@ -1156,7 +1462,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
1156
1462
|
}
|
|
1157
1463
|
|
|
1158
1464
|
_ => {
|
|
1159
|
-
println!(
|
|
1465
|
+
println!(
|
|
1466
|
+
"ERROR {}",
|
|
1467
|
+
json!({ "reason": format!("unknown command {verb}") })
|
|
1468
|
+
);
|
|
1160
1469
|
io::stdout().flush().ok();
|
|
1161
1470
|
}
|
|
1162
1471
|
}
|