Skip to content

Commit 63aff3d

Browse files
committed
Fix snapshot recovery
1 parent 617b89c commit 63aff3d

12 files changed

Lines changed: 345 additions & 72 deletions

File tree

cli/golem-cli/src/command_handler/worker/stream.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,39 @@ impl WorkerConnection {
328328
.await;
329329
}
330330
}
331+
AgentEvent::SnapshotRecoverySucceeded {
332+
timestamp,
333+
snapshot_index,
334+
} => {
335+
if matching {
336+
output
337+
.emit_log(
338+
timestamp,
339+
LogLevel::Info,
340+
"snapshot-recovery".to_string(),
341+
format!("loaded snapshot from oplog index {snapshot_index}"),
342+
)
343+
.await;
344+
}
345+
}
346+
AgentEvent::SnapshotRecoveryFailed {
347+
timestamp,
348+
snapshot_index,
349+
error,
350+
} => {
351+
if matching {
352+
output
353+
.emit_log(
354+
timestamp,
355+
LogLevel::Error,
356+
"snapshot-recovery".to_string(),
357+
format!(
358+
"failed to load snapshot from oplog index {snapshot_index}: {error}"
359+
),
360+
)
361+
.await;
362+
}
363+
}
331364
AgentEvent::ClientLagged {
332365
number_of_missed_messages,
333366
} => {

golem-api-grpc/proto/golem/worker/log_event.proto

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ message LogEvent {
1414
InvocationFinished invocation_finished = 5;
1515
ClientLagged client_lagged = 6;
1616
PluginError plugin_error = 7;
17+
SnapshotRecoverySucceeded snapshot_recovery_succeeded = 8;
18+
SnapshotRecoveryFailed snapshot_recovery_failed = 9;
1719
}
1820
}
1921

@@ -61,6 +63,17 @@ message PluginError {
6163
string message = 3;
6264
}
6365

66+
message SnapshotRecoverySucceeded {
67+
google.protobuf.Timestamp timestamp = 1;
68+
uint64 snapshot_index = 2;
69+
}
70+
71+
message SnapshotRecoveryFailed {
72+
google.protobuf.Timestamp timestamp = 1;
73+
uint64 snapshot_index = 2;
74+
string error = 3;
75+
}
76+
6477
/**
6578
* The client fell behind and the point it left of is no longer in our buffer.
6679
* {number_of_skipped_messages} is the number of messages between the client left of and the point it is now at.

golem-common/src/model/mod.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1264,6 +1264,15 @@ pub enum AgentEvent {
12641264
plugin_name: String,
12651265
message: String,
12661266
},
1267+
SnapshotRecoverySucceeded {
1268+
timestamp: Timestamp,
1269+
snapshot_index: OplogIndex,
1270+
},
1271+
SnapshotRecoveryFailed {
1272+
timestamp: Timestamp,
1273+
snapshot_index: OplogIndex,
1274+
error: String,
1275+
},
12671276
/// The client fell behind and the point it left of is no longer in our buffer.
12681277
/// {number_of_skipped_messages} is the number of messages between the client left of and the point it is now at.
12691278
ClientLagged { number_of_missed_messages: u64 },
@@ -1315,6 +1324,16 @@ impl Display for AgentEvent {
13151324
} => {
13161325
write!(f, "<plugin-error> [{plugin_name}] {message}")
13171326
}
1327+
AgentEvent::SnapshotRecoverySucceeded { snapshot_index, .. } => {
1328+
write!(f, "<snapshot-recovery-succeeded> {snapshot_index}")
1329+
}
1330+
AgentEvent::SnapshotRecoveryFailed {
1331+
snapshot_index,
1332+
error,
1333+
..
1334+
} => {
1335+
write!(f, "<snapshot-recovery-failed> {snapshot_index}: {error}")
1336+
}
13181337
AgentEvent::ClientLagged {
13191338
number_of_missed_messages,
13201339
} => {

golem-common/src/model/protobuf.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,19 @@ impl TryFrom<golem::worker::LogEvent> for AgentEvent {
545545
message: event.message,
546546
})
547547
}
548+
golem::worker::log_event::Event::SnapshotRecoverySucceeded(event) => {
549+
Ok(AgentEvent::SnapshotRecoverySucceeded {
550+
timestamp: event.timestamp.ok_or("Missing timestamp")?.into(),
551+
snapshot_index: OplogIndex::from_u64(event.snapshot_index),
552+
})
553+
}
554+
golem::worker::log_event::Event::SnapshotRecoveryFailed(event) => {
555+
Ok(AgentEvent::SnapshotRecoveryFailed {
556+
timestamp: event.timestamp.ok_or("Missing timestamp")?.into(),
557+
snapshot_index: OplogIndex::from_u64(event.snapshot_index),
558+
error: event.error,
559+
})
560+
}
548561
},
549562
None => Err("Missing event".to_string()),
550563
}
@@ -631,6 +644,30 @@ impl TryFrom<AgentEvent> for golem::worker::LogEvent {
631644
},
632645
)),
633646
}),
647+
AgentEvent::SnapshotRecoverySucceeded {
648+
timestamp,
649+
snapshot_index,
650+
} => Ok(golem::worker::LogEvent {
651+
event: Some(golem::worker::log_event::Event::SnapshotRecoverySucceeded(
652+
golem::worker::SnapshotRecoverySucceeded {
653+
timestamp: Some(timestamp.into()),
654+
snapshot_index: snapshot_index.into(),
655+
},
656+
)),
657+
}),
658+
AgentEvent::SnapshotRecoveryFailed {
659+
timestamp,
660+
snapshot_index,
661+
error,
662+
} => Ok(golem::worker::LogEvent {
663+
event: Some(golem::worker::log_event::Event::SnapshotRecoveryFailed(
664+
golem::worker::SnapshotRecoveryFailed {
665+
timestamp: Some(timestamp.into()),
666+
snapshot_index: snapshot_index.into(),
667+
error,
668+
},
669+
)),
670+
}),
634671
AgentEvent::ClientLagged {
635672
number_of_missed_messages,
636673
} => Ok(golem::worker::LogEvent {

golem-debugging-service/src/model/params.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ impl LogNotification {
120120
message,
121121
}),
122122
InternalWorkerEvent::PluginError { .. } => None,
123+
InternalWorkerEvent::SnapshotRecoverySucceeded { .. } => None,
124+
InternalWorkerEvent::SnapshotRecoveryFailed { .. } => None,
123125
}
124126
}
125127
}

golem-test-framework/src/dsl/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,6 +1013,15 @@ pub fn log_event_to_string(event: &LogEvent) -> String {
10131013
Some(log_event::Event::InvocationStarted(_)) => "".to_string(),
10141014
Some(log_event::Event::ClientLagged { .. }) => "".to_string(),
10151015
Some(log_event::Event::PluginError(err)) => err.message.clone(),
1016+
Some(log_event::Event::SnapshotRecoverySucceeded(event)) => {
1017+
format!("snapshot recovery loaded {}", event.snapshot_index)
1018+
}
1019+
Some(log_event::Event::SnapshotRecoveryFailed(event)) => {
1020+
format!(
1021+
"snapshot recovery failed {}: {}",
1022+
event.snapshot_index, event.error
1023+
)
1024+
}
10161025
None => std::panic!("Unexpected event type"),
10171026
}
10181027
}

golem-worker-executor/src/durable_host/durability.rs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -769,7 +769,7 @@ impl<Ctx: WorkerCtx> InFunctionRetryHost for DurableWorkerCtx<Ctx> {
769769

770770
fn durable_execution_state(&self) -> DurableExecutionState {
771771
DurableExecutionState {
772-
is_live: self.state.is_live(),
772+
is_live: self.state.is_live() || self.state.snapshotting_mode.is_some(),
773773
persistence_level: self.state.persistence_level,
774774
snapshotting_mode: self.state.snapshotting_mode,
775775
assume_idempotence: self.state.assume_idempotence,
@@ -782,6 +782,10 @@ impl<Ctx: WorkerCtx> InFunctionRetryHost for DurableWorkerCtx<Ctx> {
782782
retry_from: OplogIndex,
783783
retry_policy_state: Option<RetryPolicyState>,
784784
) {
785+
if self.state.snapshotting_mode.is_some() {
786+
return;
787+
}
788+
785789
use golem_common::model::oplog::AgentError;
786790
let inside_atomic_region = self.state.outermost_atomic_region_has_side_effects();
787791
let entry = OplogEntry::error(
@@ -816,13 +820,14 @@ impl<Ctx: WorkerCtx> DurabilityHost for DurableWorkerCtx<Ctx> {
816820
forced_commit: bool,
817821
) -> Result<(), WorkerExecutorError> {
818822
self.end_function(function_type, begin_index).await?;
819-
if function_type == &DurableFunctionType::WriteRemote
820-
|| matches!(function_type, DurableFunctionType::WriteRemoteBatched(_))
821-
|| matches!(
822-
function_type,
823-
DurableFunctionType::WriteRemoteTransaction(_)
824-
)
825-
|| forced_commit
823+
if self.state.snapshotting_mode.is_none()
824+
&& (function_type == &DurableFunctionType::WriteRemote
825+
|| matches!(function_type, DurableFunctionType::WriteRemoteBatched(_))
826+
|| matches!(
827+
function_type,
828+
DurableFunctionType::WriteRemoteTransaction(_)
829+
)
830+
|| forced_commit)
826831
{
827832
self.public_state
828833
.worker()

golem-worker-executor/src/durable_host/mod.rs

Lines changed: 70 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1183,6 +1183,12 @@ impl<Ctx: WorkerCtx> DurableWorkerCtx<Ctx> {
11831183
&mut self,
11841184
function_type: &DurableFunctionType,
11851185
) -> Result<OplogIndex, WorkerExecutorError> {
1186+
if self.state.snapshotting_mode.is_some() {
1187+
let begin_index = self.state.current_oplog_index().await;
1188+
self.state.current_retry_point = begin_index;
1189+
return Ok(begin_index);
1190+
}
1191+
11861192
if (*function_type == DurableFunctionType::WriteRemote && !self.state.assume_idempotence)
11871193
|| matches!(
11881194
*function_type,
@@ -1325,6 +1331,10 @@ impl<Ctx: WorkerCtx> DurableWorkerCtx<Ctx> {
13251331
function_type: &DurableFunctionType,
13261332
begin_index: OplogIndex,
13271333
) -> Result<(), WorkerExecutorError> {
1334+
if self.state.snapshotting_mode.is_some() {
1335+
return Ok(());
1336+
}
1337+
13281338
if (*function_type == DurableFunctionType::WriteRemote && !self.state.assume_idempotence)
13291339
|| matches!(
13301340
*function_type,
@@ -1717,11 +1727,6 @@ impl<Ctx: WorkerCtx> DurableWorkerCtx<Ctx> {
17171727
return Ok(Some(RetryDecision::Immediate));
17181728
}
17191729

1720-
store
1721-
.as_context_mut()
1722-
.data_mut()
1723-
.begin_call_snapshotting_function();
1724-
17251730
let load_result = invoke_observed_and_traced(
17261731
lowered,
17271732
store,
@@ -1733,7 +1738,8 @@ impl<Ctx: WorkerCtx> DurableWorkerCtx<Ctx> {
17331738
store
17341739
.as_context_mut()
17351740
.data_mut()
1736-
.end_call_snapshotting_function();
1741+
.durable_ctx_mut()
1742+
.end_call_snapshotting_function_if_active();
17371743

17381744
for span_id in local_span_ids {
17391745
let _ = store
@@ -1877,9 +1883,11 @@ impl<Ctx: WorkerCtx> DurableWorkerCtx<Ctx> {
18771883
..
18781884
} => (payload, mime_type),
18791885
_ => {
1880-
warn!(
1886+
let error = format!(
18811887
"Expected Snapshot entry at oplog index {snapshot_index}, found different entry; falling back to full replay"
18821888
);
1889+
warn!("{error}");
1890+
Self::emit_snapshot_recovery_event(store, snapshot_index, false, Some(error));
18831891
if let Err(err) = store
18841892
.as_context_mut()
18851893
.data_mut()
@@ -1906,7 +1914,11 @@ impl<Ctx: WorkerCtx> DurableWorkerCtx<Ctx> {
19061914
{
19071915
Ok(data) => data,
19081916
Err(err) => {
1909-
warn!("Failed to download snapshot payload: {err}; falling back to full replay");
1917+
let error = format!(
1918+
"Failed to download snapshot payload: {err}; falling back to full replay"
1919+
);
1920+
warn!("{error}");
1921+
Self::emit_snapshot_recovery_event(store, snapshot_index, false, Some(error));
19101922
if let Err(err) = store
19111923
.as_context_mut()
19121924
.data_mut()
@@ -1950,7 +1962,10 @@ impl<Ctx: WorkerCtx> DurableWorkerCtx<Ctx> {
19501962
) {
19511963
Ok(lowered) => lowered,
19521964
Err(err) => {
1953-
warn!("Snapshot recovery failed to lower load-snapshot invocation: {err}");
1965+
let error =
1966+
format!("Snapshot recovery failed to lower load-snapshot invocation: {err}");
1967+
warn!("{error}");
1968+
Self::emit_snapshot_recovery_event(store, snapshot_index, false, Some(error));
19541969
return SnapshotRecoveryResult::Failed;
19551970
}
19561971
};
@@ -1964,13 +1979,16 @@ impl<Ctx: WorkerCtx> DurableWorkerCtx<Ctx> {
19641979
.set_current_invocation_context(invocation_context)
19651980
.await
19661981
{
1967-
warn!("Snapshot recovery failed to install invocation context: {err}");
1982+
let error = format!("Snapshot recovery failed to install invocation context: {err}");
1983+
warn!("{error}");
1984+
Self::emit_snapshot_recovery_event(store, snapshot_index, false, Some(error));
19681985
return SnapshotRecoveryResult::Failed;
19691986
}
19701987

19711988
store
19721989
.as_context_mut()
19731990
.data_mut()
1991+
.durable_ctx_mut()
19741992
.begin_call_snapshotting_function();
19751993

19761994
let load_result =
@@ -1979,7 +1997,8 @@ impl<Ctx: WorkerCtx> DurableWorkerCtx<Ctx> {
19791997
store
19801998
.as_context_mut()
19811999
.data_mut()
1982-
.end_call_snapshotting_function();
2000+
.durable_ctx_mut()
2001+
.end_call_snapshotting_function_if_active();
19832002

19842003
for span_id in local_span_ids {
19852004
let _ = store
@@ -2024,12 +2043,38 @@ impl<Ctx: WorkerCtx> DurableWorkerCtx<Ctx> {
20242043

20252044
if let Some(error) = failed {
20262045
warn!("{error}; re-creating instance for full replay");
2046+
Self::emit_snapshot_recovery_event(store, snapshot_index, false, Some(error));
20272047
SnapshotRecoveryResult::Failed
20282048
} else {
20292049
debug!("Snapshot loaded successfully from oplog index {snapshot_index}");
2050+
Self::emit_snapshot_recovery_event(store, snapshot_index, true, None);
20302051
SnapshotRecoveryResult::Success
20312052
}
20322053
}
2054+
2055+
fn emit_snapshot_recovery_event(
2056+
store: &mut (impl AsContextMut<Data = Ctx> + Send),
2057+
snapshot_index: OplogIndex,
2058+
succeeded: bool,
2059+
error: Option<String>,
2060+
) {
2061+
store
2062+
.as_context_mut()
2063+
.data_mut()
2064+
.get_public_state()
2065+
.event_service()
2066+
.emit_event(
2067+
if succeeded {
2068+
InternalWorkerEvent::snapshot_recovery_succeeded(snapshot_index)
2069+
} else {
2070+
InternalWorkerEvent::snapshot_recovery_failed(
2071+
snapshot_index,
2072+
error.unwrap_or_else(|| "unknown".to_string()),
2073+
)
2074+
},
2075+
true,
2076+
);
2077+
}
20332078
}
20342079

20352080
enum SnapshotRecoveryResult {
@@ -2039,6 +2084,12 @@ enum SnapshotRecoveryResult {
20392084
}
20402085

20412086
impl<Ctx: WorkerCtx> DurableWorkerCtx<Ctx> {
2087+
pub(crate) fn end_call_snapshotting_function_if_active(&mut self) {
2088+
if self.state.snapshotting_mode.is_some() {
2089+
self.end_call_snapshotting_function();
2090+
}
2091+
}
2092+
20422093
pub(crate) fn register_open_websocket(
20432094
&mut self,
20442095
rep: u32,
@@ -2601,6 +2652,14 @@ impl<Ctx: WorkerCtx> UpdateManagement for DurableWorkerCtx<Ctx> {
26012652
// While calling a snapshotting function (load/save), we completely turn off persistence
26022653
// In addition to the user-controllable persistence level we also skip writing the
26032654
// oplog entries marking the exported function call.
2655+
if self.state.snapshotting_mode.is_some() {
2656+
warn!(
2657+
"begin_call_snapshotting_function called while snapshotting is already active; \
2658+
leaving persistence level unchanged"
2659+
);
2660+
return;
2661+
}
2662+
26042663
let previous_level = self.state.persistence_level;
26052664
self.state.snapshotting_mode = Some(previous_level);
26062665
self.state.persistence_level = PersistenceLevel::PersistNothing;

0 commit comments

Comments
 (0)