Skip to content

Commit 7f19dc0

Browse files
committed
sync: prevent syncing checkpoints from being GCed
Only clears the `sync_checkpoint_request` once the sync has actually completed. Checkpoints will only get GCed once the sync process has completed. Signed-off-by: Abhinav Gyawali <22275402+abhizer@users.noreply.github.com>
1 parent c082b3c commit 7f19dc0

3 files changed

Lines changed: 46 additions & 6 deletions

File tree

crates/adapters/src/controller/mod.rs

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,6 +1034,17 @@ impl Controller {
10341034
}
10351035
}
10361036

1037+
struct SyncCheckpointRequest {
1038+
uuid: Arc<TokioMutex<Option<uuid::Uuid>>>,
1039+
cb: Option<SyncCheckpointCallbackFn>,
1040+
}
1041+
1042+
impl SyncCheckpointRequest {
1043+
fn uuid(&self) -> Option<uuid::Uuid> {
1044+
*self.uuid.blocking_lock()
1045+
}
1046+
}
1047+
10371048
struct CircuitThread {
10381049
controller: Arc<ControllerInner>,
10391050
circuit: DBSPHandle,
@@ -1048,7 +1059,7 @@ struct CircuitThread {
10481059
checkpoint_requests: Vec<CheckpointRequest>,
10491060

10501061
/// Currently only allows one request at a time.
1051-
sync_checkpoint_request: Option<(uuid::Uuid, SyncCheckpointCallbackFn)>,
1062+
sync_checkpoint_request: Option<SyncCheckpointRequest>,
10521063

10531064
/// Storage backend for writing checkpoints.
10541065
storage: Option<Arc<dyn StorageBackend>>,
@@ -1467,12 +1478,18 @@ impl CircuitThread {
14671478
CHECKPOINT_WRITTEN.record((written_after - written_before) / 1_000_000);
14681479
CHECKPOINT_PROCESSED_RECORDS.store(processed_records, Ordering::Relaxed);
14691480

1470-
// TODO: check the requested checkpoint UUID
1471-
if this.sync_checkpoint_request.is_none() {
1481+
let sync_requested = this
1482+
.sync_checkpoint_request
1483+
.as_ref()
1484+
.map(|s| s.uuid())
1485+
.flatten();
1486+
1487+
if sync_requested.is_none() {
14721488
if let Err(error) = this.circuit.gc_checkpoint() {
14731489
warn!("error removing old checkpoints: {error}");
14741490
}
14751491
}
1492+
14761493
if let Some(ft) = &mut this.ft {
14771494
ft.checkpointed()?;
14781495
}
@@ -1557,7 +1574,10 @@ impl CircuitThread {
15571574
.push(CheckpointRequest::SuspendCommand(reply_callback));
15581575
}
15591576
Command::SyncCheckpoint((uuid, reply_callback)) => {
1560-
self.sync_checkpoint_request = Some((uuid, reply_callback));
1577+
self.sync_checkpoint_request = Some(SyncCheckpointRequest {
1578+
uuid: Arc::new(TokioMutex::new(Some(uuid))),
1579+
cb: Some(reply_callback),
1580+
});
15611581
}
15621582
}
15631583
}
@@ -1882,7 +1902,15 @@ impl CircuitThread {
18821902
}
18831903

18841904
fn sync_checkpoint(&mut self) {
1885-
let Some((uuid, cb)) = self.sync_checkpoint_request.take() else {
1905+
let Some((uuid_lock, Some(cb))) = self
1906+
.sync_checkpoint_request
1907+
.as_mut()
1908+
.map(|u| (u.uuid.clone(), u.cb.take()))
1909+
else {
1910+
return;
1911+
};
1912+
1913+
let Some(uuid) = uuid_lock.blocking_lock().clone() else {
18861914
return;
18871915
};
18881916

@@ -1940,6 +1968,8 @@ impl CircuitThread {
19401968
} else {
19411969
cb(Ok(()))
19421970
}
1971+
1972+
uuid_lock.blocking_lock().take()
19431973
})
19441974
.expect("failed to spawn s3-synchronizer thread");
19451975
}

python/feldera/pipeline.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,7 @@ def sync_checkpoint(self, wait: bool = False, timeout_s=300) -> str:
685685
checkpoint to complete syncing.
686686
687687
:raises FelderaAPIError: If no checkpoints have been made.
688+
:raises RuntimeError: If syncing the checkpoint fails.
688689
"""
689690

690691
uuid = self.client.sync_checkpoint(self.name)
@@ -702,6 +703,11 @@ def sync_checkpoint(self, wait: bool = False, timeout_s=300) -> str:
702703
pipeline '{self.name}' to sync checkpoint '{uuid}'"""
703704
)
704705
status = self.sync_checkpoint_status(uuid)
706+
if status == CheckpointStatus.Failure:
707+
raise RuntimeError(
708+
f"failed to sync checkpoint '{uuid}': ", status.get_error()
709+
)
710+
705711
if status in [CheckpointStatus.InProgress, CheckpointStatus.Unknown]:
706712
time.sleep(0.1)
707713
continue
@@ -716,6 +722,9 @@ def sync_checkpoint_status(self, uuid: str) -> CheckpointStatus:
716722
If the checkpoint is currently being synchronized, returns
717723
`CheckpointStatus.Unknown`.
718724
725+
Failures are not raised as runtime errors and must be explicitly
726+
checked.
727+
719728
:param uuid: The checkpoint uuid.
720729
"""
721730

@@ -731,6 +740,7 @@ def sync_checkpoint_status(self, uuid: str) -> CheckpointStatus:
731740
if uuid == fail.get("uuid"):
732741
failure = CheckpointStatus.Failure
733742
failure.error = fail.get("error", "")
743+
logging.error(f"failed to sync checkpoint '{uuid}': {failure.error}")
734744
return failure
735745

736746
if (success is None) or UUID(uuid) > UUID(success):

python/feldera/rest/feldera_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -986,7 +986,7 @@ def query_as_json(
986986
stream=True,
987987
)
988988

989-
for chunk in resp.iter_lines(chunk_size=50000000):
989+
for chunk in resp.iter_lines(chunk_size=1024):
990990
if chunk:
991991
yield json.loads(chunk, parse_float=Decimal)
992992

0 commit comments

Comments
 (0)