Skip to content

Commit bd9a679

Browse files
committed
Hottier fix (parseablehq#1645)
* fix: hottier downloads Make hottier downloads streaming * replace streaming with parallel downloads * new task per range * expose new env vars * parallel file download per stream * concurrent writes instead of mutex * crash safety by using .partial file * separate out historic and latest hottier tasks * add logs * two loops instead of clockwerk * per-stream hottier tasks * fix: deepsource, coderabbit suggestions * hottier deletion bug * x86 build + other fixes * add traces to hottier * Updates: reduce object-store calls, limitstore for metastore * try hottier abort * fix: New runtime for hottier Running multiple parallel chunked downloads on the main runtime resulted in a slowdown in other incoming requests. This was mitigated by creating a new runtime for hottier downloads. * coderabbit + deepsource * server startup function
1 parent 8f19a7b commit bd9a679

22 files changed

Lines changed: 2391 additions & 346 deletions

src/cli.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,60 @@ pub struct Options {
314314
)]
315315
pub hot_tier_storage_path: Option<PathBuf>,
316316

317+
#[arg(
318+
long = "hot-tier-download-chunk-size",
319+
env = "P_HOT_TIER_DOWNLOAD_CHUNK_SIZE",
320+
value_parser = clap::value_parser!(u64).range(5242880..),
321+
default_value = "8388608",
322+
help = "Chunk size in bytes for parallel hot tier downloads (default 8 MiB)"
323+
)]
324+
pub hot_tier_download_chunk_size: u64,
325+
326+
#[arg(
327+
long = "hot-tier-download-concurrency",
328+
env = "P_HOT_TIER_DOWNLOAD_CONCURRENCY",
329+
value_parser = clap::value_parser!(u64).range(1..),
330+
default_value = "16",
331+
help = "Number of concurrent range requests per hot tier download"
332+
)]
333+
pub hot_tier_download_concurrency: u64,
334+
335+
#[arg(
336+
long = "hot-tier-files-per-stream-concurrency",
337+
env = "P_HOT_TIER_FILES_PER_STREAM_CONCURRENCY",
338+
value_parser = clap::value_parser!(u32).range(1..),
339+
default_value = "4",
340+
help = "Number of concurrent parquet file downloads per stream during hot tier sync"
341+
)]
342+
pub hot_tier_files_per_stream_concurrency: u32,
343+
344+
#[arg(
345+
long = "hot-tier-latest-minutes",
346+
env = "P_HOT_TIER_LATEST_MINUTES",
347+
value_parser = clap::value_parser!(u64).range(1..),
348+
default_value = "10",
349+
help = "Files whose timestamp is within the last N minutes are 'latest'; rest are 'historic'."
350+
)]
351+
pub hot_tier_latest_minutes: u64,
352+
353+
#[arg(
354+
long = "hot-tier-per-tick-cap",
355+
env = "P_HISTORIC_PER_TICK_CAP",
356+
value_parser = clap::value_parser!(u32).range(10..),
357+
default_value = "100",
358+
help = "Maximum files to download per historic tick."
359+
)]
360+
pub historic_per_tick_cap: u32,
361+
362+
#[arg(
363+
long = "hot-tier-historic-sync-minutes",
364+
env = "P_HOT_TIER_HISTORIC_SYNC_MINUTES",
365+
value_parser = clap::value_parser!(u32).range(1..),
366+
default_value = "5",
367+
help = "Interval (minutes) at which the historic hot-tier sync runs."
368+
)]
369+
pub hot_tier_historic_sync_minutes: u32,
370+
317371
//TODO: remove this when smart cache is implemented
318372
#[arg(
319373
long = "index-storage-path",

src/handlers/http/logstream.rs

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use self::error::StreamError;
2020
use super::cluster::utils::{IngestionStats, QueriedStats, StorageStats};
2121
use super::query::update_schema_when_distributed;
2222
use crate::event::format::override_data_type;
23-
use crate::hottier::{CURRENT_HOT_TIER_VERSION, HotTierManager, StreamHotTier};
23+
use crate::hottier::{CURRENT_HOT_TIER_VERSION, GLOBAL_HOTTIER, StreamHotTier};
2424
use crate::metadata::SchemaVersion;
2525
use crate::metrics::{EVENTS_INGESTED_DATE, EVENTS_INGESTED_SIZE_DATE, EVENTS_STORAGE_SIZE_DATE};
2626
use crate::parseable::{DEFAULT_TENANT, PARSEABLE, StreamNotFound};
@@ -47,7 +47,7 @@ use itertools::Itertools;
4747
use serde_json::{Value, json};
4848
use std::fs;
4949
use std::sync::Arc;
50-
use tracing::warn;
50+
use tracing::{Instrument, warn};
5151

5252
pub async fn delete(
5353
req: HttpRequest,
@@ -77,7 +77,7 @@ pub async fn delete(
7777
)
7878
}
7979

80-
if let Some(hot_tier_manager) = HotTierManager::global()
80+
if let Some(hot_tier_manager) = GLOBAL_HOTTIER.get()
8181
&& hot_tier_manager.check_stream_hot_tier_exists(&stream_name, &tenant_id)
8282
{
8383
hot_tier_manager
@@ -413,16 +413,25 @@ pub async fn get_stream_info(
413413
Ok((web::Json(stream_info), StatusCode::OK))
414414
}
415415

416+
#[tracing::instrument(
417+
name = "http.put_stream_hot_tier",
418+
skip(req, logstream, hottier),
419+
fields(stream = tracing::field::Empty, tenant = tracing::field::Empty, size = hottier.size)
420+
)]
416421
pub async fn put_stream_hot_tier(
417422
req: HttpRequest,
418423
logstream: Path<String>,
419424
Json(mut hottier): Json<StreamHotTier>,
420425
) -> Result<impl Responder, StreamError> {
421426
let stream_name = logstream.into_inner();
422427
let tenant_id = get_tenant_id_from_request(&req);
428+
let current_span = tracing::Span::current();
429+
current_span
430+
.record("stream", tracing::field::display(&stream_name))
431+
.record("tenant", tracing::field::debug(&tenant_id));
423432
// For query mode, if the stream not found in memory map,
424-
//check if it exists in the storage
425-
//create stream and schema from storage
433+
// check if it exists in the storage
434+
// create stream and schema from storage
426435
if !PARSEABLE
427436
.check_or_load_stream(&stream_name, &tenant_id)
428437
.await
@@ -441,16 +450,14 @@ pub async fn put_stream_hot_tier(
441450

442451
validator::hot_tier(&hottier.size.to_string())?;
443452

444-
// TODO tenants
445-
stream.set_hot_tier(Some(hottier.clone()));
446-
let Some(hot_tier_manager) = HotTierManager::global() else {
453+
let Some(hot_tier_manager) = GLOBAL_HOTTIER.get() else {
447454
return Err(StreamError::HotTierNotEnabled(stream_name));
448455
};
449456
let existing_hot_tier_used_size = hot_tier_manager
450457
.validate_hot_tier_size(&stream_name, hottier.size, &tenant_id)
451458
.await?;
452459
hottier.used_size = existing_hot_tier_used_size;
453-
hottier.available_size = hottier.size;
460+
hottier.available_size = hottier.size.saturating_sub(existing_hot_tier_used_size);
454461
hottier.version = Some(CURRENT_HOT_TIER_VERSION.to_string());
455462
hot_tier_manager
456463
.put_hot_tier(&stream_name, &mut hottier, &tenant_id)
@@ -469,19 +476,34 @@ pub async fn put_stream_hot_tier(
469476
.metastore
470477
.put_stream_json(&stream_metadata, &stream_name, &tenant_id)
471478
.await?;
479+
stream.set_hot_tier(Some(hottier.clone()));
480+
let stream = stream_name.clone();
481+
let tenant = tenant_id.clone();
482+
hot_tier_manager
483+
.spawn_stream_task(stream, tenant)
484+
.instrument(current_span)
485+
.await;
472486

473487
Ok((
474488
format!("hot tier set for stream {stream_name}"),
475489
StatusCode::OK,
476490
))
477491
}
478492

493+
#[tracing::instrument(
494+
name = "http.get_stream_hot_tier",
495+
skip(req, logstream),
496+
fields(stream = tracing::field::Empty, tenant = tracing::field::Empty)
497+
)]
479498
pub async fn get_stream_hot_tier(
480499
req: HttpRequest,
481500
logstream: Path<String>,
482501
) -> Result<impl Responder, StreamError> {
483502
let stream_name = logstream.into_inner();
484503
let tenant_id = get_tenant_id_from_request(&req);
504+
tracing::Span::current()
505+
.record("stream", tracing::field::display(&stream_name))
506+
.record("tenant", tracing::field::debug(&tenant_id));
485507
// For query mode, if the stream not found in memory map,
486508
//check if it exists in the storage
487509
//create stream and schema from storage
@@ -492,7 +514,7 @@ pub async fn get_stream_hot_tier(
492514
return Err(StreamNotFound(stream_name.clone()).into());
493515
}
494516

495-
let Some(hot_tier_manager) = HotTierManager::global() else {
517+
let Some(hot_tier_manager) = GLOBAL_HOTTIER.get() else {
496518
return Err(StreamError::HotTierNotEnabled(stream_name));
497519
};
498520
let meta = hot_tier_manager
@@ -502,12 +524,21 @@ pub async fn get_stream_hot_tier(
502524
Ok((web::Json(meta), StatusCode::OK))
503525
}
504526

527+
#[tracing::instrument(
528+
name = "http.delete_stream_hot_tier",
529+
skip(req, logstream),
530+
fields(stream = tracing::field::Empty, tenant = tracing::field::Empty)
531+
)]
505532
pub async fn delete_stream_hot_tier(
506533
req: HttpRequest,
507534
logstream: Path<String>,
508535
) -> Result<impl Responder, StreamError> {
509536
let stream_name = logstream.into_inner();
510537
let tenant_id = get_tenant_id_from_request(&req);
538+
let current_span = tracing::Span::current();
539+
current_span
540+
.record("stream", tracing::field::display(&stream_name))
541+
.record("tenant", tracing::field::debug(&tenant_id));
511542
// For query mode, if the stream not found in memory map,
512543
//check if it exists in the storage
513544
//create stream and schema from storage
@@ -529,12 +560,13 @@ pub async fn delete_stream_hot_tier(
529560
});
530561
}
531562

532-
let Some(hot_tier_manager) = HotTierManager::global() else {
563+
let Some(hot_tier_manager) = GLOBAL_HOTTIER.get() else {
533564
return Err(StreamError::HotTierNotEnabled(stream_name));
534565
};
535566

536567
hot_tier_manager
537568
.delete_hot_tier(&stream_name, &tenant_id)
569+
.instrument(tracing::Span::current())
538570
.await?;
539571

540572
let mut stream_metadata: ObjectStoreFormat = serde_json::from_slice(

src/handlers/http/modal/mod.rs

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ use crate::{
3838
alerts::{ALERTS, get_alert_manager, target::TARGETS},
3939
cli::Options,
4040
correlation::CORRELATIONS,
41-
hottier::{HotTierManager, StreamHotTier},
41+
hottier::{GLOBAL_HOTTIER, HotTierManager, StreamHotTier},
4242
metastore::metastore_traits::MetastoreObject,
4343
oauth::{OAuthProvider, connect_oidc},
4444
option::Mode,
@@ -160,6 +160,11 @@ pub trait ParseableServer {
160160
// Shutdown resource monitor
161161
let _ = resource_shutdown_tx.send(());
162162

163+
// Shutdown hottier
164+
if let Some(htm) = GLOBAL_HOTTIER.get() {
165+
htm.abort_all().await;
166+
}
167+
163168
// Initiate graceful shutdown
164169
info!("Graceful shutdown of HTTP server triggered");
165170
srv_handle.stop(true).await;
@@ -627,7 +632,7 @@ pub type PrismMetadata = NodeMetadata;
627632
/// in their stream metadata but don't have local hot tier metadata files yet.
628633
/// This function is called once during query server startup.
629634
pub async fn initialize_hot_tier_metadata_on_startup(
630-
hot_tier_manager: &HotTierManager,
635+
hot_tier_manager: &'static HotTierManager,
631636
) -> anyhow::Result<()> {
632637
// Collect hot tier configurations from streams before doing async operations
633638
let hot_tier_configs: Vec<(String, Option<String>, StreamHotTier)> = {
@@ -653,21 +658,6 @@ pub async fn initialize_hot_tier_metadata_on_startup(
653658
})
654659
})
655660
.collect()
656-
// let streams_guard = PARSEABLE.streams.read().unwrap();
657-
// streams_guard
658-
// .iter()
659-
// .filter_map(|(stream_name, stream)| {
660-
// // Skip if hot tier metadata file already exists for this stream
661-
// if hot_tier_manager.check_stream_hot_tier_exists(stream_name) {
662-
// return None;
663-
// }
664-
665-
// // Get the hot tier configuration from the in-memory stream metadata
666-
// stream
667-
// .get_hot_tier()
668-
// .map(|config| (stream_name.clone(), config))
669-
// })
670-
// .collect()
671661
};
672662

673663
for (stream_name, tenant_id, hot_tier_config) in hot_tier_configs {

src/handlers/http/modal/query/querier_logstream.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ use tracing::{error, warn};
3232
pub static CREATE_STREAM_LOCK: Mutex<()> = Mutex::const_new(());
3333

3434
use crate::handlers::http::middleware::{CLUSTER_SECRET, CLUSTER_SECRET_HEADER};
35+
use crate::hottier::GLOBAL_HOTTIER;
3536
use crate::parseable::DEFAULT_TENANT;
3637
use crate::utils::get_user_from_request;
3738
use crate::{
@@ -47,7 +48,6 @@ use crate::{
4748
modal::{NodeMetadata, NodeType},
4849
},
4950
},
50-
hottier::HotTierManager,
5151
parseable::{PARSEABLE, StreamNotFound},
5252
stats,
5353
storage::{ObjectStoreFormat, StreamType},
@@ -85,7 +85,7 @@ pub async fn delete(
8585
)
8686
}
8787

88-
if let Some(hot_tier_manager) = HotTierManager::global()
88+
if let Some(hot_tier_manager) = GLOBAL_HOTTIER.get()
8989
&& hot_tier_manager.check_stream_hot_tier_exists(&stream_name, &tenant_id)
9090
{
9191
hot_tier_manager

src/handlers/http/modal/query_server.rs

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,18 @@ use crate::handlers::http::middleware::{DisAllowRootUser, RouteExt};
2727
use crate::handlers::http::modal::initialize_hot_tier_metadata_on_startup;
2828
use crate::handlers::http::{base_path, prism_base_path};
2929
use crate::handlers::http::{rbac, role};
30+
use crate::hottier::GLOBAL_HOTTIER;
3031
use crate::hottier::HotTierManager;
32+
use crate::hottier::HotTierMessage;
33+
use crate::hottier::hottier_runtime;
3134
use crate::rbac::role::Action;
3235
use crate::{analytics, migration, storage, sync};
3336
use actix_web::web::{ServiceConfig, resource};
3437
use actix_web::{Scope, web};
3538
use actix_web_prometheus::PrometheusMetrics;
3639
use async_trait::async_trait;
3740
use bytes::Bytes;
41+
use tokio::sync::mpsc;
3842
use tokio::sync::{OnceCell, oneshot};
3943

4044
use crate::Server;
@@ -126,13 +130,29 @@ impl ParseableServer for QueryServer {
126130
analytics::init_analytics_scheduler()?;
127131
}
128132

129-
if let Some(hot_tier_manager) = HotTierManager::global() {
130-
// Initialize hot tier metadata files for streams that have hot tier configuration
131-
// but don't have local hot tier metadata files yet
132-
if let Err(e) = initialize_hot_tier_metadata_on_startup(hot_tier_manager).await {
133-
tracing::warn!("Failed to initialize hot tier metadata on startup: {}", e);
134-
}
135-
hot_tier_manager.download_from_s3()?;
133+
// Initialize hot tier metadata files for streams that have hot tier configuration
134+
// but don't have local hot tier metadata files yet
135+
if let Some(htm) = PARSEABLE
136+
.options
137+
.hot_tier_storage_path
138+
.as_ref()
139+
.map(|hot_tier_path| {
140+
// start hottier runtime
141+
let (sender, receiver): (
142+
mpsc::UnboundedSender<HotTierMessage>,
143+
mpsc::UnboundedReceiver<HotTierMessage>,
144+
) = mpsc::unbounded_channel();
145+
std::thread::spawn(|| hottier_runtime(receiver));
146+
147+
// set global hottier
148+
GLOBAL_HOTTIER.get_or_init(|| HotTierManager::new(hot_tier_path, sender))
149+
})
150+
{
151+
// init hottier meta
152+
if let Err(e) = initialize_hot_tier_metadata_on_startup(htm).await {
153+
tracing::error!("Unable to init hottier meta- {e}");
154+
};
155+
htm.start_all_tasks().await;
136156
};
137157

138158
// Run sync on a background thread

0 commit comments

Comments
 (0)