Skip to content

Commit 19f4d4f

Browse files
abort all hot-tier tasks on tenant deletion (#1806)
* abort all hot-tier tasks on tenant deletion background tasks self stop when tenant no longer exists add tenant existence check * cleanup tenant before state removal * coderabbit comments fix
1 parent d3cc411 commit 19f4d4f

3 files changed

Lines changed: 144 additions & 10 deletions

File tree

‎src/hottier.rs‎

Lines changed: 132 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use std::{
2626
atomic::{AtomicU64, AtomicUsize, Ordering},
2727
},
2828
};
29-
use tokio::sync::{Mutex as AsyncMutex, RwLock as AsyncRwLock, mpsc};
29+
use tokio::sync::{Mutex as AsyncMutex, OwnedRwLockReadGuard, RwLock as AsyncRwLock, mpsc};
3030

3131
use crate::{
3232
catalog::manifest::{File, Manifest},
@@ -286,6 +286,7 @@ impl QueryPin {
286286
pub struct HotTierQueryGuard {
287287
state: Arc<StreamSyncState>,
288288
pin_id: u64,
289+
_tenant_admission: Option<OwnedRwLockReadGuard<bool>>,
289290
}
290291

291292
impl Drop for HotTierQueryGuard {
@@ -316,6 +317,7 @@ impl StreamSyncState {
316317
HotTierQueryGuard {
317318
state: self.clone(),
318319
pin_id,
320+
_tenant_admission: None,
319321
}
320322
}
321323
}
@@ -431,6 +433,7 @@ struct StreamTasks {
431433
pub struct HotTierManager {
432434
filesystem: LocalFileSystem,
433435
hot_tier_path: &'static Path,
436+
tenant_gates: AsyncMutex<HashMap<String, Arc<AsyncRwLock<bool>>>>,
434437
state_cache: AsyncRwLock<HashMap<StreamKey, Arc<StreamSyncState>>>,
435438
state_init_locks: AsyncMutex<HashMap<StreamKey, Arc<AsyncMutex<()>>>>,
436439
tasks: AsyncRwLock<HashMap<StreamKey, StreamTasks>>,
@@ -544,13 +547,37 @@ impl HotTierManager {
544547
HotTierManager {
545548
filesystem: LocalFileSystem::new(),
546549
hot_tier_path,
550+
tenant_gates: AsyncMutex::new(HashMap::new()),
547551
state_cache: AsyncRwLock::new(HashMap::new()),
548552
state_init_locks: AsyncMutex::new(HashMap::new()),
549553
tasks: AsyncRwLock::new(HashMap::new()),
550554
sender,
551555
}
552556
}
553557

558+
async fn tenant_gate(&self, tenant_id: &str) -> Arc<AsyncRwLock<bool>> {
559+
self.tenant_gates
560+
.lock()
561+
.await
562+
.entry(tenant_id.to_owned())
563+
.or_insert_with(|| Arc::new(AsyncRwLock::new(false)))
564+
.clone()
565+
}
566+
567+
async fn tenant_admission(
568+
&self,
569+
tenant_id: &Option<String>,
570+
) -> Result<Option<OwnedRwLockReadGuard<bool>>, ()> {
571+
let Some(tenant_id) = tenant_id else {
572+
return Ok(None);
573+
};
574+
let admission = self.tenant_gate(tenant_id).await.read_owned().await;
575+
if *admission || !TENANT_METADATA.contains_tenant(tenant_id) {
576+
return Err(());
577+
}
578+
Ok(Some(admission))
579+
}
580+
554581
#[tracing::instrument(name = "hottier.startup", skip(self))]
555582
pub async fn start_all_tasks(&'static self) {
556583
let _ = tokio::spawn(async move {
@@ -574,6 +601,22 @@ impl HotTierManager {
574601
&self,
575602
stream: &str,
576603
tenant_id: &Option<String>,
604+
) -> Result<Arc<StreamSyncState>, HotTierError> {
605+
let admission = self
606+
.tenant_admission(tenant_id)
607+
.await
608+
.map_err(|()| HotTierValidationError::NotFound(stream.to_owned()))?;
609+
let state = self
610+
.get_or_load_state_with_admission(stream, tenant_id)
611+
.await?;
612+
drop(admission);
613+
Ok(state)
614+
}
615+
616+
async fn get_or_load_state_with_admission(
617+
&self,
618+
stream: &str,
619+
tenant_id: &Option<String>,
577620
) -> Result<Arc<StreamSyncState>, HotTierError> {
578621
let key: StreamKey = (tenant_id.clone(), stream.to_owned());
579622
{
@@ -629,9 +672,16 @@ impl HotTierManager {
629672
start: DateTime<Utc>,
630673
end: DateTime<Utc>,
631674
) -> Result<HotTierQueryGuard, HotTierError> {
632-
let state = self.get_or_load_state(stream, tenant_id).await?;
675+
let admission = self
676+
.tenant_admission(tenant_id)
677+
.await
678+
.map_err(|()| HotTierValidationError::NotFound(stream.to_owned()))?;
679+
let state = self
680+
.get_or_load_state_with_admission(stream, tenant_id)
681+
.await?;
633682
let registration_guard = state.eviction.read().await;
634-
let query_guard = state.pin_query(start, end);
683+
let mut query_guard = state.pin_query(start, end);
684+
query_guard._tenant_admission = admission;
635685
drop(registration_guard);
636686
Ok(query_guard)
637687
}
@@ -644,6 +694,39 @@ impl HotTierManager {
644694
}
645695
}
646696

697+
/// Stop all hot-tier work and discard cached state for a deleted tenant.
698+
pub async fn cleanup_tenant(&self, tenant_id: &str) {
699+
let tenant = Some(tenant_id.to_owned());
700+
let gate = self.tenant_gate(tenant_id).await;
701+
*gate.write().await = true;
702+
let removed_tasks = {
703+
let mut tasks = self.tasks.write().await;
704+
tasks
705+
.extract_if(|(task_tenant, _), _| task_tenant == &tenant)
706+
.map(|(_, task)| task)
707+
.collect::<Vec<_>>()
708+
};
709+
710+
for task in removed_tasks {
711+
task.latest.abort();
712+
let _ = task.latest.await;
713+
}
714+
715+
self.state_cache
716+
.write()
717+
.await
718+
.retain(|(state_tenant, _), _| state_tenant != &tenant);
719+
self.state_init_locks
720+
.lock()
721+
.await
722+
.retain(|(state_tenant, _), _| state_tenant != &tenant);
723+
}
724+
725+
/// Remove the closed admission gate after tenant metadata is gone.
726+
pub async fn finish_tenant_cleanup(&self, tenant_id: &str) {
727+
self.tenant_gates.lock().await.remove(tenant_id);
728+
}
729+
647730
/// get the total hot tier size for all streams
648731
#[tracing::instrument(
649732
name = "hottier.get_hot_tiers_size",
@@ -880,18 +963,21 @@ impl HotTierManager {
880963
fields(stream = %stream, tenant = ?tenant_id)
881964
)]
882965
pub async fn spawn_stream_task(&'static self, stream: String, tenant_id: Option<String>) {
883-
let _ = tokio::spawn(async move {
884-
self.sender
885-
.send(HotTierMessage::StartTask((tenant_id, stream)))
886-
.unwrap();
887-
})
888-
.instrument(tracing::Span::current())
889-
.await;
966+
let Ok(admission) = self.tenant_admission(&tenant_id).await else {
967+
return;
968+
};
969+
self.sender
970+
.send(HotTierMessage::StartTask((tenant_id, stream)))
971+
.unwrap();
972+
drop(admission);
890973
}
891974

892975
/// Spawn Latest loop for a single stream. Idempotent:
893976
/// if tasks already exist for this (tenant, stream), no-op.
894977
async fn spawn_stream_task_inner(&'static self, stream: String, tenant_id: Option<String>) {
978+
let Ok(admission) = self.tenant_admission(&tenant_id).await else {
979+
return;
980+
};
895981
let key: StreamKey = (tenant_id.clone(), stream.clone());
896982

897983
let mut tasks = self.tasks.write().await;
@@ -912,6 +998,12 @@ impl HotTierManager {
912998
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
913999
loop {
9141000
interval.tick().await;
1001+
if t.as_deref()
1002+
.is_some_and(|tenant| !TENANT_METADATA.contains_tenant(tenant))
1003+
{
1004+
info!(stream = %s, tenant = ?t, "stopping hot tier task for deleted tenant");
1005+
break;
1006+
}
9151007
let anchor = floor_to_minute(Utc::now());
9161008
let tick_span = tracing::info_span!(
9171009
"hottier.tick",
@@ -932,6 +1024,8 @@ impl HotTierManager {
9321024
if let Some(old) = tasks.insert(key, StreamTasks { latest }) {
9331025
old.latest.abort();
9341026
}
1027+
drop(tasks);
1028+
drop(admission);
9351029
}
9361030

9371031
/// Abort and remove per-stream tasks. Caller must ensure no further work
@@ -1904,6 +1998,7 @@ mod tests {
19041998
Arc, Mutex,
19051999
atomic::{AtomicUsize, Ordering},
19062000
},
2001+
time::Duration,
19072002
};
19082003

19092004
use chrono::{TimeZone, Utc};
@@ -1982,6 +2077,33 @@ mod tests {
19822077
)
19832078
}
19842079

2080+
#[tokio::test]
2081+
async fn tenant_cleanup_closes_admission_before_draining() {
2082+
let root = Box::leak(tempfile::tempdir().unwrap().keep().into_boxed_path());
2083+
let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
2084+
let manager: &'static HotTierManager =
2085+
Box::leak(Box::new(HotTierManager::new(root, sender)));
2086+
let tenant = "hot-tier-cleanup-admission-test".to_owned();
2087+
let gate = manager.tenant_gate(&tenant).await;
2088+
let admission = gate.clone().read_owned().await;
2089+
2090+
let cleanup_tenant = tenant.clone();
2091+
let cleanup = tokio::spawn(async move {
2092+
manager.cleanup_tenant(&cleanup_tenant).await;
2093+
});
2094+
tokio::task::yield_now().await;
2095+
assert!(!cleanup.is_finished());
2096+
2097+
drop(admission);
2098+
tokio::time::timeout(Duration::from_secs(1), cleanup)
2099+
.await
2100+
.unwrap()
2101+
.unwrap();
2102+
assert!(*gate.read().await);
2103+
2104+
manager.finish_tenant_cleanup(&tenant).await;
2105+
}
2106+
19852107
#[tokio::test]
19862108
async fn disk_budget_reports_threshold_deficit() {
19872109
let budget = disk_budget();

‎src/parseable/mod.rs‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ use crate::{
6565
},
6666
},
6767
},
68+
hottier::GLOBAL_HOTTIER,
6869
metadata::{LogStreamMetadata, SchemaVersion},
6970
metastore::{
7071
metastore_traits::Metastore, metastores::object_store_metastore::ObjectStoreMetastore,
@@ -1248,6 +1249,10 @@ impl Parseable {
12481249
}
12491250

12501251
pub async fn delete_tenant(&self, tenant_id: &str) -> Result<(), anyhow::Error> {
1252+
if let Some(hot_tier_manager) = GLOBAL_HOTTIER.get() {
1253+
hot_tier_manager.cleanup_tenant(tenant_id).await;
1254+
}
1255+
12511256
// delete users and sessions
12521257
let users = mut_users().remove(tenant_id);
12531258
if let Some(users) = users {
@@ -1272,6 +1277,9 @@ impl Parseable {
12721277
tenants.retain(|t| t != tenant_id);
12731278
}
12741279
TENANT_METADATA.delete_tenant(tenant_id);
1280+
if let Some(hot_tier_manager) = GLOBAL_HOTTIER.get() {
1281+
hot_tier_manager.finish_tenant_cleanup(tenant_id).await;
1282+
}
12751283
Ok(())
12761284
}
12771285

‎src/tenants/mod.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ impl TenantMetadata {
6666
self.tenants.get(tenant_id).map(|t| t.meta.clone())
6767
}
6868

69+
pub fn contains_tenant(&self, tenant_id: &str) -> bool {
70+
self.tenants.contains_key(tenant_id)
71+
}
72+
6973
#[allow(clippy::too_many_arguments)]
7074
pub fn update_tenant_meta(
7175
&self,

0 commit comments

Comments
 (0)