@feltdb/core 0.7.2 → 0.7.3

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.
@@ -45,6 +45,7 @@
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,
@@ -176,6 +177,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
176
177
  let mut recv_specs: Vec<(String, String)> = Vec::new();
177
178
  let mut max_pending_entries: Option<usize> = None;
178
179
  let mut max_pending_bytes: Option<usize> = None;
180
+ let mut authority_priority: u32 = 0;
179
181
 
180
182
  let mut index = 1;
181
183
  while index < args.len() {
@@ -225,6 +227,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
225
227
  .map_err(|e| format!("--max-pending-bytes: {e}"))?,
226
228
  );
227
229
  }
230
+ // Static priority for authority election. Higher priority nodes are
231
+ // preferred during failover when multiple candidates have equal state.
232
+ "--authority-priority" => {
233
+ index += 1;
234
+ authority_priority = args
235
+ .get(index)
236
+ .ok_or("--authority-priority requires a value")?
237
+ .parse::<u32>()
238
+ .map_err(|e| format!("--authority-priority: {e}"))?;
239
+ }
228
240
  other => return Err(format!("unknown argument {other}").into()),
229
241
  }
230
242
  index += 1;
@@ -265,6 +277,23 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
265
277
  .map_err(|e| e.to_string())?,
266
278
  ));
267
279
 
280
+ // Authority state tracks who holds write authority in the cluster. The store
281
+ // is durable and survives restarts, so a node that crashes can rejoin and
282
+ // recover its previous role. The cluster ID is set by the first `authority-claim`
283
+ // command, so it starts as the node's own ID.
284
+ let authority = Arc::new(Mutex::new(
285
+ AuthorityStore::open(
286
+ data_dir
287
+ .as_ref()
288
+ .map(|dir| dir.join("authority.json"))
289
+ .unwrap_or_else(|| PathBuf::from("authority.json")),
290
+ &node_id, // cluster_id defaults to node_id until set
291
+ &node_id,
292
+ authority_priority,
293
+ )
294
+ .map_err(|e| e.to_string())?,
295
+ ));
296
+
268
297
  let initial_state = StateHash::from_hex("0".repeat(64));
269
298
  let mut executor =
270
299
  DistributedTransactionExecutor::with_log(node_id.clone(), initial_state.clone(), log_path.clone())?;
@@ -1147,6 +1176,198 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
1147
1176
  io::stdout().flush().ok();
1148
1177
  }
1149
1178
 
1179
+ // Authority failover commands.
1180
+ //
1181
+ // These commands implement the distributed authority protocol that prevents
1182
+ // split-brain and allows automatic failover when the authority node fails.
1183
+
1184
+ // Claim authority for this node. This generates a new fencing token that
1185
+ // supersedes any previous authority. The claim succeeds only if no other
1186
+ // node has a higher token and appears healthy.
1187
+ "authority-claim" => {
1188
+ let mut guard = authority.lock().await;
1189
+ let result = guard.claim_authority();
1190
+ match result {
1191
+ Ok(ClaimResult::Granted { token }) => {
1192
+ println!(
1193
+ "AUTHORITY_CLAIMED {}",
1194
+ json!({
1195
+ "node": node_id,
1196
+ "fencing_token": token.0,
1197
+ "role": "AUTHORITY",
1198
+ })
1199
+ );
1200
+ }
1201
+ Ok(ClaimResult::Rejected { current_authority, current_token }) => {
1202
+ println!(
1203
+ "AUTHORITY_REJECTED {}",
1204
+ json!({
1205
+ "current_authority": current_authority,
1206
+ "current_token": current_token.0,
1207
+ "reason": "another authority is still active",
1208
+ })
1209
+ );
1210
+ }
1211
+ Ok(ClaimResult::Fenced { token }) => {
1212
+ println!(
1213
+ "AUTHORITY_FENCED {}",
1214
+ json!({
1215
+ "node": node_id,
1216
+ "fencing_token": token.0,
1217
+ "reason": "this node is fenced",
1218
+ })
1219
+ );
1220
+ }
1221
+ Err(e) => {
1222
+ println!("ERROR {}", json!({ "reason": e.to_string() }));
1223
+ }
1224
+ }
1225
+ io::stdout().flush().ok();
1226
+ }
1227
+
1228
+ // Accept a new authority announced by another node.
1229
+ // Format: authority-accept NODE_ID TOKEN
1230
+ "authority-accept" => {
1231
+ let mut parts = argument.split_whitespace();
1232
+ 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);
1238
+ let token = FencingToken(token_value);
1239
+
1240
+ let mut guard = authority.lock().await;
1241
+ let result = guard.accept_authority(&authority_node, token);
1242
+ match result {
1243
+ Ok(()) => {
1244
+ let state = guard.state();
1245
+ println!(
1246
+ "AUTHORITY_ACCEPTED {}",
1247
+ json!({
1248
+ "authority_node": authority_node,
1249
+ "fencing_token": token.0,
1250
+ "local_role": format!("{:?}", state.local_role),
1251
+ })
1252
+ );
1253
+ }
1254
+ Err(e) => {
1255
+ println!("ERROR {}", json!({ "reason": e.to_string() }));
1256
+ }
1257
+ }
1258
+ io::stdout().flush().ok();
1259
+ }
1260
+
1261
+ // Query the current authority state.
1262
+ "authority-state" => {
1263
+ let guard = authority.lock().await;
1264
+ let state = guard.state();
1265
+ let operations_applied = {
1266
+ let core_guard = core.lock().await;
1267
+ let (executor, _) = &*core_guard;
1268
+ executor.get_replica_state(&node_id).map(|r| r.operations_applied).unwrap_or(0)
1269
+ };
1270
+ println!(
1271
+ "AUTHORITY {}",
1272
+ json!({
1273
+ "cluster_id": state.cluster_id,
1274
+ "local_node": state.local_node_id,
1275
+ "local_role": format!("{:?}", state.local_role),
1276
+ "is_authority": state.is_authority(),
1277
+ "is_fenced": state.is_fenced(),
1278
+ "authority_node": state.authority_node(),
1279
+ "current_token": state.current_token().0,
1280
+ "highest_seen_token": state.highest_seen_token.0,
1281
+ "operations_applied": operations_applied,
1282
+ })
1283
+ );
1284
+ io::stdout().flush().ok();
1285
+ }
1286
+
1287
+ // Record a heartbeat from a peer. This is used for failure detection.
1288
+ // Format: heartbeat NODE_ID [TOKEN] [OPS_APPLIED] [IS_AUTHORITY]
1289
+ "heartbeat" => {
1290
+ let mut parts = argument.split_whitespace();
1291
+ let from_node = parts.next().unwrap_or_default().to_string();
1292
+
1293
+ if from_node.is_empty() {
1294
+ println!("ERROR {}", json!({ "reason": "heartbeat requires NODE_ID" }));
1295
+ } else {
1296
+ let mut guard = authority.lock().await;
1297
+ guard.heartbeat(&from_node);
1298
+ println!(
1299
+ "HEARTBEAT_RECORDED {}",
1300
+ json!({
1301
+ "from_node": from_node,
1302
+ })
1303
+ );
1304
+ }
1305
+ io::stdout().flush().ok();
1306
+ }
1307
+
1308
+ // Check if the current authority has failed based on heartbeat timeout.
1309
+ "authority-failed" => {
1310
+ let guard = authority.lock().await;
1311
+ let failed = guard.authority_failed();
1312
+ let failed_nodes = guard.failed_nodes();
1313
+ let state = guard.state();
1314
+ println!(
1315
+ "AUTHORITY_CHECK {}",
1316
+ json!({
1317
+ "authority_failed": failed,
1318
+ "current_authority": state.authority_node(),
1319
+ "failed_nodes": failed_nodes,
1320
+ })
1321
+ );
1322
+ io::stdout().flush().ok();
1323
+ }
1324
+
1325
+ // Step down from authority role voluntarily.
1326
+ "authority-stepdown" => {
1327
+ let mut guard = authority.lock().await;
1328
+ let was_authority = guard.state().is_authority();
1329
+ let result = guard.step_down();
1330
+ match result {
1331
+ Ok(()) => {
1332
+ println!(
1333
+ "AUTHORITY_STEPDOWN {}",
1334
+ json!({
1335
+ "node": node_id,
1336
+ "was_authority": was_authority,
1337
+ "now_role": format!("{:?}", guard.state().local_role),
1338
+ })
1339
+ );
1340
+ }
1341
+ Err(e) => {
1342
+ println!("ERROR {}", json!({ "reason": e.to_string() }));
1343
+ }
1344
+ }
1345
+ io::stdout().flush().ok();
1346
+ }
1347
+
1348
+ // Recover from fenced state after catching up.
1349
+ "authority-recover" => {
1350
+ let mut guard = authority.lock().await;
1351
+ let was_fenced = guard.state().is_fenced();
1352
+ let result = guard.recover_from_fenced();
1353
+ match result {
1354
+ Ok(()) => {
1355
+ println!(
1356
+ "AUTHORITY_RECOVERED {}",
1357
+ json!({
1358
+ "node": node_id,
1359
+ "was_fenced": was_fenced,
1360
+ "now_role": format!("{:?}", guard.state().local_role),
1361
+ })
1362
+ );
1363
+ }
1364
+ Err(e) => {
1365
+ println!("ERROR {}", json!({ "reason": e.to_string() }));
1366
+ }
1367
+ }
1368
+ io::stdout().flush().ok();
1369
+ }
1370
+
1150
1371
  "shutdown" => {
1151
1372
  should_exit.store(true, Ordering::Relaxed);
1152
1373
  println!("SHUTDOWN");
@@ -57,6 +57,7 @@ pub mod adversarial_transport;
57
57
  pub mod replica_membership;
58
58
  pub mod replica_acknowledgements;
59
59
  pub mod replication_manager;
60
+ pub mod authority_failover;
60
61
  pub mod metrics;
61
62
  pub mod query_performance;
62
63
  pub mod durable_sync;
@@ -1,4 +1,5 @@
1
1
  import { default as React } from 'react';
2
+ import { ManagedPublishConfiguration } from './components/ApplicationDesigner';
2
3
  import { StateFirstDB } from '@feltdb/core';
3
4
  export interface StudioAppProps {
4
5
  db?: StateFirstDB;
@@ -8,8 +9,9 @@ export interface StudioAppProps {
8
9
  deploymentRuntime?: string;
9
10
  applicationUrl?: string;
10
11
  developmentSession?: Record<string, string>;
12
+ managedPublish?: ManagedPublishConfiguration;
11
13
  onConnect?: (url: string, token: string) => void;
12
14
  }
13
- export declare function StudioApp({ db, remoteUrl, token, namespace, deploymentRuntime, applicationUrl, developmentSession, onConnect }: StudioAppProps): React.JSX.Element;
15
+ export declare function StudioApp({ db, remoteUrl, token, namespace, deploymentRuntime, applicationUrl, developmentSession, managedPublish, onConnect }: StudioAppProps): React.JSX.Element;
14
16
  export default StudioApp;
15
17
  //# sourceMappingURL=app.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAA8B,MAAM,OAAO,CAAC;AAmBnD,OAAO,EAAgC,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAC/E,OAAO,WAAW,CAAC;AAEnB,MAAM,WAAW,cAAc;IAC7B,EAAE,CAAC,EAAE,YAAY,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5C,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAClD;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,EAAE,SAAc,EAAE,KAAU,EAAE,SAAqB,EAAE,iBAA6B,EAAE,cAAmB,EAAE,kBAAkB,EAAE,SAAS,EAAE,EAAE,cAAc,qBA6IrL;AAED,eAAe,SAAS,CAAC"}
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAA8B,MAAM,OAAO,CAAC;AAgBnD,OAA4B,EAAE,KAAK,2BAA2B,EAAE,MAAM,kCAAkC,CAAC;AAGzG,OAAO,EAAgC,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAC/E,OAAO,WAAW,CAAC;AAEnB,MAAM,WAAW,cAAc;IAC7B,EAAE,CAAC,EAAE,YAAY,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5C,cAAc,CAAC,EAAE,2BAA2B,CAAC;IAC7C,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAClD;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,EAAE,SAAc,EAAE,KAAU,EAAE,SAAqB,EAAE,iBAA6B,EAAE,cAAmB,EAAE,kBAAkB,EAAE,cAAc,EAAE,SAAS,EAAE,EAAE,cAAc,qBA6IrM;AAED,eAAe,SAAS,CAAC"}
@@ -1,11 +1,19 @@
1
1
  import { default as React } from 'react';
2
2
  import { StateFirstDB, FlowSpec } from '@feltdb/core';
3
+ export interface ManagedPublishConfiguration {
4
+ url: string;
5
+ namespace: string;
6
+ token: string;
7
+ environment?: string;
8
+ applicationId?: string;
9
+ }
3
10
  export interface ApplicationDesignerProps {
4
11
  db?: StateFirstDB;
5
12
  namespace?: string;
6
13
  projectSpec?: FlowSpec | null;
14
+ managedPublish?: ManagedPublishConfiguration;
7
15
  onSpecChange?: (spec: FlowSpec) => void;
8
16
  }
9
- export declare function ApplicationDesigner({ db, namespace, projectSpec, onSpecChange }: ApplicationDesignerProps): React.JSX.Element;
17
+ export declare function ApplicationDesigner({ db, namespace, projectSpec, managedPublish, onSpecChange }: ApplicationDesignerProps): React.JSX.Element;
10
18
  export default ApplicationDesigner;
11
19
  //# sourceMappingURL=ApplicationDesigner.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ApplicationDesigner.d.ts","sourceRoot":"","sources":["../../src/components/ApplicationDesigner.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA+C,MAAM,OAAO,CAAC;AACpE,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAkB,MAAM,cAAc,CAAC;AAO3E,MAAM,WAAW,wBAAwB;IAAG,EAAE,CAAC,EAAE,YAAY,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;IAAC,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,IAAI,CAAA;CAAE;AAE3J,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,EAAE,SAAqB,EAAE,WAAW,EAAE,YAAY,EAAE,EAAE,wBAAwB,qBAoHrH;AACD,eAAe,mBAAmB,CAAC"}
1
+ {"version":3,"file":"ApplicationDesigner.d.ts","sourceRoot":"","sources":["../../src/components/ApplicationDesigner.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA+C,MAAM,OAAO,CAAC;AACpE,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAkB,MAAM,cAAc,CAAC;AAO3E,MAAM,WAAW,2BAA2B;IAAG,GAAG,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE;AAC5I,MAAM,WAAW,wBAAwB;IAAG,EAAE,CAAC,EAAE,YAAY,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;IAAC,cAAc,CAAC,EAAE,2BAA2B,CAAC;IAAC,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,IAAI,CAAA;CAAE;AAEzM,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,EAAE,SAAqB,EAAE,WAAW,EAAE,cAAc,EAAE,YAAY,EAAE,EAAE,wBAAwB,qBAoHrI;AACD,eAAe,mBAAmB,CAAC"}
@@ -1,4 +1,4 @@
1
- import { a as e, c as t, d as n, f as r, i, l as a, m as o, n as s, o as c, p as l, r as u, s as d, t as f, u as p, x as m } from "../components-C5p2TfIU.js";
1
+ import { a as e, c as t, d as n, f as r, i, l as a, m as o, n as s, o as c, p as l, r as u, s as d, t as f, u as p, x as m } from "../components-Dxuhrv8_.js";
2
2
  import { t as h } from "../KeyManagementPanel-DZuSeWBK.js";
3
3
  import { ManagedInstancePanel as g } from "./ManagedInstancePanel.js";
4
4
  export { c as AgentExplorer, f as ApplicationDesigner, p as CapabilityExplorer, t as ConflictExplorer, a as ExecutionViewer, s as GlobalSearch, i as HealthCenter, h as KeyManagementPanel, g as ManagedInstancePanel, r as OperationsExplorer, m as OverviewDashboard, n as PeerMap, e as ProvenanceViewer, l as ReferenceExplorer, u as SettingsPanel, o as StateExplorer, d as WorkflowVisualizer };