GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — [dbsp] Keep the circuit running after a failed checkpoint. · feldera/feldera@3c6ce0a

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
  • crates
    • connectors/src
      • controller.rs
      • engine.rs
    • dbsp
      • Cargo.toml
      • src/circuit
        • dbsp_handle.rs
    • static-connectors
      • Cargo.toml
      • src
        • engine.rs
        • test
          • controller_tests.rs
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎crates/connectors/src/controller.rs‎

Copy file name to clipboardExpand all lines: crates/connectors/src/controller.rs
+20Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,8 @@ enum Command {
650650
SyncCheckpoint((uuid::Uuid, SyncCheckpointCallbackFn)),
651651
Rebalance(RebalanceCallbackFn),
652652
StartCompaction(StartCompactionCallbackFn),
653+
#[cfg(any(test, feature = "testing"))]
654+
InjectFatalError(RebalanceCallbackFn),
653655
}
654656

655657
/// Tracks whether the initial snapshot has been delivered for an output
@@ -826,6 +828,8 @@ impl Command {
826828
}
827829
Command::Rebalance(callback) => callback(Err(ControllerError::ControllerExit)),
828830
Command::StartCompaction(callback) => callback(Err(ControllerError::ControllerExit)),
831+
#[cfg(any(test, feature = "testing"))]
832+
Command::InjectFatalError(callback) => callback(Err(ControllerError::ControllerExit)),
829833
}
830834
}
831835
}
@@ -2436,6 +2440,18 @@ impl Controller {
24362440
self.status().completion_status(token)
24372441
}
24382442

2443+
/// Test-only: makes the circuit thread fail a circuit command with an
2444+
/// error that kills the circuit from that thread.
2445+
#[cfg(any(test, feature = "testing"))]
2446+
pub fn inject_fatal_error(&self) -> Result<(), ControllerError> {
2447+
let (sender, receiver) = oneshot::channel();
2448+
self.inner
2449+
.send_command(Command::InjectFatalError(Box::new(move |result| {
2450+
let _ = sender.send(result);
2451+
})));
2452+
reply_or_controller_exit(receiver.blocking_recv())
2453+
}
2454+
24392455
pub async fn rebalance(&self) -> Result<(), ControllerError> {
24402456
let (sender, receiver) = oneshot::channel();
24412457
self.inner
@@ -4512,6 +4528,10 @@ impl CircuitThread {
45124528
});
45134529
}
45144530
Command::Rebalance(reply_callback) => reply_callback(self.circuit.rebalance()),
4531+
#[cfg(any(test, feature = "testing"))]
4532+
Command::InjectFatalError(reply_callback) => {
4533+
reply_callback(self.circuit.inject_fatal_error())
4534+
}
45154535
Command::StartCompaction(reply_callback) => {
45164536
reply_callback(self.circuit.start_compaction())
45174537
}
Collapse file

‎crates/connectors/src/engine.rs‎

Copy file name to clipboardExpand all lines: crates/connectors/src/engine.rs
+7Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,13 @@ pub trait CircuitDriver {
209209
/// Starts compacting the circuit's stored state.
210210
fn start_compaction(&mut self) -> Result<(), ControllerError>;
211211

212+
/// Test-only: fails a circuit command with an error that kills the
213+
/// circuit from the calling thread.
214+
#[cfg(any(test, feature = "testing"))]
215+
fn inject_fatal_error(&mut self) -> Result<(), ControllerError> {
216+
unimplemented!("this engine cannot inject a fatal error")
217+
}
218+
212219
/// Stops the circuit and waits for its threads.
213220
fn kill(self: Box<Self>) -> Result<(), ControllerError>;
214221
}
Collapse file

‎crates/dbsp/Cargo.toml‎

Copy file name to clipboardExpand all lines: crates/dbsp/Cargo.toml
+3Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ backend-mode = []
3131
# write pprof heap profiles; see `--heap-profile-dir` in `lazy_input_map`.
3232
heap-profile = ["dep:tikv-jemallocator", "dep:jemalloc_pprof"]
3333

34+
# Test hooks for other crates, such as `DBSPHandle::inject_fatal_error`.
35+
testing = []
36+
3437
[dependencies]
3538
num = { workspace = true }
3639
anyhow = { workspace = true }
Collapse file

‎crates/dbsp/src/circuit/dbsp_handle.rs‎

Copy file name to clipboardExpand all lines: crates/dbsp/src/circuit/dbsp_handle.rs
+141-4Lines changed: 141 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,13 @@ impl Runtime {
818818
return;
819819
}
820820
}
821+
#[cfg(any(test, feature = "testing"))]
822+
Ok(Command::InjectFatalError) => {
823+
let error = DbspError::IO(std::io::Error::other("injected fatal error"));
824+
if status_sender.send(Err(error)).is_err() {
825+
return;
826+
}
827+
}
821828
Ok(Command::Rebalance) => {
822829
circuit.rebalance();
823830
if status_sender.send(Ok(Response::Unit)).is_err() {
@@ -1151,6 +1158,58 @@ enum Command {
11511158
/// Install the bootstrap circuit's state into the main circuit,
11521159
/// reactivate the full main circuit, and delete the bootstrap circuit.
11531160
CutoverBootstrapCircuit,
1161+
/// Test-only: every worker replies with an error that is fatal.
1162+
#[cfg(any(test, feature = "testing"))]
1163+
InjectFatalError,
1164+
}
1165+
1166+
impl Command {
1167+
/// Returns true if a worker's error reply to this command can leave the
1168+
/// workers out of step, so that the runtime must be killed.
1169+
///
1170+
/// Returns false only for commands that do not change circuit state, so
1171+
/// that the circuit stays usable after an error.
1172+
fn error_is_fatal(&self) -> bool {
1173+
match self {
1174+
// Takes in-memory snapshots of operator state. It writes no files
1175+
// and does no exchange between workers.
1176+
Command::PrepareCheckpoint => false,
1177+
1178+
Command::StartTransaction
1179+
| Command::Step
1180+
| Command::CommitTransaction
1181+
| Command::CommitProgress
1182+
| Command::Transaction
1183+
| Command::IsBootstrapComplete
1184+
| Command::CompleteBootstrap
1185+
| Command::EnableProfiler
1186+
| Command::DumpProfile { .. }
1187+
| Command::RetrieveGraph
1188+
| Command::RetrieveProfile { .. }
1189+
| Command::GetLir
1190+
| Command::Restore(_)
1191+
| Command::SetBalancerHintsByGlobalId(_)
1192+
| Command::SetBalancerHints(_)
1193+
| Command::GetCurrentBalancerPolicies
1194+
| Command::GetCurrentBalancerPolicy(_)
1195+
| Command::Rebalance
1196+
| Command::SetAutoRebalance(_)
1197+
| Command::StartCompaction
1198+
| Command::IsCompactionComplete
1199+
| Command::CreateBootstrapCircuit(_)
1200+
| Command::StartBootstrapTransaction
1201+
| Command::CommitBootstrapTransaction
1202+
| Command::StepBootstrapCircuit
1203+
| Command::BootstrapCommitProgress
1204+
| Command::DestroyBootstrapCircuit
1205+
| Command::RestoreConcurrent(_)
1206+
| Command::SyncBootstrapCircuit
1207+
| Command::CutoverBootstrapCircuit => true,
1208+
1209+
#[cfg(any(test, feature = "testing"))]
1210+
Command::InjectFatalError => true,
1211+
}
1212+
}
11541213
}
11551214

11561215
impl Debug for Command {
@@ -1208,6 +1267,8 @@ impl Debug for Command {
12081267
}
12091268
Command::SyncBootstrapCircuit => write!(f, "SyncBootstrapCircuit"),
12101269
Command::CutoverBootstrapCircuit => write!(f, "CutoverBootstrapCircuit"),
1270+
#[cfg(any(test, feature = "testing"))]
1271+
Command::InjectFatalError => write!(f, "InjectFatalError"),
12111272
}
12121273
}
12131274
}
@@ -1433,7 +1494,9 @@ impl DBSPHandle {
14331494
Err(DbspError::Runtime(RuntimeError::WorkerPanic { panic_info }))
14341495
}
14351496

1436-
// Receive responses.
1497+
// Receive responses. After a non-fatal error, keep receiving, so that
1498+
// no stale reply is left for the next command.
1499+
let mut error = None;
14371500
for _ in 0..self.status_receivers.len() {
14381501
let ready = select.select();
14391502
let worker = ready.index();
@@ -1445,18 +1508,21 @@ impl DBSPHandle {
14451508

14461509
match ready.recv(&self.status_receivers[worker]) {
14471510
Err(_) => return handle_panic(self),
1448-
Ok(Err(e)) => {
1511+
Ok(Err(e)) if command.error_is_fatal() => {
14491512
let _ = self.kill_inner();
14501513
return Err(e);
14511514
}
1515+
Ok(Err(e)) => {
1516+
error.get_or_insert(e);
1517+
}
14521518
Ok(Ok(resp)) => handler(worker, resp),
14531519
}
14541520
}
14551521
if self.panicked() {
14561522
return handle_panic(self);
14571523
}
14581524

1459-
Ok(())
1525+
error.map_or(Ok(()), Err)
14601526
}
14611527

14621528
fn unicast_command(&mut self, worker: usize, command: Command) -> Result<Response, DbspError> {
@@ -1465,6 +1531,7 @@ impl DBSPHandle {
14651531
}
14661532

14671533
// Send command.
1534+
let error_is_fatal = command.error_is_fatal();
14681535
if self.command_senders[worker].send(command).is_err() {
14691536
let panic_info = self.collect_panic_info().unwrap_or_default();
14701537

@@ -1488,10 +1555,11 @@ impl DBSPHandle {
14881555
}
14891556
let reply = match ready.recv(&self.status_receivers[worker]) {
14901557
Err(_) => return handle_panic(self),
1491-
Ok(Err(e)) => {
1558+
Ok(Err(e)) if error_is_fatal => {
14921559
let _ = self.kill_inner();
14931560
return Err(e);
14941561
}
1562+
Ok(Err(e)) => return Err(e),
14951563
Ok(Ok(resp)) => resp,
14961564
};
14971565

@@ -2414,6 +2482,14 @@ impl DBSPHandle {
24142482
Ok(())
24152483
}
24162484

2485+
/// Test-only: makes every worker fail a command with an error that is
2486+
/// fatal, so that the runtime is killed from the calling thread, as it is
2487+
/// for a real fatal error.
2488+
#[cfg(any(test, feature = "testing"))]
2489+
pub fn inject_fatal_error(&mut self) -> Result<(), DbspError> {
2490+
self.broadcast_command(Command::InjectFatalError, |_, _| {})
2491+
}
2492+
24172493
pub fn start_compaction(&mut self) -> Result<(), DbspError> {
24182494
self.broadcast_command(Command::StartCompaction, |_, _| {})?;
24192495
Ok(())
@@ -3237,6 +3313,67 @@ pub(crate) mod tests {
32373313
}
32383314
}
32393315

3316+
/// A failed checkpoint must return the error and leave the circuit
3317+
/// running. Only worker 0 fails, so the other workers' replies must be
3318+
/// drained, not left for the next command.
3319+
#[test]
3320+
fn test_failed_checkpoint_keeps_circuit() {
3321+
use crate::circuit::Mode;
3322+
use crate::typed_batch::IndexedZSetReader;
3323+
use std::sync::atomic::{AtomicI64, Ordering};
3324+
3325+
let temp = tempdir().unwrap();
3326+
let total = Arc::new(AtomicI64::new(0));
3327+
let total_clone = total.clone();
3328+
let cconf = mkconfig_with(temp.path(), 4, 0).with_mode(Mode::Persistent);
3329+
let (mut dbsp, input) = Runtime::init_circuit(cconf, move |circuit| {
3330+
let (stream, handle) = circuit.add_input_zset::<i32>();
3331+
stream.set_persistent_id(Some("input"));
3332+
let total = total_clone.clone();
3333+
stream.inspect(move |batch: &OrdZSet<i32>| {
3334+
total.fetch_add(batch.iter().map(|(_, _, w)| w).sum(), Ordering::SeqCst);
3335+
});
3336+
if Runtime::worker_index() == 0 {
3337+
// No persistent id, so this operator cannot be checkpointed.
3338+
stream.integrate();
3339+
}
3340+
Ok(handle)
3341+
})
3342+
.unwrap();
3343+
3344+
for round in 1..=2 {
3345+
let err = dbsp.checkpoint().run().unwrap_err();
3346+
assert!(
3347+
matches!(err, DbspError::Storage(StorageError::NoPersistentId(_))),
3348+
"unexpected error {err:?}"
3349+
);
3350+
3351+
// Panics on a stale `CheckpointPrepared` reply.
3352+
dbsp.commit_progress().unwrap();
3353+
3354+
input.append(&mut vec![Tup2(round, 1), Tup2(round + 10, 1)]);
3355+
dbsp.transaction().unwrap();
3356+
assert_eq!(total.load(Ordering::SeqCst), 2 * round as i64);
3357+
}
3358+
dbsp.kill().unwrap();
3359+
}
3360+
3361+
/// A fatal error kills the runtime, so later commands fail.
3362+
#[test]
3363+
fn test_fatal_error_kills_circuit() {
3364+
let temp = tempdir().unwrap();
3365+
let (mut dbsp, ()) =
3366+
Runtime::init_circuit(mkconfig_with(temp.path(), 4, 0), |_circuit| Ok(())).unwrap();
3367+
3368+
let err = dbsp.inject_fatal_error().unwrap_err();
3369+
assert!(matches!(err, DbspError::IO(_)), "unexpected error {err:?}");
3370+
let err = dbsp.transaction().unwrap_err();
3371+
assert!(
3372+
matches!(err, DbspError::Runtime(RuntimeError::Terminated)),
3373+
"unexpected error {err:?}"
3374+
);
3375+
}
3376+
32403377
type CircuitHandle = (
32413378
IndexedZSetHandle<i32, i32>,
32423379
OutputHandle<OrdZSet<Tup2<i32, i32>>>,
Collapse file

‎crates/static-connectors/Cargo.toml‎

Copy file name to clipboardExpand all lines: crates/static-connectors/Cargo.toml
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ bench-mode = ["dbsp_connectors/bench-mode"]
6262
# target, which runs the connector suites of `dbsp_connectors`. This crate's dev-dependency on
6363
# itself turns it on, so a plain `cargo test` builds the target.
6464
testing = [
65+
"dbsp/testing",
6566
"dbsp_connectors/testing",
6667
"feldera-adapterlib/testing",
6768
"dep:feldera-macros",
Collapse file

‎crates/static-connectors/src/engine.rs‎

Copy file name to clipboardExpand all lines: crates/static-connectors/src/engine.rs
+7Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,13 @@ impl CircuitDriver for StaticCircuitDriver {
335335
.map_err(ControllerError::dbsp_error)
336336
}
337337

338+
#[cfg(feature = "testing")]
339+
fn inject_fatal_error(&mut self) -> Result<(), ControllerError> {
340+
self.handle
341+
.inject_fatal_error()
342+
.map_err(ControllerError::dbsp_error)
343+
}
344+
338345
fn kill(self: Box<Self>) -> Result<(), ControllerError> {
339346
self.handle
340347
.kill()

0 commit comments

Comments
0 (0)
You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.