Skip to content

Commit ee86b13

Browse files
authored
fix(cubestore): stop main-node OOM from concurrent metastore scans (cube-js#11082)
1 parent 26eec6f commit ee86b13

6 files changed

Lines changed: 464 additions & 120 deletions

File tree

rust/cubestore/cubestore/src/cluster/mod.rs

Lines changed: 144 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,7 @@ impl Cluster for ClusterImpl {
746746
fn job_result_listener(&self) -> JobResultListener {
747747
JobResultListener {
748748
receiver: self.meta_store_sender.subscribe(),
749+
meta_store: self.meta_store.clone(),
749750
}
750751
}
751752

@@ -1018,6 +1019,7 @@ pub trait MessageStream: Send + Sync {
10181019

10191020
pub struct JobResultListener {
10201021
receiver: Receiver<MetaStoreEvent>,
1022+
meta_store: Arc<dyn MetaStore>,
10211023
}
10221024

10231025
impl JobResultListener {
@@ -1038,53 +1040,161 @@ impl JobResultListener {
10381040
.unwrap())
10391041
}
10401042

1043+
/// Resolve the current outcome of each still-pending job directly from the metastore.
1044+
/// Used when broadcast events can't be relied on (channel lag, or the periodic safety poll).
1045+
/// A job that is gone from the metastore finished and was deleted, but after the fact we can't
1046+
/// tell success from orphan/error — report Orphaned so the caller retries rather than treating
1047+
/// a possibly-incomplete result as success.
1048+
async fn resolve_pending_from_metastore(
1049+
&self,
1050+
results: Vec<(RowKey, JobType)>,
1051+
res: &mut Vec<JobEvent>,
1052+
) -> Result<Vec<(RowKey, JobType)>, CubeError> {
1053+
let mut still_pending = Vec::new();
1054+
for (row_key, job_type) in results.into_iter() {
1055+
match self
1056+
.meta_store
1057+
.get_job_by_ref(row_key.clone(), job_type.clone())
1058+
.await?
1059+
{
1060+
Some(job) => match job.get_row().status() {
1061+
JobStatus::Completed => res.push(JobEvent::Success(row_key, job_type)),
1062+
JobStatus::Error(e) => {
1063+
res.push(JobEvent::Error(row_key, job_type, e.to_string()))
1064+
}
1065+
JobStatus::Timeout => res.push(JobEvent::Error(
1066+
row_key,
1067+
job_type,
1068+
"Job timed out".to_string(),
1069+
)),
1070+
JobStatus::Orphaned => res.push(JobEvent::Orphaned(row_key, job_type)),
1071+
JobStatus::Scheduled(_) | JobStatus::ProcessingBy(_) => {
1072+
still_pending.push((row_key, job_type))
1073+
}
1074+
},
1075+
None => res.push(JobEvent::Orphaned(row_key, job_type)),
1076+
}
1077+
}
1078+
Ok(still_pending)
1079+
}
1080+
10411081
pub async fn wait_for_job_results(
1082+
self,
1083+
results: Vec<(RowKey, JobType)>,
1084+
) -> Result<Vec<JobEvent>, CubeError> {
1085+
self.wait_for_job_results_with_poll(results, None).await
1086+
}
1087+
1088+
/// Like `wait_for_job_results`, but when `poll_interval` is set it also actively re-checks
1089+
/// pending job statuses in the metastore whenever no event arrives within the interval. This
1090+
/// keeps the wait live even if completion events are lost or dropped by the broadcast channel
1091+
/// (used by the import finalize path, where a missed event would otherwise leave the table
1092+
/// stuck "processing" forever).
1093+
pub async fn wait_for_job_results_with_poll(
10421094
mut self,
10431095
mut results: Vec<(RowKey, JobType)>,
1096+
poll_interval: Option<Duration>,
10441097
) -> Result<Vec<JobEvent>, CubeError> {
10451098
let mut res = Vec::new();
1099+
// Independent wall-clock timer for the safety re-check. It must NOT be a per-iteration
1100+
// recv timeout: the receiver sees every metastore event, so on a busy router unrelated
1101+
// events would reset such a timeout and it would never fire — exactly where a missed
1102+
// completion event is most likely. An `interval` only advances its deadline when a tick
1103+
// completes, so repeatedly cancelling tick() while recv wins keeps the cadence; with a
1104+
// `biased` select the due tick wins even when the channel is saturated with ready events.
1105+
let mut poll_timer = poll_interval.map(|interval| {
1106+
let mut timer =
1107+
tokio::time::interval_at(tokio::time::Instant::now() + interval, interval);
1108+
timer.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
1109+
timer
1110+
});
10461111
loop {
1047-
if results.len() == 0 {
1112+
if results.is_empty() {
10481113
return Ok(res);
10491114
}
1050-
let event = self.receiver.recv().await?;
1051-
if let MetaStoreEvent::UpdateJob(old, new) = &event {
1052-
if old.get_row().status() != new.get_row().status() {
1053-
let job_event = match new.get_row().status() {
1054-
JobStatus::Scheduled(_) => None,
1055-
JobStatus::ProcessingBy(_) => None,
1056-
JobStatus::Completed => Some(JobEvent::Success(
1057-
new.get_row().row_reference().clone(),
1058-
new.get_row().job_type().clone(),
1059-
)),
1060-
JobStatus::Timeout => Some(JobEvent::Error(
1061-
new.get_row().row_reference().clone(),
1062-
new.get_row().job_type().clone(),
1063-
"Job timed out".to_string(),
1064-
)),
1065-
JobStatus::Orphaned => Some(JobEvent::Orphaned(
1066-
new.get_row().row_reference().clone(),
1067-
new.get_row().job_type().clone(),
1068-
)),
1069-
JobStatus::Error(e) => Some(JobEvent::Error(
1070-
new.get_row().row_reference().clone(),
1071-
new.get_row().job_type().clone(),
1072-
e.to_string(),
1073-
)),
1074-
};
1075-
if let Some(event) = job_event {
1076-
if let JobEvent::Success(k, t) | JobEvent::Error(k, t, _) = &event {
1077-
if let Some((index, _)) = results
1078-
.iter()
1079-
.find_position(|(row_key, job_type)| k == row_key && t == job_type)
1080-
{
1081-
res.push(event);
1082-
results.remove(index);
1115+
let mut needs_recheck = false;
1116+
tokio::select! {
1117+
biased;
1118+
// Pends forever when no poll_interval, so this arm is effectively disabled then.
1119+
_ = async {
1120+
match poll_timer.as_mut() {
1121+
Some(timer) => {
1122+
timer.tick().await;
1123+
}
1124+
None => std::future::pending::<()>().await,
1125+
}
1126+
} => {
1127+
needs_recheck = true;
1128+
}
1129+
recv_result = self.receiver.recv() => {
1130+
match recv_result {
1131+
Ok(event) => {
1132+
if let MetaStoreEvent::UpdateJob(old, new) = &event {
1133+
if old.get_row().status() != new.get_row().status() {
1134+
let job_event = match new.get_row().status() {
1135+
JobStatus::Scheduled(_) => None,
1136+
JobStatus::ProcessingBy(_) => None,
1137+
JobStatus::Completed => Some(JobEvent::Success(
1138+
new.get_row().row_reference().clone(),
1139+
new.get_row().job_type().clone(),
1140+
)),
1141+
JobStatus::Timeout => Some(JobEvent::Error(
1142+
new.get_row().row_reference().clone(),
1143+
new.get_row().job_type().clone(),
1144+
"Job timed out".to_string(),
1145+
)),
1146+
JobStatus::Orphaned => Some(JobEvent::Orphaned(
1147+
new.get_row().row_reference().clone(),
1148+
new.get_row().job_type().clone(),
1149+
)),
1150+
JobStatus::Error(e) => Some(JobEvent::Error(
1151+
new.get_row().row_reference().clone(),
1152+
new.get_row().job_type().clone(),
1153+
e.to_string(),
1154+
)),
1155+
};
1156+
if let Some(event) = job_event {
1157+
if let JobEvent::Success(k, t)
1158+
| JobEvent::Orphaned(k, t)
1159+
| JobEvent::Error(k, t, _) = &event
1160+
{
1161+
if let Some((index, _)) =
1162+
results.iter().find_position(|(row_key, job_type)| {
1163+
k == row_key && t == job_type
1164+
})
1165+
{
1166+
res.push(event);
1167+
results.remove(index);
1168+
}
1169+
}
1170+
}
1171+
}
10831172
}
10841173
}
1174+
Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {
1175+
// The channel overflowed and dropped messages — the completion events
1176+
// we were waiting for may have been among them. Re-derive pending jobs
1177+
// authoritatively instead of failing finalize.
1178+
log::warn!(
1179+
"JobResultListener lagged by {} events; re-checking {} pending job(s) from metastore",
1180+
n,
1181+
results.len()
1182+
);
1183+
needs_recheck = true;
1184+
}
1185+
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
1186+
return Err(CubeError::internal(
1187+
"JobResultListener event channel closed".to_string(),
1188+
));
1189+
}
10851190
}
10861191
}
10871192
}
1193+
if needs_recheck {
1194+
results = self
1195+
.resolve_pending_from_metastore(results, &mut res)
1196+
.await?;
1197+
}
10881198
}
10891199
}
10901200
}

rust/cubestore/cubestore/src/config/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2215,7 +2215,10 @@ impl Config {
22152215
}
22162216

22172217
pub async fn configure_meta_store(&self) {
2218-
let (metastore_event_sender, _) = broadcast::channel(8192); // TODO config
2218+
// Bounded broadcast of metastore events; sized for bursts (e.g. orphaned-job cleanup
2219+
// emitting hundreds of UpdateJob events at once). Listeners that still lag past this fall
2220+
// back to authoritative metastore re-checks rather than failing.
2221+
let (metastore_event_sender, _) = broadcast::channel(32768);
22192222
let metastore_event_sender_to_move = metastore_event_sender.clone();
22202223

22212224
if let Some(_) = self.config_obj.metastore_remote_address() {

0 commit comments

Comments
 (0)