Skip to content

Commit a750098

Browse files
committed
per-stream hottier tasks
1 parent d080cce commit a750098

2 files changed

Lines changed: 89 additions & 56 deletions

File tree

src/handlers/http/logstream.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,9 @@ pub async fn put_stream_hot_tier(
455455
hot_tier_manager
456456
.put_hot_tier(&stream_name, &mut hottier, &tenant_id)
457457
.await?;
458+
hot_tier_manager
459+
.spawn_stream_tasks(stream_name.clone(), tenant_id.clone())
460+
.await;
458461

459462
let mut stream_metadata: ObjectStoreFormat = serde_json::from_slice(
460463
&PARSEABLE

src/hottier.rs

Lines changed: 86 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,16 @@ enum SyncPhase {
8181

8282
type StreamKey = (Option<String>, String);
8383

84+
struct StreamTasks {
85+
latest: tokio::task::JoinHandle<()>,
86+
historic: tokio::task::JoinHandle<()>,
87+
}
88+
8489
pub struct HotTierManager {
8590
filesystem: LocalFileSystem,
8691
hot_tier_path: &'static Path,
8792
state_cache: AsyncRwLock<HashMap<StreamKey, Arc<StreamSyncState>>>,
93+
tasks: AsyncRwLock<HashMap<StreamKey, StreamTasks>>,
8894
}
8995

9096
impl HotTierManager {
@@ -94,6 +100,7 @@ impl HotTierManager {
94100
filesystem: LocalFileSystem::new(),
95101
hot_tier_path,
96102
state_cache: AsyncRwLock::new(HashMap::new()),
103+
tasks: AsyncRwLock::new(HashMap::new()),
97104
}
98105
}
99106

@@ -388,6 +395,9 @@ impl HotTierManager {
388395
if !self.check_stream_hot_tier_exists(stream, tenant_id) {
389396
return Err(HotTierValidationError::NotFound(stream.to_owned()).into());
390397
}
398+
// Stop loops before tearing down the directory so no in-flight tick
399+
// re-creates files mid-delete.
400+
self.abort_stream_tasks(stream, tenant_id).await;
391401
let path = if let Some(tenant_id) = tenant_id.as_ref() {
392402
self.hot_tier_path.join(tenant_id).join(stream)
393403
} else {
@@ -434,11 +444,9 @@ impl HotTierManager {
434444
Ok(path)
435445
}
436446

437-
/// Spawn two independent loops, one per phase, so Latest and Historic
438-
/// tick clocks are independent. A long-running Historic backfill no
439-
/// longer delays Latest ticks. Within each phase, the next iteration
440-
/// starts only after the previous returns + a sleep, so a phase can
441-
/// never overlap itself.
447+
/// Discover hot-tier-enabled streams at boot and spawn a per-stream pair
448+
/// of (Latest, Historic) loops for each. New streams added later acquire
449+
/// their own loops via `spawn_stream_tasks` from the PUT hot-tier handler.
442450
pub fn download_from_s3<'a>(&'a self) -> Result<(), HotTierError>
443451
where
444452
'a: 'static,
@@ -451,74 +459,96 @@ impl HotTierManager {
451459
"hot tier scheduler starting"
452460
);
453461

462+
let this: &'static HotTierManager = self;
463+
tokio::spawn(async move {
464+
// pstats hot tier may need to be created on boot before any tasks
465+
// can pick it up.
466+
if let Err(e) = this.create_pstats_hot_tier().await {
467+
tracing::error!("Skipping pstats hot tier creation because of error: {e}");
468+
}
469+
let tenants = if let Some(tenants) = PARSEABLE.list_tenants() {
470+
tenants.into_iter().map(Some).collect::<Vec<_>>()
471+
} else {
472+
vec![None]
473+
};
474+
for tenant_id in tenants {
475+
for stream in PARSEABLE.streams.list(&tenant_id) {
476+
if this.check_stream_hot_tier_exists(&stream, &tenant_id) {
477+
this.spawn_stream_tasks(stream, tenant_id.clone()).await;
478+
}
479+
}
480+
}
481+
});
482+
Ok(())
483+
}
484+
485+
/// Spawn (Latest, Historic) loops for a single stream. Idempotent:
486+
/// if tasks already exist for this (tenant, stream), no-op.
487+
pub async fn spawn_stream_tasks(&'static self, stream: String, tenant_id: Option<String>) {
488+
let key: StreamKey = (tenant_id.clone(), stream.clone());
489+
{
490+
let tasks = self.tasks.read().await;
491+
if let Some(existing) = tasks.get(&key)
492+
&& !existing.latest.is_finished()
493+
&& !existing.historic.is_finished()
494+
{
495+
return;
496+
}
497+
}
498+
454499
let latest_interval = Duration::from_secs(60);
455-
let historic_interval = Duration::from_secs(historic_min as u64 * 60);
500+
let historic_interval = Duration::from_secs(
501+
PARSEABLE.options.hot_tier_historic_sync_minutes.max(1) as u64 * 60,
502+
);
456503

457-
let this = self;
458-
tokio::spawn(async move {
504+
warn!(stream = %stream, tenant = ?tenant_id, "spawning per-stream hot tier tasks");
505+
506+
let s = stream.clone();
507+
let t = tenant_id.clone();
508+
let latest = tokio::spawn(async move {
459509
loop {
460-
warn!(phase = ?SyncPhase::Latest, "hot tier tick fired");
461-
if let Err(err) = this.sync_hot_tier(SyncPhase::Latest).await {
462-
error!("Error in hot tier latest scheduler: {:?}", err);
510+
warn!(stream = %s, tenant = ?t, phase = ?SyncPhase::Latest, "stream tick fired");
511+
if let Err(err) = self
512+
.process_stream(s.clone(), t.clone(), SyncPhase::Latest)
513+
.await
514+
{
515+
error!(stream = %s, "latest sync error: {err:?}");
463516
}
464517
tokio::time::sleep(latest_interval).await;
465518
}
466519
});
467520

468-
let this = self;
469-
tokio::spawn(async move {
521+
let s = stream.clone();
522+
let t = tenant_id.clone();
523+
let historic = tokio::spawn(async move {
470524
loop {
471-
warn!(phase = ?SyncPhase::Historic, "hot tier tick fired");
472-
if let Err(err) = this.sync_hot_tier(SyncPhase::Historic).await {
473-
error!("Error in hot tier historic scheduler: {:?}", err);
525+
warn!(stream = %s, tenant = ?t, phase = ?SyncPhase::Historic, "stream tick fired");
526+
if let Err(err) = self
527+
.process_stream(s.clone(), t.clone(), SyncPhase::Historic)
528+
.await
529+
{
530+
error!(stream = %s, "historic sync error: {err:?}");
474531
}
475532
tokio::time::sleep(historic_interval).await;
476533
}
477534
});
478-
Ok(())
479-
}
480535

481-
/// sync the hot tier files from S3 to the hot tier directory for all streams
482-
async fn sync_hot_tier(&self, phase: SyncPhase) -> Result<(), HotTierError> {
483-
// Before syncing, check if pstats stream was created and needs hot tier
484-
if let Err(e) = self.create_pstats_hot_tier().await {
485-
tracing::trace!("Skipping pstats hot tier creation because of error: {e}");
536+
let mut tasks = self.tasks.write().await;
537+
if let Some(old) = tasks.insert(key, StreamTasks { latest, historic }) {
538+
old.latest.abort();
539+
old.historic.abort();
486540
}
541+
}
487542

488-
let mut sync_hot_tier_tasks = FuturesUnordered::new();
489-
let tenants = if let Some(tenants) = PARSEABLE.list_tenants() {
490-
tenants.into_iter().map(Some).collect()
491-
} else {
492-
vec![None]
493-
};
494-
let mut scheduled = 0usize;
495-
for tenant_id in tenants {
496-
for stream in PARSEABLE.streams.list(&tenant_id) {
497-
if self.check_stream_hot_tier_exists(&stream, &tenant_id) {
498-
sync_hot_tier_tasks.push(self.process_stream(
499-
stream,
500-
tenant_id.to_owned(),
501-
phase,
502-
));
503-
scheduled += 1;
504-
}
505-
}
506-
}
507-
warn!(phase = ?phase, num_streams = scheduled, "hot tier tick: streams scheduled");
508-
509-
let tick_start = std::time::Instant::now();
510-
while let Some(res) = sync_hot_tier_tasks.next().await {
511-
if let Err(err) = res {
512-
error!("Failed to run hot tier sync task {err:?}");
513-
return Err(err);
514-
}
543+
/// Abort and remove per-stream tasks. Caller must ensure no further work
544+
/// will be enqueued for the stream after this returns.
545+
async fn abort_stream_tasks(&self, stream: &str, tenant_id: &Option<String>) {
546+
let key: StreamKey = (tenant_id.clone(), stream.to_owned());
547+
if let Some(t) = self.tasks.write().await.remove(&key) {
548+
t.latest.abort();
549+
t.historic.abort();
550+
warn!(stream = %stream, tenant = ?tenant_id, "aborted per-stream hot tier tasks");
515551
}
516-
warn!(
517-
phase = ?phase,
518-
elapsed_ms = tick_start.elapsed().as_millis() as u64,
519-
"hot tier tick complete"
520-
);
521-
Ok(())
522552
}
523553

524554
/// process the hot tier files for the stream

0 commit comments

Comments
 (0)