From 4e50143a8422df5185e028658cffff4c38a76b96 Mon Sep 17 00:00:00 2001 From: Anant Vindal Date: Wed, 15 Apr 2026 15:53:32 +0530 Subject: [PATCH 01/47] feat: Added a new trait to expose SchemaProvider --- src/lib.rs | 6 +++ src/query/mod.rs | 80 +++++++++++++++++++---------- src/query/stream_schema_provider.rs | 2 +- 3 files changed, 59 insertions(+), 29 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index de7ee4a5f..289494723 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,7 +58,12 @@ pub mod validator; use std::time::Duration; // Public re-exports of crates being used in enterprise +pub use arrow_array; +pub use arrow_flight; +pub use arrow_ipc; +pub use catalog as parseable_catalog; pub use datafusion; +pub use datafusion_proto; pub use handlers::http::modal::{ ParseableServer, ingest_server::IngestServer, query_server::QueryServer, server::Server, }; @@ -68,6 +73,7 @@ use parseable::PARSEABLE; use reqwest::{Client, ClientBuilder}; pub use {opentelemetry, opentelemetry_otlp, opentelemetry_proto, opentelemetry_sdk}; pub use {tracing_actix_web, tracing_opentelemetry, tracing_subscriber}; +pub use utils as parseable_utils; // It is very unlikely that panic will occur when dealing with locks. pub const LOCK_EXPECT: &str = "Thread shouldn't panic while holding a lock"; diff --git a/src/query/mod.rs b/src/query/mod.rs index e1df94c1a..e2ce0a1c9 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -25,6 +25,7 @@ use arrow_schema::SchemaRef; use chrono::NaiveDateTime; use chrono::{DateTime, Duration, Utc}; use datafusion::arrow::record_batch::RecordBatch; +use datafusion::catalog::SchemaProvider; use datafusion::common::tree_node::Transformed; use datafusion::execution::disk_manager::DiskManager; use datafusion::execution::{ @@ -45,7 +46,7 @@ use datafusion::sql::sqlparser::dialect::PostgreSqlDialect; use futures::Stream; use futures::stream::select_all; use itertools::Itertools; -use once_cell::sync::Lazy; +use once_cell::sync::{Lazy, OnceCell}; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; use std::ops::Bound; @@ -57,7 +58,6 @@ use sysinfo::System; use tokio::runtime::Runtime; use self::error::ExecuteError; -use self::stream_schema_provider::GlobalSchemaProvider; pub use self::stream_schema_provider::PartialTimeFilter; use crate::alerts::alert_structs::Conditions; use crate::alerts::alerts_utils::get_filter_string; @@ -70,7 +70,8 @@ use crate::handlers::http::query::QueryError; use crate::metrics::increment_bytes_scanned_in_query_by_date; use crate::option::Mode; use crate::parseable::{DEFAULT_TENANT, PARSEABLE}; -use crate::storage::{ObjectStorageProvider, ObjectStoreFormat}; +use crate::query::stream_schema_provider::GlobalSchemaProvider; +use crate::storage::{ObjectStorage, ObjectStorageProvider, ObjectStoreFormat}; use crate::utils::time::TimeRange; /// Boxed record-batch stream used as the streaming half of query results. @@ -95,6 +96,7 @@ type QueryResult = Result<(Either, BoxedBatchStream>, Vec = // Lazy::new(|| Query::create_session_context(PARSEABLE.storage())); +pub static SCHEMA_PROVIDER: OnceCell> = OnceCell::new(); pub static QUERY_SESSION_STATE: Lazy = Lazy::new(|| Query::create_session_state(PARSEABLE.storage())); @@ -110,6 +112,15 @@ pub static QUERY_SESSION: Lazy = Lazy::new(|| { } }); +/// Trait to enable implementation of SchemaProvider +pub trait ParseableSchemaProvider: Send + Sync { + fn new_provider( + &self, + storage: Option>, + tenant_id: &Option, + ) -> Box; +} + pub struct InMemorySessionContext { session_context: Arc>, } @@ -124,18 +135,23 @@ impl InMemorySessionContext { } pub fn add_schema(&self, tenant_id: &str) { + let schema_provider = if let Some(provider) = SCHEMA_PROVIDER.get() { + provider.new_provider( + Some(PARSEABLE.storage().get_object_store()), + &Some(tenant_id.to_owned()), + ) + } else { + Box::new(GlobalSchemaProvider { + storage: PARSEABLE.storage().get_object_store(), + tenant_id: Some(tenant_id.to_owned()), + }) + }; self.session_context .write() .expect("SessionContext should be writeable") .catalog("datafusion") .expect("Default catalog should be available") - .register_schema( - tenant_id, - Arc::new(GlobalSchemaProvider { - storage: PARSEABLE.storage().get_object_store(), - tenant_id: Some(tenant_id.to_owned()), - }), - ) + .register_schema(tenant_id, schema_provider.into()) .expect("Should be able to register new schema"); } @@ -184,29 +200,41 @@ impl Query { // register multiple schemas if let Some(tenants) = PARSEABLE.list_tenants() { for t in tenants.iter() { - let schema_provider = Arc::new(GlobalSchemaProvider { - storage: storage.get_object_store(), - tenant_id: Some(t.clone()), - }); - let _ = catalog.register_schema(t, schema_provider); + let schema_provider = if let Some(provider) = SCHEMA_PROVIDER.get() { + provider.new_provider( + Some(PARSEABLE.storage().get_object_store()), + &Some(t.to_owned()), + ) + } else { + Box::new(GlobalSchemaProvider { + storage: PARSEABLE.storage().get_object_store(), + tenant_id: Some(t.to_owned()), + }) + }; + let _ = catalog.register_schema(t, schema_provider.into()); } } } else { // register just one schema - let schema_provider = Arc::new(GlobalSchemaProvider { - storage: storage.get_object_store(), - tenant_id: None, - }); + let schema_provider = if let Some(provider) = SCHEMA_PROVIDER.get() { + provider.new_provider(Some(PARSEABLE.storage().get_object_store()), &None) + } else { + Box::new(GlobalSchemaProvider { + storage: PARSEABLE.storage().get_object_store(), + tenant_id: None, + }) + }; + let _ = catalog.register_schema( &state.config_options().catalog.default_schema, - schema_provider, + schema_provider.into(), ); } SessionContext::new_with_state(state) } - fn create_session_state(storage: Arc) -> SessionState { + pub fn create_session_state(storage: Arc) -> SessionState { let runtime_config = storage .get_datafusion_runtime() .with_disk_manager_builder(DiskManager::builder()); @@ -288,14 +316,10 @@ impl Query { return Ok((Either::Left(vec![]), fields)); } - let plan = QUERY_SESSION - .get_ctx() - .state() - .create_physical_plan(df.logical_plan()) - .await?; + let plan = ctx.state().create_physical_plan(df.logical_plan()).await?; let results = if !is_streaming { - let task_ctx = QUERY_SESSION.get_ctx().task_ctx(); + let task_ctx = ctx.task_ctx(); let batches = collect_partitioned(plan.clone(), task_ctx.clone()) .await? @@ -311,7 +335,7 @@ impl Query { Either::Left(batches) } else { - let task_ctx = QUERY_SESSION.get_ctx().task_ctx(); + let task_ctx = ctx.task_ctx(); let output_partitions = plan.output_partitioning().partition_count(); diff --git a/src/query/stream_schema_provider.rs b/src/query/stream_schema_provider.rs index 6d4005972..698d63a4c 100644 --- a/src/query/stream_schema_provider.rs +++ b/src/query/stream_schema_provider.rs @@ -702,7 +702,7 @@ pub enum PartialTimeFilter { } impl PartialTimeFilter { - fn try_from_expr(expr: &Expr, time_partition: &Option) -> Option { + pub fn try_from_expr(expr: &Expr, time_partition: &Option) -> Option { let Expr::BinaryExpr(binexpr) = expr else { return None; }; From 2a23cedaf833a5ab5eafc685f25f5b0e4c8390bc Mon Sep 17 00:00:00 2001 From: Anant Vindal Date: Mon, 4 May 2026 12:00:33 +0530 Subject: [PATCH 02/47] rebase with main --- Cargo.lock | 40 ++++++ Cargo.toml | 1 + src/metrics/mod.rs | 23 ++++ src/query/mod.rs | 22 ++- src/query/stream_schema_provider.rs | 200 +++++++++++++++------------- src/utils/arrow/flight.rs | 25 ++++ 6 files changed, 217 insertions(+), 94 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f43e17068..2a19cd0c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2120,6 +2120,45 @@ dependencies = [ "tokio", ] +[[package]] +name = "datafusion-proto" +version = "53.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a387aaef949dc16bb6abc81bd1af850ec7449183aef011214f9724957495738" +dependencies = [ + "arrow", + "chrono", + "datafusion-catalog", + "datafusion-catalog-listing", + "datafusion-common", + "datafusion-datasource", + "datafusion-datasource-arrow", + "datafusion-datasource-csv", + "datafusion-datasource-json", + "datafusion-datasource-parquet", + "datafusion-execution", + "datafusion-expr", + "datafusion-functions-table", + "datafusion-physical-expr", + "datafusion-physical-expr-common", + "datafusion-physical-plan", + "datafusion-proto-common", + "object_store", + "prost 0.14.1", + "rand 0.9.4", +] + +[[package]] +name = "datafusion-proto-common" +version = "53.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e614c7c53a9c304c6a850b821010bb492e57300311835f1180613f9d2c63d9" +dependencies = [ + "arrow", + "datafusion-common", + "prost 0.14.1", +] + [[package]] name = "datafusion-pruning" version = "53.1.0" @@ -3896,6 +3935,7 @@ dependencies = [ "crossterm", "dashmap", "datafusion", + "datafusion-proto", "derive_more 1.0.0", "erased-serde", "fs_extra", diff --git a/Cargo.toml b/Cargo.toml index 326ffae3f..14fe8b051 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ object_store = { version = "0.13.1", features = [ "azure", "gcp", ] } +datafusion-proto = "53.1.0" parquet = "58.0.0" # Web server and HTTP-related diff --git a/src/metrics/mod.rs b/src/metrics/mod.rs index 9c9ff86fa..4c61f2f7c 100644 --- a/src/metrics/mod.rs +++ b/src/metrics/mod.rs @@ -257,6 +257,18 @@ pub static TOTAL_QUERY_CALLS_BY_DATE: Lazy = Lazy::new(|| { .expect("metric can be created") }); +pub static TOTAL_FILES_SCANNED_IN_HOTTIER_BY_DATE: Lazy = Lazy::new(|| { + IntCounterVec::new( + Opts::new( + "total_files_scanned_in_hottier_by_date", + "Total files scanned in hottier by date", + ) + .namespace(METRICS_NAMESPACE), + &["stream", "date", "tenant_id"], + ) + .expect("metric can be created") +}); + pub static TOTAL_FILES_SCANNED_IN_QUERY_BY_DATE: Lazy = Lazy::new(|| { IntCounterVec::new( Opts::new( @@ -683,6 +695,17 @@ pub fn increment_files_scanned_in_query_by_date(count: u64, date: &str, tenant_i .inc_by(count); } +pub fn increment_files_scanned_in_hottier_by_date( + count: u64, + date: &str, + tenant_id: &str, + stream_name: &str, +) { + TOTAL_FILES_SCANNED_IN_HOTTIER_BY_DATE + .with_label_values(&[stream_name, date, tenant_id]) + .inc_by(count); +} + pub fn increment_bytes_scanned_in_query_by_date(bytes: u64, date: &str, tenant_id: &str) { TOTAL_BYTES_SCANNED_IN_QUERY_BY_DATE .with_label_values(&[date, tenant_id]) diff --git a/src/query/mod.rs b/src/query/mod.rs index e2ce0a1c9..20ebd9dcc 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -98,6 +98,12 @@ type QueryResult = Result<(Either, BoxedBatchStream>, Vec> = OnceCell::new(); +/// Additional physical optimizer rules registered by enterprise/plugins. +/// Must be populated BEFORE `QUERY_SESSION_STATE` is first accessed. +pub static ADDITIONAL_PHYSICAL_OPTIMIZER_RULES: Lazy< + RwLock>>, +> = Lazy::new(|| RwLock::new(Vec::new())); + pub static QUERY_SESSION_STATE: Lazy = Lazy::new(|| Query::create_session_state(PARSEABLE.storage())); @@ -280,11 +286,19 @@ impl Query { .parquet .schema_force_view_types = true; - SessionStateBuilder::new() + let mut builder = SessionStateBuilder::new() .with_default_features() .with_config(config) - .with_runtime_env(runtime) - .build() + .with_runtime_env(runtime); + + // Append any additional physical optimizer rules (e.g., enterprise partial agg pushdown) + if let Ok(rules) = ADDITIONAL_PHYSICAL_OPTIMIZER_RULES.read() { + for rule in rules.iter() { + builder = builder.with_physical_optimizer_rule(Arc::clone(rule)); + } + } + + builder.build() } /// this function returns the result of the query @@ -316,6 +330,8 @@ impl Query { return Ok((Either::Left(vec![]), fields)); } + let ctx = QUERY_SESSION.get_ctx(); + let plan = ctx.state().create_physical_plan(df.logical_plan()).await?; let results = if !is_streaming { diff --git a/src/query/stream_schema_provider.rs b/src/query/stream_schema_provider.rs index 698d63a4c..59daa4a88 100644 --- a/src/query/stream_schema_provider.rs +++ b/src/query/stream_schema_provider.rs @@ -56,7 +56,10 @@ use crate::{ }, event::DEFAULT_TIMESTAMP_KEY, hottier::HotTierManager, - metrics::{QUERY_CACHE_HIT, increment_files_scanned_in_query_by_date}, + metrics::{ + QUERY_CACHE_HIT, increment_files_scanned_in_hottier_by_date, + increment_files_scanned_in_query_by_date, + }, option::Mode, parseable::{DEFAULT_TENANT, PARSEABLE, STREAM_EXISTS}, storage::{ObjectStorage, ObjectStoreFormat}, @@ -205,6 +208,13 @@ impl StandardTableProvider { .await .map_err(|err| DataFusionError::External(Box::new(err)))?; + increment_files_scanned_in_hottier_by_date( + hot_tier_files.len() as u64, + &chrono::Utc::now().date_naive().to_string(), + self.tenant_id.as_deref().unwrap_or(DEFAULT_TENANT), + &self.stream, + ); + let hot_tier_files: Vec = hot_tier_files .into_iter() .map(|mut file| { @@ -352,101 +362,108 @@ impl StandardTableProvider { &self, manifest_files: Vec, ) -> (Vec>, datafusion::common::Statistics) { - let target_partition: usize = num_cpus::get(); - let mut partitioned_files = Vec::from_iter((0..target_partition).map(|_| Vec::new())); - let mut column_statistics = HashMap::>::new(); - let mut count = 0; - let mut file_count = 0u64; - for (index, file) in manifest_files - .into_iter() - .enumerate() - .map(|(x, y)| (x % target_partition, y)) + partitioned_files(&self.schema, &self.tenant_id, manifest_files) + } +} + +#[inline(always)] +pub fn partitioned_files( + schema: &SchemaRef, + tenant_id: &Option, + manifest_files: Vec, +) -> (Vec>, datafusion::common::Statistics) { + let target_partition: usize = num_cpus::get(); + let mut partitioned_files = Vec::from_iter((0..target_partition).map(|_| Vec::new())); + let mut column_statistics = HashMap::>::new(); + let mut count = 0; + let mut file_count = 0u64; + for (index, file) in manifest_files + .into_iter() + .enumerate() + .map(|(x, y)| (x % target_partition, y)) + { + #[allow(unused_mut)] + let File { + mut file_path, + num_rows, + columns, + .. + } = file; + + // Track billing metrics for files scanned in query + file_count += 1; + + // object_store::path::Path doesn't automatically deal with Windows path separators + // to do that, we are using from_absolute_path() which takes into consideration the underlying filesystem + // before sending the file path to PartitionedFile + // the github issue- https://github.com/parseablehq/parseable/issues/824 + // For some reason, the `from_absolute_path()` doesn't work for macos, hence the ugly solution + // TODO: figure out an elegant solution to this + #[cfg(windows)] { - #[allow(unused_mut)] - let File { - mut file_path, - num_rows, - columns, - .. - } = file; - - // Track billing metrics for files scanned in query - file_count += 1; - - // object_store::path::Path doesn't automatically deal with Windows path separators - // to do that, we are using from_absolute_path() which takes into consideration the underlying filesystem - // before sending the file path to PartitionedFile - // the github issue- https://github.com/parseablehq/parseable/issues/824 - // For some reason, the `from_absolute_path()` doesn't work for macos, hence the ugly solution - // TODO: figure out an elegant solution to this - #[cfg(windows)] - { - if PARSEABLE.storage.name() == "drive" { - file_path = object_store::path::Path::from_absolute_path(file_path) - .unwrap() - .to_string(); - } + if PARSEABLE.storage.name() == "drive" { + file_path = object_store::path::Path::from_absolute_path(file_path) + .unwrap() + .to_string(); } - let pf = PartitionedFile::new(file_path, file.file_size); - partitioned_files[index].push(pf); - - columns.into_iter().for_each(|col| { - column_statistics - .entry(col.name) - .and_modify(|x| { - if let Some((stats, col_stats)) = x.as_ref().cloned().zip(col.stats.clone()) - { - // update() returns None on type mismatch (e.g. column - // historically written as both Utf8 and Timestamp(ms)). - // Dropping to None here makes the planner skip min/max - // pushdown for this column instead of crashing the worker. - *x = stats.update(col_stats); - } - }) - .or_insert_with(|| col.stats.as_ref().cloned()); - }); - count += num_rows; } - let statistics = self - .schema - .fields() - .iter() - .map(|field| { - column_statistics - .get(field.name()) - .and_then(|stats| stats.as_ref()) - .and_then(|stats| stats.clone().min_max_as_scalar(field.data_type())) - .map(|(min, max)| datafusion::common::ColumnStatistics { - null_count: Precision::Absent, - max_value: Precision::Exact(max), - min_value: Precision::Exact(min), - distinct_count: Precision::Absent, - sum_value: Precision::Absent, - byte_size: Precision::Absent, - }) - .unwrap_or_default() - }) - .collect(); + let pf = PartitionedFile::new(file_path, file.file_size); + partitioned_files[index].push(pf); + + columns.into_iter().for_each(|col| { + column_statistics + .entry(col.name) + .and_modify(|x| { + if let Some((stats, col_stats)) = x.as_ref().cloned().zip(col.stats.clone()) { + // update() returns None on type mismatch (e.g. column + // historically written as both Utf8 and Timestamp(ms)). + // Dropping to None here makes the planner skip min/max + // pushdown for this column instead of crashing the worker. + *x = stats.update(col_stats); + } + }) + .or_insert_with(|| col.stats.as_ref().cloned()); + }); + count += num_rows; + } + let statistics = schema + .fields() + .iter() + .map(|field| { + column_statistics + .get(field.name()) + .and_then(|stats| stats.as_ref()) + .and_then(|stats| stats.clone().min_max_as_scalar(field.data_type())) + .map(|(min, max)| datafusion::common::ColumnStatistics { + null_count: Precision::Absent, + max_value: Precision::Exact(max), + min_value: Precision::Exact(min), + distinct_count: Precision::Absent, + sum_value: Precision::Absent, + byte_size: Precision::Absent, + }) + .unwrap_or_default() + }) + .collect(); - let statistics = datafusion::common::Statistics { - num_rows: Precision::Exact(count as usize), - total_byte_size: Precision::Absent, - column_statistics: statistics, - }; + let statistics = datafusion::common::Statistics { + num_rows: Precision::Exact(count as usize), + total_byte_size: Precision::Absent, + column_statistics: statistics, + }; - // Track billing metrics for query scan - let current_date = chrono::Utc::now().date_naive().to_string(); - increment_files_scanned_in_query_by_date( - file_count, - ¤t_date, - self.tenant_id.as_deref().unwrap_or(DEFAULT_TENANT), - ); + // Track billing metrics for query scan + let current_date = chrono::Utc::now().date_naive().to_string(); + increment_files_scanned_in_query_by_date( + file_count, + ¤t_date, + tenant_id.as_deref().unwrap_or(DEFAULT_TENANT), + ); - (partitioned_files, statistics) - } + (partitioned_files, statistics) } -async fn collect_from_snapshot( +pub async fn collect_from_snapshot( snapshot: &Snapshot, time_filters: &[PartialTimeFilter], filters: &[Expr], @@ -683,7 +700,8 @@ impl TableProvider for StandardTableProvider { } } -fn reversed_mem_table( +#[inline(always)] +pub fn reversed_mem_table( mut records: Vec, schema: Arc, ) -> Result { @@ -863,7 +881,7 @@ pub fn is_within_staging_window(time_filters: &[PartialTimeFilter]) -> bool { !has_upper_bound } -fn expr_in_boundary(filter: &Expr) -> bool { +pub fn expr_in_boundary(filter: &Expr) -> bool { let Expr::BinaryExpr(binexpr) = filter else { return false; }; @@ -881,7 +899,7 @@ fn expr_in_boundary(filter: &Expr) -> bool { ) } -fn extract_timestamp_bound( +pub fn extract_timestamp_bound( binexpr: &BinaryExpr, time_partition: &Option, ) -> Option<(Operator, NaiveDateTime)> { diff --git a/src/utils/arrow/flight.rs b/src/utils/arrow/flight.rs index 481f6c56a..787afae5a 100644 --- a/src/utils/arrow/flight.rs +++ b/src/utils/arrow/flight.rs @@ -143,6 +143,31 @@ fn lit_timestamp_milli(time: i64) -> Expr { Expr::Literal(ScalarValue::TimestampMillisecond(Some(time), None), None) } +/// Streaming variant of into_flight_data. Converts a DataFusion record batch +/// stream directly into Flight data without materializing all batches in memory. +pub fn into_flight_data_stream( + stream: datafusion::execution::SendableRecordBatchStream, +) -> Result, Box> { + let record_stream = stream.map_err(|e| { + arrow_flight::error::FlightError::Arrow(arrow_schema::ArrowError::ExternalError( + Box::new(e), + )) + }); + + let write_options = IpcWriteOptions::default() + .try_with_compression(Some(arrow_ipc::CompressionType(1))) + .map_err(|err| Status::failed_precondition(err.to_string()))?; + + let flight_data_stream = FlightDataEncoderBuilder::new() + .with_max_flight_data_size(usize::MAX) + .with_options(write_options) + .build(record_stream); + + let flight_data_stream = flight_data_stream.map_err(|err| Status::unknown(err.to_string())); + + Ok(Response::new(Box::pin(flight_data_stream) as DoGetStream)) +} + pub fn into_flight_data(records: Vec) -> Result, Box> { let input_stream = futures::stream::iter(records.into_iter().map(Ok)); let write_options = IpcWriteOptions::default() From c6be14c15b6d46ee4bd7a6270d5fdc5674d1c16d Mon Sep 17 00:00:00 2001 From: Nikhil Sinha <131262146+nikhilsinhaparseable@users.noreply.github.com> Date: Wed, 6 May 2026 11:59:41 +0530 Subject: [PATCH 03/47] update Cargo.toml for release v2.7.2 (#1642) --- Cargo.lock | 2 +- Cargo.toml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2a19cd0c0..771270353 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3905,7 +3905,7 @@ dependencies = [ [[package]] name = "parseable" -version = "2.7.1" +version = "2.7.2" dependencies = [ "actix-cors", "actix-web", diff --git a/Cargo.toml b/Cargo.toml index 14fe8b051..aa95af50a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "parseable" -version = "2.7.1" +version = "2.7.2" authors = ["Parseable Team "] edition = "2024" rust-version = "1.88.0" @@ -205,8 +205,8 @@ arrow = "58.0.0" temp-dir = "0.1.14" [package.metadata.parseable_ui] -assets-url = "https://parseable-prism-build.s3.us-east-2.amazonaws.com/v2.7.0/build.zip" -assets-sha1 = "ef1330a92c8a760b8b3449dd181326aae588c722" +assets-url = "https://parseable-prism-build.s3.us-east-2.amazonaws.com/v2.7.2/build.zip" +assets-sha1 = "541746bd89b41528477a57a2f6f066dce145dd73" [features] debug = [] From 64a34bad725ef5140f1aa5c7a78056a0da76ab03 Mon Sep 17 00:00:00 2001 From: Nitish Tiwari Date: Wed, 6 May 2026 15:27:27 +0530 Subject: [PATCH 04/47] update helm chart to v2.7.2 (#1643) --- helm-releases/parseable-2.7.2.tgz | Bin 0 -> 52673 bytes helm-releases/parseable-enterprise-2.7.2.tgz | Bin 0 -> 56830 bytes helm/Chart.yaml | 4 +- helm/values.yaml | 2 +- index.yaml | 212 ++++++++++++------- 5 files changed, 134 insertions(+), 84 deletions(-) create mode 100644 helm-releases/parseable-2.7.2.tgz create mode 100644 helm-releases/parseable-enterprise-2.7.2.tgz diff --git a/helm-releases/parseable-2.7.2.tgz b/helm-releases/parseable-2.7.2.tgz new file mode 100644 index 0000000000000000000000000000000000000000..4e5e98e8ebe31505d47cc041b370bc5510bc8698 GIT binary patch literal 52673 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 z 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%0D|o;!@Rd~de$h#%sJdHTNM@7Cx4 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�c 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 literal 0 HcmV?d00001 diff --git a/helm-releases/parseable-enterprise-2.7.2.tgz b/helm-releases/parseable-enterprise-2.7.2.tgz new file mode 100644 index 0000000000000000000000000000000000000000..caa1cae5260c44abd62dff45e5c4be7eea9644e4 GIT binary patch literal 56830 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 Date: Wed, 6 May 2026 18:33:13 +0530 Subject: [PATCH 05/47] add quantile values to known field list (#1644) --- src/otel/metrics.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/otel/metrics.rs b/src/otel/metrics.rs index 99e51a62f..4e5f1d054 100644 --- a/src/otel/metrics.rs +++ b/src/otel/metrics.rs @@ -31,7 +31,7 @@ use super::otel_utils::{ convert_epoch_nano_to_timestamp, insert_attributes, insert_number_if_some, }; -pub const OTEL_METRICS_KNOWN_FIELD_LIST: [&str; 34] = [ +pub const OTEL_METRICS_KNOWN_FIELD_LIST: [&str; 36] = [ "metric_name", "metric_description", "metric_unit", @@ -46,6 +46,8 @@ pub const OTEL_METRICS_KNOWN_FIELD_LIST: [&str; 34] = [ "data_point_sum", "data_point_bucket_counts", "data_point_explicit_bounds", + "data_point_quantile_values_quantile", + "data_point_quantile_values_value", "data_point_scale", "data_point_zero_count", "data_point_flags", From bd9a6794205533b0e53e099d2077d49f0595c3a1 Mon Sep 17 00:00:00 2001 From: parmesant Date: Fri, 15 May 2026 02:09:36 -0700 Subject: [PATCH 06/47] Hottier fix (#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 --- src/cli.rs | 54 + src/handlers/http/logstream.rs | 54 +- src/handlers/http/modal/mod.rs | 24 +- .../http/modal/query/querier_logstream.rs | 4 +- src/handlers/http/modal/query_server.rs | 34 +- src/handlers/http/modal/server.rs | 32 +- src/hottier.rs | 1502 ++++++++++++++--- src/metastore/metastore_traits.rs | 8 +- .../metastores/object_store_metastore.rs | 128 +- src/metrics/mod.rs | 44 + src/parseable/mod.rs | 17 +- src/query/stream_schema_provider.rs | 4 +- src/storage/azure_blob.rs | 187 +- src/storage/gcs.rs | 192 ++- src/storage/localfs.rs | 22 + src/storage/metrics_layer.rs | 41 +- src/storage/mod.rs | 55 +- src/storage/object_storage.rs | 55 + src/storage/s3.rs | 203 ++- src/sync.rs | 60 +- src/telemetry.rs | 5 +- src/utils/mod.rs | 12 +- 22 files changed, 2391 insertions(+), 346 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 969c3a619..50f1202c9 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -314,6 +314,60 @@ pub struct Options { )] pub hot_tier_storage_path: Option, + #[arg( + long = "hot-tier-download-chunk-size", + env = "P_HOT_TIER_DOWNLOAD_CHUNK_SIZE", + value_parser = clap::value_parser!(u64).range(5242880..), + default_value = "8388608", + help = "Chunk size in bytes for parallel hot tier downloads (default 8 MiB)" + )] + pub hot_tier_download_chunk_size: u64, + + #[arg( + long = "hot-tier-download-concurrency", + env = "P_HOT_TIER_DOWNLOAD_CONCURRENCY", + value_parser = clap::value_parser!(u64).range(1..), + default_value = "16", + help = "Number of concurrent range requests per hot tier download" + )] + pub hot_tier_download_concurrency: u64, + + #[arg( + long = "hot-tier-files-per-stream-concurrency", + env = "P_HOT_TIER_FILES_PER_STREAM_CONCURRENCY", + value_parser = clap::value_parser!(u32).range(1..), + default_value = "4", + help = "Number of concurrent parquet file downloads per stream during hot tier sync" + )] + pub hot_tier_files_per_stream_concurrency: u32, + + #[arg( + long = "hot-tier-latest-minutes", + env = "P_HOT_TIER_LATEST_MINUTES", + value_parser = clap::value_parser!(u64).range(1..), + default_value = "10", + help = "Files whose timestamp is within the last N minutes are 'latest'; rest are 'historic'." + )] + pub hot_tier_latest_minutes: u64, + + #[arg( + long = "hot-tier-per-tick-cap", + env = "P_HISTORIC_PER_TICK_CAP", + value_parser = clap::value_parser!(u32).range(10..), + default_value = "100", + help = "Maximum files to download per historic tick." + )] + pub historic_per_tick_cap: u32, + + #[arg( + long = "hot-tier-historic-sync-minutes", + env = "P_HOT_TIER_HISTORIC_SYNC_MINUTES", + value_parser = clap::value_parser!(u32).range(1..), + default_value = "5", + help = "Interval (minutes) at which the historic hot-tier sync runs." + )] + pub hot_tier_historic_sync_minutes: u32, + //TODO: remove this when smart cache is implemented #[arg( long = "index-storage-path", diff --git a/src/handlers/http/logstream.rs b/src/handlers/http/logstream.rs index 86f83329c..9fc2ab299 100644 --- a/src/handlers/http/logstream.rs +++ b/src/handlers/http/logstream.rs @@ -20,7 +20,7 @@ use self::error::StreamError; use super::cluster::utils::{IngestionStats, QueriedStats, StorageStats}; use super::query::update_schema_when_distributed; use crate::event::format::override_data_type; -use crate::hottier::{CURRENT_HOT_TIER_VERSION, HotTierManager, StreamHotTier}; +use crate::hottier::{CURRENT_HOT_TIER_VERSION, GLOBAL_HOTTIER, StreamHotTier}; use crate::metadata::SchemaVersion; use crate::metrics::{EVENTS_INGESTED_DATE, EVENTS_INGESTED_SIZE_DATE, EVENTS_STORAGE_SIZE_DATE}; use crate::parseable::{DEFAULT_TENANT, PARSEABLE, StreamNotFound}; @@ -47,7 +47,7 @@ use itertools::Itertools; use serde_json::{Value, json}; use std::fs; use std::sync::Arc; -use tracing::warn; +use tracing::{Instrument, warn}; pub async fn delete( req: HttpRequest, @@ -77,7 +77,7 @@ pub async fn delete( ) } - if let Some(hot_tier_manager) = HotTierManager::global() + if let Some(hot_tier_manager) = GLOBAL_HOTTIER.get() && hot_tier_manager.check_stream_hot_tier_exists(&stream_name, &tenant_id) { hot_tier_manager @@ -413,6 +413,11 @@ pub async fn get_stream_info( Ok((web::Json(stream_info), StatusCode::OK)) } +#[tracing::instrument( + name = "http.put_stream_hot_tier", + skip(req, logstream, hottier), + fields(stream = tracing::field::Empty, tenant = tracing::field::Empty, size = hottier.size) +)] pub async fn put_stream_hot_tier( req: HttpRequest, logstream: Path, @@ -420,9 +425,13 @@ pub async fn put_stream_hot_tier( ) -> Result { let stream_name = logstream.into_inner(); let tenant_id = get_tenant_id_from_request(&req); + let current_span = tracing::Span::current(); + current_span + .record("stream", tracing::field::display(&stream_name)) + .record("tenant", tracing::field::debug(&tenant_id)); // For query mode, if the stream not found in memory map, - //check if it exists in the storage - //create stream and schema from storage + // check if it exists in the storage + // create stream and schema from storage if !PARSEABLE .check_or_load_stream(&stream_name, &tenant_id) .await @@ -441,16 +450,14 @@ pub async fn put_stream_hot_tier( validator::hot_tier(&hottier.size.to_string())?; - // TODO tenants - stream.set_hot_tier(Some(hottier.clone())); - let Some(hot_tier_manager) = HotTierManager::global() else { + let Some(hot_tier_manager) = GLOBAL_HOTTIER.get() else { return Err(StreamError::HotTierNotEnabled(stream_name)); }; let existing_hot_tier_used_size = hot_tier_manager .validate_hot_tier_size(&stream_name, hottier.size, &tenant_id) .await?; hottier.used_size = existing_hot_tier_used_size; - hottier.available_size = hottier.size; + hottier.available_size = hottier.size.saturating_sub(existing_hot_tier_used_size); hottier.version = Some(CURRENT_HOT_TIER_VERSION.to_string()); hot_tier_manager .put_hot_tier(&stream_name, &mut hottier, &tenant_id) @@ -469,6 +476,13 @@ pub async fn put_stream_hot_tier( .metastore .put_stream_json(&stream_metadata, &stream_name, &tenant_id) .await?; + stream.set_hot_tier(Some(hottier.clone())); + let stream = stream_name.clone(); + let tenant = tenant_id.clone(); + hot_tier_manager + .spawn_stream_task(stream, tenant) + .instrument(current_span) + .await; Ok(( format!("hot tier set for stream {stream_name}"), @@ -476,12 +490,20 @@ pub async fn put_stream_hot_tier( )) } +#[tracing::instrument( + name = "http.get_stream_hot_tier", + skip(req, logstream), + fields(stream = tracing::field::Empty, tenant = tracing::field::Empty) +)] pub async fn get_stream_hot_tier( req: HttpRequest, logstream: Path, ) -> Result { let stream_name = logstream.into_inner(); let tenant_id = get_tenant_id_from_request(&req); + tracing::Span::current() + .record("stream", tracing::field::display(&stream_name)) + .record("tenant", tracing::field::debug(&tenant_id)); // For query mode, if the stream not found in memory map, //check if it exists in the storage //create stream and schema from storage @@ -492,7 +514,7 @@ pub async fn get_stream_hot_tier( return Err(StreamNotFound(stream_name.clone()).into()); } - let Some(hot_tier_manager) = HotTierManager::global() else { + let Some(hot_tier_manager) = GLOBAL_HOTTIER.get() else { return Err(StreamError::HotTierNotEnabled(stream_name)); }; let meta = hot_tier_manager @@ -502,12 +524,21 @@ pub async fn get_stream_hot_tier( Ok((web::Json(meta), StatusCode::OK)) } +#[tracing::instrument( + name = "http.delete_stream_hot_tier", + skip(req, logstream), + fields(stream = tracing::field::Empty, tenant = tracing::field::Empty) +)] pub async fn delete_stream_hot_tier( req: HttpRequest, logstream: Path, ) -> Result { let stream_name = logstream.into_inner(); let tenant_id = get_tenant_id_from_request(&req); + let current_span = tracing::Span::current(); + current_span + .record("stream", tracing::field::display(&stream_name)) + .record("tenant", tracing::field::debug(&tenant_id)); // For query mode, if the stream not found in memory map, //check if it exists in the storage //create stream and schema from storage @@ -529,12 +560,13 @@ pub async fn delete_stream_hot_tier( }); } - let Some(hot_tier_manager) = HotTierManager::global() else { + let Some(hot_tier_manager) = GLOBAL_HOTTIER.get() else { return Err(StreamError::HotTierNotEnabled(stream_name)); }; hot_tier_manager .delete_hot_tier(&stream_name, &tenant_id) + .instrument(tracing::Span::current()) .await?; let mut stream_metadata: ObjectStoreFormat = serde_json::from_slice( diff --git a/src/handlers/http/modal/mod.rs b/src/handlers/http/modal/mod.rs index 77622aca5..6e6bb370a 100644 --- a/src/handlers/http/modal/mod.rs +++ b/src/handlers/http/modal/mod.rs @@ -38,7 +38,7 @@ use crate::{ alerts::{ALERTS, get_alert_manager, target::TARGETS}, cli::Options, correlation::CORRELATIONS, - hottier::{HotTierManager, StreamHotTier}, + hottier::{GLOBAL_HOTTIER, HotTierManager, StreamHotTier}, metastore::metastore_traits::MetastoreObject, oauth::{OAuthProvider, connect_oidc}, option::Mode, @@ -160,6 +160,11 @@ pub trait ParseableServer { // Shutdown resource monitor let _ = resource_shutdown_tx.send(()); + // Shutdown hottier + if let Some(htm) = GLOBAL_HOTTIER.get() { + htm.abort_all().await; + } + // Initiate graceful shutdown info!("Graceful shutdown of HTTP server triggered"); srv_handle.stop(true).await; @@ -627,7 +632,7 @@ pub type PrismMetadata = NodeMetadata; /// in their stream metadata but don't have local hot tier metadata files yet. /// This function is called once during query server startup. pub async fn initialize_hot_tier_metadata_on_startup( - hot_tier_manager: &HotTierManager, + hot_tier_manager: &'static HotTierManager, ) -> anyhow::Result<()> { // Collect hot tier configurations from streams before doing async operations let hot_tier_configs: Vec<(String, Option, StreamHotTier)> = { @@ -653,21 +658,6 @@ pub async fn initialize_hot_tier_metadata_on_startup( }) }) .collect() - // let streams_guard = PARSEABLE.streams.read().unwrap(); - // streams_guard - // .iter() - // .filter_map(|(stream_name, stream)| { - // // Skip if hot tier metadata file already exists for this stream - // if hot_tier_manager.check_stream_hot_tier_exists(stream_name) { - // return None; - // } - - // // Get the hot tier configuration from the in-memory stream metadata - // stream - // .get_hot_tier() - // .map(|config| (stream_name.clone(), config)) - // }) - // .collect() }; for (stream_name, tenant_id, hot_tier_config) in hot_tier_configs { diff --git a/src/handlers/http/modal/query/querier_logstream.rs b/src/handlers/http/modal/query/querier_logstream.rs index 80c674706..121fc1fca 100644 --- a/src/handlers/http/modal/query/querier_logstream.rs +++ b/src/handlers/http/modal/query/querier_logstream.rs @@ -32,6 +32,7 @@ use tracing::{error, warn}; pub static CREATE_STREAM_LOCK: Mutex<()> = Mutex::const_new(()); use crate::handlers::http::middleware::{CLUSTER_SECRET, CLUSTER_SECRET_HEADER}; +use crate::hottier::GLOBAL_HOTTIER; use crate::parseable::DEFAULT_TENANT; use crate::utils::get_user_from_request; use crate::{ @@ -47,7 +48,6 @@ use crate::{ modal::{NodeMetadata, NodeType}, }, }, - hottier::HotTierManager, parseable::{PARSEABLE, StreamNotFound}, stats, storage::{ObjectStoreFormat, StreamType}, @@ -85,7 +85,7 @@ pub async fn delete( ) } - if let Some(hot_tier_manager) = HotTierManager::global() + if let Some(hot_tier_manager) = GLOBAL_HOTTIER.get() && hot_tier_manager.check_stream_hot_tier_exists(&stream_name, &tenant_id) { hot_tier_manager diff --git a/src/handlers/http/modal/query_server.rs b/src/handlers/http/modal/query_server.rs index af1910ccd..0d6941cb8 100644 --- a/src/handlers/http/modal/query_server.rs +++ b/src/handlers/http/modal/query_server.rs @@ -27,7 +27,10 @@ use crate::handlers::http::middleware::{DisAllowRootUser, RouteExt}; use crate::handlers::http::modal::initialize_hot_tier_metadata_on_startup; use crate::handlers::http::{base_path, prism_base_path}; use crate::handlers::http::{rbac, role}; +use crate::hottier::GLOBAL_HOTTIER; use crate::hottier::HotTierManager; +use crate::hottier::HotTierMessage; +use crate::hottier::hottier_runtime; use crate::rbac::role::Action; use crate::{analytics, migration, storage, sync}; use actix_web::web::{ServiceConfig, resource}; @@ -35,6 +38,7 @@ use actix_web::{Scope, web}; use actix_web_prometheus::PrometheusMetrics; use async_trait::async_trait; use bytes::Bytes; +use tokio::sync::mpsc; use tokio::sync::{OnceCell, oneshot}; use crate::Server; @@ -126,13 +130,29 @@ impl ParseableServer for QueryServer { analytics::init_analytics_scheduler()?; } - if let Some(hot_tier_manager) = HotTierManager::global() { - // Initialize hot tier metadata files for streams that have hot tier configuration - // but don't have local hot tier metadata files yet - if let Err(e) = initialize_hot_tier_metadata_on_startup(hot_tier_manager).await { - tracing::warn!("Failed to initialize hot tier metadata on startup: {}", e); - } - hot_tier_manager.download_from_s3()?; + // Initialize hot tier metadata files for streams that have hot tier configuration + // but don't have local hot tier metadata files yet + if let Some(htm) = PARSEABLE + .options + .hot_tier_storage_path + .as_ref() + .map(|hot_tier_path| { + // start hottier runtime + let (sender, receiver): ( + mpsc::UnboundedSender, + mpsc::UnboundedReceiver, + ) = mpsc::unbounded_channel(); + std::thread::spawn(|| hottier_runtime(receiver)); + + // set global hottier + GLOBAL_HOTTIER.get_or_init(|| HotTierManager::new(hot_tier_path, sender)) + }) + { + // init hottier meta + if let Err(e) = initialize_hot_tier_metadata_on_startup(htm).await { + tracing::error!("Unable to init hottier meta- {e}"); + }; + htm.start_all_tasks().await; }; // Run sync on a background thread diff --git a/src/handlers/http/modal/server.rs b/src/handlers/http/modal/server.rs index 9a7b4ea69..6a0309075 100644 --- a/src/handlers/http/modal/server.rs +++ b/src/handlers/http/modal/server.rs @@ -27,13 +27,15 @@ use crate::handlers::http::demo_data::get_demo_data; use crate::handlers::http::health_check; use crate::handlers::http::max_event_payload_size; use crate::handlers::http::middleware::IntraClusterRequest; -use crate::handlers::http::modal::initialize_hot_tier_metadata_on_startup; use crate::handlers::http::prism_base_path; use crate::handlers::http::query; use crate::handlers::http::targets; use crate::handlers::http::users::dashboards; use crate::handlers::http::users::filters; +use crate::hottier::GLOBAL_HOTTIER; use crate::hottier::HotTierManager; +use crate::hottier::HotTierMessage; +use crate::hottier::hottier_runtime; use crate::metrics; use crate::migration; use crate::storage; @@ -49,6 +51,7 @@ use actix_web_prometheus::PrometheusMetrics; use actix_web_static_files::ResourceFiles; use async_trait::async_trait; use bytes::Bytes; +use tokio::sync::mpsc; use tokio::sync::oneshot; use crate::{ @@ -132,14 +135,25 @@ impl ParseableServer for Server { // local sync on init thread::spawn(sync_start); - if let Some(hot_tier_manager) = HotTierManager::global() { - // Initialize hot tier metadata files for streams that have hot tier configuration - // but don't have local hot tier metadata files yet - if let Err(e) = initialize_hot_tier_metadata_on_startup(hot_tier_manager).await { - tracing::warn!("Failed to initialize hot tier metadata on startup: {}", e); - } - hot_tier_manager.download_from_s3()?; - }; + if let Some(htm) = PARSEABLE + .options + .hot_tier_storage_path + .as_ref() + .map(|hot_tier_path| { + // start hottier runtime + let (sender, receiver): ( + mpsc::UnboundedSender, + mpsc::UnboundedReceiver, + ) = mpsc::unbounded_channel(); + std::thread::spawn(|| hottier_runtime(receiver)); + + // set global hottier + + GLOBAL_HOTTIER.get_or_init(|| HotTierManager::new(hot_tier_path, sender)) + }) + { + htm.start_all_tasks().await; + } // Run sync on a background thread let (cancel_tx, cancel_rx) = oneshot::channel(); diff --git a/src/hottier.rs b/src/hottier.rs index c472f91c5..78b610738 100644 --- a/src/hottier.rs +++ b/src/hottier.rs @@ -16,23 +16,25 @@ * */ +use datafusion::common::HashSet; use std::{ - collections::BTreeMap, + collections::{BTreeMap, HashMap}, io, path::{Path, PathBuf}, + sync::{Arc, OnceLock}, }; +use tokio::sync::{Mutex as AsyncMutex, RwLock as AsyncRwLock, mpsc}; use crate::{ catalog::manifest::{File, Manifest}, handlers::http::cluster::PMETA_STREAM_NAME, parseable::PARSEABLE, - storage::{ObjectStorageError, field_stats::DATASET_STATS_STREAM_NAME}, + storage::{ObjectStorageError, StreamType, field_stats::DATASET_STATS_STREAM_NAME}, tenants::TENANT_METADATA, utils::{extract_datetime, human_size::bytes_to_human_size}, validator::error::HotTierValidationError, }; -use chrono::NaiveDate; -use clokwerk::{AsyncScheduler, Interval, Job}; +use chrono::{DateTime, NaiveDate, Timelike, Utc}; use futures::{StreamExt, TryStreamExt, stream::FuturesUnordered}; use futures_util::TryFutureExt; use object_store::{ObjectStoreExt, local::LocalFileSystem}; @@ -42,13 +44,34 @@ use relative_path::RelativePathBuf; use std::time::Duration; use sysinfo::Disks; use tokio::fs::{self, DirEntry}; -use tokio::io::AsyncWriteExt; use tokio_stream::wrappers::ReadDirStream; -use tracing::{error, warn}; +use tracing::{Instrument, error, info}; + +pub enum HotTierMessage { + StartTask(StreamKey), + KillTask(StreamKey), + // KillAll, + StartAll, +} + +pub static GLOBAL_HOTTIER: OnceLock = OnceLock::new(); + +pub static HOTTIER_RUNTIME: OnceCell<( + mpsc::UnboundedSender, + mpsc::UnboundedReceiver, +)> = OnceCell::new(); + +/// Floor a timestamp to the start of its minute (seconds + sub-second zeroed). +/// Used to produce a stable per-tick anchor so all spans within one tick share +/// the same cutoff value. +fn floor_to_minute(ts: DateTime) -> DateTime { + ts.with_second(0) + .and_then(|t| t.with_nanosecond(0)) + .unwrap_or(ts) +} pub const STREAM_HOT_TIER_FILENAME: &str = ".hot_tier.json"; pub const MIN_STREAM_HOT_TIER_SIZE_BYTES: u64 = 10737418240; // 10 GiB -const HOT_TIER_SYNC_DURATION: Interval = clokwerk::Interval::Minutes(1); pub const INTERNAL_STREAM_HOT_TIER_SIZE_BYTES: u64 = 10485760; //10 MiB pub const CURRENT_HOT_TIER_VERSION: &str = "v2"; @@ -65,32 +88,455 @@ pub struct StreamHotTier { pub oldest_date_time_entry: Option, } +/// Per-stream in-memory bookkeeping. Mutex protects concurrent reservation, +/// commit, and per-date manifest writes. Downloads run outside the lock. +struct StreamSyncState { + sht: AsyncMutex, + /// Past-date keys (e.g. `date=2026-05-11`) whose local file count is + /// known to match the S3 manifest count. Historic phase skips fetching + /// these. Populated after a tick observes `local_count >= s3_count`. + /// In-memory only; rebuilt on restart. + completed_dates: AsyncRwLock>, +} + +/// Hot-tier sync runs in two phases. Latest pulls files newer than +/// `hot_tier_latest_minutes` ago and may evict historic to make room. +/// Historic pulls older files, runs less often, never triggers eviction. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +enum SyncPhase { + Latest, + Historic, +} + +pub type StreamKey = (Option, String); +pub type HotTierResponse = Result<(), HotTierError>; + +struct StreamTasks { + latest: tokio::task::JoinHandle<()>, + historic: tokio::task::JoinHandle<()>, +} + pub struct HotTierManager { filesystem: LocalFileSystem, hot_tier_path: &'static Path, + state_cache: AsyncRwLock>>, + tasks: AsyncRwLock>, + sender: mpsc::UnboundedSender, +} + +#[tokio::main(flavor = "multi_thread")] +pub async fn hottier_runtime( + mut receiver: mpsc::UnboundedReceiver, + // sender: mpsc::UnboundedSender, +) { + while let Some(msg) = receiver.recv().await { + match msg { + HotTierMessage::StartTask((tenant_id, stream)) => { + tokio::spawn(async move { + if let Some(htm) = GLOBAL_HOTTIER.get() { + htm.spawn_stream_task_inner(stream, tenant_id).await; + } + }); + } + HotTierMessage::KillTask((tenant_id, stream)) => { + if let Some(htm) = GLOBAL_HOTTIER.get() { + htm.abort_stream_tasks(&stream, &tenant_id).await; + let path = if let Some(tenant_id) = tenant_id.as_ref() { + htm.hot_tier_path.join(tenant_id).join(&stream) + } else { + htm.hot_tier_path.join(&stream) + }; + let _ = fs::remove_dir_all(path) + .await + .map_err(|e| { + error!( + stream = %stream, + tenant = ?tenant_id, + error = ?e + ); + e + }) + .map_err(|e| { + error!( + stream=?stream, + tenant_id=?tenant_id, + error=?e, + "kill task" + ) + }); + htm.invalidate_state(&stream, &tenant_id).await; + } + } + HotTierMessage::StartAll => { + let htm = GLOBAL_HOTTIER.get().unwrap(); + + let startup_span = tracing::info_span!("hottier.startup.bootstrap"); + let span = startup_span.clone(); + tokio::spawn( + async move { + // pstats hot tier may need to be created on boot before any tasks + // can pick it up. + if let Err(e) = htm.create_pstats_hot_tier().await { + tracing::error!("Skipping pstats hot tier creation because of error: {e}"); + } + let tenants = if let Some(tenants) = PARSEABLE.list_tenants() { + tenants.into_iter().map(Some).collect::>() + } else { + vec![None] + }; + for tenant_id in tenants { + for stream in PARSEABLE.streams.list(&tenant_id) { + if htm.check_stream_hot_tier_exists(&stream, &tenant_id) { + let tenant_id = tenant_id.clone(); + + tokio::spawn( + async move { + htm.spawn_stream_task_inner(stream, tenant_id).await; + } + .instrument(span.clone()), + ); + tokio::time::sleep(Duration::from_secs(2)).await; + } else { + // check for potential orphan directory on disk + let path = if let Some(tenant_id) = tenant_id.as_ref() { + htm.hot_tier_path.join(tenant_id).join(stream) + } else { + htm.hot_tier_path.join(stream) + }; + if path.exists() { + // delete this entire folder as stream meta says no hottier for stream + if let Err(e) = fs::remove_dir_all(&path).await { + tracing::error!( + "Unable to remove orphaned hottier dir- `{path:?}` with error- {e}" + ); + }; + } + } + } + } + } + .instrument(startup_span.clone()), + ); + } + } + } } impl HotTierManager { - pub fn new(hot_tier_path: &'static Path) -> Self { + pub fn new( + hot_tier_path: &'static Path, + sender: mpsc::UnboundedSender, + ) -> Self { std::fs::create_dir_all(hot_tier_path).unwrap(); HotTierManager { filesystem: LocalFileSystem::new(), hot_tier_path, + state_cache: AsyncRwLock::new(HashMap::new()), + tasks: AsyncRwLock::new(HashMap::new()), + sender, } } - /// Get a global - pub fn global() -> Option<&'static HotTierManager> { - static INSTANCE: OnceCell = OnceCell::new(); + #[tracing::instrument(name = "hottier.startup", skip(self))] + pub async fn start_all_tasks(&'static self) { + let _ = tokio::spawn(async move { + self.sender.send(HotTierMessage::StartAll).unwrap(); + }) + .instrument(tracing::Span::current()) + .await; + } - PARSEABLE - .options - .hot_tier_storage_path - .as_ref() - .map(|hot_tier_path| INSTANCE.get_or_init(|| HotTierManager::new(hot_tier_path))) + /// Lazy-load and cache the `StreamHotTier` for a (tenant, stream) pair. + /// All sync-path mutations should acquire `state.sht.lock()`. + async fn get_or_load_state( + &self, + stream: &str, + tenant_id: &Option, + ) -> Result, HotTierError> { + let key: StreamKey = (tenant_id.clone(), stream.to_owned()); + { + if let Some(state) = self.state_cache.read().await.get(&key).cloned() { + return Ok(state); + } + } + // key not present, reconcile + let sht = self.reconcile_stream(stream, tenant_id).await?; + let state = Arc::new(StreamSyncState { + sht: AsyncMutex::new(sht), + completed_dates: AsyncRwLock::new(HashSet::new()), + }); + + { + let mut cache = self.state_cache.write().await; + if cache.insert(key, state.clone()).is_some() { + tracing::warn!( + "Key- {:?} was absent during read lock but already exists after reconcile!", + (tenant_id, stream), + ); + }; + } + Ok(state) + } + + /// Drop cached state for a stream (used after delete). + pub async fn invalidate_state(&self, stream: &str, tenant_id: &Option) { + let key: StreamKey = (tenant_id.clone(), stream.to_owned()); + { + self.state_cache.write().await.remove(&key); + } + } + + /// Walk the on-disk hot-tier directory for a stream and bring it into + /// agreement with `hottier.manifest.json` files. Removes `.partial` + /// orphans, drops manifest entries whose files are missing or wrong size, + /// deletes parquet files that exist but are not in their date manifest, + /// then recomputes `used_size` / `available_size` from the cleaned + /// manifests and persists the updated `StreamHotTier`. + #[tracing::instrument( + name = "hottier.reconcile_stream", + skip(self), + fields(stream = %stream, tenant = ?tenant_id), + err + )] + async fn reconcile_stream( + &self, + stream: &str, + tenant_id: &Option, + ) -> Result { + let mut sht = self.get_hot_tier(stream, tenant_id).await?; + let dates = self.fetch_hot_tier_dates(stream, tenant_id).await?; + let mut total_used: u64 = 0; + let mut partials_removed = 0usize; + let mut entries_dropped = 0usize; + let mut orphans_removed = 0usize; + + for date in dates { + let date_dir = self.get_stream_path_for_date(stream, &date, tenant_id); + if !date_dir.exists() { + continue; + } + + let mut on_disk: HashSet = HashSet::new(); + + // Pass 1: collect on-disk parquet files (drop .partial orphans). + self.drop_partials( + &mut on_disk, + &date_dir, + &mut partials_removed, + stream, + tenant_id, + ) + .await?; + + // Pass 2: clean manifest of stale entries. + let mut keep_names: HashSet = HashSet::new(); + self.clean_manifest( + &mut keep_names, + &date_dir, + &mut total_used, + &mut entries_dropped, + stream, + tenant_id, + ) + .await?; + + // Pass 3: delete on-disk parquet files not referenced by the cleaned manifest. + for name in on_disk.difference(&keep_names) { + let p = date_dir.join(name); + let _ = fs::remove_file(&p).await; + orphans_removed += 1; + info!( + stream = %stream, + tenant = ?tenant_id, + file = %p.display(), + "reconcile: deleted orphan parquet not in manifest" + ); + } + } + + sht.used_size = total_used; + sht.available_size = sht.size.saturating_sub(total_used); + self.put_hot_tier(stream, &mut sht, tenant_id).await?; + info!( + stream = %stream, + tenant = ?tenant_id, + partials_removed, + entries_dropped, + orphans_removed, + used = sht.used_size, + available = sht.available_size, + "reconcile done" + ); + Ok(sht) + } + + #[tracing::instrument( + name = "hottier.drop_partials", + skip(self, on_disk, partials_removed), + fields(stream = %stream, tenant = ?tenant_id, date_dir = %date_dir.display()) + )] + async fn drop_partials( + &self, + on_disk: &mut HashSet, + date_dir: &PathBuf, + partials_removed: &mut usize, + stream: &str, + tenant_id: &Option, + ) -> Result<(), HotTierError> { + let mut stack: Vec = vec![date_dir.clone()]; + while let Some(dir) = stack.pop() { + let mut entries = fs::read_dir(&dir).await.map_err(|e| { + error!( + stream = %stream, + tenant = ?tenant_id, + dir = ?dir, + error = ?e + ); + e + })?; + while let Some(entry) = entries.next_entry().await.map_err(|e| { + error!( + stream = %stream, + tenant = ?tenant_id, + error = ?e + ); + e + })? { + let p = entry.path(); + let ft = entry.file_type().await.map_err(|e| { + error!( + stream = %stream, + tenant = ?tenant_id, + entry = ?entry, + error = ?e + ); + e + })?; + if ft.is_dir() { + stack.push(p); + continue; + } + let Some(name_os) = p.file_name() else { + continue; + }; + let name = name_os.to_string_lossy(); + if name.ends_with(".partial") { + let _ = fs::remove_file(&p).await; + *partials_removed += 1; + info!( + stream = %stream, + tenant = ?tenant_id, + path = %p.display(), + "reconcile: deleted partial orphan" + ); + continue; + } + if name.ends_with(".manifest.json") { + continue; + } + if !ft.is_file() { + continue; + } + if let Ok(rel) = p.strip_prefix(date_dir) { + on_disk.insert(rel.to_string_lossy().into_owned()); + } + } + } + Ok(()) + } + + #[tracing::instrument( + name = "hottier.clean_manifest", + skip(self, keep_names, total_used, entries_dropped), + fields(stream = %stream, tenant = ?tenant_id, date_dir = %date_dir.display()) + )] + async fn clean_manifest( + &self, + keep_names: &mut HashSet, + date_dir: &PathBuf, + total_used: &mut u64, + entries_dropped: &mut usize, + stream: &str, + tenant_id: &Option, + ) -> Result<(), HotTierError> { + let manifest_path = date_dir.join("hottier.manifest.json"); + let mut manifest: Manifest = if manifest_path.exists() { + let bytes = fs::read(&manifest_path).await.map_err(|e| { + error!( + stream = %stream, + tenant = ?tenant_id, + manifest_path = ?manifest_path, + error = ?e + ); + e + })?; + serde_json::from_slice(&bytes).unwrap_or_default() + } else { + Manifest::default() + }; + + let mut kept = Vec::with_capacity(manifest.files.len()); + for f in manifest.files.drain(..) { + let local = self.hot_tier_path.join(&f.file_path); + let ok = match fs::metadata(&local).await { + Ok(m) => m.len() == f.file_size, + Err(_) => false, + }; + if ok { + if let Ok(rel) = local.strip_prefix(date_dir) { + keep_names.insert(rel.to_string_lossy().into_owned()); + } + *total_used += f.file_size; + kept.push(f); + } else { + let _ = fs::remove_file(&local).await; + *entries_dropped += 1; + info!( + stream = %stream, + tenant = ?tenant_id, + file = %f.file_path, + "reconcile: dropped manifest entry (file missing or wrong size)" + ); + } + } + kept.sort_by_key(|f| f.file_path.clone()); + manifest.files = kept; + + if manifest_path.exists() || !manifest.files.is_empty() { + fs::create_dir_all(&date_dir).await?; + fs::write( + &manifest_path, + serde_json::to_vec(&manifest).map_err(|e| { + error!( + stream = %stream, + tenant = ?tenant_id, + mainfest_path = ?manifest_path, + error = ?e + ); + e + })?, + ) + .await + .map_err(|e| { + error!( + stream = %stream, + tenant = ?tenant_id, + manifest_path = ?manifest_path, + error = ?e + ); + e + })?; + } + Ok(()) } /// get the total hot tier size for all streams + #[tracing::instrument( + name = "hottier.get_hot_tiers_size", + skip(self), + fields(current_stream = %current_stream, current_tenant = ?current_tenant_id), + err + )] pub async fn get_hot_tiers_size( &self, current_stream: &str, @@ -122,6 +568,12 @@ impl HotTierManager { /// check disk usage and hot tier size of all other streams /// check if total hot tier size of all streams is less than max disk usage /// delete all the files from hot tier once validation is successful and hot tier is ready to be updated + #[tracing::instrument( + name = "hottier.validate_size", + skip(self), + fields(stream = %stream, tenant = ?tenant_id, size = stream_hot_tier_size), + err + )] pub async fn validate_hot_tier_size( &self, stream: &str, @@ -184,6 +636,12 @@ impl HotTierManager { } /// get the hot tier metadata file for the stream + #[tracing::instrument( + name = "hottier.get_hot_tier", + skip(self), + fields(stream = %stream, tenant = ?tenant_id), + err + )] pub async fn get_hot_tier( &self, stream: &str, @@ -206,26 +664,41 @@ impl HotTierManager { Ok(stream_hot_tier) } + #[tracing::instrument( + name = "hottier.delete_hot_tier", + skip(self), + fields(stream = %stream, tenant = ?tenant_id), + err + )] pub async fn delete_hot_tier( - &self, + &'static self, stream: &str, tenant_id: &Option, ) -> Result<(), HotTierError> { if !self.check_stream_hot_tier_exists(stream, tenant_id) { return Err(HotTierValidationError::NotFound(stream.to_owned()).into()); } - let path = if let Some(tenant_id) = tenant_id.as_ref() { - self.hot_tier_path.join(tenant_id).join(stream) - } else { - self.hot_tier_path.join(stream) - }; - fs::remove_dir_all(path).await?; + let stream_name = stream.to_owned(); + let tenant = tenant_id.to_owned(); + let _ = tokio::spawn(async move { + self.sender + .send(HotTierMessage::KillTask((tenant, stream_name))) + .unwrap(); + }) + .instrument(tracing::Span::current()) + .await; Ok(()) } /// put the hot tier metadata file for the stream /// set the updated_date_range in the hot tier metadata file + #[tracing::instrument( + name = "hottier.put_hot_tier", + skip(self, hot_tier), + fields(stream = %stream, tenant = ?tenant_id, size = hot_tier.size), + err + )] pub async fn put_hot_tier( &self, stream: &str, @@ -259,216 +732,729 @@ impl HotTierManager { Ok(path) } - /// schedule the download of the hot tier files from S3 every minute - pub fn download_from_s3<'a>(&'a self) -> Result<(), HotTierError> - where - 'a: 'static, - { - let mut scheduler = AsyncScheduler::new(); - scheduler - .every(HOT_TIER_SYNC_DURATION) - .plus(Interval::Seconds(5)) - .run(move || async { - if let Err(err) = self.sync_hot_tier().await { - error!("Error in hot tier scheduler: {:?}", err); - } - }); - - tokio::spawn(async move { - loop { - scheduler.run_pending().await; - tokio::time::sleep(Duration::from_secs(10)).await; + #[tracing::instrument(name = "hottier.abort", skip(self))] + pub async fn abort_all(&self) { + { + let guard = self.tasks.write().await; + for (streamkey, task) in guard.iter() { + task.latest.abort(); + task.historic.abort(); + info!("aborted hot tier tasks for- {streamkey:?}"); } - }); - Ok(()) + } + } + + #[tracing::instrument( + name = "hottier.spawn_stream_task", + skip(self), + fields(stream = %stream, tenant = ?tenant_id) + )] + pub async fn spawn_stream_task(&'static self, stream: String, tenant_id: Option) { + let _ = tokio::spawn(async move { + self.sender + .send(HotTierMessage::StartTask((tenant_id, stream))) + .unwrap(); + }) + .instrument(tracing::Span::current()) + .await; } - /// sync the hot tier files from S3 to the hot tier directory for all streams - async fn sync_hot_tier(&self) -> Result<(), HotTierError> { - // Before syncing, check if pstats stream was created and needs hot tier - if let Err(e) = self.create_pstats_hot_tier().await { - tracing::trace!("Skipping pstats hot tier creation because of error: {e}"); + /// Spawn (Latest, Historic) loops for a single stream. Idempotent: + /// if tasks already exist for this (tenant, stream), no-op. + async fn spawn_stream_task_inner(&'static self, stream: String, tenant_id: Option) { + let key: StreamKey = (tenant_id.clone(), stream.clone()); + { + let tasks = self.tasks.read().await; + if let Some(existing) = tasks.get(&key) + && !existing.latest.is_finished() + && !existing.historic.is_finished() + { + return; + } } - let mut sync_hot_tier_tasks = FuturesUnordered::new(); - let tenants = if let Some(tenants) = PARSEABLE.list_tenants() { - tenants.into_iter().map(Some).collect() - } else { - vec![None] - }; - for tenant_id in tenants { - for stream in PARSEABLE.streams.list(&tenant_id) { - if self.check_stream_hot_tier_exists(&stream, &tenant_id) { - sync_hot_tier_tasks.push(self.process_stream(stream, tenant_id.to_owned())); + let latest_interval = Duration::from_secs(30); + let historic_interval = + Duration::from_secs(PARSEABLE.options.hot_tier_historic_sync_minutes as u64 * 60); + + info!(stream = %stream, tenant = ?tenant_id, "spawning per-stream hot tier tasks"); + + let s = stream.clone(); + let t = tenant_id.clone(); + let latest = tokio::spawn(async move { + loop { + let anchor = floor_to_minute(Utc::now()); + let tick_span = tracing::info_span!( + "hottier.tick", + stream = %s, + tenant = ?t, + phase = "latest", + anchor = %anchor + ); + async { + if let Err(err) = self + .process_stream(s.clone(), t.clone(), SyncPhase::Latest, anchor) + .await + { + error!("latest sync error: {err:?}"); + } } + .instrument(tick_span) + .await; + tokio::time::sleep(latest_interval).await; + } + }); + + let s = stream.clone(); + let t = tenant_id.clone(); + let historic = tokio::spawn(async move { + loop { + if let Ok(stream) = PARSEABLE.get_stream(&s, &t) + && stream.get_stream_type().ne(&StreamType::Internal) + { + info!( + stream = ?s, + tenant = ?t, + "skipping historic phase for user-defined stream" + ); + } else { + let anchor = floor_to_minute(Utc::now()); + let tick_span = tracing::info_span!( + "hottier.tick", + stream = %s, + tenant = ?t, + phase = "historic", + anchor = %anchor + ); + async { + if let Err(err) = self + .process_stream(s.clone(), t.clone(), SyncPhase::Historic, anchor) + .await + { + error!("historic sync error: {err:?}"); + } + } + .instrument(tick_span) + .await; + } + tokio::time::sleep(historic_interval).await; + } + }); + + { + let mut tasks = self.tasks.write().await; + if let Some(old) = tasks.insert(key, StreamTasks { latest, historic }) { + old.latest.abort(); + old.historic.abort(); } } + } - while let Some(res) = sync_hot_tier_tasks.next().await { - if let Err(err) = res { - error!("Failed to run hot tier sync task {err:?}"); - return Err(err); + /// Abort and remove per-stream tasks. Caller must ensure no further work + /// will be enqueued for the stream after this returns. + async fn abort_stream_tasks(&self, stream: &str, tenant_id: &Option) { + let key: StreamKey = (tenant_id.clone(), stream.to_owned()); + { + if let Some(t) = self.tasks.write().await.remove(&key) { + t.latest.abort(); + t.historic.abort(); + info!(stream = %stream, tenant = ?tenant_id, "aborted per-stream hot tier tasks"); } } - Ok(()) } /// process the hot tier files for the stream /// delete the files from the hot tier directory if the available date range is outside the hot tier range + #[tracing::instrument( + name = "hottier.process_stream", + skip(self), + fields(stream = %stream, tenant = ?tenant_id, phase = ?phase, anchor = %anchor), + err + )] async fn process_stream( &self, stream: String, tenant_id: Option, + phase: SyncPhase, + anchor: DateTime, ) -> Result<(), HotTierError> { - let stream_hot_tier = self.get_hot_tier(&stream, &tenant_id).await?; - let mut parquet_file_size = stream_hot_tier.used_size; + let stream_start = std::time::Instant::now(); + self.process_manifest(&stream, &tenant_id, phase, anchor) + .await + .map_err(|e| { + error!( + stream = %stream, + tenant = ?tenant_id, + phase = ?phase, + error = ?e + ); + e + })?; - let mut s3_manifest_file_list = PARSEABLE + info!( + stream = %stream, + tenant = ?tenant_id, + phase = ?phase, + elapsed_ms = stream_start.elapsed().as_millis() as u64, + "stream sync done" + ); + Ok(()) + } + + /// process the hot tier files for the stream + /// Determine the candidate dates for the current phase, fetch only those + /// manifests from the metastore, build a work list sorted newest-first by + /// file timestamp, then download via the existing reserve/commit flow. + #[tracing::instrument( + name = "hottier.process_manifest", + skip(self), + fields( + stream = %stream, + tenant = ?tenant_id, + phase = ?phase, + anchor = %anchor, + candidate_dates = tracing::field::Empty, + work_count = tracing::field::Empty, + total_bytes = tracing::field::Empty, + ), + err + )] + async fn process_manifest( + &self, + stream: &str, + tenant_id: &Option, + phase: SyncPhase, + anchor: DateTime, + ) -> Result<(), HotTierError> { + let state = self.get_or_load_state(stream, tenant_id).await?; + let latest_minutes = PARSEABLE.options.hot_tier_latest_minutes; + let historic_cutoff = anchor - chrono::Duration::minutes(latest_minutes as i64); + let today_date_key = format!("date={}", anchor.date_naive()); + + let candidate_dates = match phase { + SyncPhase::Latest => Self::latest_candidate_dates(historic_cutoff, anchor), + SyncPhase::Historic => { + self.historic_candidate_dates(stream, tenant_id, &state, &today_date_key) + .await + } + }; + + if candidate_dates.is_empty() { + info!(stream = %stream, tenant = ?tenant_id, phase = ?phase, "no candidate dates this tick"); + return Ok(()); + } + + let s3_manifests = self + .fetch_manifests(stream, tenant_id, phase, &candidate_dates) + .await?; + + let mut work = self.build_work_list(&s3_manifests, historic_cutoff.naive_utc(), phase); + work.sort_by_key(|b| std::cmp::Reverse(b.1)); + let truncated = Self::cap_historic_work(&mut work, phase); + + let total_bytes: u64 = work.iter().map(|(_, _, f, _)| f.file_size).sum(); + tracing::Span::current() + .record("work_count", work.len()) + .record("total_bytes", total_bytes); + if truncated > 0 { + info!( + stream = %stream, tenant = ?tenant_id, phase = ?phase, + cap = PARSEABLE.options.historic_per_tick_cap as usize, + deferred = truncated, + "historic per-tick cap hit; deferring rest to next tick" + ); + } + if work.is_empty() { + info!(stream = %stream, tenant = ?tenant_id, phase = ?phase, "no files to download this tick"); + return Ok(()); + } + + self.download_work(stream, tenant_id, phase, &state, work) + .await?; + + if matches!(phase, SyncPhase::Historic) && truncated == 0 { + self.mark_complete_dates(stream, tenant_id, &state, &candidate_dates, &s3_manifests) + .await; + } + + Ok(()) + } + + /// Dates covered by `[historic_cutoff, anchor]`. Usually today, or + /// today + yesterday when latest window crosses midnight. + fn latest_candidate_dates( + historic_cutoff: DateTime, + anchor: DateTime, + ) -> Vec { + let mut out = Vec::new(); + let mut d = historic_cutoff.date_naive(); + let end = anchor.date_naive(); + while d <= end { + out.push(format!("date={d}")); + d = d.succ_opt().unwrap_or(d); + } + out + } + + /// Union of local hot-tier dates and S3 dates, minus today and dates + /// already marked complete. Newest-first. + async fn historic_candidate_dates( + &self, + stream: &str, + tenant_id: &Option, + state: &Arc, + today_date_key: &str, + ) -> Vec { + let local = self + .fetch_hot_tier_dates(stream, tenant_id) + .await + .unwrap_or_default() + .into_iter() + .map(|d| format!("date={d}")); + let s3 = PARSEABLE + .hottier_connection_pool + .list_dates(stream, tenant_id) + .await + .unwrap_or_default(); + let mut union: std::collections::BTreeSet = local.collect(); + union.extend(s3); + + let completed = state.completed_dates.read().await; + let mut out: Vec = union + .into_iter() + .filter(|d| d.as_str() < today_date_key && !completed.contains(d)) + .collect(); + out.sort(); + out.reverse(); + out + } + + async fn fetch_manifests( + &self, + stream: &str, + tenant_id: &Option, + phase: SyncPhase, + candidate_dates: &[String], + ) -> Result>, HotTierError> { + PARSEABLE .metastore - .get_all_manifest_files(&stream, &tenant_id) + .get_manifest_files_for_dates(stream, tenant_id, candidate_dates) .await .map_err(|e| { + error!( + stream = %stream, tenant = ?tenant_id, phase = ?phase, + error = ?e, "manifest fetch failed" + ); HotTierError::ObjectStorageError(ObjectStorageError::MetastoreError(Box::new( e.to_detail(), ))) - })?; + }) + } - self.process_manifest( - &stream, - &mut s3_manifest_file_list, - &mut parquet_file_size, - &tenant_id, - ) - .await?; + /// Flatten manifests into work list. Keep only files matching this + /// phase's cutoff and not already on disk. + fn build_work_list( + &self, + s3_manifests: &BTreeMap>, + historic_cutoff_naive: chrono::NaiveDateTime, + phase: SyncPhase, + ) -> Vec<(NaiveDate, chrono::NaiveDateTime, File, PathBuf)> { + let mut work = Vec::new(); + for (str_date, manifest_files) in s3_manifests.iter() { + let Some(date) = Self::parse_date_key(str_date) else { + continue; + }; + for manifest in manifest_files { + for parquet_file in &manifest.files { + if let Some(item) = + self.work_item_for(parquet_file, date, historic_cutoff_naive, phase) + { + work.push(item); + } + } + } + } + work + } - Ok(()) + fn parse_date_key(str_date: &str) -> Option { + match NaiveDate::parse_from_str(str_date.trim_start_matches("date="), "%Y-%m-%d") { + Ok(d) => Some(d), + Err(_) => { + error!("Invalid date format: {}", str_date); + None + } + } } - /// process the hot tier files for the date for the stream - /// collect all manifests from metastore for the date, sort the parquet file list - /// in order to download the latest files first - /// download the parquet files if not present in hot tier directory - async fn process_manifest( + fn work_item_for( + &self, + parquet_file: &File, + date: NaiveDate, + historic_cutoff_naive: chrono::NaiveDateTime, + phase: SyncPhase, + ) -> Option<(NaiveDate, chrono::NaiveDateTime, File, PathBuf)> { + let parquet_path = self.hot_tier_path.join(&parquet_file.file_path); + if parquet_path.exists() { + return None; + } + let dt = extract_datetime(&parquet_file.file_path)?; + let is_latest = dt >= historic_cutoff_naive; + let keep = match phase { + SyncPhase::Latest => is_latest, + SyncPhase::Historic => !is_latest, + }; + keep.then(|| (date, dt, parquet_file.clone(), parquet_path)) + } + + /// Historic ticks cap per-tick work. Returns count truncated (0 for Latest). + fn cap_historic_work( + work: &mut Vec<(NaiveDate, chrono::NaiveDateTime, File, PathBuf)>, + phase: SyncPhase, + ) -> usize { + if !matches!(phase, SyncPhase::Historic) { + return 0; + } + let cap = PARSEABLE.options.historic_per_tick_cap as usize; + if work.len() <= cap { + return 0; + } + let dropped = work.len() - cap; + work.truncate(cap); + dropped + } + + async fn download_work( &self, stream: &str, - manifest_files_to_download: &mut BTreeMap>, - parquet_file_size: &mut u64, tenant_id: &Option, + phase: SyncPhase, + state: &Arc, + work: Vec<(NaiveDate, chrono::NaiveDateTime, File, PathBuf)>, ) -> Result<(), HotTierError> { - if manifest_files_to_download.is_empty() { + let concurrency = PARSEABLE.options.hot_tier_files_per_stream_concurrency; + let stop = Arc::new(std::sync::atomic::AtomicBool::new(false)); + let stream_owned = stream.to_owned(); + let tenant_owned = tenant_id.clone(); + + let reserved = { + let mut sht = state.sht.lock().await; + self.reserve_disk_budget(stream, &work, tenant_id, phase, &mut sht) + .await? + }; + if !reserved { return Ok(()); } - for (str_date, manifest_files) in manifest_files_to_download.iter().rev() { - let mut storage_combined_manifest = Manifest::default(); - for storage_manifest in manifest_files { - storage_combined_manifest - .files - .extend(storage_manifest.files.clone()); - } + let results: Vec> = futures::stream::iter(work) + .map(|(date, _dt, file, parquet_path)| { + let state = state.clone(); + let stream = stream_owned.clone(); + let tenant_id = tenant_owned.clone(); + let stop = stop.clone(); + async move { + if stop.load(std::sync::atomic::Ordering::Relaxed) { + return Ok(()); + } + let processed = self + .process_parquet_file_concurrent( + &stream, + &file, + parquet_path, + date, + &tenant_id, + &state, + phase, + ) + .await?; + if !processed && !stop.swap(true, std::sync::atomic::Ordering::Relaxed) { + info!( + stream = %stream, tenant = ?tenant_id, phase = ?phase, + "sticky stop: halting further reservations this tick" + ); + } + Ok(()) + } + }) + .buffered(concurrency as usize) + .collect() + .await; - storage_combined_manifest - .files - .sort_by_key(|file| file.file_path.clone()); + for r in results { + r?; + } + Ok(()) + } - while let Some(parquet_file) = storage_combined_manifest.files.pop() { - let parquet_file_path = &parquet_file.file_path; - let parquet_path = self.hot_tier_path.join(parquet_file_path); + /// For each candidate date where local manifest caught up to S3, + /// add it to `completed_dates` so future Historic ticks skip it. + async fn mark_complete_dates( + &self, + stream: &str, + tenant_id: &Option, + state: &Arc, + candidate_dates: &[String], + s3_manifests: &BTreeMap>, + ) { + let mut newly_complete = Vec::new(); + for date_key in candidate_dates { + let s3_count: usize = s3_manifests + .get(date_key) + .map_or(0, |v| v.iter().map(|m| m.files.len()).sum()); + if s3_count == 0 { + continue; + } + let local_count = self + .local_manifest_file_count(stream, date_key, tenant_id) + .await; + if local_count >= s3_count { + newly_complete.push(date_key.clone()); + } + } + if newly_complete.is_empty() { + return; + } + let mut completed = state.completed_dates.write().await; + for d in newly_complete { + info!(stream = %stream, tenant = ?tenant_id, date = %d, "marking date locally complete"); + completed.insert(d); + } + } - if !parquet_path.exists() { - if let Ok(date) = - NaiveDate::parse_from_str(str_date.trim_start_matches("date="), "%Y-%m-%d") - { + /// Count files recorded in a date's local `hottier.manifest.json`. + /// Returns 0 if the manifest is missing or fails to parse. + async fn local_manifest_file_count( + &self, + stream: &str, + date_key: &str, + tenant_id: &Option, + ) -> usize { + let date_dir = if let Some(tenant) = tenant_id.as_ref() { + self.hot_tier_path.join(tenant).join(stream).join(date_key) + } else { + self.hot_tier_path.join(stream).join(date_key) + }; + let manifest_path = date_dir.join("hottier.manifest.json"); + if !manifest_path.exists() { + return 0; + } + match fs::read(&manifest_path).await { + Ok(bytes) => serde_json::from_slice::(&bytes) + .map(|m| m.files.len()) + .unwrap_or(0), + Err(_) => 0, + } + } + #[allow(clippy::too_many_arguments)] + #[tracing::instrument( + name = "hottier.reserve_disk_budget", + skip(self, work, sht), + fields( + stream = %stream, + tenant = ?tenant_id, + phase = ?phase, + ), + err + )] + async fn reserve_disk_budget( + &self, + stream: &str, + // parquet_file: &File, + // parquet_path: PathBuf, + // date: NaiveDate, + work: &Vec<(NaiveDate, chrono::NaiveDateTime, File, PathBuf)>, + tenant_id: &Option, + phase: SyncPhase, + sht: &mut StreamHotTier, + ) -> Result { + // RESERVE + + for (_, _, parquet_file, parquet_path) in work { + // let mut sht = state.sht.lock().await; + if !self.is_disk_available(parquet_file.file_size).await? + || sht.available_size < parquet_file.file_size + { + match phase { + SyncPhase::Latest => { + info!( + stream = %stream, + tenant = ?tenant_id, + file = %parquet_file.file_path, + file_size = parquet_file.file_size, + available = sht.available_size, + "tight on space; triggering eviction" + ); if !self - .process_parquet_file( + .cleanup_hot_tier_old_data( stream, - &parquet_file, - parquet_file_size, + sht, parquet_path, - date, + parquet_file.file_size, tenant_id, ) .await? { - break; + info!( + stream = %stream, + tenant = ?tenant_id, + file = %parquet_file.file_path, + file_size = parquet_file.file_size, + "eviction freed nothing, skipping file" + ); + return Ok(false); } - } else { - warn!("Invalid date format: {}", str_date); + } + SyncPhase::Historic => { + info!( + stream = %stream, + tenant = ?tenant_id, + file = %parquet_file.file_path, + file_size = parquet_file.file_size, + available = sht.available_size, + "historic phase: full, skipping file" + ); + return Ok(false); } } } + if sht.available_size < parquet_file.file_size { + info!( + stream = %stream, + tenant = ?tenant_id, + file = %parquet_file.file_path, + file_size = parquet_file.file_size, + available = sht.available_size, + "still no space after eviction, skipping" + ); + return Ok(false); + } + sht.available_size = + if let Some(val) = sht.available_size.checked_sub(parquet_file.file_size) { + val + } else { + tracing::error!( + stream = %stream, + tenant = ?tenant_id, + file = %parquet_file.file_path, + file_size = parquet_file.file_size, + available = sht.available_size, + "file_size > sht.available_size, setting available_size to 0 and moving on" + ); + 0 + }; } - - Ok(()) + self.put_hot_tier(stream, sht, tenant_id).await?; + Ok(true) } - /// process the parquet file for the stream - /// check if the disk is available to download the parquet file - /// if not available, delete the oldest entry from the hot tier directory - /// download the parquet file from S3 to the hot tier directory - /// update the used and available size in the hot tier metadata - /// return true if the parquet file is processed successfully - async fn process_parquet_file( + /// Reserve disk budget under the per-stream lock, download outside the lock, + /// then commit usage + per-date manifest under the lock again. + /// Returns false when no budget is available (caller should stop scheduling + /// further work for this stream). + #[allow(clippy::too_many_arguments)] + async fn process_parquet_file_concurrent( &self, stream: &str, parquet_file: &File, - parquet_file_size: &mut u64, parquet_path: PathBuf, date: NaiveDate, tenant_id: &Option, + state: &Arc, + phase: SyncPhase, ) -> Result { - let mut file_processed = false; - let mut stream_hot_tier = self.get_hot_tier(stream, tenant_id).await?; - if !self.is_disk_available(parquet_file.file_size).await? - || stream_hot_tier.available_size <= parquet_file.file_size - { - if !self - .cleanup_hot_tier_old_data( - stream, - &mut stream_hot_tier, - &parquet_path, - parquet_file.file_size, - tenant_id, - ) - .await? - { - return Ok(file_processed); - } - *parquet_file_size = stream_hot_tier.used_size; - } + // DOWNLOAD (no lock held) let parquet_file_path = RelativePathBuf::from(parquet_file.file_path.clone()); fs::create_dir_all(parquet_path.parent().unwrap()).await?; - let mut file = fs::File::create(parquet_path.clone()).await?; - let parquet_data = PARSEABLE - .storage - .get_object_store() - .get_object(&parquet_file_path, tenant_id) - .await?; - file.write_all(&parquet_data).await?; - *parquet_file_size += parquet_file.file_size; - stream_hot_tier.used_size = *parquet_file_size; + info!( + stream = %stream, + tenant = ?tenant_id, + file = %parquet_file.file_path, + file_size = parquet_file.file_size, + phase = ?phase, + "download starting" + ); + let dl_start = std::time::Instant::now(); + let download_result = PARSEABLE + .hottier_connection_pool + .parallel_chunked_download(&parquet_file_path, tenant_id, parquet_path.clone()) + .await; + let dl_elapsed = dl_start.elapsed(); + + if let Err(e) = download_result { + info!( + stream = %stream, + tenant = ?tenant_id, + file = %parquet_file.file_path, + elapsed_ms = dl_elapsed.as_millis() as u64, + err = %e, + phase = ?phase, + "download failed, refunding reservation" + ); + // refund reservation + let mut sht = state.sht.lock().await; + sht.available_size += parquet_file.file_size; + if let Err(put_err) = self.put_hot_tier(stream, &mut sht, tenant_id).await { + error!("failed to persist refund after download failure: {put_err:?}"); + } + // backend already cleaned up its `.partial` file; final path was never created. + return Err(e.into()); + } + let elapsed_ms = dl_elapsed.as_millis() as u64; + let mbps = if dl_elapsed.as_secs_f64() > 0.0 { + (parquet_file.file_size as f64 * 8.0) / dl_elapsed.as_secs_f64() / 1_000_000.0 + } else { + 0.0 + }; + info!( + stream = %stream, + tenant = ?tenant_id, + file = %parquet_file.file_path, + file_size = parquet_file.file_size, + elapsed_ms, + phase = ?phase, + mbps = format!("{mbps:.1}"), + "download finished, committing" + ); + + // COMMIT + { + let mut sht = state.sht.lock().await; + sht.used_size += parquet_file.file_size; + self.put_hot_tier(stream, &mut sht, tenant_id).await?; - stream_hot_tier.available_size -= parquet_file.file_size; - self.put_hot_tier(stream, &mut stream_hot_tier, tenant_id) - .await?; - file_processed = true; - let path = self.get_stream_path_for_date(stream, &date, tenant_id); - let mut hot_tier_manifest = HotTierManager::get_hot_tier_manifest_from_path(path).await?; - hot_tier_manifest.files.push(parquet_file.clone()); - hot_tier_manifest - .files - .sort_by_key(|file| file.file_path.clone()); - // write the manifest file to the hot tier directory - let manifest_path = self - .get_stream_path_for_date(stream, &date, tenant_id) - .join("hottier.manifest.json"); - fs::create_dir_all(manifest_path.parent().unwrap()).await?; - fs::write(manifest_path, serde_json::to_vec(&hot_tier_manifest)?).await?; - - Ok(file_processed) - } - - ///fetch the list of dates available in the hot tier directory for the stream and sort them + let path = self.get_stream_path_for_date(stream, &date, tenant_id); + let mut hot_tier_manifest = + HotTierManager::get_hot_tier_manifest_from_path(path).await?; + hot_tier_manifest.files.push(parquet_file.clone()); + hot_tier_manifest + .files + .sort_by_key(|file| file.file_path.clone()); + // write the manifest file to the hot tier directory + let manifest_path = self + .get_stream_path_for_date(stream, &date, tenant_id) + .join("hottier.manifest.json"); + fs::create_dir_all(manifest_path.parent().unwrap()).await?; + fs::write(manifest_path, serde_json::to_vec(&hot_tier_manifest)?).await?; + info!( + stream = %stream, + tenant = ?tenant_id, + file = %parquet_file.file_path, + used = sht.used_size, + available = sht.available_size, + "committed" + ); + } + + Ok(true) + } + + /// fetch the list of dates available in the hot tier directory for the stream and sort them + #[tracing::instrument( + name = "hottier.fetch_dates", + skip(self), + fields(stream = %stream, tenant = ?tenant_id), + err + )] pub async fn fetch_hot_tier_dates( &self, stream: &str, @@ -582,6 +1568,12 @@ impl HotTierManager { } ///get the list of parquet files from the hot tier directory for the stream + #[tracing::instrument( + name = "hottier.get_parquet_files", + skip(self), + fields(stream = %stream, tenant = ?tenant_id), + err + )] pub async fn get_hot_tier_parquet_files( &self, stream: &str, @@ -628,12 +1620,18 @@ impl HotTierManager { path.exists() } - ///delete the parquet file from the hot tier directory for the stream + /// delete entire parquet file minute from the hot tier directory for the stream /// loop through all manifests in the hot tier directory for the stream /// loop through all parquet files in the manifest /// check for the oldest entry to delete if the path exists in hot tier /// update the used and available size in the hot tier metadata /// loop if available size is still less than the parquet file size + #[tracing::instrument( + name = "hottier.cleanup_old_data", + skip(self, stream_hot_tier, download_file_path), + fields(stream = %stream, tenant = ?tenant_id, target_size = parquet_file_size), + err + )] pub async fn cleanup_hot_tier_old_data( &self, stream: &str, @@ -642,11 +1640,33 @@ impl HotTierManager { parquet_file_size: u64, tenant_id: &Option, ) -> Result { + info!( + stream = %stream, + tenant = ?tenant_id, + target_size = parquet_file_size, + available = stream_hot_tier.available_size, + "eviction starting" + ); let mut delete_successful = false; + let mut freed_total: u64 = 0; let dates = self.fetch_hot_tier_dates(stream, tenant_id).await?; + if dates.is_empty() { + info!( + stream = %stream, + tenant = ?tenant_id, + "eviction: no date dirs found, nothing to evict" + ); + } 'loop_dates: for date in dates { let path = self.get_stream_path_for_date(stream, &date, tenant_id); if !path.exists() { + info!( + stream = %stream, + tenant = ?tenant_id, + date = %date, + path = %path.display(), + "eviction: date path missing, skipping" + ); continue; } @@ -658,57 +1678,133 @@ impl HotTierManager { .to_string_lossy() .ends_with(".manifest.json") }); + if manifest_files.is_empty() { + info!( + stream = %stream, + tenant = ?tenant_id, + date = %date, + path = %path.display(), + "eviction: no .manifest.json files in date dir" + ); + continue; + } for manifest_file in manifest_files { let file = fs::read(manifest_file.path()).await?; let mut manifest: Manifest = serde_json::from_slice(&file)?; - manifest.files.sort_by_key(|file| file.file_path.clone()); - manifest.files.reverse(); - - 'loop_files: while let Some(file_to_delete) = manifest.files.pop() { - let file_size = file_to_delete.file_size; - let path_to_delete = self.hot_tier_path.join(&file_to_delete.file_path); - - if path_to_delete.exists() { - if let (Some(download_date_time), Some(delete_date_time)) = ( - extract_datetime(download_file_path.to_str().unwrap()), - extract_datetime(path_to_delete.to_str().unwrap()), - ) && download_date_time <= delete_date_time - { - delete_successful = false; - break 'loop_files; - } - - fs::write(manifest_file.path(), serde_json::to_vec(&manifest)?).await?; + if manifest.files.is_empty() { + info!( + stream = %stream, + tenant = ?tenant_id, + manifest = %manifest_file.path().display(), + "eviction: manifest has zero file entries" + ); + continue; + } - fs::remove_dir_all(path_to_delete.parent().unwrap()).await?; - delete_empty_directory_hot_tier( - path_to_delete.parent().unwrap().to_path_buf(), - ) - .await?; + // sort in an ascending manner + // idx0: minute=00 + // idx59: minute=59 + manifest.files.sort_by_key(|file| file.file_path.clone()); - stream_hot_tier.used_size -= file_size; - stream_hot_tier.available_size += file_size; - self.put_hot_tier(stream, stream_hot_tier, tenant_id) - .await?; - delete_successful = true; + // get first file's parent (/hottier/stream/date=d/hour=h/minute=m) + let first_file = manifest.files.first().unwrap(); + let first_file_path = self.hot_tier_path.join(&first_file.file_path); + let minute_to_delete = first_file_path.parent().unwrap(); + + if !minute_to_delete.exists() { + info!( + stream = %stream, + tenant = ?tenant_id, + manifest = %manifest_file.path().display(), + first_file = %first_file.file_path, + minute = %minute_to_delete.display(), + "eviction: minute dir referenced by manifest does not exist on disk" + ); + continue; + } + { + if let (Some(download_date_time), Some(delete_date_time)) = ( + extract_datetime(download_file_path.to_str().unwrap()), + extract_datetime(first_file_path.to_str().unwrap()), + ) && download_date_time <= delete_date_time + { + info!( + stream = %stream, + tenant = ?tenant_id, + candidate = %minute_to_delete.display(), + target = %download_file_path.display(), + "skip evict: candidate newer than target" + ); + continue; + } - if stream_hot_tier.available_size <= parquet_file_size { - continue 'loop_files; + let minute_to_delete_owned = minute_to_delete.to_path_buf(); + let mut minute_freed: u64 = 0; + manifest.files.retain(|file| { + let file_path = self.hot_tier_path.join(&file.file_path); + let file_minute = file_path.parent().unwrap(); + if file_minute == minute_to_delete_owned { + minute_freed = minute_freed.saturating_add(file.file_size); + false } else { - break 'loop_dates; + true } + }); + + stream_hot_tier.used_size = stream_hot_tier + .used_size + .checked_sub(minute_freed) + .unwrap_or_else(|| { + tracing::error!( + stream = %stream, + tenant = ?tenant_id, + minute = %minute_to_delete_owned.display(), + minute_freed, + used_size = stream_hot_tier.used_size, + "minute_freed > used_size, clamping used_size to 0" + ); + 0 + }); + stream_hot_tier.available_size = + stream_hot_tier.available_size.saturating_add(minute_freed); + freed_total = freed_total.saturating_add(minute_freed); + + fs::write(manifest_file.path(), serde_json::to_vec(&manifest)?).await?; + fs::remove_dir_all(&minute_to_delete_owned).await?; + delete_empty_directory_hot_tier(minute_to_delete_owned.clone()).await?; + self.put_hot_tier(stream, stream_hot_tier, tenant_id) + .await?; + delete_successful = true; + info!( + stream = %stream, + tenant = ?tenant_id, + evicted_minute = %minute_to_delete_owned.display(), + evicted_size = minute_freed, + freed_total, + new_available = stream_hot_tier.available_size, + "evicted" + ); + if stream_hot_tier.available_size < parquet_file_size { + continue; } else { - fs::write(manifest_file.path(), serde_json::to_vec(&manifest)?).await?; + break 'loop_dates; } } } } + info!( + stream = %stream, + tenant = ?tenant_id, + freed_total, + success = delete_successful, + "eviction complete" + ); Ok(delete_successful) } - ///check if the disk is available to download the parquet file + /// check if the disk is available to download the parquet file /// check if the disk usage is above the threshold pub async fn is_disk_available(&self, size_to_download: u64) -> Result { if let Some(DiskUtil { @@ -780,6 +1876,7 @@ impl HotTierManager { Ok(None) } + #[tracing::instrument(name = "hottier.put_internal_stream", skip(self), err)] pub async fn put_internal_stream_hot_tier(&self) -> Result<(), HotTierError> { let tenants = if let Some(tenants) = PARSEABLE.list_tenants() { tenants.into_iter().map(Some).collect() @@ -811,6 +1908,7 @@ impl HotTierManager { } /// Creates hot tier for pstats internal stream if the stream exists in storage + #[tracing::instrument(name = "hottier.create_pstats", skip(self), err)] async fn create_pstats_hot_tier(&self) -> Result<(), HotTierError> { let tenants = if let Some(tenants) = PARSEABLE.list_tenants() { tenants.into_iter().map(Some).collect() diff --git a/src/metastore/metastore_traits.rs b/src/metastore/metastore_traits.rs index 79745ecf8..0df0e8a5d 100644 --- a/src/metastore/metastore_traits.rs +++ b/src/metastore/metastore_traits.rs @@ -248,11 +248,15 @@ pub trait Metastore: std::fmt::Debug + Send + Sync { tenant_id: &Option, ) -> Result, MetastoreError>; - /// manifest - async fn get_all_manifest_files( + /// Fetch manifests only for the explicitly requested date keys + /// (e.g. `["date=2026-05-12"]`). Skips the top-level LIST to discover + /// dates — callers must already know which dates to query (e.g. via + /// the local hot-tier dir + a single object-store LIST when needed). + async fn get_manifest_files_for_dates( &self, stream_name: &str, tenant_id: &Option, + dates: &[String], ) -> Result>, MetastoreError>; async fn get_manifest( &self, diff --git a/src/metastore/metastores/object_store_metastore.rs b/src/metastore/metastores/object_store_metastore.rs index 904bcca24..d22f87226 100644 --- a/src/metastore/metastores/object_store_metastore.rs +++ b/src/metastore/metastores/object_store_metastore.rs @@ -28,7 +28,7 @@ use chrono::{DateTime, Utc}; use dashmap::DashMap; use relative_path::RelativePathBuf; use tonic::async_trait; -use tracing::warn; +use tracing::{info, warn}; use ulid::Ulid; use crate::{ @@ -928,55 +928,103 @@ impl Metastore for ObjectStoreMetastore { .await?) } - /// Fetch all `Manifest` files - async fn get_all_manifest_files( + /// Fetch manifest files for the given date keys in parallel. + /// + /// Per-date work runs concurrently via `futures::future::try_join_all` + /// so a slow LIST on one date does not block the others. Each date's + /// timing is logged at info level so any pathologically slow dates + /// (S3 retries, oversized prefixes) are visible without needing extra + /// span instrumentation. + async fn get_manifest_files_for_dates( &self, stream_name: &str, tenant_id: &Option, + dates: &[String], ) -> Result>, MetastoreError> { - let mut result_file_list: BTreeMap> = BTreeMap::new(); - let root = if let Some(tenant) = tenant_id { - format!("{tenant}/{stream_name}") - } else { - stream_name.into() - }; - let resp = self.storage.list_with_delimiter(Some(root.into())).await?; - - let dates = resp - .common_prefixes - .iter() - .flat_map(|path| path.parts()) - .filter(|name| name.as_ref() != stream_name && name.as_ref() != STREAM_ROOT_DIRECTORY) - .map(|name| name.as_ref().to_string()) - .collect::>(); + let total_start = std::time::Instant::now(); + let date_futures = dates.iter().map(|date| { + // let storage = self.storage.clone(); + let stream = stream_name.to_string(); + let tenant = tenant_id.clone(); + let date = date.clone(); + async move { + let t_start = std::time::Instant::now(); + let date_path = if let Some(tenant) = tenant.as_ref() { + object_store::path::Path::from(format!("{}/{}/{}", tenant, &stream, &date)) + } else { + object_store::path::Path::from(format!("{}/{}", &stream, &date)) + }; - for date in dates { - let date_path = if let Some(tenant) = tenant_id { - object_store::path::Path::from(format!("{}/{}/{}", tenant, stream_name, &date)) - } else { - object_store::path::Path::from(format!("{}/{}", stream_name, &date)) - }; - let resp = self.storage.list_with_delimiter(Some(date_path)).await?; + let t_list = std::time::Instant::now(); + let resp = match self.storage.list_with_delimiter(Some(date_path)).await { + Ok(r) => r, + Err(ObjectStorageError::NoSuchKey(_)) => { + info!( + stream = %stream, + tenant = ?tenant, + date = %date, + list_ms = t_list.elapsed().as_millis() as u64, + "manifest fetch: date prefix not found" + ); + return Ok::<(String, Vec), MetastoreError>((date, Vec::new())); + } + Err(e) => return Err(e.into()), + }; + let list_ms = t_list.elapsed().as_millis() as u64; - let manifest_paths: Vec = resp - .objects - .iter() - .filter(|name| name.location.filename().unwrap().ends_with("manifest.json")) - .map(|name| name.location.to_string()) - .collect(); + let manifest_paths: Vec = resp + .objects + .iter() + .filter(|name| name.location.filename().unwrap().ends_with("manifest.json")) + .map(|name| name.location.to_string()) + .collect(); + let manifest_count = manifest_paths.len(); + + let t_gets = std::time::Instant::now(); + let mut manifests: Vec = Vec::with_capacity(manifest_count); + for path in manifest_paths { + let bytes = self + .storage + .get_object(&RelativePathBuf::from(path), &tenant) + .await?; + manifests.push(serde_json::from_slice::(&bytes)?); + } + let gets_ms = t_gets.elapsed().as_millis() as u64; + let total_ms = t_start.elapsed().as_millis() as u64; + + if total_ms > 100 { + info!( + stream = %stream, + tenant = ?tenant, + date = %date, + list_ms, + gets_ms, + total_ms, + manifest_count, + "manifest fetch per-date timing" + ); + } + Ok((date, manifests)) + } + }); - for path in manifest_paths { - let bytes = self - .storage - .get_object(&RelativePathBuf::from(path), tenant_id) - .await?; + let results: Vec<(String, Vec)> = + futures::future::try_join_all(date_futures).await?; - result_file_list - .entry(date.clone()) - .or_default() - .push(serde_json::from_slice::(&bytes)?); + let mut result_file_list: BTreeMap> = BTreeMap::new(); + for (date, manifests) in results { + if !manifests.is_empty() { + result_file_list.insert(date, manifests); } } + info!( + stream = %stream_name, + tenant = ?tenant_id, + dates = dates.len(), + populated = result_file_list.len(), + total_ms = total_start.elapsed().as_millis() as u64, + "get_manifest_files_for_dates done" + ); Ok(result_file_list) } diff --git a/src/metrics/mod.rs b/src/metrics/mod.rs index 4c61f2f7c..cff4ba5d0 100644 --- a/src/metrics/mod.rs +++ b/src/metrics/mod.rs @@ -318,6 +318,19 @@ pub static TOTAL_FILES_SCANNED_IN_OBJECT_STORE_CALLS_BY_DATE: Lazy = + Lazy::new(|| { + IntCounterVec::new( + Opts::new( + "partial_file_scans_in_object_store_calls_by_date", + "Partial file scans in object store calls by date", + ) + .namespace(METRICS_NAMESPACE), + &["method", "date", "tenant_id"], + ) + .expect("metric can be created") + }); + pub static TOTAL_BYTES_SCANNED_IN_OBJECT_STORE_CALLS_BY_DATE: Lazy = Lazy::new(|| { IntCounterVec::new( @@ -388,6 +401,18 @@ pub static STORAGE_REQUEST_RESPONSE_TIME: Lazy = Lazy::new(|| { .expect("metric can be created") }); +pub static STORAGE_REQUESTS_INFLIGHT: Lazy = Lazy::new(|| { + IntGaugeVec::new( + Opts::new( + "storage_requests_inflight", + "Number of in-flight object store requests", + ) + .namespace(METRICS_NAMESPACE), + &["provider", "method"], + ) + .expect("metric can be created") +}); + pub static TOTAL_METRICS_COLLECTED_BY_DATE: Lazy = Lazy::new(|| { IntCounterVec::new( Opts::new( @@ -539,6 +564,11 @@ fn custom_metrics(registry: &Registry) { TOTAL_FILES_SCANNED_IN_OBJECT_STORE_CALLS_BY_DATE.clone(), )) .expect("metric can be registered"); + registry + .register(Box::new( + PARTIAL_FILE_SCANS_IN_OBJECT_STORE_CALLS_BY_DATE.clone(), + )) + .expect("metric can be registered"); registry .register(Box::new( TOTAL_BYTES_SCANNED_IN_OBJECT_STORE_CALLS_BY_DATE.clone(), @@ -559,6 +589,9 @@ fn custom_metrics(registry: &Registry) { registry .register(Box::new(STORAGE_REQUEST_RESPONSE_TIME.clone())) .expect("metric can be registered"); + registry + .register(Box::new(STORAGE_REQUESTS_INFLIGHT.clone())) + .expect("metric can be registered"); registry .register(Box::new(TOTAL_METRICS_COLLECTED_BY_DATE.clone())) .expect("metric can be registered"); @@ -718,6 +751,17 @@ pub fn increment_object_store_calls_by_date(method: &str, date: &str, tenant_id: .inc(); } +pub fn increment_partial_file_scans_in_object_store_calls_by_date( + method: &str, + count: u64, + date: &str, + tenant_id: &str, +) { + PARTIAL_FILE_SCANS_IN_OBJECT_STORE_CALLS_BY_DATE + .with_label_values(&[method, date, tenant_id]) + .inc_by(count); +} + pub fn increment_files_scanned_in_object_store_calls_by_date( method: &str, count: u64, diff --git a/src/parseable/mod.rs b/src/parseable/mod.rs index 8d553fbc9..b2eef36ea 100644 --- a/src/parseable/mod.rs +++ b/src/parseable/mod.rs @@ -75,8 +75,8 @@ use crate::{ }, static_schema::{StaticSchema, convert_static_schema_to_arrow_schema}, storage::{ - ObjectStorageError, ObjectStorageProvider, ObjectStoreFormat, Owner, Permisssion, - StorageMetadata, StreamType, put_remote_metadata, + ObjectStorage, ObjectStorageError, ObjectStorageProvider, ObjectStoreFormat, Owner, + Permisssion, StorageMetadata, StreamType, put_remote_metadata, }, tenants::{Service, TENANT_METADATA}, validator, @@ -173,13 +173,14 @@ pub static PARSEABLE: Lazy = Lazy::new(|| { let metastore = ObjectStoreMetastore { storage: args.storage.construct_client(), }; - + let hottier_connection_pool = args.storage.construct_client(); Parseable::new( args.options, #[cfg(feature = "kafka")] args.kafka, Arc::new(args.storage), Arc::new(metastore), + hottier_connection_pool, ) } StorageOptions::S3(args) => { @@ -187,12 +188,14 @@ pub static PARSEABLE: Lazy = Lazy::new(|| { let metastore = ObjectStoreMetastore { storage: args.storage.construct_client(), }; + let hottier_connection_pool = args.storage.construct_client(); Parseable::new( args.options, #[cfg(feature = "kafka")] args.kafka, Arc::new(args.storage), Arc::new(metastore), + hottier_connection_pool, ) } StorageOptions::Blob(args) => { @@ -200,12 +203,14 @@ pub static PARSEABLE: Lazy = Lazy::new(|| { let metastore = ObjectStoreMetastore { storage: args.storage.construct_client(), }; + let hottier_connection_pool = args.storage.construct_client(); Parseable::new( args.options, #[cfg(feature = "kafka")] args.kafka, Arc::new(args.storage), Arc::new(metastore), + hottier_connection_pool, ) } StorageOptions::Gcs(args) => { @@ -213,12 +218,14 @@ pub static PARSEABLE: Lazy = Lazy::new(|| { let metastore = ObjectStoreMetastore { storage: args.storage.construct_client(), }; + let hottier_connection_pool = args.storage.construct_client(); Parseable::new( args.options, #[cfg(feature = "kafka")] args.kafka, Arc::new(args.storage), Arc::new(metastore), + hottier_connection_pool, ) } } @@ -238,6 +245,8 @@ pub struct Parseable { pub tenants: Arc>>, /// metastore pub metastore: Arc, + /// Hot-tier connection pool + pub hottier_connection_pool: Arc, /// Used to configure the kafka connector #[cfg(feature = "kafka")] pub kafka_config: KafkaConfig, @@ -249,11 +258,13 @@ impl Parseable { #[cfg(feature = "kafka")] kafka_config: KafkaConfig, storage: Arc, metastore: Arc, + hottier_connection_pool: Arc, ) -> Self { Parseable { options: Arc::new(options), storage, metastore, + hottier_connection_pool, streams: Streams::default(), tenants: Arc::new(RwLock::new(vec![])), #[cfg(feature = "kafka")] diff --git a/src/query/stream_schema_provider.rs b/src/query/stream_schema_provider.rs index 59daa4a88..c316ecd4c 100644 --- a/src/query/stream_schema_provider.rs +++ b/src/query/stream_schema_provider.rs @@ -55,11 +55,11 @@ use crate::{ snapshot::{ManifestItem, Snapshot}, }, event::DEFAULT_TIMESTAMP_KEY, - hottier::HotTierManager, metrics::{ QUERY_CACHE_HIT, increment_files_scanned_in_hottier_by_date, increment_files_scanned_in_query_by_date, }, + hottier::{GLOBAL_HOTTIER, HotTierManager}, option::Mode, parseable::{DEFAULT_TENANT, PARSEABLE, STREAM_EXISTS}, storage::{ObjectStorage, ObjectStoreFormat}, @@ -630,7 +630,7 @@ impl TableProvider for StandardTableProvider { } // Hot tier data fetch - if let Some(hot_tier_manager) = HotTierManager::global() + if let Some(hot_tier_manager) = GLOBAL_HOTTIER.get() && hot_tier_manager.check_stream_hot_tier_exists(&self.stream, &self.tenant_id) { self.get_hottier_exectuion_plan( diff --git a/src/storage/azure_blob.rs b/src/storage/azure_blob.rs index 62abad0db..1482f2333 100644 --- a/src/storage/azure_blob.rs +++ b/src/storage/azure_blob.rs @@ -16,7 +16,13 @@ * */ -use std::{collections::HashSet, path::Path, sync::Arc, time::Duration}; +use std::{ + collections::HashSet, + ops::Range, + path::{Path, PathBuf}, + sync::Arc, + time::Duration, +}; use async_trait::async_trait; use bytes::Bytes; @@ -47,6 +53,7 @@ use crate::{ increment_bytes_scanned_in_object_store_calls_by_date, increment_files_scanned_in_object_store_calls_by_date, increment_object_store_calls_by_date, + increment_partial_file_scans_in_object_store_calls_by_date, }, parseable::{DEFAULT_TENANT, LogStream, PARSEABLE}, }; @@ -54,7 +61,8 @@ use crate::{ use super::{ CONNECT_TIMEOUT_SECS, ObjectStorage, ObjectStorageError, ObjectStorageProvider, PARSEABLE_ROOT_DIRECTORY, REQUEST_TIMEOUT_SECS, STREAM_METADATA_FILE_NAME, - metrics_layer::MetricLayer, object_storage::parseable_json_path, to_object_store_path, + metrics_layer::MetricLayer, object_storage::parseable_json_path, partial_path, + to_object_store_path, }; #[derive(Debug, Clone, clap::Args)] @@ -180,8 +188,9 @@ impl ObjectStorageProvider for AzureBlobConfig { let azure = self.get_default_builder().build().unwrap(); // limit objectstore to a concurrent request limit let azure = LimitStore::new(azure, super::MAX_OBJECT_STORE_REQUESTS); + let azure = MetricLayer::new(azure, "azure_blob"); Arc::new(BlobStore { - client: azure, + client: Arc::new(azure), account: self.account.clone(), container: self.container.clone(), root: StorePath::from(""), @@ -197,13 +206,123 @@ impl ObjectStorageProvider for AzureBlobConfig { // object store such as S3 and Azure Blob #[derive(Debug)] pub struct BlobStore { - client: LimitStore, + client: Arc>>, account: String, container: String, root: StorePath, } impl BlobStore { + async fn _parallel_download( + &self, + path: &RelativePath, + tenant_id: &Option, + write_path: PathBuf, + ) -> Result<(), ObjectStorageError> { + let partial = partial_path(&write_path)?; + match self + ._parallel_download_inner(path, tenant_id, partial.clone()) + .await + { + Ok(()) => { + if let Err(e) = tokio::fs::rename(&partial, &write_path).await { + let _ = tokio::fs::remove_file(&partial).await; + return Err(e.into()); + } + Ok(()) + } + Err(e) => { + let _ = tokio::fs::remove_file(&partial).await; + Err(e) + } + } + } + + #[tracing::instrument( + name = "azure.parallel_download", + skip(self, partial_path), + fields(path = %path, tenant = ?tenant_id, total_bytes = tracing::field::Empty, chunks = tracing::field::Empty), + err + )] + async fn _parallel_download_inner( + &self, + path: &RelativePath, + tenant_id: &Option, + partial_path: PathBuf, + ) -> Result<(), ObjectStorageError> { + let tenant_str = tenant_id.as_deref().unwrap_or(DEFAULT_TENANT); + let date = Utc::now().date_naive().to_string(); + let src = to_object_store_path(path); + + let meta = self.client.head(&src).await?; + increment_object_store_calls_by_date("HEAD", &date, tenant_str); + let total = meta.size; + + if let Some(parent) = partial_path.parent() { + tokio::fs::create_dir_all(parent).await?; + } + let file = tokio::fs::File::create(&partial_path).await?; + file.set_len(total).await?; + let std_file = Arc::new(file.into_std().await); + + let chunk = PARSEABLE.options.hot_tier_download_chunk_size; + let concurrency = PARSEABLE.options.hot_tier_download_concurrency; + let ranges: Vec> = (0..total) + .step_by(chunk as usize) + .map(|s| s..(s + chunk).min(total)) + .collect(); + let chunk_count = ranges.len() as u64; + tracing::Span::current() + .record("total_bytes", total) + .record("chunks", chunk_count); + let semaphore = Arc::new(tokio::sync::Semaphore::new(concurrency as usize)); + + futures::stream::iter(ranges) + .map(|r| { + let src = src.clone(); + let std_file = std_file.clone(); + let semaphore = semaphore.clone(); + async move { + let _permit = semaphore.acquire_owned().await.map_err(|e| { + ObjectStorageError::Custom(format!("semaphore closed: {e}")) + })?; + let bytes = self.client.get_range(&src, r.clone()).await?; + let offset = r.start; + tokio::task::spawn_blocking(move || -> std::io::Result<()> { + crate::storage::write_all_at(&std_file, &bytes, offset) + }) + .await + .map_err(|e| ObjectStorageError::Custom(format!("join: {e}")))??; + Ok::<_, ObjectStorageError>(()) + } + }) + .buffer_unordered(concurrency as usize) + .try_collect::>() + .await?; + + let std_file_sync = std_file.clone(); + tokio::task::spawn_blocking(move || std_file_sync.sync_all()) + .await + .map_err(|e| ObjectStorageError::Custom(format!("join: {e}")))??; + + increment_object_store_calls_by_date("GET", &date, tenant_str); + increment_files_scanned_in_object_store_calls_by_date("GET", 1, &date, tenant_str); + increment_bytes_scanned_in_object_store_calls_by_date("GET", total, &date, tenant_str); + increment_partial_file_scans_in_object_store_calls_by_date( + "GET", + chunk_count, + &date, + tenant_str, + ); + Ok(()) + } + + #[tracing::instrument( + name = "azure.get_object", + skip(self), + fields(path = %path, tenant = ?tenant_id, bytes = tracing::field::Empty), + err + )] async fn _get_object( &self, path: &RelativePath, @@ -216,6 +335,7 @@ impl BlobStore { match resp { Ok(resp) => { let body: Bytes = resp.bytes().await?; + tracing::Span::current().record("bytes", body.len()); increment_files_scanned_in_object_store_calls_by_date( "GET", 1, @@ -234,6 +354,11 @@ impl BlobStore { } } + #[tracing::instrument( + name = "azure.put_object", + skip(self, resource), + fields(path = %path, tenant = ?tenant_id, bytes = resource.content_length()) + )] async fn _put_object( &self, path: &RelativePath, @@ -257,6 +382,12 @@ impl BlobStore { } } + #[tracing::instrument( + name = "azure.delete_prefix", + skip(self), + fields(prefix = %key, tenant = ?tenant_id, deleted = tracing::field::Empty, failed = tracing::field::Empty), + err + )] async fn _delete_prefix( &self, key: &str, @@ -287,6 +418,9 @@ impl BlobStore { } let total_files = files_deleted + failed_deletes; + tracing::Span::current() + .record("deleted", files_deleted) + .record("failed", failed_deletes); increment_files_scanned_in_object_store_calls_by_date( "LIST", total_files, @@ -309,6 +443,12 @@ impl BlobStore { Ok(()) } + #[tracing::instrument( + name = "azure.list_dates", + skip(self), + fields(stream = %stream, tenant = ?tenant_id, dates = tracing::field::Empty), + err + )] async fn _list_dates( &self, stream: &str, @@ -343,10 +483,16 @@ impl BlobStore { .filter_map(|path| path.as_ref().strip_prefix(&format!("{stream}/"))) .map(String::from) .collect(); + tracing::Span::current().record("dates", dates.len()); Ok(dates) } + #[tracing::instrument( + name = "azure.upload_file", + skip(self), + fields(key = %key, path = %path.display(), tenant = ?tenant_id) + )] async fn _upload_file( &self, key: &str, @@ -371,6 +517,11 @@ impl BlobStore { } } + #[tracing::instrument( + name = "azure.upload_multipart", + skip(self), + fields(key = %key, path = %path.display(), tenant = ?tenant_id, total_bytes = tracing::field::Empty) + )] async fn _upload_multipart( &self, key: &RelativePath, @@ -383,6 +534,7 @@ impl BlobStore { let meta = file.metadata().await?; let total_size = meta.len() as usize; + tracing::Span::current().record("total_bytes", total_size); let min_multipart_size = PARSEABLE.options.min_multipart_size as usize; if total_size < min_multipart_size || !PARSEABLE.options.enable_multipart { let mut data = Vec::new(); @@ -466,6 +618,14 @@ impl BlobStore { #[async_trait] impl ObjectStorage for BlobStore { + async fn parallel_chunked_download( + &self, + path: &RelativePath, + tenant_id: &Option, + write_path: PathBuf, + ) -> Result<(), ObjectStorageError> { + self._parallel_download(path, tenant_id, write_path).await + } async fn get_buffered_reader( &self, _path: &RelativePath, @@ -488,6 +648,12 @@ impl ObjectStorage for BlobStore { self._upload_multipart(key, path, tenant_id).await } + #[tracing::instrument( + name = "azure.head", + skip(self), + fields(path = %path, tenant = ?tenant_id), + err + )] async fn head( &self, path: &RelativePath, @@ -628,6 +794,12 @@ impl ObjectStorage for BlobStore { Ok(()) } + #[tracing::instrument( + name = "azure.delete_object", + skip(self), + fields(path = %path, tenant = ?tenant_id), + err + )] async fn delete_object( &self, path: &RelativePath, @@ -652,11 +824,18 @@ impl ObjectStorage for BlobStore { Ok(result?) } + #[tracing::instrument( + name = "azure.check", + skip(self), + fields(tenant = ?tenant_id, ok = tracing::field::Empty), + err + )] async fn check(&self, tenant_id: &Option) -> Result<(), ObjectStorageError> { let result = self .client .head(&to_object_store_path(&parseable_json_path())) .await; + tracing::Span::current().record("ok", result.is_ok()); let tenant = tenant_id.as_deref().unwrap_or(DEFAULT_TENANT); increment_object_store_calls_by_date("HEAD", &Utc::now().date_naive().to_string(), tenant); if result.is_ok() { diff --git a/src/storage/gcs.rs b/src/storage/gcs.rs index 019802f24..01c98848f 100644 --- a/src/storage/gcs.rs +++ b/src/storage/gcs.rs @@ -16,13 +16,20 @@ * */ -use std::{collections::HashSet, path::Path, sync::Arc, time::Duration}; +use std::{ + collections::HashSet, + ops::Range, + path::{Path, PathBuf}, + sync::Arc, + time::Duration, +}; use crate::{ metrics::{ increment_bytes_scanned_in_object_store_calls_by_date, increment_files_scanned_in_object_store_calls_by_date, increment_object_store_calls_by_date, + increment_partial_file_scans_in_object_store_calls_by_date, }, parseable::{DEFAULT_TENANT, LogStream, PARSEABLE}, }; @@ -47,12 +54,14 @@ use object_store::{ }; use relative_path::{RelativePath, RelativePathBuf}; use tokio::{fs::OpenOptions, io::AsyncReadExt}; + use tracing::error; use super::{ CONNECT_TIMEOUT_SECS, ObjectStorage, ObjectStorageError, ObjectStorageProvider, PARSEABLE_ROOT_DIRECTORY, REQUEST_TIMEOUT_SECS, STREAM_METADATA_FILE_NAME, - metrics_layer::MetricLayer, object_storage::parseable_json_path, to_object_store_path, + metrics_layer::MetricLayer, object_storage::parseable_json_path, partial_path, + to_object_store_path, }; #[derive(Debug, Clone, clap::Args)] @@ -141,7 +150,9 @@ impl ObjectStorageProvider for GcsConfig { fn construct_client(&self) -> Arc { let gcs = self.get_default_builder().build().unwrap(); - + // limit objectstore to a concurrent request limit + let gcs = LimitStore::new(gcs, super::MAX_OBJECT_STORE_REQUESTS); + let gcs = MetricLayer::new(gcs, "gcs"); Arc::new(Gcs { client: Arc::new(gcs), bucket: self.bucket_name.clone(), @@ -163,12 +174,122 @@ impl ObjectStorageProvider for GcsConfig { #[derive(Debug)] pub struct Gcs { - client: Arc, + client: Arc>>, bucket: String, root: StorePath, } impl Gcs { + async fn _parallel_download( + &self, + path: &RelativePath, + tenant_id: &Option, + write_path: PathBuf, + ) -> Result<(), ObjectStorageError> { + let partial = partial_path(&write_path)?; + match self + ._parallel_download_inner(path, tenant_id, partial.clone()) + .await + { + Ok(()) => { + if let Err(e) = tokio::fs::rename(&partial, &write_path).await { + let _ = tokio::fs::remove_file(&partial).await; + return Err(e.into()); + } + Ok(()) + } + Err(e) => { + let _ = tokio::fs::remove_file(&partial).await; + Err(e) + } + } + } + + #[tracing::instrument( + name = "gcs.parallel_download", + skip(self, partial_path), + fields(path = %path, tenant = ?tenant_id, total_bytes = tracing::field::Empty, chunks = tracing::field::Empty), + err + )] + async fn _parallel_download_inner( + &self, + path: &RelativePath, + tenant_id: &Option, + partial_path: PathBuf, + ) -> Result<(), ObjectStorageError> { + let tenant_str = tenant_id.as_deref().unwrap_or(DEFAULT_TENANT); + let date = Utc::now().date_naive().to_string(); + let src = to_object_store_path(path); + + let meta = self.client.head(&src).await?; + increment_object_store_calls_by_date("HEAD", &date, tenant_str); + let total = meta.size; + + if let Some(parent) = partial_path.parent() { + tokio::fs::create_dir_all(parent).await?; + } + let file = tokio::fs::File::create(&partial_path).await?; + file.set_len(total).await?; + let std_file = Arc::new(file.into_std().await); + + let chunk = PARSEABLE.options.hot_tier_download_chunk_size; + let concurrency = PARSEABLE.options.hot_tier_download_concurrency; + let ranges: Vec> = (0..total) + .step_by(chunk as usize) + .map(|s| s..(s + chunk).min(total)) + .collect(); + let chunk_count = ranges.len() as u64; + tracing::Span::current() + .record("total_bytes", total) + .record("chunks", chunk_count); + let semaphore = Arc::new(tokio::sync::Semaphore::new(concurrency as usize)); + + futures::stream::iter(ranges) + .map(|r| { + let src = src.clone(); + let std_file = std_file.clone(); + let semaphore = semaphore.clone(); + async move { + let _permit = semaphore.acquire_owned().await.map_err(|e| { + ObjectStorageError::Custom(format!("semaphore closed: {e}")) + })?; + let bytes = self.client.get_range(&src, r.clone()).await?; + let offset = r.start; + tokio::task::spawn_blocking(move || -> std::io::Result<()> { + crate::storage::write_all_at(&std_file, &bytes, offset) + }) + .await + .map_err(|e| ObjectStorageError::Custom(format!("join: {e}")))??; + Ok::<_, ObjectStorageError>(()) + } + }) + .buffer_unordered(concurrency as usize) + .try_collect::>() + .await?; + + let std_file_sync = std_file.clone(); + tokio::task::spawn_blocking(move || std_file_sync.sync_all()) + .await + .map_err(|e| ObjectStorageError::Custom(format!("join: {e}")))??; + + increment_object_store_calls_by_date("GET", &date, tenant_str); + increment_files_scanned_in_object_store_calls_by_date("GET", 1, &date, tenant_str); + increment_bytes_scanned_in_object_store_calls_by_date("GET", total, &date, tenant_str); + increment_partial_file_scans_in_object_store_calls_by_date( + "GET", + chunk_count, + &date, + tenant_str, + ); + Ok(()) + } + + #[tracing::instrument( + name = "gcs.get_object", + skip(self), + fields(path = %path, tenant = ?tenant_id, bytes = tracing::field::Empty), + err + )] async fn _get_object( &self, path: &RelativePath, @@ -180,6 +301,7 @@ impl Gcs { match resp { Ok(resp) => { let body: Bytes = resp.bytes().await?; + tracing::Span::current().record("bytes", body.len()); increment_files_scanned_in_object_store_calls_by_date( "GET", 1, @@ -198,6 +320,11 @@ impl Gcs { } } + #[tracing::instrument( + name = "gcs.put_object", + skip(self, resource), + fields(path = %path, tenant = ?tenant_id, bytes = resource.content_length()) + )] async fn _put_object( &self, path: &RelativePath, @@ -221,6 +348,12 @@ impl Gcs { } } + #[tracing::instrument( + name = "gcs.delete_prefix", + skip(self), + fields(prefix = %key, tenant = ?tenant_id, deleted = tracing::field::Empty, failed = tracing::field::Empty), + err + )] async fn _delete_prefix( &self, key: &str, @@ -251,6 +384,9 @@ impl Gcs { } let total_files = files_deleted + failed_deletes; + tracing::Span::current() + .record("deleted", files_deleted) + .record("failed", failed_deletes); increment_files_scanned_in_object_store_calls_by_date( "LIST", total_files, @@ -273,6 +409,12 @@ impl Gcs { Ok(()) } + #[tracing::instrument( + name = "gcs.list_dates", + skip(self), + fields(stream = %stream, tenant = ?tenant_id, dates = tracing::field::Empty), + err + )] async fn _list_dates( &self, stream: &str, @@ -307,10 +449,16 @@ impl Gcs { .filter_map(|path| path.as_ref().strip_prefix(&format!("{stream}/"))) .map(String::from) .collect(); + tracing::Span::current().record("dates", dates.len()); Ok(dates) } + #[tracing::instrument( + name = "gcs.upload_file", + skip(self), + fields(key = %key, path = %path.display(), tenant = ?tenant_id) + )] async fn _upload_file( &self, key: &str, @@ -335,6 +483,11 @@ impl Gcs { } } + #[tracing::instrument( + name = "gcs.upload_multipart", + skip(self), + fields(key = %key, path = %path.display(), tenant = ?tenant_id, total_bytes = tracing::field::Empty) + )] async fn _upload_multipart( &self, key: &RelativePath, @@ -346,6 +499,7 @@ impl Gcs { let tenant = tenant_id.as_deref().unwrap_or(DEFAULT_TENANT); let meta = file.metadata().await?; let total_size = meta.len() as usize; + tracing::Span::current().record("total_bytes", total_size); let min_multipart_size = PARSEABLE.options.min_multipart_size as usize; if total_size < min_multipart_size || !PARSEABLE.options.enable_multipart { let mut data = Vec::new(); @@ -431,6 +585,14 @@ impl Gcs { #[async_trait] impl ObjectStorage for Gcs { + async fn parallel_chunked_download( + &self, + path: &RelativePath, + tenant_id: &Option, + write_path: PathBuf, + ) -> Result<(), ObjectStorageError> { + self._parallel_download(path, tenant_id, write_path).await + } async fn get_buffered_reader( &self, path: &RelativePath, @@ -455,8 +617,7 @@ impl ObjectStorage for Gcs { } }; - let store: Arc = self.client.clone(); - let buf = object_store::buffered::BufReader::new(store, &meta); + let buf = object_store::buffered::BufReader::new(self.client.clone(), &meta); Ok(buf) } @@ -469,6 +630,12 @@ impl ObjectStorage for Gcs { self._upload_multipart(key, path, tenant_id).await } + #[tracing::instrument( + name = "gcs.head", + skip(self), + fields(path = %path, tenant = ?tenant_id), + err + )] async fn head( &self, path: &RelativePath, @@ -609,6 +776,12 @@ impl ObjectStorage for Gcs { Ok(()) } + #[tracing::instrument( + name = "gcs.delete_object", + skip(self), + fields(path = %path, tenant = ?tenant_id), + err + )] async fn delete_object( &self, path: &RelativePath, @@ -633,11 +806,18 @@ impl ObjectStorage for Gcs { Ok(result?) } + #[tracing::instrument( + name = "gcs.check", + skip(self), + fields(tenant = ?tenant_id, ok = tracing::field::Empty), + err + )] async fn check(&self, tenant_id: &Option) -> Result<(), ObjectStorageError> { let result = self .client .head(&to_object_store_path(&parseable_json_path())) .await; + tracing::Span::current().record("ok", result.is_ok()); let tenant = tenant_id.as_deref().unwrap_or(DEFAULT_TENANT); increment_object_store_calls_by_date("HEAD", &Utc::now().date_naive().to_string(), tenant); diff --git a/src/storage/localfs.rs b/src/storage/localfs.rs index 6f981981c..97d909eb6 100644 --- a/src/storage/localfs.rs +++ b/src/storage/localfs.rs @@ -106,6 +106,28 @@ impl LocalFS { #[async_trait] impl ObjectStorage for LocalFS { + async fn parallel_chunked_download( + &self, + path: &RelativePath, + _tenant_id: &Option, + write_path: PathBuf, + ) -> Result<(), ObjectStorageError> { + let src = self.path_in_root(path); + let partial = super::partial_path(&write_path)?; + if let Some(parent) = partial.parent() { + fs::create_dir_all(parent).await?; + } + match fs::copy(&src, &partial).await { + Ok(_) => { + fs::rename(&partial, &write_path).await?; + Ok(()) + } + Err(e) => { + let _ = fs::remove_file(&partial).await; + Err(e.into()) + } + } + } async fn upload_multipart( &self, key: &RelativePath, diff --git a/src/storage/metrics_layer.rs b/src/storage/metrics_layer.rs index ac82e43d6..297e65eec 100644 --- a/src/storage/metrics_layer.rs +++ b/src/storage/metrics_layer.rs @@ -31,7 +31,34 @@ use object_store::{ Result as ObjectStoreResult, path::Path, }; -use crate::metrics::STORAGE_REQUEST_RESPONSE_TIME; +use crate::metrics::{STORAGE_REQUEST_RESPONSE_TIME, STORAGE_REQUESTS_INFLIGHT}; + +/// RAII guard that increments the in-flight gauge on construction and +/// decrements on drop. Handles early returns, panics, and dropped futures. +struct InflightGuard { + provider: String, + method: &'static str, +} + +impl InflightGuard { + fn new(provider: &str, method: &'static str) -> Self { + STORAGE_REQUESTS_INFLIGHT + .with_label_values(&[provider, method]) + .inc(); + Self { + provider: provider.to_string(), + method, + } + } +} + +impl Drop for InflightGuard { + fn drop(&mut self) { + STORAGE_REQUESTS_INFLIGHT + .with_label_values(&[&self.provider, self.method]) + .dec(); + } +} // Public helper function to map object_store errors to HTTP status codes pub fn error_to_status_code(err: &object_store::Error) -> &'static str { @@ -91,6 +118,7 @@ impl ObjectStore for MetricLayer { payload: PutPayload, opts: PutOptions, ) -> ObjectStoreResult { + let _guard = InflightGuard::new(&self.provider, "PUT"); let time = time::Instant::now(); let put_result = self.inner.put_opts(location, payload, opts).await; let elapsed = time.elapsed().as_secs_f64(); @@ -111,6 +139,7 @@ impl ObjectStore for MetricLayer { location: &Path, opts: PutMultipartOptions, ) -> ObjectStoreResult> { + let _guard = InflightGuard::new(&self.provider, "PUT_MULTIPART"); let time = time::Instant::now(); let result = self.inner.put_multipart_opts(location, opts).await; let elapsed = time.elapsed().as_secs_f64(); @@ -127,6 +156,7 @@ impl ObjectStore for MetricLayer { } async fn get_opts(&self, location: &Path, options: GetOptions) -> ObjectStoreResult { + let _guard = InflightGuard::new(&self.provider, "GET"); let time = time::Instant::now(); let result = self.inner.get_opts(location, options).await; let elapsed = time.elapsed().as_secs_f64(); @@ -147,6 +177,7 @@ impl ObjectStore for MetricLayer { location: &Path, ranges: &[Range], ) -> ObjectStoreResult> { + let _guard = InflightGuard::new(&self.provider, "GET_RANGES"); let time = time::Instant::now(); let result = self.inner.get_ranges(location, ranges).await; let elapsed = time.elapsed().as_secs_f64(); @@ -170,6 +201,7 @@ impl ObjectStore for MetricLayer { } fn list(&self, prefix: Option<&Path>) -> BoxStream<'static, ObjectStoreResult> { + let _guard = InflightGuard::new(&self.provider, "LIST"); let time = time::Instant::now(); let inner = self.inner.list(prefix); let res = StreamMetricWrapper { @@ -177,6 +209,7 @@ impl ObjectStore for MetricLayer { provider: self.provider.clone(), method: "LIST", status: "200", + _guard, inner, }; Box::pin(res) @@ -187,6 +220,7 @@ impl ObjectStore for MetricLayer { prefix: Option<&Path>, offset: &Path, ) -> BoxStream<'static, ObjectStoreResult> { + let _guard = InflightGuard::new(&self.provider, "LIST_OFFSET"); let time = time::Instant::now(); let inner = self.inner.list_with_offset(prefix, offset); let res = StreamMetricWrapper { @@ -194,6 +228,7 @@ impl ObjectStore for MetricLayer { provider: self.provider.clone(), method: "LIST_OFFSET", status: "200", + _guard, inner, }; @@ -201,6 +236,7 @@ impl ObjectStore for MetricLayer { } async fn list_with_delimiter(&self, prefix: Option<&Path>) -> ObjectStoreResult { + let _guard = InflightGuard::new(&self.provider, "LIST_DELIM"); let time = time::Instant::now(); let result = self.inner.list_with_delimiter(prefix).await; let elapsed = time.elapsed().as_secs_f64(); @@ -222,6 +258,7 @@ impl ObjectStore for MetricLayer { to: &Path, options: CopyOptions, ) -> ObjectStoreResult<()> { + let _guard = InflightGuard::new(&self.provider, "COPY"); let time = time::Instant::now(); let result = self.inner.copy_opts(from, to, options).await; let elapsed = time.elapsed().as_secs_f64(); @@ -243,6 +280,7 @@ impl ObjectStore for MetricLayer { to: &Path, options: RenameOptions, ) -> ObjectStoreResult<()> { + let _guard = InflightGuard::new(&self.provider, "RENAME"); let time = time::Instant::now(); let result = self.inner.rename_opts(from, to, options).await; let elapsed = time.elapsed().as_secs_f64(); @@ -264,6 +302,7 @@ struct StreamMetricWrapper<'a, T> { provider: String, method: &'static str, status: &'static str, + _guard: InflightGuard, inner: BoxStream<'a, T>, } diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 2ca7a10de..e40100195 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -48,6 +48,44 @@ pub mod retention; mod s3; pub mod store_metadata; +/// Cross-platform positional write: pwrite(2) on Unix, seek_write+loop on Windows. +/// Both APIs accept `&File`, so concurrent ranged downloads can share an Arc. +#[inline(always)] +pub(crate) fn write_all_at(file: &std::fs::File, buf: &[u8], offset: u64) -> std::io::Result<()> { + #[cfg(unix)] + { + use std::os::unix::fs::FileExt; + file.write_all_at(buf, offset) + } + #[cfg(windows)] + { + use std::os::windows::fs::FileExt; + let mut buf = buf; + let mut offset = offset; + while !buf.is_empty() { + match file.seek_write(buf, offset) { + Ok(0) => { + return Err(std::io::Error::new( + std::io::ErrorKind::WriteZero, + "failed to write whole buffer", + )); + } + Ok(n) => { + buf = &buf[n..]; + offset += n as u64; + } + Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => {} + Err(e) => return Err(e), + } + } + Ok(()) + } + #[cfg(not(any(unix, windows)))] + { + compile_error!("write_all_at: unsupported platform"); + } +} + use self::retention::Retention; pub use azure_blob::AzureBlobConfig; pub use gcs::GcsConfig; @@ -83,7 +121,7 @@ pub const CURRENT_OBJECT_STORE_VERSION: &str = "v7"; pub const CURRENT_SCHEMA_VERSION: &str = "v7"; const CONNECT_TIMEOUT_SECS: u64 = 5; -const REQUEST_TIMEOUT_SECS: u64 = 300; +const REQUEST_TIMEOUT_SECS: u64 = 30; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ObjectStoreFormat { @@ -344,3 +382,18 @@ pub enum ObjectStorageError { pub fn to_object_store_path(path: &RelativePath) -> Path { Path::from(path.as_str()) } + +/// Append `.partial` to the file name of a local path. Used by hot-tier +/// downloaders to write to a sibling path and atomically rename on success. +pub fn partial_path( + write_path: &std::path::Path, +) -> Result { + let name = write_path + .file_name() + .ok_or_else(|| ObjectStorageError::Custom("download write_path has no file name".into()))?; + let mut next = std::ffi::OsString::from(name); + next.push(".partial"); + let mut buf = write_path.to_path_buf(); + buf.set_file_name(next); + Ok(buf) +} diff --git a/src/storage/object_storage.rs b/src/storage/object_storage.rs index 2e58a0df5..b67cf18f9 100644 --- a/src/storage/object_storage.rs +++ b/src/storage/object_storage.rs @@ -98,6 +98,11 @@ pub(crate) struct UploadResult { } /// Handles the upload of a single parquet file +#[tracing::instrument( + name = "object_store.upload_single_parquet", + skip(store, schema), + fields(stream = %stream_name, tenant = ?tenant_id, path = %path.display(), key = %stream_relative_path) +)] async fn upload_single_parquet_file( store: Arc, path: std::path::PathBuf, @@ -240,6 +245,11 @@ async fn calculate_stats_if_enabled( } /// Validates that a parquet file uploaded to object storage matches the staging file size +#[tracing::instrument( + name = "object_store.validate_uploaded_parquet", + skip(store), + fields(stream = %stream_name, tenant = ?tenant_id, key = %stream_relative_path, expected_size) +)] async fn validate_uploaded_parquet_file( store: &Arc, stream_relative_path: &str, @@ -302,6 +312,12 @@ pub trait ObjectStorage: Debug + Send + Sync + 'static { path: &RelativePath, tenant_id: &Option, ) -> Result; + async fn parallel_chunked_download( + &self, + path: &RelativePath, + tenant_id: &Option, + write_path: PathBuf, + ) -> Result<(), ObjectStorageError>; async fn get_object( &self, path: &RelativePath, @@ -998,6 +1014,12 @@ pub trait ObjectStorage: Debug + Send + Sync + 'static { // pick a better name fn get_bucket_name(&self) -> String; + #[tracing::instrument( + name = "object_store.upload_files_from_staging", + skip(self), + fields(stream = %stream_name, tenant = ?tenant_id), + err + )] async fn upload_files_from_staging( &self, stream_name: &str, @@ -1027,6 +1049,18 @@ pub trait ObjectStorage: Debug + Send + Sync + 'static { } /// Processes parquet files concurrently and returns stats status and manifest files +#[tracing::instrument( + name = "object_store.process_parquet_files", + skip(upload_context), + fields( + stream = %stream_name, + tenant = ?tenant_id, + parquet_count = tracing::field::Empty, + total_size_bytes = tracing::field::Empty, + smallest_date = tracing::field::Empty, + largest_date = tracing::field::Empty, + ) +)] async fn process_parquet_files( upload_context: &UploadContext, stream_name: &str, @@ -1053,6 +1087,27 @@ async fn process_parquet_files( ret }; + let mut total_size: u64 = 0; + let mut min_dt: Option> = None; + let mut max_dt: Option> = None; + for path in &parquet_paths { + if let Ok(meta) = path.metadata() { + total_size = total_size.saturating_add(meta.len()); + } + if let Ok(dt) = extract_datetime_from_parquet_path_regex(path) { + min_dt = Some(min_dt.map_or(dt, |cur| cur.min(dt))); + max_dt = Some(max_dt.map_or(dt, |cur| cur.max(dt))); + } + } + let span = tracing::Span::current(); + span.record("parquet_count", parquet_paths.len()); + span.record("total_size_bytes", total_size); + if let Some(dt) = min_dt { + span.record("smallest_date", tracing::field::display(dt)); + } + if let Some(dt) = max_dt { + span.record("largest_date", tracing::field::display(dt)); + } // Spawn upload tasks for each parquet file for path in parquet_paths { spawn_parquet_upload_task( diff --git a/src/storage/s3.rs b/src/storage/s3.rs index 169d3c508..b66bd5a6e 100644 --- a/src/storage/s3.rs +++ b/src/storage/s3.rs @@ -17,7 +17,13 @@ */ use std::{ - collections::HashSet, fmt::Display, path::Path, str::FromStr, sync::Arc, time::Duration, + collections::HashSet, + fmt::Display, + ops::Range, + path::{Path, PathBuf}, + str::FromStr, + sync::Arc, + time::Duration, }; use async_trait::async_trait; @@ -48,6 +54,7 @@ use crate::{ increment_bytes_scanned_in_object_store_calls_by_date, increment_files_scanned_in_object_store_calls_by_date, increment_object_store_calls_by_date, + increment_partial_file_scans_in_object_store_calls_by_date, }, parseable::{DEFAULT_TENANT, LogStream, PARSEABLE}, }; @@ -55,7 +62,8 @@ use crate::{ use super::{ CONNECT_TIMEOUT_SECS, ObjectStorage, ObjectStorageError, ObjectStorageProvider, PARSEABLE_ROOT_DIRECTORY, REQUEST_TIMEOUT_SECS, STREAM_METADATA_FILE_NAME, - metrics_layer::MetricLayer, object_storage::parseable_json_path, to_object_store_path, + metrics_layer::MetricLayer, object_storage::parseable_json_path, partial_path, + to_object_store_path, }; // in bytes @@ -228,6 +236,8 @@ impl S3Config { let mut client_options = ClientOptions::default() .with_allow_http(true) .with_connect_timeout(Duration::from_secs(CONNECT_TIMEOUT_SECS)) + .with_pool_idle_timeout(Duration::from_secs(15)) + .with_pool_max_idle_per_host(128) .with_timeout(Duration::from_secs(REQUEST_TIMEOUT_SECS)); if self.skip_tls { @@ -235,7 +245,7 @@ impl S3Config { } let retry_config = RetryConfig { max_retries: 5, - retry_timeout: Duration::from_secs(30), + retry_timeout: Duration::from_secs(5), backoff: BackoffConfig::default(), }; @@ -310,9 +320,11 @@ impl ObjectStorageProvider for S3Config { fn construct_client(&self) -> Arc { let s3 = self.get_default_builder().build().unwrap(); - + // limit objectstore to a concurrent request limit + let s3 = LimitStore::new(s3, super::MAX_OBJECT_STORE_REQUESTS); + let s3 = MetricLayer::new(s3, "s3"); Arc::new(S3 { - client: s3, + client: Arc::new(s3), bucket: self.bucket_name.clone(), root: StorePath::from(""), }) @@ -325,12 +337,123 @@ impl ObjectStorageProvider for S3Config { #[derive(Debug)] pub struct S3 { - client: AmazonS3, + client: Arc>>, bucket: String, root: StorePath, } impl S3 { + async fn _parallel_download( + &self, + path: &RelativePath, + tenant_id: &Option, + write_path: PathBuf, + ) -> Result<(), ObjectStorageError> { + let partial = partial_path(&write_path)?; + match self + ._parallel_download_inner(path, tenant_id, partial.clone()) + .await + { + Ok(()) => { + if let Err(e) = tokio::fs::rename(&partial, &write_path).await { + let _ = tokio::fs::remove_file(&partial).await; + return Err(e.into()); + } + Ok(()) + } + Err(e) => { + let _ = tokio::fs::remove_file(&partial).await; + Err(e) + } + } + } + + #[tracing::instrument( + name = "s3.parallel_download", + skip(self, partial_path), + fields(path = %path, tenant = ?tenant_id, total_bytes = tracing::field::Empty, chunks = tracing::field::Empty), + err + )] + async fn _parallel_download_inner( + &self, + path: &RelativePath, + tenant_id: &Option, + partial_path: PathBuf, + ) -> Result<(), ObjectStorageError> { + let tenant_str = tenant_id.as_deref().unwrap_or(DEFAULT_TENANT); + let date = Utc::now().date_naive().to_string(); + let src = to_object_store_path(path); + + let meta = self.client.head(&src).await?; + increment_object_store_calls_by_date("HEAD", &date, tenant_str); + let total = meta.size; + + if let Some(parent) = partial_path.parent() { + tokio::fs::create_dir_all(parent).await?; + } + let file = tokio::fs::File::create(&partial_path).await?; + file.set_len(total).await?; + let std_file = Arc::new(file.into_std().await); + + let chunk = PARSEABLE.options.hot_tier_download_chunk_size; + let concurrency = PARSEABLE.options.hot_tier_download_concurrency; + let ranges: Vec> = (0..total) + .step_by(chunk as usize) + .map(|s| s..(s + chunk).min(total)) + .collect(); + let chunk_count = ranges.len() as u64; + tracing::Span::current() + .record("total_bytes", total) + .record("chunks", chunk_count); + + let semaphore = Arc::new(tokio::sync::Semaphore::new(concurrency as usize)); + + futures::stream::iter(ranges) + .map(|r| { + let src = src.clone(); + let std_file = std_file.clone(); + let semaphore = semaphore.clone(); + async move { + let _permit = semaphore.acquire_owned().await.map_err(|e| { + ObjectStorageError::Custom(format!("semaphore closed: {e}")) + })?; + let bytes = self.client.get_range(&src, r.clone()).await?; + let offset = r.start; + tokio::task::spawn_blocking(move || -> std::io::Result<()> { + crate::storage::write_all_at(&std_file, &bytes, offset) + }) + .await + .map_err(|e| ObjectStorageError::Custom(format!("join: {e}")))??; + Ok::<_, ObjectStorageError>(()) + } + }) + .buffer_unordered(concurrency as usize) + .try_collect::>() + .await?; + + let std_file_sync = std_file.clone(); + tokio::task::spawn_blocking(move || std_file_sync.sync_all()) + .await + .map_err(|e| ObjectStorageError::Custom(format!("join: {e}")))??; + + increment_object_store_calls_by_date("GET", &date, tenant_str); + increment_files_scanned_in_object_store_calls_by_date("GET", 1, &date, tenant_str); + increment_bytes_scanned_in_object_store_calls_by_date("GET", total, &date, tenant_str); + increment_partial_file_scans_in_object_store_calls_by_date( + "GET", + chunk_count, + &date, + tenant_str, + ); + Ok(()) + } + + #[tracing::instrument( + name = "s3.get_object", + skip(self), + fields(path = %path, tenant = ?tenant_id, bytes = tracing::field::Empty), + err + )] async fn _get_object( &self, path: &RelativePath, @@ -347,6 +470,7 @@ impl S3 { match resp { Ok(resp) => { let body = resp.bytes().await?; + tracing::Span::current().record("bytes", body.len()); increment_files_scanned_in_object_store_calls_by_date( "GET", 1, @@ -365,6 +489,11 @@ impl S3 { } } + #[tracing::instrument( + name = "s3.put_object", + skip(self, resource), + fields(path = %path, tenant = ?tenant_id, bytes = resource.content_length()) + )] async fn _put_object( &self, path: &RelativePath, @@ -392,6 +521,12 @@ impl S3 { } } + #[tracing::instrument( + name = "s3.delete_prefix", + skip(self), + fields(prefix = %key, tenant = ?tenant_id, deleted = tracing::field::Empty, failed = tracing::field::Empty), + err + )] async fn _delete_prefix( &self, key: &str, @@ -426,6 +561,9 @@ impl S3 { } let total_files = files_deleted + failed_deletes; + tracing::Span::current() + .record("deleted", files_deleted) + .record("failed", failed_deletes); increment_files_scanned_in_object_store_calls_by_date( "LIST", total_files, @@ -448,6 +586,12 @@ impl S3 { Ok(()) } + #[tracing::instrument( + name = "s3.list_dates", + skip(self), + fields(stream = %stream, tenant = ?tenant_id, dates = tracing::field::Empty), + err + )] async fn _list_dates( &self, stream: &str, @@ -490,10 +634,16 @@ impl S3 { .filter_map(|path| path.as_ref().strip_prefix(&prefix)) .map(String::from) .collect(); + tracing::Span::current().record("dates", dates.len()); Ok(dates) } + #[tracing::instrument( + name = "s3.upload_file", + skip(self), + fields(key = %key, path = %path.display(), tenant = ?tenant_id) + )] async fn _upload_file( &self, key: &str, @@ -523,6 +673,11 @@ impl S3 { } } + #[tracing::instrument( + name = "s3.upload_multipart", + skip(self), + fields(key = %key, path = %path.display(), tenant = ?tenant_id, total_bytes = tracing::field::Empty, parts = tracing::field::Empty) + )] async fn _upload_multipart( &self, key: &RelativePath, @@ -579,6 +734,9 @@ impl S3 { let has_final_partial_part = !total_size.is_multiple_of(min_multipart_size); let num_full_parts = total_size / min_multipart_size; let total_parts = num_full_parts + if has_final_partial_part { 1 } else { 0 }; + tracing::Span::current() + .record("total_bytes", total_size) + .record("parts", total_parts); // Upload each part with metrics for part_number in 0..(total_parts) { @@ -648,8 +806,7 @@ impl ObjectStorage for S3 { } }; - let store: Arc = Arc::new(self.client.clone()); - let buf = object_store::buffered::BufReader::new(store, &meta); + let buf = object_store::buffered::BufReader::new(self.client.clone(), &meta); Ok(buf) } @@ -662,6 +819,12 @@ impl ObjectStorage for S3 { self._upload_multipart(key, path, tenant_id).await } + #[tracing::instrument( + name = "s3.head", + skip(self), + fields(path = %path, tenant = ?tenant_id), + err + )] async fn head( &self, path: &RelativePath, @@ -686,12 +849,21 @@ impl ObjectStorage for S3 { Ok(result?) } + async fn parallel_chunked_download( + &self, + path: &RelativePath, + tenant_id: &Option, + write_path: PathBuf, + ) -> Result<(), ObjectStorageError> { + self._parallel_download(path, tenant_id, write_path).await + } + async fn get_object( &self, path: &RelativePath, tenant_id: &Option, ) -> Result { - Ok(self._get_object(path, tenant_id).await?) + self._get_object(path, tenant_id).await } async fn get_objects( @@ -815,6 +987,12 @@ impl ObjectStorage for S3 { Ok(()) } + #[tracing::instrument( + name = "s3.delete_object", + skip(self), + fields(path = %path, tenant = ?tenant_id), + err + )] async fn delete_object( &self, path: &RelativePath, @@ -839,12 +1017,19 @@ impl ObjectStorage for S3 { Ok(result?) } + #[tracing::instrument( + name = "s3.check", + skip(self), + fields(tenant = ?tenant_id, ok = tracing::field::Empty), + err + )] async fn check(&self, tenant_id: &Option) -> Result<(), ObjectStorageError> { let tenant_str = tenant_id.as_deref().unwrap_or(DEFAULT_TENANT); let result = self .client .head(&to_object_store_path(&parseable_json_path())) .await; + tracing::Span::current().record("ok", result.is_ok()); increment_object_store_calls_by_date( "HEAD", &Utc::now().date_naive().to_string(), diff --git a/src/sync.rs b/src/sync.rs index a9c5d4485..14a88ab38 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -277,34 +277,42 @@ pub fn local_sync() -> ( /// local and object store sync at the start of the server #[tokio::main(flavor = "current_thread")] pub async fn sync_start() -> anyhow::Result<()> { - // Monitor local sync duration at startup - monitor_task_duration( - "startup_local_sync", - Duration::from_secs(PARSEABLE.options.local_sync_threshold), - || async { - let mut local_sync_joinset = JoinSet::new(); - PARSEABLE - .streams - .flush_and_convert(&mut local_sync_joinset, true, false); - while let Some(res) = local_sync_joinset.join_next().await { - log_join_result(res, "flush and convert"); - } - }, - ) + async { + // Monitor local sync duration at startup + monitor_task_duration( + "startup_local_sync", + Duration::from_secs(PARSEABLE.options.local_sync_threshold), + || async { + let mut local_sync_joinset = JoinSet::new(); + PARSEABLE + .streams + .flush_and_convert(&mut local_sync_joinset, true, false); + while let Some(res) = local_sync_joinset.join_next().await { + log_join_result(res, "flush and convert"); + } + }, + ) + .await; + } + .instrument(info_span!("local_sync_startup")) .await; - // Monitor object store sync duration at startup - monitor_task_duration( - "startup_object_store_sync", - Duration::from_secs(PARSEABLE.options.object_store_sync_threshold), - || async { - let mut object_store_joinset = JoinSet::new(); - sync_all_streams(&mut object_store_joinset); - while let Some(res) = object_store_joinset.join_next().await { - log_join_result(res, "object store sync"); - } - }, - ) + async { + // Monitor object store sync duration at startup + monitor_task_duration( + "startup_object_store_sync", + Duration::from_secs(PARSEABLE.options.object_store_sync_threshold), + || async { + let mut object_store_joinset = JoinSet::new(); + sync_all_streams(&mut object_store_joinset); + while let Some(res) = object_store_joinset.join_next().await { + log_join_result(res, "object store sync"); + } + }, + ) + .await; + } + .instrument(info_span!("object_store_sync_startup")) .await; Ok(()) diff --git a/src/telemetry.rs b/src/telemetry.rs index 4e4d7dfdb..7c67e68f3 100644 --- a/src/telemetry.rs +++ b/src/telemetry.rs @@ -24,6 +24,7 @@ use opentelemetry_sdk::{ propagation::TraceContextPropagator, trace::{BatchSpanProcessor, SdkTracerProvider}, }; + // Consts describing the env vars const OTEL_EXPORTER_OTLP_ENDPOINT: &str = "OTEL_EXPORTER_OTLP_ENDPOINT"; const OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: &str = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"; @@ -49,7 +50,7 @@ const OTEL_EXPORTER_OTLP_PROTOCOL: &str = "OTEL_EXPORTER_OTLP_PROTOCOL"; /// /// Returns \`None\` when \`OTEL_EXPORTER_OTLP_ENDPOINT\` or `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` is not set (OTEL disabled). /// The caller must call \`provider.shutdown()\` before process exit. -pub fn init_tracing() -> Option { +pub fn init_tracing(service: &'static str) -> Option { // Only used to decide whether OTEL is enabled; the SDK reads it again // from env to build the exporter (which also appends /v1/traces for HTTP). if std::env::var(OTEL_EXPORTER_OTLP_ENDPOINT).is_err() @@ -101,7 +102,7 @@ pub fn init_tracing() -> Option { // migration tables to rewrite attribute names across semconv versions — // so even if upstream semconv drifts, emitted telemetry remains translatable. let resource = Resource::builder_empty() - .with_service_name("parseable") + .with_service_name(service) .with_schema_url( std::iter::empty::(), "https://opentelemetry.io/schemas/1.56.0", diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 237094444..a561215ac 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -40,7 +40,16 @@ use actix_web::http::header::HeaderMap; use actix_web::{FromRequest, HttpRequest}; use actix_web_httpauth::extractors::basic::BasicAuth; use chrono::{NaiveDate, NaiveDateTime, NaiveTime, Utc}; +use once_cell::sync::Lazy; use regex::Regex; + +/// Compiled once at startup; reused for every `extract_datetime` call. +/// Recompiling per call was the hot-path bottleneck for hot-tier work-list +/// construction (~50μs per call * 100k+ files per tick). +static PARTITION_DATETIME_RE: Lazy = Lazy::new(|| { + Regex::new(r"date=(\d{4}-\d{2}-\d{2})/hour=(\d{2})/minute=(\d{2})") + .expect("partition datetime regex is valid") +}); use sha2::{Digest, Sha256}; pub fn get_node_id() -> String { @@ -49,8 +58,7 @@ pub fn get_node_id() -> String { } pub fn extract_datetime(path: &str) -> Option { - let re = Regex::new(r"date=(\d{4}-\d{2}-\d{2})/hour=(\d{2})/minute=(\d{2})").unwrap(); - if let Some(caps) = re.captures(path) { + if let Some(caps) = PARTITION_DATETIME_RE.captures(path) { let date_str = caps.get(1)?.as_str(); let hour_str = caps.get(2)?.as_str(); let minute_str = caps.get(3)?.as_str(); From 44dcb244b76df12459a36eb5ce9958eee2802558 Mon Sep 17 00:00:00 2001 From: Nikhil Sinha <131262146+nikhilsinhaparseable@users.noreply.github.com> Date: Tue, 19 May 2026 11:31:37 +0530 Subject: [PATCH 07/47] chore: stdout for span exporter (#1647) if env OTEL_EXPORTER_OTLP_ENDPOINT or OTEL_EXPORTER_OTLP_TRACES_ENDPOINT set to `stdout` export the traces to stdout --- Cargo.lock | 12 ++++++ Cargo.toml | 1 + src/telemetry.rs | 97 +++++++++++++++++++++++++++++------------------- 3 files changed, 72 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 771270353..1172dd9be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3787,6 +3787,17 @@ dependencies = [ "tonic-prost", ] +[[package]] +name = "opentelemetry-stdout" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc8887887e169414f637b18751487cce4e095be787d23fad13c454e2fb1b3811" +dependencies = [ + "chrono", + "opentelemetry 0.31.0 (registry+https://github.com/rust-lang/crates.io-index)", + "opentelemetry_sdk 0.31.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "opentelemetry_sdk" version = "0.31.0" @@ -3959,6 +3970,7 @@ dependencies = [ "opentelemetry 0.31.0 (registry+https://github.com/rust-lang/crates.io-index)", "opentelemetry-otlp", "opentelemetry-proto 0.31.0 (git+https://github.com/open-telemetry/opentelemetry-rust/?rev=b096b70b2ffe9beb65a716cf47d5e5db80a9e930)", + "opentelemetry-stdout", "opentelemetry_sdk 0.31.0 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot", "parquet", diff --git a/Cargo.toml b/Cargo.toml index aa95af50a..23d720d31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -128,6 +128,7 @@ opentelemetry-otlp = { version = "0.31.1", features = [ "http-proto", "http-json", ] } +opentelemetry-stdout = "0.31.0" tracing-actix-web = "0.7" # Time and Date diff --git a/src/telemetry.rs b/src/telemetry.rs index 7c67e68f3..431d4938a 100644 --- a/src/telemetry.rs +++ b/src/telemetry.rs @@ -33,7 +33,8 @@ const OTEL_EXPORTER_OTLP_PROTOCOL: &str = "OTEL_EXPORTER_OTLP_PROTOCOL"; /// Initialise an OTLP tracer provider. /// /// **Required env var:** -/// - \`OTEL_EXPORTER_OTLP_ENDPOINT\` — collector address. +/// - \`OTEL_EXPORTER_OTLP_ENDPOINT\` — collector address, or the literal +/// value \`stdout\` to write spans as JSON to process stdout (no collector). /// For HTTP exporters the SDK appends the signal path automatically: /// e.g. \`http://localhost:4318\` → \`http://localhost:4318/v1/traces\`. /// Set a signal-specific var \`OTEL_EXPORTER_OTLP_TRACES_ENDPOINT\` to @@ -44,6 +45,7 @@ const OTEL_EXPORTER_OTLP_PROTOCOL: &str = "OTEL_EXPORTER_OTLP_PROTOCOL"; /// - \`grpc\` → gRPC / tonic (Jaeger, Tempo, …) /// - \`http/json\` → HTTP + JSON (Parseable OSS ingest at \`/v1/traces\`) /// - \`http/protobuf\` → HTTP + protobuf +/// Ignored when endpoint is \`stdout\`. /// - \`OTEL_EXPORTER_OTLP_HEADERS\` — comma-separated \`key=value\` pairs forwarded /// as gRPC metadata or HTTP headers, e.g. /// \`authorization=Basic ,x-p-stream=my-stream,x-p-log-source=otel-traces\` @@ -51,52 +53,73 @@ const OTEL_EXPORTER_OTLP_PROTOCOL: &str = "OTEL_EXPORTER_OTLP_PROTOCOL"; /// Returns \`None\` when \`OTEL_EXPORTER_OTLP_ENDPOINT\` or `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` is not set (OTEL disabled). /// The caller must call \`provider.shutdown()\` before process exit. pub fn init_tracing(service: &'static str) -> Option { - // Only used to decide whether OTEL is enabled; the SDK reads it again - // from env to build the exporter (which also appends /v1/traces for HTTP). - if std::env::var(OTEL_EXPORTER_OTLP_ENDPOINT).is_err() - && std::env::var(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT).is_err() - { + let endpoint = std::env::var(OTEL_EXPORTER_OTLP_ENDPOINT).ok(); + let traces_endpoint = std::env::var(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT).ok(); + if endpoint.is_none() && traces_endpoint.is_none() { return None; } + // Endpoint sentinel: route spans to stdout, skip OTLP exporter entirely. + // Either env var set to "stdout" (case-insensitive) triggers stdout mode. + let is_stdout_sentinel = |v: &Option| { + v.as_deref() + .is_some_and(|s| s.eq_ignore_ascii_case("stdout")) + }; + let use_stdout = is_stdout_sentinel(&endpoint) || is_stdout_sentinel(&traces_endpoint); + let protocol = std::env::var(OTEL_EXPORTER_OTLP_PROTOCOL).unwrap_or_else(|_| "http/json".to_string()); - // Build the exporter using the SDK's env-var-aware builders. + // Build the exporter and wrap it in a BatchSpanProcessor. The stdout + // exporter has a different concrete type, so we build the processor + // inside each match arm to keep the types monomorphic. + // // We intentionally do NOT call .with_endpoint() / .with_headers() / - // .with_metadata() here — the SDK reads OTEL_EXPORTER_OTLP_ENDPOINT and - // OTEL_EXPORTER_OTLP_HEADERS from the environment automatically, which + // .with_metadata() on OTLP builders — the SDK reads OTEL_EXPORTER_OTLP_ENDPOINT + // and OTEL_EXPORTER_OTLP_HEADERS from the environment automatically, which // preserves correct path-appending behaviour for HTTP exporters. - let exporter = match protocol.as_str() { - // ── gRPC ───────────────────────────────────────────────────────────── - "grpc" => opentelemetry_otlp::SpanExporter::builder() - .with_tonic() - .build(), - // ── HTTP/Protobuf ──────────────────────────────────────────────────── - "http/protobuf" => SpanExporter::builder() - .with_http() - .with_protocol(Protocol::HttpBinary) - .build(), - // ── HTTP/JSON ───────────────────────────────────────────────────────── - "http/json" => SpanExporter::builder() - .with_http() - .with_protocol(Protocol::HttpJson) - .build(), - // return none if an invalid value is set - other => { - tracing::warn!( - "Unknown OTEL_EXPORTER_OTLP_PROTOCOL value '{}'; disabling OTEL tracing. \ - Supported values: grpc, http/protobuf, http/json", - other - ); - return None; + let processor = if use_stdout { + let exporter = opentelemetry_stdout::SpanExporter::default(); + BatchSpanProcessor::builder(exporter).build() + } else { + match protocol.as_str() { + "grpc" => { + let exporter = opentelemetry_otlp::SpanExporter::builder() + .with_tonic() + .build() + .map_err(|e| tracing::warn!("Failed to build OTEL span exporter: {}", e)) + .ok()?; + BatchSpanProcessor::builder(exporter).build() + } + "http/protobuf" => { + let exporter = SpanExporter::builder() + .with_http() + .with_protocol(Protocol::HttpBinary) + .build() + .map_err(|e| tracing::warn!("Failed to build OTEL span exporter: {}", e)) + .ok()?; + BatchSpanProcessor::builder(exporter).build() + } + "http/json" => { + let exporter = SpanExporter::builder() + .with_http() + .with_protocol(Protocol::HttpJson) + .build() + .map_err(|e| tracing::warn!("Failed to build OTEL span exporter: {}", e)) + .ok()?; + BatchSpanProcessor::builder(exporter).build() + } + other => { + tracing::warn!( + "Unknown OTEL_EXPORTER_OTLP_PROTOCOL value '{}'; disabling OTEL tracing. \ + Supported values: grpc, http/protobuf, http/json", + other + ); + return None; + } } }; - let exporter = exporter - .map_err(|e| tracing::warn!("Failed to build OTEL span exporter: {}", e)) - .ok()?; - // Declare conformance to OTel Semantic Conventions v1.56.0 via schema_url. // Downstream collectors (e.g., the Schema Translate Processor) can apply // migration tables to rewrite attribute names across semconv versions — @@ -109,8 +132,6 @@ pub fn init_tracing(service: &'static str) -> Option { ) .build(); - let processor = BatchSpanProcessor::builder(exporter).build(); - let provider = SdkTracerProvider::builder() .with_span_processor(processor) .with_resource(resource) From b67b82861d6fb82f975109c1386df78ce6da5de8 Mon Sep 17 00:00:00 2001 From: Nikhil Sinha <131262146+nikhilsinhaparseable@users.noreply.github.com> Date: Thu, 21 May 2026 15:24:41 +0530 Subject: [PATCH 08/47] feat: add series hash in metrics ingestion, bounded streaming merge (#1648) add series hash in metrics ingestion, bounded streaming merge and sort by metric name for otel-metrics stream --- Cargo.lock | 1 + Cargo.toml | 1 + src/otel/metrics.rs | 174 ++++++++++++++++++++++++++++++++++++++- src/parseable/streams.rs | 123 +++++++++++++++++++++++++-- src/query/mod.rs | 60 +++++++------- 5 files changed, 323 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1172dd9be..0b2752d32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3985,6 +3985,7 @@ dependencies = [ "relative-path", "reqwest", "rstest", + "rustc-hash", "rustls", "rustls-pemfile", "sasl2-sys", diff --git a/Cargo.toml b/Cargo.toml index 23d720d31..15a67d435 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -182,6 +182,7 @@ thiserror = "2.0" ulid = { version = "1.0", features = ["serde"] } uuid = { version = "1", features = ["v4"] } xxhash-rust = { version = "0.8", features = ["xxh3"] } +rustc-hash = "2" futures-core = "0.3.31" tempfile = "3.20.0" lazy_static = "1.4.0" diff --git a/src/otel/metrics.rs b/src/otel/metrics.rs index 4e5f1d054..c6f017c28 100644 --- a/src/otel/metrics.rs +++ b/src/otel/metrics.rs @@ -23,6 +23,8 @@ use opentelemetry_proto::tonic::metrics::v1::{ }; use serde_json::{Map, Value}; +use rustc_hash::FxHasher; +use std::hash::Hasher; use tracing::info_span; use crate::metrics::increment_metrics_collected_by_date; @@ -31,7 +33,7 @@ use super::otel_utils::{ convert_epoch_nano_to_timestamp, insert_attributes, insert_number_if_some, }; -pub const OTEL_METRICS_KNOWN_FIELD_LIST: [&str; 36] = [ +pub const OTEL_METRICS_KNOWN_FIELD_LIST: [&str; 37] = [ "metric_name", "metric_description", "metric_unit", @@ -68,8 +70,58 @@ pub const OTEL_METRICS_KNOWN_FIELD_LIST: [&str; 36] = [ "scope_dropped_attributes_count", "resource_dropped_attributes_count", "resource_schema_url", + // Precomputed per-sample identity of the physical series. Stable + // u64 hash of `metric_name` + sorted attribute key/value pairs, + // stored as a decimal-encoded string so arrow-json infers Utf8 and + // we get byte-exact roundtrip. Int64/Float64 inference dropped bits + // for hashes near the high range; string sidesteps that entirely. + // Lets the query layer group samples into physical series via a + // single column read instead of decoding every label column and + // hashing per row. + "__series_hash", ]; +/// Compute a stable u64 identifier for the physical series a sample +/// belongs to. Hashes `metric_name` plus every attribute key/value pair +/// that survived OTel flattening — everything in the flattened data +/// point that isn't a known sample-level field is treated as a label. +/// +/// Hash output must be stable across process restarts and matching at +/// query time. Uses rustc-hash's FxHasher (fast, deterministic, +/// non-cryptographic) and feeds keys in sorted order so the hash +/// doesn't depend on JSON Map iteration order. +fn compute_series_hash(dp: &Map) -> u64 { + let known: std::collections::HashSet<&str> = + OTEL_METRICS_KNOWN_FIELD_LIST.iter().copied().collect(); + let mut label_pairs: Vec<(&str, String)> = dp + .iter() + .filter(|(k, _)| !known.contains(k.as_str())) + .map(|(k, v)| { + let v_str = match v { + Value::String(s) => s.clone(), + other => other.to_string(), + }; + (k.as_str(), v_str) + }) + .collect(); + label_pairs.sort_by(|a, b| a.0.cmp(b.0)); + + let mut hasher = FxHasher::default(); + // Include metric_name in the identity. Without it, two different + // metrics with identical label sets would collide into one series. + if let Some(Value::String(name)) = dp.get("metric_name") { + hasher.write(name.as_bytes()); + hasher.write(b"\0"); + } + for (k, v) in &label_pairs { + hasher.write(k.as_bytes()); + hasher.write(b"="); + hasher.write(v.as_bytes()); + hasher.write(b"\0"); + } + hasher.finish() +} + /// otel metrics event has json array for exemplar /// this function flatten the exemplar json array /// and returns a `Map` of the exemplar json @@ -564,6 +616,19 @@ fn process_resource_metrics( for (k, v) in &envelope { dp.insert(k.clone(), v.clone()); } + // Compute the physical-series hash AFTER envelope merge + // so resource/scope attributes participate in series + // identity (they're labels from the query layer's + // perspective). Computed once per data point — O(label + // count) per sample, ~200 ns at typical attribute counts. + let series_hash = compute_series_hash(&dp); + // Stored as decimal-encoded string. Arrow-json + // infers Utf8, preserving all 64 bits — Int64/Float64 + // inference truncated values near the high range. + dp.insert( + "__series_hash".to_string(), + Value::String(series_hash.to_string()), + ); vec_otel_json.push(Value::Object(dp)); } } @@ -655,3 +720,110 @@ fn flatten_data_point_flags(flags: u32) -> Map { ); data_point_flags_json } + +#[cfg(test)] +mod tests { + use super::*; + + fn make_dp() -> Map { + let mut dp = Map::new(); + dp.insert( + "metric_name".to_string(), + Value::String("counter.app.metric_0006".into()), + ); + dp.insert( + "time_unix_nano".to_string(), + Value::String("2026-05-19T09:00:00Z".into()), + ); + dp.insert("data_point_value".to_string(), Value::Number(1000.into())); + dp.insert("is_monotonic".to_string(), Value::Bool(true)); + dp.insert("service.name".to_string(), Value::String("api".into())); + dp.insert("http.method".to_string(), Value::String("GET".into())); + dp.insert("request.id".to_string(), Value::String("req-1".into())); + dp + } + + #[test] + fn series_hash_stable_across_runs() { + // Same input → same hash. Locks in the wire contract between + // ingest and query layers; any algorithm change here breaks + // grouping for already-ingested data. + let dp = make_dp(); + let h1 = compute_series_hash(&dp); + let h2 = compute_series_hash(&dp); + assert_eq!(h1, h2); + } + + #[test] + fn series_hash_independent_of_label_insertion_order() { + // serde_json::Map preserves insertion order; query side may see + // labels in different order. Hash must be insertion-order-agnostic. + let mut a = Map::new(); + a.insert("metric_name".to_string(), Value::String("m".into())); + a.insert("service.name".to_string(), Value::String("api".into())); + a.insert("http.method".to_string(), Value::String("GET".into())); + + let mut b = Map::new(); + b.insert("http.method".to_string(), Value::String("GET".into())); + b.insert("metric_name".to_string(), Value::String("m".into())); + b.insert("service.name".to_string(), Value::String("api".into())); + + assert_eq!(compute_series_hash(&a), compute_series_hash(&b)); + } + + #[test] + fn series_hash_changes_with_label_value() { + let dp = make_dp(); + let mut dp2 = dp.clone(); + dp2.insert("service.name".to_string(), Value::String("billing".into())); + assert_ne!(compute_series_hash(&dp), compute_series_hash(&dp2)); + } + + #[test] + fn series_hash_changes_with_metric_name() { + // Two different metrics with identical labels must hash to + // different values, otherwise samples for `requests_total` and + // `latency_seconds` would collide into one logical series. + let dp = make_dp(); + let mut dp2 = dp.clone(); + dp2.insert( + "metric_name".to_string(), + Value::String("other.metric".into()), + ); + assert_ne!(compute_series_hash(&dp), compute_series_hash(&dp2)); + } + + #[test] + fn series_hash_ignores_sample_level_fields() { + // time_unix_nano and data_point_value belong to the SAMPLE, not + // the series. Hash must be identical across samples of the same + // physical series taken at different times with different values. + let dp = make_dp(); + let mut dp_later = dp.clone(); + dp_later.insert( + "time_unix_nano".to_string(), + Value::String("2026-05-19T10:00:00Z".into()), + ); + dp_later.insert("data_point_value".to_string(), Value::Number(2000.into())); + assert_eq!(compute_series_hash(&dp), compute_series_hash(&dp_later)); + } + + #[test] + fn series_hash_distinguishes_label_kv_swap() { + // Pathological pair: {a=bc, d=e} vs {a=b, cd=e}. A naive + // concatenation hash would emit identical bytes. Delimiters in + // the hasher input prevent this — verify here so a future + // optimisation can't silently regress collision resistance. + let mut a = Map::new(); + a.insert("metric_name".to_string(), Value::String("m".into())); + a.insert("a".to_string(), Value::String("bc".into())); + a.insert("d".to_string(), Value::String("e".into())); + + let mut b = Map::new(); + b.insert("metric_name".to_string(), Value::String("m".into())); + b.insert("a".to_string(), Value::String("b".into())); + b.insert("cd".to_string(), Value::String("e".into())); + + assert_ne!(compute_series_hash(&a), compute_series_hash(&b)); + } +} diff --git a/src/parseable/streams.rs b/src/parseable/streams.rs index b7aec7ef2..6c75dec75 100644 --- a/src/parseable/streams.rs +++ b/src/parseable/streams.rs @@ -17,7 +17,7 @@ * */ -use arrow_array::RecordBatch; +use arrow_array::{ArrayRef, RecordBatch}; use arrow_ipc::reader::StreamReader; use arrow_schema::{Field, Fields, Schema}; use chrono::{NaiveDateTime, Timelike, Utc}; @@ -589,12 +589,26 @@ impl Stream { Encoding::DELTA_BINARY_PACKED, ); - // Create sorting columns - let mut sorting_column_vec = vec![SortingColumn { + // Build sorting columns. For OTel-metrics streams, put + // `metric_name` ahead of the time partition so per-page parquet + // min/max stats can prune by metric (PromQL's universal selector + // predicate). The actual row order is enforced at write time + // (`sort_batch_for_metric_pruning`) — this just advertises the + // sort in parquet footer metadata so readers can rely on it. + let is_otel_metrics = self.is_otel_metrics(); + let mut sorting_column_vec: Vec = Vec::new(); + if is_otel_metrics && let Ok(name_idx) = merged_schema.index_of("metric_name") { + sorting_column_vec.push(SortingColumn { + column_idx: name_idx as i32, + descending: false, + nulls_first: false, + }); + } + sorting_column_vec.push(SortingColumn { column_idx: time_partition_idx as i32, descending: true, - nulls_first: true, - }]; + nulls_first: false, + }); // Describe custom partition column encodings and sorting if let Some(custom_partition) = custom_partition { @@ -616,6 +630,66 @@ impl Stream { props.set_sorting_columns(Some(sorting_column_vec)).build() } + /// True if this stream's log_source carries the OTel-metrics + /// format. Determines whether per-batch sort and metric_name-first + /// SortingColumn metadata get applied at write time. + fn is_otel_metrics(&self) -> bool { + self.get_log_source() + .iter() + .any(|s| matches!(s.log_source_format, LogSource::OtelMetrics)) + } + + /// Permute a `RecordBatch` so rows are ordered by + /// `(metric_name ASC, time_partition DESC)`. Required for parquet + /// page-index pruning to be effective on PromQL's + /// `metric_name = 'X'` selector — without this, pages within a row + /// group hold interleaved metrics and per-page min/max stats span + /// every metric in the stream, killing pruning. + /// + /// Bails out without sorting when either source column is missing + /// (non-metric stream, schema drift) so the caller can write the + /// batch unchanged. + fn sort_batch_for_metric_pruning( + batch: &RecordBatch, + time_partition_field: &str, + ) -> Result { + use arrow::compute::{SortColumn, kernels::sort::SortOptions, lexsort_to_indices, take}; + let schema = batch.schema(); + let Some(name_idx) = schema.index_of("metric_name").ok() else { + return Ok(batch.clone()); + }; + let Some(time_idx) = schema.index_of(time_partition_field).ok() else { + return Ok(batch.clone()); + }; + if batch.num_rows() < 2 { + return Ok(batch.clone()); + } + + let sort_cols = vec![ + SortColumn { + values: batch.column(name_idx).clone(), + options: Some(SortOptions { + descending: false, + nulls_first: false, + }), + }, + SortColumn { + values: batch.column(time_idx).clone(), + options: Some(SortOptions { + descending: true, + nulls_first: false, + }), + }, + ]; + let indices = lexsort_to_indices(&sort_cols, None)?; + let columns: Vec = batch + .columns() + .iter() + .map(|c| take(c.as_ref(), &indices, None)) + .collect::>()?; + Ok(RecordBatch::try_new(schema, columns)?) + } + fn reset_staging_metrics(&self, tenant_id: &Option) { let tenant_str = tenant_id.as_deref().unwrap_or(DEFAULT_TENANT); metrics::STAGING_FILES @@ -739,8 +813,43 @@ impl Stream { .open(part_path) .map_err(|_| StagingError::Create)?; let mut writer = ArrowWriter::try_new(&mut part_file, schema.clone(), Some(props.clone()))?; - for ref record in record_reader.merged_iter(schema.clone(), time_partition.cloned()) { - writer.write(record)?; + let sort_for_metric_pruning = self.is_otel_metrics(); + let time_partition_field = time_partition.map_or_else( + || DEFAULT_TIMESTAMP_KEY.to_string(), + |s| s.as_str().to_string(), + ); + + if sort_for_metric_pruning { + // Buffer batches up to the row-group target, then + // concat + sort + write as a single contiguous batch. The + // ArrowWriter splits the sorted batch into row groups at the + // same boundary, so the row order survives intact and + // per-page (metric_name min, max) stats narrow to the slice + // each page actually carries. + let target = self.options.row_group_size; + let mut buffer: Vec = Vec::new(); + let mut buffered_rows: usize = 0; + for record in record_reader.merged_iter(schema.clone(), time_partition.cloned()) { + buffered_rows += record.num_rows(); + buffer.push(record); + if buffered_rows >= target { + let combined = arrow::compute::concat_batches(schema, &buffer)?; + let sorted = + Self::sort_batch_for_metric_pruning(&combined, &time_partition_field)?; + writer.write(&sorted)?; + buffer.clear(); + buffered_rows = 0; + } + } + if !buffer.is_empty() { + let combined = arrow::compute::concat_batches(schema, &buffer)?; + let sorted = Self::sort_batch_for_metric_pruning(&combined, &time_partition_field)?; + writer.write(&sorted)?; + } + } else { + for ref record in record_reader.merged_iter(schema.clone(), time_partition.cloned()) { + writer.write(record)?; + } } writer.close()?; diff --git a/src/query/mod.rs b/src/query/mod.rs index 20ebd9dcc..d5b842dea 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -44,7 +44,6 @@ use datafusion::sql::parser::DFParser; use datafusion::sql::resolve::resolve_table_references; use datafusion::sql::sqlparser::dialect::PostgreSqlDialect; use futures::Stream; -use futures::stream::select_all; use itertools::Itertools; use once_cell::sync::{Lazy, OnceCell}; use serde::{Deserialize, Serialize}; @@ -56,6 +55,8 @@ use std::sync::{Arc, RwLock}; use std::task::{Context, Poll}; use sysinfo::System; use tokio::runtime::Runtime; +use tokio_stream::wrappers::ReceiverStream; +use tracing::Instrument; use self::error::ExecuteError; pub use self::stream_schema_provider::PartialTimeFilter; @@ -75,21 +76,7 @@ use crate::storage::{ObjectStorage, ObjectStorageProvider, ObjectStoreFormat}; use crate::utils::time::TimeRange; /// Boxed record-batch stream used as the streaming half of query results. -type BoxedBatchStream = Pin< - Box< - RecordBatchStreamAdapter< - select_all::SelectAll< - Pin< - Box< - dyn RecordBatchStream< - Item = Result, - > + Send, - >, - >, - >, - >, - >, ->; +type BoxedBatchStream = SendableRecordBatchStream; /// Result type returned by query execution: either collected batches or a streaming adapter, plus field names. type QueryResult = Result<(Either, BoxedBatchStream>, Vec), ExecuteError>; @@ -360,19 +347,36 @@ impl Query { active_streams: AtomicUsize::new(output_partitions), }); - let streams = execute_stream_partitioned(plan.clone(), task_ctx.clone())? - .into_iter() - .map(|s| { - let wrapped = - PartitionedMetricMonitor::new(s, monitor_state.clone(), tenant_id.clone()); - Box::pin(wrapped) as SendableRecordBatchStream - }) - .collect_vec(); - - let merged_stream = futures::stream::select_all(streams); + let partition_streams = execute_stream_partitioned(plan.clone(), task_ctx.clone())?; + let n = partition_streams.len(); + // Bound channel so a slow consumer backpressures producers — caps peak memory. + let (tx, rx) = tokio::sync::mpsc::channel::< + Result, + >((num_cpus::get() * 4).max(n * 2).max(1)); + + for s in partition_streams { + let wrapped = + PartitionedMetricMonitor::new(s, monitor_state.clone(), tenant_id.clone()); + let tx = tx.clone(); + let span = tracing::Span::current(); + tokio::spawn( + async move { + let mut stream: SendableRecordBatchStream = Box::pin(wrapped); + use futures::StreamExt; + while let Some(batch) = stream.next().await { + if tx.send(batch).await.is_err() { + break; + } + } + } + .instrument(span), + ); + } + drop(tx); - let final_stream = RecordBatchStreamAdapter::new(plan.schema(), merged_stream); - Either::Right(Box::pin(final_stream)) + let merged = ReceiverStream::new(rx); + let final_stream = RecordBatchStreamAdapter::new(plan.schema(), merged); + Either::Right(Box::pin(final_stream) as SendableRecordBatchStream) }; Ok((results, fields)) From 073f9ac83e6227e089ccc7bf89a745d167dda9ea Mon Sep 17 00:00:00 2001 From: parmesant Date: Tue, 26 May 2026 19:34:41 -0700 Subject: [PATCH 09/47] fix: Surface ingestion errors (#1652) - DiskWriter and MemWriter expect and unwrap replaced - New cli env var `P_DATAFUSION_TARGET_PARTITIONS` for controlling number of partitions (default num cpu / 2) - Streaming response uses unbounded channel now --------- Co-authored-by: Nikhil Sinha --- src/cli.rs | 12 ++++++- src/handlers/airplane.rs | 2 +- src/parseable/staging/writer.rs | 25 ++++++++----- src/parseable/streams.rs | 56 +++++++++++++++++++---------- src/query/mod.rs | 32 +++++++++++------ src/query/stream_schema_provider.rs | 13 +++++-- 6 files changed, 98 insertions(+), 42 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 50f1202c9..d6664ff2e 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -17,7 +17,7 @@ */ use clap::{Parser, value_parser}; -use std::{env, fs, path::PathBuf}; +use std::{env, fs, ops::Div, path::PathBuf}; use url::Url; @@ -194,6 +194,16 @@ pub struct Options { )] pub max_connections: usize, + // DataFusion target partitions + #[arg( + long, + env = "P_DATAFUSION_TARGET_PARTITIONS", + default_value_t = num_cpus::get().div(2).max(1) as u64, + value_parser = value_parser!(u64).range(1..), + help = "Number of partitions for DF to split execution into" + )] + pub target_partitions: u64, + #[arg( long = "origin", env = "P_ORIGIN_URI", diff --git a/src/handlers/airplane.rs b/src/handlers/airplane.rs index 88b4955ae..8352ea3f9 100644 --- a/src/handlers/airplane.rs +++ b/src/handlers/airplane.rs @@ -235,7 +235,7 @@ impl FlightService for AirServiceImpl { if event.is_some() { // Clear staging of stream once airplane has taxied - PARSEABLE.get_or_create_stream(&stream_name, &None).clear(); + let _ = PARSEABLE.get_or_create_stream(&stream_name, &None).clear(); } let time = time.elapsed().as_secs_f64(); diff --git a/src/parseable/staging/writer.rs b/src/parseable/staging/writer.rs index 56a059d9f..ec58800a3 100644 --- a/src/parseable/staging/writer.rs +++ b/src/parseable/staging/writer.rs @@ -135,16 +135,17 @@ impl Default for MemWriter { } impl MemWriter { - pub fn push(&mut self, schema_key: &str, rb: &RecordBatch) { + pub fn push(&mut self, schema_key: &str, rb: &RecordBatch) -> Result<(), StagingError> { if !self.schema_map.contains(schema_key) { self.schema_map.insert(schema_key.to_owned()); - self.schema = Schema::try_merge([self.schema.clone(), (*rb.schema()).clone()]).unwrap(); + self.schema = Schema::try_merge([self.schema.clone(), (*rb.schema()).clone()])?; } if let Some(record) = self.mutable_buffer.push(rb) { - let record = concat_records(&Arc::new(self.schema.clone()), &record); + let record = concat_records(&Arc::new(self.schema.clone()), &record)?; self.read_buffer.push(record); } + Ok(()) } pub fn clear(&mut self) { @@ -154,23 +155,29 @@ impl MemWriter { self.mutable_buffer.inner.clear(); } - pub fn recordbatch_cloned(&self, schema: &Arc) -> Vec { + pub fn recordbatch_cloned( + &self, + schema: &Arc, + ) -> Result, StagingError> { let mut read_buffer = self.read_buffer.clone(); if !self.mutable_buffer.inner.is_empty() { - let rb = concat_records(schema, &self.mutable_buffer.inner); + let rb = concat_records(schema, &self.mutable_buffer.inner)?; read_buffer.push(rb) } - read_buffer + Ok(read_buffer .into_iter() .map(|rb| adapt_batch(schema, &rb)) - .collect() + .collect()) } } -fn concat_records(schema: &Arc, record: &[RecordBatch]) -> RecordBatch { +fn concat_records( + schema: &Arc, + record: &[RecordBatch], +) -> Result { let records = record.iter().map(|x| adapt_batch(schema, x)).collect_vec(); - concat_batches(schema, records.iter()).unwrap() + Ok(concat_batches(schema, records.iter())?) } #[derive(Debug, Default)] diff --git a/src/parseable/streams.rs b/src/parseable/streams.rs index 6c75dec75..8c1e55115 100644 --- a/src/parseable/streams.rs +++ b/src/parseable/streams.rs @@ -186,8 +186,7 @@ impl Stream { OBJECT_STORE_DATA_GRANULARITY, ); let file_path = self.data_path.join(&filename); - let mut writer = DiskWriter::try_new(file_path, &record.schema(), range) - .expect("File and RecordBatch both are checked"); + let mut writer = DiskWriter::try_new(file_path, &record.schema(), range)?; writer.write(record)?; guard.disk.insert(filename, writer); @@ -195,7 +194,7 @@ impl Stream { }; } - guard.mem.push(schema_key, record); + guard.mem.push(schema_key, record)?; Ok(()) } @@ -532,26 +531,46 @@ impl Stream { Ok(()) } - pub fn recordbatches_cloned(&self, schema: &Arc) -> Vec { - self.writer.lock().unwrap().mem.recordbatch_cloned(schema) - } - - pub fn clear(&self) { - self.writer.lock().unwrap().mem.clear(); + pub fn recordbatches_cloned( + &self, + schema: &Arc, + ) -> Result, StagingError> { + let writer = self.writer.lock().map_err(|poisoned| { + StagingError::PoisonError(PoisonError::new(format!( + "Writer lock poisoned while cloning record batches for stream {} - {}", + self.stream_name, poisoned + ))) + })?; + + writer.mem.recordbatch_cloned(schema) + } + + pub fn clear(&self) -> Result<(), StagingError> { + self.writer + .lock() + .map_err(|poisoned| { + StagingError::PoisonError(PoisonError::new(format!( + "Writer lock poisoned while clearing stream {} - {}", + self.stream_name, poisoned + ))) + })? + .mem + .clear(); + Ok(()) } - pub fn flush(&self, forced: bool) { + pub fn flush(&self, forced: bool) -> Result<(), StagingError> { let _span = info_span!("flush", stream_name = %self.stream_name, forced).entered(); // Swap out stale writers under the lock, drop them after releasing it. // DiskWriter::Drop does I/O (IPC finish + file rename) so dropping // outside the lock avoids blocking concurrent push() calls. let stale_writers = { - let mut writer = self.writer.lock().unwrap_or_else(|_| { - panic!( - "Writer lock poisoned while flushing data for stream {}", - self.stream_name - ) - }); + let mut writer = self.writer.lock().map_err(|poisoned| { + StagingError::PoisonError(PoisonError::new(format!( + "Writer lock poisoned while flushing data for stream {} - {}", + self.stream_name, poisoned + ))) + })?; writer.mem.clear(); let mut old_disk = HashMap::new(); @@ -567,6 +586,7 @@ impl Stream { }; // DiskWriter::Drop I/O happens here, outside the lock drop(stale_writers); + Ok(()) } fn parquet_writer_props( @@ -1306,7 +1326,7 @@ impl Stream { // Force flush for init or shutdown signals to convert all .part files to .arrows // For regular cycles, use false to only flush non-current writers let forced = init_signal || shutdown_signal; - self.flush(forced); + self.flush(forced)?; info!( "Flushing stream ({}) took: {}s", self.stream_name, @@ -1717,7 +1737,7 @@ mod tests { StreamType::UserDefined, ) .unwrap(); - staging.flush(true); + staging.flush(true).unwrap(); } #[test] diff --git a/src/query/mod.rs b/src/query/mod.rs index d5b842dea..14955518e 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -44,6 +44,7 @@ use datafusion::sql::parser::DFParser; use datafusion::sql::resolve::resolve_table_references; use datafusion::sql::sqlparser::dialect::PostgreSqlDialect; use futures::Stream; +use futures::StreamExt; use itertools::Itertools; use once_cell::sync::{Lazy, OnceCell}; use serde::{Deserialize, Serialize}; @@ -55,7 +56,7 @@ use std::sync::{Arc, RwLock}; use std::task::{Context, Poll}; use sysinfo::System; use tokio::runtime::Runtime; -use tokio_stream::wrappers::ReceiverStream; +use tokio_stream::wrappers::UnboundedReceiverStream; use tracing::Instrument; use self::error::ExecuteError; @@ -251,7 +252,8 @@ impl Query { .with_prefer_existing_sort(true) //batch size has been made configurable via environment variable //default value is 20000 - .with_batch_size(PARSEABLE.options.execution_batch_size); + .with_batch_size(PARSEABLE.options.execution_batch_size) + .with_target_partitions(PARSEABLE.options.target_partitions as usize); // Pushdown filters allows DF to push the filters as far down in the plan as possible // and thus, reducing the number of rows decoded @@ -260,10 +262,22 @@ impl Query { // Reorder filters allows DF to decide the order of filters minimizing the cost of filter evaluation config.options_mut().execution.parquet.reorder_filters = true; config.options_mut().execution.parquet.binary_as_string = true; + // Bump footer-read hint from the 512 KiB default. Streams with + // many label columns + page-indexed value columns can have + // parquet footers in the 1-2 MiB range; sizing the hint above + // the actual footer collapses the two-read fallback into a + // single GET per file, saving a round trip on every file open. + config.options_mut().execution.parquet.metadata_size_hint = Some(2 * 1024 * 1024); config .options_mut() .execution .use_row_number_estimates_to_optimize_partitioning = true; + config.options_mut().execution.parquet.enable_page_index = true; + config + .options_mut() + .execution + .parquet + .max_predicate_cache_size = Some(1024 * 1024 * 1024); //adding this config as it improves query performance as explained here - // https://github.com/apache/datafusion/pull/13101 @@ -348,13 +362,12 @@ impl Query { }); let partition_streams = execute_stream_partitioned(plan.clone(), task_ctx.clone())?; - let n = partition_streams.len(); - // Bound channel so a slow consumer backpressures producers — caps peak memory. - let (tx, rx) = tokio::sync::mpsc::channel::< + + let (tx, rx) = tokio::sync::mpsc::unbounded_channel::< Result, - >((num_cpus::get() * 4).max(n * 2).max(1)); + >(); - for s in partition_streams { + for s in partition_streams.into_iter() { let wrapped = PartitionedMetricMonitor::new(s, monitor_state.clone(), tenant_id.clone()); let tx = tx.clone(); @@ -362,9 +375,8 @@ impl Query { tokio::spawn( async move { let mut stream: SendableRecordBatchStream = Box::pin(wrapped); - use futures::StreamExt; while let Some(batch) = stream.next().await { - if tx.send(batch).await.is_err() { + if tx.send(batch).is_err() { break; } } @@ -374,7 +386,7 @@ impl Query { } drop(tx); - let merged = ReceiverStream::new(rx); + let merged = UnboundedReceiverStream::new(rx); let final_stream = RecordBatchStreamAdapter::new(plan.schema(), merged); Either::Right(Box::pin(final_stream) as SendableRecordBatchStream) }; diff --git a/src/query/stream_schema_provider.rs b/src/query/stream_schema_provider.rs index c316ecd4c..09dbd2b43 100644 --- a/src/query/stream_schema_provider.rs +++ b/src/query/stream_schema_provider.rs @@ -153,14 +153,19 @@ impl StandardTableProvider { // parquet file source, default table parquet options let file_source = if let Some(phyiscal_expr) = filters { - ParquetSource::new(self.schema.clone()).with_predicate(phyiscal_expr) + ParquetSource::new(self.schema.clone()) + .with_predicate(phyiscal_expr) + .with_pushdown_filters(true) + .with_reorder_filters(true) } else { ParquetSource::new(self.schema.clone()) + .with_pushdown_filters(true) + .with_reorder_filters(true) }; let mut conf_builder = FileScanConfigBuilder::new(object_store_url, file_source.into()) .with_statistics(statistics) - .with_batch_size(Some(20000)) + .with_batch_size(Some(PARSEABLE.options.execution_batch_size)) .with_constraints(Constraints::default()) .with_file_groups(file_groups) .with_output_ordering(vec![LexOrdering::new([sort_expr]).unwrap()]); @@ -263,7 +268,9 @@ impl StandardTableProvider { }; // Staging arrow exection plan - let records = staging.recordbatches_cloned(&self.schema); + let records = staging + .recordbatches_cloned(&self.schema) + .map_err(|e| DataFusionError::Internal(format!("Error during staging exec- {e}")))?; let arrow_exec = reversed_mem_table(records, self.schema.clone())? .scan(state, projection, filters, limit) .await?; From b726d61f0ca93cb1a817d1c77261d9578a95dc45 Mon Sep 17 00:00:00 2001 From: AdheipSingh <34169002+AdheipSingh@users.noreply.github.com> Date: Thu, 28 May 2026 14:54:17 +0530 Subject: [PATCH 10/47] add helm chart for parseable-enterprise 2.7.3 (#1654) Co-authored-by: AdheipSingh --- helm-releases/parseable-enterprise-2.7.3.tgz | Bin 0 -> 57306 bytes index.yaml | 189 +++++++++++-------- 2 files changed, 107 insertions(+), 82 deletions(-) create mode 100644 helm-releases/parseable-enterprise-2.7.3.tgz diff --git a/helm-releases/parseable-enterprise-2.7.3.tgz b/helm-releases/parseable-enterprise-2.7.3.tgz new file mode 100644 index 0000000000000000000000000000000000000000..ddf4231bb0cd192f24f41e0ed9763b48de44cb26 GIT binary patch literal 57306 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 literal 0 HcmV?d00001 diff --git a/index.yaml b/index.yaml index 97d25572b..4a93d79ed 100644 --- a/index.yaml +++ b/index.yaml @@ -3,7 +3,7 @@ entries: pai: - apiVersion: v2 appVersion: 0.2.0 - created: "2026-05-06T15:17:48.551638+05:30" + created: "2026-05-28T13:55:39.17393+05:30" description: Parseable Auto Instrumentation (PAI) operator for Kubernetes digest: fd04518e8fc9e25d3fa876971a38b4c65005a207ba9440ae8ce981cd070646d9 home: https://github.com/parseablehq/pai @@ -21,7 +21,7 @@ entries: version: 0.2.0 - apiVersion: v2 appVersion: 0.1.0 - created: "2026-05-06T15:17:48.551349+05:30" + created: "2026-05-28T13:55:39.173679+05:30" description: Parseable Auto Instrumentation (PAI) operator for Kubernetes digest: 9443e75bef96a424fd5063ddd02cee18e6050207d73e505f66be467287c9f7f7 home: https://github.com/parseablehq/pai @@ -40,7 +40,7 @@ entries: parseable: - apiVersion: v2 appVersion: v2.7.2 - created: "2026-05-06T15:17:48.680502+05:30" + created: "2026-05-28T13:55:39.302197+05:30" dependencies: - condition: vector.enabled name: vector @@ -65,7 +65,7 @@ entries: version: 2.7.2 - apiVersion: v2 appVersion: v2.7.1 - created: "2026-05-06T15:17:48.678608+05:30" + created: "2026-05-28T13:55:39.299936+05:30" dependencies: - condition: vector.enabled name: vector @@ -90,7 +90,7 @@ entries: version: 2.7.1 - apiVersion: v2 appVersion: v2.6.6 - created: "2026-05-06T15:17:48.676215+05:30" + created: "2026-05-28T13:55:39.297942+05:30" dependencies: - condition: vector.enabled name: vector @@ -115,7 +115,7 @@ entries: version: 2.6.6 - apiVersion: v2 appVersion: v2.6.5 - created: "2026-05-06T15:17:48.673718+05:30" + created: "2026-05-28T13:55:39.295645+05:30" dependencies: - condition: vector.enabled name: vector @@ -140,7 +140,7 @@ entries: version: 2.6.5 - apiVersion: v2 appVersion: v2.5.13 - created: "2026-05-06T15:17:48.659624+05:30" + created: "2026-05-28T13:55:39.28281+05:30" dependencies: - condition: vector.enabled name: vector @@ -165,7 +165,7 @@ entries: version: 2.5.13 - apiVersion: v2 appVersion: v2.5.7 - created: "2026-05-06T15:17:48.671446+05:30" + created: "2026-05-28T13:55:39.293599+05:30" dependencies: - condition: vector.enabled name: vector @@ -190,7 +190,7 @@ entries: version: 2.5.7 - apiVersion: v2 appVersion: v2.5.6 - created: "2026-05-06T15:17:48.668991+05:30" + created: "2026-05-28T13:55:39.291413+05:30" dependencies: - condition: vector.enabled name: vector @@ -215,7 +215,7 @@ entries: version: 2.5.6 - apiVersion: v2 appVersion: v2.5.5 - created: "2026-05-06T15:17:48.666477+05:30" + created: "2026-05-28T13:55:39.289035+05:30" dependencies: - condition: vector.enabled name: vector @@ -240,7 +240,7 @@ entries: version: 2.5.5 - apiVersion: v2 appVersion: v2.5.4 - created: "2026-05-06T15:17:48.664034+05:30" + created: "2026-05-28T13:55:39.286852+05:30" dependencies: - condition: vector.enabled name: vector @@ -264,7 +264,7 @@ entries: version: 2.5.4 - apiVersion: v2 appVersion: v2.5.3 - created: "2026-05-06T15:17:48.661743+05:30" + created: "2026-05-28T13:55:39.284702+05:30" dependencies: - condition: vector.enabled name: vector @@ -288,7 +288,7 @@ entries: version: 2.5.3 - apiVersion: v2 appVersion: v2.4.0 - created: "2026-05-06T15:17:48.65697+05:30" + created: "2026-05-28T13:55:39.280505+05:30" dependencies: - condition: vector.enabled name: vector @@ -312,7 +312,7 @@ entries: version: 2.4.0 - apiVersion: v2 appVersion: v2.3.3 - created: "2026-05-06T15:17:48.654651+05:30" + created: "2026-05-28T13:55:39.278613+05:30" dependencies: - condition: vector.enabled name: vector @@ -336,7 +336,7 @@ entries: version: 2.3.3 - apiVersion: v2 appVersion: v2.3.2 - created: "2026-05-06T15:17:48.652224+05:30" + created: "2026-05-28T13:55:39.276526+05:30" dependencies: - condition: vector.enabled name: vector @@ -360,7 +360,7 @@ entries: version: 2.3.2 - apiVersion: v2 appVersion: v2.3.1 - created: "2026-05-06T15:17:48.65002+05:30" + created: "2026-05-28T13:55:39.274211+05:30" dependencies: - condition: vector.enabled name: vector @@ -384,7 +384,7 @@ entries: version: 2.3.1 - apiVersion: v2 appVersion: v2.3.0 - created: "2026-05-06T15:17:48.64756+05:30" + created: "2026-05-28T13:55:39.27203+05:30" dependencies: - condition: vector.enabled name: vector @@ -408,7 +408,7 @@ entries: version: 2.3.0 - apiVersion: v2 appVersion: v2.1.0 - created: "2026-05-06T15:17:48.645308+05:30" + created: "2026-05-28T13:55:39.270174+05:30" dependencies: - condition: vector.enabled name: vector @@ -432,7 +432,7 @@ entries: version: 2.1.0 - apiVersion: v2 appVersion: v1.7.5 - created: "2026-05-06T15:17:48.642931+05:30" + created: "2026-05-28T13:55:39.268094+05:30" dependencies: - condition: vector.enabled name: vector @@ -456,7 +456,7 @@ entries: version: 2.0.0 - apiVersion: v2 appVersion: v1.7.5 - created: "2026-05-06T15:17:48.640755+05:30" + created: "2026-05-28T13:55:39.266067+05:30" dependencies: - condition: vector.enabled name: vector @@ -480,7 +480,7 @@ entries: version: 1.7.5 - apiVersion: v2 appVersion: v1.7.3 - created: "2026-05-06T15:17:48.63863+05:30" + created: "2026-05-28T13:55:39.263868+05:30" dependencies: - condition: vector.enabled name: vector @@ -504,7 +504,7 @@ entries: version: 1.7.3 - apiVersion: v2 appVersion: v1.7.2 - created: "2026-05-06T15:17:48.636301+05:30" + created: "2026-05-28T13:55:39.262005+05:30" dependencies: - condition: vector.enabled name: vector @@ -528,7 +528,7 @@ entries: version: 1.7.2 - apiVersion: v2 appVersion: v1.7.1 - created: "2026-05-06T15:17:48.63435+05:30" + created: "2026-05-28T13:55:39.259709+05:30" dependencies: - condition: vector.enabled name: vector @@ -552,7 +552,7 @@ entries: version: 1.7.1 - apiVersion: v2 appVersion: v1.7.0 - created: "2026-05-06T15:17:48.632333+05:30" + created: "2026-05-28T13:55:39.257913+05:30" dependencies: - condition: vector.enabled name: vector @@ -575,7 +575,7 @@ entries: version: 1.7.0 - apiVersion: v2 appVersion: v1.6.3 - created: "2026-05-06T15:17:48.63059+05:30" + created: "2026-05-28T13:55:39.2559+05:30" dependencies: - condition: vector.enabled name: vector @@ -598,7 +598,7 @@ entries: version: 1.6.8 - apiVersion: v2 appVersion: v1.6.3 - created: "2026-05-06T15:17:48.628478+05:30" + created: "2026-05-28T13:55:39.253885+05:30" dependencies: - condition: vector.enabled name: vector @@ -621,7 +621,7 @@ entries: version: 1.6.7 - apiVersion: v2 appVersion: v1.6.3 - created: "2026-05-06T15:17:48.626402+05:30" + created: "2026-05-28T13:55:39.252068+05:30" dependencies: - condition: vector.enabled name: vector @@ -644,7 +644,7 @@ entries: version: 1.6.6 - apiVersion: v2 appVersion: v1.6.3 - created: "2026-05-06T15:17:48.624682+05:30" + created: "2026-05-28T13:55:39.249466+05:30" dependencies: - condition: vector.enabled name: vector @@ -667,7 +667,7 @@ entries: version: 1.6.5 - apiVersion: v2 appVersion: v1.6.3 - created: "2026-05-06T15:17:48.62269+05:30" + created: "2026-05-28T13:55:39.247552+05:30" dependencies: - condition: vector.enabled name: vector @@ -690,7 +690,7 @@ entries: version: 1.6.4 - apiVersion: v2 appVersion: v1.6.3 - created: "2026-05-06T15:17:48.620696+05:30" + created: "2026-05-28T13:55:39.245395+05:30" dependencies: - condition: vector.enabled name: vector @@ -713,7 +713,7 @@ entries: version: 1.6.3 - apiVersion: v2 appVersion: v1.6.2 - created: "2026-05-06T15:17:48.618713+05:30" + created: "2026-05-28T13:55:39.243544+05:30" dependencies: - condition: vector.enabled name: vector @@ -736,7 +736,7 @@ entries: version: 1.6.2 - apiVersion: v2 appVersion: v1.6.1 - created: "2026-05-06T15:17:48.617056+05:30" + created: "2026-05-28T13:55:39.241382+05:30" dependencies: - condition: vector.enabled name: vector @@ -759,7 +759,7 @@ entries: version: 1.6.1 - apiVersion: v2 appVersion: v1.6.0 - created: "2026-05-06T15:17:48.615168+05:30" + created: "2026-05-28T13:55:39.239534+05:30" dependencies: - condition: vector.enabled name: vector @@ -782,7 +782,7 @@ entries: version: 1.6.0 - apiVersion: v2 appVersion: v1.5.5 - created: "2026-05-06T15:17:48.613517+05:30" + created: "2026-05-28T13:55:39.237498+05:30" dependencies: - condition: vector.enabled name: vector @@ -805,7 +805,7 @@ entries: version: 1.5.5 - apiVersion: v2 appVersion: v1.5.4 - created: "2026-05-06T15:17:48.611592+05:30" + created: "2026-05-28T13:55:39.235553+05:30" dependencies: - condition: vector.enabled name: vector @@ -828,7 +828,7 @@ entries: version: 1.5.4 - apiVersion: v2 appVersion: v1.5.3 - created: "2026-05-06T15:17:48.609833+05:30" + created: "2026-05-28T13:55:39.233526+05:30" dependencies: - condition: vector.enabled name: vector @@ -851,7 +851,7 @@ entries: version: 1.5.3 - apiVersion: v2 appVersion: v1.5.2 - created: "2026-05-06T15:17:48.607764+05:30" + created: "2026-05-28T13:55:39.231838+05:30" dependencies: - condition: vector.enabled name: vector @@ -874,7 +874,7 @@ entries: version: 1.5.2 - apiVersion: v2 appVersion: v1.5.1 - created: "2026-05-06T15:17:48.606079+05:30" + created: "2026-05-28T13:55:39.23009+05:30" dependencies: - condition: vector.enabled name: vector @@ -897,7 +897,7 @@ entries: version: 1.5.1 - apiVersion: v2 appVersion: v1.5.0 - created: "2026-05-06T15:17:48.604122+05:30" + created: "2026-05-28T13:55:39.228114+05:30" dependencies: - condition: vector.enabled name: vector @@ -920,7 +920,7 @@ entries: version: 1.5.0 - apiVersion: v2 appVersion: v1.4.0 - created: "2026-05-06T15:17:48.602306+05:30" + created: "2026-05-28T13:55:39.226282+05:30" dependencies: - condition: vector.enabled name: vector @@ -943,7 +943,7 @@ entries: version: 1.4.1 - apiVersion: v2 appVersion: v1.4.0 - created: "2026-05-06T15:17:48.600621+05:30" + created: "2026-05-28T13:55:39.224252+05:30" dependencies: - condition: vector.enabled name: vector @@ -966,7 +966,7 @@ entries: version: 1.4.0 - apiVersion: v2 appVersion: v1.3.0 - created: "2026-05-06T15:17:48.59866+05:30" + created: "2026-05-28T13:55:39.222646+05:30" dependencies: - condition: vector.enabled name: vector @@ -989,7 +989,7 @@ entries: version: 1.3.1 - apiVersion: v2 appVersion: v1.3.0 - created: "2026-05-06T15:17:48.597011+05:30" + created: "2026-05-28T13:55:39.22055+05:30" dependencies: - condition: vector.enabled name: vector @@ -1012,7 +1012,7 @@ entries: version: 1.3.0 - apiVersion: v2 appVersion: v1.2.0 - created: "2026-05-06T15:17:48.594828+05:30" + created: "2026-05-28T13:55:39.218848+05:30" dependencies: - condition: vector.enabled name: vector @@ -1035,7 +1035,7 @@ entries: version: 1.2.0 - apiVersion: v2 appVersion: v1.1.0 - created: "2026-05-06T15:17:48.593207+05:30" + created: "2026-05-28T13:55:39.217112+05:30" dependencies: - condition: vector.enabled name: vector @@ -1058,7 +1058,7 @@ entries: version: 1.1.0 - apiVersion: v2 appVersion: v1.0.0 - created: "2026-05-06T15:17:48.591099+05:30" + created: "2026-05-28T13:55:39.215071+05:30" dependencies: - condition: vector.enabled name: vector @@ -1081,7 +1081,7 @@ entries: version: 1.0.0 - apiVersion: v2 appVersion: v0.9.0 - created: "2026-05-06T15:17:48.589044+05:30" + created: "2026-05-28T13:55:39.213216+05:30" dependencies: - condition: vector.enabled name: vector @@ -1104,7 +1104,7 @@ entries: version: 0.9.0 - apiVersion: v2 appVersion: v0.8.1 - created: "2026-05-06T15:17:48.587497+05:30" + created: "2026-05-28T13:55:39.210839+05:30" dependencies: - condition: vector.enabled name: vector @@ -1127,7 +1127,7 @@ entries: version: 0.8.1 - apiVersion: v2 appVersion: v0.8.0 - created: "2026-05-06T15:17:48.585892+05:30" + created: "2026-05-28T13:55:39.208841+05:30" dependencies: - condition: vector.enabled name: vector @@ -1150,7 +1150,7 @@ entries: version: 0.8.0 - apiVersion: v2 appVersion: v0.7.3 - created: "2026-05-06T15:17:48.58384+05:30" + created: "2026-05-28T13:55:39.207177+05:30" dependencies: - condition: vector.enabled name: vector @@ -1173,7 +1173,7 @@ entries: version: 0.7.3 - apiVersion: v2 appVersion: v0.7.2 - created: "2026-05-06T15:17:48.58227+05:30" + created: "2026-05-28T13:55:39.205058+05:30" dependencies: - condition: vector.enabled name: vector @@ -1196,7 +1196,7 @@ entries: version: 0.7.2 - apiVersion: v2 appVersion: v0.7.1 - created: "2026-05-06T15:17:48.580659+05:30" + created: "2026-05-28T13:55:39.203365+05:30" dependencies: - condition: vector.enabled name: vector @@ -1219,7 +1219,7 @@ entries: version: 0.7.1 - apiVersion: v2 appVersion: v0.7.0 - created: "2026-05-06T15:17:48.578735+05:30" + created: "2026-05-28T13:55:39.20123+05:30" dependencies: - condition: vector.enabled name: vector @@ -1242,7 +1242,7 @@ entries: version: 0.7.0 - apiVersion: v2 appVersion: v0.6.2 - created: "2026-05-06T15:17:48.577186+05:30" + created: "2026-05-28T13:55:39.199666+05:30" dependencies: - condition: vector.enabled name: vector @@ -1265,7 +1265,7 @@ entries: version: 0.6.2 - apiVersion: v2 appVersion: v0.6.1 - created: "2026-05-06T15:17:48.575399+05:30" + created: "2026-05-28T13:55:39.198031+05:30" dependencies: - condition: vector.enabled name: vector @@ -1288,7 +1288,7 @@ entries: version: 0.6.1 - apiVersion: v2 appVersion: v0.6.0 - created: "2026-05-06T15:17:48.573796+05:30" + created: "2026-05-28T13:55:39.196001+05:30" dependencies: - condition: vector.enabled name: vector @@ -1311,7 +1311,7 @@ entries: version: 0.6.0 - apiVersion: v2 appVersion: v0.5.1 - created: "2026-05-06T15:17:48.57223+05:30" + created: "2026-05-28T13:55:39.194072+05:30" dependencies: - condition: vector.enabled name: vector @@ -1334,7 +1334,7 @@ entries: version: 0.5.1 - apiVersion: v2 appVersion: v0.5.0 - created: "2026-05-06T15:17:48.570296+05:30" + created: "2026-05-28T13:55:39.19203+05:30" dependencies: - condition: vector.enabled name: vector @@ -1357,7 +1357,7 @@ entries: version: 0.5.0 - apiVersion: v2 appVersion: v0.4.4 - created: "2026-05-06T15:17:48.568686+05:30" + created: "2026-05-28T13:55:39.190386+05:30" dependencies: - condition: vector.enabled name: vector @@ -1380,7 +1380,7 @@ entries: version: 0.4.5 - apiVersion: v2 appVersion: v0.4.3 - created: "2026-05-06T15:17:48.566522+05:30" + created: "2026-05-28T13:55:39.188082+05:30" dependencies: - condition: vector.enabled name: vector @@ -1403,7 +1403,7 @@ entries: version: 0.4.4 - apiVersion: v2 appVersion: v0.4.2 - created: "2026-05-06T15:17:48.564577+05:30" + created: "2026-05-28T13:55:39.186545+05:30" dependencies: - condition: vector.enabled name: vector @@ -1426,7 +1426,7 @@ entries: version: 0.4.3 - apiVersion: v2 appVersion: v0.4.1 - created: "2026-05-06T15:17:48.562944+05:30" + created: "2026-05-28T13:55:39.18469+05:30" dependencies: - condition: vector.enabled name: vector @@ -1449,7 +1449,7 @@ entries: version: 0.4.2 - apiVersion: v2 appVersion: v0.4.0 - created: "2026-05-06T15:17:48.56085+05:30" + created: "2026-05-28T13:55:39.18308+05:30" dependencies: - condition: vector.enabled name: vector @@ -1472,7 +1472,7 @@ entries: version: 0.4.1 - apiVersion: v2 appVersion: v0.4.0 - created: "2026-05-06T15:17:48.559239+05:30" + created: "2026-05-28T13:55:39.181038+05:30" dependencies: - condition: vector.enabled name: vector @@ -1495,7 +1495,7 @@ entries: version: 0.4.0 - apiVersion: v2 appVersion: v0.3.1 - created: "2026-05-06T15:17:48.55706+05:30" + created: "2026-05-28T13:55:39.179426+05:30" dependencies: - condition: vector.enabled name: vector @@ -1518,7 +1518,7 @@ entries: version: 0.3.1 - apiVersion: v2 appVersion: v0.3.0 - created: "2026-05-06T15:17:48.555268+05:30" + created: "2026-05-28T13:55:39.176814+05:30" description: Helm chart for Parseable Server digest: ff30739229b727dc637f62fd4481c886a6080ce4556bae10cafe7642ddcfd937 name: parseable @@ -1528,7 +1528,7 @@ entries: version: 0.3.0 - apiVersion: v2 appVersion: v0.2.2 - created: "2026-05-06T15:17:48.555106+05:30" + created: "2026-05-28T13:55:39.176657+05:30" description: Helm chart for Parseable Server digest: 477d0dc2f0c07d4f4c32e105d4bdd70c71113add5c2a75ac5f1cb42aa0276db7 name: parseable @@ -1538,7 +1538,7 @@ entries: version: 0.2.2 - apiVersion: v2 appVersion: v0.2.1 - created: "2026-05-06T15:17:48.55494+05:30" + created: "2026-05-28T13:55:39.176508+05:30" description: Helm chart for Parseable Server digest: 84826fcd1b4c579f301569f43b0309c07e8082bad76f5cdd25f86e86ca2e8192 name: parseable @@ -1548,7 +1548,7 @@ entries: version: 0.2.1 - apiVersion: v2 appVersion: v0.2.0 - created: "2026-05-06T15:17:48.554764+05:30" + created: "2026-05-28T13:55:39.176377+05:30" description: Helm chart for Parseable Server digest: 7a759f7f9809f3935cba685e904c021a0b645f217f4e45b9be185900c467edff name: parseable @@ -1558,7 +1558,7 @@ entries: version: 0.2.0 - apiVersion: v2 appVersion: v0.1.1 - created: "2026-05-06T15:17:48.553785+05:30" + created: "2026-05-28T13:55:39.176248+05:30" description: Helm chart for Parseable Server digest: 37993cf392f662ec7b1fbfc9a2ba00ec906d98723e38f3c91ff1daca97c3d0b3 name: parseable @@ -1568,7 +1568,7 @@ entries: version: 0.1.1 - apiVersion: v2 appVersion: v0.1.0 - created: "2026-05-06T15:17:48.553618+05:30" + created: "2026-05-28T13:55:39.176118+05:30" description: Helm chart for Parseable Server digest: 1d580d072af8d6b1ebcbfee31c2e16c907d08db754780f913b5f0032b403789b name: parseable @@ -1578,7 +1578,7 @@ entries: version: 0.1.0 - apiVersion: v2 appVersion: v0.0.8 - created: "2026-05-06T15:17:48.553437+05:30" + created: "2026-05-28T13:55:39.175988+05:30" description: Helm chart for Parseable Server digest: c805254ffa634f96ecec448bcfff9973339aa9487dd8199b21b17b79a4de9345 name: parseable @@ -1588,7 +1588,7 @@ entries: version: 0.0.8 - apiVersion: v2 appVersion: v0.0.7 - created: "2026-05-06T15:17:48.553187+05:30" + created: "2026-05-28T13:55:39.175845+05:30" description: Helm chart for Parseable Server digest: c591f617ed1fe820bb2c72a4c976a78126f1d1095d552daa07c4700f46c4708a name: parseable @@ -1598,7 +1598,7 @@ entries: version: 0.0.7 - apiVersion: v2 appVersion: v0.0.6 - created: "2026-05-06T15:17:48.553037+05:30" + created: "2026-05-28T13:55:39.175582+05:30" description: Helm chart for Parseable Server digest: f9ae56a6fcd6a59e7bee0436200ddbedeb74ade6073deb435b8fcbaf08dda795 name: parseable @@ -1608,7 +1608,7 @@ entries: version: 0.0.6 - apiVersion: v2 appVersion: v0.0.5 - created: "2026-05-06T15:17:48.552876+05:30" + created: "2026-05-28T13:55:39.17477+05:30" description: Helm chart for Parseable Server digest: 4d6b08a064fba36e16feeb820b77e1e8e60fb6de48dbf7ec8410d03d10c26ad0 name: parseable @@ -1618,7 +1618,7 @@ entries: version: 0.0.5 - apiVersion: v2 appVersion: v0.0.2 - created: "2026-05-06T15:17:48.552086+05:30" + created: "2026-05-28T13:55:39.174532+05:30" description: Helm chart for Parseable Server digest: 38a0a3e4c498afbbcc76ebfcb9cb598fa2ca843a53cc93b3cb4f135b85c10844 name: parseable @@ -1628,7 +1628,7 @@ entries: version: 0.0.2 - apiVersion: v2 appVersion: v0.0.1 - created: "2026-05-06T15:17:48.551879+05:30" + created: "2026-05-28T13:55:39.174344+05:30" description: Helm chart for Parseable Server digest: 1f1142db092b9620ee38bb2294ccbb1c17f807b33bf56da43816af7fe89f301e name: parseable @@ -1637,9 +1637,34 @@ entries: - https://charts.parseable.com/helm-releases/parseable-0.0.1.tgz version: 0.0.1 parseable-enterprise: + - apiVersion: v2 + appVersion: v2.7.3 + created: "2026-05-28T13:55:39.313252+05:30" + dependencies: + - condition: vector.enabled + name: vector + repository: https://helm.vector.dev + version: 0.20.1 + - condition: fluent-bit.enabled + name: fluent-bit + repository: https://fluent.github.io/helm-charts + version: 0.48.0 + description: Helm chart for Parseable Enterprise version - Needs a license to + run. Please contact sales@parseable.com for more details. + digest: 8cc36a35779d4e8128ddc54ed76a7143a99b40e67984347daa239f7cebb75495 + icon: https://raw.githubusercontent.com/parseablehq/.github/main/images/new-logo.svg + maintainers: + - email: hi@parseable.com + name: Parseable Team + url: https://parseable.com + name: parseable-enterprise + type: application + urls: + - https://charts.parseable.com/helm-releases/parseable-enterprise-2.7.3.tgz + version: 2.7.3 - apiVersion: v2 appVersion: v2.7.2 - created: "2026-05-06T15:17:48.693455+05:30" + created: "2026-05-28T13:55:39.310923+05:30" dependencies: - condition: vector.enabled name: vector @@ -1664,7 +1689,7 @@ entries: version: 2.7.2 - apiVersion: v2 appVersion: v2.7.1 - created: "2026-05-06T15:17:48.68841+05:30" + created: "2026-05-28T13:55:39.308775+05:30" dependencies: - condition: vector.enabled name: vector @@ -1689,7 +1714,7 @@ entries: version: 2.7.1 - apiVersion: v2 appVersion: v2.6.6 - created: "2026-05-06T15:17:48.685741+05:30" + created: "2026-05-28T13:55:39.306617+05:30" dependencies: - condition: vector.enabled name: vector @@ -1714,7 +1739,7 @@ entries: version: 2.6.7 - apiVersion: v2 appVersion: v2.6.6 - created: "2026-05-06T15:17:48.683372+05:30" + created: "2026-05-28T13:55:39.304275+05:30" dependencies: - condition: vector.enabled name: vector @@ -1737,4 +1762,4 @@ entries: urls: - https://charts.parseable.com/helm-releases/parseable-enterprise-2.6.6.tgz version: 2.6.6 -generated: "2026-05-06T15:17:48.550959+05:30" +generated: "2026-05-28T13:55:39.172114+05:30" From bc5d7b11e8761a56dc14cb431c48775526766d09 Mon Sep 17 00:00:00 2001 From: Nikhil Sinha <131262146+nikhilsinhaparseable@users.noreply.github.com> Date: Thu, 28 May 2026 21:10:40 +0530 Subject: [PATCH 11/47] update Cargo.toml for release v2.8.0 (#1656) --- Cargo.lock | 2 +- Cargo.toml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0b2752d32..1c518e09d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3916,7 +3916,7 @@ dependencies = [ [[package]] name = "parseable" -version = "2.7.2" +version = "2.8.0" dependencies = [ "actix-cors", "actix-web", diff --git a/Cargo.toml b/Cargo.toml index 15a67d435..9c30ed840 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "parseable" -version = "2.7.2" +version = "2.8.0" authors = ["Parseable Team "] edition = "2024" rust-version = "1.88.0" @@ -207,8 +207,8 @@ arrow = "58.0.0" temp-dir = "0.1.14" [package.metadata.parseable_ui] -assets-url = "https://parseable-prism-build.s3.us-east-2.amazonaws.com/v2.7.2/build.zip" -assets-sha1 = "541746bd89b41528477a57a2f6f066dce145dd73" +assets-url = "https://parseable-prism-build.s3.us-east-2.amazonaws.com/v2.8.0/build.zip" +assets-sha1 = "a7523ef819d38678275ae165c443564b2f9a3fc1" [features] debug = [] From 6b3f8b5954e1cb89f689974b7c7255ea41572c83 Mon Sep 17 00:00:00 2001 From: Nitish Tiwari Date: Fri, 29 May 2026 11:28:35 +0530 Subject: [PATCH 12/47] update to helm 2.8.0 (#1658) Signed-off-by: Nitish Tiwari --- helm-reindex.sh | 1 + helm-releases/parseable-2.8.0.tgz | Bin 0 -> 52673 bytes helm-releases/parseable-enterprise-2.8.0.tgz | Bin 0 -> 57243 bytes helm/Chart.yaml | 4 +- helm/values.yaml | 2 +- index.yaml | 201 +++++++++++-------- 6 files changed, 117 insertions(+), 91 deletions(-) create mode 100644 helm-releases/parseable-2.8.0.tgz create mode 100644 helm-releases/parseable-enterprise-2.8.0.tgz diff --git a/helm-reindex.sh b/helm-reindex.sh index a78058e48..ebc0b200f 100755 --- a/helm-reindex.sh +++ b/helm-reindex.sh @@ -1,5 +1,6 @@ #!/bin/bash helm package helm -d helm-releases/ +helm package ../enterprise/helm -d helm-releases/ helm repo index --merge index.yaml --url https://charts.parseable.com . diff --git a/helm-releases/parseable-2.8.0.tgz b/helm-releases/parseable-2.8.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..b23ecce75731835424f6af990914bc6689a5fc9f GIT binary patch literal 52673 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
qz?f?C~n*G0bu-*R~d3H)s$Kh~?q(O^L|4Fq* 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 F1pvfd)&>9o literal 0 HcmV?d00001 diff --git a/helm/Chart.yaml b/helm/Chart.yaml index 8dd97015e..4c9c9b4d4 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: parseable description: Parseable Helm chart - Predictive, unified observability — faster resolution, fewer escalations, happier customers. type: application -version: 2.7.2 -appVersion: "v2.7.2" +version: 2.8.0 +appVersion: "v2.8.0" icon: "https://raw.githubusercontent.com/parseablehq/.github/main/images/new-logo.svg" maintainers: diff --git a/helm/values.yaml b/helm/values.yaml index 1f757abfc..c4cc9df1f 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -1,7 +1,7 @@ parseable: image: repository: parseable/parseable - tag: "v2.7.2" + tag: "v2.8.0" pullPolicy: Always ## object store can be local-store, s3-store, blob-store or gcs-store. store: local-store diff --git a/index.yaml b/index.yaml index 4a93d79ed..3acaa3af2 100644 --- a/index.yaml +++ b/index.yaml @@ -3,7 +3,7 @@ entries: pai: - apiVersion: v2 appVersion: 0.2.0 - created: "2026-05-28T13:55:39.17393+05:30" + created: "2026-05-29T10:50:20.982305+05:30" description: Parseable Auto Instrumentation (PAI) operator for Kubernetes digest: fd04518e8fc9e25d3fa876971a38b4c65005a207ba9440ae8ce981cd070646d9 home: https://github.com/parseablehq/pai @@ -21,7 +21,7 @@ entries: version: 0.2.0 - apiVersion: v2 appVersion: 0.1.0 - created: "2026-05-28T13:55:39.173679+05:30" + created: "2026-05-29T10:50:20.982057+05:30" description: Parseable Auto Instrumentation (PAI) operator for Kubernetes digest: 9443e75bef96a424fd5063ddd02cee18e6050207d73e505f66be467287c9f7f7 home: https://github.com/parseablehq/pai @@ -38,9 +38,34 @@ entries: - https://charts.parseable.com/helm-releases/pai-0.1.0.tgz version: 0.1.0 parseable: + - apiVersion: v2 + appVersion: v2.8.0 + created: "2026-05-29T10:50:21.149696+05:30" + dependencies: + - condition: vector.enabled + name: vector + repository: https://helm.vector.dev + version: 0.20.1 + - condition: fluent-bit.enabled + name: fluent-bit + repository: https://fluent.github.io/helm-charts + version: 0.48.0 + description: Parseable Helm chart - Predictive, unified observability — faster + resolution, fewer escalations, happier customers. + digest: 8ef2f064df9d0fc1ac376aacbdaf462c6674f7fc7c04f574af1eb22b33285ae0 + icon: https://raw.githubusercontent.com/parseablehq/.github/main/images/new-logo.svg + maintainers: + - email: hi@parseable.com + name: Parseable Team + url: https://parseable.com + name: parseable + type: application + urls: + - https://charts.parseable.com/helm-releases/parseable-2.8.0.tgz + version: 2.8.0 - apiVersion: v2 appVersion: v2.7.2 - created: "2026-05-28T13:55:39.302197+05:30" + created: "2026-05-29T10:50:21.147014+05:30" dependencies: - condition: vector.enabled name: vector @@ -52,7 +77,7 @@ entries: version: 0.48.0 description: Parseable Helm chart - Predictive, unified observability — faster resolution, fewer escalations, happier customers. - digest: 088e065317c79a2880e805a40dc9e09d331fa0a1542d705ac1ade7fffe088ac4 + digest: 97505137cd6b45bafc77d88e88cd85906a6821c08fb2922806920b7e9bafe8f3 icon: https://raw.githubusercontent.com/parseablehq/.github/main/images/new-logo.svg maintainers: - email: hi@parseable.com @@ -65,7 +90,7 @@ entries: version: 2.7.2 - apiVersion: v2 appVersion: v2.7.1 - created: "2026-05-28T13:55:39.299936+05:30" + created: "2026-05-29T10:50:21.144242+05:30" dependencies: - condition: vector.enabled name: vector @@ -90,7 +115,7 @@ entries: version: 2.7.1 - apiVersion: v2 appVersion: v2.6.6 - created: "2026-05-28T13:55:39.297942+05:30" + created: "2026-05-29T10:50:21.141113+05:30" dependencies: - condition: vector.enabled name: vector @@ -115,7 +140,7 @@ entries: version: 2.6.6 - apiVersion: v2 appVersion: v2.6.5 - created: "2026-05-28T13:55:39.295645+05:30" + created: "2026-05-29T10:50:21.138482+05:30" dependencies: - condition: vector.enabled name: vector @@ -140,7 +165,7 @@ entries: version: 2.6.5 - apiVersion: v2 appVersion: v2.5.13 - created: "2026-05-28T13:55:39.28281+05:30" + created: "2026-05-29T10:50:21.121593+05:30" dependencies: - condition: vector.enabled name: vector @@ -165,7 +190,7 @@ entries: version: 2.5.13 - apiVersion: v2 appVersion: v2.5.7 - created: "2026-05-28T13:55:39.293599+05:30" + created: "2026-05-29T10:50:21.135328+05:30" dependencies: - condition: vector.enabled name: vector @@ -190,7 +215,7 @@ entries: version: 2.5.7 - apiVersion: v2 appVersion: v2.5.6 - created: "2026-05-28T13:55:39.291413+05:30" + created: "2026-05-29T10:50:21.132728+05:30" dependencies: - condition: vector.enabled name: vector @@ -215,7 +240,7 @@ entries: version: 2.5.6 - apiVersion: v2 appVersion: v2.5.5 - created: "2026-05-28T13:55:39.289035+05:30" + created: "2026-05-29T10:50:21.130091+05:30" dependencies: - condition: vector.enabled name: vector @@ -240,7 +265,7 @@ entries: version: 2.5.5 - apiVersion: v2 appVersion: v2.5.4 - created: "2026-05-28T13:55:39.286852+05:30" + created: "2026-05-29T10:50:21.127103+05:30" dependencies: - condition: vector.enabled name: vector @@ -264,7 +289,7 @@ entries: version: 2.5.4 - apiVersion: v2 appVersion: v2.5.3 - created: "2026-05-28T13:55:39.284702+05:30" + created: "2026-05-29T10:50:21.124491+05:30" dependencies: - condition: vector.enabled name: vector @@ -288,7 +313,7 @@ entries: version: 2.5.3 - apiVersion: v2 appVersion: v2.4.0 - created: "2026-05-28T13:55:39.280505+05:30" + created: "2026-05-29T10:50:21.118874+05:30" dependencies: - condition: vector.enabled name: vector @@ -312,7 +337,7 @@ entries: version: 2.4.0 - apiVersion: v2 appVersion: v2.3.3 - created: "2026-05-28T13:55:39.278613+05:30" + created: "2026-05-29T10:50:21.115623+05:30" dependencies: - condition: vector.enabled name: vector @@ -336,7 +361,7 @@ entries: version: 2.3.3 - apiVersion: v2 appVersion: v2.3.2 - created: "2026-05-28T13:55:39.276526+05:30" + created: "2026-05-29T10:50:21.113059+05:30" dependencies: - condition: vector.enabled name: vector @@ -360,7 +385,7 @@ entries: version: 2.3.2 - apiVersion: v2 appVersion: v2.3.1 - created: "2026-05-28T13:55:39.274211+05:30" + created: "2026-05-29T10:50:21.110494+05:30" dependencies: - condition: vector.enabled name: vector @@ -384,7 +409,7 @@ entries: version: 2.3.1 - apiVersion: v2 appVersion: v2.3.0 - created: "2026-05-28T13:55:39.27203+05:30" + created: "2026-05-29T10:50:21.107589+05:30" dependencies: - condition: vector.enabled name: vector @@ -408,7 +433,7 @@ entries: version: 2.3.0 - apiVersion: v2 appVersion: v2.1.0 - created: "2026-05-28T13:55:39.270174+05:30" + created: "2026-05-29T10:50:21.10505+05:30" dependencies: - condition: vector.enabled name: vector @@ -432,7 +457,7 @@ entries: version: 2.1.0 - apiVersion: v2 appVersion: v1.7.5 - created: "2026-05-28T13:55:39.268094+05:30" + created: "2026-05-29T10:50:21.102134+05:30" dependencies: - condition: vector.enabled name: vector @@ -456,7 +481,7 @@ entries: version: 2.0.0 - apiVersion: v2 appVersion: v1.7.5 - created: "2026-05-28T13:55:39.266067+05:30" + created: "2026-05-29T10:50:21.099584+05:30" dependencies: - condition: vector.enabled name: vector @@ -480,7 +505,7 @@ entries: version: 1.7.5 - apiVersion: v2 appVersion: v1.7.3 - created: "2026-05-28T13:55:39.263868+05:30" + created: "2026-05-29T10:50:21.096926+05:30" dependencies: - condition: vector.enabled name: vector @@ -504,7 +529,7 @@ entries: version: 1.7.3 - apiVersion: v2 appVersion: v1.7.2 - created: "2026-05-28T13:55:39.262005+05:30" + created: "2026-05-29T10:50:21.093884+05:30" dependencies: - condition: vector.enabled name: vector @@ -528,7 +553,7 @@ entries: version: 1.7.2 - apiVersion: v2 appVersion: v1.7.1 - created: "2026-05-28T13:55:39.259709+05:30" + created: "2026-05-29T10:50:21.091335+05:30" dependencies: - condition: vector.enabled name: vector @@ -552,7 +577,7 @@ entries: version: 1.7.1 - apiVersion: v2 appVersion: v1.7.0 - created: "2026-05-28T13:55:39.257913+05:30" + created: "2026-05-29T10:50:21.08838+05:30" dependencies: - condition: vector.enabled name: vector @@ -575,7 +600,7 @@ entries: version: 1.7.0 - apiVersion: v2 appVersion: v1.6.3 - created: "2026-05-28T13:55:39.2559+05:30" + created: "2026-05-29T10:50:21.085825+05:30" dependencies: - condition: vector.enabled name: vector @@ -598,7 +623,7 @@ entries: version: 1.6.8 - apiVersion: v2 appVersion: v1.6.3 - created: "2026-05-28T13:55:39.253885+05:30" + created: "2026-05-29T10:50:21.083235+05:30" dependencies: - condition: vector.enabled name: vector @@ -621,7 +646,7 @@ entries: version: 1.6.7 - apiVersion: v2 appVersion: v1.6.3 - created: "2026-05-28T13:55:39.252068+05:30" + created: "2026-05-29T10:50:21.08027+05:30" dependencies: - condition: vector.enabled name: vector @@ -644,7 +669,7 @@ entries: version: 1.6.6 - apiVersion: v2 appVersion: v1.6.3 - created: "2026-05-28T13:55:39.249466+05:30" + created: "2026-05-29T10:50:21.077854+05:30" dependencies: - condition: vector.enabled name: vector @@ -667,7 +692,7 @@ entries: version: 1.6.5 - apiVersion: v2 appVersion: v1.6.3 - created: "2026-05-28T13:55:39.247552+05:30" + created: "2026-05-29T10:50:21.074994+05:30" dependencies: - condition: vector.enabled name: vector @@ -690,7 +715,7 @@ entries: version: 1.6.4 - apiVersion: v2 appVersion: v1.6.3 - created: "2026-05-28T13:55:39.245395+05:30" + created: "2026-05-29T10:50:21.07247+05:30" dependencies: - condition: vector.enabled name: vector @@ -713,7 +738,7 @@ entries: version: 1.6.3 - apiVersion: v2 appVersion: v1.6.2 - created: "2026-05-28T13:55:39.243544+05:30" + created: "2026-05-29T10:50:21.070036+05:30" dependencies: - condition: vector.enabled name: vector @@ -736,7 +761,7 @@ entries: version: 1.6.2 - apiVersion: v2 appVersion: v1.6.1 - created: "2026-05-28T13:55:39.241382+05:30" + created: "2026-05-29T10:50:21.067103+05:30" dependencies: - condition: vector.enabled name: vector @@ -759,7 +784,7 @@ entries: version: 1.6.1 - apiVersion: v2 appVersion: v1.6.0 - created: "2026-05-28T13:55:39.239534+05:30" + created: "2026-05-29T10:50:21.0647+05:30" dependencies: - condition: vector.enabled name: vector @@ -782,7 +807,7 @@ entries: version: 1.6.0 - apiVersion: v2 appVersion: v1.5.5 - created: "2026-05-28T13:55:39.237498+05:30" + created: "2026-05-29T10:50:21.061979+05:30" dependencies: - condition: vector.enabled name: vector @@ -805,7 +830,7 @@ entries: version: 1.5.5 - apiVersion: v2 appVersion: v1.5.4 - created: "2026-05-28T13:55:39.235553+05:30" + created: "2026-05-29T10:50:21.059651+05:30" dependencies: - condition: vector.enabled name: vector @@ -828,7 +853,7 @@ entries: version: 1.5.4 - apiVersion: v2 appVersion: v1.5.3 - created: "2026-05-28T13:55:39.233526+05:30" + created: "2026-05-29T10:50:21.057272+05:30" dependencies: - condition: vector.enabled name: vector @@ -851,7 +876,7 @@ entries: version: 1.5.3 - apiVersion: v2 appVersion: v1.5.2 - created: "2026-05-28T13:55:39.231838+05:30" + created: "2026-05-29T10:50:21.054496+05:30" dependencies: - condition: vector.enabled name: vector @@ -874,7 +899,7 @@ entries: version: 1.5.2 - apiVersion: v2 appVersion: v1.5.1 - created: "2026-05-28T13:55:39.23009+05:30" + created: "2026-05-29T10:50:21.052172+05:30" dependencies: - condition: vector.enabled name: vector @@ -897,7 +922,7 @@ entries: version: 1.5.1 - apiVersion: v2 appVersion: v1.5.0 - created: "2026-05-28T13:55:39.228114+05:30" + created: "2026-05-29T10:50:21.049735+05:30" dependencies: - condition: vector.enabled name: vector @@ -920,7 +945,7 @@ entries: version: 1.5.0 - apiVersion: v2 appVersion: v1.4.0 - created: "2026-05-28T13:55:39.226282+05:30" + created: "2026-05-29T10:50:21.046779+05:30" dependencies: - condition: vector.enabled name: vector @@ -943,7 +968,7 @@ entries: version: 1.4.1 - apiVersion: v2 appVersion: v1.4.0 - created: "2026-05-28T13:55:39.224252+05:30" + created: "2026-05-29T10:50:21.044559+05:30" dependencies: - condition: vector.enabled name: vector @@ -966,7 +991,7 @@ entries: version: 1.4.0 - apiVersion: v2 appVersion: v1.3.0 - created: "2026-05-28T13:55:39.222646+05:30" + created: "2026-05-29T10:50:21.041954+05:30" dependencies: - condition: vector.enabled name: vector @@ -989,7 +1014,7 @@ entries: version: 1.3.1 - apiVersion: v2 appVersion: v1.3.0 - created: "2026-05-28T13:55:39.22055+05:30" + created: "2026-05-29T10:50:21.039714+05:30" dependencies: - condition: vector.enabled name: vector @@ -1012,7 +1037,7 @@ entries: version: 1.3.0 - apiVersion: v2 appVersion: v1.2.0 - created: "2026-05-28T13:55:39.218848+05:30" + created: "2026-05-29T10:50:21.037427+05:30" dependencies: - condition: vector.enabled name: vector @@ -1035,7 +1060,7 @@ entries: version: 1.2.0 - apiVersion: v2 appVersion: v1.1.0 - created: "2026-05-28T13:55:39.217112+05:30" + created: "2026-05-29T10:50:21.034761+05:30" dependencies: - condition: vector.enabled name: vector @@ -1058,7 +1083,7 @@ entries: version: 1.1.0 - apiVersion: v2 appVersion: v1.0.0 - created: "2026-05-28T13:55:39.215071+05:30" + created: "2026-05-29T10:50:21.032552+05:30" dependencies: - condition: vector.enabled name: vector @@ -1081,7 +1106,7 @@ entries: version: 1.0.0 - apiVersion: v2 appVersion: v0.9.0 - created: "2026-05-28T13:55:39.213216+05:30" + created: "2026-05-29T10:50:21.030326+05:30" dependencies: - condition: vector.enabled name: vector @@ -1104,7 +1129,7 @@ entries: version: 0.9.0 - apiVersion: v2 appVersion: v0.8.1 - created: "2026-05-28T13:55:39.210839+05:30" + created: "2026-05-29T10:50:21.027637+05:30" dependencies: - condition: vector.enabled name: vector @@ -1127,7 +1152,7 @@ entries: version: 0.8.1 - apiVersion: v2 appVersion: v0.8.0 - created: "2026-05-28T13:55:39.208841+05:30" + created: "2026-05-29T10:50:21.02548+05:30" dependencies: - condition: vector.enabled name: vector @@ -1150,7 +1175,7 @@ entries: version: 0.8.0 - apiVersion: v2 appVersion: v0.7.3 - created: "2026-05-28T13:55:39.207177+05:30" + created: "2026-05-29T10:50:21.023301+05:30" dependencies: - condition: vector.enabled name: vector @@ -1173,7 +1198,7 @@ entries: version: 0.7.3 - apiVersion: v2 appVersion: v0.7.2 - created: "2026-05-28T13:55:39.205058+05:30" + created: "2026-05-29T10:50:21.020734+05:30" dependencies: - condition: vector.enabled name: vector @@ -1196,7 +1221,7 @@ entries: version: 0.7.2 - apiVersion: v2 appVersion: v0.7.1 - created: "2026-05-28T13:55:39.203365+05:30" + created: "2026-05-29T10:50:21.018558+05:30" dependencies: - condition: vector.enabled name: vector @@ -1219,7 +1244,7 @@ entries: version: 0.7.1 - apiVersion: v2 appVersion: v0.7.0 - created: "2026-05-28T13:55:39.20123+05:30" + created: "2026-05-29T10:50:21.016344+05:30" dependencies: - condition: vector.enabled name: vector @@ -1242,7 +1267,7 @@ entries: version: 0.7.0 - apiVersion: v2 appVersion: v0.6.2 - created: "2026-05-28T13:55:39.199666+05:30" + created: "2026-05-29T10:50:21.013676+05:30" dependencies: - condition: vector.enabled name: vector @@ -1265,7 +1290,7 @@ entries: version: 0.6.2 - apiVersion: v2 appVersion: v0.6.1 - created: "2026-05-28T13:55:39.198031+05:30" + created: "2026-05-29T10:50:21.011481+05:30" dependencies: - condition: vector.enabled name: vector @@ -1288,7 +1313,7 @@ entries: version: 0.6.1 - apiVersion: v2 appVersion: v0.6.0 - created: "2026-05-28T13:55:39.196001+05:30" + created: "2026-05-29T10:50:21.00925+05:30" dependencies: - condition: vector.enabled name: vector @@ -1311,7 +1336,7 @@ entries: version: 0.6.0 - apiVersion: v2 appVersion: v0.5.1 - created: "2026-05-28T13:55:39.194072+05:30" + created: "2026-05-29T10:50:21.006637+05:30" dependencies: - condition: vector.enabled name: vector @@ -1334,7 +1359,7 @@ entries: version: 0.5.1 - apiVersion: v2 appVersion: v0.5.0 - created: "2026-05-28T13:55:39.19203+05:30" + created: "2026-05-29T10:50:21.004408+05:30" dependencies: - condition: vector.enabled name: vector @@ -1357,7 +1382,7 @@ entries: version: 0.5.0 - apiVersion: v2 appVersion: v0.4.4 - created: "2026-05-28T13:55:39.190386+05:30" + created: "2026-05-29T10:50:21.001669+05:30" dependencies: - condition: vector.enabled name: vector @@ -1380,7 +1405,7 @@ entries: version: 0.4.5 - apiVersion: v2 appVersion: v0.4.3 - created: "2026-05-28T13:55:39.188082+05:30" + created: "2026-05-29T10:50:20.999445+05:30" dependencies: - condition: vector.enabled name: vector @@ -1403,7 +1428,7 @@ entries: version: 0.4.4 - apiVersion: v2 appVersion: v0.4.2 - created: "2026-05-28T13:55:39.186545+05:30" + created: "2026-05-29T10:50:20.997323+05:30" dependencies: - condition: vector.enabled name: vector @@ -1426,7 +1451,7 @@ entries: version: 0.4.3 - apiVersion: v2 appVersion: v0.4.1 - created: "2026-05-28T13:55:39.18469+05:30" + created: "2026-05-29T10:50:20.994806+05:30" dependencies: - condition: vector.enabled name: vector @@ -1449,7 +1474,7 @@ entries: version: 0.4.2 - apiVersion: v2 appVersion: v0.4.0 - created: "2026-05-28T13:55:39.18308+05:30" + created: "2026-05-29T10:50:20.992637+05:30" dependencies: - condition: vector.enabled name: vector @@ -1472,7 +1497,7 @@ entries: version: 0.4.1 - apiVersion: v2 appVersion: v0.4.0 - created: "2026-05-28T13:55:39.181038+05:30" + created: "2026-05-29T10:50:20.99043+05:30" dependencies: - condition: vector.enabled name: vector @@ -1495,7 +1520,7 @@ entries: version: 0.4.0 - apiVersion: v2 appVersion: v0.3.1 - created: "2026-05-28T13:55:39.179426+05:30" + created: "2026-05-29T10:50:20.987625+05:30" dependencies: - condition: vector.enabled name: vector @@ -1518,7 +1543,7 @@ entries: version: 0.3.1 - apiVersion: v2 appVersion: v0.3.0 - created: "2026-05-28T13:55:39.176814+05:30" + created: "2026-05-29T10:50:20.985352+05:30" description: Helm chart for Parseable Server digest: ff30739229b727dc637f62fd4481c886a6080ce4556bae10cafe7642ddcfd937 name: parseable @@ -1528,7 +1553,7 @@ entries: version: 0.3.0 - apiVersion: v2 appVersion: v0.2.2 - created: "2026-05-28T13:55:39.176657+05:30" + created: "2026-05-29T10:50:20.985135+05:30" description: Helm chart for Parseable Server digest: 477d0dc2f0c07d4f4c32e105d4bdd70c71113add5c2a75ac5f1cb42aa0276db7 name: parseable @@ -1538,7 +1563,7 @@ entries: version: 0.2.2 - apiVersion: v2 appVersion: v0.2.1 - created: "2026-05-28T13:55:39.176508+05:30" + created: "2026-05-29T10:50:20.984922+05:30" description: Helm chart for Parseable Server digest: 84826fcd1b4c579f301569f43b0309c07e8082bad76f5cdd25f86e86ca2e8192 name: parseable @@ -1548,7 +1573,7 @@ entries: version: 0.2.1 - apiVersion: v2 appVersion: v0.2.0 - created: "2026-05-28T13:55:39.176377+05:30" + created: "2026-05-29T10:50:20.984734+05:30" description: Helm chart for Parseable Server digest: 7a759f7f9809f3935cba685e904c021a0b645f217f4e45b9be185900c467edff name: parseable @@ -1558,7 +1583,7 @@ entries: version: 0.2.0 - apiVersion: v2 appVersion: v0.1.1 - created: "2026-05-28T13:55:39.176248+05:30" + created: "2026-05-29T10:50:20.984537+05:30" description: Helm chart for Parseable Server digest: 37993cf392f662ec7b1fbfc9a2ba00ec906d98723e38f3c91ff1daca97c3d0b3 name: parseable @@ -1568,7 +1593,7 @@ entries: version: 0.1.1 - apiVersion: v2 appVersion: v0.1.0 - created: "2026-05-28T13:55:39.176118+05:30" + created: "2026-05-29T10:50:20.984321+05:30" description: Helm chart for Parseable Server digest: 1d580d072af8d6b1ebcbfee31c2e16c907d08db754780f913b5f0032b403789b name: parseable @@ -1578,7 +1603,7 @@ entries: version: 0.1.0 - apiVersion: v2 appVersion: v0.0.8 - created: "2026-05-28T13:55:39.175988+05:30" + created: "2026-05-29T10:50:20.984078+05:30" description: Helm chart for Parseable Server digest: c805254ffa634f96ecec448bcfff9973339aa9487dd8199b21b17b79a4de9345 name: parseable @@ -1588,7 +1613,7 @@ entries: version: 0.0.8 - apiVersion: v2 appVersion: v0.0.7 - created: "2026-05-28T13:55:39.175845+05:30" + created: "2026-05-29T10:50:20.983315+05:30" description: Helm chart for Parseable Server digest: c591f617ed1fe820bb2c72a4c976a78126f1d1095d552daa07c4700f46c4708a name: parseable @@ -1598,7 +1623,7 @@ entries: version: 0.0.7 - apiVersion: v2 appVersion: v0.0.6 - created: "2026-05-28T13:55:39.175582+05:30" + created: "2026-05-29T10:50:20.983133+05:30" description: Helm chart for Parseable Server digest: f9ae56a6fcd6a59e7bee0436200ddbedeb74ade6073deb435b8fcbaf08dda795 name: parseable @@ -1608,7 +1633,7 @@ entries: version: 0.0.6 - apiVersion: v2 appVersion: v0.0.5 - created: "2026-05-28T13:55:39.17477+05:30" + created: "2026-05-29T10:50:20.982895+05:30" description: Helm chart for Parseable Server digest: 4d6b08a064fba36e16feeb820b77e1e8e60fb6de48dbf7ec8410d03d10c26ad0 name: parseable @@ -1618,7 +1643,7 @@ entries: version: 0.0.5 - apiVersion: v2 appVersion: v0.0.2 - created: "2026-05-28T13:55:39.174532+05:30" + created: "2026-05-29T10:50:20.982689+05:30" description: Helm chart for Parseable Server digest: 38a0a3e4c498afbbcc76ebfcb9cb598fa2ca843a53cc93b3cb4f135b85c10844 name: parseable @@ -1628,7 +1653,7 @@ entries: version: 0.0.2 - apiVersion: v2 appVersion: v0.0.1 - created: "2026-05-28T13:55:39.174344+05:30" + created: "2026-05-29T10:50:20.982501+05:30" description: Helm chart for Parseable Server digest: 1f1142db092b9620ee38bb2294ccbb1c17f807b33bf56da43816af7fe89f301e name: parseable @@ -1638,8 +1663,8 @@ entries: version: 0.0.1 parseable-enterprise: - apiVersion: v2 - appVersion: v2.7.3 - created: "2026-05-28T13:55:39.313252+05:30" + appVersion: v2.8.0 + created: "2026-05-29T10:50:21.164816+05:30" dependencies: - condition: vector.enabled name: vector @@ -1651,7 +1676,7 @@ entries: version: 0.48.0 description: Helm chart for Parseable Enterprise version - Needs a license to run. Please contact sales@parseable.com for more details. - digest: 8cc36a35779d4e8128ddc54ed76a7143a99b40e67984347daa239f7cebb75495 + digest: 3d22ca3d869b257ddf42ee239abf2d1c8b029ab2715233e7401e56d7b5a85002 icon: https://raw.githubusercontent.com/parseablehq/.github/main/images/new-logo.svg maintainers: - email: hi@parseable.com @@ -1660,11 +1685,11 @@ entries: name: parseable-enterprise type: application urls: - - https://charts.parseable.com/helm-releases/parseable-enterprise-2.7.3.tgz - version: 2.7.3 + - https://charts.parseable.com/helm-releases/parseable-enterprise-2.8.0.tgz + version: 2.8.0 - apiVersion: v2 appVersion: v2.7.2 - created: "2026-05-28T13:55:39.310923+05:30" + created: "2026-05-29T10:50:21.161875+05:30" dependencies: - condition: vector.enabled name: vector @@ -1689,7 +1714,7 @@ entries: version: 2.7.2 - apiVersion: v2 appVersion: v2.7.1 - created: "2026-05-28T13:55:39.308775+05:30" + created: "2026-05-29T10:50:21.159052+05:30" dependencies: - condition: vector.enabled name: vector @@ -1714,7 +1739,7 @@ entries: version: 2.7.1 - apiVersion: v2 appVersion: v2.6.6 - created: "2026-05-28T13:55:39.306617+05:30" + created: "2026-05-29T10:50:21.155682+05:30" dependencies: - condition: vector.enabled name: vector @@ -1739,7 +1764,7 @@ entries: version: 2.6.7 - apiVersion: v2 appVersion: v2.6.6 - created: "2026-05-28T13:55:39.304275+05:30" + created: "2026-05-29T10:50:21.152886+05:30" dependencies: - condition: vector.enabled name: vector @@ -1762,4 +1787,4 @@ entries: urls: - https://charts.parseable.com/helm-releases/parseable-enterprise-2.6.6.tgz version: 2.6.6 -generated: "2026-05-28T13:55:39.172114+05:30" +generated: "2026-05-29T10:50:20.981719+05:30" From ce0ae602dff8a64466add137e76ef21aff4e39e2 Mon Sep 17 00:00:00 2001 From: Nikhil Sinha <131262146+nikhilsinhaparseable@users.noreply.github.com> Date: Sat, 30 May 2026 16:11:16 +0530 Subject: [PATCH 13/47] update registry to quay (#1659) --- .github/workflows/build-push-edge-debug.yaml | 11 ++++++----- .github/workflows/build-push-edge-kafka.yaml | 13 +++++++------ .github/workflows/build-push-edge.yaml | 11 ++++++----- .github/workflows/release.yml | 15 ++++++++------- 4 files changed, 27 insertions(+), 23 deletions(-) diff --git a/.github/workflows/build-push-edge-debug.yaml b/.github/workflows/build-push-edge-debug.yaml index 2833e3c31..5fd649a1d 100644 --- a/.github/workflows/build-push-edge-debug.yaml +++ b/.github/workflows/build-push-edge-debug.yaml @@ -25,17 +25,18 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Login to Docker Hub + - name: Login to Quay uses: docker/login-action@v3 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + registry: quay.io + username: ${{ secrets.QUAY_USERNAME }} + password: ${{ secrets.QUAY_PASSWORD }} - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@v5 with: - images: parseable/parseable + images: quay.io/parseablehq/parseable - name: Clean up builder cache run: docker builder prune -f @@ -46,5 +47,5 @@ jobs: context: . file: ./Dockerfile.debug push: true - tags: parseable/parseable:edge-debug + tags: quay.io/parseablehq/parseable:edge-debug platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/build-push-edge-kafka.yaml b/.github/workflows/build-push-edge-kafka.yaml index bf2fbc851..a53693e30 100644 --- a/.github/workflows/build-push-edge-kafka.yaml +++ b/.github/workflows/build-push-edge-kafka.yaml @@ -25,17 +25,18 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Login to Docker Hub + - name: Login to Quay uses: docker/login-action@v3 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + registry: quay.io + username: ${{ secrets.QUAY_USERNAME }} + password: ${{ secrets.QUAY_PASSWORD }} - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@v5 with: - images: parseable/parseable + images: quay.io/parseablehq/parseable - name: Clean up builder cache run: docker builder prune -f @@ -46,7 +47,7 @@ jobs: context: . file: ./Dockerfile.kafka push: true - tags: parseable/parseable:edge-kafka-amd64 + tags: quay.io/parseablehq/parseable:edge-kafka-amd64 platforms: linux/amd64 build-args: | LIB_DIR=x86_64-linux-gnu @@ -57,7 +58,7 @@ jobs: context: . file: ./Dockerfile.kafka push: true - tags: parseable/parseable:edge-kafka-arm64 + tags: quay.io/parseablehq/parseable:edge-kafka-arm64 platforms: linux/arm64 build-args: | LIB_DIR=aarch64-linux-gnu \ No newline at end of file diff --git a/.github/workflows/build-push-edge.yaml b/.github/workflows/build-push-edge.yaml index 0266f76cf..be35f14fa 100644 --- a/.github/workflows/build-push-edge.yaml +++ b/.github/workflows/build-push-edge.yaml @@ -25,17 +25,18 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Login to Docker Hub + - name: Login to Quay uses: docker/login-action@v3 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + registry: quay.io + username: ${{ secrets.QUAY_USERNAME }} + password: ${{ secrets.QUAY_PASSWORD }} - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@v5 with: - images: parseable/parseable + images: quay.io/parseablehq/parseable - name: Clean up builder cache run: docker builder prune -f @@ -46,5 +47,5 @@ jobs: context: . file: ./Dockerfile push: true - tags: parseable/parseable:edge + tags: quay.io/parseablehq/parseable:edge platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e2de56206..4302fd80d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -361,17 +361,18 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Login to Docker Hub + - name: Login to Quay uses: docker/login-action@v3 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + registry: quay.io + username: ${{ secrets.QUAY_USERNAME }} + password: ${{ secrets.QUAY_PASSWORD }} - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@v5 with: - images: parseable/parseable + images: quay.io/parseablehq/parseable - name: Build and push uses: docker/build-push-action@v6 @@ -379,7 +380,7 @@ jobs: context: . file: ./Dockerfile push: true - tags: parseable/parseable:${{ github.ref_name }} + tags: quay.io/parseablehq/parseable:${{ github.ref_name }} platforms: linux/amd64,linux/arm64 - name: Build and push kafka amd64 @@ -388,7 +389,7 @@ jobs: context: . file: ./Dockerfile.kafka push: true - tags: parseable/parseable:${{ github.ref_name }}-kafka-amd64 + tags: quay.io/parseablehq/parseable:${{ github.ref_name }}-kafka-amd64 platforms: linux/amd64 build-args: | LIB_DIR=x86_64-linux-gnu @@ -399,7 +400,7 @@ jobs: context: . file: ./Dockerfile.kafka push: true - tags: parseable/parseable:${{ github.ref_name }}-kafka-arm64 + tags: quay.io/parseablehq/parseable:${{ github.ref_name }}-kafka-arm64 platforms: linux/arm64 build-args: | LIB_DIR=aarch64-linux-gnu From 3f0f21d82492e633f7c2da9243ebb7daf597e8e9 Mon Sep 17 00:00:00 2001 From: YGN <149811989+ygndotgg@users.noreply.github.com> Date: Mon, 1 Jun 2026 11:42:57 +0530 Subject: [PATCH 14/47] fix: load IRSA web identity config for S3 (#1649) Addressed with a smaller invariant-based implementation. Static S3 credentials and IRSA web identity values are now validated as pairs fixes #1646 --- src/storage/s3.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/storage/s3.rs b/src/storage/s3.rs index b66bd5a6e..589d71b94 100644 --- a/src/storage/s3.rs +++ b/src/storage/s3.rs @@ -69,6 +69,9 @@ use super::{ // in bytes // const MULTIPART_UPLOAD_SIZE: usize = 1024 * 1024 * 100; const AWS_CONTAINER_CREDENTIALS_RELATIVE_URI: &str = "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"; +const AWS_WEB_IDENTITY_TOKEN_FILE: &str = "AWS_WEB_IDENTITY_TOKEN_FILE"; +const AWS_ROLE_ARN: &str = "AWS_ROLE_ARN"; +const AWS_ROLE_SESSION_NAME: &str = "AWS_ROLE_SESSION_NAME"; #[derive(Debug, Clone, clap::Args)] #[command( @@ -261,12 +264,35 @@ impl S3Config { builder = builder.with_checksum_algorithm(Checksum::SHA256) } + assert!( + self.access_key_id.is_some() == self.secret_key.is_some(), + "P_S3_ACCESS_KEY and P_S3_SECRET_KEY must be set together" + ); + if let Some((access_key, secret_key)) = self.access_key_id.as_ref().zip(self.secret_key.as_ref()) { builder = builder .with_access_key_id(access_key) .with_secret_access_key(secret_key); + } else { + let token_file = std::env::var(AWS_WEB_IDENTITY_TOKEN_FILE).ok(); + let role_arn = std::env::var(AWS_ROLE_ARN).ok(); + + assert!( + token_file.is_some() == role_arn.is_some(), + "{AWS_WEB_IDENTITY_TOKEN_FILE} and {AWS_ROLE_ARN} must be set together" + ); + + if let Some((token_file, role_arn)) = token_file.zip(role_arn) { + builder = builder + .with_config(AmazonS3ConfigKey::WebIdentityTokenFile, token_file) + .with_config(AmazonS3ConfigKey::RoleArn, role_arn); + + if let Ok(session_name) = std::env::var(AWS_ROLE_SESSION_NAME) { + builder = builder.with_config(AmazonS3ConfigKey::RoleSessionName, session_name); + } + } } if let Some(ssec_encryption_key) = &self.ssec_encryption_key { From 8fd4b1a3141cd26b3340e3f10b3e1933f7b26509 Mon Sep 17 00:00:00 2001 From: parmesant Date: Tue, 2 Jun 2026 04:05:53 -0700 Subject: [PATCH 15/47] Ingestion optimization (#1661) * cli var for dataset stats * perf: batch staging arrow writes Batch disk staging writes per output filename before writing to Arrow IPC. This reduces per-request writer mutex hold time and cuts the number of DiskWriter::write calls during high-volume OTEL metrics ingest. Also keep targeted hotpath probes around ingest, JSON conversion, staging, and parquet conversion paths, and skip object-store sync work for streams without local parquet/schema files. * perf: improve otel metrics ingest path Batch staging arrow writes per output file and prepare OTEL metric parquet row groups off-thread before sequential writes. This reduces request-path writer contention and hides row-group concat/sort work behind merge/write. Reduce JSON allocation churn in OTEL metric flattening and generic flattening by reusing owned maps, pre-sizing containers, inserting attributes/exemplars directly, and avoiding per-row known-field set construction for series hashing. Also guard shutdown local sync against concurrent local sync cycles and avoid panicking on transient arrow-file metadata races. * added hotpath as a feature flag * fix: coderabbit suggestions --- Cargo.toml | 17 ++ src/cli.rs | 9 + src/event/format/json.rs | 7 +- src/event/format/mod.rs | 23 ++ src/event/mod.rs | 1 + src/handlers/http/health_check.rs | 16 +- src/handlers/http/modal/utils/ingest_utils.rs | 1 + src/lib.rs | 2 + src/main.rs | 1 + src/otel/logs.rs | 2 +- src/otel/metrics.rs | 272 ++++++++---------- src/otel/otel_utils.rs | 64 +++-- src/parseable/staging/writer.rs | 130 ++++++++- src/parseable/streams.rs | 178 ++++++++---- src/storage/object_storage.rs | 12 +- src/sync.rs | 35 +++ src/utils/json/flatten.rs | 14 +- src/utils/json/mod.rs | 3 + 18 files changed, 516 insertions(+), 271 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9c30ed840..1acdf9748 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,6 +91,14 @@ tokio = { version = "^1.43", default-features = false, features = [ tokio-stream = { version = "0.1.17", features = ["fs"] } tokio-util = { version = "0.7" } +# perf +hotpath = { version = "0.16.0", optional = true, features = [ + "hotpath", + "hotpath-cpu", + "hotpath-alloc", + "tokio" +] } + # # Logging and Metrics # opentelemetry-proto = { version = "0.30.0", features = [ # "gen-tonic", @@ -212,6 +220,15 @@ assets-sha1 = "a7523ef819d38678275ae165c443564b2f9a3fc1" [features] debug = [] +hotpath = [ + "dep:hotpath", + "hotpath/hotpath", + "hotpath/hotpath-cpu", + "hotpath/hotpath-alloc", + "hotpath/tokio", +] +hotpath-alloc = ["hotpath"] +hotpath-cpu = ["hotpath"] kafka = [ "rdkafka", "rdkafka/ssl-vendored", diff --git a/src/cli.rs b/src/cli.rs index d6664ff2e..f380a02c4 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -559,6 +559,15 @@ pub struct Options { )] pub max_field_statistics: usize, + // collect statistics for dataset fields + #[arg( + long, + env = "P_COLLECT_DATASET_STATS", + default_value = "true", + help = "Collect statistics for dataset fields" + )] + pub collect_dataset_stats: bool, + #[arg( long, env = "P_MAX_EVENT_PAYLOAD_SIZE", diff --git a/src/event/format/json.rs b/src/event/format/json.rs index cc4ca90f3..02c9a51aa 100644 --- a/src/event/format/json.rs +++ b/src/event/format/json.rs @@ -60,6 +60,7 @@ impl EventFormat for Event { // convert the incoming json to a vector of json values // also extract the arrow schema, tags and metadata from the incoming json + #[cfg_attr(feature = "hotpath", hotpath::measure)] fn to_data( self, schema: &HashMap>, @@ -156,7 +157,7 @@ impl EventFormat for Event { infer_schema.clone(), ]).map_err(|err| anyhow!("Could not merge schema of this event with that of the existing stream. {:?}", err))?; is_first = true; - let schema = infer_schema + let schema: Vec> = infer_schema .fields .iter() .filter(|field| !field.data_type().is_null()) @@ -327,6 +328,10 @@ fn rename_json_keys(values: Vec) -> Result, anyhow::Error> { .into_iter() .map(|value| { if let Value::Object(map) = value { + if !map.keys().any(|key| key.starts_with('@')) { + return Ok(Value::Object(map)); + } + // Collect original keys to check for collisions let original_keys: HashSet = map.keys().cloned().collect(); diff --git a/src/event/format/mod.rs b/src/event/format/mod.rs index 5d8419a79..ae015a8e9 100644 --- a/src/event/format/mod.rs +++ b/src/event/format/mod.rs @@ -162,6 +162,7 @@ pub trait EventFormat: Sized { /// Returns the UTC time at ingestion fn get_p_timestamp(&self) -> DateTime; + #[cfg_attr(feature = "hotpath", hotpath::measure)] fn into_recordbatch( self, storage_schema: &HashMap>, @@ -606,6 +607,28 @@ pub fn rename_per_record_type_mismatches( let Value::Object(map) = value else { return value; }; + let needs_rename = map.iter().any(|(key, val)| { + if val.is_null() { + return false; + } + let target_type = existing_schema + .get(key) + .map(|f| f.data_type()) + .or_else(|| inferred_types.get(key.as_str()).copied()); + let Some(target_type) = target_type else { + return false; + }; + if (val.is_array() || val.is_object()) + && (target_type.is_list() + || matches!(target_type, DataType::Struct(_) | DataType::Map(_, _))) + { + return false; + } + !value_compatible_with_type(val, target_type, schema_version) + }); + if !needs_rename { + return Value::Object(map); + } let new_map: serde_json::Map = map .into_iter() .map(|(key, val)| { diff --git a/src/event/mod.rs b/src/event/mod.rs index f9955e986..4820e854c 100644 --- a/src/event/mod.rs +++ b/src/event/mod.rs @@ -72,6 +72,7 @@ impl Event { is_first_event = self.is_first_event ) )] + #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn process(self) -> Result<(), EventError> { let mut key = get_schema_key(&self.rb.schema().fields); if self.time_partition.is_some() { diff --git a/src/handlers/http/health_check.rs b/src/handlers/http/health_check.rs index 1cd90f447..3c567f655 100644 --- a/src/handlers/http/health_check.rs +++ b/src/handlers/http/health_check.rs @@ -32,6 +32,7 @@ use once_cell::sync::Lazy; use tokio::{sync::Mutex, task::JoinSet}; use tracing::{error, info}; +use crate::sync::shutdown_local_sync_flush_and_convert; use crate::utils::get_tenant_id_from_request; use crate::{parseable::PARSEABLE, storage::object_storage::sync_all_streams}; @@ -84,20 +85,7 @@ async fn perform_sync_operations() { } async fn perform_local_sync() { - let mut local_sync_joinset = JoinSet::new(); - - // Sync staging - PARSEABLE - .streams - .flush_and_convert(&mut local_sync_joinset, false, true); - - while let Some(res) = local_sync_joinset.join_next().await { - match res { - Ok(Ok(_)) => info!("Successfully converted arrow files to parquet."), - Ok(Err(err)) => error!("Failed to convert arrow files to parquet. {err:?}"), - Err(err) => error!("Failed to join async task: {err}"), - } - } + shutdown_local_sync_flush_and_convert().await; } async fn perform_object_store_sync() { diff --git a/src/handlers/http/modal/utils/ingest_utils.rs b/src/handlers/http/modal/utils/ingest_utils.rs index b353f1b29..0ecdb0486 100644 --- a/src/handlers/http/modal/utils/ingest_utils.rs +++ b/src/handlers/http/modal/utils/ingest_utils.rs @@ -156,6 +156,7 @@ pub async fn flatten_and_push_logs( skip(json, log_source, p_custom_fields, time_partition, telemetry_type, tenant_id), fields(stream_name, record_count = tracing::field::Empty) )] +#[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn push_logs( stream_name: &str, json: Value, diff --git a/src/lib.rs b/src/lib.rs index 289494723..648b252ed 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -67,6 +67,8 @@ pub use datafusion_proto; pub use handlers::http::modal::{ ParseableServer, ingest_server::IngestServer, query_server::QueryServer, server::Server, }; +#[cfg(feature = "hotpath")] +pub use hotpath; use once_cell::sync::Lazy; pub use openid; use parseable::PARSEABLE; diff --git a/src/main.rs b/src/main.rs index 42cba34f5..4d07c1608 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,6 +32,7 @@ use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::{EnvFilter, Registry, fmt}; #[actix_web::main] +#[cfg_attr(feature = "hotpath", hotpath::main)] async fn main() -> anyhow::Result<()> { init_logger(); // Install the rustls crypto provider before any TLS operations. diff --git a/src/otel/logs.rs b/src/otel/logs.rs index f3bf7c430..f6d770fad 100644 --- a/src/otel/logs.rs +++ b/src/otel/logs.rs @@ -138,7 +138,7 @@ pub fn flatten_log_record(log_record: &LogRecord) -> Map { if log_record.body.is_some() { let body = &log_record.body; - let body_json = collect_json_from_values(body, &"body".to_string()); + let body_json = collect_json_from_values(body, "body"); for (key, value) in &body_json { // Always insert the original body field as is log_record_json.insert(key.clone(), value.clone()); diff --git a/src/otel/metrics.rs b/src/otel/metrics.rs index c6f017c28..7d6a44d63 100644 --- a/src/otel/metrics.rs +++ b/src/otel/metrics.rs @@ -15,6 +15,7 @@ * along with this program. If not, see . * */ +use once_cell::sync::Lazy; use opentelemetry_proto::tonic::collector::metrics::v1::ExportMetricsServiceRequest; use opentelemetry_proto::tonic::metrics::v1::number_data_point::Value as NumberDataPointValue; use opentelemetry_proto::tonic::metrics::v1::{ @@ -24,6 +25,8 @@ use opentelemetry_proto::tonic::metrics::v1::{ use serde_json::{Map, Value}; use rustc_hash::FxHasher; +use std::borrow::Cow; +use std::collections::HashSet; use std::hash::Hasher; use tracing::info_span; @@ -81,6 +84,9 @@ pub const OTEL_METRICS_KNOWN_FIELD_LIST: [&str; 37] = [ "__series_hash", ]; +static OTEL_METRICS_KNOWN_FIELDS: Lazy> = + Lazy::new(|| OTEL_METRICS_KNOWN_FIELD_LIST.iter().copied().collect()); + /// Compute a stable u64 identifier for the physical series a sample /// belongs to. Hashes `metric_name` plus every attribute key/value pair /// that survived OTel flattening — everything in the flattened data @@ -91,19 +97,17 @@ pub const OTEL_METRICS_KNOWN_FIELD_LIST: [&str; 37] = [ /// non-cryptographic) and feeds keys in sorted order so the hash /// doesn't depend on JSON Map iteration order. fn compute_series_hash(dp: &Map) -> u64 { - let known: std::collections::HashSet<&str> = - OTEL_METRICS_KNOWN_FIELD_LIST.iter().copied().collect(); - let mut label_pairs: Vec<(&str, String)> = dp - .iter() - .filter(|(k, _)| !known.contains(k.as_str())) - .map(|(k, v)| { - let v_str = match v { - Value::String(s) => s.clone(), - other => other.to_string(), - }; - (k.as_str(), v_str) - }) - .collect(); + let mut label_pairs: Vec<(&str, Cow<'_, str>)> = Vec::with_capacity(dp.len()); + for (key, value) in dp { + if OTEL_METRICS_KNOWN_FIELDS.contains(key.as_str()) { + continue; + } + let value = match value { + Value::String(s) => Cow::Borrowed(s.as_str()), + other => Cow::Owned(other.to_string()), + }; + label_pairs.push((key.as_str(), value)); + } label_pairs.sort_by(|a, b| a.0.cmp(b.0)); let mut hasher = FxHasher::default(); @@ -122,51 +126,42 @@ fn compute_series_hash(dp: &Map) -> u64 { hasher.finish() } -/// otel metrics event has json array for exemplar -/// this function flatten the exemplar json array -/// and returns a `Map` of the exemplar json -/// this function is reused in all json objects that have exemplar -fn flatten_exemplar(exemplars: &[Exemplar]) -> Vec> { - exemplars - .iter() - .map(|exemplar| { - let mut exemplar_json = Map::new(); - insert_attributes(&mut exemplar_json, &exemplar.filtered_attributes); - exemplar_json.insert( - "exemplar_time_unix_nano".to_string(), - Value::String(convert_epoch_nano_to_timestamp( - exemplar.time_unix_nano as i64, - )), - ); - exemplar_json.insert( - "exemplar_span_id".to_string(), - Value::String(hex::encode(&exemplar.span_id)), - ); - exemplar_json.insert( - "exemplar_trace_id".to_string(), - Value::String(hex::encode(&exemplar.trace_id)), - ); - if let Some(value) = &exemplar.value { - match value { - ExemplarValue::AsDouble(double_val) => { - exemplar_json.insert( - "exemplar_value".to_string(), - serde_json::Number::from_f64(*double_val) - .map(Value::Number) - .unwrap_or(Value::Null), - ); - } - ExemplarValue::AsInt(int_val) => { - exemplar_json.insert( - "exemplar_value".to_string(), - Value::Number(serde_json::Number::from(*int_val)), - ); - } +fn insert_exemplars(map: &mut Map, exemplars: &[Exemplar]) { + for exemplar in exemplars { + insert_attributes(map, &exemplar.filtered_attributes); + map.insert( + "exemplar_time_unix_nano".to_string(), + Value::String(convert_epoch_nano_to_timestamp( + exemplar.time_unix_nano as i64, + )), + ); + map.insert( + "exemplar_span_id".to_string(), + Value::String(hex::encode(&exemplar.span_id)), + ); + map.insert( + "exemplar_trace_id".to_string(), + Value::String(hex::encode(&exemplar.trace_id)), + ); + if let Some(value) = &exemplar.value { + match value { + ExemplarValue::AsDouble(double_val) => { + map.insert( + "exemplar_value".to_string(), + serde_json::Number::from_f64(*double_val) + .map(Value::Number) + .unwrap_or(Value::Null), + ); + } + ExemplarValue::AsInt(int_val) => { + map.insert( + "exemplar_value".to_string(), + Value::Number(serde_json::Number::from(*int_val)), + ); } } - exemplar_json - }) - .collect() + } + } } /// otel metrics event has json array for number data points @@ -177,7 +172,7 @@ fn flatten_number_data_points(data_points: &[NumberDataPoint]) -> Vec Vec { @@ -239,11 +227,8 @@ fn flatten_gauge(gauge: &Gauge) -> Vec> { /// and returns a `Vec` of `Map` for each data point fn flatten_sum(sum: &Sum) -> Vec> { let mut data_points = flatten_number_data_points(&sum.data_points); - let agg_temp = flatten_aggregation_temporality(sum.aggregation_temporality); for dp in &mut data_points { - for (k, v) in &agg_temp { - dp.insert(k.clone(), v.clone()); - } + insert_aggregation_temporality(dp, sum.aggregation_temporality); dp.insert("is_monotonic".to_string(), Value::Bool(sum.is_monotonic)); } data_points @@ -254,9 +239,9 @@ fn flatten_sum(sum: &Sum) -> Vec> { /// this function flatten the histogram json object /// and returns a `Vec` of `Map` for each data point fn flatten_histogram(histogram: &Histogram) -> Vec> { - let mut data_points_json = Vec::new(); + let mut data_points_json = Vec::with_capacity(histogram.data_points.len()); for data_point in &histogram.data_points { - let mut data_point_json = Map::new(); + let mut data_point_json = Map::with_capacity(data_point.attributes.len() + 10); insert_attributes(&mut data_point_json, &data_point.attributes); data_point_json.insert( "start_time_unix_nano".to_string(), @@ -301,29 +286,14 @@ fn flatten_histogram(histogram: &Histogram) -> Vec> { "data_point_explicit_bounds".to_string(), data_point_explicit_bounds, ); - let exemplar_json = flatten_exemplar(&data_point.exemplars); - if !exemplar_json.is_empty() { - for exemplar in exemplar_json { - for (key, value) in exemplar { - data_point_json.insert(key, value); - } - } - } + insert_exemplars(&mut data_point_json, &data_point.exemplars); - data_point_json.extend(flatten_data_point_flags(data_point.flags)); + insert_data_point_flags(&mut data_point_json, data_point.flags); insert_number_if_some(&mut data_point_json, "min", &data_point.min); insert_number_if_some(&mut data_point_json, "max", &data_point.max); + insert_aggregation_temporality(&mut data_point_json, histogram.aggregation_temporality); data_points_json.push(data_point_json); } - let mut histogram_json = Map::new(); - histogram_json.extend(flatten_aggregation_temporality( - histogram.aggregation_temporality, - )); - for data_point_json in &mut data_points_json { - for (key, value) in &histogram_json { - data_point_json.insert(key.clone(), value.clone()); - } - } data_points_json } @@ -331,7 +301,7 @@ fn flatten_histogram(histogram: &Histogram) -> Vec> { /// this function flatten the buckets json object /// and returns a `Map` of the flattened json fn flatten_buckets(bucket: &Buckets) -> Map { - let mut bucket_json = Map::new(); + let mut bucket_json = Map::with_capacity(2); bucket_json.insert("offset".to_string(), Value::Number(bucket.offset.into())); bucket_json.insert( "bucket_count".to_string(), @@ -351,9 +321,9 @@ fn flatten_buckets(bucket: &Buckets) -> Map { /// this function flatten the exponential histogram json object /// and returns a `Vec` of `Map` for each data point fn flatten_exp_histogram(exp_histogram: &ExponentialHistogram) -> Vec> { - let mut data_points_json = Vec::new(); + let mut data_points_json = Vec::with_capacity(exp_histogram.data_points.len()); for data_point in &exp_histogram.data_points { - let mut data_point_json = Map::new(); + let mut data_point_json = Map::with_capacity(data_point.attributes.len() + 12); insert_attributes(&mut data_point_json, &data_point.attributes); data_point_json.insert( "start_time_unix_nano".to_string(), @@ -392,26 +362,11 @@ fn flatten_exp_histogram(exp_histogram: &ExponentialHistogram) -> Vec Vec Vec> { - let mut data_points_json = Vec::new(); + let mut data_points_json = Vec::with_capacity(summary.data_points.len()); for data_point in &summary.data_points { - let mut data_point_json = Map::new(); + let mut data_point_json = Map::with_capacity(data_point.attributes.len() + 6); insert_attributes(&mut data_point_json, &data_point.attributes); data_point_json.insert( "start_time_unix_nano".to_string(), @@ -453,25 +408,20 @@ fn flatten_summary(summary: &Summary) -> Vec> { .quantile_values .iter() .map(|quantile_value| { - Value::Object( - vec![ - ( - "quantile", - serde_json::Number::from_f64(quantile_value.quantile) - .map(Value::Number) - .unwrap_or(Value::Null), - ), - ( - "value", - serde_json::Number::from_f64(quantile_value.value) - .map(Value::Number) - .unwrap_or(Value::Null), - ), - ] - .into_iter() - .map(|(k, v)| (k.to_string(), v)) - .collect(), - ) + let mut quantile_map = Map::with_capacity(2); + quantile_map.insert( + "quantile".to_string(), + serde_json::Number::from_f64(quantile_value.quantile) + .map(Value::Number) + .unwrap_or(Value::Null), + ); + quantile_map.insert( + "value".to_string(), + serde_json::Number::from_f64(quantile_value.value) + .map(Value::Number) + .unwrap_or(Value::Null), + ); + Value::Object(quantile_map) }) .collect(), ), @@ -505,29 +455,25 @@ pub fn flatten_metrics_record(metrics_record: &Metric) -> Vec let metric_desc = Value::String(metrics_record.description.clone()); let metric_unit = Value::String(metrics_record.unit.clone()); let metric_type_val = Value::String(metric_type.to_string()); - let mut metadata = Map::new(); + let mut metadata = Map::with_capacity(metrics_record.metadata.len()); insert_attributes(&mut metadata, &metrics_record.metadata); if data_points.is_empty() { - let mut single = Map::new(); + let mut single = Map::with_capacity(metadata.len() + 8); single.insert("metric_name".to_string(), metric_name); single.insert("metric_description".to_string(), metric_desc); single.insert("metric_unit".to_string(), metric_unit); single.insert("metric_type".to_string(), metric_type_val); match &metrics_record.data { Some(metric::Data::Sum(sum)) => { - single.extend(flatten_aggregation_temporality(sum.aggregation_temporality)); + insert_aggregation_temporality(&mut single, sum.aggregation_temporality); single.insert("is_monotonic".to_string(), Value::Bool(sum.is_monotonic)); } Some(metric::Data::Histogram(histogram)) => { - single.extend(flatten_aggregation_temporality( - histogram.aggregation_temporality, - )); + insert_aggregation_temporality(&mut single, histogram.aggregation_temporality); } Some(metric::Data::ExponentialHistogram(exp_histogram)) => { - single.extend(flatten_aggregation_temporality( - exp_histogram.aggregation_temporality, - )); + insert_aggregation_temporality(&mut single, exp_histogram.aggregation_temporality); } _ => {} } @@ -549,6 +495,17 @@ pub fn flatten_metrics_record(metrics_record: &Metric) -> Vec data_points } +fn metric_data_point_count(metric: &Metric) -> usize { + match &metric.data { + Some(metric::Data::Gauge(gauge)) => gauge.data_points.len(), + Some(metric::Data::Sum(sum)) => sum.data_points.len(), + Some(metric::Data::Histogram(histogram)) => histogram.data_points.len(), + Some(metric::Data::ExponentialHistogram(exp_histogram)) => exp_histogram.data_points.len(), + Some(metric::Data::Summary(summary)) => summary.data_points.len(), + None => 0, + } +} + /// Common function to process resource metrics and merge resource-level fields #[allow(clippy::too_many_arguments)] fn process_resource_metrics( @@ -571,7 +528,12 @@ fn process_resource_metrics( for resource_metric in resource_metrics { // Build resource-level fields once per resource - let mut resource_fields = Map::new(); + let mut resource_fields = Map::with_capacity( + get_resource(resource_metric) + .map(|resource| resource.attributes.len()) + .unwrap_or_default() + + 2, + ); if let Some(resource) = get_resource(resource_metric) { insert_attributes(&mut resource_fields, &resource.attributes); resource_fields.insert( @@ -606,6 +568,12 @@ fn process_resource_metrics( ); let metrics = get_metrics(scope_metric); + vec_otel_json.reserve( + metrics + .iter() + .map(|metric| metric_data_point_count(get_metric(metric)).max(1)) + .sum::(), + ); let date = chrono::Utc::now().date_naive().to_string(); increment_metrics_collected_by_date(metrics.len() as u64, &date, tenant_id); @@ -640,6 +608,7 @@ fn process_resource_metrics( /// this function performs the custom flattening of the otel metrics /// and returns a `Vec` of `Value::Object` of the flattened json +#[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn flatten_otel_metrics(message: MetricsData, tenant_id: &str) -> Vec { process_resource_metrics( &message.resource_metrics, @@ -682,13 +651,8 @@ pub fn flatten_otel_metrics_protobuf( result } -/// otel metrics event has json object for aggregation temporality -/// there is a mapping of aggregation temporality to its description provided in proto -/// this function fetches the description from the aggregation temporality -/// and adds it to the flattened json -fn flatten_aggregation_temporality(aggregation_temporality: i32) -> Map { - let mut aggregation_temporality_json = Map::new(); - aggregation_temporality_json.insert( +fn insert_aggregation_temporality(map: &mut Map, aggregation_temporality: i32) { + map.insert( "aggregation_temporality".to_string(), Value::Number(aggregation_temporality.into()), ); @@ -698,27 +662,23 @@ fn flatten_aggregation_temporality(aggregation_temporality: i32) -> Map "CUMULATIVE", _ => "", }; - aggregation_temporality_json.insert( + map.insert( "aggregation_temporality_description".to_string(), Value::String(description.to_string()), ); - - aggregation_temporality_json } -fn flatten_data_point_flags(flags: u32) -> Map { - let mut data_point_flags_json = Map::new(); - data_point_flags_json.insert("data_point_flags".to_string(), Value::Number(flags.into())); +fn insert_data_point_flags(map: &mut Map, flags: u32) { + map.insert("data_point_flags".to_string(), Value::Number(flags.into())); let description = match flags { 0 => "DO_NOT_USE", 1 => "NO_RECORDED_VALUE_MASK", _ => "", }; - data_point_flags_json.insert( + map.insert( "data_point_flags_description".to_string(), Value::String(description.to_string()), ); - data_point_flags_json } #[cfg(test)] diff --git a/src/otel/otel_utils.rs b/src/otel/otel_utils.rs index 88cb788d8..6caec1f21 100644 --- a/src/otel/otel_utils.rs +++ b/src/otel/otel_utils.rs @@ -23,21 +23,26 @@ use opentelemetry_proto::tonic::common::v1::{ use serde_json::{Map, Value}; // Value can be one of types - String, Bool, Int, Double, ArrayValue, AnyValue, KeyValueList, Byte -pub fn collect_json_from_value(key: &String, value: OtelValue) -> Map { +pub fn collect_json_from_value(key: &str, value: OtelValue) -> Map { let mut value_json: Map = Map::new(); + insert_json_from_value(&mut value_json, key, value); + value_json +} + +fn insert_json_from_value(map: &mut Map, key: &str, value: OtelValue) { match value { OtelValue::StringValue(str_val) => { - value_json.insert(key.to_string(), Value::String(str_val)); + map.insert(key.to_string(), Value::String(str_val)); } OtelValue::BoolValue(bool_val) => { - value_json.insert(key.to_string(), Value::Bool(bool_val)); + map.insert(key.to_string(), Value::Bool(bool_val)); } OtelValue::IntValue(int_val) => { - value_json.insert(key.to_string(), Value::String(int_val.to_string())); + map.insert(key.to_string(), Value::String(int_val.to_string())); } OtelValue::DoubleValue(double_val) => { if let Some(number) = serde_json::Number::from_f64(double_val) { - value_json.insert(key.to_string(), Value::Number(number)); + map.insert(key.to_string(), Value::Number(number)); } } OtelValue::ArrayValue(array_val) => { @@ -51,17 +56,15 @@ pub fn collect_json_from_value(key: &String, value: OtelValue) -> Map { // Create a JSON object to store the key-value list let kv_object = collect_json_from_key_value_list(kv_list_val); - for (key, value) in kv_object.iter() { - value_json.insert(key.clone(), value.clone()); - } + map.extend(kv_object); } OtelValue::BytesValue(bytes_val) => { - value_json.insert( + map.insert( key.to_string(), Value::String(String::from_utf8_lossy(&bytes_val).to_string()), ); @@ -72,15 +75,13 @@ pub fn collect_json_from_value(key: &String, value: OtelValue) -> Map Value { - let mut json_array = Vec::new(); + let mut json_array = Vec::with_capacity(array_value.values.len()); for value in &array_value.values { if let Some(val) = &value.value { match val { @@ -122,12 +123,11 @@ fn collect_json_from_array_value(array_value: &ArrayValue) -> Value { /// The function iterates through the key-value pairs in the list /// and collects their JSON representations into a single Map fn collect_json_from_key_value_list(key_value_list: KeyValueList) -> Map { - let mut kv_list_json: Map = Map::new(); + let mut kv_list_json: Map = Map::with_capacity(key_value_list.values.len()); for key_value in key_value_list.values { if let Some(val) = key_value.value { if let Some(val) = val.value { - let json_value = collect_json_from_value(&key_value.key, val); - kv_list_json.extend(json_value); + insert_json_from_value(&mut kv_list_json, &key_value.key, val); } else { tracing::warn!("Key '{}' has no value in key-value list", key_value.key); } @@ -136,23 +136,25 @@ fn collect_json_from_key_value_list(key_value_list: KeyValueList) -> Map Map { +pub fn collect_json_from_anyvalue(key: &str, value: AnyValue) -> Map { collect_json_from_value(key, value.value.unwrap()) } //traverse through Value by calling function ollect_json_from_any_value -pub fn collect_json_from_values(values: &Option, key: &String) -> Map { - let mut value_json: Map = Map::new(); +pub fn collect_json_from_values(values: &Option, key: &str) -> Map { + let mut value_json: Map = Map::with_capacity(1); for value in values.iter() { - value_json = collect_json_from_anyvalue(key, value.clone()); + if let Some(value) = value.value.clone() { + insert_json_from_value(&mut value_json, key, value); + } } value_json } pub fn value_to_string(value: serde_json::Value) -> String { - match value.clone() { + match value { e @ Value::Number(_) | e @ Value::Bool(_) => e.to_string(), Value::String(s) => s, _ => "".to_string(), @@ -160,13 +162,12 @@ pub fn value_to_string(value: serde_json::Value) -> String { } pub fn flatten_attributes(attributes: &[KeyValue]) -> Map { - let mut attributes_json: Map = Map::new(); + let mut attributes_json: Map = Map::with_capacity(attributes.len()); for attribute in attributes { - let key = &attribute.key; - let value = &attribute.value; - let value_json = collect_json_from_values(value, &key.to_string()); - for (attr_key, attr_val) in &value_json { - attributes_json.insert(attr_key.clone(), attr_val.clone()); + if let Some(value) = &attribute.value + && let Some(value) = value.value.clone() + { + insert_json_from_value(&mut attributes_json, &attribute.key, value); } } attributes_json @@ -193,9 +194,12 @@ pub fn insert_bool_if_some(map: &mut Map, key: &str, option: &Opt } pub fn insert_attributes(map: &mut Map, attributes: &[KeyValue]) { - let attributes_json = flatten_attributes(attributes); - for (key, value) in attributes_json { - map.insert(key, value); + for attribute in attributes { + if let Some(value) = &attribute.value + && let Some(value) = value.value.clone() + { + insert_json_from_value(map, &attribute.key, value); + } } } diff --git a/src/parseable/staging/writer.rs b/src/parseable/staging/writer.rs index ec58800a3..a3fe96096 100644 --- a/src/parseable/staging/writer.rs +++ b/src/parseable/staging/writer.rs @@ -19,7 +19,7 @@ use std::{ collections::{HashMap, HashSet}, - fs::{File, OpenOptions}, + fs::{self, File, OpenOptions}, io::BufWriter, path::PathBuf, sync::Arc, @@ -41,10 +41,137 @@ use crate::{ use super::StagingError; +const DISK_WRITE_BATCH_ROWS: usize = 32_768; + #[derive(Default)] pub struct Writer { pub mem: MemWriter<16384>, pub disk: HashMap, + disk_pending: HashMap, +} + +impl Writer { + #[cfg_attr(feature = "hotpath", hotpath::measure)] + pub fn push_disk( + &mut self, + filename: String, + rb: &RecordBatch, + file_path: PathBuf, + range: TimeRange, + ) -> Result<(), StagingError> { + let pending = self.disk_pending.entry(filename.clone()).or_default(); + pending.rows += rb.num_rows(); + pending.range.get_or_insert(range); + pending.batches.push(rb.clone()); + + if pending.rows >= DISK_WRITE_BATCH_ROWS { + self.flush_pending_disk(&filename, file_path)?; + } + + Ok(()) + } + + pub fn flush_pending_disk( + &mut self, + filename: &str, + file_path: PathBuf, + ) -> Result<(), StagingError> { + let Some(pending) = self.disk_pending.remove(filename) else { + return Ok(()); + }; + if pending.batches.is_empty() { + return Ok(()); + } + + write_pending_disk_batch(&mut self.disk, filename.to_owned(), pending, file_path)?; + + Ok(()) + } + + pub fn take_flushable_disk( + &mut self, + forced: bool, + ) -> (HashMap, PendingDiskWrites) { + let mut flushable_disk = HashMap::new(); + let old_disk = std::mem::take(&mut self.disk); + for (filename, writer) in old_disk { + if !forced && writer.is_current() { + self.disk.insert(filename, writer); + } else { + flushable_disk.insert(filename, writer); + } + } + + let mut flushable_pending = HashMap::new(); + let old_pending = std::mem::take(&mut self.disk_pending); + for (filename, pending) in old_pending { + if !forced && pending.is_current() { + self.disk_pending.insert(filename, pending); + } else { + flushable_pending.insert(filename, pending); + } + } + + (flushable_disk, PendingDiskWrites(flushable_pending)) + } +} + +pub struct PendingDiskWrites(HashMap); + +impl PendingDiskWrites { + pub fn flush_into( + self, + disk: &mut HashMap, + data_path: &std::path::Path, + ) -> Result<(), StagingError> { + for (filename, pending) in self.0 { + write_pending_disk_batch(disk, filename.clone(), pending, data_path.join(filename))?; + } + Ok(()) + } +} + +fn write_pending_disk_batch( + disk: &mut HashMap, + filename: String, + pending: PendingDiskBatch, + file_path: PathBuf, +) -> Result<(), StagingError> { + if pending.batches.is_empty() { + return Ok(()); + } + + let schema = pending.batches[0].schema(); + let batch = concat_batches(&schema, pending.batches.iter())?; + match disk.get_mut(&filename) { + Some(writer) => writer.write(&batch)?, + None => { + let range = pending.range.expect("pending disk batch must have range"); + if let Some(parent) = file_path.parent() { + fs::create_dir_all(parent)?; + } + let mut writer = DiskWriter::try_new(file_path, &schema, range)?; + writer.write(&batch)?; + disk.insert(filename, writer); + } + } + + Ok(()) +} + +#[derive(Default)] +struct PendingDiskBatch { + rows: usize, + batches: Vec, + range: Option, +} + +impl PendingDiskBatch { + fn is_current(&self) -> bool { + self.range + .as_ref() + .is_some_and(|range| range.contains(Utc::now())) + } } pub struct DiskWriter { @@ -77,6 +204,7 @@ impl DiskWriter { } /// Write a single recordbatch into file + #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn write(&mut self, rb: &RecordBatch) -> Result<(), StagingError> { self.inner.write(rb).map_err(StagingError::Arrow) } diff --git a/src/parseable/streams.rs b/src/parseable/streams.rs index 8c1e55115..cfb3f6b45 100644 --- a/src/parseable/streams.rs +++ b/src/parseable/streams.rs @@ -35,6 +35,7 @@ use parquet::{ use relative_path::RelativePathBuf; use std::sync::PoisonError; use std::{ + collections::VecDeque, collections::{HashMap, HashSet}, fs::{self, File, OpenOptions, remove_file, write}, num::NonZeroU32, @@ -43,7 +44,7 @@ use std::{ time::{Instant, SystemTime, UNIX_EPOCH}, }; use tokio::task::JoinSet; -use tracing::{Instrument, error, info, info_span, instrument, trace, warn}; +use tracing::{error, info, info_span, instrument, trace, warn}; use ulid::Ulid; use crate::{ @@ -65,14 +66,15 @@ use crate::{ use super::{ ARROW_FILE_EXTENSION, LogStream, PART_FILE_EXTENSION, - staging::{ - StagingError, - reader::MergedReverseRecordReader, - writer::{DiskWriter, Writer}, - }, + staging::{StagingError, reader::MergedReverseRecordReader, writer::Writer}, }; const INPROCESS_DIR_PREFIX: &str = "processing_"; +const METRIC_ROW_GROUP_PREP_IN_FLIGHT: usize = 1; + +struct PreparedMetricRowGroup { + batch: RecordBatch, +} /// Returns the filename for parquet if provided arrows file path is valid as per our expectation fn arrow_path_to_parquet( @@ -115,7 +117,6 @@ pub struct Stream { pub writer: Mutex, pub ingestor_id: Option, } - impl Stream { pub fn new( options: Arc, @@ -138,6 +139,7 @@ impl Stream { } // Concatenates record batches and puts them in memory store for each event. + #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn push( &self, schema_key: &str, @@ -173,25 +175,13 @@ impl Stream { if self.options.mode != Mode::Query || stream_type == StreamType::Internal { let filename = self.filename_by_partition(schema_key, parsed_timestamp, custom_partition_values); - match guard.disk.get_mut(&filename) { - Some(writer) => { - writer.write(record)?; - } - None => { - // entry is not present thus we create it - std::fs::create_dir_all(&self.data_path)?; - - let range = TimeRange::granularity_range( - parsed_timestamp.and_local_timezone(Utc).unwrap(), - OBJECT_STORE_DATA_GRANULARITY, - ); - let file_path = self.data_path.join(&filename); - let mut writer = DiskWriter::try_new(file_path, &record.schema(), range)?; + let range = TimeRange::granularity_range( + parsed_timestamp.and_local_timezone(Utc).unwrap(), + OBJECT_STORE_DATA_GRANULARITY, + ); + let file_path = self.data_path.join(&filename); - writer.write(record)?; - guard.disk.insert(filename, writer); - } - }; + guard.push_disk(filename, record, file_path, range)?; } guard.mem.push(schema_key, record)?; @@ -368,14 +358,13 @@ impl Stream { let mut arrow_files = self.arrow_files(); if !shutdown_signal { arrow_files.retain(|path| { - let creation = path - .metadata() + path.metadata() .ok() .and_then(|meta| meta.created().or_else(|_| meta.modified()).ok()) - .expect("Arrow file should have a valid creation or modified time"); - - // Compare if creation time is actually from previous minute - minute_from_system_time(creation) < minute_from_system_time(exclude) + .is_some_and(|creation| { + // Compare if creation time is actually from previous minute + minute_from_system_time(creation) < minute_from_system_time(exclude) + }) }); } arrow_files @@ -420,6 +409,7 @@ impl Stream { base.join(format!("{INPROCESS_DIR_PREFIX}{minute}")) } + #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn parquet_files(&self) -> Vec { let Ok(dir) = self.data_path.read_dir() else { return vec![]; @@ -480,6 +470,7 @@ impl Stream { skip(self, tenant_id), fields(stream_name = %self.stream_name) )] + #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn prepare_parquet( &self, init_signal: bool, @@ -559,12 +550,13 @@ impl Stream { Ok(()) } + #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn flush(&self, forced: bool) -> Result<(), StagingError> { let _span = info_span!("flush", stream_name = %self.stream_name, forced).entered(); // Swap out stale writers under the lock, drop them after releasing it. // DiskWriter::Drop does I/O (IPC finish + file rename) so dropping // outside the lock avoids blocking concurrent push() calls. - let stale_writers = { + let (mut stale_writers, pending_writes) = { let mut writer = self.writer.lock().map_err(|poisoned| { StagingError::PoisonError(PoisonError::new(format!( "Writer lock poisoned while flushing data for stream {} - {}", @@ -572,18 +564,9 @@ impl Stream { ))) })?; writer.mem.clear(); - - let mut old_disk = HashMap::new(); - std::mem::swap(&mut writer.disk, &mut old_disk); - if !forced { - for (k, v) in old_disk.drain() { - if v.is_current() { - writer.disk.insert(k, v); - } - } - } - old_disk + writer.take_flushable_disk(forced) }; + pending_writes.flush_into(&mut stale_writers, &self.data_path)?; // DiskWriter::Drop I/O happens here, outside the lock drop(stale_writers); Ok(()) @@ -669,6 +652,7 @@ impl Stream { /// Bails out without sorting when either source column is missing /// (non-metric stream, schema drift) so the caller can write the /// batch unchanged. + #[cfg_attr(feature = "hotpath", hotpath::measure)] fn sort_batch_for_metric_pruning( batch: &RecordBatch, time_partition_field: &str, @@ -710,6 +694,44 @@ impl Stream { Ok(RecordBatch::try_new(schema, columns)?) } + #[cfg_attr(feature = "hotpath", hotpath::measure)] + fn prepare_metric_row_group( + schema: Arc, + buffer: Vec, + time_partition_field: String, + ) -> Result { + let combined = arrow::compute::concat_batches(&schema, &buffer)?; + let batch = Self::sort_batch_for_metric_pruning(&combined, &time_partition_field)?; + + Ok(PreparedMetricRowGroup { batch }) + } + + fn spawn_metric_row_group_prepare( + schema: Arc, + buffer: Vec, + time_partition_field: String, + ) -> std::sync::mpsc::Receiver> { + let (tx, rx) = std::sync::mpsc::sync_channel(1); + rayon::spawn(move || { + let _ = tx.send(Self::prepare_metric_row_group( + schema, + buffer, + time_partition_field, + )); + }); + rx + } + + fn receive_prepared_metric_row_group( + rx: std::sync::mpsc::Receiver>, + ) -> Result { + rx.recv().map_err(|err| { + StagingError::ObjectStorage(std::io::Error::other(format!( + "Metric row-group preparation worker failed: {err}" + ))) + })? + } + fn reset_staging_metrics(&self, tenant_id: &Option) { let tenant_str = tenant_id.as_deref().unwrap_or(DEFAULT_TENANT); metrics::STAGING_FILES @@ -750,6 +772,7 @@ impl Stream { /// This function reads arrow files, groups their schemas /// /// converts them into parquet files and returns a merged schema + #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn convert_disk_files_to_parquet( &self, time_partition: Option<&String>, @@ -806,7 +829,6 @@ impl Stream { self.cleanup_arrow_files_and_dir(&arrow_files, tenant_id); } } - if schemas.is_empty() { return Ok(None); } @@ -814,6 +836,7 @@ impl Stream { Ok(Some(Schema::try_merge(schemas)?)) } + #[cfg_attr(feature = "hotpath", hotpath::measure)] fn write_parquet_part_file( &self, part_path: &Path, @@ -847,31 +870,63 @@ impl Stream { // per-page (metric_name min, max) stats narrow to the slice // each page actually carries. let target = self.options.row_group_size; - let mut buffer: Vec = Vec::new(); + let buffer_capacity = record_reader.readers.len(); + let mut pending_row_groups = VecDeque::with_capacity(METRIC_ROW_GROUP_PREP_IN_FLIGHT); + let mut buffer: Vec = Vec::with_capacity(buffer_capacity); let mut buffered_rows: usize = 0; - for record in record_reader.merged_iter(schema.clone(), time_partition.cloned()) { - buffered_rows += record.num_rows(); + let mut merged_iter = + record_reader.merged_iter(schema.clone(), time_partition.cloned()); + loop { + let Some(record) = merged_iter.next() else { + break; + }; + let record_rows = record.num_rows(); + buffered_rows += record_rows; buffer.push(record); if buffered_rows >= target { - let combined = arrow::compute::concat_batches(schema, &buffer)?; - let sorted = - Self::sort_batch_for_metric_pruning(&combined, &time_partition_field)?; - writer.write(&sorted)?; + let row_group_buffer = + std::mem::replace(&mut buffer, Vec::with_capacity(buffer_capacity)); + let next_row_group = Self::spawn_metric_row_group_prepare( + schema.clone(), + row_group_buffer, + time_partition_field.clone(), + ); + pending_row_groups.push_back(next_row_group); + if pending_row_groups.len() > METRIC_ROW_GROUP_PREP_IN_FLIGHT + && let Some(rx) = pending_row_groups.pop_front() + { + let prepared = Self::receive_prepared_metric_row_group(rx)?; + writer.write(&prepared.batch)?; + } buffer.clear(); buffered_rows = 0; } } if !buffer.is_empty() { - let combined = arrow::compute::concat_batches(schema, &buffer)?; - let sorted = Self::sort_batch_for_metric_pruning(&combined, &time_partition_field)?; - writer.write(&sorted)?; + let next_row_group = Self::spawn_metric_row_group_prepare( + schema.clone(), + buffer, + time_partition_field.clone(), + ); + pending_row_groups.push_back(next_row_group); + if pending_row_groups.len() > METRIC_ROW_GROUP_PREP_IN_FLIGHT + && let Some(rx) = pending_row_groups.pop_front() + { + let prepared = Self::receive_prepared_metric_row_group(rx)?; + writer.write(&prepared.batch)?; + } } + while let Some(rx) = pending_row_groups.pop_front() { + let prepared = Self::receive_prepared_metric_row_group(rx)?; + writer.write(&prepared.batch)?; + } + writer.close()?; } else { for ref record in record_reader.merged_iter(schema.clone(), time_partition.cloned()) { writer.write(record)?; } + writer.close()?; } - writer.close()?; if !Self::is_valid_parquet_file(part_path, &self.stream_name) { error!( @@ -886,6 +941,7 @@ impl Stream { } /// function to validate parquet files + #[cfg_attr(feature = "hotpath", hotpath::measure)] fn is_valid_parquet_file(path: &Path, stream_name: &str) -> bool { // First check file size as a quick validation match path.metadata() { @@ -928,6 +984,7 @@ impl Stream { } } + #[cfg_attr(feature = "hotpath", hotpath::measure)] fn cleanup_arrow_files_and_dir(&self, arrow_files: &[PathBuf], tenant_id: &Option) { let tenant_str = tenant_id.as_deref().unwrap_or(DEFAULT_TENANT); for (i, file) in arrow_files.iter().enumerate() { @@ -1311,6 +1368,7 @@ impl Stream { skip(self, tenant_id), fields(stream_name = %self.stream_name) )] + #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn flush_and_convert( &self, init_signal: bool, @@ -1481,12 +1539,10 @@ impl Streams { for stream in streams { let tenant = tenant_id.clone(); let span = info_span!("stream_sync", stream_name = %stream.stream_name); - joinset.spawn( - async move { - stream.flush_and_convert(init_signal, shutdown_signal, &Some(tenant)) - } - .instrument(span), - ); + joinset.spawn_blocking(move || { + let _guard = span.enter(); + stream.flush_and_convert(init_signal, shutdown_signal, &Some(tenant)) + }); } } } diff --git a/src/storage/object_storage.rs b/src/storage/object_storage.rs index b67cf18f9..a1831b5d8 100644 --- a/src/storage/object_storage.rs +++ b/src/storage/object_storage.rs @@ -184,8 +184,10 @@ async fn upload_single_parquet_file( let manifest = catalog::create_from_parquet_file(absolute_path, &path) .map_err(|e| (path.clone(), ObjectStorageError::from(e)))?; - // Calculate field stats if enabled - calculate_stats_if_enabled(&stream_name, &path, &schema, tenant_id).await; + if PARSEABLE.options.collect_dataset_stats { + // collect field stats if enabled + calculate_stats_if_enabled(&stream_name, &path, &schema, tenant_id).await; + } Ok(UploadResult { file_path: path, @@ -1293,6 +1295,12 @@ pub fn sync_all_streams(joinset: &mut JoinSet>) { }; for tenant_id in tenants { for stream_name in PARSEABLE.streams.list(&tenant_id) { + if let Ok(stream) = PARSEABLE.get_stream(&stream_name, &tenant_id) + && stream.parquet_files().is_empty() + && stream.schema_files().is_empty() + { + continue; + } let object_store = object_store.clone(); let id = tenant_id.clone(); let span = info_span!("stream_upload", stream_name = %stream_name); diff --git a/src/sync.rs b/src/sync.rs index 14a88ab38..ebdc1d057 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -47,6 +47,41 @@ impl Drop for SyncRunningGuard { } } +async fn wait_for_local_sync_guard() -> SyncRunningGuard { + let mut warned = false; + loop { + if LOCAL_SYNC_RUNNING + .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) + .is_ok() + { + return SyncRunningGuard(&LOCAL_SYNC_RUNNING); + } + + if !warned { + warn!("Waiting for existing local_sync cycle before shutdown sync"); + warned = true; + } + sleep(Duration::from_millis(250)).await; + } +} + +pub async fn shutdown_local_sync_flush_and_convert() { + let _guard = wait_for_local_sync_guard().await; + let mut local_sync_joinset = JoinSet::new(); + + PARSEABLE + .streams + .flush_and_convert(&mut local_sync_joinset, false, true); + + while let Some(res) = local_sync_joinset.join_next().await { + match res { + Ok(Ok(_)) => info!("Successfully converted arrow files to parquet."), + Ok(Err(err)) => error!("Failed to convert arrow files to parquet. {err:?}"), + Err(err) => error!("Failed to join async task: {err}"), + } + } +} + use crate::alerts::alert_enums::AlertTask; use crate::alerts::alerts_utils; use crate::parseable::PARSEABLE; diff --git a/src/utils/json/flatten.rs b/src/utils/json/flatten.rs index f14890aeb..d3a462ff2 100644 --- a/src/utils/json/flatten.rs +++ b/src/utils/json/flatten.rs @@ -63,6 +63,7 @@ pub enum JsonFlattenError { // Recursively flattens JSON objects and arrays, e.g. with the separator `.`, starting from the TOP // `{"key": "value", "nested_key": {"key":"value"}}` becomes `{"key": "value", "nested_key.key": "value"}` +#[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn flatten( nested_value: &mut Value, separator: &str, @@ -77,8 +78,9 @@ pub fn flatten( validate_time_partition(nested_dict, time_partition, time_partition_limit)?; validate_custom_partition(nested_dict, custom_partition)?; } - let mut map = Map::new(); - flatten_object(&mut map, None, nested_dict, separator)?; + let original = std::mem::take(nested_dict); + let mut map = Map::with_capacity(original.len()); + flatten_object(&mut map, None, original, separator)?; *nested_dict = map; } Value::Array(arr) => { @@ -221,24 +223,25 @@ pub fn validate_time_partition( fn flatten_object( output_map: &mut Map, parent_key: Option<&str>, - nested_map: &mut Map, + nested_map: Map, separator: &str, ) -> Result<(), JsonFlattenError> { for (key, mut value) in nested_map { let new_key = match parent_key { Some(parent) => format!("{parent}{separator}{key}"), - None => key.to_string(), + None => key, }; match &mut value { Value::Object(obj) => { + let obj = std::mem::take(obj); flatten_object(output_map, Some(&new_key), obj, separator)?; } Value::Array(arr) if arr.iter().any(Value::is_object) => { flatten_array_objects(output_map, &new_key, arr, separator)?; } _ => { - output_map.insert(new_key, std::mem::take(value)); + output_map.insert(new_key, value); } } } @@ -258,6 +261,7 @@ pub fn flatten_array_objects( match value { Value::Object(nested_object) => { let mut output_map = Map::new(); + let nested_object = std::mem::take(nested_object); flatten_object(&mut output_map, Some(parent_key), nested_object, separator)?; for (key, value) in output_map { let column = columns diff --git a/src/utils/json/mod.rs b/src/utils/json/mod.rs index 1027baec6..87405fe7c 100644 --- a/src/utils/json/mod.rs +++ b/src/utils/json/mod.rs @@ -34,6 +34,7 @@ pub mod strict; /// calls the function `flatten_json` which results Vec or Error /// in case when Vec is returned, converts the Vec to Value of Array /// this is to ensure recursive flattening does not happen for heavily nested jsons +#[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn flatten_json_body( body: Value, time_partition: Option<&String>, @@ -194,6 +195,7 @@ fn process_partitioned_non_array( } /// Processes data when no partitioning is configured (original logic) +#[cfg_attr(feature = "hotpath", hotpath::measure)] fn process_non_partitioned( body: Value, time_partition: Option<&String>, @@ -217,6 +219,7 @@ fn process_non_partitioned( Ok(vec![data]) } +#[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn convert_array_to_object( body: Value, time_partition: Option<&String>, From a9b48478a6b3533ad947554461da4dfcde1bfc0f Mon Sep 17 00:00:00 2001 From: Nitish Tiwari Date: Tue, 2 Jun 2026 16:58:56 +0530 Subject: [PATCH 16/47] chore: cleanup readme and update helm to point to new container registry (#1663) --- .github/ISSUE_TEMPLATE/add-adopter.yml | 54 ---- README.md | 97 ++------ helm-releases/parseable-2.8.1.tgz | Bin 0 -> 52662 bytes helm-releases/parseable-enterprise-2.8.1.tgz | Bin 0 -> 57254 bytes helm/Chart.yaml | 8 +- helm/values.yaml | 2 +- index.yaml | 247 ++++++++++++------- src/cli.rs | 12 +- 8 files changed, 198 insertions(+), 222 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/add-adopter.yml create mode 100644 helm-releases/parseable-2.8.1.tgz create mode 100644 helm-releases/parseable-enterprise-2.8.1.tgz diff --git a/.github/ISSUE_TEMPLATE/add-adopter.yml b/.github/ISSUE_TEMPLATE/add-adopter.yml deleted file mode 100644 index e707b8863..000000000 --- a/.github/ISSUE_TEMPLATE/add-adopter.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: "Add Adopter" -description: "Add your organization to the Parseable adopters list" -title: "Add adopter: " -labels: ["new-adopter"] -body: - - type: markdown - attributes: - value: | - Thanks for using Parseable! Fill out the form below to add your organization to our adopters list. - A PR will be created automatically — no fork needed. - - - type: input - id: org_name - attributes: - label: Organization Name - description: "The name of your organization" - placeholder: "Acme Corp" - validations: - required: true - - - type: input - id: org_url - attributes: - label: Organization URL - description: "Your organization's website" - placeholder: "https://example.com" - validations: - required: true - - - type: input - id: contact - attributes: - label: Contact - description: "GitHub @handle or name of the contact person" - placeholder: "@username" - validations: - required: true - - - type: textarea - id: description - attributes: - label: Description of Use - description: "How does your organization use Parseable?" - placeholder: "We use Parseable for centralized logging of our microservices..." - validations: - required: true - - - type: checkboxes - id: confirmation - attributes: - label: Confirmation - options: - - label: "I am authorized to represent this organization" - required: true diff --git a/README.md b/README.md index 4dab307d9..82e769e92 100644 --- a/README.md +++ b/README.md @@ -6,74 +6,51 @@
-
+
-[![Docker Pulls](https://img.shields.io/docker/pulls/parseable/parseable?logo=docker&label=Docker%20Pulls)](https://hub.docker.com/r/parseable/parseable) [![Slack](https://img.shields.io/badge/slack-brightgreen.svg?logo=slack&label=Community&style=flat&color=%2373DC8C&)](https://logg.ing/community) [![Docs](https://img.shields.io/badge/stable%20docs-parseable.com%2Fdocs-brightgreen?style=flat&color=%2373DC8C&label=Docs)](https://logg.ing/docs) -[![Build](https://img.shields.io/github/checks-status/parseablehq/parseable/main?style=flat&color=%2373DC8C&label=Checks)](https://github.com/parseablehq/parseable/actions) +[![Build](https://img.shields.io/github/checks-status/parseablehq/parseable/main?style=flat&color=%2373DC8C&label=Build)](https://github.com/parseablehq/parseable/actions) -[Key Concepts](https://www.parseable.com/docs/key-concepts) | [Features](https://www.parseable.com/docs/features/alerts) | [Documentation](https://www.parseable.com/docs) | [Demo](https://app.parseable.com) | [FAQ](https://www.parseable.com/docs/key-concepts/data-model#faq) +**[Introduction](https://www.parseable.com/docs/introduction)   •  ** +**[Docs](https://parseable.com/docs)   •  ** +**[Features](https://www.parseable.com/docs/features)   •  ** +**[Benchmarks](https://www.parseable.com/docs/benchmarks)     **
-Parseable is a full-stack observability platform built to ingest, analyze and extract insights from all types of telemetry (MELT) data. You can run Parseable on your local machine, in the cloud, or use [Parseable Cloud](https://app.parseable.com) — the fully managed service. To experience Parseable UI, checkout [app.parseable.com ↗︎](https://app.parseable.com). +Parseable is an open source, columnar data lake platform - purpose built for observability. Send logs, metrics & traces to Parseable via popular logging agents, OpenTelemetry, Kafka, eBPF or other integrations. Use native observability features like alerting, dashboards, anomaly detection, APM, PromQL & SQL - all within a single binary. -
-

- Try Parseable Cloud — Start Free ↗︎ -

-

The fastest way to get started. No infrastructure to manage.

-
+## Why Parseable? - +Purpose built for observability and designed around proven data lake engineering patterns, Parseable gives you everything you need to make sense of your telemetry data, right out of the box, with no external dependencies or stitching together of multiple tools. -## Quickstart :zap: +Some of the key highlights include: -### Run Parseable +- [Data lake architecture](https://www.parseable.com/docs/architecture): Parseable Data lake architecture allows running stateless compute with object storage as the backing storage. This allows scaling storage and compute independently, and avoids the pitfalls of traditional observability systems. -
-Docker Image -

+- [Fully featured](https://www.parseable.com/docs/features): Parseable is feature complete with alerting, dashboards, anomaly detection, APM, and more. You can do all of this and more from a single binary, without needing to stitch together multiple tools. -Get started with Parseable Docker image with a single command: +- [Agent ready](https://www.parseable.com/docs/integrations#ai-agents--llms): Whether you need to observe your AI agents or use LLMs to analyze your telemetry data, Parseable has you covered with native support for AI agents and LLMs. -```bash -docker run -p 8000:8000 \ - parseable/parseable:latest \ - parseable local-store -``` +- [OpenTelemetry native](https://www.parseable.com/docs/ingest-data/otel): With native OTel support, you can send telemetry data to Parseable without any custom modifications or plugins. Parseable can be used as a drop-in replacement for your existing OpenTelemetry Collector setup. -

-
- -
-Executable Binary -

+## Quickstart Download and run the Parseable binary on your laptop: -- Linux or MacOS - ```bash curl -fsSL https://logg.ing/install | bash ``` -- Windows +

+For Windows ```pwsh powershell -c "irm https://logg.ing/install-windows | iex" ``` - -

-### Ingestion and query - Once you have Parseable running, ingest data with the below command. This will send logs to the `demo` stream. You can see the logs in the dashboard. ```bash @@ -90,47 +67,23 @@ curl --location --request POST 'http://localhost:8000/api/v1/ingest' \ ]' ``` -Access the UI at [http://localhost:8000 ↗︎](http://localhost:8000). You can login to the dashboard default credentials `admin`, `admin`. +Access the UI at http://localhost:8000. You can login to the dashboard default credentials `admin`, `admin`. -## Getting started :bulb: +For production deployments, refer the [installation guide ↗︎](https://www.parseable.com/docs/self-hosted/installation) for best practices and hardening tips. -For quickstart, refer the [quickstart section ↗︎](#quickstart-zap). +> [!TIP] +> Try out the [Parseable cloud](https://app.parseable.com) — 14 days free trial, no credit card required. -This section elaborates available options to run Parseable in production or development environments. +## Contributing -- Distributed Parseable on Kubernetes: [Helm Installation](https://www.parseable.com/docs/installation/distributed/k8s-helm). -- Distributed Parseable on AWS EC2 / VMs / Linux: [Binary Installation](https://www.parseable.com/docs/installation/distributed/linux). +Contributors -## Features :rocket: - -- [High availability & Cluster mode ↗︎](https://www.parseable.com/docs/key-concepts/high-availability) -- [Smart cache ↗︎](https://www.parseable.com/docs/features/smart-cache) -- [Alerts ↗︎](https://www.parseable.com/docs/features/alerts) -- [Role based access control ↗︎](https://www.parseable.com/docs/features/rbac) -- [OAuth2 support ↗︎](https://www.parseable.com/docs/features/oepnid) -- [OpenTelemetry support ↗︎](https://www.parseable.com/docs/OpenTelemetry/logs) - -## Adopters :handshake: - -Organizations using Parseable in production. [Add yours here](https://github.com/parseablehq/parseable/issues/new?template=add-adopter.yml) — no fork needed! - - -| Organization | Description of Use | -| --- | --- | -| [HireXL](https://www.hirexl.in/) | Frontend application logging | -| [Elfsquad](https://elfsquad.io) | Centralized application/infrastructure logging | - +[Contribution guide ↗︎](https://github.com/parseablehq/parseable/blob/main/CONTRIBUTING.md) -## Verify images :writing_hand: +## Verify images Parseable builds are attested for build provenance and integrity using the [attest-build-provenance](https://github.com/actions/attest-build-provenance) action. The attestations can be verified by having the latest version of [GitHub CLI](https://github.com/cli/cli/releases/latest) installed in your system. Then, execute the following command: ```sh gh attestation verify PATH/TO/YOUR/PARSEABLE/ARTIFACT-BINARY -R parseablehq/parseable ``` - -## Contributing :trophy: - - - -[Contribution guide ↗︎](https://github.com/parseablehq/parseable/blob/main/CONTRIBUTING.md) diff --git a/helm-releases/parseable-2.8.1.tgz b/helm-releases/parseable-2.8.1.tgz new file mode 100644 index 0000000000000000000000000000000000000000..6a75f03dc99b7538bb4bc74a69740d78c42cf0a0 GIT binary patch literal 52662 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 literal 0 HcmV?d00001 diff --git a/helm/Chart.yaml b/helm/Chart.yaml index 4c9c9b4d4..a1747b4a4 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -1,10 +1,10 @@ apiVersion: v2 name: parseable -description: Parseable Helm chart - Predictive, unified observability — faster resolution, fewer escalations, happier customers. +description: Helm chart for Parseable OSS version. Columnar data lake platform - purpose built for observability type: application -version: 2.8.0 -appVersion: "v2.8.0" -icon: "https://raw.githubusercontent.com/parseablehq/.github/main/images/new-logo.svg" +version: 2.8.1 +appVersion: "v2.8.1" +icon: "https://raw.githubusercontent.com/parseablehq/.github/main/images/logo.svg" maintainers: - name: Parseable Team diff --git a/helm/values.yaml b/helm/values.yaml index c4cc9df1f..ec07ccf57 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -1,6 +1,6 @@ parseable: image: - repository: parseable/parseable + repository: quay.io/parseablehq/parseable tag: "v2.8.0" pullPolicy: Always ## object store can be local-store, s3-store, blob-store or gcs-store. diff --git a/index.yaml b/index.yaml index 3acaa3af2..57878b7d7 100644 --- a/index.yaml +++ b/index.yaml @@ -3,7 +3,7 @@ entries: pai: - apiVersion: v2 appVersion: 0.2.0 - created: "2026-05-29T10:50:20.982305+05:30" + created: "2026-06-02T15:41:36.741733+05:30" description: Parseable Auto Instrumentation (PAI) operator for Kubernetes digest: fd04518e8fc9e25d3fa876971a38b4c65005a207ba9440ae8ce981cd070646d9 home: https://github.com/parseablehq/pai @@ -21,7 +21,7 @@ entries: version: 0.2.0 - apiVersion: v2 appVersion: 0.1.0 - created: "2026-05-29T10:50:20.982057+05:30" + created: "2026-06-02T15:41:36.741579+05:30" description: Parseable Auto Instrumentation (PAI) operator for Kubernetes digest: 9443e75bef96a424fd5063ddd02cee18e6050207d73e505f66be467287c9f7f7 home: https://github.com/parseablehq/pai @@ -38,9 +38,34 @@ entries: - https://charts.parseable.com/helm-releases/pai-0.1.0.tgz version: 0.1.0 parseable: + - apiVersion: v2 + appVersion: v2.8.1 + created: "2026-06-02T15:41:36.852979+05:30" + dependencies: + - condition: vector.enabled + name: vector + repository: https://helm.vector.dev + version: 0.20.1 + - condition: fluent-bit.enabled + name: fluent-bit + repository: https://fluent.github.io/helm-charts + version: 0.48.0 + description: Helm chart for Parseable OSS version. Columnar data lake platform + - purpose built for observability + digest: bb6e433061a3f93a9ecdf117bfb92b2e420d5c818e37d5a353f2f6a836da6f64 + icon: https://raw.githubusercontent.com/parseablehq/.github/main/images/logo.svg + maintainers: + - email: hi@parseable.com + name: Parseable Team + url: https://parseable.com + name: parseable + type: application + urls: + - https://charts.parseable.com/helm-releases/parseable-2.8.1.tgz + version: 2.8.1 - apiVersion: v2 appVersion: v2.8.0 - created: "2026-05-29T10:50:21.149696+05:30" + created: "2026-06-02T15:41:36.851325+05:30" dependencies: - condition: vector.enabled name: vector @@ -65,7 +90,7 @@ entries: version: 2.8.0 - apiVersion: v2 appVersion: v2.7.2 - created: "2026-05-29T10:50:21.147014+05:30" + created: "2026-06-02T15:41:36.849344+05:30" dependencies: - condition: vector.enabled name: vector @@ -77,7 +102,7 @@ entries: version: 0.48.0 description: Parseable Helm chart - Predictive, unified observability — faster resolution, fewer escalations, happier customers. - digest: 97505137cd6b45bafc77d88e88cd85906a6821c08fb2922806920b7e9bafe8f3 + digest: 088e065317c79a2880e805a40dc9e09d331fa0a1542d705ac1ade7fffe088ac4 icon: https://raw.githubusercontent.com/parseablehq/.github/main/images/new-logo.svg maintainers: - email: hi@parseable.com @@ -90,7 +115,7 @@ entries: version: 2.7.2 - apiVersion: v2 appVersion: v2.7.1 - created: "2026-05-29T10:50:21.144242+05:30" + created: "2026-06-02T15:41:36.847684+05:30" dependencies: - condition: vector.enabled name: vector @@ -115,7 +140,7 @@ entries: version: 2.7.1 - apiVersion: v2 appVersion: v2.6.6 - created: "2026-05-29T10:50:21.141113+05:30" + created: "2026-06-02T15:41:36.846001+05:30" dependencies: - condition: vector.enabled name: vector @@ -140,7 +165,7 @@ entries: version: 2.6.6 - apiVersion: v2 appVersion: v2.6.5 - created: "2026-05-29T10:50:21.138482+05:30" + created: "2026-06-02T15:41:36.843907+05:30" dependencies: - condition: vector.enabled name: vector @@ -165,7 +190,7 @@ entries: version: 2.6.5 - apiVersion: v2 appVersion: v2.5.13 - created: "2026-05-29T10:50:21.121593+05:30" + created: "2026-06-02T15:41:36.833301+05:30" dependencies: - condition: vector.enabled name: vector @@ -190,7 +215,7 @@ entries: version: 2.5.13 - apiVersion: v2 appVersion: v2.5.7 - created: "2026-05-29T10:50:21.135328+05:30" + created: "2026-06-02T15:41:36.842289+05:30" dependencies: - condition: vector.enabled name: vector @@ -215,7 +240,7 @@ entries: version: 2.5.7 - apiVersion: v2 appVersion: v2.5.6 - created: "2026-05-29T10:50:21.132728+05:30" + created: "2026-06-02T15:41:36.840302+05:30" dependencies: - condition: vector.enabled name: vector @@ -240,7 +265,7 @@ entries: version: 2.5.6 - apiVersion: v2 appVersion: v2.5.5 - created: "2026-05-29T10:50:21.130091+05:30" + created: "2026-06-02T15:41:36.838625+05:30" dependencies: - condition: vector.enabled name: vector @@ -265,7 +290,7 @@ entries: version: 2.5.5 - apiVersion: v2 appVersion: v2.5.4 - created: "2026-05-29T10:50:21.127103+05:30" + created: "2026-06-02T15:41:36.836817+05:30" dependencies: - condition: vector.enabled name: vector @@ -289,7 +314,7 @@ entries: version: 2.5.4 - apiVersion: v2 appVersion: v2.5.3 - created: "2026-05-29T10:50:21.124491+05:30" + created: "2026-06-02T15:41:36.834987+05:30" dependencies: - condition: vector.enabled name: vector @@ -313,7 +338,7 @@ entries: version: 2.5.3 - apiVersion: v2 appVersion: v2.4.0 - created: "2026-05-29T10:50:21.118874+05:30" + created: "2026-06-02T15:41:36.831202+05:30" dependencies: - condition: vector.enabled name: vector @@ -337,7 +362,7 @@ entries: version: 2.4.0 - apiVersion: v2 appVersion: v2.3.3 - created: "2026-05-29T10:50:21.115623+05:30" + created: "2026-06-02T15:41:36.829534+05:30" dependencies: - condition: vector.enabled name: vector @@ -361,7 +386,7 @@ entries: version: 2.3.3 - apiVersion: v2 appVersion: v2.3.2 - created: "2026-05-29T10:50:21.113059+05:30" + created: "2026-06-02T15:41:36.827539+05:30" dependencies: - condition: vector.enabled name: vector @@ -385,7 +410,7 @@ entries: version: 2.3.2 - apiVersion: v2 appVersion: v2.3.1 - created: "2026-05-29T10:50:21.110494+05:30" + created: "2026-06-02T15:41:36.825881+05:30" dependencies: - condition: vector.enabled name: vector @@ -409,7 +434,7 @@ entries: version: 2.3.1 - apiVersion: v2 appVersion: v2.3.0 - created: "2026-05-29T10:50:21.107589+05:30" + created: "2026-06-02T15:41:36.823921+05:30" dependencies: - condition: vector.enabled name: vector @@ -433,7 +458,7 @@ entries: version: 2.3.0 - apiVersion: v2 appVersion: v2.1.0 - created: "2026-05-29T10:50:21.10505+05:30" + created: "2026-06-02T15:41:36.822233+05:30" dependencies: - condition: vector.enabled name: vector @@ -457,7 +482,7 @@ entries: version: 2.1.0 - apiVersion: v2 appVersion: v1.7.5 - created: "2026-05-29T10:50:21.102134+05:30" + created: "2026-06-02T15:41:36.82011+05:30" dependencies: - condition: vector.enabled name: vector @@ -481,7 +506,7 @@ entries: version: 2.0.0 - apiVersion: v2 appVersion: v1.7.5 - created: "2026-05-29T10:50:21.099584+05:30" + created: "2026-06-02T15:41:36.818459+05:30" dependencies: - condition: vector.enabled name: vector @@ -505,7 +530,7 @@ entries: version: 1.7.5 - apiVersion: v2 appVersion: v1.7.3 - created: "2026-05-29T10:50:21.096926+05:30" + created: "2026-06-02T15:41:36.816689+05:30" dependencies: - condition: vector.enabled name: vector @@ -529,7 +554,7 @@ entries: version: 1.7.3 - apiVersion: v2 appVersion: v1.7.2 - created: "2026-05-29T10:50:21.093884+05:30" + created: "2026-06-02T15:41:36.81465+05:30" dependencies: - condition: vector.enabled name: vector @@ -553,7 +578,7 @@ entries: version: 1.7.2 - apiVersion: v2 appVersion: v1.7.1 - created: "2026-05-29T10:50:21.091335+05:30" + created: "2026-06-02T15:41:36.813018+05:30" dependencies: - condition: vector.enabled name: vector @@ -577,7 +602,7 @@ entries: version: 1.7.1 - apiVersion: v2 appVersion: v1.7.0 - created: "2026-05-29T10:50:21.08838+05:30" + created: "2026-06-02T15:41:36.81103+05:30" dependencies: - condition: vector.enabled name: vector @@ -600,7 +625,7 @@ entries: version: 1.7.0 - apiVersion: v2 appVersion: v1.6.3 - created: "2026-05-29T10:50:21.085825+05:30" + created: "2026-06-02T15:41:36.809456+05:30" dependencies: - condition: vector.enabled name: vector @@ -623,7 +648,7 @@ entries: version: 1.6.8 - apiVersion: v2 appVersion: v1.6.3 - created: "2026-05-29T10:50:21.083235+05:30" + created: "2026-06-02T15:41:36.807551+05:30" dependencies: - condition: vector.enabled name: vector @@ -646,7 +671,7 @@ entries: version: 1.6.7 - apiVersion: v2 appVersion: v1.6.3 - created: "2026-05-29T10:50:21.08027+05:30" + created: "2026-06-02T15:41:36.805976+05:30" dependencies: - condition: vector.enabled name: vector @@ -669,7 +694,7 @@ entries: version: 1.6.6 - apiVersion: v2 appVersion: v1.6.3 - created: "2026-05-29T10:50:21.077854+05:30" + created: "2026-06-02T15:41:36.804415+05:30" dependencies: - condition: vector.enabled name: vector @@ -692,7 +717,7 @@ entries: version: 1.6.5 - apiVersion: v2 appVersion: v1.6.3 - created: "2026-05-29T10:50:21.074994+05:30" + created: "2026-06-02T15:41:36.802459+05:30" dependencies: - condition: vector.enabled name: vector @@ -715,7 +740,7 @@ entries: version: 1.6.4 - apiVersion: v2 appVersion: v1.6.3 - created: "2026-05-29T10:50:21.07247+05:30" + created: "2026-06-02T15:41:36.80092+05:30" dependencies: - condition: vector.enabled name: vector @@ -738,7 +763,7 @@ entries: version: 1.6.3 - apiVersion: v2 appVersion: v1.6.2 - created: "2026-05-29T10:50:21.070036+05:30" + created: "2026-06-02T15:41:36.799072+05:30" dependencies: - condition: vector.enabled name: vector @@ -761,7 +786,7 @@ entries: version: 1.6.2 - apiVersion: v2 appVersion: v1.6.1 - created: "2026-05-29T10:50:21.067103+05:30" + created: "2026-06-02T15:41:36.797603+05:30" dependencies: - condition: vector.enabled name: vector @@ -784,7 +809,7 @@ entries: version: 1.6.1 - apiVersion: v2 appVersion: v1.6.0 - created: "2026-05-29T10:50:21.0647+05:30" + created: "2026-06-02T15:41:36.796123+05:30" dependencies: - condition: vector.enabled name: vector @@ -807,7 +832,7 @@ entries: version: 1.6.0 - apiVersion: v2 appVersion: v1.5.5 - created: "2026-05-29T10:50:21.061979+05:30" + created: "2026-06-02T15:41:36.79425+05:30" dependencies: - condition: vector.enabled name: vector @@ -830,7 +855,7 @@ entries: version: 1.5.5 - apiVersion: v2 appVersion: v1.5.4 - created: "2026-05-29T10:50:21.059651+05:30" + created: "2026-06-02T15:41:36.79269+05:30" dependencies: - condition: vector.enabled name: vector @@ -853,7 +878,7 @@ entries: version: 1.5.4 - apiVersion: v2 appVersion: v1.5.3 - created: "2026-05-29T10:50:21.057272+05:30" + created: "2026-06-02T15:41:36.790796+05:30" dependencies: - condition: vector.enabled name: vector @@ -876,7 +901,7 @@ entries: version: 1.5.3 - apiVersion: v2 appVersion: v1.5.2 - created: "2026-05-29T10:50:21.054496+05:30" + created: "2026-06-02T15:41:36.789243+05:30" dependencies: - condition: vector.enabled name: vector @@ -899,7 +924,7 @@ entries: version: 1.5.2 - apiVersion: v2 appVersion: v1.5.1 - created: "2026-05-29T10:50:21.052172+05:30" + created: "2026-06-02T15:41:36.787684+05:30" dependencies: - condition: vector.enabled name: vector @@ -922,7 +947,7 @@ entries: version: 1.5.1 - apiVersion: v2 appVersion: v1.5.0 - created: "2026-05-29T10:50:21.049735+05:30" + created: "2026-06-02T15:41:36.785713+05:30" dependencies: - condition: vector.enabled name: vector @@ -945,7 +970,7 @@ entries: version: 1.5.0 - apiVersion: v2 appVersion: v1.4.0 - created: "2026-05-29T10:50:21.046779+05:30" + created: "2026-06-02T15:41:36.784211+05:30" dependencies: - condition: vector.enabled name: vector @@ -968,7 +993,7 @@ entries: version: 1.4.1 - apiVersion: v2 appVersion: v1.4.0 - created: "2026-05-29T10:50:21.044559+05:30" + created: "2026-06-02T15:41:36.782454+05:30" dependencies: - condition: vector.enabled name: vector @@ -991,7 +1016,7 @@ entries: version: 1.4.0 - apiVersion: v2 appVersion: v1.3.0 - created: "2026-05-29T10:50:21.041954+05:30" + created: "2026-06-02T15:41:36.781025+05:30" dependencies: - condition: vector.enabled name: vector @@ -1014,7 +1039,7 @@ entries: version: 1.3.1 - apiVersion: v2 appVersion: v1.3.0 - created: "2026-05-29T10:50:21.039714+05:30" + created: "2026-06-02T15:41:36.779589+05:30" dependencies: - condition: vector.enabled name: vector @@ -1037,7 +1062,7 @@ entries: version: 1.3.0 - apiVersion: v2 appVersion: v1.2.0 - created: "2026-05-29T10:50:21.037427+05:30" + created: "2026-06-02T15:41:36.777815+05:30" dependencies: - condition: vector.enabled name: vector @@ -1060,7 +1085,7 @@ entries: version: 1.2.0 - apiVersion: v2 appVersion: v1.1.0 - created: "2026-05-29T10:50:21.034761+05:30" + created: "2026-06-02T15:41:36.77639+05:30" dependencies: - condition: vector.enabled name: vector @@ -1083,7 +1108,7 @@ entries: version: 1.1.0 - apiVersion: v2 appVersion: v1.0.0 - created: "2026-05-29T10:50:21.032552+05:30" + created: "2026-06-02T15:41:36.774935+05:30" dependencies: - condition: vector.enabled name: vector @@ -1106,7 +1131,7 @@ entries: version: 1.0.0 - apiVersion: v2 appVersion: v0.9.0 - created: "2026-05-29T10:50:21.030326+05:30" + created: "2026-06-02T15:41:36.773203+05:30" dependencies: - condition: vector.enabled name: vector @@ -1129,7 +1154,7 @@ entries: version: 0.9.0 - apiVersion: v2 appVersion: v0.8.1 - created: "2026-05-29T10:50:21.027637+05:30" + created: "2026-06-02T15:41:36.771757+05:30" dependencies: - condition: vector.enabled name: vector @@ -1152,7 +1177,7 @@ entries: version: 0.8.1 - apiVersion: v2 appVersion: v0.8.0 - created: "2026-05-29T10:50:21.02548+05:30" + created: "2026-06-02T15:41:36.7698+05:30" dependencies: - condition: vector.enabled name: vector @@ -1175,7 +1200,7 @@ entries: version: 0.8.0 - apiVersion: v2 appVersion: v0.7.3 - created: "2026-05-29T10:50:21.023301+05:30" + created: "2026-06-02T15:41:36.768113+05:30" dependencies: - condition: vector.enabled name: vector @@ -1198,7 +1223,7 @@ entries: version: 0.7.3 - apiVersion: v2 appVersion: v0.7.2 - created: "2026-05-29T10:50:21.020734+05:30" + created: "2026-06-02T15:41:36.766742+05:30" dependencies: - condition: vector.enabled name: vector @@ -1221,7 +1246,7 @@ entries: version: 0.7.2 - apiVersion: v2 appVersion: v0.7.1 - created: "2026-05-29T10:50:21.018558+05:30" + created: "2026-06-02T15:41:36.764974+05:30" dependencies: - condition: vector.enabled name: vector @@ -1244,7 +1269,7 @@ entries: version: 0.7.1 - apiVersion: v2 appVersion: v0.7.0 - created: "2026-05-29T10:50:21.016344+05:30" + created: "2026-06-02T15:41:36.763574+05:30" dependencies: - condition: vector.enabled name: vector @@ -1267,7 +1292,7 @@ entries: version: 0.7.0 - apiVersion: v2 appVersion: v0.6.2 - created: "2026-05-29T10:50:21.013676+05:30" + created: "2026-06-02T15:41:36.762149+05:30" dependencies: - condition: vector.enabled name: vector @@ -1290,7 +1315,7 @@ entries: version: 0.6.2 - apiVersion: v2 appVersion: v0.6.1 - created: "2026-05-29T10:50:21.011481+05:30" + created: "2026-06-02T15:41:36.76023+05:30" dependencies: - condition: vector.enabled name: vector @@ -1313,7 +1338,7 @@ entries: version: 0.6.1 - apiVersion: v2 appVersion: v0.6.0 - created: "2026-05-29T10:50:21.00925+05:30" + created: "2026-06-02T15:41:36.758801+05:30" dependencies: - condition: vector.enabled name: vector @@ -1336,7 +1361,7 @@ entries: version: 0.6.0 - apiVersion: v2 appVersion: v0.5.1 - created: "2026-05-29T10:50:21.006637+05:30" + created: "2026-06-02T15:41:36.757287+05:30" dependencies: - condition: vector.enabled name: vector @@ -1359,7 +1384,7 @@ entries: version: 0.5.1 - apiVersion: v2 appVersion: v0.5.0 - created: "2026-05-29T10:50:21.004408+05:30" + created: "2026-06-02T15:41:36.755522+05:30" dependencies: - condition: vector.enabled name: vector @@ -1382,7 +1407,7 @@ entries: version: 0.5.0 - apiVersion: v2 appVersion: v0.4.4 - created: "2026-05-29T10:50:21.001669+05:30" + created: "2026-06-02T15:41:36.754126+05:30" dependencies: - condition: vector.enabled name: vector @@ -1405,7 +1430,7 @@ entries: version: 0.4.5 - apiVersion: v2 appVersion: v0.4.3 - created: "2026-05-29T10:50:20.999445+05:30" + created: "2026-06-02T15:41:36.752471+05:30" dependencies: - condition: vector.enabled name: vector @@ -1428,7 +1453,7 @@ entries: version: 0.4.4 - apiVersion: v2 appVersion: v0.4.2 - created: "2026-05-29T10:50:20.997323+05:30" + created: "2026-06-02T15:41:36.751115+05:30" dependencies: - condition: vector.enabled name: vector @@ -1451,7 +1476,7 @@ entries: version: 0.4.3 - apiVersion: v2 appVersion: v0.4.1 - created: "2026-05-29T10:50:20.994806+05:30" + created: "2026-06-02T15:41:36.74973+05:30" dependencies: - condition: vector.enabled name: vector @@ -1474,7 +1499,7 @@ entries: version: 0.4.2 - apiVersion: v2 appVersion: v0.4.0 - created: "2026-05-29T10:50:20.992637+05:30" + created: "2026-06-02T15:41:36.747936+05:30" dependencies: - condition: vector.enabled name: vector @@ -1497,7 +1522,7 @@ entries: version: 0.4.1 - apiVersion: v2 appVersion: v0.4.0 - created: "2026-05-29T10:50:20.99043+05:30" + created: "2026-06-02T15:41:36.746576+05:30" dependencies: - condition: vector.enabled name: vector @@ -1520,7 +1545,7 @@ entries: version: 0.4.0 - apiVersion: v2 appVersion: v0.3.1 - created: "2026-05-29T10:50:20.987625+05:30" + created: "2026-06-02T15:41:36.745115+05:30" dependencies: - condition: vector.enabled name: vector @@ -1543,7 +1568,7 @@ entries: version: 0.3.1 - apiVersion: v2 appVersion: v0.3.0 - created: "2026-05-29T10:50:20.985352+05:30" + created: "2026-06-02T15:41:36.743223+05:30" description: Helm chart for Parseable Server digest: ff30739229b727dc637f62fd4481c886a6080ce4556bae10cafe7642ddcfd937 name: parseable @@ -1553,7 +1578,7 @@ entries: version: 0.3.0 - apiVersion: v2 appVersion: v0.2.2 - created: "2026-05-29T10:50:20.985135+05:30" + created: "2026-06-02T15:41:36.743078+05:30" description: Helm chart for Parseable Server digest: 477d0dc2f0c07d4f4c32e105d4bdd70c71113add5c2a75ac5f1cb42aa0276db7 name: parseable @@ -1563,7 +1588,7 @@ entries: version: 0.2.2 - apiVersion: v2 appVersion: v0.2.1 - created: "2026-05-29T10:50:20.984922+05:30" + created: "2026-06-02T15:41:36.742943+05:30" description: Helm chart for Parseable Server digest: 84826fcd1b4c579f301569f43b0309c07e8082bad76f5cdd25f86e86ca2e8192 name: parseable @@ -1573,7 +1598,7 @@ entries: version: 0.2.1 - apiVersion: v2 appVersion: v0.2.0 - created: "2026-05-29T10:50:20.984734+05:30" + created: "2026-06-02T15:41:36.742815+05:30" description: Helm chart for Parseable Server digest: 7a759f7f9809f3935cba685e904c021a0b645f217f4e45b9be185900c467edff name: parseable @@ -1583,7 +1608,7 @@ entries: version: 0.2.0 - apiVersion: v2 appVersion: v0.1.1 - created: "2026-05-29T10:50:20.984537+05:30" + created: "2026-06-02T15:41:36.742698+05:30" description: Helm chart for Parseable Server digest: 37993cf392f662ec7b1fbfc9a2ba00ec906d98723e38f3c91ff1daca97c3d0b3 name: parseable @@ -1593,7 +1618,7 @@ entries: version: 0.1.1 - apiVersion: v2 appVersion: v0.1.0 - created: "2026-05-29T10:50:20.984321+05:30" + created: "2026-06-02T15:41:36.742578+05:30" description: Helm chart for Parseable Server digest: 1d580d072af8d6b1ebcbfee31c2e16c907d08db754780f913b5f0032b403789b name: parseable @@ -1603,7 +1628,7 @@ entries: version: 0.1.0 - apiVersion: v2 appVersion: v0.0.8 - created: "2026-05-29T10:50:20.984078+05:30" + created: "2026-06-02T15:41:36.742451+05:30" description: Helm chart for Parseable Server digest: c805254ffa634f96ecec448bcfff9973339aa9487dd8199b21b17b79a4de9345 name: parseable @@ -1613,7 +1638,7 @@ entries: version: 0.0.8 - apiVersion: v2 appVersion: v0.0.7 - created: "2026-05-29T10:50:20.983315+05:30" + created: "2026-06-02T15:41:36.742335+05:30" description: Helm chart for Parseable Server digest: c591f617ed1fe820bb2c72a4c976a78126f1d1095d552daa07c4700f46c4708a name: parseable @@ -1623,7 +1648,7 @@ entries: version: 0.0.7 - apiVersion: v2 appVersion: v0.0.6 - created: "2026-05-29T10:50:20.983133+05:30" + created: "2026-06-02T15:41:36.742206+05:30" description: Helm chart for Parseable Server digest: f9ae56a6fcd6a59e7bee0436200ddbedeb74ade6073deb435b8fcbaf08dda795 name: parseable @@ -1633,7 +1658,7 @@ entries: version: 0.0.6 - apiVersion: v2 appVersion: v0.0.5 - created: "2026-05-29T10:50:20.982895+05:30" + created: "2026-06-02T15:41:36.742088+05:30" description: Helm chart for Parseable Server digest: 4d6b08a064fba36e16feeb820b77e1e8e60fb6de48dbf7ec8410d03d10c26ad0 name: parseable @@ -1643,7 +1668,7 @@ entries: version: 0.0.5 - apiVersion: v2 appVersion: v0.0.2 - created: "2026-05-29T10:50:20.982689+05:30" + created: "2026-06-02T15:41:36.741969+05:30" description: Helm chart for Parseable Server digest: 38a0a3e4c498afbbcc76ebfcb9cb598fa2ca843a53cc93b3cb4f135b85c10844 name: parseable @@ -1653,7 +1678,7 @@ entries: version: 0.0.2 - apiVersion: v2 appVersion: v0.0.1 - created: "2026-05-29T10:50:20.982501+05:30" + created: "2026-06-02T15:41:36.74185+05:30" description: Helm chart for Parseable Server digest: 1f1142db092b9620ee38bb2294ccbb1c17f807b33bf56da43816af7fe89f301e name: parseable @@ -1662,9 +1687,34 @@ entries: - https://charts.parseable.com/helm-releases/parseable-0.0.1.tgz version: 0.0.1 parseable-enterprise: + - apiVersion: v2 + appVersion: v2.8.1 + created: "2026-06-02T15:41:36.866498+05:30" + dependencies: + - condition: vector.enabled + name: vector + repository: https://helm.vector.dev + version: 0.20.1 + - condition: fluent-bit.enabled + name: fluent-bit + repository: https://fluent.github.io/helm-charts + version: 0.48.0 + description: Helm chart for Parseable Enterprise version - Needs a license to + run. Please contact sales@parseable.com for more details. + digest: 6387485ebb5fd14ec60004f4f80dbeafe83583590db2544a3f789597ec381d46 + icon: https://raw.githubusercontent.com/parseablehq/.github/main/images/logo.svg + maintainers: + - email: hi@parseable.com + name: Parseable Team + url: https://parseable.com + name: parseable-enterprise + type: application + urls: + - https://charts.parseable.com/helm-releases/parseable-enterprise-2.8.1.tgz + version: 2.8.1 - apiVersion: v2 appVersion: v2.8.0 - created: "2026-05-29T10:50:21.164816+05:30" + created: "2026-06-02T15:41:36.864707+05:30" dependencies: - condition: vector.enabled name: vector @@ -1676,7 +1726,7 @@ entries: version: 0.48.0 description: Helm chart for Parseable Enterprise version - Needs a license to run. Please contact sales@parseable.com for more details. - digest: 3d22ca3d869b257ddf42ee239abf2d1c8b029ab2715233e7401e56d7b5a85002 + digest: 3caa6c78936f31b1c49220645311f32ca29e322dae3396c841a6f2e17e476e55 icon: https://raw.githubusercontent.com/parseablehq/.github/main/images/new-logo.svg maintainers: - email: hi@parseable.com @@ -1687,9 +1737,34 @@ entries: urls: - https://charts.parseable.com/helm-releases/parseable-enterprise-2.8.0.tgz version: 2.8.0 + - apiVersion: v2 + appVersion: v2.7.3 + created: "2026-06-02T15:41:36.862583+05:30" + dependencies: + - condition: vector.enabled + name: vector + repository: https://helm.vector.dev + version: 0.20.1 + - condition: fluent-bit.enabled + name: fluent-bit + repository: https://fluent.github.io/helm-charts + version: 0.48.0 + description: Helm chart for Parseable Enterprise version - Needs a license to + run. Please contact sales@parseable.com for more details. + digest: 8cc36a35779d4e8128ddc54ed76a7143a99b40e67984347daa239f7cebb75495 + icon: https://raw.githubusercontent.com/parseablehq/.github/main/images/new-logo.svg + maintainers: + - email: hi@parseable.com + name: Parseable Team + url: https://parseable.com + name: parseable-enterprise + type: application + urls: + - https://charts.parseable.com/helm-releases/parseable-enterprise-2.7.3.tgz + version: 2.7.3 - apiVersion: v2 appVersion: v2.7.2 - created: "2026-05-29T10:50:21.161875+05:30" + created: "2026-06-02T15:41:36.860815+05:30" dependencies: - condition: vector.enabled name: vector @@ -1714,7 +1789,7 @@ entries: version: 2.7.2 - apiVersion: v2 appVersion: v2.7.1 - created: "2026-05-29T10:50:21.159052+05:30" + created: "2026-06-02T15:41:36.859+05:30" dependencies: - condition: vector.enabled name: vector @@ -1739,7 +1814,7 @@ entries: version: 2.7.1 - apiVersion: v2 appVersion: v2.6.6 - created: "2026-05-29T10:50:21.155682+05:30" + created: "2026-06-02T15:41:36.856853+05:30" dependencies: - condition: vector.enabled name: vector @@ -1764,7 +1839,7 @@ entries: version: 2.6.7 - apiVersion: v2 appVersion: v2.6.6 - created: "2026-05-29T10:50:21.152886+05:30" + created: "2026-06-02T15:41:36.85507+05:30" dependencies: - condition: vector.enabled name: vector @@ -1787,4 +1862,4 @@ entries: urls: - https://charts.parseable.com/helm-releases/parseable-enterprise-2.6.6.tgz version: 2.6.6 -generated: "2026-05-29T10:50:20.981719+05:30" +generated: "2026-06-02T15:41:36.74136+05:30" diff --git a/src/cli.rs b/src/cli.rs index f380a02c4..bd9e3c957 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -40,9 +40,9 @@ pub const DATASET_FIELD_COUNT_LIMIT: usize = 1000; #[command( name = "parseable", bin_name = "parseable", - about = "Cloud Native, log analytics platform for modern applications.", + about = "The open source observability data lake", long_about = r#" -Cloud Native, log analytics platform for modern applications. +Open source observability data lake — logs, metrics, and traces in a single binary. Usage: parseable [command] [options..] @@ -57,9 +57,11 @@ parseable [command] --help version = env!("CARGO_PKG_VERSION"), propagate_version = true, next_line_help = false, - help_template = r#"{name} v{version} -{about} -Join the community at https://logg.ing/community. + help_template = r#" + +{about}. Join the community at https://logg.ing/community. + +Version: v{version} {all-args} "#, From 5684bee34a339027338152fd2f015d16666ee228 Mon Sep 17 00:00:00 2001 From: Nitish Tiwari Date: Tue, 2 Jun 2026 17:17:35 +0530 Subject: [PATCH 17/47] chore: readme cleanup (#1664) --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 82e769e92..1b2f782e5 100644 --- a/README.md +++ b/README.md @@ -23,15 +23,15 @@ Parseable is an open source, columnar data lake platform - purpose built for obs ## Why Parseable? -Purpose built for observability and designed around proven data lake engineering patterns, Parseable gives you everything you need to make sense of your telemetry data, right out of the box, with no external dependencies or stitching together of multiple tools. +Purpose built for observability and designed around proven data engineering patterns, Parseable gives you everything you need to make sense of your telemetry data, right out of the box. Some of the key highlights include: -- [Data lake architecture](https://www.parseable.com/docs/architecture): Parseable Data lake architecture allows running stateless compute with object storage as the backing storage. This allows scaling storage and compute independently, and avoids the pitfalls of traditional observability systems. +- [Data lake architecture](https://www.parseable.com/docs/architecture): Stateless compute over object storage as the backing store. Storage and compute scale independently, so you're not paying for one to grow the other. -- [Fully featured](https://www.parseable.com/docs/features): Parseable is feature complete with alerting, dashboards, anomaly detection, APM, and more. You can do all of this and more from a single binary, without needing to stitch together multiple tools. +- [Fully featured](https://www.parseable.com/docs/features): Parseable is feature complete with alerting, dashboards, anomaly detection, APM, and more, all from a single binary without stitching together multiple tools. -- [Agent ready](https://www.parseable.com/docs/integrations#ai-agents--llms): Whether you need to observe your AI agents or use LLMs to analyze your telemetry data, Parseable has you covered with native support for AI agents and LLMs. +- [Agent ready](https://www.parseable.com/docs/integrations#ai-agents--llms): Whether you need to observe your AI agents or use LLMs to analyze your telemetry data, Parseable supports both natively. - [OpenTelemetry native](https://www.parseable.com/docs/ingest-data/otel): With native OTel support, you can send telemetry data to Parseable without any custom modifications or plugins. Parseable can be used as a drop-in replacement for your existing OpenTelemetry Collector setup. @@ -51,7 +51,7 @@ powershell -c "irm https://logg.ing/install-windows | iex" ```
-Once you have Parseable running, ingest data with the below command. This will send logs to the `demo` stream. You can see the logs in the dashboard. +Once you have Parseable running, ingest data with the command below. This will send logs to the `demo` stream. You can see the logs in the dashboard. ```bash curl --location --request POST 'http://localhost:8000/api/v1/ingest' \ @@ -67,9 +67,9 @@ curl --location --request POST 'http://localhost:8000/api/v1/ingest' \ ]' ``` -Access the UI at http://localhost:8000. You can login to the dashboard default credentials `admin`, `admin`. +Access the UI at http://localhost:8000. Log in with the default credentials `admin` / `admin`. -For production deployments, refer the [installation guide ↗︎](https://www.parseable.com/docs/self-hosted/installation) for best practices and hardening tips. +For production deployments, refer to the [installation guide ↗︎](https://www.parseable.com/docs/self-hosted/installation) for best practices and hardening tips. > [!TIP] > Try out the [Parseable cloud](https://app.parseable.com) — 14 days free trial, no credit card required. From 0a594b014b52316c932855eb60341b387d282419 Mon Sep 17 00:00:00 2001 From: parmesant Date: Tue, 2 Jun 2026 23:51:44 -0700 Subject: [PATCH 18/47] fix: commit schema bug (#1666) * fix: commit schema bug * fix: flush pending batches to disk, schema mutex pending batches by default get pushed to disk near instantly schema writer is behind a mutex to prevent incorrect schemas * fix: deepsource and coderabbit --- src/event/mod.rs | 21 ++++---- src/parseable/staging/mod.rs | 2 + src/parseable/staging/writer.rs | 33 +++++++++++-- src/parseable/streams.rs | 86 +++++++++++++++++++++------------ 4 files changed, 98 insertions(+), 44 deletions(-) diff --git a/src/event/mod.rs b/src/event/mod.rs index 4820e854c..4baa8f8c7 100644 --- a/src/event/mod.rs +++ b/src/event/mod.rs @@ -86,19 +86,22 @@ impl Event { } } + let stream = PARSEABLE.get_or_create_stream(&self.stream_name, &self.tenant_id); + if self.is_first_event { commit_schema(&self.stream_name, self.rb.schema(), &self.tenant_id)?; + if !stream.get_static_schema_flag() { + stream.stage_schema_file(stream.get_schema().as_ref().clone())?; + } } - PARSEABLE - .get_or_create_stream(&self.stream_name, &self.tenant_id) - .push( - &key, - &self.rb, - self.parsed_timestamp, - &self.custom_partition_values, - self.stream_type, - )?; + stream.push( + &key, + &self.rb, + self.parsed_timestamp, + &self.custom_partition_values, + self.stream_type, + )?; let tenant = self.tenant_id.as_deref().unwrap_or(DEFAULT_TENANT); update_stats( diff --git a/src/parseable/staging/mod.rs b/src/parseable/staging/mod.rs index 5d1a3a071..823f95c45 100644 --- a/src/parseable/staging/mod.rs +++ b/src/parseable/staging/mod.rs @@ -40,4 +40,6 @@ pub enum StagingError { TenantNotFound(#[from] TenantNotFound), #[error("{0}")] PoisonError(#[from] PoisonError), + #[error("JSON Error {0}")] + Json(#[from] serde_json::Error), } diff --git a/src/parseable/staging/writer.rs b/src/parseable/staging/writer.rs index a3fe96096..8e8ee63ac 100644 --- a/src/parseable/staging/writer.rs +++ b/src/parseable/staging/writer.rs @@ -29,8 +29,9 @@ use arrow_array::RecordBatch; use arrow_ipc::writer::StreamWriter; use arrow_schema::Schema; use arrow_select::concat::concat_batches; -use chrono::Utc; +use chrono::{TimeDelta, Utc}; use itertools::Itertools; +use once_cell::sync::Lazy; use rand::distributions::{Alphanumeric, DistString}; use tracing::error; @@ -41,7 +42,16 @@ use crate::{ use super::StagingError; -const DISK_WRITE_BATCH_ROWS: usize = 32_768; +const DISK_WRITE_BATCH_MAX_AGE_SECS_VAR: &str = "DISK_WRITE_BATCH_MAX_AGE_SECS"; +static DISK_WRITE_BATCH_MAX_AGE_SECS: Lazy = Lazy::new(|| { + if let Ok(var) = std::env::var(DISK_WRITE_BATCH_MAX_AGE_SECS_VAR) + && let Ok(var) = var.parse::() + { + var + } else { + 1 + } +}); #[derive(Default)] pub struct Writer { @@ -58,13 +68,18 @@ impl Writer { rb: &RecordBatch, file_path: PathBuf, range: TimeRange, + batch_rows: usize, ) -> Result<(), StagingError> { + let now = Utc::now(); let pending = self.disk_pending.entry(filename.clone()).or_default(); pending.rows += rb.num_rows(); pending.range.get_or_insert(range); + pending.first_seen.get_or_insert(now); pending.batches.push(rb.clone()); - if pending.rows >= DISK_WRITE_BATCH_ROWS { + let should_flush = pending.rows >= batch_rows.max(1) + || pending.is_older_than(now, TimeDelta::seconds(*DISK_WRITE_BATCH_MAX_AGE_SECS)); + if should_flush { self.flush_pending_disk(&filename, file_path)?; } @@ -104,8 +119,12 @@ impl Writer { let mut flushable_pending = HashMap::new(); let old_pending = std::mem::take(&mut self.disk_pending); + let now = Utc::now(); for (filename, pending) in old_pending { - if !forced && pending.is_current() { + if !forced + && pending.is_current() + && !pending.is_older_than(now, TimeDelta::seconds(*DISK_WRITE_BATCH_MAX_AGE_SECS)) + { self.disk_pending.insert(filename, pending); } else { flushable_pending.insert(filename, pending); @@ -164,6 +183,7 @@ struct PendingDiskBatch { rows: usize, batches: Vec, range: Option, + first_seen: Option>, } impl PendingDiskBatch { @@ -172,6 +192,11 @@ impl PendingDiskBatch { .as_ref() .is_some_and(|range| range.contains(Utc::now())) } + + fn is_older_than(&self, now: chrono::DateTime, age: TimeDelta) -> bool { + self.first_seen + .is_some_and(|first_seen| now.signed_duration_since(first_seen) >= age) + } } pub struct DiskWriter { diff --git a/src/parseable/streams.rs b/src/parseable/streams.rs index cfb3f6b45..0c2b3eb3e 100644 --- a/src/parseable/streams.rs +++ b/src/parseable/streams.rs @@ -23,6 +23,7 @@ use arrow_schema::{Field, Fields, Schema}; use chrono::{NaiveDateTime, Timelike, Utc}; use derive_more::derive::{Deref, DerefMut}; use itertools::Itertools; +use once_cell::sync::Lazy; use parquet::{ arrow::ArrowWriter, basic::Encoding, @@ -69,6 +70,17 @@ use super::{ staging::{StagingError, reader::MergedReverseRecordReader, writer::Writer}, }; +const DISK_WRITE_BATCH_ROWS_VAR: &str = "DISK_WRITE_BATCH_ROWS"; +static DISK_WRITE_BATCH_ROWS: Lazy = Lazy::new(|| { + if let Ok(var) = std::env::var(DISK_WRITE_BATCH_ROWS_VAR) + && let Ok(var) = var.parse::() + { + var + } else { + 1 + } +}); + const INPROCESS_DIR_PREFIX: &str = "processing_"; const METRIC_ROW_GROUP_PREP_IN_FLIGHT: usize = 1; @@ -115,6 +127,7 @@ pub struct Stream { pub data_path: PathBuf, pub options: Arc, pub writer: Mutex, + schema_writer: Mutex<()>, pub ingestor_id: Option, } impl Stream { @@ -134,6 +147,7 @@ impl Stream { data_path, options, writer: Mutex::new(Writer::default()), + schema_writer: Mutex::new(()), ingestor_id, }) } @@ -181,7 +195,7 @@ impl Stream { ); let file_path = self.data_path.join(&filename); - guard.push_disk(filename, record, file_path, range)?; + guard.push_disk(filename, record, file_path, range, *DISK_WRITE_BATCH_ROWS)?; } guard.mem.push(schema_key, record)?; @@ -435,10 +449,8 @@ impl Stream { .collect() } - pub fn get_schemas_if_present(&self) -> Option> { - let Ok(dir) = self.data_path.read_dir() else { - return None; - }; + pub fn get_schemas_if_present(&self) -> Result, StagingError> { + let dir = self.data_path.read_dir()?; let mut schemas: Vec = Vec::new(); @@ -446,21 +458,13 @@ impl Stream { if let Some(ext) = file.path().extension() && ext.eq("schema") { - let file = File::open(file.path()).expect("Schema File should exist"); + let file = File::open(file.path())?; - let schema = match serde_json::from_reader(file) { - Ok(schema) => schema, - Err(_) => continue, - }; - schemas.push(schema); + schemas.push(serde_json::from_reader(file)?); } } - if !schemas.is_empty() { - Some(schemas) - } else { - None - } + Ok(schemas) } /// Converts arrow files in staging into parquet files, does so only for past minutes when run with `!shutdown_signal` @@ -497,28 +501,48 @@ impl Stream { // check if there is already a schema file in staging pertaining to this stream // if yes, then merge them and save - if let Some(mut schema) = schema { + if let Some(schema) = schema { let static_schema_flag = self.get_static_schema_flag(); if !static_schema_flag { - // schema is dynamic, read from staging and merge if present + self.stage_schema_file(schema)?; + } + } + + Ok(()) + } - // need to add something before .schema to make the file have an extension of type `schema` - let path = RelativePathBuf::from_iter([format!("{}.schema", self.stream_name)]) - .to_path(&self.data_path); + pub fn stage_schema_file(&self, mut schema: Schema) -> Result<(), StagingError> { + let _schema_writer = self.schema_writer.lock().map_err(|poisoned| { + StagingError::PoisonError(PoisonError::new(format!( + "Schema writer lock poisoned while staging schema for stream {} - {}", + self.stream_name, poisoned + ))) + })?; - let staging_schemas = self.get_schemas_if_present(); - if let Some(mut staging_schemas) = staging_schemas { - staging_schemas.push(schema); - schema = Schema::try_merge(staging_schemas)?; - } + // schema is dynamic, read from staging and merge if present + fs::create_dir_all(&self.data_path)?; - // save the merged schema on staging disk - // the path should be stream/.ingestor.{id}.schema - info!("writing schema to path - {path:?}"); - write(path, to_bytes(&schema))?; - } + // need to add something before .schema to make the file have an extension of type `schema` + let file_name = self.ingestor_id.as_ref().map_or_else( + || ".schema".to_owned(), + |id| format!(".ingestor.{id}.schema"), + ); + let path = RelativePathBuf::from_iter([file_name]).to_path(&self.data_path); + let tmp_path = path.with_extension("schema.tmp"); + + let staging_schemas = self.get_schemas_if_present()?; + if !staging_schemas.is_empty() { + let mut staging_schemas = staging_schemas; + staging_schemas.push(schema); + schema = Schema::try_merge(staging_schemas)?; } + // save the merged schema on staging disk + // the path should be stream/.ingestor.{id}.schema + info!("writing schema to path - {path:?}"); + write(&tmp_path, to_bytes(&schema))?; + fs::rename(tmp_path, path)?; + Ok(()) } From f6eb4f52f1cff19ea7dcc75b00bb76a69cedaec9 Mon Sep 17 00:00:00 2001 From: AdheipSingh <34169002+AdheipSingh@users.noreply.github.com> Date: Mon, 8 Jun 2026 10:10:20 +0530 Subject: [PATCH 19/47] helm: support tolerations (plural) and affinity in standalone deployment (#1669) --- helm/Chart.yaml | 2 +- helm/templates/standalone-deployment.yaml | 6 +++++- helm/values.yaml | 5 ++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/helm/Chart.yaml b/helm/Chart.yaml index a1747b4a4..9bff03983 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: parseable description: Helm chart for Parseable OSS version. Columnar data lake platform - purpose built for observability type: application -version: 2.8.1 +version: 2.8.2 appVersion: "v2.8.1" icon: "https://raw.githubusercontent.com/parseablehq/.github/main/images/logo.svg" diff --git a/helm/templates/standalone-deployment.yaml b/helm/templates/standalone-deployment.yaml index 20627bfe3..e009094c1 100644 --- a/helm/templates/standalone-deployment.yaml +++ b/helm/templates/standalone-deployment.yaml @@ -165,8 +165,12 @@ spec: nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} - {{- with .Values.parseable.toleration }} + {{- with .Values.parseable.tolerations }} tolerations: {{- toYaml . | nindent 8 }} {{- end }} + {{- with .Values.parseable.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} {{- end }} diff --git a/helm/values.yaml b/helm/values.yaml index ec07ccf57..7448d93a1 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -159,7 +159,8 @@ parseable: httpGet: path: /api/v1/readiness port: 8000 - toleration: [] + tolerations: [] + affinity: {} resources: limits: cpu: 500m @@ -212,11 +213,9 @@ parseable: fsGroupChangePolicy: "Always" nameOverride: "" fullnameOverride: "" - affinity: {} podLabels: app: parseable component: query - tolerations: [] ## Use this section to create ServiceMonitor object for ## this Parseable deployment. Read more on ServiceMonitor ## here: https://prometheus-operator.dev/docs/api-reference/api/#monitoring.coreos.com/v1.ServiceMonitor From d28b61919fbd2c24dad44bcbc02400355e0de274 Mon Sep 17 00:00:00 2001 From: Nitish Tiwari Date: Mon, 8 Jun 2026 10:45:51 +0530 Subject: [PATCH 20/47] chore: update to new helm chart with tolerations fix (#1670) --- Cargo.lock | 224 ++++++++++++++++++- helm-releases/parseable-2.8.2.tgz | Bin 0 -> 52670 bytes helm-releases/parseable-enterprise-2.8.1.tgz | Bin 57254 -> 57255 bytes helm/Chart.yaml | 2 +- index.yaml | 203 +++++++++-------- 5 files changed, 336 insertions(+), 93 deletions(-) create mode 100644 helm-releases/parseable-2.8.2.tgz diff --git a/Cargo.lock b/Cargo.lock index 1c518e09d..d038a589f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -295,7 +295,7 @@ dependencies = [ "futures-lite", "pin-project", "prometheus", - "quanta", + "quanta 0.10.1", "thiserror 1.0.69", ] @@ -734,6 +734,12 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "ascii" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" + [[package]] name = "async-compression" version = "0.4.42" @@ -1128,6 +1134,12 @@ dependencies = [ "phf", ] +[[package]] +name = "chunked_transfer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" + [[package]] name = "clap" version = "4.5.53" @@ -1198,7 +1210,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b03b7db8e0b4b2fdad6c551e634134e99ec000e5c8c3b6856c65e8bbaded7a3b" dependencies = [ "unicode-segmentation", - "unicode-width", + "unicode-width 0.2.2", ] [[package]] @@ -1355,6 +1367,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -2340,6 +2361,27 @@ dependencies = [ "crypto-common 0.2.1", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -2375,6 +2417,12 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -2739,6 +2787,16 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hdrhistogram" +version = "7.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" +dependencies = [ + "byteorder", + "num-traits", +] + [[package]] name = "heck" version = "0.5.0" @@ -2774,6 +2832,59 @@ dependencies = [ "windows-link", ] +[[package]] +name = "hotpath" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff002d5c53fa1c6891f32156b9451d16654bc8a761894d7660b25c0a332d517" +dependencies = [ + "arc-swap", + "cfg-if", + "crossbeam-channel", + "flate2", + "futures-util", + "hdrhistogram", + "hotpath-macros", + "hotpath-meta", + "libc", + "object 0.36.7", + "pin-project-lite", + "prettytable-rs", + "quanta 0.12.6", + "regex", + "rustc-demangle", + "serde", + "serde_json", + "tiny_http", + "tokio", +] + +[[package]] +name = "hotpath-macros" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e2f4ac4534511584b7082657e133dcf3d8727b2f456a6b2a2c3eb02b82c1277" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "hotpath-macros-meta" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a87070853e9402ec79184f8d8d930d7eb86cd274aecdcf973f73b6f40271b0" + +[[package]] +name = "hotpath-meta" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31cfef2b9d280ad754c23b40b50cc74676489597f3cebfe0c180389e08a53ed" +dependencies = [ + "hotpath-macros-meta", +] + [[package]] name = "http" version = "0.2.12" @@ -3141,6 +3252,17 @@ dependencies = [ "serde", ] +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -3304,6 +3426,15 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "libc", +] + [[package]] name = "libz-sys" version = "1.1.23" @@ -3586,6 +3717,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + [[package]] name = "object" version = "0.37.3" @@ -3955,6 +4095,7 @@ dependencies = [ "futures-util", "hex", "hostname", + "hotpath", "http 1.4.0", "http-auth-basic", "human-size", @@ -4179,6 +4320,19 @@ dependencies = [ "syn", ] +[[package]] +name = "prettytable-rs" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eea25e07510aa6ab6547308ebe3c036016d162b8da920dbb079e3ba8acf3d95a" +dependencies = [ + "encode_unicode", + "is-terminal", + "lazy_static", + "term", + "unicode-width 0.1.14", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -4360,12 +4514,27 @@ dependencies = [ "libc", "mach", "once_cell", - "raw-cpuid", + "raw-cpuid 10.7.0", "wasi 0.10.2+wasi-snapshot-preview1", "web-sys", "winapi", ] +[[package]] +name = "quanta" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid 11.6.0", + "wasi 0.11.1+wasi-snapshot-preview1", + "web-sys", + "winapi", +] + [[package]] name = "quick-xml" version = "0.39.2" @@ -4546,6 +4715,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags 2.10.0", +] + [[package]] name = "rayon" version = "1.11.0" @@ -4629,6 +4807,17 @@ dependencies = [ "bitflags 2.10.0", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 1.0.69", +] + [[package]] name = "regex" version = "1.12.2" @@ -5358,6 +5547,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -5460,6 +5660,18 @@ dependencies = [ "crunchy", ] +[[package]] +name = "tiny_http" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82" +dependencies = [ + "ascii", + "chunked_transfer", + "httpdate", + "log", +] + [[package]] name = "tinystr" version = "0.8.2" @@ -5880,6 +6092,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "unicode-width" version = "0.2.2" diff --git a/helm-releases/parseable-2.8.2.tgz b/helm-releases/parseable-2.8.2.tgz new file mode 100644 index 0000000000000000000000000000000000000000..5b375d7f018073c11cdf539acd742495dd8828d0 GIT binary patch literal 52670 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 =0B|kqz?qc(EgaOA+YvUmhpex z8_W`3svY!QU)$gLZA~bT__;YXme+^zfE_$D<(@Xvk@?kP zjpcIxs!1QCe8VZu4a;I0`~Q5}5R&GwvoPka-HfZ9na(}4aoAuXP z@{prZ=v_l*@QgZAO`2s&Di`Ji=2;-YNAe2b{^ZRGmuh^Fv7*2R*VYBR-O5d2J;KEa zSuiDccD_rOJp!(goaHv{SyJ*5N-uLkRXvr#)jw;6I4G9;p#4_e0)aW7SOT^CQ8@ZIx@+s%J_v zKX?m|fA#Ft$MoG6jva7BAMMC(3-H`w!~-o`IarK2ykI*u(4ze#_w? z4=gwm!+`tFxde?XWaxQYY1i0=q)hR)Z8|5YHL7w&(tDN=om#t$D?z3;2wjC}VyNDc zqPXFq(SvY*Th2s!@HLrx0t=@Wo_6T65J3 zU^P@wqtePIkEWB$KgR3k>G+uzdXu`dTK=L4`b0UW*74o{-YJ83njB#%1^!6abV_Gn z*eg_KJsOH1T){ z0rehe9kl|OGB_4asjEaLXGWlFh{#TS04UO6l(WN!%+;hH62CmiboqWBBqWE)0SAA* z#YybJGec2Ycx$JsHq7eCEOyb*pb7>>BF3II|K&R5=8gFk-7lC6(~I!+41@yqSI~_U z{65`xk?G{k$1S?|ng9F^9SlE}u=>qMVGT?UA0Mx?u3b?V&jDttz#Yfr*6e5tPdr@$ zUVgoCLg0jdxddR8(dYwz(eXW8<_SJLP!1ro;o=93<*i0XMI!|-NRH)-+83c9H?!;h zMAM`<2Mvsdj&#n4V2LFSg+R1p%$8r81~}VFAW1WWS!ScqX)Gt-+bmvi8+2F6&G3Wr zi>nVOzMmk40#TU@6QP;qP_o&Tii-G2#3Xb^790M2F86`;a}_uQMsDDe61BU)ThgWL zV$*Nf_{as@a?^f^lGg7475~DT`j{(s(+j(l9c&__szRLkit5-;#mh_Nm#vn0iBJ*3 zV9*_6ze|m3x%M@)eLQ=sEdcGzA+)T3*lHiZ(82Gp8}M3>pziD0DAmyl{>rwMsE+2P zq^sF;*+Wemc-(k7_0m88*tbzv4cwyqj=}7BdyXHp=!AFLE)y!x>RoslEe@wKZq-dS ztv9W|agHSm%zX6$i>-k8S64{?%bnJcz%9)`+QY72=*z?By`iRfNpt4xK2*{!Vyv9Q z`hTEi0#s||gcF)YA!y)de^8|?&<~Ot@k4!!dc*}Wb-#e^DZB8EUm2Ike{Tjh?|j0xn@qgL z-NQrPA)eVH`lZ!0;XVg?^7{9L1v84NUl7pgA z}(3M3I%LEhAxt<-2c_M~dG8o*>g@KvTmExl759DNzoG3D#$ zP*CNB4-1?kJ;MgYHD2EQU2RxpJ7MmSF;zlNyjKn-;Q;IUjT^)F#b3MUELI41$S@MeAjRq9JOy!Q zdWtsH-c7Y0(O|QFo29yh(|x?@E+&z~?qbU-wZkmON;tu7Ku;I{jI+CHK{+eWgC~sb5>`YKJh>r2s>N#@__7_|{JSp6^yK`fzMeN*j_ee?!{ULj=b* zo|+4dUN0g%e!)*H(i+b@kbE&8Sa$ za?GsUR1W~{>A)D0fT7Uruh(M^#=yR`vJl{o|JDw!F>v1G-7Git(H`;BZVEcJ2Dp{^ z(bm;A2fR9ISn(5?{g-tMO6ivIEzmoG5vbigg2SD5SdpK2a|c)O>wPEu{M_?(1@7!@ z{O?fs^6?t+1$LqrwXnPMy({n9?w)baA<hR8Ak=#cpZ&j`E8}U zP#~qoQt&?WOC{hjZN5Nj87Sr(d(6~vR-yk&rw#o4(qsSH`Ti@N_~sJ*_>x+1z2CfD zX#6r~!E1Q{Y0m-gZ+z@))8+>fKD=i=`Z3O=qPgnWU9ngyY+MOeE&q*V6L~zWrr}LS zPhXQITXb|rUlR_5ajE5Wu3G(Zs{JefMjR`^U1Ib}H)_(~8pNU&9}r(z-}~@)bLRV3 zXxBu8uvpI$8aOaD?0{9{z`=p5JKM;sC+TpKZA#d-LnA%FfI%BV))_$9TQ_V(lVF~EFgS_y+5Y=QqKEh)n*lA zrslofXW#g*d^bFWi*XV0edQx5)UV~o;1dPzVTTXx=eN%18Q|Hc^YXj-W>o+(?A)X4 z46KA#+w)7`L-RHtt$^%@pC#WX`?|#mFo^%Q#tmrg3_Q@wrQbaiEhke?Kjac;i3*7H zFg<}YuoI8O&&auuiSXm04Uvi8gNu=O4K+@ljGXPGOAd(K>c651W$uSBh9D!$A%bN| zJd(`#(;p++O^U`QLLe(7K8Kf-{oT6=ajq_ua3@J0e<0sPjNHU<`@jILl}Ut(><3Jv za7j=>B8!sEphB2&sRpDhL^x%(I+ydPH7b^&C3pSXyOkC~?wNB1bNDg+fFH-&p-Szf z3x+Qz4)8P&5NIO#%CCx6h%K-6fWglHWrNs|iKJUqO*Mz^F`g#2RGUqR`NOmkTnrIu z3vZ($sfz@^8RD{aD0rND<6J#t93@HHhukOWUC}oSbwu9Pmr&VDXC#M~;}6ytUs!8Y zDdH2UiNAAptoesasOE!rnaeJl!E$A3EG`T~ESdFEXPh@Sxndl>hW(@$M)JN_+w(Qmrp{RFi|9moTaVQ8sp@I@$lPPyNwUH*+>+}0%!ctlf z1-V#FNOAY@<>oJ$$E_HyU(%48|6@v?{4RT^P4x>5-k1-CkE=hYkBq?;F5f?Ndw93s zd^~ym8M`9m3p{|P7pyzWbRhwL>uY|lra}Q#Yu~ra#Wr!!>;hUk^bQiuhm7W>Tx_Ho z=3Ktf2hrPk)+)G`$fuP-GU^5SCKB}Ex>y+s7;4n&eL*TzZ5}x|#>BAd#95*d72oeB z)(ddeyOo^N?kJaEf9{w^qE*DnKGBACS+b_2rH6d1OS9(pd3H7cX4;>uk%$S5NoOvS zKslwY`-D0#;d%_|5L4+C>R9}0ip4!8`Gw>kJk%)^PE2n>7m7@%Kyj5iQU0t8Z%n5B zEw;`N>W0!U@>n|5^h|Pw5y+)aocqwUXtIz2(eI|pN`FkF=T$#k#D*%*Gd?b81pWKO!8gNWVCpsdv-8CpF5S8vRaiI z5ez3TtdJyJT$ZT)v0SLL+f)MEeO_O7f3zkIfPSWyP%c$is7fgJv(Va6puaUp`pd!w z5S49PesUMxQ<3JbJ3^AE80#bAeN|=?$GrCnx>(sumhv^$?MawZ`l3FhC3G$Fy3iM! z+(wH~V)Z6pe3ivsgl?^I&Uzl4+>9L94^y#mI_l!xMlze6-#9=P)QKC!FEThw|5v0z zH=iY42fWP#-s=N>Ril<_d)oI_x!Vj@#U|w}vDC&~SQ%YeCkO?}(jaGyqHqg{7hk7B zu5oY(=VO1U)oB;r1`sQfWAq^qd_RwW%3q;*+a~JGU9}vL%8HrD<2=(Wj4n*|h|WBp z7;Jww&O7~u7-dffUe;WOct&m_>1vQPTc zPnG{+7os|CxGpU=jfDr=I466F=e5Aj;wX^iv>JB1C!Z5rz+9L;^6Yt~V}>fM(A=&s z?wZax*3^p!UO=8lY~^JZ(SR*sxW%?OS8Me0@?4E0Erf?~+QUnhcQkvd)-ot!v)1l@ zPROGp$YGAErfFnD#q5Z5RITWjTzxY_e$z^ayWhG5(t3~Q?A2lfRF+HeRs~}X=BH(r zgZNg3AS}mOQm1m8N(4+tsgYy0`Fh@rq2`~ed!xy`&xdpW7wMM=oVgCR^*yyeS_i(< z{Cw7SdlIS!mUD0RxWP5f$`a3v%+k<*GKS+5#4Q{d(HjV0!i<;l_RmBtEm(_IS`Jg|G# zq?12BDXF$RBWeSr;p;!}xt-!MWD5YL9Y05<|DD^oDh^<9n~q?q8obmb4bSJUfG=ub zmaZn7TT@os57%{*vvC|-9ewuPG~gO4(R%Y}g-KS?o$H29)f%8mI?&Tt|ADJ5+HzC*Et#kTL_9iv z2~!sue9gnzF+@AWR#|*n`Np9Hbt=S~t6SV!TO9v>wp)zUyKQFrL!W1L^n$xWanz!; zwa&OQ3Z)~^x~IYURUzT~nXMiprMz?P@BHfpbt+nd()#7t%#1ho(sl4U-Gs&#$uEsG z3HuuGo6eej>zUAV3Fut{1iBK>J71)C9EI#GU*iCQS6}KiZ!=x`{5~(tz`>l4)?LDJ zF}XxiNIMET6*&2AAcKxW;uvE(p9FM%7PhX8IhOTMarK>$in@1F1V_KYK=iI%1fn;8 zo;75L38EMxEc~q2x^E)9ACCYpUOxG_yA>>*m^~;sNP5d~cK1-oQ7>aRU-#;@5#bIq z?*-EB<_9rZiO9_X`JVaV!81bZ?vTTvKIR|T6QpJRc9bO3qer}{_H1a*QBUUMilm}& ziW!h9iE(l+gpN7t6HP&1;Q;ut^g)Y;ot@VW#lE8xcr!aESppjE^vpKb>MeK__+5l`iVHZF(I`*^tK88`?tFC5$F+^6h24<;ubPz3-~alUOMV# z41C1%)cqRi?oEVw$RTFjXPhBPv~TPj9QVYcfAI;jLPg>W*M?Rq<=h_X5w6{QUbU@`|w+_1In=UCitU6gzej z50M8!{yq~#9Pb|fn0`&fOM8L31*^sQV{s%lfHMjB1u}y;jhKrMWa8uE#t{(wxKEmH zD+{A!M9d{X)F=3AP~J9Hcf=KK91;p{%4Y;nXf|_8d#>0=Lz)JN{veSlr=?{770>x% zYK`WAM;UKCvA07T7I=rE32SBlmFY7NhaE+v;@h$j@Nkh44dTT=f!{o7@X10)g*oWl z>ofm)X=|Oi9g@LK|F^9A1sAhP&bv`)Y+>@iWGcM`x%5=*I3uq-8@FR9CWj@ZSc{sm z^nU`!3pn(}#VdJ1ntHjcHcx>yVM-aKZayibs*T~P>%$Sv27c+8TL|yc3Z2dzj(+<780OqsHsuvdtAc+L{PJ_Z zk^BL|SvspvnHB2%3ko3lXR8%Rp}0-ncAS`V>)1T+3_5!|dwac|{oel0+1}1z@6q7l z*ZrN{$B*{*{?h4oBwujZ8FV}#oo*+9(iwDqtg;CW-~{jR^gG=S)gSH*Iw4SK;B>B~ zo-PN2aUo>JP3PTB0N8uD|IO~k0KhfH_?1r(+tmcWEVgaq-_(Cq`8%(M-m73>^Eiw9B$OCO30RkkUo%cQMTOSVkq z;dC{&P7rQ?ngrA|>vm{`qYRR&r-)l%}*9Y`78Vt@EuVR;$JP}R~# z9Tj1(Lf-(kfJCOFiC?(~UL?R|UaWpv)!D5S=e%~fc0(_PN`sLAH?B||#$4))zXR=N z+*}At5}@JKE(6Kg0xJN77!o(*p|9vwi@AaI1V$cuN{k5 z&)3nO&`-ADXB*C15EFRy>P0QI6NHMy5W*rGu{&j-g^>g7W3U|q2xDML0OjkwulHis z|7Jwry+TCfNW1`JM%u|I;88b0l_Ts*&0C`C!2u1yBU z;1$Q{yQ)cNUXL2niR)#wpg2q|2$hs4H~ixZvEXpVg#4$^qmsk&ys1%c`|q};Sa2^& z{{8EMo8=SBRmRQV>>m)NeE|!>FS^Ni=?{9xO-vd+qJ{%%HtNHlgNVYU)u{RhD?d1$ zk36`(qf%Z?qGW`u&Tz^TR`Tr?;dzaa%-`_TPqxd1f?m(L<$CEFDmW_b?9Aw5EV-R& z?NCKK(~t=^RoY@y=~(i)ZN<6=jlsg4Es^lh#4#!4U>#?_28l>oYp}>LSDVM7qCY?a zYbeKlNK61u_Vb%f8j?y>FT#Bg&j?#5SaXC`VQoVz#PTZc9@`yhUo*?+In zm~T7AHTK{Ay~iu{KR(|7x})EgKj$lz(Vt|QPB9@P1-U!zg%qP;t_>d~TU(`dt1=6c zx4?2&av*)MdvsRpxAAJiB)G~q)eUd@g$k5W=1>x|f!F-Mu6AV`N zD5ZvHTsgsHD7Av|SiwjcPr9&6X($q;o@w=f%u>QZp}0!TGWTsG6i&lbxKERi9a*F# zFt+Sp1L#yX!49oJl)Qk!kJ^Y|y;v?!baqfUZBr(>sVzpr!qbpUxGmPtg*pw+#R2RQ z26#&#&Q8mrD=bJc7mk)_cL&BSJ?AR3zAyHs>T7o=0U#5~)03Znc0!6@kvvbhNj;rU z`2fD&Ns?S=&xADv-Vyk&$fn#~0_Z_Z`Z9oBPt9B{JRNz7d7X9B)`aD>QDDy_7+Edp z3LyD{V{lOU9B*{A$rf#LXU>n2vY*T273eYWUJ&KHm+n0^oo4N@Ye;mq0kYMp6w^) zR|BHhQ~tUTCT!UsPYzInDpVSDqSyxkk>-Ub&z+q)6=mmhZI%Ogw7d6GAg`iPh;N1F zd(?WX)_(8NvWWq&_4C2wzLxkdJ#YL)Wb z;eRSUZ<|>wrmC0gtfzFwx3^-?J*KiAQ%>~alO+Bp%+)K;B7mhvTML<(Ot|rDd+--q z3(xLjEvM{2IdM??93VG*!j0kCv%+f+A?dDA)1#@<<>SLQJT2%RFfDrzF1VP^98P>m zv3`Wm`x!U$dXiO)et*Sf!!Q->LCmGI!#6Q+x^2EKlLxP+Y4^03wB7SM52@+lNUgji z#@-%)25IfSYsOCXJlBdV_xk}*tMO4ij+)qW4@Uf>1DumNSiR?2~6dZ&d7r-gqIv(mj^AMIE~rt~DiKzso5Ycj zYgIDzs=4mU@*8o~wR@@c@`hWZ+1kHfvnOu@`xcxXKRX_P)mnWTjn`pFXIRO8wpIYA zx1Rq?vEYhZdt&sssSZZIA6uXijYq3Eodb>2Ls&W&3<+Yk@wU%$5icJQ%c?{++sqO| zM@UQ*(g4uo;}3uzcwnPB7J}ZUVl?ywYfG|RkYvd+aW#5nlY7Fi3F0jg+yc5*%_NKq zAze?XQdyIlWiSpAj!7^Bl0aKt;12NidT=yBsJoRnczr+Zld;j?rNTt$>wTpO)UHXM zr~<8962*@z5`|EfSFuAutiXsTl!YrNJH;i{vg_bca#;=nRDT*6dv!h}1E^t4Uv;VT zb}?20;j#$nb@N*K(t<=2j#s*5U-^E(x=a@?)Kfy+2u34C!F$f$q*^I>xbw~9cwRf^ zELfeMa|hdQo^MCnF}<|MO%48RK5hZnw-5Bws`yot5&OTY_$H> zCBqWy1XK`=9t+hR+@*OBsPPPDu zP)I0dBwDn=jIofm>-8D(=Z>oAp$iLu3*BJDX*HZ?wuy-!qk!3C%nHqhuCj}8P4`3!E8F(A%Z+Pj{LMg**daD85^g;)-LDJc#0@%13H7P z-M#ki-tMz+o_~WHjqx`-XuIio?s#X%Yq;OEb|=rh?Z)$+Z`_@|=ey14&%S}JXU`hX z$GZ*JYr@GMLfhNc6a^@P40&x(Yc*Qic4NnG?F^gG+dF&h#y3uX^Z8!$+1}TU-S&2) z_Rq1;5=bt0~}$>0GWUT;+~@jL}-dB zixz+)H$qG>5-LQ0Bn}1#Q`t!>D|fHpJd}Ok*pNBB*!KsbE>+0@Xnr(b^cDEGXdw%o zz&X+*&sEj6kQ>>uHxeEh|Ax3I$GFVq!T^jB@QDk3TO>UNbX)%#`(!MC0}_F$OXVM2 zFcd%9Nm-2yDL=X-nBZv~38|@40F`z2BLo=&Y=#o?Q{gT~rZ|`iB!KgHj9lgeR~A+U z1pmXXA-!n`??f#CYPc5KF zC~1@E17Cl%AzHCOp+@;gK++4D4*;A@aKOuh51&*L$7)EtPQY+S=Negj4gVhF2zdu_ zgoEk8ogpvgOXzSKkR-W#h1^)2c`|iv;KMQU2gny2eKwPSbI9D;5idiFnSe|E#;L%{ zXS-w`k^lv)os2Y7hLMo?WXjiRTM%y)Acm+A5eR=}fn2qMyO4;r!ShC=p%Ty)i(tNE zrQH$83kVadj2f8T00Hq(e(bzwJY61Za@}UcN0A2f%dbh5Fv29lY_ab{N>6fUr5p@@ zolY;qol9b#x~(x0T%^GIXM^GB==60PT*4?wl6#~5H{JcWqqE*YXV`6n3G^v4a(kWr z4`+Yf;b{N#xYzFv28X97qt4Om(>9<148w&{^!W6ktGi?*A06&@PX^u5L1)+*y*fPV zj(VNpn>MJkd01ELxq@|=y*xcS>h2E(ltFhm8Vozbfw5l1`aI)D?-`~51zt$7T=x_O z9zYi;V3hxvEbp}1J_#uC5!gIC+~O-3%JnhnBR~SdR|%V;2v8JVU>AW+vGfIe0g;#Z zJx0-j6V#Rhs|u(1msGRw$CRPyu$QE3lk^282d|9{kg!;jlOP5@f3l(~m_Qi|$T9#v z#5b=v~p{`bfqs_=WDNWy~fi%+{FUVEtT?xl3OMU zXDQ2uC39H5mI^Cr)jG44$>F8s(my@^;b?T+`N!zE zdwkmedDQFnM?ai(`#-lq?fLHWZ@$^y+5M(fE2}Z+9QTg8gG`l{Rs}W6t91K^9e^-pl@GkPf7)`jKE;-W_ zUmyzNa4X-R6x3*Vc-%cb8;%Cu{XrYl_DXu6c4M8=Z%&89!)||cc=BWSWXSQFtF)bt zw~uJ-^S-+?l!vq5*?-eDr|0-3uiSdJ_q@=iEIS+?={A}>J0)F9L3U4ud^VCfC=~4+ z9lkyhBj=O$f79Xc&1f*}_S&GvS$9pH+yvbL7<-rjpG>EmoJo*!KEY`@$%!IIvP31# zh9k~JP}BxBzP?Q@bs}ZiiqSZ!0-IjAjw?BhBgAxdVjaa4Me_7lgWenmIz5DxUXsX5 z)8%kFp2yb>jz};^0RtB>!n}n7h^nnxdIR=zP2ujApW+(vA^FV|c3dHuG z5e6h2e)1L4R$5yeCVeuMf3@}5Qk~UnQZMPDIjLEig(gk3t@I26U3t+{^N^1isu@|H z>H}Xhe+{peZ$c_9$C%xDD;tWmL5L!XDMNvaqSnI6RIey^X2&zArE0ppH>gkbIt3$|=e4dw2)9L?=>NtBm-odsLSiN;KTn#zSdb?F+!1!FeTXpl{fR^n5YI~TfDv@lg|b3W}O zbt1liQC-beeM-pG|EHM1sY?qp$kUSPP{)LhTm!O;Qj9gISjmFPpe3g-Y9Wexf21lc z(h7Z1S}>z8Dbto1x{@*#NKOH=T>X(zexzzHrRB=$LZoP$>O!=rn>M#@O4jKzDr<^{$Tdr*A`~BN zEh9zMel-dfG~A#-VCJVJwKixf;jvIZs~uNlx!Y*WQ?w|$+)*IPS`)Qef4e2MWWyA> zu~_^hVCYJ%PU!pOvKQeC?4v2_QWyGa`Dr4{sjrpnO6G{oP%Q4ts2f4fBNFXPzJOff zwdak}OflIt%^v26McAb^+47*Q&ruw7=ov+kkjNWp+UtnKp^-SLO4*;mV2U*T*E;?s zT!r|bHZqfn3}5e8dXf-uqsbq_GMG4-g3RNsjn(Z-YjfEEZ5NB8<2s z*S6|#zKXy{c`D!6k5DjWv-12Op&wTjI7cWvh)5VBg$-)vLdKvwJJwt_GcqxRyt)E_ zG4U1y{D&U1sBsP_f9G(Y1c7n$wd$7OwJ@af!CPJz?B^7DKhj9#9e4hO@2 zr#BjO`#*O3gEpwm7q)2Bk^e0yW!v90zG>CK2H592pG{E+h1DMLmFVV-4B{oa|Mks}waKE=1B2#6l z10?I0G98|P0P#&9MhmBi8I9oA{nN8v1%_;QT7pwu^X1vASKa<-aQNT4ZBT1w?~|%Z zs~Z_zVNRD+87R$sT4!%XBX7E${_thDGaMbB47>dwe>+D)J-oC|O4od04ZE-Tltv^D zyoii(@HFBmT~hzJ^AAo6NBJg~HrhSuygcfTI%mVv(dnyK(j?=w-_6Jwr8Uhe{qCU4 z(Sl&)V?^1KYVykM`0!-(^5>zrekxloxze3~B&By-JFVx>myRi|^i$`McWZZPae=o4 z_p)-*f49cY^WA5AOKSgc);;SUhyfjU2ZPRQRc{5}bv1FfC##L#>Q?u5cej_dsU|J$ zboca?SpGg;He>P{YQHbo*s8*ZgVUpqcDDW5v(a7h0cf2P%CInZo_!QcpV12zC5uEr>Gp?*uMYP+!|pP0V!IF^oe<5-LA~w%TnZwL@C9T@135>F>pQRuf5Zhwx%kzE(!ClMx08w%6VYJ)*%a0EIro1uUy_ab>f&(6DnSh+aINq6W}nYlAnn2d<;|3sp5f1kkg5~adVgJFJj!K9wJZ>`WIhK9p~KGjP94LKil z2G#Ef2`5e}b*~}uj>U*LmNfL!$U_nG`UrXpS>^!KC>9g-GWMoRE-~gf=xCc;ZT&Wf zS7$+DiOZ`-W9XJ@zec~yV$DorZ?Yl-&1>JlPj zPoGY;(gthI@fAZ+0R3SECll-@>Ng3L)89+z!@%VYCCNB^1?SjbbL#%%$We>++-M$iq<^D4g0nCBZig@iMyu=y?(E257dP zISr@bG~3Vjp6{vL(1(kuI6!5takE zhNg=)7(Fus4-J$oQ6(?Dv9W*K#SHgjjl zJvZx>ktj&6=5B--e;;3BxJgt=0{wd z9P~Q-UEXE6Eu3gQGnJgcrVGl@{3-Ax+S46%KQ-d7*wxc}f3}P}b?RwZ^s{SHk=#oU|m9A#%-BOk|01DTax895^w|RMO-VrijMiVV~n@(T3m#lex-fFZOg`H1o z&Nedd9L>IDqm!-Pq0boPdw2 z)4vS5{T~nayXM8NT=(E~GBFZg@d1GkAAs|tbXpf91fM<`Wsb;nbc8ODZhjL z_;B+2@Z=w(-s!=JbNf%WIj#+ZDnKyB!PQ6}R5SU7A#E%j)W(-MEEGjKKixYXRgv2G zu3*ZSlG&RgEbqbdBx5<*%5B)oQQC}9mZ7wPEXG$jLlGDaejW_F#|NXclf&T)-WeY; zNAWD98bA7ue_lmoJ{m&o-)t#PM3ZuW>bJ2NUmYF|yZyqja$kxrbM^2;LrKBh5{ss2 zbVQ~`p$y-8i%^)$&oSh;&j+^h)^QZ^8)a8LCLk61Js&8>l)x8eEL_S7I34nhAliX;EKU;e%J>(^Vgr(*Y!`TUmvQG1l# zWc6f&1Zl=P(38I2j8}>+3T)+c3k=K&LnG*U(RaW6JNWg<7O;)_Un`HW%|3pldbQ!zORWnW01vEgWvrA z+cEJLQWrL&C}28ApqH_5Bu(QY1#zpE=J0-w;8a#pna2G3!X(RRNoVlkWX2apo!q+SWTKKFXCyH+mB+Hxe;E#C_OfCCMG~7yOQBeqKla| z@mTJOnGgl5A>nad)~n|#CSkjnJBRKJ2MBwsAadlW!ZuEi2U0uqO6n_eI4GOy3?j^V zxc1l!l`DrU)kCL&zc3VKOK@O9e=Z$%0i4#9v;ht#sQhaexdnTSx4Ez)thUHtERt!9ujuXRT}s4jJLqYL%|riFs2AlGDqNT zqAHOipJ3#BR2VG_w;$w5839wUIbO)<3nv3!76((|(Xi!MhZ7JX6&jEOQ5>48_~9ZA zGjNH0AB+)*Deor*P;_4Pe|8Cy6EPz}G@g{;HSY-{r!P?`q3FJ660h0KFv0n}1gpOZjce_$=^qbwmiDWXEt znY`3(gAX474y1CgI4DOpTUJ7X)b2S+QAzW1pq=E60-guM(8&gfOrl1A_)t>iBltZg z4CMx1gd$D0JbS#5fHd_epQ%#B9jbe0aI_M2`ilDK?opTp{TD^$4=M=CYD_A}=+A)2 z4CKEk9(A|ZUd`z^MeWi~$b&^W!sIUHl=og3Mg}g*_TA+6%hnP|1~MU05*r9m1{s>f zK1FQ()%GV1k(bZBol(z4l3lR~vM5M`*rnmb)?JhQ8%KXGdP8E2@YUeOqp!d__AYuCd}UdZb;w)P z%`Nc30$_hI>~}lIFCH~50FXN)e^>v4cQwKBbR0Y*Qsg69=UwgZmIVY~F5_DHxG(yp zFnOb^FEm~}+H~W{2R0o5+m_b5;H!Q4oR&Szzg1QLjgsIiV8@{LsF%16)!O_o?HE)8 z_9gi0&2ZQo3_HWKLHqFsah2o}4}JOy*tSoU>yLkJtLq}rI~yhzz2zH?!ArrXPde~V zV#}J}V8l&5`1Gm$;RAS7$uFHy+Knal!FnOr6^?6Lf0roM4EZmQxzCR&wP`DZy_cs4 zKfidiDM2m7y4wK1izpJGgyb6$4b;1z84IUY-7fL3*P`By2I3MY|n)S)zOrx*ko|Y{0 z&i?T5^yI~(mIc7US-&Iy+O~=uV1QV{0Md}KT-JNw0r$oO>Ivp+z!nZQp!Um$cQxZ> z#CNs!yV@f;uObXDfD?uy@JLVZyV}!tH5q^Q)+{TMb;w(pk=FS1>jU@sE6vdZ->YIn z$#ii9W#4AfIpTaGVsfmiZ^}^M!v}HJ`SdBnbJJx0DPHsL`&+Wr`>ETn~it}H= zljevvw%Vs+OG|U`#sAD9k`5^s&9YUa>)I%x7W4#f2wQ0_8zdD|F;^=XU}r{zqPab zp#Qs<@5|-?GZiah%?&`atrB))Jb;%E+imj_T>N*n3ve5pkO18|7qD$RgDx0`w4UNj zt*!xLx5e9y_seVVJ;wWbH`ws>d}YyG@~Y@9Fuy0;RolnO?T9c9fpg8xM&5tciJr`H zAmZaBQC?cYNndhXg@Ig@?EIskHZPR@TF|#Km zk}pWwMK@F#&jGk`->LbTSjOA8{h~}XY3O3|H`rwId1k*1RZ7GNaB6aw?)}h z+s!3>%>1piE47~>7D`^@3SZr6Cm{|1WW~1#9LK5U017J*fCKPolcL{26YMp%{ti5n zD+Yw6p;RAEz;#rv8SGXn^9o)4yLj2G&EkGp)D#0uO)JG^xUU}7@C%45_4SU!VEsv2)=16q_2g0q# z0o^M>0fk1zVD*EsDLZxcgvH=F1`}sIsT4 z?~`pNLGw*UnMTsT_Z|urUR5y)5+T7N|a+xG8vMzGkcK?em@HE#fJ}+fM&t`}h8?UfpLxPQ zovJsdgW*Z%xSOFF;j{TFBJ=zcFT$y#k0$wa5hl<)z~-|>Md>T2#d4?c9+m7-Z*+L_ zx_>(up7xW_s>RN=!J|#x^)2USCy`1~q~zU%TWQ5Kb<3-&N9Snq6g(2c0PPpoJ17Ph zS+K^6<8ZT?39fjqY_EhcGAlcmTFQ~lXN1exnK_l6vx7tNbkgf?zoKU=8qn_%mv=qi zlD?(BX7F7PoZyi3bKt*LY!9UMF!~a=Nu-lj8FA>FMjE?tiG$ z>m41+2=Jr*e)phzGCb@Y4c1;jCBjPj&>}xYUQ@&=#pfCeAPezf1^lzFfZUV5s|wtT zspddE5Y=CwsEU)EeG_aYy{wcK3-E3#AhOWc4Lw>Zk1QX)adD(Ok(?Fp;F7sSdy$Rn zY8EYv_?iMK(VAq>lOtUF;LYlV;$p9PMxoEuUHfd>9HZYi5@HGiyR~9yooTzp zgjKy(TYRauVxr9ZF7tWXM6Xnefzpnp)r(wAy-=G|QmzNrtXqU!!W7D$$$w3r$h}6p z_>bI39t=*u$Zv)Hk7lXT{cjG#n;3wuu>Wj4YwYChKlip<5B5Lz@m*(6wr$(m0EYo( zQS8bqo(YL$nEu3hPgYBm`WwL75TSKl=Ie)&)p25q0u*6)J&Ozt_no!DS*tyX$XrB{ zf00xf=`R09N$?`!ZT0d3Ykxtac)Ao2#ZwI@sJ6A)uw-OHHEt0HNQstUOxR2y?!OLB zPZI3$?f@Nj`oE7) zt^a^9u!-g&TP$-**{U@9OCo39(0uLgt-MQY@N~D-+g!=8+Tf#I0DrhCA})bPj+PCb zmyjY6O24+;Exxw(7cv4~d;R}L#y4{QSiSytw)eL4`+uX=d|3bY@!f9yi}X%YPz$+c9_ru^&geuayQ7OEpTNFygAPh#JH7Uu5W={7SqrGw^*;?ZTxj?>|0MJV(Xyqg= zu~}(l)iXKUjAi;g;zB>K#9i}#LDV<h&ljUM<|o<-#3bFMr(Rf?>%O!>UV$Yg{ud zyJ)CTt1nOXDY_X6Wb6YT=Krrc|M}z!JN4&IBl{YfwnPWNfSNKtuRR4d#q{cV4qF4i zS^$ipdrl@3@c>mDv>I9h|KbGjKlxiB|7We>Kac$1+-~I0|INmO{_lRi>iZKu^QQLi zb2$6uU0CkabAL^a;PZNBXWRIkUf8SJUgdDTs{S>7t7YZoe$?r9qh23f%cZh3T;~#n z%AdW0{@UMq{Qnlt-z)il^V#-JGtd8@?L6pz@8$av`2SzR(|bL(^x56KWys*qvQz%K z{@g3^dt+zr0+Fro;5ho=IJ!*B|KK=!pN^w++ZJx3N#Ymp8+sj!cr9*!0iKekL8s`}K6GcL z9Le-8?DbENe>fWTyMwc%;b64i*?-d=9UPYWPnveoVMOM?`?ky5sNJH|5t;vRbbHT) z1kPukh<_Uo^udqy!H@O9k99rf{#S9hdhlbt-U4{=WBu1y0S|tx4@7l+qPn3YXW5O` z8d`!c+lf>6@n73vvv8MD(b>EA*1XN>s3$^rGJ zG<}bbtLl>AZoO8OLBjpItg1$r`K#V?HCIhbVSl0C(X_}ee2%%1kpGMcQZ28ZrGks` zpMAmpg?}sTKk^Tc-N*uPrTt&4VE?hbyZd1OaUb7@FD?Fmj^e(g=U+arDdC^=l*=t1 zY*}qOXxQW2=#iDm7x@1w?v-Esw=Vy`jRoLx{@-df_VWC{)!KV_|Lb19O38NFgQ}mC z?|=U#i~-l>OP}2wP<7-kcgf7ymUz+L)X;AY+}+sJuY`y;Ho)-o;Is|c3{w?*V*FV*`_rb9C!LYX2@qg0GH2_3hngblQ!G{N9-1{)bt(9p7YMCPc zw9mNwaMtY~cJKHVm;AZpI*+&*$5^oL#jhl`}~U^@I@I{aYc_Ql$`J(vzZm<~UzfCtmz2co)X)8ULXzC_3IWs{95 zt$$+MiU2CC02O`UVCuLeLWJ@yzn-@ZR6y?Pjfmuatay{M z_IMJAS>Cs1;JgZ;Sp)HFnuxD3UteO8$B}rAspQq1-^f^)amlvzUlsetT7Z`M=Ul&k z(wf|iN@%7O?cr|@e3>Er&H5e^Z&>JlE`RZsBgi*#_k%!z7n0O0F8TA89Z$jDeV1v* zs-M3Uw zMbx>(+fqudiMz+L;#(Xr;Q#Q-@Gk=$J z(;k;La5sGyHOf1m&$?;v%bU1$_sar(yACtd}S^pkJH&lKPv-T8aZhw!Yq*u6# zs;%FdEVPnuez75dR6aczu8F<_ohQSd*?`tpBQ^zna{0!bUSZis>Z_hu+k z$XVW-Wu5pxmo)r8*BQc609M*bMQ&HEtxU(ozbUa@ZEt0J{yzZ7Q)s4YmOArQzfR0w zWTe4N)g|~UC5e^)|9@Y%sx63%uOz;{dJ*Dc6sDLVYFW0)IP_xQY&xx-ueU74pthYW z%Leby<1uoX|6aYi9!JPip$}9wDaBbf7;M`vnTL>J&Zo%uH{>!c%z(|#PXjR6ehQ9p zaCrI@Am%z-5)cF)@~{gz;~B^9IbxP=Fg#_yD%ckVo9~m|b$_G{WPhV1crUn2NhZb9 z_k2tpFfau#D8umhNVwV0P>Kb01KtMjDT9FrBk%o|z~O+Q5H#ChU!K~Ni*doP#*oe| zg)7BW)61sRnK_vd0j!k(Q||CQYXc0n#b7!yo2d-T>MLV%==(tYni1baku;{G5|IlM z^XXtQ^pn{VCx33IvWv6vFM82%#Q%gd zWmzHZKK9~U=*@9p>%YG%E3Cc0bYcB>X<;o&dpLSgv_>wivF*f>|KeM^ZR=Z7JC6Kv z=-cXK;+&&}s3QNg&(Wf)Lh5%QD#$;VRgi=(3d^5%C4Xw@wiBT#CV?m|f2x8(p0b`i z8Ek{I{t-Y|;+l`PpFH8jFBV@Lyq_@^(ssR0x1DblBnQsnA0&X6!oK9aWy>Y13b*U^ zISw#!FD* z(+U*p#eciq2K$;Nf-;HAvbr8-RnpiPO-O`N5w9(AxUbld#^C@l@V+yZzCi zyWj5)-vdSj)1q*WKy%M_XE1`U;KqJ9gF!qOsO18kj9tRnhLqz3^Ppf8t3C3m3@Y9LF@6%LaJOCx4YsGE5k%1Z2$+^g!K17xg%Tfk)=x zBRJY0b&if+>;o17+jBw3{%_F!qhWsoJ_1qRc0qIR?;>Q1ME+Umap=5xX*-OADg?)Y zhoUy$!qW42Apo1kf`ft!r-0^c<>Wc3RRzn7NY<{lL$L`Z!+CEW`Bl#|EvIUP_acao z;D7WQ|6rfY=Oj=x$VdG8ZP(nso`UybgyuM&+tDTAo$bCIw88sn*y4H9aGR%fMA{q% zaEd%zRp9w*LsTQ^Yvqh(9VlBdt*_x!MqH)z-Gtt6F=b#3Df0MH<&s3_K7rm=+p<3L zK8GaWCyA^n2Y`7f8owPNIdGWPg3M)6e`}{FA3xA5#$!d<0EU2Q4j?eFUx7 z7}UYeYpfEik3yK_Lfv|$)1$*ju-j!tGtq!y3naUhdH+O9_K zKoW2mvSS|!`)56%5h6Jxzkoh-IPttxPN$w z1ImOjlL&f_WVn-3W?IRNH^7f5!jpw@pf(xO{tUV2QfeX|1u#M&ih}?L(_~wkA|^94 z;H?eNy$X@brBV{JFlJ!$5@H5g_BNQ~Am;p#uf+s5BVdexh-|mjwl<87O@_s4DC9Kj zoLc&-7iKvf(xS0ux#o&;n!8DWYk!n$u34^CESKO~T?Ek#e(8l;+A-q~ThC}@Qy1bX4t z^Q5R@anZf~yitPh(+~xT1SllE8KF$b`tqpdlPM0gf(QX2wQ~v2vK zz_L6}CS}S4+opWkju$V!a(qbH5YG{1a2|feK^-9KMtIBxYd%!YYp6RzUhE4ApVHXi zxSlS|i>|-9lbk9#G6Ane~*xIQdU17=uQ&HLszUo{OMaWG+mz;qx1=y2k6CXe~B9w~FSR7Jh zoaKTg#LeTePv_{uv71}}&$gen z9@hVTd>hplz|Wi(uz|D_0jG)jK* z?J;!Ek!RfOIvi^EN?7#JHc%esPr*5yoWp$*1WGKtQ2$PZ$Y_r-voLLu0xdkZ1y8KRx43jny$<%6!g)+Iz`;boT*m6euVN>o-_c{x#8b3*f# z#BJoVwyLjxHI~&`dcCo%;?ir5WfiNhJC@Zdx%RLF@NIIh_MIYC09qNs)E5al=I(Xr zFTIE;^zJqn66*q6c`=bgf|ZnNTRP&2vYgW)O^kTT^|I|r6lZYS1~tj0WGJ1`2xoFQ zIU%eUAu2R%C84~*FTYyKd=0e$ClCJ1lJ7+vB|)5%QZ~mfigi%}WpMcVq%%D0cSkQf zgYMqWH6T(vH3xorbkyA+if#?M!_i>a84lW@#)*?hH&TD6>LfuCUrM85K!9NswNsOq zlr~H0TGr9ppfO_5NlzX8R01!l?GxzPW9Y-cMUgE#82IBP+)8<{Hh$1~oehy5if~zl z_ae*^`L$aT%tc%xWkR{QVcZ6_){Yr4Y%Xq{+Mu@c{57sEjRYgBH%n``(#jhfK)!O4 zs@voxgd{`{K^h6WSotc74d(D_aE>n9zzoN&YraK`HpouJj8K+~$$dFP!C62d!*qhh zjsXTzjf6VE;u0>5vVvZU?-qdamx&UST{t0MG*H65OZaRB=Pu*h{wi1*jxG9bfs|P{ z)i@GXUCOAJv+1>&v}Vz1FYbmv-!XSypXYv7j=PfI=D2M+ug!7V5Z%xg$0o2 zG>N}eTbL>Q6k&!=gXA4FivK`uX&+L0bo%TKn$ZSx%UIEgo-6}5swB;tX5(NAHnXpB zNQzL4{xgqhcPK|>2r{1;f0BX*+ghs;p$Uo#l9&;L%7maq1j_SN{X${L%IxQ@&|mMtJVT6^9o%~Ub0)gV=P zjhbqDP}b`x4m$LVaxt=*2xL6%bwuLONSsuq?29*AwLY)b@h{;*w#lhO-$p2RQ|&pN zOU@j=WN*!*R>|SWI2uWRk;f{YvgEuSODuqTKDMEF50+=OO`%-nR%{r5;{8wQ{hzQ@ zqxJNfr7iC(f@yM_rd@3UcOwY3m7pxfi9cHW4`p|OUbmYo_n={^z)a= zdjFFaJfY-DHEAa9DI2Ld4DAT<)1P{YmJx8#aGE<@HqXTnfXVxRc5Sfjw4A1uf&2<8 z!yDgt-qt5m&aL=nN+OY~ObJm0NCb{>5MMn7r-OfpyqALm&Mn{^`wKuqv8;nBpbN^- zoI2Kwh^W!Zh(VJ77&DFu3%k4%Ixm8k&J=SraS4GRl~KTi2FVTFNgYP)l0@hAIpq6J zJ&ee2$W2z1mx8W;bFk^3HviLhiBBSjUQDfXwBUkr(v z#C{tfFNTpv{>_E{B4ilyE+%0ZkVkQdqO^1XDMIsqV-XA}W)2L)q^!(3k9{A`(h7`R z&e7r=UjEyZCuE=xbzWMWGIW8Q84fRz@0U~&`6}CzNyb=zlVgsZOoN;Pfp=NZN-r45>v zm0S*+SHo_9EU{LRb}4G-fm3ifBQ&*B!PI#4BmcT87tI;pu4;CT{}T|# zPlH867fW77TL5@?il~r)tM8&2qU==!=ZNnQZL4^?O+#E)-;a-Dx)_ryDXSYzNhw(# zeBiVVStkk=#s!iA)|F!qaG5^);v|GDeAVQF$`bMS~O4hPA zz)vWD>kI-sb*vX1nV20^TO98m?<-%JGd!K4$TE&*iaZSTYg`x3)EnS5M8RN&C(NcH za`6PaS^sQ{=mHt))ev)s*uw4LM&d=jzb|;rt7PR$gDigD6$=fsrP_gVrGRYWj3Q|( z&80iu5Dv5l;!_Yop&Vf|6eP~?=o0A1?{&R@B$|;`a>(UPZJ5W`mN?_j2xTWoh<+L) zUO@HgR9c8v!ik4xNRm!~iIp#&Or>pWLuq)LPRrSLcEsN@7z8qM1KKow8*GnoG0;&C z)k(4W!tny?$odYAClh=nGD)Z;|BZ|yE$|4VmbL)Wj{&!8xnjr*s2X+0$B-h)Og2P+ z5gfKbP4!+^)R(+QgS81E(j_B>UK_(mr&)Qeh{l!+LupNeQ*d5H!W4Ap=t%t5Pe=6$ zVXv@{q`0prYC~%#PDsED z8cga@bBO~X3v6sCWes5?W+KD7Y=^G$Xyb%-w!S)AJ_ za@#~r3L7cOAEkS&-1>bjRoDRfh|`l!L6LuO%4qJ+)t}l_EhV1|6p8B^H^SoRV_BYd zHK(P!zW^bu38xX9zyOYt3YiBHq+wh6=`5z0UOU1i1^>M+u39 z5gM>m+(cK%O^1@aiup^eKE^?v&T3CV&35^JsgF}sfia4?M!EuU_cmyb4ED&%I*G{k$aJD(IAND<75kw6S&)a#-)Ddr!8={ zEz~(DIiiK^ zr8&HkN(kf`o#9N7=Y~Yydd=ZgM;0>F4q^qg_zD0@Y*8zq(KyB>Ey-9{t6IXrbz2gN zvABmzPoA>iKCt^(=L>?$P-rB0D>M7lP*Goh>gNq1|ezB zgyhAMj{3^kDW7ct*8x33sR)0>X!z9EPA3!2E)N6DF!cG)K9c&Hnheug>%@%I9MzU= zS=t}*46BTR%yH0{YGx|8ggLxQ6Sk9=l=gdPXAJxJ4?(Cs6uBs1VmE%atK@l6PDX!P zT66syJ^y9)kdPXAr2MHMT?T~nOLiYA0^v~HWBJqE8R^Kodo=pOK z9{UXQ)viQx8wQ^3#*~q{ol5C4p)W+7gnSh6GWVJ!_{4*f7zCq(q2cxkQmFL(;w#M)n z&EN$VHXizCvU`fd4D^4|0l5rPIRFUNvZ0|WBHA3LR<-S9Q$rMCp@SrWN84bpQ3^~_ zUMak0V->))S$#;n15Be>C=Or7o|?_VN-OTkb9i+YB;ebiw%vGKv#dZEk}185*boW^ zH(5I#Lmg%nHj@%h2`e@Gkt*&D@P=Fhp9E8|i31?1ly1prB~*W$qIG~Q$^4WAf&^%bcS8!WBK>s)UDV6WH9sn0BJ`;lA<9C*g;w5b_3o?F zQ%N>j0X&9L&9XK`SZ)-5T>OODY#90gMiE?qG4jc!I8r3R9%Fuh0g!?k3NC6-!8jIo zZfWpu7W!Zk2huT|A6(@M1nT_}fqNMj(y^4=Ip6fh9JIs~-<(XfNeC$g-Xef=?CRY_ zg%Og0E7HbAs3jOpMHmZEvu%nP`1S%u!q@1#7vGBN-|>^8W-`2gibH3Tz0AlZBkHPr z#BfMQFz`m4GoI7>hG@rTGi0;SxAh>c6m2Eg9<{RgHlAsi5YiG4Fl<;GfdA13V;Hr> zyuFIZTrL!aBZWo^5EdIWa>uYkiPQBS5LuLNv*F$ivt-Bw4zDy(O$Pz%s4UBl{ZHo>7>1IU1@a0mm3>Q;LCG;wsV0&&SLN9;5?6cMf zGLTwbL|jOJ*l}G851a!SHZZuZiuq zw6y@IlSz{Fic^{S%#0wr^bKkNj>vR$gf5V8mh#C|rWplAK#+)zUSS^r(+UVN6`n7m z4)5)HHF(Lo6go$xwciYfz0pA2e1g*;n|vfZCx8lnosrEG2V`R`#_}?U^G0YuMlv?n zsWz2gIjJEIk&gpZ$3Yk~D%21BM=~piC%v;_IwyQD1q@=}zzUmz`UQ;YKAG0jIE?it z9PO#|7?;|4OVSrL0v>VCQS=nJ5l#vX;Z&$9oF_@rF`AEF#*>kJ%W(JjWs);E$KmLR z1k=%f5e^VlgRbbZybkp;EzJmJMoR-(gikL+5f}}A9t^w32cxr-!{H0wD<3f@wZNAj z7luZBZyF6D_HTmtiD-^vUj0f?|LX8)*zM;Ak{$t46hY?Big=5rXmmuTCb7LmD3oc& zntJ@0P-Pa*p(OlwzNVu z`2b~*&PK0DG>6O_QXjEsp$O_#soM<@C1Y&RN>|L*!RRj;KCdiYsU-y2nFAaDbfpywk-=?37MmJdN4Za9Cu$l+AO|P zVW%&P@nzeWw(m~zA{qGf3D`cU!7$9ljS(R*&8LD5?wY<6{Ipn6o58%uHwZp`ssWn_ zL6`58FKQng%+Q?v()iWU&lC&Tf1hewNrdvXhLCy2;E~3NQ1Z2frq7KdAE=o1iTozA zfb}POrd@mek7mdZQA8aUuImc6BK~jV*>3**|LxY!L;UZ1`96H8KLHnb-WG3Q@Y4?$ z6JN|ZTHG0GgC}+Ih;#jk)x8Q~AYb_qCl#fa6~y9Ls+rLuz{?81;t;u8 zj&(Qz%Hu}@QL-;p55kBvf6Sx_ue{pw0Hgql&a2)o!*2R+x<-oeT6o@(D=AZ1U@ffs zG==m?jG+Bu{fRaDnIJ<#$+GLDubI?qcQA}_z$T#fnA(qNEeB1udmU1%`kf&`V{XM` z5z0>sQlKMxC2L38DK4L5QLt7yA`{{AxAtZHsEuSn?duw-jcPYpe^v6u4SDgtw43?~ zeqL8!mWr-t-~2BQ%#lyUmln}p8Ar+c{sx(sId2vvg2WTN)Zf#1ZQppUz6eFXH#c6i zGlGB)b5n{uZQHO8`tl>SOS>V@R4WCelA2mDY8T{~^1d5ks!fhhvtfNW3ydjRF*4J0 z^rVVgWs5n=JopMBrImLZ0so`bhfHM2IgQjlgI(DsUNkqfK+k(Sm7)cEkLaXHZ>Dya~814h&qxx3{JPUz$S^nCYpzAAydF6 z`dzH?e95h}5!tV*cK%JU8d+v5fvx{8vL^n&8gSBMvK)nOf2AC0hw@$)bduO0g3KbR z<$a!bye>zcvi3~_jAW>dGvv*z0-@78R3)kN22xN{cgQ%HI_J+N4{6r6z$QiW3l!~> zc_@q&zkAVin%~%s+7gAtYs}P7iKQ~oDLvkze`Ca8WhZxT-$?;KyK>MKLxJd4Hea$( zsGYzdpSEOqXK8EVrolv_R{_)n!LO% z`g;A>)em#E&GCqZyJAc!9{7lasrnY!6ohjZ=%}V7&~%#5my?pFqpfS>l%L6mi}0Gb z?5SG+8aR|l?0I7VSFlLIM@%>ArC1HU@oo5}ilI=+e+zoiUY6e^$(6h&#nGEy zXZ1EBtwMY>h3-PWuL&rTwt-sb0qWpfT(ROmkUXVnL4gq!Vq>jcX%@oUVA!zru3xnH z_**5o{|6E`H7ZUFpy1&1zqyK9*3i|)O z#)JOvKEA5mfOv9s6%THeN6)e|UUF6!XCFjEnk;HLuhZ zn{yORQFeWUO%J;a)EJx^+g?%omZ_vw5{61H=|s=C9ycgc)=Tfs(^23FKL_Y?D7Y|x zYws0*)k5-+LIJnDUSdtW9CZcc@joLNOc8j58JdIkizQvi%(*C^D4MjjmGTVv@Ig?a zEF*2$9_3M%=}F-agIy{Q5(5#!p`>KMxeCiEr+`OwZ>-xfEOV|&lf|ip(UcmTL)mXQ z%6LP!s7-t-h9!VhOsRpcO7PM;NG94d!c4P&kFv@5ls*Vlh-5E(^!23YoIh3@yk~tv zO2%MBfXPs{B-Lf5Os zl~GRl!!{+5vIWaKm>F^EE}k@%d+p|0BhDL8nlo(}QReOM$_yzDYBLsS>QuAdI+9I) z`!SU9pXsXR&0-~_gC_v}p0q5yT5CzT(poup*OE<{q2!vJKfmHKAeJ>+EWNzJ^;ncf ztK>_{41cZdy$D$D@Fk4Z=AiU}*ScHnR7yO1(g8V5WxngB15i7s%9gQE_AK_Kqc zbvT0C9PiT@2jVbqggm$&)aBZ})!()Ce=4Y5M0~VvIQSLzzpcjZc257d(|oZ1yPq$& zIHNIiP0hIZM2EPEc}8?KUdmN}84)tg)G#7f zi}LeMd4pzTbhAmBUJmI3MPp->)8qj$8~B)}e+cm;-Te7My+qe3H7uGy3U^O0noetX z{mUn{+m%d{itAo6Rrvt$7|~486F5g%fpjghDpKbqtDT(7x69b5k`e-8FG*i5I$p|a}iUQ2_`b*=nc;ico9unFhb0cc4JhWBb zpn`1p;wmqs8?fu^um8k(-x$_r8VtAot3jT4!3BaTEUV2z>gKQlI_dL&l9k zwY)ioRP|O#n%M=U6+MqiR??#qYy{>yGn98_WsR_evNX)U&suRZ!rw8?TD8o19X|Ld z&j)3t(q8J|+2GTs_R=yU*}PxW^R?bgefR*a5eB~t0-UPV*acGsGTPt zZGm!Gt-P9yJ$c<)DsJL+tEoVQ<<*R%K*A_ZHmP9fm}f;^nZu8$UxwI%;&qKy({OEf z((aeaweRk`Hvg9wzZ8G5JLCVYqW%B&?%sp^cQ4<1CIBY$hha!ljbHle^B27Xl-k?K z(kqR7ch<8)yv3nE&vs~tTp0&CwOjyT4w*YUGO$pMhhoTt)i#CMSl_ZxF8=rRMp4p6 z?u`UlD*tdo;=7Q8rg@OUhCyxsWXa>EsCjOhs1`b(yd#k>Rf3ZoR}vRIb%FfDahZGL zrGPU=yEEjT(|E4r`plC>S3D0$V3{>D0D84AqgvsQRalddR~mnB=VAZ9kFRq7Po{a5 z0FXX{^B@7-xdc$c&JAzuEJKfyhs+EY5tXg@5}$LE&t6zxW48`eyj99yNzZs{&Vv|{=1LQu=fRqqemwvx&!7?YRm0?J~7Ajjy-VQB@Q_HHZkhD0W zhT?HmW%UL?nas}93vxTSo-oyvF0Uo4E1l=R(N{i!EujRq?RwO;R3L!wECM!^*p=*4!Hyh(gRL>TXA z-w`g>=;Q6tVZ3u}MEP90+RQ4(f?MWj_-8ZlTYlHx|8)qwYsdgA_Wzw`v!MTJ?e0G8 z|M&56lk!-Ye=8tAsC+pF;7&3zX3{iMkyiq!YMuZ{+(XSSyCiTSD~wkTq_F0$SOnjg zW0c5{T@tVK6-FvzUnHXrA)8TCezNtuZAMx$W0kBeefK~Rt zt!5$qf5UF`LH~CzUrsE@Je^-9UY1ADS5jo{!~{{hneHyLf!?DIPf|7CMHM6))Rf>> z6WQM2GhS(`?Y1HgWp-+G?EFLonHWJE&Ds+g+mR>mCYACddzoZx9pIN=3!6YDM4^yY zlIVT4;eM}X>=j|M$CMs;9=4A!?YlPr554gnf8YN!ThH?T9}n+;-q&}-`=5|_2be~2 z$j{s_V{eMsb?<@l4^9}Gh+Yx;mwg9RcBfy!&+W{EO0*4gcy$)Q3y6J=_-sJ)lJ>L3 zKfi@e=`NPHxT+lVUg_cfVY|5F@A~{d5-)k9m`49Ud;i|t#*H)z+^yaXr`24L76%zqYP0M(_1~B~SpFqh$xt2Fc2Q56HcV{&^uETJR_Le{RtB9;)!#f=Pmfg(Z;}7k_S3zp z{=eH_$Nzbd@A?7bJyx%gR~~aKbbr6~TZ{jyh^uvLHHCaBFpHJ;jNuEMqA^Mae_#Yt z6>xC_9HT#S%=B~O8B+m00pc-C4O~G1qf|r^$uy;xM;6M9qL2&j-2|(rH5p2Bl0YJ~ z5>6-yB^dC(L4vsgRN~{bx21p#a{(?9h#>(PqZc?)&y^+QBcs1UBq?k@(${fA0bSmVB5<7kqVeSoTyV9KCv8Ca|0w@b?^<4b294 z_xAbQfhDhgbK3?JS47Q;A`Q)jB5+9#9u=$D0DN~W;U19LcWTe{3nuwh(SoG4W@P|5 zR}rh#>vuFk`l(6Z&u``RpU3fi+y9>K)z<%={q_3)AYa4!Uk_?r z&sqGgW@W8!jDB41_}U)^e_ek0Zw+&N-)=}tk>MUM1Ntj|QoM#S0D|QxXjcfz?4KwX z&oR?4DjcQx7~5>KDLz=BRcy$GU&?jm;Nk>JFTfWoxUNv7I}3J^re2k*r-aP&(3R?q7{$1Je^MMqFf)Q?QgDJ~E(Z*Lr?5KjYh zF84c%1xf-FL!2S8V7**j{+si>`}6mj4g&HHP&vaf`q1GndBW1VYp^aIs zcf}VW<`2y;6ed5y6p|RR&gOg+Q>UzQ=GC*LnmQM=g@8}ge;uD=wQH-vcQbDl?x*GwUVde>if z2+`Zle{+I=AM<}_d%y1gv5x=wFyDRqe;D?7ZP?>>{vRhEkY=&c=z_4M1IQfzj|%?x z>;KU%cx-#zdAoGQovYnb{!WKV6H-ARqVs%+7(oIle>Z(hxR83j+@p~sqnK26bXVwc}-_P)^tp5qbh5k2;DIq9U&*;rn0~ekD?(bIPf9?0y=f4m0RYaZ~ z*Ov@V(IsW)ZQ9#~$B)$#Ne$eA4;Nq(V04GAWEZTc9I50m8RSKKMLnw?-@YwRN}X#)&#tHLGkjb!DSD zLs9?QE%fr4`q{yT2kPa9oMl z*x76huO@k?Wld=WZkCzGCcgH=>6-kge-h#|94FfVenyxCT5=kZe`KD@x01ieDC6nq z1#Bck!erc)|Ja}Wf^u znC2Y3dj5h3k#V;SBAg)D)x{b8J%}!NOcT`oD+r<`=l}7GgXgbbMAPIpe{C!}|LZ@k z+JEGK>-ql>-v)T8Mj8Bw1^5Z2Q=kSv2tJhQzdfD^k?}#-aD*}h&`p3jfM5irXJ>p86v?WFE)4wM zNWn45hjPauONW~IWqs8W)=dK_1)JO_C{-%4Ez8+)*3hP{R+chrA+>0UOhpzwOGzg0Ni0!WAdIPYDf^6 zP@3prQ362?$RG&8p>~Oapm-d}%oLXkqaXwaNdlm`@cNqOL~udF7nfBd5Wf9$i%cih1Sa?_|BE(Mk*k)C>Qs6fFnnxg4Y&wq}D7NZ)@8Gu4CJj{i< z+beU7tT0_{!tHDeP%%O55_4p$AkDvo3dy%4nv((mJU7&OT5;V-mL6AT`8B)j+ zUM~$mf&^kyOfVNXevb<^>--khs9 z|D|d9SNxWo|K+GVsnPG8k|o-hJOBIr-TwB|s{d!NxAy;jkncJGpyQl8bOzvB&ALvC zx#$eQe+O%kF6LrK?jpo1(iwpMR+%bQMs4CRV3-^+%Vb)|aTnZj^K+cY>_^W}PLGb? zzJBrUrx))}%BZq8P9u84cl3mJ(kD1+NX0QFhcu;3f@k9)eB9dt+x`9)*xuXQ0{u+~ zkB%~c4?xF#i}C*l4pPK~i*M1JVoF#{wS8Jje=>z}CqY_8!c>x~ zesxBe$1wdjg6u>m@CpMErSH5ycl&mik+CIDa<%Z+|u(#eg2rwAMs+mJ7b2EBg%cqlM3I- ze;#b_Itf>nOy>7Y{;t7R80_v9zss)wR;1~J7w*gg;B$5>&oAcs#eDB^J^&qtMrzDE z2hQ9ue>pf~p%|%iBA{^EEbw9UQr9=ouODP6E0du_YlKmnXn#n*S>yi0tI}i&vka55 zKU5mg2}68B)1+EQ)}o2-MDfsAbu{EAf7&1F`hc6Fv6U$s$?h)PffZ1yjMA0O^sD6F zREy7TV*7_5;@$muP4uepz2CpJGE`2ml<*xw32GL%^a3%46Qqf~{QTTmb$<4n=jVR$ z(-&SqGs4w#DBzLud6h7F1=J4B{MD?a>1)FXns^QxN`l&4$dEg#o!@gV@MuRBxW#;>%7-VAs4PZ@Y!9>9PZ5~P_PB>ltHoT<0{aV z@cL3Cf2LxD*x<|q(D@%eSDq|(P>7LQ%pwm z`gb~f_OT<=LN1^{p}D#6v9nb;e}O#v*m+d^_}JOfcixiI;q3GrljPaQ@{YNmD=W#$ z#xDG^bF=B=HKJ?^)eIAOicUFV7*$jsd%dAPiZgdMGFV9I&(Xs zNu>pK#ki&#wJj%GU*BL7nxIpz7PHfs67B6iZ)A%?&pvk4ADxLrSISBoRlJE}1KAfu~F-Uj@86u(rq)>KbKbqB~zq~HI z+oB8ae7m}^v-f(f`S)wli&s{Etzy`w-@+6Lfxu4hUXheyD6gDEWU8 zqg5_}D6ZZeNj7^|3B4N8?NG=%IR#baX1^eWj2gO?{_rUyC2 zq^8u5aGJi&V2p+2{Jm=ZF@uTJ+-xV5CR`~q!3lcF8c0Kg>rvscPIN7F*dl!=g4v6ZR+!iktvH3m7YrKWs>|CC+btL}bcf0&}@nCs&o-9)Xs zmg7y+b8+SqdReIE9fB@U4OgB)@+MDH-oj&C!1x@KaT6*koTwH6HL^~N(PUGY&PTJ{ zGg>~XHD$rk(WnBeb#m2rE8y;1S2*M}&BeWJT*nL{@;SGy3?=ixe}C6K>yH_~-!1EY zWf|>nFIYzRnwQb-e|cNh#%*aKM3&tWzM(=+oA#Yyf_aw0Vz;U+xn6?BUTb~#sP(;7bX}``Yqf8!_BnZL zwePD{`_?Mne~Ky|`{EY8ev22%Zf~vQt#!PLP0w7DTkCiCXw^G5R}a?;UL()06}+{A z=j5#wytQ3#t@Pd5uGgyd_19Y8TI*}H?5%ab`_%o6ZBHwIYu&GzbN>@{zy4bHTkC#n z-Oumdx1;;8$9+mVcP2CzP)Z=i5~Ke z>-L&$fBzFy`-ijqzK}FsTYFtNYi)0>?Ui}orndK`ti5ZW&9C=b&iAVtaJP4wmAAd! zJ85cvIScE5h!$qzwSTs&HOt2*dRR^Ar4GZc?!T;+Bm-xyB(0UCGH)*mw!cM=ZEGwzxNG4Bl2@TM2sMT`1%gTW<@3uTKyw_jve}TS*Jy1-EfS4e5Iz>~;W~Y~w$yMc)BlEzE z^HyT2-U@AfVl<=4v;X=zF36`Wp41)UD$z zKh32AdCE~ti9PQb@O*lT85HRHYuW37j3&2?RCLGP&KDcU2iy-WvmJ`F{4RSTZy4>e z^Z5c#9Ui@3dTSII2(k^S%E&@{e|nTVJf5H_EJei5_d40?=Luxzx}<=Pa?}xMnn|_R zQS`KkEM`d97jr2ml9_zVVV}`JR0R`DmxxVPu%wIGTsN3Fp}j!KRP{y zZp;~XkXt3(BKfvRx_OPD5hl3U%Sk>tRoL-TkYO^Sw~|;fLr|dPbT~Ub$0T|7vGW|F zDJ3V$CgOdTGt9Q%=_HyG>T3`?*6a;8@K_{@<3U9Y41v!%Pf46s{GFqjy@^yGm=5 zEHHew-NZ|8?5K1aIEOT47Cdo%4WeT`E>Qu-UrLu0X)z^SKqBS|f3$g>FjzEofz`8G zJMcwtf0?H#kgYdiZv}T7`bJy;M_r1FIv~2A4ypgko#KtwDe$zE-kp&)-*pJR40n2V zYt<3dBzC-d8II2#gMpoIr&fV;H#t`MQGfS)&W|1UN59<8b@rUU`|ghtGnq3f|K4`~ z?wif~Z?+G3icTGAf4Q&~7x2Cd*llHd*ZomS=FZc^#n;#)v-s$Fh|{Z0-5053ls~=x`*;tG=tOhmTmNUz25%ruKItk*Q@@&zq|kRZ~eXP zet&m&e|NwCx88QI-+THu(7WfpHhwu5ko~Q9=eDYy`-^;#5GvHM3LgXjKql}A#$u8W zBTTz^3de{Kf50at>O$qEI{=z5j3iV=20-BH5HU;*(+qmi_V-agOfVOWrik+}#lzSG zGl3bSS2G`uNfaoabj{x(OfyW-U_084RIJVlk|~Z6;b;I}AN>+;N4=&JBx>M@;~J|$En=%PD8=`_?cf27`0#*ooxq;4SB?@j~Fj18@LRJf4V4B>&w4c`6tTN0193C{4kLGBC_>nqV>pbjal@ zJ;W&%Gmzm7rI;WdOsFEb>6z|MBs&w}ShmrSlQG1t2nc1Vh+m9@b2Pi8Ea8J7v}_Op zbb*NAe?bURI+p)4+lwId@d-k}1w(KeX2lynf!vi8eul%YYe(IiF^8uKnkBG0;5?pjiPhTqL#d=AGboI(P}h;b!W zyFsv_rt({Jya~b(6n~2e(hm{P5wP}V9tCcBrF{k)z-qXyaAbidVAYBipp?!M$cJN^ zf1=+W&+ll0vXsswow`b@cQ+MP#RPLpO+^b5qb*5xoB*?l0Z0fxqM#huf~hH3PGeb~rbjfzv1;ty1c47_Qpv05#pM;Ls3>-gDTz@g)E{Q4H7Z|s zL;XLLfWk~cQcSFxts`2tHvq3jZ>Tt8h)ZVhu{=RBLxLA*s+^-TH8 zKvWl0weL8P+Augrvn_yRe>Js|13sa7n#f8FUaBE{^Y$GWLXP%#RjY`^G(kxO4#>=+ z0AfgFm5k8~ESs7jh9dAvwxT6tI+GpH#EKb85D^%rTs1(&1Tg?(soVpGQYBlB`ly>uuQNmd6f6_LyF;&Z6!6F}iq745jC0TkzlLJ#4vAI(~Kz`1=``08B^9^9;YAQ%sH>vGW=eJk6)2DDs5XPe%t5q*F)Ajv7dK z<$;7(OMoov3_!nEgQ>lB4v*fy7dXX#Dk2^s7E8T$thBs$ssuPZdJpolOk-p7ff*;j zruMa4b}I^xF%g><5$$2VBrbCR*I83Lu<6UJPR}9G8D=Z5e_C^^1pv4;ov8^zpm)!5 zd9i6gE(l82nV-M__LA%_-sz^Bt$SCgq1!sRKn#wN5|w^FbaDpZhrJpBhG+sWFlB0T zDxEWD2ym1^sfPkJk-|Jv(?kDA=%o~qx|*66_J>0T?NLlGNr^8P$_}DC_Ag8ldU>Lq zsRm%D*9+ttfAbu3maDD6k9lHxt5(x$hAUHEs6VAlIjR@Dg*=HZ8Nw_X)`dv39B9IE zipjwR#Hm8h=rC3NV*&-3=3D?S>=IX0fg>T869Ui$j)l>XT%cBBG&T;e-V^9HZ>CfW z5@zAc$)ff>xPu^ILm2CEl7B^7os#Xn8fpIkqhTo)f0DY}@s9_G&W;1Tm22unRWi%{ zpHV)5fnYZ|8P5!3@N8atiXl!=oHHzDt7ur<)alCg#Qreb0K*U#{|MCDkh{+WWemk? z{fAr<+POM^(T}$KdNXU)8iuA+YR8zU+`S0AA_9@b=oz_TvSKgWMjy?O0Y~ft$LJuA zX-idJ~v8;S>!~6a4FZN6l){|=7tJTfz%ri?rg|&^(4Esov3Dw@0uC`CAu5wkqbJN zf4!sUh$xcS6mW3#%F`jeMXiA}9;kB5pV8#Nf2aD1>T8)qiNDA5YuIgmMw8NRAVC;o zK|^h09|!<&OhD*LowW`?M`G47AdeWPa&jD|kn=ZY=p=N#3##TWIYZ+KN^-e^g;qGJ z$!fuc8AE8Qx!UnVC`MgvTzUNJQlKr1f} ze<=~@O6`vi(v)6uFr&HTTY7=mCBs77Tzk#VqKU8VYOMmSZ{tlzH3IU``Vj3tlIs-DOv^$`L9pC~70x)H}I1 zlhVR9Ulmi+4u~nFjb@M}%32a^0LaFCe|}BK#>$Y)qd*px6Db5?sK+WyFjEPgE)ual zhEUvy$b}^?A{UrZbvX{$-JmN%yj~)5QJ8Y<0pRO@pZEI@?pP!GnLDoE{t=zBoBK{rSbePhUO1-)8;&f2^UF z1gq^&%7ocMG+D?R=O`C^hcBlvvVUkOw$=iG$I8@_(ivX zx!EKa8zt}6;$xN}x$&W+N)^d{?r+P4g@$Dr#;`?Um9c*DYE?|}6bog@vgd_%@wLTl zZ{sWi((e0Qj}eE3f-l`HwmD5R{b z>OPMvMYr_P2j&2#oR;PzF+s|>ZslJAt5AcR>=C?U?HKmZBtaP>iIuSNe}r;GjA~dM za6B#~?t2gK&GPWOVFf8n#ZG1jGhEmp!-8MBiE1F^NQ4*t?ncp`6vvzXp~_=N%6~3& ztUa_q$Z1oe)OMEhxIP~DNTUL_L@DrDNU`xov8g|%snl;YeN;MgQwD)%*vfuP+n=@9 zni3fQWH3fYh+(ym%k6gaf66qsN)*h^9)SIxI=Z9@I#Is)Rf7TqAKc8_D|wJ}JSIw& zkpR0J@?UPiHfk_#yLNRKG&S;+`*=DvA1z*-6F_Ob#jG zg2_%-DE+~f16&mZaar;Xs&3aKDP-K8nEKs}p_NFT!!+jtu~$bO4-{N-^XbE!v(b+%VSh%m z+6mqVH*;BgjsnX=f1-C|n2c>&XrXmC)DAzarW^YoMrqR46BnUEPgJUekCfalD!=2h zfUO$&CI2)Ra_5&~XpAXw%02PNC95f)+zhtmnrvyE)2pEli3?rR(!nfvPKVz>M1 z=wcrvIZQ(pW8?NXmN2>sGr#FJ%rL611P_80M$1U$pAbx>w6BS0o8gIRo4CBVuEJkV zZqLX06&W*$e?)-wAI1^Kq%J@$=kos+Mrqb=KIO1LW6zf7^1msu(403+>A1Vy+wOIH zyWQS)7f&UTlzRaW1y!@uz>>;zh)L+E*Zrvfz326!CpF|eq@xfLTXl1**ur9lxNC*~ z$1@dJ@;rh0A!5Ge^p~ak@676)6$;mq9)gp$~=;| z=w!unP)vBC*f=<&zy`agK6Q6^j?Qem*^bSkN; z@fsV5f3!mBcfXu8QC6fLUA3J^GnPl)P^8>8tVhG8ZPP#m8vtElE-)FFwke6zFPICt zJlkBybma?)*pnZaa3OVWI?}R0t^9zXOF#*Vz_B4~puI?*V`eu)NJQYE@Fq-WTQ&QY zr+jR=CoYAw)OtzjpQGn7Mv3yyk!n`30cJF3e}Lz5sYi*AzBso*NEL_z1KOTBco;9RKr6Jj03PYKo;-nR`s7Jr8zM-XrCc&c;GLu!M{3Wch%84W zf7mdGT*}qi9&V|9a7MFS{s760z$uQcQI#}GPFa~UGTqtI70kWq6_6GaCe_pT1)tjk`4ZFBt(b+ayle)7LSl@|+=8g>0 zAmrwjrloYmB+Hd(*RX#pGL)xV;hF`EIW$4>0Q}A=(c(og_$y8GDY9-39yfH`YuoPm=(2EoIh3=#Pc&!vGu8Rj3l)*7NtgD)G2O$C1O2`&l`t9SS8f(OB zqXu6tnQSV1z(**H4c5dd8Brua`YXZ)6(yt}nu0{3M4l_oDUfSw(F?Qev6w9nT4psq3 z8$`8=cO(^Nqvg8@*}O<;5$;m25{jcDTh6Oo$EI)!+Hwn;cGdaQC7f~Ze*~)LO9YH| z-|1dJ)=lxyEUHQ~NeFMT=@dD4*truPYL6>p`%phZovf~4G}-ryh6ljdCtqU5g~7H| zqN>>HoO{MrCTcZC`I#6ctO@6e4x<8@Aa-FmTO0KH(RQ>G?HZEtaoka=Zf03>q@q9GGS1QwB=KxgFjwDp@QZLb-qgE>^i=l+@o<6?4 zKm>7q#OM$;UiYyjK&FatB&6i$N{#naU94GitK8SQ*%8dJUHQ~GgV~X&C79(X3OrN_ z(#iq;Cn~fjvv-{!n2L!_lRHGIDC&cAnBe(6DvB)KrC?)mr<%IteKg& z36|VgGNwawIYFEey=+xio&|E8HbSk0S`sz}1#fo0I`#J3?lSH*Gl_)6SF@qVhHt>ODfoX`Y00>&KXDSySWZuQjf7GKyeOvZ>8 z9?wuwX&*GJ z8GFHfQdk4F45JN<1)43>S(SK)r-%zU&6+wR%FM5%LnXSUQT}aaMs}FvRD_s-pAL=~ zy_%^*8eorYqQFG*(6Z9}m@g<>0M!Jgg6Q1Owe|c9x;DIs(F839cet!;+R+N5dr!AL zfJtNH{^eaCfBQ1e=%wwqY?}dp@%y%*?Z&O`sJ(LAbm_LoQ*?9&QH)g`n#>ACCpr)gJl@w0d5rts<{XJa$~+k5}zrh2Y_WnPm9 zp+}nK9Ry3svcN+S%rBsQ1YI1Ky0%iKS<}sx3edJYEDfOCFc%dlbFZz`ZmqO@omEg= zZM1FC;O;KLC1`L+2oT)e-QAsy1$TFMhu}^K?$StbcX#c}f6l$Hw`*5F_QS5~-o3sx z$C$nLZK_I^$0>_|@NtW91*7xW9?Hb$&+qa-Z)Bd#)PI^aN#itc)Yx;C;kppU7?ylC zGTdDamrZk9ZB`2j()X)(hU(j8mNXwqib&-V^BIKA@Cr=d+x73s7sV|%?>q=Y`QEcI zF`)NMebdr<^trn+RR}`X?-?_<0p|o~o6J3hQ71m99F@3a?I>^*^R8#}e(e6o{Kn8y_+v(7*ntW&d_@8C_m0*I;@CFK&5!&_Dhj z_XV=SQ!l>?&whhH7>rP#2vhNSg)kl+%`n(SmERU0URBE^K?afPDeWPLuA0+ESZX`@ z%nrPLSARd*)$tX47j3g>oUKRJGm6ctP|=^kE2?+^7S@Ed{8=vWjWT&|RmO2Q(5LL2 zh*;vSLNkcPtaP>3OpD^}?%WvilUGCVU^T76>s7cnL2~;4v@~h{Dvp%Ve32JPDpY%v z{>yI_sE_11ztEcf*V2s27^hT+Mke5fe8-|wT7y*1@*;;#RMINrCD&!zVbaHbqHVpl8qJwfO3 z6>2}54CU|A=jtwLZ}c$A?qc>~VN`>sOHqE^v4dqM+p98i(=$^f<)W_uffG>L>0d|+ z2^(ZS7Hb}EEDVEE^z2kPI47-x(dK!vmi{yKpl1J@V2<-Ak_9Q-Y zdomT(GU6F4c^%4=o{2(;P30}2nWqhsY2LFcp#YUtw;lNDnEc5_IynWt=tof&nB%X1 zjD+uBu#Rx8m))iwA|VNRZ9ej+9tr|J(N1Lj7vQ{woBMI?yM4WhEFZgl@SvU1^H!4EH@JMPQb1s*VT(6N%d8=y?~{hV<#V>w5ZGWrg-kZCgxFE$|CEQy@5(^1_a zCG$5(Y@8Sl^6({aBERdz>aIiYttDBe+j-3D4LE8pQ)F6=)xz$FE0`bU*9 zl~ajexM2!g23r;G#CAEKFH#k$I?-XK#OCIqDi-v~DqW$Zk_z#$|0=ajY+eGWKxBxT zhL6X~6!H3yc9?nkTny-0@sHc(i7CMgBbl|NQNNn}mUCBl)}2)GWhr+ob-&7CO(iVp zlAn6!Es6qjdOzWB=Hb>ywfNCtZxcZa#_U(djT|S`r1Jr{mOX)@=wSY_#A*97De-UW zn9kl9Y+%dSH*&~0HXdgm*4uP@#|UpVc=H35iz?rc%6?f{NeIA1D99wLxA4d9Ym{We z-s`>+nUxjHszqfp0s5^tEc_w{Sh4ya zn?`08u?t*IR$lP7L@%_&IMwfgcL{Klz5H6ZpM~b$NJmBh=C;k$kwRmclrcV z{tQ#q@2|zn9i|SauN9XZGzjfb7>76+JvHb1cfzhQ2E1%l;(eRxaPzsyv9urc6K1AI zT(bm1eQF(!m>XL!(_fz;vqSA!A++JS7KZeNB z#GHkBRH5Q~l*+pGL;~8wv53JU)0qBS&hBuFp6(th)qKSW2^{=W?0>n|2;6;b+Y1h& zvndFhMs<{qn#GdU)78b8`>}%&FE!lcYUE5oL-hN&Q57GF*EMO zS(5Wp5c>cke&ad=9sT7$sPy%8j<7Zph;h#vWT>$&FrRrLkGU=1tzd#(7vEEkto{r8 z-raL#t$Hp(<_e3>y21JdqFZWm=@(+b(Y08QD&#?KM%rrLlAcA27)ztT;@~50YW7%s znu6OG>7z8R`Zyue*}@`QlRRfHM2I+WR!)YQ%_J}sySXDi4FaoPn@rZypsC#S4#k%0 zKG_Ris9!=62fA-E^Vl_)9OV>Y>O21f&Bt$(k6%c8Ol=5YPklL!Ok7TPYBcqQO*S%nsY=vxh# zIn^>bMt02;c39$%RRV8ag^IDU1EsG?_80VBCS{xsLb&ruHBqYR^Y$iW4FnZ*vv(B> z)$pFJbd4#(#T6YKXJ#Ys_i5;A;+gX;G`?`UrjUCGE4{SFcekpRk!oBjxu-;}v;uo~ z_#F(bq|AmPYRRKxmBP9NHtc~XTF1Z8VnmbSCzb!AGXHT?Wab8&^GZ)e{r0;vJl@VO zR)eBTpC32R%{GtnQjKRI`^Lw|0bxitkTO9U5TnBMY3&@iI#27fxeiS#N6KvdJu$$y zB}wt1^OAT7XOslD71kgK0xKy57k(y(R|Pa6+cl}NPz97H+|zG!Y-}VWZIWQTDwnEjW1q?(_2mK;y>-r zUHqX>bVnJ(YrLv{Rt#IM1y`)l-ni$DU3|;b^4ha?LyJ5Uo^eYyo*@J2CYJ;A@d+ir z5%>nMX1tU(R$;)_sJ^*J5KO4bQ-#;t6Oyp>_B{_h6>5!MWJ=oen-og#TysDDQ}jwR zdYc0YJe60~F33AtzR7c1{1hF?~sZRjak^|fQN^i`g$FMe?CIr$k`lVrq z3FB7HtmWpHDuSoItB?iNKzE=xOyM}Jy-o$=vlp{)_>G1oTL4Iv!PTi7N26E_HaX8y ziY0oxxFYTG4+oDdzmy4`YgKR&+J4Yxv6u9;NUs;YOU_UJj%+(~{df6Uw&K(!N0yn(r7D|hS9q)?!#06@HC8Y$vmD5tC<6YA{zE<-`N7q9r_8yBFg8k_WIakWqB zvWPT|(vd;Q&np0*bYc~W7Zd9<4&1wF3$g+VlZr!bMFv&r$0k2-$118hqL%$w$RSz8ZCc z7aG^G*iKfFeW)1A6oKgEPLYu6u!Zh`2jBRO!;PkXPLVK}jwyc_@1~l0(*Lz(K zXJ+ORa$hg>zFELvHjrr<@V3?~=OYi>l46&C)rwv+UFh7yH|+6LKUR*E&-+!;A64Nx z&$0tU07BZa`Z57?fS1)|a@d0v^!fHvp0BZ=rG?gB2fhj%aeU<9pv-f`6 z_;`MHbu<#Gub0#9`SLn&lo_Q+i<)UwE_c1Y zNPJ3ujB?+Xz<7}Xkc^iF`V&oS)ekroM46pWt@o&I1=9Ajl)pN7aYWG+jn9n!5HCLd zl^CI~QV7Ix)zMpL$(&RF(sFyDh6U=P%PCau{f#;qg-D&Gm3O~-o@-bXa^^ytdAoai zyVLW~lQ=^@6`{=P*}L<87m#*#B9&8E{nwHce$;@TG@sqa8uP7yBFIwnEyk>qAPf1 zl_|r_sDyO3tEP${rGeG>)*TxuNu#q9$08s!0arn_^#lLne4u!m1oYFWpj4{=RrF*W>m6`|V>oJ2C7&xlS4J zq0rO&LFOOn=MW>wPFLsyx6nu<_5-jsh={=cM1ww<2Rcl|-4c=7%G-`vE@5C)T}v~8 zkn|(*mzN3{QMzokYQF9qC0;YbdlZ_??Q4aJOoEb@jA@H)hkhyJw{VoT0Om;=1&&cy z#n+gjilKGtCl}V;9&B{^=Bv~}yOz?6#5~>g($LrA#`lzcmJ_@pcs2W(Hh?u@=3JvW z$y_hPr?0lS{MmUPq|`cNX`hrAe)}gW+K{3ze&H%cAcVj$SFNkDlqDM8_rJ9-}FvQ!T%x^2> z!dL!d8F3u#5!GqBr z7~Z-fM6t(Bm0ygOgzJoOwJ9Z6$sZ~{jmL{r!IZZ!`ec(?a)0C15|R0-?8rAD@IFDW z45i&x!DX#*ns#iZ$}gW(!j!ym9_CIWk-fn{<-qTam+v;EJR)XYxd3dNr)%JtX?P}7 zJ-|NPRu_TsxO2``!5iyq8?}dE{;sp^ruPpl?8l9`#>|^x%9UL`q(=?UHcWWvI>aMO zHO?KQSC?^hsvhISRiZu-KRhhc;&5(M_uE*{*78Ry9sD&tQp-HG47c8@hjv0i?b zGVGR42<~lEr;mO&ya(7@`9DK_hwiglp}kOD9ThAsmM>6iL3(y29l6uer^n)|7}-gh zy%B)Uj`V|bL8K$O)Yw?Cy!^J_=sB}r!YAXT-H7()R$J@py}AEW+<;E&4W_XlXR1`T z)eBCRIb61RtiaScaUh#o%zS;h#S{o8CJ|(CQ})M$LaxHwf? z1fQKHPGpDx?p`Zzqt87n8yD4!;LykS{+y*U{d!xE_&)4ormzjaQg{4+iXFM`_@j$) z-^N6c2VNQ`lk>fOTnyj)5zZQ4oX}{;eMfq|fvx~HHP1E!y@OP*w!F5-KX}N;97CRe z@?iImW4y)}m zZ6_S~8`-1U{iL^SZNI)OS!MW3(*V1oLZ;fzsM$b((Aquxm!I*VV%0y}T7%5|zspgv z%6!3=N8u)uBYR4mzo1w>5R?e}=Sd6Yw^U^iAUnFq?sa1+dO=uId5k6L*&z!-Pz_YO zfo2qdim(<@W20Cqwnv_z#I4mSk6eHjnT)?woC2j1AC0G#Gd?ErXH+)^Y62-&L*qZ?$j}kE6;&A1Uj&CMCip{iSaiG- zn;%k~Qetp;@538eGg4_9CnO&00K(xzYoiTT$M3VY_mC1{j48{h-dm!IR)l8Gy+^z+b4rkn?7g7M zw(2CNH}uqkBIwqD;)Ew(=U1H6qfAIm%b`0#8_OX_Bb^Eolv(>BXGNRBb-&uhxfb(2 z6Y3DoX;nLp#AePa?vU?Ki)?&1p=3|z&ue6jaA33Q>a_-&3S7^J{(6}X5@7o zIdmDNznp+sO!q9u0;CXwMc(^1wWMT*LyD=8rW;fsbeACfpSxc zArByZ(4hMCq9^84_jAR+$7|p09)?wAbJaGB!+t$3q4y#({eJckX8gBco2TSdhZAVL55BoK9eo!q-`lV$rt{$h@ zjh0udZA~4LzlGb@&P?iK!^Q5ld}LDy4m>AmPzQ7Sj*9Bb2g!5WR*cYyPkE6DT!#$7jA@FZRV<%#i1VJwFWMlW6bxfL=DWp{HW z_CU!4GP$x|RT}f5#doMF#G*)uyZI+|bq8UqaPB@FSv7|bL2zTR!cRD6dYl*rgbz=C zjZ&lPI2~hKRw8U^V0iuE+;nWShKfy0`;yn>>@Lj`J70Aler1qz7ht0EvzB{o$UJLt z^#C4p=J2=W@3BO|Tvw;FP53L9DPd!E*(Bi8(+>{p+UDq{sG8SkDf3qZm@gVgw z+xylVJK?`(eJ%}sFCxqZb^oP<1*z4^y$j>=-4=p-$ks;qHPm#g)ek%w`pnQ{JpQrB)z#JQXg#)EUPzw=y^#{4mq?#LRUR9{Kf8NJs>ar*xv7VpbE) zm~haa*}V8BY(|mz6pGc1YPdaX=doC=z35&XcaCaimCt;Hlz^ur2_EZ6^*`4_q2mJ6 z)1j%EkFZ|Rm$QM;PSr|l>07*4J5zABi9<3{UgSAox484|^mgF_EHK;>4Bkj^Q(crh z9l7Arw=(6KN!i7nI%F|dQ#!Bsex+^%d+kh8!?G`?c?G!;r*F!kICOZ1 zD>hu~`c0Rb%t5E9&;hn_HLcGF=YP^uzsOPv&7=W#2fRDJg2C@|cW+a0Pip*N<`};T z+pbXkJnNz4GxO3ANy9gMhoeH}YSi(b@3h=$3pDr zIF^$gfEK9aw<@aFK52K`Wk#KjN6{Q{N}iXWGPDOclj=R9U@7eFg0QEY6ssrh}cVi4=#FupM zQ6ojm&XjGZSh-tQb$Ah-g+WJ!@}cgx#9uRwE6SFhD+ary_d)w$D>vYUxdD7}0s1ic z=)VBp#t&dP+GwM@DR#%zosAqW3~BA1^&HknojSVN*x6MQypl?3C6d%;)Xu!#)he%A z3ydapbDV5L@mlyATki}9^w+QfeVWHMs;A1VmDMrNXt2Qk8_5qQe<4vUkAP~Tu(zh+ zqWR`4FMq)$sgrLs#vwfVs&E=6U|_>n%NuN4+v5k`67G(eulsDHZfVZu-U8pOp`(+a zARL(SC+)5W&HDx_{S6C=Xv}7gP0EUYatC4eTcKvPuKKqNmHu1aGFRfbSh>DUf=!84 z(vJ_slQ9y2k}Y~N)SR7nQ-=wc9-S;6<^}7CTb_DzwUq3;T{N{(VAfrT0~yYhd5i&J z5x)*nL83EK!;G?np|Lo6iCeN&4nxuUdm-C_N6szj#*?g^OQUy13tT4;FxtTZb?&pl z(stkO-KwkRX=#$w?c>t57=~QcQ0`P2E^JpD)b`Xg#}zy?nHYeUn#BRMlu`>rGJ#ze z78mL3TD}M@3S#gQa)jg=a2#tXzW21=n}6d}7g0R#0vdFP9S69__8rFxah8f-nKm(_ z-G70W#f$e-b%PZz1%r|PY7eX{rKznj)L^S=-}okwHIbQ5earDvx%-hrR+@htbgGcV zt~gYmWyszWW~IfVCKT3{x9KW!=?*6&L_)a_rG()8{p(#x7YYYHVBbwSOO*5>uK)FB z*L9|_EbG%@PRV!#)?|N0zVGp~H}9|luW0vxUs?4{N2V;U{|mlua(&w$!4EHHa!bEn ziUTDUJS(W%vu%=qw5=AP@w)Z^^AlZJ0vpOd*oi?%Vak#bf38)FlJNR!4?6ue;PLdj zXzQSsrLBYC+SY-Xr%%+Y&zTR^G+;~iVwjcE(NtUGnJ0PsS_ZItbIam2Shzc6?SsW# zG)sy^pGJaOBdSrjR~<#)Q=v4e!f8pYoOUV#Aw@Z}6x_ zvQisFc4R=SV(IEuYkRn@`9&@CISNMnAhkYvrSb<*Xemkq~l&0>?w)c!~&;roC8;SPb;_$Xn`vRO+z4egpT5Ewl&9d8Z zt6#iU0CSYrJJ9+R%c2KShBW^(KwldD|13`*lBXD`J37cNfd&E=4`gNMAGXr0Fo`Wx z*(4v4fNks}cj(pE4*`_Ib=J&yagHu7n%@(3*l^g>urYKlR~y~4Si#E;gFl(tzv{9Mx@mi~BrI4#fPb+<}z z^gkXbf{E}wr{C{u^)iCwzb}%`@Bec>dHdy<(q~Xo8a#ij%%i-;^bY`?vTd&@5Pe`_ z%Kz$`>g2aT5T_SsXQ!|bQLTaQ-!>PtdbwY!8{dCZMA_#Xfb+XeV=$17%Ab5pkR64uQt(pb!6x` zP<0`k4VX~kONHnFna+ZkmLN|ZsnGA=N{$M~${uQdVw;ap_!GDdw8$JEf=X}^8JW1( zJ7o&OZx{>1-yH*%rpwcezC@6v9*Y&fsBdVs7e+R_N*dXy@TsB~4x;Xw(YK%YmJ4PuTyFd)@1*G(Ns9ZEa^M9R?!acw)JAori1C!a1_g>%+E!9MK*-)Ceu_$`KqpSFZ z&Pl?a8ctrU8dhFx1(Zrha?ivj887=sQ{KC3?Bt9Ml0bVL31pCYF^iN)Zqigs6@;A~ z_GHOFTGmxyLyXGKbLu%0%aU;tXrfG-nq#6xz#Rv7mA>$U9$8kb!LgxF^K;7=>p}oO z#E^v)3wmDLcVw5#vPh+v%QEHem@}RTe6`0B2Ph4V?VKN@oE3bqBI^^39*QREFOhW6 z{+{hepc#nHe3&M5dyqdv3qnzY2}CjrI!`gLffT^35OJd-O|3MF7ZKMGV|i^m$XQN2 zHHiSx@Rz<554zJ54dO|Gqh0zvl;cm4Y|S!0uBIfcw3dx9ZE(`P|*p6cFUY z9NBNK(+OsM;BairXmqzP6#-n{he*8wSyZ5+B>b zhVWrdEh+s@g954Mu7k!;d){Sbi}eYFpNBgXHpBUrY6z(i&a{hdy|U5<47~XZM_RSI zl14j3H1`&!VSeCc2A9YJa`b{z9Mf%w?%zxOXCY%bLc$?pkNsRTxmfFJ-`BOas_faLxEI}rXL3uRlsAcjC`ZTDd_pI9@lSF!NjOhy^se%?KVk3cv~ zyH+k^Npa=EA^Tdeid8HGXO(ZY?a!?obf<4`hb3j_$K0akWLu~re%kwj?BBnV7T1t%=;>3pq%S)$BhI8GL65o!15VN}IubY6r~xgKj8p)W2P@U&%km z{I`estj{TYg#F<)I&|Y3EO^^kmjk_TtY{8_{nG5AiG?Z9=N`Ck>4L@(C=JqfIQvYH z+3|Dx*|rtyAlGGC)ms*l6cgnYG6Ef?LbV9`OgfA0x{-VW=3xc?*KQd!dJ)}AQFdU?!#!T(U z`q6&qC}0!ayIR?_sE%v0mqh(II|K`O<2LxQy979SO22ei`2q?7v>Z_*25<>-Y!EFN z%f?@y;v-bO3A(`(2pj)3M*asOP**o=W+Uh@a0uWOUh4_ewFrL#Ubi=&PVi}H4}BuPf~v^gZ>WQghxaP|5)2a^9xVf46y-Jk<*j~;-N%IUC1Z~ zPf__iO|!p2oWpWS!m$604mre}3ZVWGcd=hqcxewdVEt+3>TmMtqm7qtPFDX1rGFkm zR(T+MW(0zfUOs)+a-i}kepeLoP&y+(Ooc3^HAr5f0}+xSkdm|p6|9%1;!cVYvJ<|F zW?iIG(B(w4$QYtx4U-RJP+!b-&PU+Pn(S{Z^##38$(2yCq{A9LI?%uv8h90DIV1TQ zv?WiMJ-R0NAL)k_2&~XheNBLYZzE2DHE9SuQCAsC>|0*eWUW7{Xx>LK&=fWMNuA5{ z>uy0?I4&&XB`JYW0Dkl(1=^uxyYgJDtFAQi-92WUlc%E^4Y=L?<4lLRDKa5&qCe}Z8i9Bz-t`=B z$R$Iu;FK`V86Y&Ks}j<@_Q%Alpg(U2%(?lupTO}ZDmES4>;1)>f%YntgG2idQkQOK zpQYFRit)iBO_(l(ROmcbKt6_%M7XPSSb^9LHKh5MR=TAaIO~o2^#u)1XJHkj73w(s#>OcinYVN%9u$mTgByza>E)hnQ$mLH-^e! z!?5gad;MqRI))JHoIu~g62L5Wa`N;fU`vw8Q43dW?;l_R7X&hL1>?@l_*3Eek~}%52DH;FBwi|V^GCiH$5mz zr_2q&EFl1mvfp|*{@T`Ig2WH!Nf$wBqM^gk#J_xsg7=_S4dGb)b;JaZ+G^N&!Skp< z4{VeXRu-3|XY1Mv;T^=~f0|x{@qtQ@Kh~+4{e(5#1f@o-V<)MZxQ&m*8bGCJovyU% zQd9!LI6&|{u|cA4pgM5`mZl>SkuRi8NE} z%zD{~^g`nP%AujF_taRnqKeoMy==VPpMcS%9AoPW&Nf-KQ;cG0*5E3M=~@yW#4Zkq zJlV(W6YFF&J3GFb;(r3@UrSTII9mC*sF+qxkQUKfU>|Ft;48K{(1j>tBq)zWs+}H5 zvGWEY%f)|}LA-wcQNhKI6`T(8KYNCkW5?c^K_s$c-dv>>8BORZ<=#L=lXk1 zZDD!X@v(rn+4>`JHw|=e!}Usw5&>KSKuT*L(jhhkj*MU>1y^pSVe|y5VA0L#5}RXj z!POBLQ7lr(Uo1q11!1aIafUz$UZzIz^FKA+;YZ z*Ls?4Ni?J;QM1V?rkO`D-o=VCgy2evK(AMt-u=8C3xa^sowV7+k<_94S_j;tA7J4t zWk`82G^hX3wys9^vGBBVJl{kdp7KoH``fhPLJLdvwE-7ve$C}4OTudumiuRoa{}st zeH&ULx`Xb|kCG93%VU1>ZdYGB|IGPlWo__F_zd-ZV6Z6DS*6I*7Kr#Xl~CH3t-6rl zmmH44Q1J_4y%hcSbWb!yTxI|x>mzW9hn8wFVF$_zZ#MXnPH5^XH4bT!-G-I`leCpF zfmp)C>a?d|laU1HhFkP6huqDiv)1>oo=8GJr|Lbr4Dsu$m$n;OVbYn-bh(xEuXt8z zP?^#I=S92IqBm#kmj=WQC=VE?7|hFE1erNn`O}QU^ZHh;HdZN0XC}b7nRnnSO0SQ2-)fY$oZpOaYOlhh~H?Gl+ayGlstADil*wkRz@ZMo0^4mPuq-=!egnBQirGlCiyv{} zkciJAJd|4G!_0G`4Yy7Sd0D}_AmR(VGH@uSLjPZI3{fKk{y%z+;@lrnIQ$IKYlL4X z4mjHM6(k!>v9{=osDhRJZ5jGgbt4gV8@QAR-49@f3TuGgISQk+4An_8p0QixXf`~V z+<75GqhU?~BQkDLw7(zM8&^+(C|U;S!X%tgfBgKD2kO+1YnQmVQ{KpT9FM6v5uHktUA6j3vY=sG5)WQ<~5n7vo-H&A* zNRXy_qNxANVAd)B^$pR~l_7aWQj$&gXW+cNw3d)P@u&Dn=!f&v#O7m-ag3c>c-%^< zEA0OuQRVqR8m8~Tvo~$eKO$Jrg|o=m_-vrmGi!ims9_b9m|&g8HZ9aHF;wSbUOt&k zGjZ1bfjYd(_G7lO35l8pm_`ZUN8+|`O4}eTyilv@#mseq%+e>D$os|p_#;BS52e8S zK;3tH$*9Kiy}xu!|FRC$lB$U{^R^dfza>vKp&nu#GyH5x%d)jy{N5o{gCg=e7{$QE zO*`?C98u^NyZD*biw)LH&qy|}`W?wLj*sINx+LlghfK0Yc2?>5P6D-D3PXseuBvl( zYGRBV0oNWQv&B-khpV#Abwr(Yg=sL+mlx8y_c+)zRj8wRt=Fd3hUyJ-^6(zck8(B^ z=uXP9q3lyus?6MY-u`iIH)muo$b2+D604L17be05L1dNcSJ@X|}e zd8=(z-@$(Ecd2Z*Qw;05?X9?U8UCC89vDWeoQEnf%oZ3(w?!3S$IByt zP$>^3qBVtC*y#2)sxhsT?deb9e4J>he@gU#%vjH5?`>50+;M>Qil3zCnw2XgxkFMHI~7 zX5~8lpeg@3*bU?Pe$`nxv`+>oJ2AE;$Z~3QR8DA=gapVU^r-wnA~zBni7ovq!C1$P zn)!$FTdFz#LMDM;JmYdYtqsnTpjyEVnf23x+E=tgJ`!6r)@%-X5mJ7sNJ1Ym&d&)> zn1OA`K@wXQ3ME}`J*BSyQbP)7&XrEZec=pIDRQZk^bl1><(@E-msw zelcB7;#iYM@>Z@*kr{Wxy@-56r!iBU<_O93Kw?APa?3MJ7>Xl<;QcHm6_t!&0a+smsVVLZwcGT0VUumzRu8jbn|%pg%Zjv1m6&; zYDrDi&d+hWkDB3WP>M0j$%|0Br9}pQH)ilpOPp_0@0&ALrb6~6j8_)petE(lPCFr4 zvebAdN~n+SPC!5i$Ub+EkuMFU8+ABE3h$33!XI(Q&)#1j-;X&Hye-FkavbqVmDcb1 z)&?B_WkK6A``e}7K8Dq&P;U1SX#ikW0(z!${wY>mp_pgOz$Xeo_=XW+j zB~0L59EZ5SwojJ@Flsze~ZW;&Kt$M;V!-LAMr$H-!fVt}Dg?LJpyn9JP+hsdc*F?)S>dpW~8W z+s%KW5HBPoQvhx*b$SD7Ulq7ks+l`jIS7c!A+b=t*t<8>6~(T^jnar6U=ov}t)lVL zpdmTVUn^p<4rDj24n4p4-51UxQNh!=y9blGqE80F2>GpO;6Tc86Hb^md60)mw)jdG z3OdHX^hVZkhBLJBvb)?uQ-(Q_tk0GCqzyeLojm;W5x|T}*j(@}SeT|~qy6NTh)qw8 z8eF3Ls85#dDZpztL__sA*6kgoEM-*0lO25Z?bi>Z0PHKF?+sY*|OV&5W z?wwleZeWKKBY0&+c_Bb2T85}?3)bImg@4>JihO)rfL7~Lo6)c)*rWeGDb!0syyMVG zroSHaRRdH-6|RQCg<|R%x5%ET$qr+M&p52Isk#AKR|0Tza~E!$6TdULc(P=QuuWn5^7LGC-c zTrg<1dWR+BQ!N z?akx2$s(O%j0w_6h*!T?*Q1fCaXmmvN~U}Z&1u*$4Mm+*f8J6XY*gaC@o{5#yS&g- zVyreua|R02U-T@VJbYcAYZ`AE?_*)10JET{!hZo90R%ZZq#+6{*+#b@lOE|6Gl+GEHW6s}(V=TG8qk6YMW;^n0C9f6YLWj4X zenK$zElbluK;_l4_NKK2F?Xs`cj{b;=?w>&#Z#G)z@S2BcAuH`fkg7*;i(D>473Rv zIVIF2KB<0--1+w4wYIMJ_5Ee;oeRA9Gl%lsnVvQh)!TxS^&Ln2b7D$yejJ}VUMgJ; z1^-?%TS5z4!Ue-mr+&g+x$)hHeZnHm&=uB^|IfvZnpg#XUJg%tb+&%18$nHS0$}!_ zmbjv>tAiEo68s>W7GcDL@f!Y+glg1K6EG{4XSg(O;OG1xKbe~TaYir%E%W8mcz&`r<9h5cPmriWXc5H1Koa~E?O zlkYGs>|U+upzDin9fGYq2>JGf8R2P7u<{(JyuNlMu9b#JIRyv#`GU#rUsm3F!o|-3 z{ub$Qf1{@C?JqcG{AdhYmUMcKDaDY-?j5>QSa+Bda`hl)OGn0SCJ+ET9Ue{)p#_L+ zNMcT{v(faFvSDygj8H-B^gL%#i*GYa&mUZm<-%-19t*D^)i{?gFf1GfdLk-}vAzz_ zI5ak$rTn0jH<}8!3c75LdxVlMe4e(4rY%X$Rln*~s=0DXxni9#IHkFi-mpSY0B>H3 zA5-jbw{{k(cRQ9cNJsz!UJ0EL(X|I{>&6GHd z_)}0o7Nj7yJVz_^4#QsZxg0MecjvEXb$DSW^}0CW_u4I8eUu|0E3(kbUl?a=QZ}YH zGa{jY7g@vL?;7lY4So_%#C~&dZ$8XNVSMES-tH`F=MFdS?l*>BA-2Ngjt}8-si=YE z0J_mL+;B}op^00#xt?q?mSH#?#vbHx**8IuX5k3igSV5G|NNk`GD)5t(K~FHU+o12 zCk;=E@!3!9awK&iP@uAiO`qlR5^4o<(HmYbotvjqV#!p7l7vrzNAUY$7F)R)7YY2I z(qJUz#i?(N1DrNQ`m=81KZxO`+NYjN-X#=gA)`;J`kK~lew^A!rpzk992TCW6aJ2W z95|)nB5+_gOi03@EBKW@1%3Gu!%`JLU;b@Ag$-FxL)Ks?&#FW@mJz9O8N>4=db`hd ze0x47+6?^!#%6jt&b$wX4-ntIGN!9E&QJC(*ih|&di$-**2gBzj2Lc8^NAu7{`r_+ ztYs$mJ-E97K)}Y?@UfXO8P@73!->DKnSVCLYl2P3boCJ8GQeh257pMprN94_7HZ5_ zE%&z=KdkE)iQv>9BueN@`%4t`o1?(-Pkj179vA7;`N;+Uej+7&NYp2b+wgy$0gvA~ z32^>XSbUO*t`-Z3%G09p3H){Yb&s44Hz6djOem=yp!2PuOss9vychuPTc!PndR&~CG3h#~8%btzj&9Ck#)YD3D@*k_cmL8z>4!klvM+ff8 zDoW7)^+KtuP)O&oL1&x$HK*yJr;EGe6NyGU_@O=d4Orq|ItI9}vGf0k5zcuG| zEKXRJ{bqTHUcA>l)ARQ$Lzqz$=00@7$I7l0FDX zZNL@0^kA%M(MLXH1^>oj%`cygw|3A816Hvn8p#Al_@ zNdLIysI-QE{}W~!qQ1+;kav*cS5`MHT?Zg2Zn0qu9}*Mvvo_!@yR@Z81go#%S^+iE zyoHuE`;Kb3@6_rmV8s+nvYNyi+gIIgu=iS!ALxB6p`VHGXW28yB*=h98KR(o-q%gSg#=C94}SX}|( z{KWx13!2wU8`PT>dZI}Ymv`;T1~7#qr1kr=NaYabj?B+f_JAUCrbKAr9~gOEH7n6f z?U{BaGYo4EAb{N{o#}zvB;3H2+H~ypP4Xl)S;g(4OQ~f_SD^A{iY8?6r}z5mFWO8r1(|8M}7U{Hg*M7zqZ5=t${x< zcb;%5&l&Z+PtxDh=pv(GfP%6;J3I`m0#9=Ic)YwDneoYV+@A5ZAqZa|s=-*dna|&R z8Y8t7;7A(%mX&hgWIUUvL_BK5!HdR_saL>TKLfaB4YrPZiN?5pA#DcI z&70orqx}{Op~vpQddc7uGGd#(o(X~T_gpNO7cZAQ?2alE+NwDqn5JS{0inmNpI0Ao^wmz*4@DN+l~)_XNvmLeKPtUXs+k==^hmf^?w3v7nA6YWSQ{; zT>!@rn4+wYT4rtgN`mqu$~y`(Eq_fb;#1L@@iVWUfmI`QBpnsFCxshkZ z{hvpsz0OUbW&6K(Q04y*_n$u9?*C0ZJE$q@@QuU4Gy&ai+Xwipw*QTWqX^ZwmK3mL z|Mz+||F8YMr(6C1e?}e~rgUB?&qYLFQTWet=+C;F@Abkr^!iW1g|-)bd-(0)H3B#o zF{B8jpua`c>}L=gq8nxBf3hBv3aFsB`5ZWzj&plC*zr zHH7ETImKx7bL-2O&ewnMd_C!W9d&9TltA?5OUDUEdb{`dfA4!)Z=V&P73lxc(E7{T zSWN$W`|4Zu{@3>Y$7Y@#RLY4^Nv<%;Xha~3fsT0P<_i$zj1;?&o$E}qej+!gL%LBMW@O{iGeR=+A# z%orT;mGDjFkMM2GgdCXsKDP9*h&z`bgb{%}hpvmJ3HGCSfODD*Cvq^MBK*ZG95^kQ zwW?`zBcr)VvR1A2q?2+McEn6{hN9y@$*=clgk9vc9FEr~Vv6mS&jBDK5@4o%idUl&oZ$Q>iryD2!ruj^h)ZtfBp6dGAdp|KI=o9}B;~ zASovy^z=?2Kte6#A2rq1>t!WG9vBk#{8RjowIT% z<};gedMOeqi+*GYm5jHG8@DM>&c&kGCG^M1(wglJDu_y^6H&&7RUc#;+3E6TJctW zvY6cviCi*!Pm_yP3Dg7I)~QI_W@OePuuxGao2=E_@P6RuI9wkGTd^634Wl6RG1~^V zn1-u!umAi1{Qv*^zZR9KgI4T@f5aC=AdW9>bV`mPOQswvt{V$-xUWqXB(fz|LW3q3 zt25sY8V_kl7_HZuXgn|^iooLQ;PJqasGjmGR$XORR6!S;0w5Kc>}bsDTqc~m}af7mJ|$uXL?0JZw9d$*@;Eok>LgdxGS&*+e^i4A^BEd-&w}n%HC<|^ zG+S@S&!i$U>Mu4W{E|~CNn*8M<@(?;$g)I|Xn_>pux4($cdb+v6^n6u0O#f_6O<0RZ!ZbI&^CTmU2jJ;jFNFo{dR7F=md7 z?3{!$KC@_&2toG9c!nfHDQ6)eQQKD0jtt&8L1)vwcK5%WwJUZ`qvqD8$(M=9xMOWS zXL1Y(f11uh56qbA9l2{ADP~|X$x>f3Kf;n!+ub~Qj_9r2`4YQaQZi*5`_TSj^2pMadxgo+0fF6$%ss0RDC_AZB^?}e?mWde+VPw zd?*uPYs}t3$N7HqyqDkxpl$DV(Hxx~qYELZPf*A+cC?rV(|C#rcnM$YYLn%KYM@hO_(BNGQWzBS^94H zB7}Tj5Q!>kuy*bil+wWknSIy#G8+BSO8{jg2;uVq&;l%ZYMU| zCq8FWL*XT+abfWW=&EyARdlg(XV#+vg^Y6&$~HRIqX9J)aWn2+ECHi4LC0^ue?LJJ zA_N|hf}|DBKOWTlMr|Z@fDjyx-60b+xp=G{Nd75Uf?}Ew;eBpNIw3;hN!kip{cgYS zclZ3g?uULi=pO|8-?qEG!-IbRAFbV1Or|7m1ufN))^5vmr4_VZ7tw?QbP9KP+O6G| z&Oh7=TE+0?SqETF!GEW^-ioseR#`rjyD&XU zFtXdNOi4yMDo54vPruiTWyts?<}iVj7mLLZVdEj8X{rJHNO)r=jB5qH0M_DB92-p^ zunR@BDarDntvZ)%9?CwQsz}*`7@a+B?;V_Vf6flof9hx& zK{vSLpcG<>{`_bE(cg~V{PgnuBlH&(MJDtOc%=Eh?-^PVcnB3CChBplaz~5jERIPi zkK)*T1$a;|6D0R!C~|3oj0$}(T;CAdL7HVc9;%spq*ww7=7rtU$|biky&%rvx@(|h zNobS=uyJWZ&^d|O<$py?e~t;iq(XCmoak~9iV2=V1_0wjBFDtQvQ{-48<-8znZn|% zZ3?KCVpSQ@ z0X76+UUxY|n+Y2T5j*n-vonA-u3x%~*{sX|Q0!(d8-dGSv!k|qo?$JC+4D?Mbnndd zFg?@joMkf2^eo|2fAL_Ps>8<|JzBDx!l7e4%dHrtq*`Fi-IZP9SZz`BzvVbf>{#(` zA=g^kABIacRoghFKP+TQffI10NkbTv;Am}E4tJ*HsB?YdDjv-U4kLkR;^ikd`uK)U z3HpF({L431$iZn^mMA-_UMHqgqFBlQ_G?D?>^nh{i14rXe~$D@dyJ-(T<&`3Gj-5Q z!fnv#GhHyy#m?I3?}kOev0xzj8!bPQEIm`*Re*AS)w7%tkUCCuoUoj3*O=!9CYfB3 zPz|<>Xo7rS%2_jajf#F0(%A>jvh$dTF=G-$v=0nL6ps=nz^n8;CKFNdbvF`&KodNJ zg$F8*VHO8Xf0TU*5_V}dU=cyAHibj6{tzs%l5nh=gaF`Lf4lYP{&$h^yiyxW^uK%k!&?52?ftI}Jv-{0FGNOl zl0__pfQ;J7T{6X_@B~d5CxN%KlWVsMwV*Z;2qLqze`C%;sN6|$c!5X6*9LzsK=ak{ zyd6*$omkP%_fh`0`T!SK)6^SgaqJe%uimI`JhuX)Hq#+a3`3xf5$n3vq=~f;Ips?tI%yzYA0ycwligJ7jXV2S{4;4Qt z)%p%Pe@bSy3)UuSn2iWD-MffNxM?t%fW`!zl@&@MClN^`#j#1X0{&P=Noh|yvo&>7 znvr_Jl1ExCMJ37-l7xJgsSo%hu#j31S1ajz&hmJdx&uFuSt%>I22i>Y zhjhr)%KCAd``gRsA5Pv4-kiJ|e1G!p$D;FiBR_RtW-7>}dy7Z1yzJGA|BULEi z;gBX&&J+emz=;)fP?(^!R(?$7fe;z!z9;RKbJ+Eq>XQ>HIkB*btM@`tP4d_NHyKhO` zA)BNuA&I=b4mdeJiq}>W`$2WX{;2fj6HAC%xULUQi5tl65aqkwVi(b&J{q4q!EyZL ziMdNI@{6hG1c{V{bVy00DJ}SihWf_2^~}_p0oWd!#6ZjcCdM?1W(l6qf6#hiRrg7i z2ZA+8l8{t($xZJq*Ha(7M)qAtqG1g8y=n&Exe! zAJZvGh!CfootMQye@pjm2~eUDs}O;LT+w46K`S6Chn{ zexh)+u5c8uYa9hl1Sx!n3|j#nPrwToMt1F&-q9?H z1p4wt=!3<^pK-cd!@)!-LJ>=3%tlET$9c{p$te+NuTqkeh>BDR29*yuj?Zy;k)^gl zNhd@Nj?USH4yy092V4EUbrCu+cqkDxo)Qi&I3!Ahens#) zF%7NbbsMH8IM~ZE1h|7LrnVpEJL*?Es4C1*&%Rb{Qz}U`NbyW<Q17%<`FI z7K7%BMyS^n?n`zqSWF~Ahh2m0kh4URB+~1|E+fUgD1v2vpVK5lK@jx1{r&PRs6sIt z**2t1e|;m8tNriWl%>UmUf+h=Q6%Cs5JJ?k?k!&`eXU(Y{)oPO@y9mp>*vqtk8PusYQN3xI$yrjBG+MkStH%Q`_`CmpY5}Kw$H!D^Pd3# O0RR65PD644q6Glzi{gI( delta 55586 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

ytZF3A{LeGG{MmND6P{!uj>Azw7ng5keZ|!b};L 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 S{AU0F0RR8ZOu8@tq6Gk;Ly8dq diff --git a/helm/Chart.yaml b/helm/Chart.yaml index 9bff03983..296b7c1cf 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -3,7 +3,7 @@ name: parseable description: Helm chart for Parseable OSS version. Columnar data lake platform - purpose built for observability type: application version: 2.8.2 -appVersion: "v2.8.1" +appVersion: "v2.8.0" icon: "https://raw.githubusercontent.com/parseablehq/.github/main/images/logo.svg" maintainers: diff --git a/index.yaml b/index.yaml index 57878b7d7..fa5bccbcd 100644 --- a/index.yaml +++ b/index.yaml @@ -3,7 +3,7 @@ entries: pai: - apiVersion: v2 appVersion: 0.2.0 - created: "2026-06-02T15:41:36.741733+05:30" + created: "2026-06-08T13:03:45.109378+08:00" description: Parseable Auto Instrumentation (PAI) operator for Kubernetes digest: fd04518e8fc9e25d3fa876971a38b4c65005a207ba9440ae8ce981cd070646d9 home: https://github.com/parseablehq/pai @@ -21,7 +21,7 @@ entries: version: 0.2.0 - apiVersion: v2 appVersion: 0.1.0 - created: "2026-06-02T15:41:36.741579+05:30" + created: "2026-06-08T13:03:45.109233+08:00" description: Parseable Auto Instrumentation (PAI) operator for Kubernetes digest: 9443e75bef96a424fd5063ddd02cee18e6050207d73e505f66be467287c9f7f7 home: https://github.com/parseablehq/pai @@ -38,9 +38,34 @@ entries: - https://charts.parseable.com/helm-releases/pai-0.1.0.tgz version: 0.1.0 parseable: + - apiVersion: v2 + appVersion: v2.8.0 + created: "2026-06-08T13:03:45.221758+08:00" + dependencies: + - condition: vector.enabled + name: vector + repository: https://helm.vector.dev + version: 0.20.1 + - condition: fluent-bit.enabled + name: fluent-bit + repository: https://fluent.github.io/helm-charts + version: 0.48.0 + description: Helm chart for Parseable OSS version. Columnar data lake platform + - purpose built for observability + digest: 341c7b13072557693dcdd6f02206f057b371254c16fd593f9679890612bd0e83 + icon: https://raw.githubusercontent.com/parseablehq/.github/main/images/logo.svg + maintainers: + - email: hi@parseable.com + name: Parseable Team + url: https://parseable.com + name: parseable + type: application + urls: + - https://charts.parseable.com/helm-releases/parseable-2.8.2.tgz + version: 2.8.2 - apiVersion: v2 appVersion: v2.8.1 - created: "2026-06-02T15:41:36.852979+05:30" + created: "2026-06-08T13:03:45.22012+08:00" dependencies: - condition: vector.enabled name: vector @@ -65,7 +90,7 @@ entries: version: 2.8.1 - apiVersion: v2 appVersion: v2.8.0 - created: "2026-06-02T15:41:36.851325+05:30" + created: "2026-06-08T13:03:45.218159+08:00" dependencies: - condition: vector.enabled name: vector @@ -90,7 +115,7 @@ entries: version: 2.8.0 - apiVersion: v2 appVersion: v2.7.2 - created: "2026-06-02T15:41:36.849344+05:30" + created: "2026-06-08T13:03:45.216492+08:00" dependencies: - condition: vector.enabled name: vector @@ -115,7 +140,7 @@ entries: version: 2.7.2 - apiVersion: v2 appVersion: v2.7.1 - created: "2026-06-02T15:41:36.847684+05:30" + created: "2026-06-08T13:03:45.21479+08:00" dependencies: - condition: vector.enabled name: vector @@ -140,7 +165,7 @@ entries: version: 2.7.1 - apiVersion: v2 appVersion: v2.6.6 - created: "2026-06-02T15:41:36.846001+05:30" + created: "2026-06-08T13:03:45.212753+08:00" dependencies: - condition: vector.enabled name: vector @@ -165,7 +190,7 @@ entries: version: 2.6.6 - apiVersion: v2 appVersion: v2.6.5 - created: "2026-06-02T15:41:36.843907+05:30" + created: "2026-06-08T13:03:45.211094+08:00" dependencies: - condition: vector.enabled name: vector @@ -190,7 +215,7 @@ entries: version: 2.6.5 - apiVersion: v2 appVersion: v2.5.13 - created: "2026-06-02T15:41:36.833301+05:30" + created: "2026-06-08T13:03:45.200212+08:00" dependencies: - condition: vector.enabled name: vector @@ -215,7 +240,7 @@ entries: version: 2.5.13 - apiVersion: v2 appVersion: v2.5.7 - created: "2026-06-02T15:41:36.842289+05:30" + created: "2026-06-08T13:03:45.20911+08:00" dependencies: - condition: vector.enabled name: vector @@ -240,7 +265,7 @@ entries: version: 2.5.7 - apiVersion: v2 appVersion: v2.5.6 - created: "2026-06-02T15:41:36.840302+05:30" + created: "2026-06-08T13:03:45.207505+08:00" dependencies: - condition: vector.enabled name: vector @@ -265,7 +290,7 @@ entries: version: 2.5.6 - apiVersion: v2 appVersion: v2.5.5 - created: "2026-06-02T15:41:36.838625+05:30" + created: "2026-06-08T13:03:45.205892+08:00" dependencies: - condition: vector.enabled name: vector @@ -290,7 +315,7 @@ entries: version: 2.5.5 - apiVersion: v2 appVersion: v2.5.4 - created: "2026-06-02T15:41:36.836817+05:30" + created: "2026-06-08T13:03:45.203943+08:00" dependencies: - condition: vector.enabled name: vector @@ -314,7 +339,7 @@ entries: version: 2.5.4 - apiVersion: v2 appVersion: v2.5.3 - created: "2026-06-02T15:41:36.834987+05:30" + created: "2026-06-08T13:03:45.20227+08:00" dependencies: - condition: vector.enabled name: vector @@ -338,7 +363,7 @@ entries: version: 2.5.3 - apiVersion: v2 appVersion: v2.4.0 - created: "2026-06-02T15:41:36.831202+05:30" + created: "2026-06-08T13:03:45.198621+08:00" dependencies: - condition: vector.enabled name: vector @@ -362,7 +387,7 @@ entries: version: 2.4.0 - apiVersion: v2 appVersion: v2.3.3 - created: "2026-06-02T15:41:36.829534+05:30" + created: "2026-06-08T13:03:45.196995+08:00" dependencies: - condition: vector.enabled name: vector @@ -386,7 +411,7 @@ entries: version: 2.3.3 - apiVersion: v2 appVersion: v2.3.2 - created: "2026-06-02T15:41:36.827539+05:30" + created: "2026-06-08T13:03:45.195026+08:00" dependencies: - condition: vector.enabled name: vector @@ -410,7 +435,7 @@ entries: version: 2.3.2 - apiVersion: v2 appVersion: v2.3.1 - created: "2026-06-02T15:41:36.825881+05:30" + created: "2026-06-08T13:03:45.19334+08:00" dependencies: - condition: vector.enabled name: vector @@ -434,7 +459,7 @@ entries: version: 2.3.1 - apiVersion: v2 appVersion: v2.3.0 - created: "2026-06-02T15:41:36.823921+05:30" + created: "2026-06-08T13:03:45.191377+08:00" dependencies: - condition: vector.enabled name: vector @@ -458,7 +483,7 @@ entries: version: 2.3.0 - apiVersion: v2 appVersion: v2.1.0 - created: "2026-06-02T15:41:36.822233+05:30" + created: "2026-06-08T13:03:45.18972+08:00" dependencies: - condition: vector.enabled name: vector @@ -482,7 +507,7 @@ entries: version: 2.1.0 - apiVersion: v2 appVersion: v1.7.5 - created: "2026-06-02T15:41:36.82011+05:30" + created: "2026-06-08T13:03:45.187602+08:00" dependencies: - condition: vector.enabled name: vector @@ -506,7 +531,7 @@ entries: version: 2.0.0 - apiVersion: v2 appVersion: v1.7.5 - created: "2026-06-02T15:41:36.818459+05:30" + created: "2026-06-08T13:03:45.185956+08:00" dependencies: - condition: vector.enabled name: vector @@ -530,7 +555,7 @@ entries: version: 1.7.5 - apiVersion: v2 appVersion: v1.7.3 - created: "2026-06-02T15:41:36.816689+05:30" + created: "2026-06-08T13:03:45.184297+08:00" dependencies: - condition: vector.enabled name: vector @@ -554,7 +579,7 @@ entries: version: 1.7.3 - apiVersion: v2 appVersion: v1.7.2 - created: "2026-06-02T15:41:36.81465+05:30" + created: "2026-06-08T13:03:45.182331+08:00" dependencies: - condition: vector.enabled name: vector @@ -578,7 +603,7 @@ entries: version: 1.7.2 - apiVersion: v2 appVersion: v1.7.1 - created: "2026-06-02T15:41:36.813018+05:30" + created: "2026-06-08T13:03:45.18069+08:00" dependencies: - condition: vector.enabled name: vector @@ -602,7 +627,7 @@ entries: version: 1.7.1 - apiVersion: v2 appVersion: v1.7.0 - created: "2026-06-02T15:41:36.81103+05:30" + created: "2026-06-08T13:03:45.178724+08:00" dependencies: - condition: vector.enabled name: vector @@ -625,7 +650,7 @@ entries: version: 1.7.0 - apiVersion: v2 appVersion: v1.6.3 - created: "2026-06-02T15:41:36.809456+05:30" + created: "2026-06-08T13:03:45.17705+08:00" dependencies: - condition: vector.enabled name: vector @@ -648,7 +673,7 @@ entries: version: 1.6.8 - apiVersion: v2 appVersion: v1.6.3 - created: "2026-06-02T15:41:36.807551+05:30" + created: "2026-06-08T13:03:45.175007+08:00" dependencies: - condition: vector.enabled name: vector @@ -671,7 +696,7 @@ entries: version: 1.6.7 - apiVersion: v2 appVersion: v1.6.3 - created: "2026-06-02T15:41:36.805976+05:30" + created: "2026-06-08T13:03:45.173404+08:00" dependencies: - condition: vector.enabled name: vector @@ -694,7 +719,7 @@ entries: version: 1.6.6 - apiVersion: v2 appVersion: v1.6.3 - created: "2026-06-02T15:41:36.804415+05:30" + created: "2026-06-08T13:03:45.171846+08:00" dependencies: - condition: vector.enabled name: vector @@ -717,7 +742,7 @@ entries: version: 1.6.5 - apiVersion: v2 appVersion: v1.6.3 - created: "2026-06-02T15:41:36.802459+05:30" + created: "2026-06-08T13:03:45.169949+08:00" dependencies: - condition: vector.enabled name: vector @@ -740,7 +765,7 @@ entries: version: 1.6.4 - apiVersion: v2 appVersion: v1.6.3 - created: "2026-06-02T15:41:36.80092+05:30" + created: "2026-06-08T13:03:45.168407+08:00" dependencies: - condition: vector.enabled name: vector @@ -763,7 +788,7 @@ entries: version: 1.6.3 - apiVersion: v2 appVersion: v1.6.2 - created: "2026-06-02T15:41:36.799072+05:30" + created: "2026-06-08T13:03:45.166634+08:00" dependencies: - condition: vector.enabled name: vector @@ -786,7 +811,7 @@ entries: version: 1.6.2 - apiVersion: v2 appVersion: v1.6.1 - created: "2026-06-02T15:41:36.797603+05:30" + created: "2026-06-08T13:03:45.165135+08:00" dependencies: - condition: vector.enabled name: vector @@ -809,7 +834,7 @@ entries: version: 1.6.1 - apiVersion: v2 appVersion: v1.6.0 - created: "2026-06-02T15:41:36.796123+05:30" + created: "2026-06-08T13:03:45.163611+08:00" dependencies: - condition: vector.enabled name: vector @@ -832,7 +857,7 @@ entries: version: 1.6.0 - apiVersion: v2 appVersion: v1.5.5 - created: "2026-06-02T15:41:36.79425+05:30" + created: "2026-06-08T13:03:45.161756+08:00" dependencies: - condition: vector.enabled name: vector @@ -855,7 +880,7 @@ entries: version: 1.5.5 - apiVersion: v2 appVersion: v1.5.4 - created: "2026-06-02T15:41:36.79269+05:30" + created: "2026-06-08T13:03:45.160224+08:00" dependencies: - condition: vector.enabled name: vector @@ -878,7 +903,7 @@ entries: version: 1.5.4 - apiVersion: v2 appVersion: v1.5.3 - created: "2026-06-02T15:41:36.790796+05:30" + created: "2026-06-08T13:03:45.158678+08:00" dependencies: - condition: vector.enabled name: vector @@ -901,7 +926,7 @@ entries: version: 1.5.3 - apiVersion: v2 appVersion: v1.5.2 - created: "2026-06-02T15:41:36.789243+05:30" + created: "2026-06-08T13:03:45.156791+08:00" dependencies: - condition: vector.enabled name: vector @@ -924,7 +949,7 @@ entries: version: 1.5.2 - apiVersion: v2 appVersion: v1.5.1 - created: "2026-06-02T15:41:36.787684+05:30" + created: "2026-06-08T13:03:45.155314+08:00" dependencies: - condition: vector.enabled name: vector @@ -947,7 +972,7 @@ entries: version: 1.5.1 - apiVersion: v2 appVersion: v1.5.0 - created: "2026-06-02T15:41:36.785713+05:30" + created: "2026-06-08T13:03:45.153649+08:00" dependencies: - condition: vector.enabled name: vector @@ -970,7 +995,7 @@ entries: version: 1.5.0 - apiVersion: v2 appVersion: v1.4.0 - created: "2026-06-02T15:41:36.784211+05:30" + created: "2026-06-08T13:03:45.151903+08:00" dependencies: - condition: vector.enabled name: vector @@ -993,7 +1018,7 @@ entries: version: 1.4.1 - apiVersion: v2 appVersion: v1.4.0 - created: "2026-06-02T15:41:36.782454+05:30" + created: "2026-06-08T13:03:45.150445+08:00" dependencies: - condition: vector.enabled name: vector @@ -1016,7 +1041,7 @@ entries: version: 1.4.0 - apiVersion: v2 appVersion: v1.3.0 - created: "2026-06-02T15:41:36.781025+05:30" + created: "2026-06-08T13:03:45.148661+08:00" dependencies: - condition: vector.enabled name: vector @@ -1039,7 +1064,7 @@ entries: version: 1.3.1 - apiVersion: v2 appVersion: v1.3.0 - created: "2026-06-02T15:41:36.779589+05:30" + created: "2026-06-08T13:03:45.14726+08:00" dependencies: - condition: vector.enabled name: vector @@ -1062,7 +1087,7 @@ entries: version: 1.3.0 - apiVersion: v2 appVersion: v1.2.0 - created: "2026-06-02T15:41:36.777815+05:30" + created: "2026-06-08T13:03:45.145808+08:00" dependencies: - condition: vector.enabled name: vector @@ -1085,7 +1110,7 @@ entries: version: 1.2.0 - apiVersion: v2 appVersion: v1.1.0 - created: "2026-06-02T15:41:36.77639+05:30" + created: "2026-06-08T13:03:45.14406+08:00" dependencies: - condition: vector.enabled name: vector @@ -1108,7 +1133,7 @@ entries: version: 1.1.0 - apiVersion: v2 appVersion: v1.0.0 - created: "2026-06-02T15:41:36.774935+05:30" + created: "2026-06-08T13:03:45.142627+08:00" dependencies: - condition: vector.enabled name: vector @@ -1131,7 +1156,7 @@ entries: version: 1.0.0 - apiVersion: v2 appVersion: v0.9.0 - created: "2026-06-02T15:41:36.773203+05:30" + created: "2026-06-08T13:03:45.141059+08:00" dependencies: - condition: vector.enabled name: vector @@ -1154,7 +1179,7 @@ entries: version: 0.9.0 - apiVersion: v2 appVersion: v0.8.1 - created: "2026-06-02T15:41:36.771757+05:30" + created: "2026-06-08T13:03:45.139568+08:00" dependencies: - condition: vector.enabled name: vector @@ -1177,7 +1202,7 @@ entries: version: 0.8.1 - apiVersion: v2 appVersion: v0.8.0 - created: "2026-06-02T15:41:36.7698+05:30" + created: "2026-06-08T13:03:45.138147+08:00" dependencies: - condition: vector.enabled name: vector @@ -1200,7 +1225,7 @@ entries: version: 0.8.0 - apiVersion: v2 appVersion: v0.7.3 - created: "2026-06-02T15:41:36.768113+05:30" + created: "2026-06-08T13:03:45.136413+08:00" dependencies: - condition: vector.enabled name: vector @@ -1223,7 +1248,7 @@ entries: version: 0.7.3 - apiVersion: v2 appVersion: v0.7.2 - created: "2026-06-02T15:41:36.766742+05:30" + created: "2026-06-08T13:03:45.135012+08:00" dependencies: - condition: vector.enabled name: vector @@ -1246,7 +1271,7 @@ entries: version: 0.7.2 - apiVersion: v2 appVersion: v0.7.1 - created: "2026-06-02T15:41:36.764974+05:30" + created: "2026-06-08T13:03:45.133544+08:00" dependencies: - condition: vector.enabled name: vector @@ -1269,7 +1294,7 @@ entries: version: 0.7.1 - apiVersion: v2 appVersion: v0.7.0 - created: "2026-06-02T15:41:36.763574+05:30" + created: "2026-06-08T13:03:45.13154+08:00" dependencies: - condition: vector.enabled name: vector @@ -1292,7 +1317,7 @@ entries: version: 0.7.0 - apiVersion: v2 appVersion: v0.6.2 - created: "2026-06-02T15:41:36.762149+05:30" + created: "2026-06-08T13:03:45.130069+08:00" dependencies: - condition: vector.enabled name: vector @@ -1315,7 +1340,7 @@ entries: version: 0.6.2 - apiVersion: v2 appVersion: v0.6.1 - created: "2026-06-02T15:41:36.76023+05:30" + created: "2026-06-08T13:03:45.128581+08:00" dependencies: - condition: vector.enabled name: vector @@ -1338,7 +1363,7 @@ entries: version: 0.6.1 - apiVersion: v2 appVersion: v0.6.0 - created: "2026-06-02T15:41:36.758801+05:30" + created: "2026-06-08T13:03:45.126687+08:00" dependencies: - condition: vector.enabled name: vector @@ -1361,7 +1386,7 @@ entries: version: 0.6.0 - apiVersion: v2 appVersion: v0.5.1 - created: "2026-06-02T15:41:36.757287+05:30" + created: "2026-06-08T13:03:45.125232+08:00" dependencies: - condition: vector.enabled name: vector @@ -1384,7 +1409,7 @@ entries: version: 0.5.1 - apiVersion: v2 appVersion: v0.5.0 - created: "2026-06-02T15:41:36.755522+05:30" + created: "2026-06-08T13:03:45.123443+08:00" dependencies: - condition: vector.enabled name: vector @@ -1407,7 +1432,7 @@ entries: version: 0.5.0 - apiVersion: v2 appVersion: v0.4.4 - created: "2026-06-02T15:41:36.754126+05:30" + created: "2026-06-08T13:03:45.122052+08:00" dependencies: - condition: vector.enabled name: vector @@ -1430,7 +1455,7 @@ entries: version: 0.4.5 - apiVersion: v2 appVersion: v0.4.3 - created: "2026-06-02T15:41:36.752471+05:30" + created: "2026-06-08T13:03:45.120645+08:00" dependencies: - condition: vector.enabled name: vector @@ -1453,7 +1478,7 @@ entries: version: 0.4.4 - apiVersion: v2 appVersion: v0.4.2 - created: "2026-06-02T15:41:36.751115+05:30" + created: "2026-06-08T13:03:45.11885+08:00" dependencies: - condition: vector.enabled name: vector @@ -1476,7 +1501,7 @@ entries: version: 0.4.3 - apiVersion: v2 appVersion: v0.4.1 - created: "2026-06-02T15:41:36.74973+05:30" + created: "2026-06-08T13:03:45.117488+08:00" dependencies: - condition: vector.enabled name: vector @@ -1499,7 +1524,7 @@ entries: version: 0.4.2 - apiVersion: v2 appVersion: v0.4.0 - created: "2026-06-02T15:41:36.747936+05:30" + created: "2026-06-08T13:03:45.116037+08:00" dependencies: - condition: vector.enabled name: vector @@ -1522,7 +1547,7 @@ entries: version: 0.4.1 - apiVersion: v2 appVersion: v0.4.0 - created: "2026-06-02T15:41:36.746576+05:30" + created: "2026-06-08T13:03:45.113982+08:00" dependencies: - condition: vector.enabled name: vector @@ -1545,7 +1570,7 @@ entries: version: 0.4.0 - apiVersion: v2 appVersion: v0.3.1 - created: "2026-06-02T15:41:36.745115+05:30" + created: "2026-06-08T13:03:45.112619+08:00" dependencies: - condition: vector.enabled name: vector @@ -1568,7 +1593,7 @@ entries: version: 0.3.1 - apiVersion: v2 appVersion: v0.3.0 - created: "2026-06-02T15:41:36.743223+05:30" + created: "2026-06-08T13:03:45.111179+08:00" description: Helm chart for Parseable Server digest: ff30739229b727dc637f62fd4481c886a6080ce4556bae10cafe7642ddcfd937 name: parseable @@ -1578,7 +1603,7 @@ entries: version: 0.3.0 - apiVersion: v2 appVersion: v0.2.2 - created: "2026-06-02T15:41:36.743078+05:30" + created: "2026-06-08T13:03:45.111004+08:00" description: Helm chart for Parseable Server digest: 477d0dc2f0c07d4f4c32e105d4bdd70c71113add5c2a75ac5f1cb42aa0276db7 name: parseable @@ -1588,7 +1613,7 @@ entries: version: 0.2.2 - apiVersion: v2 appVersion: v0.2.1 - created: "2026-06-02T15:41:36.742943+05:30" + created: "2026-06-08T13:03:45.110512+08:00" description: Helm chart for Parseable Server digest: 84826fcd1b4c579f301569f43b0309c07e8082bad76f5cdd25f86e86ca2e8192 name: parseable @@ -1598,7 +1623,7 @@ entries: version: 0.2.1 - apiVersion: v2 appVersion: v0.2.0 - created: "2026-06-02T15:41:36.742815+05:30" + created: "2026-06-08T13:03:45.110391+08:00" description: Helm chart for Parseable Server digest: 7a759f7f9809f3935cba685e904c021a0b645f217f4e45b9be185900c467edff name: parseable @@ -1608,7 +1633,7 @@ entries: version: 0.2.0 - apiVersion: v2 appVersion: v0.1.1 - created: "2026-06-02T15:41:36.742698+05:30" + created: "2026-06-08T13:03:45.110273+08:00" description: Helm chart for Parseable Server digest: 37993cf392f662ec7b1fbfc9a2ba00ec906d98723e38f3c91ff1daca97c3d0b3 name: parseable @@ -1618,7 +1643,7 @@ entries: version: 0.1.1 - apiVersion: v2 appVersion: v0.1.0 - created: "2026-06-02T15:41:36.742578+05:30" + created: "2026-06-08T13:03:45.110158+08:00" description: Helm chart for Parseable Server digest: 1d580d072af8d6b1ebcbfee31c2e16c907d08db754780f913b5f0032b403789b name: parseable @@ -1628,7 +1653,7 @@ entries: version: 0.1.0 - apiVersion: v2 appVersion: v0.0.8 - created: "2026-06-02T15:41:36.742451+05:30" + created: "2026-06-08T13:03:45.110047+08:00" description: Helm chart for Parseable Server digest: c805254ffa634f96ecec448bcfff9973339aa9487dd8199b21b17b79a4de9345 name: parseable @@ -1638,7 +1663,7 @@ entries: version: 0.0.8 - apiVersion: v2 appVersion: v0.0.7 - created: "2026-06-02T15:41:36.742335+05:30" + created: "2026-06-08T13:03:45.109937+08:00" description: Helm chart for Parseable Server digest: c591f617ed1fe820bb2c72a4c976a78126f1d1095d552daa07c4700f46c4708a name: parseable @@ -1648,7 +1673,7 @@ entries: version: 0.0.7 - apiVersion: v2 appVersion: v0.0.6 - created: "2026-06-02T15:41:36.742206+05:30" + created: "2026-06-08T13:03:45.10983+08:00" description: Helm chart for Parseable Server digest: f9ae56a6fcd6a59e7bee0436200ddbedeb74ade6073deb435b8fcbaf08dda795 name: parseable @@ -1658,7 +1683,7 @@ entries: version: 0.0.6 - apiVersion: v2 appVersion: v0.0.5 - created: "2026-06-02T15:41:36.742088+05:30" + created: "2026-06-08T13:03:45.109722+08:00" description: Helm chart for Parseable Server digest: 4d6b08a064fba36e16feeb820b77e1e8e60fb6de48dbf7ec8410d03d10c26ad0 name: parseable @@ -1668,7 +1693,7 @@ entries: version: 0.0.5 - apiVersion: v2 appVersion: v0.0.2 - created: "2026-06-02T15:41:36.741969+05:30" + created: "2026-06-08T13:03:45.109605+08:00" description: Helm chart for Parseable Server digest: 38a0a3e4c498afbbcc76ebfcb9cb598fa2ca843a53cc93b3cb4f135b85c10844 name: parseable @@ -1678,7 +1703,7 @@ entries: version: 0.0.2 - apiVersion: v2 appVersion: v0.0.1 - created: "2026-06-02T15:41:36.74185+05:30" + created: "2026-06-08T13:03:45.109495+08:00" description: Helm chart for Parseable Server digest: 1f1142db092b9620ee38bb2294ccbb1c17f807b33bf56da43816af7fe89f301e name: parseable @@ -1689,7 +1714,7 @@ entries: parseable-enterprise: - apiVersion: v2 appVersion: v2.8.1 - created: "2026-06-02T15:41:36.866498+05:30" + created: "2026-06-08T13:03:45.235107+08:00" dependencies: - condition: vector.enabled name: vector @@ -1701,7 +1726,7 @@ entries: version: 0.48.0 description: Helm chart for Parseable Enterprise version - Needs a license to run. Please contact sales@parseable.com for more details. - digest: 6387485ebb5fd14ec60004f4f80dbeafe83583590db2544a3f789597ec381d46 + digest: 562470cd36439ec088aadee756e92efd543787e1e9172da183df827ef5e0657a icon: https://raw.githubusercontent.com/parseablehq/.github/main/images/logo.svg maintainers: - email: hi@parseable.com @@ -1714,7 +1739,7 @@ entries: version: 2.8.1 - apiVersion: v2 appVersion: v2.8.0 - created: "2026-06-02T15:41:36.864707+05:30" + created: "2026-06-08T13:03:45.233405+08:00" dependencies: - condition: vector.enabled name: vector @@ -1726,7 +1751,7 @@ entries: version: 0.48.0 description: Helm chart for Parseable Enterprise version - Needs a license to run. Please contact sales@parseable.com for more details. - digest: 3caa6c78936f31b1c49220645311f32ca29e322dae3396c841a6f2e17e476e55 + digest: 3d22ca3d869b257ddf42ee239abf2d1c8b029ab2715233e7401e56d7b5a85002 icon: https://raw.githubusercontent.com/parseablehq/.github/main/images/new-logo.svg maintainers: - email: hi@parseable.com @@ -1739,7 +1764,7 @@ entries: version: 2.8.0 - apiVersion: v2 appVersion: v2.7.3 - created: "2026-06-02T15:41:36.862583+05:30" + created: "2026-06-08T13:03:45.231589+08:00" dependencies: - condition: vector.enabled name: vector @@ -1764,7 +1789,7 @@ entries: version: 2.7.3 - apiVersion: v2 appVersion: v2.7.2 - created: "2026-06-02T15:41:36.860815+05:30" + created: "2026-06-08T13:03:45.22962+08:00" dependencies: - condition: vector.enabled name: vector @@ -1789,7 +1814,7 @@ entries: version: 2.7.2 - apiVersion: v2 appVersion: v2.7.1 - created: "2026-06-02T15:41:36.859+05:30" + created: "2026-06-08T13:03:45.227818+08:00" dependencies: - condition: vector.enabled name: vector @@ -1814,7 +1839,7 @@ entries: version: 2.7.1 - apiVersion: v2 appVersion: v2.6.6 - created: "2026-06-02T15:41:36.856853+05:30" + created: "2026-06-08T13:03:45.225599+08:00" dependencies: - condition: vector.enabled name: vector @@ -1839,7 +1864,7 @@ entries: version: 2.6.7 - apiVersion: v2 appVersion: v2.6.6 - created: "2026-06-02T15:41:36.85507+05:30" + created: "2026-06-08T13:03:45.223825+08:00" dependencies: - condition: vector.enabled name: vector @@ -1862,4 +1887,4 @@ entries: urls: - https://charts.parseable.com/helm-releases/parseable-enterprise-2.6.6.tgz version: 2.6.6 -generated: "2026-06-02T15:41:36.74136+05:30" +generated: "2026-06-08T13:03:45.109001+08:00" From abf47a0966b16f96b841125097fd568453a4eba6 Mon Sep 17 00:00:00 2001 From: parmesant Date: Mon, 8 Jun 2026 01:10:20 -0700 Subject: [PATCH 21/47] Further optimizations for ingestion flow (#1668) * Investigations for ingestion optimization Try to find more areas for optimization. - compress arrow files while writing (lz4_frame or zstd) - create new .part file based on size - one parquet per arrow file (converted in parallel) - separate runtimes to run ingestion tasks, sync and conversion tasks * add env vars for sort pruning and parquet creation * revert pruning env var --- src/parseable/staging/writer.rs | 82 +++++++++++++++--- src/parseable/streams.rs | 142 +++++++++++++++++++------------- src/storage/object_storage.rs | 39 +++++---- src/sync.rs | 4 + 4 files changed, 184 insertions(+), 83 deletions(-) diff --git a/src/parseable/staging/writer.rs b/src/parseable/staging/writer.rs index 8e8ee63ac..c37e5204d 100644 --- a/src/parseable/staging/writer.rs +++ b/src/parseable/staging/writer.rs @@ -26,14 +26,19 @@ use std::{ }; use arrow_array::RecordBatch; -use arrow_ipc::writer::StreamWriter; +use arrow_ipc::{ + CompressionType, + writer::{IpcWriteOptions, StreamWriter}, +}; use arrow_schema::Schema; use arrow_select::concat::concat_batches; use chrono::{TimeDelta, Utc}; +use datafusion::physical_plan::buffer::SizedMessage; use itertools::Itertools; use once_cell::sync::Lazy; use rand::distributions::{Alphanumeric, DistString}; use tracing::error; +use ulid::Ulid; use crate::{ parseable::{ARROW_FILE_EXTENSION, PART_FILE_EXTENSION}, @@ -53,9 +58,31 @@ static DISK_WRITE_BATCH_MAX_AGE_SECS: Lazy = Lazy::new(|| { } }); +const ARROW_FLUSH_SIZE_LIMIT_VAR: &str = "ARROW_FLUSH_SIZE_LIMIT"; +static ARROW_FLUSH_SIZE_LIMIT: Lazy = Lazy::new(|| { + if let Ok(var) = std::env::var(ARROW_FLUSH_SIZE_LIMIT_VAR) + && let Ok(var) = var.parse::() + { + var + } else { + 1024 * 1024 * 1024 * 10 + } +}); + +const ONE_PARQUET_PER_ARROW_VAR: &str = "ONE_PARQUET_PER_ARROW"; +static ONE_PARQUET_PER_ARROW: Lazy = Lazy::new(|| { + if let Ok(var) = std::env::var(ONE_PARQUET_PER_ARROW_VAR) + && let Ok(var) = var.parse::() + { + var + } else { + false + } +}); + #[derive(Default)] pub struct Writer { - pub mem: MemWriter<16384>, + pub mem: MemWriter<4096>, pub disk: HashMap, disk_pending: HashMap, } @@ -162,7 +189,7 @@ fn write_pending_disk_batch( let schema = pending.batches[0].schema(); let batch = concat_batches(&schema, pending.batches.iter())?; - match disk.get_mut(&filename) { + let s = match disk.get_mut(&filename) { Some(writer) => writer.write(&batch)?, None => { let range = pending.range.expect("pending disk batch must have range"); @@ -170,9 +197,13 @@ fn write_pending_disk_batch( fs::create_dir_all(parent)?; } let mut writer = DiskWriter::try_new(file_path, &schema, range)?; - writer.write(&batch)?; - disk.insert(filename, writer); + let s = writer.write(&batch)?; + disk.insert(filename.clone(), writer); + s } + }; + if s >= *ARROW_FLUSH_SIZE_LIMIT { + disk.remove(&filename); } Ok(()) @@ -203,6 +234,7 @@ pub struct DiskWriter { inner: StreamWriter>, path: PathBuf, range: TimeRange, + size: usize, } impl DiskWriter { @@ -219,9 +251,22 @@ impl DiskWriter { .truncate(true) .create(true) .open(&path)?; - let inner = StreamWriter::try_new_buffered(file, schema)?; - - Ok(Self { inner, path, range }) + let inner = StreamWriter::try_new_with_options( + BufWriter::new(file), + schema, + IpcWriteOptions::default() + .try_with_compression(Some(CompressionType::LZ4_FRAME)) + .unwrap(), + )?; + + let size = 0; + + Ok(Self { + inner, + path, + range, + size, + }) } pub fn is_current(&self) -> bool { @@ -230,8 +275,10 @@ impl DiskWriter { /// Write a single recordbatch into file #[cfg_attr(feature = "hotpath", hotpath::measure)] - pub fn write(&mut self, rb: &RecordBatch) -> Result<(), StagingError> { - self.inner.write(rb).map_err(StagingError::Arrow) + pub fn write(&mut self, rb: &RecordBatch) -> Result { + self.size += rb.size(); + self.inner.write(rb).map_err(StagingError::Arrow)?; + Ok(self.size) } } @@ -244,7 +291,15 @@ impl Drop for DiskWriter { } let mut arrow_path = self.path.to_owned(); - arrow_path.set_extension(ARROW_FILE_EXTENSION); + + // a rudimentary way to ensure one parquet per arrow file + if *ONE_PARQUET_PER_ARROW { + arrow_path.set_extension(Ulid::new().to_string()); + #[allow(clippy::incompatible_msrv)] + arrow_path.add_extension(ARROW_FILE_EXTENSION); + } else { + arrow_path.set_extension(ARROW_FILE_EXTENSION); + } // If file exists, append a random string before .date to avoid overwriting if arrow_path.exists() { @@ -260,6 +315,11 @@ impl Drop for DiskWriter { if let Err(err) = std::fs::rename(&self.path, &arrow_path) { error!("Couldn't rename file {:?}, error = {err}", self.path); } + tracing::info!( + "flushing {:?} due to drop with size {}\n", + self.path, + self.size + ); } } diff --git a/src/parseable/streams.rs b/src/parseable/streams.rs index 0c2b3eb3e..eb6d1a247 100644 --- a/src/parseable/streams.rs +++ b/src/parseable/streams.rs @@ -23,7 +23,7 @@ use arrow_schema::{Field, Fields, Schema}; use chrono::{NaiveDateTime, Timelike, Utc}; use derive_more::derive::{Deref, DerefMut}; use itertools::Itertools; -use once_cell::sync::Lazy; +use once_cell::sync::{Lazy, OnceCell}; use parquet::{ arrow::ArrowWriter, basic::Encoding, @@ -33,6 +33,7 @@ use parquet::{ }, schema::types::ColumnPath, }; +use rayon::iter::{IntoParallelIterator, ParallelIterator}; use relative_path::RelativePathBuf; use std::sync::PoisonError; use std::{ @@ -62,6 +63,7 @@ use crate::{ option::Mode, parseable::{DEFAULT_TENANT, PARSEABLE}, storage::{StreamType, object_storage::to_bytes, retention::Retention}, + sync::FLUSH_AND_CONVERT_RUNTIME, utils::time::{Minute, TimeRange}, }; @@ -70,6 +72,8 @@ use super::{ staging::{StagingError, reader::MergedReverseRecordReader, writer::Writer}, }; +static HOSTNAME: OnceCell = OnceCell::new(); + const DISK_WRITE_BATCH_ROWS_VAR: &str = "DISK_WRITE_BATCH_ROWS"; static DISK_WRITE_BATCH_ROWS: Lazy = Lazy::new(|| { if let Ok(var) = std::env::var(DISK_WRITE_BATCH_ROWS_VAR) @@ -209,12 +213,16 @@ impl Stream { parsed_timestamp: NaiveDateTime, custom_partition_values: &HashMap, ) -> String { - let mut hostname = hostname::get() - .unwrap_or_else(|_| std::ffi::OsString::from(&Ulid::new().to_string())) - .into_string() - .unwrap_or_else(|_| Ulid::new().to_string()) - .matches(|c: char| c.is_alphanumeric() || c == '-' || c == '_') - .collect::(); + let mut hostname = HOSTNAME + .get_or_init(|| { + hostname::get() + .unwrap_or_else(|_| std::ffi::OsString::from(&Ulid::new().to_string())) + .into_string() + .unwrap_or_else(|_| Ulid::new().to_string()) + .matches(|c: char| c.is_alphanumeric() || c == '-' || c == '_') + .collect::() + }) + .clone(); if let Some(id) = &self.ingestor_id { hostname.push_str(id); @@ -257,7 +265,7 @@ impl Stream { return vec![]; }; - //iterate through all the inprocess_ directories and collect all arrow files + // iterate through all the inprocess_ directories and collect all arrow files dir.filter_map(|entry| { let path = entry.ok()?.path(); if path.is_dir() @@ -587,6 +595,7 @@ impl Stream { self.stream_name, poisoned ))) })?; + // why clean Writer.MemWriter? writer.mem.clear(); writer.take_flushable_disk(forced) }; @@ -825,32 +834,52 @@ impl Stream { } self.update_staging_metrics(&staging_files, tenant_id); - for (parquet_path, arrow_files) in staging_files { - let record_reader = MergedReverseRecordReader::try_new(&arrow_files); - if record_reader.readers.is_empty() { - continue; - } - let merged_schema = record_reader.merged_schema(); - let props = self.parquet_writer_props(&merged_schema, time_partition, custom_partition); - schemas.push(merged_schema.clone()); - let schema = Arc::new(merged_schema); - - let part_path = parquet_path.with_extension("part"); - - if !self.write_parquet_part_file( - &part_path, - record_reader, - &schema, - &props, - time_partition, - )? { - continue; - } - if let Err(e) = std::fs::rename(&part_path, &parquet_path) { - error!("Couldn't rename part file: {part_path:?} -> {parquet_path:?}, error = {e}"); - } else { - self.cleanup_arrow_files_and_dir(&arrow_files, tenant_id); + let _schemas: Vec, StagingError>> = staging_files.into_par_iter().map( + |(parquet_path, arrow_files)| -> Result, StagingError> { + let record_reader = MergedReverseRecordReader::try_new(&arrow_files); + if record_reader.readers.is_empty() { + Ok(None) + } else { + let merged_schema = record_reader.merged_schema(); + let props = + self.parquet_writer_props(&merged_schema, time_partition, custom_partition); + // schemas.push(merged_schema.clone()); + let schema = Arc::new(merged_schema.clone()); + + let part_path = parquet_path.with_extension("part"); + + if !self.write_parquet_part_file( + &part_path, + record_reader, + &schema, + &props, + time_partition, + )? { + return Ok(None) + } + + if let Err(e) = std::fs::rename(&part_path, &parquet_path) { + error!( + "Couldn't rename part file: {part_path:?} -> {parquet_path:?}, error = {e}" + ); + } else { + self.cleanup_arrow_files_and_dir(&arrow_files, tenant_id); + } + Ok(Some(merged_schema)) + } + }, + ) + .collect(); + + for res in _schemas { + match res { + Ok(s) => { + if let Some(s) = s { + schemas.push(s) + } + } + Err(e) => return Err(e), } } if schemas.is_empty() { @@ -880,6 +909,8 @@ impl Stream { .open(part_path) .map_err(|_| StagingError::Create)?; let mut writer = ArrowWriter::try_new(&mut part_file, schema.clone(), Some(props.clone()))?; + + // does pruning help with query? let sort_for_metric_pruning = self.is_otel_metrics(); let time_partition_field = time_partition.map_or_else( || DEFAULT_TIMESTAMP_KEY.to_string(), @@ -1409,20 +1440,24 @@ impl Stream { // For regular cycles, use false to only flush non-current writers let forced = init_signal || shutdown_signal; self.flush(forced)?; - info!( - "Flushing stream ({}) took: {}s", - self.stream_name, - start_flush.elapsed().as_secs_f64() - ); + if self.get_stream_type().eq(&StreamType::UserDefined) { + info!( + "Flushing stream ({}) took: {}s", + self.stream_name, + start_flush.elapsed().as_secs_f64() + ); + } let start_convert = Instant::now(); self.prepare_parquet(init_signal, shutdown_signal, tenant_id)?; - info!( - "Converting arrows to parquet on stream ({}) took: {}s", - self.stream_name, - start_convert.elapsed().as_secs_f64() - ); + if self.get_stream_type().eq(&StreamType::UserDefined) { + info!( + "Converting arrows to parquet on stream ({}) took: {}s", + self.stream_name, + start_convert.elapsed().as_secs_f64() + ); + } Ok(()) } @@ -1512,15 +1547,6 @@ impl Streams { } else { vec![] } - - // self.read() - // .expect(LOCK_EXPECT) - // .get(&tenant_id) - // .and_then(|v|v.keys()) - // .map(f) - // .keys() - // .map(String::clone) - // .collect() } pub fn list_internal_streams(&self, tenant_id: &Option) -> Vec { @@ -1553,6 +1579,7 @@ impl Streams { vec![DEFAULT_TENANT.to_owned()] }; + let handle = FLUSH_AND_CONVERT_RUNTIME.handle(); for tenant_id in tenants { let guard = self.read().expect(LOCK_EXPECT); let streams: Vec> = if let Some(tenant_streams) = guard.get(&tenant_id) { @@ -1563,10 +1590,13 @@ impl Streams { for stream in streams { let tenant = tenant_id.clone(); let span = info_span!("stream_sync", stream_name = %stream.stream_name); - joinset.spawn_blocking(move || { - let _guard = span.enter(); - stream.flush_and_convert(init_signal, shutdown_signal, &Some(tenant)) - }); + joinset.spawn_blocking_on( + move || { + let _guard = span.enter(); + stream.flush_and_convert(init_signal, shutdown_signal, &Some(tenant)) + }, + handle, + ); } } } diff --git a/src/storage/object_storage.rs b/src/storage/object_storage.rs index a1831b5d8..9c9089faa 100644 --- a/src/storage/object_storage.rs +++ b/src/storage/object_storage.rs @@ -64,6 +64,7 @@ use crate::storage::field_stats::DATASET_STATS_STREAM_NAME; use crate::storage::field_stats::calculate_field_stats; use crate::storage::field_stats::extract_datetime_from_parquet_path_regex; use crate::sync::ACTIVE_OBJECT_STORE_SYNC_FILES; +use crate::sync::FLUSH_AND_CONVERT_RUNTIME; use super::{ ALERTS_ROOT_DIRECTORY, MANIFEST_FILE, ObjectStorageError, ObjectStoreFormat, @@ -1079,13 +1080,14 @@ async fn process_parquet_files( let parquet_paths: Vec = upload_context .stream .parquet_files() - .into_par_iter() + .into_iter() .filter(|p| !guard.contains(p)) .collect(); let mut ret = Vec::with_capacity(parquet_paths.len()); ret.clone_from(&parquet_paths); guard.extend(parquet_paths); + tracing::info!(ACTIVE_OBJECT_STORE_SYNC_FILES=?ACTIVE_OBJECT_STORE_SYNC_FILES); ret }; @@ -1153,20 +1155,23 @@ async fn spawn_parquet_upload_task( let stream_name = stream_name.to_string(); let schema = upload_context.schema.clone(); - - join_set.spawn(async move { - let _permit = semaphore.acquire().await.expect("semaphore is not closed"); - - upload_single_parquet_file( - store, - path, - stream_relative_path, - stream_name, - schema, - tenant_id, - ) - .await - }); + let handle = FLUSH_AND_CONVERT_RUNTIME.handle(); + join_set.spawn_on( + async move { + let _permit = semaphore.acquire().await.expect("semaphore is not closed"); + + upload_single_parquet_file( + store, + path, + stream_relative_path, + stream_name, + schema, + tenant_id, + ) + .await + }, + handle, + ); } /// Collects results from all upload tasks @@ -1293,6 +1298,7 @@ pub fn sync_all_streams(joinset: &mut JoinSet>) { } else { vec![None] }; + let handle = FLUSH_AND_CONVERT_RUNTIME.handle(); for tenant_id in tenants { for stream_name in PARSEABLE.streams.list(&tenant_id) { if let Ok(stream) = PARSEABLE.get_stream(&stream_name, &tenant_id) @@ -1304,7 +1310,7 @@ pub fn sync_all_streams(joinset: &mut JoinSet>) { let object_store = object_store.clone(); let id = tenant_id.clone(); let span = info_span!("stream_upload", stream_name = %stream_name); - joinset.spawn( + joinset.spawn_on( async move { let start = Instant::now(); let result = object_store @@ -1321,6 +1327,7 @@ pub fn sync_all_streams(joinset: &mut JoinSet>) { result } .instrument(span), + handle, ); } } diff --git a/src/sync.rs b/src/sync.rs index ebdc1d057..f6f20be48 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -26,12 +26,16 @@ use std::panic::AssertUnwindSafe; use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; +use tokio::runtime::Runtime; use tokio::sync::{RwLock, mpsc, oneshot}; use tokio::task::JoinSet; use tokio::time::{Duration, Instant, interval_at, sleep}; use tokio::{select, task}; use tracing::{Instrument, error, info, info_span, trace, warn}; +pub static FLUSH_AND_CONVERT_RUNTIME: Lazy = + Lazy::new(|| Runtime::new().expect("Runtime should be constructible")); + static LOCAL_SYNC_RUNNING: AtomicBool = AtomicBool::new(false); static REMOTE_SYNC_RUNNING: AtomicBool = AtomicBool::new(false); From 57dd9e9c2d0bc25ac4691a8f613ce40f897df5eb Mon Sep 17 00:00:00 2001 From: parmesant Date: Wed, 10 Jun 2026 00:39:05 -0700 Subject: [PATCH 22/47] Remove async (#1671) * Remove async - make MemWriter optional - remove unnecessary async * separate ingestion runtime * bump MSRV * make MemWriter optional * docker image versions bump --- Cargo.toml | 2 +- Dockerfile | 2 +- Dockerfile.debug | 2 +- Dockerfile.dev | 2 +- Dockerfile.kafka | 2 +- src/handlers/http/ingest.rs | 135 +++++------------ src/handlers/http/kinesis.rs | 2 +- src/handlers/http/modal/utils/ingest_utils.rs | 143 +++++++++++++++++- src/otel/otel_utils.rs | 2 +- src/parseable/staging/writer.rs | 50 ++++-- src/parseable/streams.rs | 26 +++- 11 files changed, 237 insertions(+), 131 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1acdf9748..88613953a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "parseable" version = "2.8.0" authors = ["Parseable Team "] edition = "2024" -rust-version = "1.88.0" +rust-version = "1.91.0" categories = ["logs", "observability", "metrics", "traces"] build = "build.rs" diff --git a/Dockerfile b/Dockerfile index 920390bc5..1be4b2fb7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ # along with this program. If not, see . # build stage -FROM rust:1.93.0-bookworm AS builder +FROM rust:1.96.0-bookworm AS builder LABEL org.opencontainers.image.title="Parseable" LABEL maintainer="Parseable Team " diff --git a/Dockerfile.debug b/Dockerfile.debug index 4a7802400..b6b4443e8 100644 --- a/Dockerfile.debug +++ b/Dockerfile.debug @@ -14,7 +14,7 @@ # along with this program. If not, see . # build stage -FROM docker.io/rust:1.93.0-bookworm AS builder +FROM docker.io/rust:1.96.0-bookworm AS builder LABEL org.opencontainers.image.title="Parseable" LABEL maintainer="Parseable Team " diff --git a/Dockerfile.dev b/Dockerfile.dev index d077819f1..d4f5abe7e 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -14,7 +14,7 @@ # along with this program. If not, see . # build stage -FROM rust:1.93.0-bookworm AS builder +FROM rust:1.96.0-bookworm AS builder LABEL org.opencontainers.image.title="Parseable" LABEL maintainer="Parseable Team " diff --git a/Dockerfile.kafka b/Dockerfile.kafka index e8bb88553..f5ced645a 100644 --- a/Dockerfile.kafka +++ b/Dockerfile.kafka @@ -14,7 +14,7 @@ # along with this program. If not, see . # build stage -FROM rust:1.93.0-bookworm AS builder +FROM rust:1.96.0-bookworm AS builder LABEL org.opencontainers.image.title="Parseable" LABEL maintainer="Parseable Team " diff --git a/src/handlers/http/ingest.rs b/src/handlers/http/ingest.rs index 41a7d6eb4..6f5543783 100644 --- a/src/handlers/http/ingest.rs +++ b/src/handlers/http/ingest.rs @@ -24,16 +24,19 @@ use actix_web::{HttpRequest, HttpResponse, http::header::ContentType}; use arrow_array::RecordBatch; use bytes::Bytes; use chrono::Utc; +use tokio::sync::oneshot; use tracing::error; use crate::event::error::EventError; use crate::event::format::known_schema::{self, KNOWN_SCHEMA_LIST}; use crate::event::format::{self, EventFormat, LogSource, LogSourceEntry}; use crate::event::{self, FORMAT_KEY, USER_AGENT_KEY}; -use crate::handlers::http::modal::utils::ingest_utils::validate_stream_for_ingestion; +use crate::handlers::http::modal::utils::ingest_utils::{ + ingest_helper, process_otel_content, validate_stream_for_ingestion, +}; use crate::handlers::{ - CONTENT_TYPE_JSON, CONTENT_TYPE_PROTOBUF, DatasetTag, EXTRACT_LOG_KEY, LOG_SOURCE_KEY, - STREAM_NAME_HEADER_KEY, TELEMETRY_TYPE_KEY, TelemetryType, + DatasetTag, EXTRACT_LOG_KEY, LOG_SOURCE_KEY, STREAM_NAME_HEADER_KEY, TELEMETRY_TYPE_KEY, + TelemetryType, }; use crate::metadata::SchemaVersion; use crate::metastore::MetastoreError; @@ -81,10 +84,11 @@ pub async fn ingest( .and_then(|h| h.to_str().ok()) .map_or(TelemetryType::default(), TelemetryType::from); - let extract_log = req - .headers() - .get(EXTRACT_LOG_KEY) - .and_then(|h| h.to_str().ok()); + let extract_log = req.headers().get(EXTRACT_LOG_KEY).and_then(|h| { + h.to_str() + .ok() + .map_or_else(|| None, |h| Some(h.to_string())) + }); if matches!( log_source, @@ -97,7 +101,6 @@ pub async fn ingest( } let mut p_custom_fields = get_custom_fields_from_header(&req); - let mut json = json.into_inner(); let fields = match &log_source { @@ -105,7 +108,7 @@ pub async fn ingest( &mut json, &mut p_custom_fields, src, - extract_log, + extract_log.as_deref(), )?, _ => HashSet::new(), }; @@ -129,12 +132,13 @@ pub async fn ingest( e })?; - //if stream exists, fetch the stream log source - //return error if the stream log source is otel traces or otel metrics or otel logs + // if stream exists, fetch the stream log source + // return error if the stream log source is otel traces or otel metrics validate_stream_for_ingestion(&stream_name, &log_source, &tenant_id).map_err(|e| { error!("Ingestion failed for stream {stream_name}: {e}"); e })?; + let stream = PARSEABLE.get_stream(&stream_name, &tenant_id)?; PARSEABLE .add_update_log_source(&stream_name, log_source_entry, &tenant_id) @@ -144,22 +148,27 @@ pub async fn ingest( e })?; - if let Err(e) = flatten_and_push_logs( - json, - &stream_name, - &log_source, - &p_custom_fields, - None, - telemetry_type, - &tenant_id, - ) - .await - { - error!("Ingestion failed for stream {stream_name}: {e}"); - return Err(e); - } + let time_partition = stream.get_time_partition(); - Ok(HttpResponse::Ok().finish()) + let (s, r) = oneshot::channel(); + rayon::spawn(move || { + let res = ingest_helper( + stream_name, + tenant_id, + log_source, + telemetry_type, + p_custom_fields, + json, + time_partition, + ); + let _ = s.send(res); + }); + + if let Err(e) = r.await.map_err(|e| PostError::CustomError(e.to_string()))? { + Err(e) + } else { + Ok(HttpResponse::Ok().finish()) + } } pub async fn ingest_internal_stream( @@ -285,76 +294,6 @@ pub async fn setup_otel_stream( Ok((stream_name, log_source, log_source_entry, time_partition)) } -// Common content processing for OTEL ingestion -async fn process_otel_content( - req: &HttpRequest, - body: web::Bytes, - stream_name: &str, - log_source: &LogSource, - telemetry_type: TelemetryType, -) -> Result<(), PostError> { - let p_custom_fields = get_custom_fields_from_header(req); - - match req - .headers() - .get("Content-Type") - .and_then(|h| h.to_str().ok()) - { - Some(content_type) => { - let tenant_id = get_tenant_id_from_request(req); - if content_type == CONTENT_TYPE_JSON { - let json: serde_json::Value = match serde_json::from_slice(&body) { - Ok(v) => v, - Err(e) => { - error!( - "Ingestion failed for stream {stream_name}: malformed JSON in request body" - ); - return Err(PostError::SerdeError(e)); - } - }; - if let Err(e) = flatten_and_push_logs( - json, - stream_name, - log_source, - &p_custom_fields, - None, - telemetry_type, - &tenant_id, - ) - .await - { - error!("Ingestion failed for stream {stream_name}: {e}"); - return Err(e); - } - } else if content_type == CONTENT_TYPE_PROTOBUF { - error!( - "Ingestion failed for stream {stream_name}: Protobuf ingestion is not supported in Parseable OSS" - ); - return Err(PostError::Invalid(anyhow::anyhow!( - "Ingestion failed for stream {stream_name}: Protobuf ingestion is not supported in Parseable OSS" - ))); - } else { - error!( - "Ingestion failed for stream {stream_name}: Unsupported Content-Type: {content_type}. Expected application/json or application/x-protobuf" - ); - return Err(PostError::Invalid(anyhow::anyhow!( - "Ingestion failed for stream {stream_name}: Unsupported Content-Type: {content_type}. Expected application/json or application/x-protobuf" - ))); - } - } - None => { - error!( - "Ingestion failed for stream {stream_name}: Missing Content-Type header. Expected application/json or application/x-protobuf" - ); - return Err(PostError::Invalid(anyhow::anyhow!( - "Ingestion failed for stream {stream_name}: Missing Content-Type header. Expected application/json or application/x-protobuf" - ))); - } - } - - Ok(()) -} - // Handler for POST /v1/logs to ingest OTEL logs // ingests events by extracting stream name from header // creates if stream does not exist @@ -519,9 +458,7 @@ pub async fn post_event( None, TelemetryType::Logs, &tenant_id, - ) - .await - { + ) { error!("Ingestion failed for stream {stream_name}: {e}"); return Err(e); } diff --git a/src/handlers/http/kinesis.rs b/src/handlers/http/kinesis.rs index b8968fa14..6fc713773 100644 --- a/src/handlers/http/kinesis.rs +++ b/src/handlers/http/kinesis.rs @@ -59,7 +59,7 @@ struct Data { // "requestId": "b858288a-f5d8-4181-a746-3f3dd716be8a", // "timestamp": "1704964113659" // } -pub async fn flatten_kinesis_logs(message: Message) -> Result, anyhow::Error> { +pub fn flatten_kinesis_logs(message: Message) -> Result, anyhow::Error> { let mut vec_kinesis_json = Vec::new(); for record in message.records.iter() { diff --git a/src/handlers/http/modal/utils/ingest_utils.rs b/src/handlers/http/modal/utils/ingest_utils.rs index 0ecdb0486..e8e1ca74a 100644 --- a/src/handlers/http/modal/utils/ingest_utils.rs +++ b/src/handlers/http/modal/utils/ingest_utils.rs @@ -16,8 +16,8 @@ * */ -use actix_web::HttpRequest; use actix_web::http::header::USER_AGENT; +use actix_web::{HttpRequest, web}; use chrono::Utc; use opentelemetry_proto::tonic::{ logs::v1::LogsData, metrics::v1::MetricsData, trace::v1::TracesData, @@ -25,7 +25,8 @@ use opentelemetry_proto::tonic::{ use rayon::iter::{IntoParallelIterator, ParallelIterator}; use serde_json::Value; use std::collections::HashMap; -use tracing::{instrument, warn}; +use tokio::sync::oneshot; +use tracing::{error, instrument, warn}; use crate::{ event::{ @@ -33,7 +34,8 @@ use crate::{ format::{EventFormat, LogSource, json}, }, handlers::{ - EXTRACT_LOG_KEY, LOG_SOURCE_KEY, STREAM_NAME_HEADER_KEY, TelemetryType, + CONTENT_TYPE_JSON, CONTENT_TYPE_PROTOBUF, EXTRACT_LOG_KEY, LOG_SOURCE_KEY, + STREAM_NAME_HEADER_KEY, TelemetryType, http::{ ingest::PostError, kinesis::{Message, flatten_kinesis_logs}, @@ -42,13 +44,142 @@ use crate::{ otel::{logs::flatten_otel_logs, metrics::flatten_otel_metrics, traces::flatten_otel_traces}, parseable::{DEFAULT_TENANT, PARSEABLE}, storage::StreamType, - utils::json::{convert_array_to_object, flatten::convert_to_array}, + utils::{ + get_tenant_id_from_request, + json::{convert_array_to_object, flatten::convert_to_array}, + }, }; const IGNORE_HEADERS: [&str; 3] = [STREAM_NAME_HEADER_KEY, LOG_SOURCE_KEY, EXTRACT_LOG_KEY]; const MAX_CUSTOM_FIELDS: usize = 10; const MAX_FIELD_VALUE_LENGTH: usize = 100; +pub fn ingest_helper( + stream_name: String, + tenant_id: Option, + log_source: LogSource, + telemetry_type: TelemetryType, + p_custom_fields: HashMap, + json: Value, + time_partition: Option, +) -> Result<(), PostError> { + if let Err(e) = flatten_and_push_logs( + json, + &stream_name, + &log_source, + &p_custom_fields, + time_partition, + telemetry_type, + &tenant_id, + ) { + error!("Ingestion failed for stream {stream_name}: {e}"); + return Err(e); + } + + Ok(()) +} + +// Common content processing for OTEL ingestion +pub async fn process_otel_content( + req: &HttpRequest, + body: web::Bytes, + stream_name: &str, + log_source: &LogSource, + telemetry_type: TelemetryType, +) -> Result<(), PostError> { + let p_custom_fields = get_custom_fields_from_header(req); + let content_type = req + .headers() + .get("Content-Type") + .and_then(|h| h.to_str().ok()) + .map(str::to_owned); + let tenant_id = get_tenant_id_from_request(req); + let (s, r) = oneshot::channel(); + let stream_name = stream_name.to_string(); + let log_source = log_source.clone(); + + rayon::spawn(move || { + let r = handle_otel_ingestion( + body, + p_custom_fields, + stream_name, + log_source, + telemetry_type, + content_type, + tenant_id, + ); + let _ = s.send(r); + }); + + if let Err(e) = r.await.map_err(|e| PostError::CustomError(e.to_string()))? { + Err(e) + } else { + Ok(()) + } +} + +#[allow(clippy::too_many_arguments)] +fn handle_otel_ingestion( + body: web::Bytes, + p_custom_fields: HashMap, + stream_name: String, + log_source: LogSource, + telemetry_type: TelemetryType, + content_type: Option, + tenant_id: Option, +) -> Result<(), PostError> { + match content_type { + Some(content_type) => { + if content_type == CONTENT_TYPE_JSON { + let json: serde_json::Value = match serde_json::from_slice(&body) { + Ok(v) => v, + Err(e) => { + error!( + "Ingestion failed for stream {stream_name}: malformed JSON in request body" + ); + return Err(PostError::SerdeError(e)); + } + }; + if let Err(e) = flatten_and_push_logs( + json, + &stream_name, + &log_source, + &p_custom_fields, + None, + telemetry_type, + &tenant_id, + ) { + error!("Ingestion failed for stream {stream_name}: {e}"); + return Err(e); + } + } else if content_type == CONTENT_TYPE_PROTOBUF { + error!( + "Ingestion failed for stream {stream_name}: Protobuf ingestion is not supported in Parseable OSS" + ); + return Err(PostError::Invalid(anyhow::anyhow!( + "Ingestion failed for stream {stream_name}: Protobuf ingestion is not supported in Parseable OSS" + ))); + } else { + error!( + "Ingestion failed for stream {stream_name}: Unsupported Content-Type: {content_type}. Expected application/json or application/x-protobuf" + ); + return Err(PostError::Invalid(anyhow::anyhow!( + "Ingestion failed for stream {stream_name}: Unsupported Content-Type: {content_type}. Expected application/json or application/x-protobuf" + ))); + } + } + None => { + error!( + "Ingestion failed for stream {stream_name}: Missing Content-Type header. Expected application/json or application/x-protobuf" + ); + return Err(PostError::Invalid(anyhow::anyhow!( + "Ingestion failed for stream {stream_name}: Missing Content-Type header. Expected application/json or application/x-protobuf" + ))); + } + } + Ok(()) +} + #[instrument( name = "flatten_and_push_logs", level = "info", @@ -62,7 +193,7 @@ const MAX_FIELD_VALUE_LENGTH: usize = 100; ), fields(stream_name) )] -pub async fn flatten_and_push_logs( +pub fn flatten_and_push_logs( json: Value, stream_name: &str, log_source: &LogSource, @@ -79,7 +210,7 @@ pub async fn flatten_and_push_logs( LogSource::Kinesis => { //custom flattening required for Amazon Kinesis let message: Message = serde_json::from_value(json)?; - let flattened_kinesis_data = flatten_kinesis_logs(message).await?; + let flattened_kinesis_data = flatten_kinesis_logs(message)?; let record = convert_to_array(flattened_kinesis_data)?; push_logs( stream_name, diff --git a/src/otel/otel_utils.rs b/src/otel/otel_utils.rs index 6caec1f21..0662d7b77 100644 --- a/src/otel/otel_utils.rs +++ b/src/otel/otel_utils.rs @@ -140,7 +140,7 @@ pub fn collect_json_from_anyvalue(key: &str, value: AnyValue) -> Map, key: &str) -> Map { let mut value_json: Map = Map::with_capacity(1); diff --git a/src/parseable/staging/writer.rs b/src/parseable/staging/writer.rs index c37e5204d..bb1afa0f5 100644 --- a/src/parseable/staging/writer.rs +++ b/src/parseable/staging/writer.rs @@ -69,6 +69,17 @@ static ARROW_FLUSH_SIZE_LIMIT: Lazy = Lazy::new(|| { } }); +const ENABLE_MEMORY_STAGING_VAR: &str = "ENABLE_MEMORY_STAGING"; +pub static ENABLE_MEMORY_STAGING: Lazy = Lazy::new(|| { + if let Ok(var) = std::env::var(ENABLE_MEMORY_STAGING_VAR) + && let Ok(var) = var.parse::() + { + var + } else { + true + } +}); + const ONE_PARQUET_PER_ARROW_VAR: &str = "ONE_PARQUET_PER_ARROW"; static ONE_PARQUET_PER_ARROW: Lazy = Lazy::new(|| { if let Ok(var) = std::env::var(ONE_PARQUET_PER_ARROW_VAR) @@ -80,13 +91,28 @@ static ONE_PARQUET_PER_ARROW: Lazy = Lazy::new(|| { } }); -#[derive(Default)] +// #[derive(Default)] pub struct Writer { - pub mem: MemWriter<4096>, + pub mem: Option>, pub disk: HashMap, disk_pending: HashMap, } +impl Default for Writer { + fn default() -> Self { + let mem = if *ENABLE_MEMORY_STAGING { + Some(MemWriter::default()) + } else { + None + }; + Self { + mem, + disk: HashMap::default(), + disk_pending: HashMap::default(), + } + } +} + impl Writer { #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn push_disk( @@ -245,7 +271,15 @@ impl DiskWriter { range: TimeRange, ) -> Result { let mut path = path.into(); - path.set_extension(PART_FILE_EXTENSION); + + // a rudimentary way to ensure one parquet per arrow file + if *ONE_PARQUET_PER_ARROW { + path.set_extension(Ulid::new().to_string()); + path.add_extension(PART_FILE_EXTENSION); + } else { + path.set_extension(PART_FILE_EXTENSION); + } + let file = OpenOptions::new() .write(true) .truncate(true) @@ -292,15 +326,7 @@ impl Drop for DiskWriter { let mut arrow_path = self.path.to_owned(); - // a rudimentary way to ensure one parquet per arrow file - if *ONE_PARQUET_PER_ARROW { - arrow_path.set_extension(Ulid::new().to_string()); - #[allow(clippy::incompatible_msrv)] - arrow_path.add_extension(ARROW_FILE_EXTENSION); - } else { - arrow_path.set_extension(ARROW_FILE_EXTENSION); - } - + arrow_path.set_extension(ARROW_FILE_EXTENSION); // If file exists, append a random string before .date to avoid overwriting if arrow_path.exists() { let file_name = arrow_path.file_name().unwrap().to_string_lossy(); diff --git a/src/parseable/streams.rs b/src/parseable/streams.rs index eb6d1a247..d9a9ae1b8 100644 --- a/src/parseable/streams.rs +++ b/src/parseable/streams.rs @@ -202,7 +202,9 @@ impl Stream { guard.push_disk(filename, record, file_path, range, *DISK_WRITE_BATCH_ROWS)?; } - guard.mem.push(schema_key, record)?; + if let Some(mem) = guard.mem.as_mut() { + mem.push(schema_key, record)?; + } Ok(()) } @@ -558,18 +560,23 @@ impl Stream { &self, schema: &Arc, ) -> Result, StagingError> { - let writer = self.writer.lock().map_err(|poisoned| { + let mut writer = self.writer.lock().map_err(|poisoned| { StagingError::PoisonError(PoisonError::new(format!( "Writer lock poisoned while cloning record batches for stream {} - {}", self.stream_name, poisoned ))) })?; - writer.mem.recordbatch_cloned(schema) + if let Some(mem) = writer.mem.as_mut() { + mem.recordbatch_cloned(schema) + } else { + Ok(Vec::new()) + } } pub fn clear(&self) -> Result<(), StagingError> { - self.writer + if let Some(m) = self + .writer .lock() .map_err(|poisoned| { StagingError::PoisonError(PoisonError::new(format!( @@ -578,7 +585,10 @@ impl Stream { ))) })? .mem - .clear(); + .as_mut() + { + m.clear() + } Ok(()) } @@ -595,8 +605,10 @@ impl Stream { self.stream_name, poisoned ))) })?; - // why clean Writer.MemWriter? - writer.mem.clear(); + + if let Some(mem) = writer.mem.as_mut() { + mem.clear(); + } writer.take_flushable_disk(forced) }; pending_writes.flush_into(&mut stale_writers, &self.data_path)?; From dea2cb99111f4d30d1c9e6b558a2abefb4aa8deb Mon Sep 17 00:00:00 2001 From: Nitish Tiwari Date: Wed, 10 Jun 2026 22:25:42 +0530 Subject: [PATCH 23/47] helm: make terminationGracePeriodSeconds configurable for ingestor and querier (#1673) --------- Co-authored-by: AdheipSingh --- helm-releases/parseable-2.8.3.tgz | Bin 0 -> 52698 bytes helm-releases/parseable-enterprise-2.8.1.tgz | Bin 57255 -> 0 bytes helm-releases/parseable-enterprise-2.8.3.tgz | Bin 0 -> 56798 bytes helm/Chart.yaml | 2 +- helm/templates/ingestor-statefulset.yaml | 2 +- helm/templates/querier-statefulset.yaml | 2 +- helm/values.yaml | 2 + index.yaml | 232 +++++++++++-------- 8 files changed, 146 insertions(+), 94 deletions(-) create mode 100644 helm-releases/parseable-2.8.3.tgz delete mode 100644 helm-releases/parseable-enterprise-2.8.1.tgz create mode 100644 helm-releases/parseable-enterprise-2.8.3.tgz diff --git a/helm-releases/parseable-2.8.3.tgz b/helm-releases/parseable-2.8.3.tgz new file mode 100644 index 0000000000000000000000000000000000000000..7d1d5b28916100adc6927d932ab25e696f801fe1 GIT binary patch literal 52698 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

`;_K8cuEtOnS`sb*1j8QDTbA*`H*#Zla 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 literal 0 HcmV?d00001 diff --git a/helm-releases/parseable-enterprise-2.8.1.tgz b/helm-releases/parseable-enterprise-2.8.1.tgz deleted file mode 100644 index 51d7f2123448e782f0481b1f4fe5a0f9657986b6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 57255 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�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 O0RR65PD644q6GkW-{m&| diff --git a/helm-releases/parseable-enterprise-2.8.3.tgz b/helm-releases/parseable-enterprise-2.8.3.tgz new file mode 100644 index 0000000000000000000000000000000000000000..b42b9823cf45e6445f84ec4b66426a17a15c7d5e GIT binary patch literal 56798 zcmV)}KzqL*iwG0|00000|0w_~VMtOiV@ORlOnEsqVl!4SWK%V1T2nbTPgYhoO;>Dc zVQyr3R8em|NM&qo0PMZnavQm_D7aoveFYBXj+Wf6;zbv`xt6;(P0_ZFb>R}J?$z7w zO@mb+iB_>tT|h}<+m1MK9_DFc;ymn_IUg|}Fc0%J`v>M1CUC1l-AL+U*}b-Et!Rt5 zBoYZAkw|1FLKsm5$3C)Az)%!Mn4)@j1|#My;N1VqU3`s3qp`QMBmQkP8u@>ltw!@N z&D~bBwcTv(>^%KTqq)1?X#52qvL3pi8EX7Sj;M6_+Yd~(xl zyF0R@)QxZ`+kK7vIS@s`ghZgPmkH=4%R~bJHaJGeqW}UQyC|RtFan}Da4Z1&K7thS z9x>=LK%tN5za)#!amif3pOXjy4>5>+>R8z2DCllP@KW_5rYPc&c&}VCuP1P3zt>ff z`W)h*j^}WSXgxrewoj(Sp%+t&XE2BZ6p3L(bBO&mnBgoo33byR4-w?aapb2sW$VZq zWvs+vix9N|3_~BgkdxL*$j+zNw)LeKz<=^@3IF$rdwwTvtl=F`3J8oTZ7M(v-IQwkB5=VATd z$Cr$M+X6s{4g8PnYE8=2OSgDloWW@u)TG2%6DeWr`+eeLchLq1lVigA5uzyIoM2-E z9Ae4Hg-b>RQN+Y>AD8N5;P)F2FG^sktx!V+4HSLf;liPXOK4 ze~o=ImVW~hfvHR74=xUhKiWxIO$;eDx+Iw3X&ebTsZ#8eb-EFP3;{MniO{KN7gHQe z1rfk`JVq|_fh!BE0)p#d*O1pVgmjZ`V;H%fx%5uzlHTtw^T z1=fT`@pLIDil-LPJ(M$39Ru2cMX@5r05L9d^8|n=U_oMV2?LSlq0lD_gOE4?5WMby zIq?wKoX0-Hp-ARi3yCQB4$KGx45MgELRTVuKm@%Z12a6GbuJ+G;TZdvEdZuG7tlBi zNyL!H8xeCuv|@omjq;v=1PhrD0Gv#4z{`UVpHvd3YDm0Jz;H+B8d-eh{vP89dHZpM zgXzGXAur}j=wKR!!hy)$QRptHj{sI$lTc>FGGu&fXn#?yRh=v zE}4fUKmltfJ zf_MR8VwF*YvKt^E9?JKfW5(0vK_-VbBR+~Wptrv!Rl*392(v}khm;=YPDeQy{yIo6 zqn%4)p1Q4ZLeU%t0;yLKbWtBgn0N!k57o2{wiAj47ipuXXM^GB@Z?n+T*4?wlKZ3X z>t6TG=&Zlr8TQ&>0)2{%+q@NSGk!3iVG2;-g#=6fKvCcUbb$g!`Okz6q}944pu|UD^Xy=YuXHHav5x==1pg&$ zh9W>wbb(z2HU*0i@C8I(;`bOu3qJ9-6j)U_rN5kAT|cG_MF;&PUC(os5FzN{08!eH z$k?dP<>^<5r3nCpkj>hl4nthOXx5X8W{Lb7+5ibFHfO(&=a^+u0C2;&4VsyxIhqS; zp=G3UUWDj1DmN9&jWC_(_QOq=1S}#x2z?kJPD#EU&Z}5yfpzA(Tf#6(R$c|OC}R~_ zM!|<1Be_J&xWoz`A-X0C`m0dPu!gjr;tG+uAGBb4%kW!zwGUZ6Qais@ajf!u;B zUrxEjq$rzi0I+1F%U4`sy{%epxye^pY58?lSSV#{thmBfR7+MDng9I|usGn7^?&^L ze+M|=!mSWo7y)C#W^#N03_LxRr4Cc*55?N!Fmj78oe;)U+5mqS>`!(toi3**M}I#Y z9d-UOI_e#roc=QE_fAKDKkJ?T(gw9>yU)J=etT#4`&O;2#-MZ5KkN-MRa#n|)F`jg zJ3T0`QA-dV;yEHQqhRwKp%932quxJyUGT$mAg5+;*nJHsNr>VRM1yY`KM}ad2V*qh zV!z~HQ+$Cah{LTsJ}Ict@ZhL-ayA?ddfh=A)b>iSPtjPX^y`!1@St}(IynBRcRb{D z%~je?`+JIL>~q}R8Op=ycDk>7=I|W7;FViX_nsBnlx2s*L)}JmXQu?U6lCvs$VVd? zgF?~H;lZn8(Q`g%KOYQVj|RhDzYS`f!PnF|P|Q03V-GXnlj)RCXA-3RQSf6vMe;0JgWenmIz5Dx zUXsX5(`9!$p2yb>jz};^0RtB>!n}nkw!`AFth%naO zFzddU1N#SqH>0yb=T(m{%5Lv?*ao%lccuPdoee9U0gUJ)F68q7P!=I5nHQU(06@(i{#4%DRn(?fGovoxDcnrK_;sRz39LaF8< zA2C!jvOI+YUsnyUmTy8TN5`1mc`F-g`hyTf6jO!*7fHPV=>ODJ)IeY#GAM{RU{I_r z4@aWgSZiXAlQXd>tMlcF7$FaNfCT3ETt>{sXnaZlm!Em!7%rv1t!7SZLOis&egK zvmMon9&SE@d{{k9&pDgaTNv~_Dl%q-*$kt|(bWw7R#C@6f?Nd!?+eh;I0)QByUnEC zRF;awPD~J_xo8bPz=~jw*m)E+^M7S;C1pXTv{Ny0E8WJ-x z1EhwK0(mRMCrliYKq=8=$uQ-bVtO2jgOE@x;t5FR3Y983QACy=`((VlaH=tm9=V(FA!?w*AQItv0yMHE8Z~d7> z#rhl%XCX|eO3G61Pt`a0N94#!IXXv+vMhc~Eu<$$jY3Ww@#$pFj4B1d*GP_)Lv0k@ zR(;JRNm@F2TGDMt_M|Ll9J}X;P6v%4$kqa!I0dDh1g~ew-+~|4EJ0)O{3Q`$|gIkxCLiVBiw78GwLd54li`g#4>= zaT@ow*_+#OOc!HvrE^KwUlC~HmBYVDTPk_}Vj z#$xf4fT1h3I-&2A%YK9}u#cvwM_uTv<){WG~sk3j{My)YCBp^J>uadt)jM#f@D zi~^h!MpMMpP^A~QVgQF#dHUKgn@aVTYhKYOXbsA8sW4wTdg?;*wn3 z3gLVeL67oOzONsmV9I9Y`9DEFt}1YjP`DqFFhq(P)Xas9L3ehfxooCqq6>L-1ps5> zEe7}x-Dgqb98S(*mjr=vhqme_;gvAh^T8Wl7j$!qJRo$7Z#pmEbVe^uPKJZwX{SFL z^iF^3oetWdHecAHQAhr_ptx;+-}t^&0~?^rcRrh<5DM!+;45n-+3c7QMp*Zf$gh zIbBj^ptSdCoxK%}yzX^Qhc9}a;ppIa*gO5Hb12lqOY5ZbnlG$j?-d`?h{S;xkueUQ zM4Y5c>K}Ff!KcDuzR9JH_KrI*4tt}{+3;j^^75rL?l?K^W#o*~nr4;L-k`_Hf?(uh zMA?#R@>=fb;CS@nm!Y_#DqAkO(w%=KrFUC9t!K}c_9?CObLW7gwY#*qpj$$FSvkS2 zvGZ*A>E4pse?RM;_4Y-Fj(USZ=as6rg6_JSxc`&YMsIYhd%L^a%i2_fmLlCdc`25^ zPnV6DysGM+3O2T?@WJ5Zup_x_@24K0OPqO*_@M7Em)*(?-fD6R%OZ0ItL2>3 z(6VZlTY(dyH>JP^FBU1EI_z*r{}`s#WjWAngTdes^a3|p$dr@7Ulh<9^txxK2g6@R z{nL}-N%w>!G#DHf@gEEhN8Qfo@T4nf#wBgU+&ZOx-9-_jbr;(F4?MvxKTSDqRI*3} zl-}v^;N?NLGwdw`C$~(&SPrA#84P|tIo(%VJX+XV zcT-C!3jKD%?az@ovg<41m0|NkkO28P&+4lsbWm>y?PXLL)tHGetKPZQO zHt3xmcaAv!6;hNPQxsKTVvLI{n+!{V(A(97CNaAaos-422AxFNJbiM6$}ABW}zO=1?E-eFURG z9MId=Msf^KU9IHFn<5s6bsSIzeW|9YhtNHTQ)JI!0H-LT!jGzMT_i`V!oeycK0h;e zrizjg@%^7kbnX+FUZPa^gD}jGE|}C4H?b9(#L#fD(5G7IF(Su<&Y=1`Lc&p$O5JNn zyd%*gP9+WfB=S&%yi)|dg)Fm=X%vfrdJ%imC6^d;9CWnxuC|by!>h9(vFYViqcL<# zwef2y?8}S@h4xYA#*7H%rdYU1wVd9_7;$vv`mtxK)r1;W6tjVC%M*>QFCT2RaYzXD zHB4#t8hKTJ@hVL!|7(f%zv>bqW5J&ewbBM_&G8jOQ2_m61Sb>hCh9i{l+V8x(1(G` z8%mOK_!7>szsQwV$rx$ysw1xv^n65Vd564!YB;J(FL5MP%Wg~=nM=t{*5yahk%yx; zP&Bo9NWe+-#BGxgYWcr_X?7^k+9lpDERX-RLW zT2+hEuyRC^D>9QTm&`*>A)#xY{()1Ba1h2UcX3zNSxI`Ac!u?mgzO==4Kj`bafC~Z z`p^j5xijRRoAt`3C`hj6ZbTR#U!Z_*mL@oTFKCtShek*TwJD}4 z*V-7B+c1|PBm~oaxXvPkWHbboV0a^^f0k~J=a?_aeTe2H7$kSKD0)pOEA!-8$0{@d za+a8d>Y3WzSadC3c?{s?{Ww|XJYdtfSoWQ0F%Fz4?@FdyO9krXf1FZD& z%(!i*zMVzCPI+S@_87`uMsSWUNpxQ6+_v5=WoZMTaD92}&A4)#m)GVUAp>SK(So<> z^p$(bn%C#8MypZS`J@KDkUS7u!ztv`F4<2rcOrT@uDph~%%!Tff@@#mOEyymYU0Oq zCa0Ua>*<@l-AETP7ipcu)YZYHK8Hcf84O?67IBIb~^>El94FtOYCqX{rP~07W zhLAuS`CL9QCc?&eWpREjMtjudy9#ebWUVujAr6s`15}rLx?~1&drt@A;JAM_G>3un zHozeEGbCX%X)olHX+3pFsXyj)Hf536XPCDsS#rd;lyek40d9niVna9;eA9VsBp#vp z=tVpkiOVXmd-TG{8=T{CbV!2f=#a0Jgbe&dneD6#%`e~%6SIgaj%NAjqeJkd?}f|DZ+9Lo+TN} zr>)$Ez1&Qj5y~<%Z6J&B70yruMuT4l!`{*U=wgk zn`Z-CdGt7H^5j4m1$t6kx*4fR%o4Vx{x(Q;ayBfR0YwLHO_~BRB;Ou!;nL*8smPgS z!&-hUmQ8YUTa8k47s@9&neqx9!T+C`yM+`)i(piG28bf~C>2o6dIjl~SH3wxpOq}v zYZA4x)1RH%l6dFOsBLnVI8=T|T&$Bqst^E_Okm8ve&=*hvQ*`MHwwszpS?{%hD3A7 zvgcwTJRe7V(dAZ|N?(8+6p^k#ee;X`&D{RR8-DY;{moJPn}PGqp|O2r5j6G>{#EgN z1)9sGQ*@QdI^`++kAK_z@oyY0%HTZw;oac7f87F09r9Gn$er8Bqcl$y@@LiClWC zzZ-m4dtyu%4nhAK#gYHR+kdP5`rTITiP(K)KL1OAs6EPVvU;#VLbPIjGU)Yh=Jjtp zpa*@u5w8?m6xhn)78IBhhDOlyq95M=8~F9{7O;)V|;yKl4Z1{GkAA0{R<;-xS<`3bk3x>1A?ns&w15BmSvb-2l>X#<~VG$feIq;$@Oc@TQQo(!# zL#Y-Mmx;nNPbtgDM0T17~6aRI)1>-Xyx1Srd=to|p+^uo@B`*JZtWu3{1> zjJb2@&TxRRw+bRBjw)>9^mrh(LocPiB1eO=q0SJ(9EWT7y->NbyHY)L68H;4QMQBz zCgjp?7r<#vNgLo`LjF0!Eq--hY5Yf~ohs_18S+EEKv{V6P>?I)zwT{s@8#k@K7HD3 zKE!{#kMF~W`eUni6{=VXQV&8V;%{+p;#iOCpFUY1KG=MF!T~bOUmdY40XxkX+m|Eb z2Mdu;pMkPJ%%IM&Zd!WQKjZ-MuRf-!PoOc9`Dj=-Blg&})B!N~WhFeVo6ILMQGH>O~7 zypT~5jt9If4yMA>V9T)%CLls8NFWEII44!{!$lfq;1c^j7$Xo`@u(X zLp*=$(}2~xYw zBt_+%mmTdSZxrx67=}(ZRAdr0`oo8kDj&h`F<~gz@gfvyO6A$(g#@H$kMdfK1FQ()%JzD$jfKm&8X*M%C1-hSrQ}x z=+aPN>n@V7tp7UVK%x6Gie6dgWpJG}2VWHaDtj(tU{=W!d2w7vm{mbjxL_Jnw5;b@ z8=2$PGGw-bqa2QN;{9d`aZ(x8*;)-ZFc-ZcvFR3S>W|trK^5e)FMYcD`oA^L|0AjT z8zlkp9CpRlZDYmxf49-ho&Q_g+pXP)^Z$K(8~?RF#zCFVEQ%Pg5%??E0I%T%0`f5| zfUYnDSVp=QHXN1l!m;#dbS2hSgU3*NDJ(YxSV%aW`^-fnJgfe#h{gW+kf zbM*XC(*gjwGxAsUFL+lI98brmGa@)Xl6BtI{%To3@Z~bDm5;}wUkZ~qt@@(j`J+uY zj(lL#0kCapRSUlD%ICD~Vg8+}`frp3-vT=ZwMYHLou}63zqG?n4cM39+t7#&f|K0`t6pkER)Nsvk2H8`1W1!Z3dR}*q~m${_U2PE~ifaAQ`;& zyIPvTs{nb0CJf};;E~aKp=5%Ey5-izc~9TftWOqa8mBrQX~{D0bcY8g$Il7BR#zDYERzPWH?&0tVq_OXk|uPh#R-&srAgD6+gPJYH&nILafo2r$4 zC`a^aoy(<7c_ZTne z-C)DhlBo>wZ4Ry*%K1UHzMt#d#H@F0Ngn8<*V6s z{%2c8P5>L=5Km|967m1avXVG%_OSvT{oP37IOyA=Y^v?%5(efC%WR4U9I1uhO4(MHp8Bl0cbXGqJyCP!MHQDb{ zXy9XFbg*I?0bqH5zPGqG^iVm0@WcwAwo5R$Fg>2seq5?yR<9{$?ziNpcL-G^|I&qiQ76mNWm^6f z7zJV(x$s1oITta^JSem(e_20D7kaj~2qkH}N0fE0Ue(`QHBcamTiCSCju-~p29Bk# zaTI59$!$@lo-xat4HLeS1xhL+yu&JXa*3?%X7u~&Y?U?U+gO>`v?UWeesx`Syn!8XRzT~4SWP~7^0{g#2eKX zZ&MdE9E2S0Ae5xoGNp1+rD%Qnq<+7tSZ8GnQ}*KxT9h))v>Dak;+8Hy@uXns8ZYe1 z>m;r_oo+4Jq_li>a`Ni1H|q5JhX*p|`>1=`+wUC@4?2f~wHHu{u#!Hl$j_(O6md%F zxyAy>LVQ>O|Ewz@_lWPR0=HtSIZzLi>d!x^ii4be1#IPfSt%Ao)N=hSU&DN=ZzV3Nlrq81iExWhiiwd9Yc9ZyK&yy-8fenr9Snu14*% zX>$y`ZzRMNI(BQt&^p6*ivg>8pSJimZN)&DH)-aBw25A+lmexQrPYgEOub2)Q&O%6 z*Q{HFT*4H}9>z@`#l1$m_>Wvq9!yzp@vX4`(Tq>J|IJ}|69do{_MeTXjh(#x=U(gS zgZ?28+u-Clp&myE=#Wff9KdJ+D4qt8#gWW)3|@De@n5z6wLijbIKZm) zzq{AU+yAv1I}hvsK0dYn1H!;2nulz$%qeB7(&#UVXn8~PwY#_SF0sMWXsI`|l5VxZ zN4o%UQ$$<>j~p$*IWHhZBE)@dxm$c~>rZ3^y!QJ4jf`*P{IPod?`-dF=lB0cbMImO z-^X{m^)FrsOU+pS8ySDi>p$^qSZZ*teXUMKHp0YHhf1^;R~3e}8|^O}5G%~r2A zJ`q3Sa2i1`d;Gkn-Ab8t=rtW*O3gPT#Fs_-=3?*?Vn2>Fu9apBOZ7gXFuSU-TX}BQf7q%%#3qn7?Iz^3%)s9kD_7_fT(L|psJy)ya2e9D}Xz`1h~mH zz>?>v35*@q+HD!KY zdje{T>DBWbwg!H+02o8}oJ=O-VW>7}HM9i&)d}EV_*)_WXRY8rkNn@h|V>2{-D z1zpRfvNT-h5{1f_UO|8EZ$18h3+L~Z{J;5>^ZXqDf4aN#`x^NFpTg67J+}1O z-MnSU;LoyCep!F+mGr%_Gk0N5w%Q%iI5&6MM^ubAV*VwbfgU{-E%>B>eT=n3ydc6hk;IsOlV+B0; ztUgSt>rbj1I&GHSi>;w0__`f7HIDzeoihtJA{EHqy=UfaPFI!4RUMg>-6BtZ@EyIK z?=%lKy8t&FZRhc#1uY0O$beV7JEmw2ZuoM>R9Sw^_;d6|Q zg#0B2NVU9rlnU;|U;3Kt?opcjZ8QNAAF-SRCHU=5UQ6ysp*Z{o5Np zJn-`Hz{|q}FUI$EKk)KkWn0b2v?(jNPxICyzPV$UD1+ zh3f52qEU8^QbCqf&V$wN4Xt*UjvK`ua$(6>RVt2b zRpqNtAZhg1RwQLBFgU%w(kKg}TzQlPcU%fVl`3eRme=A%mS~Z#b2d*$|DF!#H*`PI zc#7(`qw=QlDIW?Zsu{i}lSuLWqCXU+BTlh)*BR6;YQXb*pL;L8l@Z-#qFykPzXw#YjoGEE^Bi4DfzpU z_3u%1L*@4KzZA zxp(_ER=oeT{j}A{^8e=U_TIz$-}mxufWDpqpqIwXzi2so&K|JAYvj+tJ_>!Zm~)2u zD#jkNtlK{i9+y)cWQHU|=6Pezz} z4<^`04j9gmf-BmVA}9qAc$l&Xk7I^B9l}8hdu$dVB%BaM&>PwY(s^~$B_#5202zt4 zo`Ao{C|ZC`ieTi<>M(%*f?=0#JrO2R2f-9^#s)UUX&j56m;_snWo>MLPUOxoLoSOW zWLfXuzb}{<^6x+X{eSAHQlaSTN%W~zv*PV+TO}y z{yzZ7&(KWO?Ci`}{Vp+Ikud@@RhQ7KbV{uJ|NpvGZ85p{O5*FQA0aMAVTu`|mSvla zLofEtrqkN_Zp%^(YTLQ8Z1Da(9wV3e@71g8afCb-`ao5aQk-Rj!M5#^c?cQie2R>J zLoU<84A|`aJOG33C*TMN2PaPeVy?3#0YTs)54(^vo^kA+BWBqK!&COBf_+i2`99fQ zN7}&kH%fx{g3FX-QapXn`_usgQ}BW^43Ce5oBa%>R8TkIZSbBl7}9DW;lUHl@zU$$$u8tpu2IXXjZPV6ZJZ(}~$kWmwj!G6sjf z55!+H;(I8P#&A>;azSD~94v-@GFsxq?Np+uOz5X5KoNE`Z@paDD)XHMfNf7AGPixq zPz3$w(DS0_4M+S>I75~d((Yf+{|3D|4s8AVhqA)jdrKGAze@{iQQE`N^P)9zX^m|s zj{N6;quaK=CAH(oFNeOZPA1MdT8Jw0r+tnVRTWad0Z~EzTvkC&bWvFTX;+elZaWd0 zViJhb@=sMT$j_|Dj|bb}?DP zFNJ-{d&`zfR26R5>vJ4n;>iCNU17g(G#c;ayggJaRnOV$&N;o(4I1!8|Ga#~KmS_@ z+3bgUdF~oOD4tfspm^=eqBh^c((`yB0Gpcgu?Z#Jd2b&1 z73P_iQ#HbS5yVGudQE@OCG$B6)EwkJe)XnjZeLHp`!GUt9MA3Ol5k{uZw77fej2uT z-Zb3iX&sR^hXI@-&sG(9zS!T1Rxlp%W>GbIE5$rY^b5I9O^|L8Tx6;yrX8m;hfYjpA zJPl+rNZZxK9SGgkG6Lj&=X06(D!PZq|3+GI%G8FJ61)I>ZAV1z&v2LTSI z$+k2_OlD@lTN|Kv6(W~Qr6guy%)sU)#0<3TZ7|0{%=sZ-iwSH-z!(7$*>0!Sxc!5NuIV02!U^B=>csVf{xMJn&&9L!-5^H~P)1Oy0q$ZK0a0FA>BK#z@tBxkt~U>H8HKuA=U(**?{ zpUwM_K|Vcjz&QBsciy zk)_7FWW^_OsnZMJ{82!M^^84_2NP1*1|r41%q&f(An3RsrM$)rqqVB3^0+wtQ0w~h}f8{#>l49>%E zIjDU^-3X7lV9mSAc@1@E$cud;;ZqtL9M{u@dC?U#5W}P-NC}2ZkdGhtIme?ccF*~E z90Zs>eylaW#W;y-5JavX3P-@{Mv%@92?<|7_xxls;ZW3$t)24G6{buu6{XGKtIj1+ zgxmyl$r%V!fIWFO@d0!rLaEq{#UVvD-v6l!`f~Lc?Pe4~qH!u(pFvYEjRrJdV^o;?la>i$sjIz4!EC{olv8QGEgY)M)`5NIMa5qUM4SaAuf_D-NJ% zh?MWOKc1XEQur08P7p@Ki(SDg2HW-+y64CF)yOsblXPZ%)GVBwrFGBy8}L@7)Wz(p#C+h&htPRvucHKJzpmD96< zhz{BYmoQ4B>Gwz7*S+qW(OG}LGwiiNZ32BW&QpKX@0|YqtT!BWPmcPhy}{t%9K1U2 z49`w`qZge)Z*S)s5GkIT9X~ld>~)6%twC=%8VozbK^xTg^=pFBmI>#r+zZ{*M z4Mt~!&MVoMLRr2q<_@A@F0MJylEWz2L|4#d{sJ`jvNv?@3`TUzL^%Vy4Jer-b%Q30 zBnlf{M?OT>)sOCEqTTRWog}Eqm(EeqA;2(-+NsG)IyXz_wXCDHL1V=mvpjpQP$H%n``(#jhfK)!O4s@voxgq$8?Y9#Dp<*O(*n8T~V zIl62EGn}@r`35c8AUhN@LRl`R_T>x(X90x_(+L(k1{g>+66yqtOSmw~iuqD}cL0>X zOq5KUFf@*-E}7Tm_$Q1^GI?!6s4j_3Q$h*Uj-|} zu?6lHNSSq0^&?@`rHpzxn_inqYZjeiaX0+=j=A&tJomG5+?D(`$8F1bZI08H@LA1e zT@tW}_;%>Swuo+Y2f$<`dD~*R3w=83&maNld zt8~pZs#hp)U8mf3ectKn?2nE*{}}Bb4Bn_!+3%fpd&fgQvb#=0cXqu6yp9m|8JGCd z1)iN6$>c72AZ4d{ev+3>@%#kOPw(={ZP!6+3%I}!f#J7d;&NJLQ;7C@TQ zB>q-yVW#kNgc&*sl6TN3{sXn8eMsri>9aRzMhna>V>M6oU>USgC27_)8wXRcnSD(| z&Iq;WKXadUhq6b8AoH2&Cuh)LTWd8UG(nM?N#+X!RZs`y$|t2-t$a?Zw#w%yd-GhXR5mG+?qezx zxrEKcUO^%x^b?#4AFHWPu2r@e|u92$v}s+6vHqgCtkY90R)E@Yd0cIev(}0Tp`7l z!U#n&7-@Rm%7jX2SulBquMH}wByFNX$>%%3Pv7%>0%D&<(4Iib03$?Ru#RKw3#&Ua z=KMz8GJp_=$mjc!H6w!Hs%Nr04)2VGUEW4XP8cDoS%A$;IrL&`oudVpe1zSVng+Hu z8ndNQo2?n3WoPlRxR4YfIJd)yaPdbQHYeox_J`+9r@8Ai?8dj&9OCqiMjN0x#J-+; z|H7qjk4$I3^YKaIO6<#2D>F@#(~-S$Xcfb=q|^XI;bSWl%@@XqHJs)@kFm-c{NK3F zzgZj0`Ty>2qgmkp+dHiX{(m3eM&i7o>=`*fk!H9SmrjxT0fc38*6NE9;3F6XU``^` zwl>Pxh%o%3h{a*n{40d+Ih-O}`tU?lu+_R{ZDiy-VB3l1iMTBn4||YFs`PNIg5YOu z6a>No9A2OdSg9mwZ2-=lIgq!f#7C)}TX{!Q!>h&!_RW`#^Q&Z3)aM2S^cI*KX zN>3yx30dn*QNU_X_#e{=j0yBYZ%hJCm4Vs!mm2YQ{Vv{3yY9 z3GLlPc~hb#@Eq(zG$+9TF_ESLgXdtNG>$T>taV;Hb!g!Tv@?h+0{TCzb~zzCo(CXs zXUILL@tltW$8oA$@;v3PcmzW}%QV8~8PKszkor=7Lp&%#0u>HfH{^MkSAxD$cnik_ z-%p5S%mA~#JUECYf&)2m^!SNq^%(&%MUIu2F>HXs`zq$l^AabtDc|uIVnD?#Ojq5v zRKc*aHW4`i%V^AaSjDzQ%#62bQaV?&N_jC|T@tt`Wa1CC)hdy%z9Ig#IUqX}ki;Yw z%@z@@w7tk;sh>#KXy(l_Zj3Aqk&7n_987JI#2ZseBAWy@v_s-;31k>awM}YOCWG21 z9@V`u4MoyUFgVr7^FQVIR}9_;_!m9iwhi`CgfEZ>CJ~v7Hih{4Ed{AK=vdmUrDilq zXHYjec->jvB%Q&8VaWg?g4P2`9mzmrK8^T#>nlGnxdt5YlVC)eBy3ZHzUu0V%I8zTxEQH#gv3)j3kNPIFh+EXZw;!0 z23uR&ciutADnYg_eY=n+yy%1rg*4TI${{-SHBCn=VHt)Bb3^Mh9c|rZ+me^RL5sSu zzXX@q_rVx(PKG?~7$ckcmdZdN;teCjt4VKTIwP^~3Fj_&DTeU)WGGx*_jW|9C~%2~ zJO^}w)W5-n0k0B91#Ywmd0~wPBVdZze|`Me0@~&+xp3S7^Tp@_M#?6jHeYDt zKGBrArvd7SFoGc-iTD0$FvRvbTId4sl1itxH)wI-@%+TL&?q8_oD^(>nrh4}ZNZok z>Ox;#d29e9Aqksj+vyZ}`Vok0S4(gtw(WS8)uXHEP=$?oUL0% zb;132dcv=i7@;6RKCG z5LI@i5^yF-pW=;fn%S~9m^iFssFy z2+hfb^rq#xyAC)A7z#WkXT)lfC1=?x`lvgx;w|F_s0e3WMz(Ay>)hPW-BuO=o57^{ z@zeOp51pQn-V)B8ZOXCAOson`h!dk;PfCX&<-rWzO4Q^X32~1{{P^7L?RZ?-30Z5Rx8D$vvwC*^e+07o<8yu7)m8LHXP@ zS7_o-ZENNv7OMtlhY><^!4K;jI??5A@H0?JhYDEggJej&*MzcT#Q3=@yG_oFj1rhe zy<}-_l-wDMN#XnCl7a<^`Mf0;D7uWWa*@%;Xp^0(s%va-#&$+LJ5e4aOim=BD^}O1 z!+baLH{5Er=wql3GPHUM8mfjw+xEl^u1ccJvD8}9gvKKOHn4HZ&S#|~lxk4KLA0!Q z`o&#SeU&UtJs_%(RGNubeBb`cD~pGBm)3;QR49_EV~K}+#Ure?Eyv21HZ(S<)e&>` zHH4UrD7etXMZpD*NFWHOPlF~4$@CHh7l|rIUjQEcGWw}=deC`s*t4XFmU(tw%8>a7 z=QlEn8ZV@k zb0kWYuaa^CwUl}iw+vz#w8ZQ&o+?%+Hx+WklZYfUB3tx{b}2O?;vi3NL;T=FCb=qn z_L;x+Buw8eRue`|i$p7bEwJZ0+tHVUd=a zV6HB2@!}KYE?hs^7}A6pA?KBIIw_W*8b)ZqNT^#zSIAArNUzZ(!3Om)4(ilLC^Qml z&4yTNv55CJV0`anCY%Z>5*tB|Au>DmKFvzz`=o zNIRJ?bx$;4Q^f3xW_=@RkF(>g?4`=4!R0>}I$9r^AY`=3QL4Jj*({F79nvU)%~7(l zQ%FqXjpR^&N_;NgNcxEM$V_Qla>C}$V^aUjP&CJZU>L6==%PM~u-M4?al3qHT3W@i zmS(rXUPD}63ZqE%xMD_9g5a&0d2=BTQi`X65M?;P`i8JCS71ve7_(iyyBjubSv-xJ zDl(WtLdKJN%b5>D-p%nB0Bjg|c8Y_Lyi_Edg0C3`yUtmElIUdt45GT@Fy`cju-hZ$VxK)RsVP18AvydpnJAwI`q&x~>`7x%7DF zScbiB>PBR!x60Hg>UttQtC!uiWHeRzAYupoR=HQ0LAglo&3nHu7fIFU6o^Y)6ND1iPj;%vdK zxcV$DwZ>6aT{#)bT3b1#*5%@`HRPV60`vn>?x!{JLf$NnNEy}@%2`WHL(gZ{-H@M7rO zltK6W<~XJ>>H@QD(^Z&Zl-~*N1uKl!;mWTO^f{OM~IqQ_#d7F4#fh% z%{|_gc60T{#`~PmzuZ4~Bj@R#pYQyg5C7GyrPp*uJIR#MdXUXoi!92c1^<{V@* zng})qj-p7qP1z!$Eu-_=>L~&lBvc=Y1t>!cV{5k`Ljxm{hC)m@;1CikNjW>PYnI)} z%o)CuC_R3ZTulY$K9#L|CN%M)LixZpo#yu$)JVikB~t2QRm-J{#tYMc`8i4t4+mwG zWqyXPQcol&R))GZ^QmrFj)qCwvVpKRWb8{EOjF$y$LSDL#ULbaU5THvy0_1>GMOI5%k?iA0ABGTfKqs*#^cPPG`m2oa@x;7wNw}qyOH%tB zxsEaNk^uD9h76leW4_fRFZVjXBHMcW82bL>$BAwzKvFGbk~w`fLQDouWIiI+hPvburq0P5sJwPa!kGU6-Xef= z>}sPX!$A653f^WXS;knp+GaS_pJb{h$2I8tx;KtxQYJYyaO#abPCo6g!BdnLeFMM{ zCmO-RaR>;XSH5e4X6va4P}gif+k3XBazh_3rV)vQ%syy%oC^hZ5_Uzd;sm6Qc(*6L z`!)=ljhqfUaj>Yhl++}Ab}Vf>;gQ(jVG;yC+!86Vyg0i_Q0 zklO|#EP-GzEQ0F217$X>O@?)0PYSywL>BkhR9>hxa#%3;c@P#h%J!kmYt z7Z-Urx&a(umKGG^O@NtESIzp2#b5^QEQAc=S-?G>bREb!7;DI#!h+F!fz--H6xhh* zt|8QwzC~MQ#g>7s85OU@a0w<6FV9sZ+?UObgdZi9)A zV(2jIMMQjqIpeJTQ)vmaIx`Ydwxq&L!5^1OYTKxh> zbsvw_rYa;8FQ64Voh0WLC-;Eb(&9?#KEw}OCJPsgGW~wia1!U>rFbSpVw(z4d2D6Q z9b?NAr5GdZOq38-hI3Aa5g|fRbfIRpROmIGmb2~bD3t?@+!-=*1KKnncCbA%hd6N5 z$ss8=UpQVs9a-O@@nnLpM5e;d04TU!;1NbsxkOryYN;eGhP;3_!!lC+WG)%ywXi_0A{m8PthHY{5~C94muLLRuqa#YP&kVuqm4 zW;#v8e}EyD7w2Z!iwXy(OstGGx@2vv?o?Tqaxpk=+9&fSu1`D_j=`9i2F*p#`?O5= zbYCH+92oh5B~0=Y6J7Otsccf-cC|^ZhWQ&A3yZUYAEU5Plh^Jn!stS#{zk^itt=vQ z4qew5PR%(jdSdQpv?o*&`L;wNBS1zI(K_(9h!$d_EB*sT1xR_mo*WsokmLYr&+#0P z2#J33877@DCNCxN z4k&|o1Ia`SV)Q|F?KIbuBQy0g^hdTj%65yc>{k+D)k)%+oG$NP>SCF3y`<&7m)RvI zM7&)6)TSjZHX{LvQt-*@66qra9aEEm==XWv%BiOG1$Rkd30O}UsbEadtXR&XMDk`% zS!Y<9`IU4iMK@K-pM+{;$1(PqjRWwy(~rp2LR``SeQo1dDw2ofmFmZGMp*%_GzAyORK$X%K?9Je_K%HajQG3 zU2e;qZi{`YcE45dOlMf_Oe@32$gqV8yj!`Wn;qCU9w{`k zK)jCgL+bvpj7ryZgjh+iQg4V10+qI#Z*|sE4sdhJJ}F@2O+xdFo|=Y#l6E;0KkYj= zhgX9*nxZyn?S5mXW=#LmyfO>I3~AOk5KKAC3JXE7ynwV3G;vtVZ6!~$GS8J9&?+C6 z5>RHCiyV}tZYu?^l?wP3UM0DvZs-nD^ajcA%oWZXa*iaS2U{vWqq%ag<5Z#hZQ5oR zqdBGxyD*dyu`n`xV!cQjZfh+==kQRk2?V{9z+apa!d_w@(FJ8_o?*-)w&F1fc#&d; z5r{F<&DB==>Ey34(?@h|D}4aU?DRQ4X)D?)BzxVN59(Q9u~}n2{&%{YW=++~BZ3wI zuC{!r0Ke8Y_O;pfWi0%Pqrs&;MZBV=-x{`l>stG*Z||42_$#Aux$WQUPX9F9w{YF* zTrdF4ngEvJ@Mks#Ty73n*4r;`7+7H%ScdBtG!m>Z6I_DjeH#oGO$IZk_PiYj$L)+F zX^T>{!^s`bu49KI+Ra+qRBT=HMcgtDu*}LPYhxo?yFWV{P_&3C3P+WOFeyQAX9*)( z8)kc_?P1iZ=XJpv1_u;HNx(p562r&G0WTIBAw@L#=)Lgs7ojFagB4a;!VNLgWk!-i zSYZx50hcrE&UD}gEh91{UwW^kV;a0fUqR)meob4Dyxdzf2dMzQzF~-NqMH#hD{MnD zdXt-2iR3#}W++nV()CS7O6gW?Cl^}DY6f)nZN!+8G(mfhv9Hg7AXGb_W z1DkKutWz)3gVlHR$$dk`UA=e=)Vy7nlgteV#636OgAg~O%|>e{Pr{XV>M2nx?$#?_ zq*xn=FYlcSR3s^r^IHHEJybKQx+-7Qyt@3}oK=O6Sh}m0KM#@el&`{dRVsqC^J;$E z&G@f=>2TB!-#ve8#D7m@|N8OCus3kn6}w&=E8hQTY&W)Z@!y-fPaopH-^=&mLvpqQ zwULJ}u!o`=`1HxLK70T;5D~3FZRC?FsR0Lk1UT?ez`*v7F0@vC<@LZw$f4+Lc^~Ke zB)utqk1=B4)2B?uBz%*j0=mljA3gvScmmLuCgGp@yVm+2&5$4R!zT-`^AyDL_21fU z?dI2iYv<|S!}`CE@56`sW2<)+YJHV?n=Yn-V?C~a`ec3hVCzevnp(?3y$|d(Q;4yS zd@f2{3hITOd5Ym9U{M^nU~gOeiRS}hk_&3KU>h8kUfBY<sqCDd*i03uI7`ZT}NW7$2yn*_0Eq3qV$nk-z6;wSQ6h-DC8goFnxHUny%W!Af+vyB&007Y@v!&=C4C; zpC)h~3Dn!q*B{Rm$AkzHh;u3_B(GFzoNi5pN^c4-Wu zt#b5?7Z{Ck7jV{Fv-wSoX5S!9OhT(^Z<<;iSMlL;BfH#QxS1cBl(josRiFZ6C`{(3 zf!5R?wY30L5bG4NIFgpcFbpFSMp$f`$y0UG3#=Gd8%R$i(CHtPO<6;w z>yt3sP@az2Tu#i^Dq_CU*M2YnFpA*9u)&abzR4}_`-C|gw|eiZSGc&`l#~R3=SMJ@ zBJe0-J#TQ}Ju!Y$E)SJ80zOHtwi*!hdkzSwDgAd%my~5$9Qr8JKusO39DUu%`&bjF zZG8|{1(He!OdYf}R6*|wG;nbK#gTv^&Y^%OX8Xw%-ltEN_OgQ^PW+5Md=M`MoT4eZ zdMO~jZP?%c`rV@%*yNlk(-jqhKyEYQMPo)>;}5eMLIPPv6mK2XPidLzAkZ7{sAZsram&6N6I^3O7y(ZVI_UDtHaJ8oKnJm z1m|cB$1YTSP>q7%92qJnnZm`c{e0zo6d(+OrU2ADH&1Z2VYi>JIwv0DD?1KD6m6L+ z?!yPw6}fKo3X!Pg){;oqEsDI{x=L!PNf8hRHhJgx?vz&D^fA?V*3S0(wXH2Bd`}?u zL9MID=vxZjbJX7hOa(BgtyPeh;PnKA@?;)CO!)-Bl*U3Ku-^NxOAd-l_NH#uBI(Av zQ`WNSYu~0az5e3+)n~%y<{MXLFzqGW+J9&pAB}SogsFLCF$+T>2)+C{* zZBMRG!4-R<(%QV|ll(oO@s~(+%0tqfPY4AGq-l8Jd%Q|i>6kzpFUZM8`&CyhMtA0&Kd+yNU9$`txseGP&_Bo@(s~lH(_s)=+0kCb(wsiIc)VN^LuGhbz^=Smd+3%dE zu59AgIu~t($C=!|v?q}d#0U$*35hOYWW3PlSVQ%4O@xFK8T7*$1U|YzzKZRdzOsxa zkk>(;C5;j1B+?-Sd=Uy&JSxeDR=$&L93V;tw#4owkzQwz4#b*KU64=Qw4Y1!^+cgV z3uXFBZm!jU0c+^fT%su+i;+sQDi5K;`(*hEbp7*ZSxX0*!)`Y@9wcq&XDs?Xf2vZI zYMj&5wm>a;B3dr$PoJ#+vV^|RWdIgqQZJ+O>cdG6smyurzvQ)O#Xk7DsKSH!^dyu@ zfQnxGlEAux8hKS*SFa(xZKXx|@>ivWn(O+Gb7hugu(#vWhiKS%eTrgu_yfjmW(OdB$U<$pRLxRO06RiKT@W z5mbWsKDktam#CuR3oqc@uL+hR#IY2TyqLGFm*OqK3tm?*B;&MtI40rG)`lG!J!dfq6G{5GhW5~Hq?6&amM zF|^HdDpq`Il2Cirde-7ore;om#hO>4R4T-N9*puUG_%f4L<2IS)Hg??vQN5bwe%<^ z#YpV`vXr??ub9xgazgKl2|X2;?tkJboyN`9P^C(E7>+MJ9LAqt+En(R^1Gh>cSyWz+J66KrTuqvyS3NI`+w{;n-BKi z_wk8Sv5cRXM7|w_$jyYOt+u35+O49|T&ZDVnQdKS8Je-P%)L7P>60}x9~1xZA+2XH ziadyXcqoBsHY}5HV9^GgiE~S94dBH9S)V>xg}_+r;hKgO|MbZW=)Gp$^pKqf*1loK zioqj4eX{a#BX0~m8;yWN&W09P53XxeNCR@K2So~v<|B5gJ#W2;DyJ+VJ90eDpjj8l&;+vgRo9Ooni|a&8 z({uf&UUztMIvVs2UyhDX_Isn_&QUKBpEB>s>rc43s7Kp#sp4@48RaYZ|Ji%D?KW<- zQSf@#Qy^6G+wxWwNuBI)w|oDJ4~bTM(6;3C^mcnFunHv6Di$gLNQtpEceB>q&HY@> zW6bl+3(PCb!Uv!Vb>cymWT%^zwPLXf_;CL4oj(?lH##7AQ+p?GpWjAv6C$x<+k#}Z z=1xzJp51DjK&6%%K6&@{*~`=ZySH!8icR%U3qqQ;Q_jLxsn+S=PZw7bbJ1FWMQob> zhK*&vnLU}0di|Y-vZ!T_d@VnO1q-GHda9b8UJs+LQ%1G2)KIDHa8k01szuTUHsiI2 z$#qhwd~SA9C@!`UHTWP8$kw}L|34Xj)gXirwhRB?(gpIuLLJ%BO$REH$gh+ z?5_q$r13Pl@W&)<99zf4bXf{YVkx3VFx}mQ)xgX}%tlRMe$<)8mV?Rijn#rVTop_? z*+E06er$r~Zqqdo{;f3Ia8VY6qnM^^f}K&fbnHMX=RJCSqz^)qP(j;7J#>47?U}5@}YiZB=j6X1G8$m%+`_fR2 zneCX3+jwqDfmy_yO&>tMMw*P!d9^d=1r^X@SyQRAwQ66R)X^MF$ID(ZPzK6!9_?^^hvm%@SB* zE*63UnrFd;Hl4C+e7v%o2h~*y z*JX`dLL1h~dm0l&!r8Cj@3by|p2hZ|`msE5Zqg(+wM zB%vgc?N{N3T;b-)tu$7Rr!DE`dAJpSMOIHZ*RnWS7y4H)X*ld zau7pvmP5xqz5d3ihh4ll>TZXH_2{5b&$$Yks@cn+it(Fj)bY7Z$bAuBufr%CCD5Hb zYw3UVv76u&3{zHaV@dq4{k?8k|Fipe6aVXeo=>@#$zmj)JE8Z`tm|FSB(S)We#n# zkBTtbrKyCn+-LsY4?gd$w4mDe- zYQAYn*8(_2VV4F}5RX|$f_}v~Ll-W8H=nI&Y(din>6ivq5~yjhSbt2yMKj?_#x$6` zo&^L#u|+^!x!8n7q=#gfmchEBH7y`p0J0z>YXs?v*@}iq%?bTo03`uiDWN~g6Z#`f zdrtaRo>gARJYiwKBJ#>|{FhhztksO+`)bcQoH4&12I3tKkkyLTX*DAPbp7TFO(KqZ0}h-!0Vg?^i@+C=+{`zB?;|P|bu@ z*r!5P+>)Qu%9^P4<$}dJ->dstyOc>A)#PE!*u^wOty#B)S{alUdZ@j1YV41D+PC+tcmL*iac8D&yar%2@0EHx7#H<^fr9|L=8nyQTYo zzk6`7x&QCu`8P|kQFXDc$;e;|wU)$h|Drq$&0V{lJPdg@B8c~ z3C(l&^%5s6%w$l=zh1L3*2!G3Xz(j#E~qcQG=o9)6GO^`}>EFH}v0qJT)fMp-`D*YBUqG1;gP;n@cRU8#9Jt!+SJ4DZ#PKDH zS%_n+MxWrTkR=J)@BDb^S_~lpDV|YVPEvaetpx?U{rvHik{AIZ zDLp8^S408f)%R+~NC*`?WPH71MYW=kaS|oAJb3+T7*Qg+V3FcSi2%aQ*Crx?n1z82 zdkag`JJ`!c2ypx5k=lNk?`XH&K~-Uny7n~=Ly}4o^;0}oq+wssKL~o#{c$%liismM z!!eCew-nBteHAGuR;qSyH6;iQPslA`rUU{2tIGLcLxOl7bgjK^4mFzSJ+@ zRvkLR9DH6PSLZwvD`eUFuVvvusn}96Q3=zM0+GS9_)kyV!2QZV(#G z4c=^fuDzjYP=*WIGcbz{mCOVinsV$5S7|8&T3X<;Yi{FA=5~lH&7=dkW=lnj$L828 zt}-?Qveb@id)Uy9W5l>pI~ruMoq~yC)9f8~Ivwpji^gkhNCO?d)^E~=hBae|z`l;N z0Zl67^g*C&F9bkge&*vM_zZPBs9$I+=*ut-mIm+g#{@?)5u!PYR}~{x5v&$C7l*#^ z4Bpa^bsJxQtV__qEsfT;^7Y5N92MQtkn@JVfk7|AhPO3dXyY3g>mrPNTVsVbzW!Jj z-wL-hSTm^m9jv{@knQ2dc6Rd=pBu@4*0cj!692Kg+b!Gw?mzDAZ{$Ds@iZt09J7{N zu=cqF$<6}v99wriGaXy&*35O9HE_%Ab+WqOhQ&@cHw~6MH#Rq^7brKIoh-L8v0c4C zTa#7Rd6BV+)8jiZI;m}{Hpr*0Qw{a`1@pEnEbJo9W#ED-h3c&9Qgt@V(O*}z{izG< zs>?12?z-;wtVTjL(_e~*nvb+5@XDg84{LzfKn_Lu@xmEiGRx)Is`(UmcZ-H=HPJ|m zyRk7UY1P!g4f?b=_-J**=Rrtw2zhkSe5U+i=|y`gcpC_9h#6%um!ny5Dwr6H#W^fS zTg)ofM_p{xv(%XhHmpu#FvjxYd|ELsV_`I_$7fvB++BofEbjLDfQ_Y7QWLh(tYl3r zz#{xo2*+B$Mc?*SAyV-z6iY(_R-=VItr~D_ptVYt__N* zpW4uvIk@Jim9=Ox)AO#PIo8ow0nQ9n+6 z!upGRmC@FQCcKT|EpD-nPFzkA)-;sG$y<5qmc?1ClN{6#EHxBaL$N#*X&%C@UV4yc zn_GR=N;v}84fWMn;sX=g}CCLqz(O*xj4DFv` zJ|Z$pC{ZT}SMMvr@ZuG@w6_zQyjvuZcIVG2YbxWb)hp*^O=UOcP=n0Z1t4KUo}IjZ zFKJBw0IYvPct{e7M;S@#o8Ujz$<|Qs{h ziyi0`g9!oi&bVT~LRZ63`>7TQF9r6gF@01SM9gsXlW{gYJ~0zsc(_o32)0yJ<@5vo0(yp7oNFY*RIDkop^GL0+wL)9E^jo`uRc7hYX2m#TE? z!xEJBHx+=lue4dDtVzqdnW}`hRu_|2R4>gHORAQ%tXXNUS<$4VWyQ&Yx~`%mE$c-V z)N}P9X<6;jT(6+!NXuG{=6ZE1gS4z6_<}rFU)oLqL9D86sM9i}Wu-!6V`XcgwM|0I zWuxi$1vpU^SY{<;i<(R-zmmDZYVMmgt%f$#_P@pRCG@`uOt6#{@wM?Yc#CY8U54P6*Pm>WRLaa*zER+B5Kd$6| zJLqomKitbx!Wwpk?JezG<}Qn1i-nn!1Oy4H3uD%O;&&DWGW(fI8_R6l5lU(~&ciVl zzmj>SU6(#@mBaKoU28cYzwe{oQzVGIIke{SFFC|?i0Y>1$5p4M!d)dqUNU~6P|n2@ z9i_2bkh#@0w|}e64T!|uVx&2@3b~RbfmnFOvU*Z95pt}j@&03lS8#aUR`^23ujHY) z{U| zwMt%PAt$*vS&;v3ysxt#d>WIbV<>b^k3zSk{;GCyETv-~n_@Yxxc-=S;9Joe*mp7q zLoB5_R~xf?)c3P_x@fn2zs7dS9}76|##ttHE_S72mz{-CC|U?-9dAT?EA_P8R_0Pz zZmnpO{C%@Jd$D@@`?3}C;y4Xu8)Roj8JE>Q<|j;j&!U=Ei{%NimlXU|6a3u)Go1_w zcLBkR1)%Ci)7*wxj5puV)SE78?~tYAZuE92aTEW#I+3;L|1=uhf&8!A+1oA4{|+|! zpYP`>UHK`Co>Rf65ZvacX*42oC5c@M78|O1lvN+->J0+ClWI%#bl|dF{$DJ;s}O^A zlQ5yl(G1gAO+_a8%HHIT;GY&`zpsUhuihuP1iA387WQ4`j`cW=Xn{X>`ewi1C89UckW(C% zoD`t}h%{&6`;~+jHV7S9Jw{rVE?y*2>SW6HjWJdI5Y@Mw>dsl)roi z#f%TdXX*WaShfGy+t`2I%Tss%i_{u*ExY$M+h+y*)!V-KJ-_2^XfAE&|Fw)ZQchugq3t}iIK2^;6gLmrfQ8Ir3 zwA(A1Os|^1Pt!Ce6Ou?A|IFDm6@{X&r$Rz;y~dzLaLBR=O?s%CUF;Kl)kB8|2YUw| zLPG)YZU@|k3iy|O*1G@s0Eagq0G8eV-TlYq`~UI&#{TnOo|^j~R@o8&jjlWX^|iN! zP6(e+0ThABPv|E9JUTfJye;%j--rUqF_!4vPe;!Xp2{)f#-IyPfsCXfD3A}(lR{C) z+QB53(jm~_v3Qv?>aCLznJ(E!IHfN7Rg<;gMyUQ4q$4oatom~UX&PC+={uk_(pJ)=%WOVOa zhQ-9#2irMb&~wik>$;nK^?r)!K+%I{i`F`0phdR63#0O8o4A?|HIoi)$cAtKS?m7S z49D6Oz>@pFv;Vj(|KHu)-SGeS@>JxDdW}UeA_?I%EOYdZTbC-gE&~02Jp}rF8G&lE z^0z7eeM(Gr$K1Z^`wKGPLEq2RKv3^#Zi`YgCb0Qd70~rH^x0JVj0T7MBncbxS^vwS zv^CE^+z5Gkn$y!_|GQ=X-@Sv*L0SH@|9GSSxtFKma=63G`_FP`H_gnK zMEm@LD4#cs?s=1_o*QE1PfUz#!gFp&n{P5a=dBV@HE3r+pv+Bx%*N*?^xvv+AMLYD z|999e+yCx&_V+gQ-+esw_wk*?c3f^&aGMc5zJZ92_Z!LaX3u)(--MwP>lpzomJgNZjpu|$a1 z#WLiD;DsfgBYKS!JOab-8b+$-dmRCFd#->LyzXjXdftm)l#c)i*3o)IvKYh~eWyUR8 ztDlh=G_BQdq@8{VdN!Q1eE z`(iw5tN+=bVwSXXKm#1AOIY2P!g~~m%hY=m4Df~bD3BBNH{*2VJ^J6gE%Y1aluZRX ze*QvufpNI>0vZwA*2Ovdrx(mb$Rg7IGsuNjIREcn96f*iBA7%sZDaBIKiJ#rmh8W~ zkN5XB_Mi9iY@y$@)Y<#)L&n~9g56Hg{rGS!Wh#2@Hl2(DF{UJrz@cVD6knx{qcnAg@g(T_ z5fxJLv_kkX9fV67%>nqPz6$^#EbWKqebBR-w`EAMh|JpChzf+!n2yGNN;rT@Lb9VS zYhN;7-PvrQ_$BSSH3#wtr8Ff98EP-ObSed!vniqpQp7!HF=9jYhSyXM&@&b*YGMaT zjuWBU1dNP0n@WOk9P6qv8;KnoYG0R-9ESvM2b|2&j3g1`y6H=tNF-T==TK06W^zn; zq0j`+ZNUlSoLvTH#~GJDfg+C8T!tuQLLyG0X{cs)2PJGqVm)}0a2k#kKdd_iFRja& zy018a))GP@MCA@J?)o(qXrcgzESZsnD$;4lxE`+QXyAEUTj&>dX?++?4}T%?L~-{_ zP|5h*^Zss|P_q$5NUG~FFQ8rn++sygPkQI)=ib=9huV5iAiG7>#~|M)PGQY@0|eDk z<4>t(LwH{9Z`niV5MT8m==dF7^nXlgc!3m(lb-j}97SY^r?K3zBjnRWAgs3Ixg*6n zSG!7%i&7w@jzEBpB?@tZa10u3bc!FNi~gIV z*DwBhuDY)mXn@67&%1@?`%tA=1N}pBP5O2;N;|VQTj)JlP10lpKey0J&L&7v5H!KD z?w0VpN9eSI>3Pj}Idjinb}+S7rNdv0xHyF9BvJd=-ecEP&O+ zNoaA_^XZ=2Pwj}rL=xR@d2XX@LzJi3)GAic6M3c@Vv`AAC~!*<$gZs8UmUVY$`X)k zU?EsYv9$DLz6StZKvTlq3i`}znah;x6ct`KK<|YbsXD|T&dq7@XPRA`<4Jt}@uBl; z8-*?apds}^;ABWR0km$EC>k|iCg|J+7kImY*)ycFtMyQzVnQy96``&2lvdwJ5G0o_*7BdbAVl%=yjUtllsQI8$2P9E5 zBPLAW667i+oT@p}49C=-95eWt^;LfK$n#E#o}LfI31<*XQ+#~5h2z*SeDlq>ZFNsT zVaZTLMX34)yD4QtQ0pCy*ifxlga-4 zhhAJ!0U11R29qX3j_X0f(Rj<@PWtff*BAMWah!x>!eP2n&J@Wa+Wdml&&VCjglPs^ zS!F(6T#;}pR{%_87%i9$xDxY=?NB^0Lv7YI||ptG^7%ZaT38@285fA)Z|ehoDt>( zO<`01mDs1 zb<34hn*>n{YUvVvW1|C=Ej4=-$EX&mYcgQS)WrcC4k9GSYA;%}rz@$~Ddu=WB;g8? zS1(dvWX6PYO99*a5uK(aq{BHPI2`9t%szm%_I*L5Iy6tA&WD}s&yVK_^U+ihHww|7 zyOiU3fE+x$vo?9DTU7`2DNw@AG$+#1WPLWxsAuNYXY?zXqt6KPIebRX&EVBvBIFb% z8h!RY`|8pExBnFWf?wWegoi_#D3uFuiujFckigDsVx@gk0aV;-_3 zB&if_p^+X0`|!XgnM=r11$ z^cgmd>gk7;Z-@jxqP(@!Qs~agxmq99Fy3m)h2%KE?ijn&7Iac2+M?`!qcvUCbWO*BvNgogQD zNSrQ}IIW0+f|7-eZtj>nUUy;im=4bsluCzJ4)1Kowr(Qj_kdxPylF>3R_Rr z+tHjTOcOAWHRc$aq$6gdMTux!6Sut&S-}-=Fm2l+{5(%uJ4yr~IrFW(rGBWFdS14( z6|KFuqC^hB?&| zUk5~XdU8M@F)IUmt`BMhJXJ#nq!Ea5ly3k7w7N0M@L#E4Y?u{wU=(*=nUoij;0W-5 z!bE^5Kv=^P66BHZ>^?Uqm&ib&HG|UP`A!A#6ud{Y%)1;=!|Gt!G>!`R01dD@6IKWT zEQNYq=z4&ThpHnxT8N!fz7k&$fl^LF5`l0AD`zk$8VYc@@4P)PPDh+Uta;LIrtBR=~k(S_6U~e4>|?KPV!PW?wa1`bHy*fE7h`z@Z__^#|nXM zgW${FMHwwwSt;FjM}t$CT}dYc!YMPiTyf#-rsrfth2&ZX28+0OqQn^2><2YPr@tK8 zeYF^b#3L8PbHeG2M7i+)nK_p@(`5#x2~~4h6E&$YYpzShZa5{F7L1sTo<&G`%E`so z{D*P^%cdkaFyd~E1(o=2qM*`E#NvVTyj(Z<>+hz_j7<#nk{>FtHX%-)U4z>S3m>y zPgs=YXIUs*!03Zp_7){CAi_3a8>qh~i%eJW@{Zu>Z=6cRnCVdcG#ZpdicQ@srC+QF!W3rKxntD~GpiL3aiTE()TxSxig{88-C)uM zjk>uci&Dg<1}9S?k&rZw;dWi2)nD?Q7PaEt?B;NiUo@?1ooKG*NPfr_I2yEiErm^6 z`8}@dH9)QSI2CsLTOJWkB6V%dtCV6*_0_IC>k1~U1~pc!h2z+$MIMt3xNifRZ)a!7 zv6NcPdWolMB2ST5fxXU6=fJ5uDRtS@7J-FwebZAAMrOp2RhoVfA;+oJ z+vUrZayB7yOs4sA<*(WxwUi}kV)56O2HZE2wfZqQS>5t+sP5GaL*`|_v7uzC0SjjZ zr)f!x4~}#dA`_NJbHkEZ?~Q`YIN`i+HMMuf zj!lP$6_B>CHI`g8(6_E_GhJedwHkdZHPqS5Pfhi!VSnzu(MHl@~>G~eg|6MNMtnLK-5&;r7FUVPfA2sG{I1Z77Jv&0PoPD zS?Fd)iDA8uM)6hAg9moL#2Pa$SoZASno@5BYD-Py78gFa-PrYIwlb!aRst1srU?*I zJ-{P|>(phtN{}@Qk^vDVehG6<4e6SQ;~s^>n>+gI20<)TeEjKG{BA+JaoaoYY&# zqQXpOjBxBq)WnjiZm&wUEzX+H<2<0VXBYjM?_*B}H6*IWOJ?KHehxrM9!ysU8lsfH`~5wgaw=ew~> z1Q8{i55fK0eVC*}%xG61yW<$A57c+%$j^VqT~Wn$+qske{J)x_w9Yl!S6Rg{+UIO< zolO$61Y-;{LlEpyqrluqHxu-@;k!eYw}C5hdOm%zVJRXCkIV%vrKT$sM(-p7)%&x{`7%7KOf{Rs)6@eEaA_wL$HY*9;grLl1E&(V za|}QM^}e#-$a`shxUI&XCwga4MglOUf)6X^`=iEG1%WLoGzU~a0=u#Dy1~!6_%G7N z=}JDN<_atqlX{0|`ItYnDd8RoMvLY#!a|CfTO{&&5v$6&$8cqs#9_W>P zhIhjG1Ie^tEDYmn6%tQHx*f+eRE{@42iK0XU~g>#tIA9zQ6LiRqNI^^*t}?r1{;7e z%*aUMZt}37k?chmo|9A&;9y}@W1_FkOk}${d5+ea8`-(SX2qQ)ca@5b$fifEXDefE zkUAA%rK!GA^{;KmY8_OIUmo^oDZ}6Q!L)P2v}7!2B`JXzr*&$FH&!Vl&PCeG>gG{% zVDn+wwhrGJ-wdsrd1tz6NBTC&&N~&k zZbONxVsBD;(Z@=!_mz0N-?Ulw>r7wxm_)nmvDz1PEMW7>wcYFJ_2#b|+gs1|(bxeW zx?{s>T^K3wfgC$ldJ{EE$YNLfKjG`p%cPA#@$EtJ?n;a?7OyxihD5m-?4q@>O!~3^ z&r1)7R0Hq?PiA<}r+E=3 zoU&9f`;cl&(OPwQqcY&Auu!d*8x-8fF46rvmfpoI1!H=uypd@`l?RYhql|B z4)P|SwyCQ(TdG-gUi4&83BLJvUoJkdm?n19B0&T3b5I~H(7M&?3`~t<~7O1a9Z3^Bx54*PvkMSjVYU*Poo?As=uyU1M;t9ds6Zgt`3n#Ah zqr7zDPy1x@cok)MMf|q=@|Bc;B0+4{4rDRG-Y&5x?-4U*wkhkck+|Wt&SfO*W_i;Y zZ6r>t3A}Tx63!`55)LkSjdDHh;w(smumXIhyo5Tl)Yn(|T?SlPp43kqvT3UroGt1* z-g%&QmK)clBv$s=b0~0y;11R`Al>9(lqZ8;&(JL$`N>cLjePSGsZ)+L{+l1&rKbSx z)gYIvQP-sy)GGd@_z~?0u}3Z>bvcHhGyt!m(5ZG%6bLUZx#%rv@*jBYKR6toWdNg5q_}!Hyhtg<}1Fx$Omgp z1h{qkEO>P(fx0;VG#|dk#Bx$}zmP-5dj7j8u48H(t{n;yosu7qWt|b_i&z%9Si>d(B|z+HnK}MjC3D zh)0civaCvwb7~<{=LuF1e_lS861^QBP|q@eTwlTs9yl@)C|>-P4SA(eKB`y2PI3qE zSECL?9|H^xbi|SJqYxV6zv;w#Ct5MyfXLaY{6Juv#UH6QP5f0lLG`;;AD6^ zcPlJL$mG(IYLZ!G7w_?2+VnV#1I4?coq-s{!foIv-YVNGc=(&Mz_CwuZBHp2p2z#Z zM9W(dLZCn)5A%D_x=X+-KQrdhsYDFxB_T_zE2J^#Qb|9Vy11SZ9Fh^C1$p?nkONAH z+)ApEx|Ov#)BCAl}Xz znjQ+_`U5ncU`1p`du{}?8dJ&vo~6}e`3^m;7c>}^=djC}dgb*>Nhsher}?c18!{ke z+|PC5+3$zD#Qx~<^00jCK~Ge6q_Wo|hN&v@i2oxz$}KiPSostJHd$pLi9o_*rG z!n5A*czHNJtt-d3M-}n43L~qnl+nUJ=i@^ZtZ{h-TrJ*xA<0hRD*MfgO1t=0L*95(k31!FxT@hO!|DXUdWDI z6u|zcsQAQ|X5@_pDxbgB&(9>sImcOZ2Pj4R>QihX>2+7Oz2)BR?d`@D&UNC4dl&|s z_~y_&Qn$jP8%yX;np{IwmsxqZdjtm*=}f+{t}wI#z=GDo>(Kjg<~sHAZzHHy?%Avm6L_EESzk}T}1=T&N( z)8Vc<06-WX&lQNPc(U_1dED8%K4ARb;kvZG1^94Be#0D;!a;@}igx}PAVLJUlgpb9 z7XQtg({77eB9yxQyAGtcGC0`^iy72u)?d?xXrSfI{crFd)(IaxE4c1!vH~knXz}BY zZL9nD3#Zoy_D_=@-pgld5Y0X@<)Mh)FKp^x1_jj1MB$w&UfROa;UxhC!n*LVC5F8E z8DEUue|aAF0xiq#``|1EaGhu0|MhY2&>ydH@ANw{efHo%{fqwI;Gtgd``DTm z{r01q0siy@arUzd|FV+X=$UL>e`}TO$=nMtIq|EN23+CcSikmscx*oGAUJ=Ry5}Ku zROOLX?FVyK$w#5HrtP)lE=~I}pgqxi_&LM5!jIqQj%*D~z4iPZ;QIX{zebCukdYu5arCrN!SrtI%9Y^ko__W5&N zZf;yyzkfPBtn-x;3|$xxI2b1?{xqaI_RWfoZq^TS1Ipe*XaOe*c~QYl1EcVLj|P(zvrz^t3t-!72x#tkasLD8NgWBLuz@~mgM^}n259{L(qMV$(s&KMzBZ!Izk1HW z;pONQ0D>3@Jq%!Oim(>`F;zFlH_!NcgD^g*(7mqI?yKdpG?v(X={<<&+2Y8L91)r* z=w&8=0?e0!Pi8rW%pdE_^|bn>HTi|Fqas8Sws+fd?TOp1`~_IqT}(CZHv)g_ zVQKnlU`O5gx#{^-zWUjG@?9UT35H}r79T?)jexKrMUD0Dczp2Vl*A(eKX=!s77&^ zWUpkO1T7rdg+thruPLjcLRjhvS=UrWtB#KrQo;a80}g&6Yqea~ErL@Wp>Bf^SyHoQ zNhjnUHHfMkrMvtUu{$O`&L?qaBn9u-;iMsoo+$2QmSv8)&ld%jK5xvP=mVLu|F5oX zuSH*#icBRo=9F?mvSqc{RZ?Re&2hnhAr8so?Lvr4I9V%MrJd5udmZ%MGrwO|3* z-<{62AwQLCKRjg5bT>kuW@#(vA2t}zoe)&=w4!MWaeB(Wl@Ax6Oe0XDZqQk zX;6l@bH+&{ITgqc@kx}`IA<8E62l8qLMc&Bv;kz*tVz**curxBto$({_=-ook3 zvxjEA!$wle(4#78|1ROYS@B0{GR{yeXNDHS7_#<1Xyw!px;6K>ks$-@01o{0UcWdb zIH2-N-QIf%7Daul>sz+zs9+DVv4gw(ma#`$S;L|55Hs8Eyhpk*^aap<8^00!@C=*4 ziSEv}5z?8Bs%e97$K46;kHtak>BDyvP#hXZ;uo?R-^DI0H<)e1zloM*Z^y;3e7rI# zN@Mlv=f=rCu(1b0^}6J4e`rAms7l)5djVyjD2B}8`2H^nG+8Sj) z8JqF5;vjL2#|#o>M!ZA3R!*Q>bli&%&+#Dts1Mv<3ZyckQ4o8ohsB(nssG|7gb^X3 zsY@@Cx`qfljU2cg_QODyig?XA-k1V1E|aD(R8t^ti2 zV%5lYee3_l*9a7(O=@tMS5q0QI^}+zR*VjB8z}@byJ~H4yH%IhK zF9&{${MS~~tL^zKd&{^$CRu3)+0G&Cl!`n~l8R`D*(<3mX&PyW@w-6wyYz^>^5~VT zvl(IvyJ2DXoX5FVlW3T0@O0RnvqgGZOz@y#=f5QJ0}IDF{6u>Rj)>y$l%xQD+o%a^ zl;A1oehtEA*_alJsPR%%tq@`k(fMVe>aAt9l~NmKb3K_L*IG$t=hVn1uchKQ)m&Hf z@iMO^vHXrjiLFXaO6&+#nYBaqne_JDsfIMUpA*gF`}k4(0A0xi>MRTDYL{G~#)0Le zf-g2W$^ju{Ee)ku`kou0*yvC}c2u)y{W1=f*P){9Q(V6p(S5F~qIycZr20ZRFXUF) z_0Gw4X{$`8XQp#4801(hiQ?QEne_81@;a7Ey^9wdAuN+?om8&{JMLXX9n~aLyp|e& z-fH&FUT|ciOs!=`%eG@ot1~yDia;H4N|Gl@g=J=1S?;1r|WF z=Az`Ok7_G`TuRB?`_-!gj%W;xg=p?A68O_#-7arQM{3Hvm_;usJ<6KPB$KGO{|b!a?>M>U%nYve2WP~CJAAWw1)DLJKEpsZA}gpr zM~0MIz#^I?4riU-zg`_QXT|T~`BDU)SOJ?RG)(t#SFR=tbk)3r^fARhBQOUEYB#9q_1m;>J&OH(bQ|`4&Q% zc-w|a5P`1!Ip9e6U7QsXEF4f>0E^lAOW>rjEAK&XCMY3iA-t{G@?M0fWOJ?sE)8b@ zNX9H5!c_JtVzWLJ#c4K8T1k(QuRkGaV~^MbQVfx?1vV}~?{VGciG`kSqz8g9Gl>!- z+cLk?7hTx+7-iYj+>=<$pe&7P3_+BFteeDot~MP2#JK7M!BCS5Zjclw^|38mSxR>V zB~zB+C$ze0xR>CFzuxkPW%MX-p##zHw$j6A*|vp{A~am=-NY z8{$Bc)pg!`v=b(Q{?(A(g_wMB#y{22SB&^W!uD0YdrE+)T=0E^d<}fi83bp+ix68U zuW6mKbJ^v*08aunU2oZD{5;41{3+FRfYQ^~{t>&5YZQnJl+LWj0rBv0a(R9~pM1}a zIRIAB{ruM7Cpyr{YGZS{PJ4XsR#cBia#oG>TS=G&i}^1Z0|=ssh6vXbAdE^1`32#v zbN$txvvPoJp78RIVuET3j_rt7&#E(|IuG<8l7N&&g|En12h?>Plfi zIhROjNQ*h{+SZpm2)|u^qQqU@+WDyWGQ;&}0sF|l_no+*T{T%#zV5vzA0EYPB_hQ< zE5k7v;QP|GOCdZ+O&b7K%&|iY3XTa09xeyJ1lfy8r!PT$1s0EjaIhx!;io;ED)8&yKhN{_)=|+!1SbzRf2pcp@JXR9X$PXsB^R5=H*iclvP@QZtcbfD29`~ zPw0I@^x$@R0`?z&y;DUJA-{uD&wKB@{H{6mfIK23t#GJf6pAvVj+G$)yT9rU<}PnX zDQ-FzcQOx-hx(qA*-h9K!aU~8txWa89}kK!3DeP<9A#_dDUdbcGQyuunvaOo+gA)F z0D{yw0SHaQLH3mhDFNb`J0e0s<_`J|G#n}%8xdgAY{|+>k6qxz68X@JB2P0S{A@z~ z&P34V*x1=XlY?Db$ej-`f$2g&t`Tt#mzC4wD9H{db!Eb691XmZ1$P=h!0SD2uKtzJ zLd~G#(ePosBKk$A`RKERcXm}K_mu$KUCZ{`U1E{mkKPr%$;wLn&Q2zc%P_d695|Ir zdM8sGcBE?kM{wYgpG}!vA(D6}tFYD~cJc8^$SnF;rxHh{N6w>4$`e6)komVPIy-=Jn!cC~G$?qvAPtAw$|)ilN~=)l0^k1 zYhDi>shA(?-)51(??{KHcB(K=CGSQ4oqv&{Owj}41R3sYQ!%`q1x$g{^f7$AP110( z3q)dTs!!_-*@z;N-!D6*!85tg3BtjEI=3RXb=C|8$F>95l;ElYwlXiVMbT0VW z9oUFKqdxy9Hbq}zjUQaF63FDS!i?e%zf0CBy8-5Q4Z;kIFk>50eug#X9Ozqj(u3zY z*3s8!TO-qSNl;oigAtN?6H_Qrq!<*kz6xjfm~fKy7VHSY53^}-5D;#}9^>Vb<}*fc zxmMW>#yVj` z!`ec8P%MNDLaLmp;6e4(e{Q^7nvB1~`r1S|x`R7TepZ?Z*-4f@Ft{X68(@`*%!8t! z3D>y7B<@(!qy!AVj7(UH;(qko3Hz%>n!miL{}eT`WpiY-Q^<((n2mwZq!b{@ASVQS z=TtPbpeN)bChvHVax%iQ^r%gwn2@mnK`bD7AQ%_Y00r90>{xM(?Pu9T%RE zM$p6c{=2J);R~eY1ZppT{p8QQkQ`@DYQrf9sy8$*FrQ2zVmvG^PlJG-XIZ5ylZ2@y zK1F1kY&!*xAqO?_suD=ktI7j3IVltHu)vzv?J2pPR@|6Hv3UPvIm3vsxh`;_8!_q6 z_I!NwuO^;^sclI%5>z>+Lvy08W6Ds+BJOMQB#g!wi+`XP&OD)#)W?X$bu0SJ^mg|} zkLQOI;Aq3G_V4sPxQx05PjQ6{OZCi{e7)im6HKBJ^HaFP$kGTC!K1j#OLjN9=e3Q= ztI%q4Q>ZRlt^NH+5@g~jWJ?ZYXO*hl&8piEf_`*7`t9flinGWJ1uJK4a4H*orff4K zYgUlhkO+0~fX8`y-Cg2e=}d)Xm%iW8{+OcV`?7n>q9W8gKqym|Rf&l6izMYhz#wp5 zb9ouAZ%T=2AxomUA%6@9`{Tnh=tH#{ zx4V{UuFWr<4W2p0q|7{=vXuBNhIDLs*c92_BsO8z2@S!-_s^WWhp(f0TC$gFORyh> zi>Z9-f{Vj0Hulhq{3T)J@2*j}!eQ^0-j9$YcZFiZsoKV|PJqBB?0#Zabq#Ns*QoO7StA4iQQ;LkflLZ9egL?;qByP$XUz6mZ2{q zVP&oDnt~eKzQWCcLto^74*WSA^zG}WmWetL z8Ug{ihmB5c!JpUx1J0}xGf@MdsUYR#!`6U}8RIs^vK(Srdfu+$Dic&z zh8`|rp6Mx;Chv15{MMO?L}apI{`<;&U{s0*t2|=45iN&|5>mcYbt4`kVZisYobY~F zKuw#@V~^X&N+l22^mK)7zxRb_3rVjg2dob23XOD+fmSt-RPkRbyck1^r>Qz3+;a!2hgd!efG*_>1|zfC`hcxiA;|V|W{|}5udE2_QL}zWFKmrtNCy+4)C&(=CrKZ$Pk0b> zkN{sLOFAMc#8VFEJ^mWG0}4Z5SY#zX3oaX*uk3Cm6@)Owi3E!=3l4&U zi$L0={|7Fb6i!S2`->crKj?!KMufmLpQRH{-@+5ZYYp<3Z4bBN2K)o=RSH)b<)p$& zx2mxcrNpA#oi^@4cb*@@W6?)pG1CY&_!#Q{BX%&0AeQ)FvqPk(0_4R1nH_%Op<8q{ zfs`nX@is!^i)_?woPcf_jSQ~Gch<#NLBw1j#DW$UyEfXVG3Zg4B1fSe9L|mwyXSWn z#cCvLa%DhyzD5iuB0Xdlthu7Kc4#@gt5|xIm<)YM(Up#2noRI#2#S(6aM-OVb+K0FFm3?PYBm%o!-zA@oG}*hX0?2C!Jqk zA+%h0lp-%4&$0`m!(K{5x=yf;zf&L@S$@?m@Y;9Q!h^lIXYJIYu_kPR`J)Z}5?rSi z=#2zzK3rGKs652o-XaLK8zFi?%?MST-4Lg%VC>NnqKoHCuPL?n%n3f9Fo)b$SE{2| ziJ#0Ex|1O@PP?VB72q$)f{X{NLUzd(O9x`J#e*6g<_{pj;0u;uw>F}s!|b6y>su$y zO5#yafNQVrJ?8UyT7cJ>bSE(Va}zj8hxu1{=3z-*j2Y8|cYMa=`OiIRn&wa-r3`7> zFpqEn#WCW?48q8Rvf^(vfa>Yb8!P-&q zB;Dq{>lXB5aakMxG^yTVIhf9e;kJW`<2~K8ZV(I6 z%r#@>&FMyIsMS1-|40Ca&lz+IR&wVE@5_>3@;v8vsPxxFrH#fTpQ`#R3m?KwzI!xY z4{`iyQv#K2WN3%SEtqOV>X;9M+RJ0PeoWrs<1D*5`CWe`B)t$kMlEYWctlK|+AN(L z8x9m9!5MRajJ9CEEi^K~CY@tXc+PT3UAIXy$85Fm&s9{xd+`yo#)*z4 zuupqS_}0TKP#DV*G8*C)cgh9zXf&}R%8qAEy}&q;0e7|%#=q9kd&L#=QtEpegq_ZZ zmL}#Q2hAT)4}$CT@KN|GnH=WVZor(hQczG@%z8)m)|TsugqPdQ+xblU_jLeKI10$( zHsxdgtmZb_yB5bkBVn*Ctkcoit2~y6lJ?qj{n#^e>3=7$L77*3(mmN1_nsbFr#6xa z3kU}8CnVA`}WH3+mj5EBhB5hGAulid0uapTZVR&8gzz*z7aIZezPc+a6d+Ds& zh|~70Mtp(r>0b3%;~~-q0!iV#3kX9_oO!BO^T8}Qewj-VY3K)9rb+Xw7RezLC0kJ+ z6ea3#QBp0<%D89wiN|f#CTQ}~zU4TEsoRrTEs)78E;(jtpKvAaJceXu2y};(bn{NueN5b z2S<{#OtE1?SjP~vPcLDRtVgM~2ta8vM*jM(FKD}y$mZ6yg<2be8mGZ{=e*mn?O_6~ z$j(-X_o{@GP5o+`|8^`7l3L7ks5UK6LNuS2^5AJ$_TPbt4%Md=rKZX#1#2FwaB^4k zZBVx@$a&XccYCb+Wp?{Dy%TY9{xQx8qpNr)e1lUl3^>e$qwV1cj7&E*HQe^q-}~z% z!Iy=RI`RFK-dfvWE;wTU*!56)Q8{Ir4>Ct5J3^z83uB9@p{NnQYC{h}ohLo_ut}b& z8WDLPKahzWcE)t9O)KD&KTNs2n}Dj`uz^n3wi5h~jD>4FPYrI(H+-NJP*81_g?9YT zbx(5b%QPI7h%=*AOa-K_GD!n=tY0aj-p{okmsIUTX~ZLABqsbSf4}&t9DH>f#G)`8 z6)SP!Z;><-_A`uKm(v6rUlw>3J`k!;fo71uX9V6Lwhu-u>bC5zC3z?Un*`!gQMi0E zW0Zk!Yl&(kdkkXK9!eFdC`of23+~F}N@;fBPqb-CA5lgz%Sa?bL1Z_LQ-l^80 zQg>_L^d_5@Gt0bh+2AlXC9h4gWH)TEn@UYh_x1?oQ!5cn;N|A?#jj4*%A6BxzK;gtw0mJ?kDz94V+j#t&*-KZ#0wH1 zOQJ{}CAH z?yeg=z4TJ4fUJ8B%ASx{m<;qnPziVky62}Wh{ z%3!oDaw;^$V5r=ce|cH`Z(j!K#!y4c@YWVtZ8>3OHN^uZOJ_9=?k}Q=R7(^7NH>?k zA1oz6z;Mek(@x`5tkJSr*fySwjU@=d+%_rN+)+hMcDR3JZ?ISsbzy%y_`H=WwPjP4 z;6NO?4F}Lr6boN0$q9W%z=Qy|mu$7`=27|$6$NK7cJHEyl7hXQKKXCc@wgMSo` zT&&c0L0?t3F~MwvdZ^jL&62})prNM#IN|VE^;Lhe^fVcB&mUHPoG937B>_Q*9Cb_s zN1cLHJJ^7eMS*+~Zc!0bhn94%!~4MR)m)1W54@ros(W4#vaY@%yB;l$VnMSOs+)5@ z1gAd8Dz|nF%?X$jx9)_lRX9;avtX>9rcVktm*RxmrBBIdka+ee%^!#OL4+*6ywE3D zK;24)oPpCIzNKnWpn36;)<3TKL@=_KVCUa3d~;%IAa))yDX7ZE#Au#M_43A^eE!5i zuw5ep42bBPOcausS?i}OUShOno&&+R#ac;85wmMhA-R-fGIT|oXx{-#yh zONdBoZ?0#6xuT&1;ktLmt0*rV_`$(sF0?jPgU!v)D7_!l)ZC5(V#X}OZmPit8x)2t zzV@o=GfQ%XZu?h6A|XOAh-QUDKhYk77Dbe{b^P4ykee!P>i%t@_+Dx72QYOF^w48d za^<<}-=>`fJJGUhwBG4bymQ^nzQLB(XsBF1_I6WC}BW5q0X6jy5kXExcVcoT;lOS}Ul{uVRT@(V8-0_~wnC zmi9?*G0 z7txxWox^%nMtgd>-(VF06^FhlZB1VDn%4`8j4n z?+jfU$VrWC)7qqt$v~b!Nn|t%*>>&Wx-m}-#0Rk3edr8PPR)pP3L4nfI0tcfqGx>w zo>5C4e>Az~6A7Wj)5adlO67{FS=K#b&%9d~Wr)7ogkNtI|*%G3{$}r8hk-`(C=P_+_K${RN#sqJA^D&!!)jUC%x; zfg1}~u^Q&A2;EJJm-^ELsf4r;+|(?VoLZ{EQt9}4eE6l&O?plnvYS3#HOgQ)LvJ@? zy*P~v*!UZ`IlaNIRFMMA!$oem2e!vWe#lJa94bao0f}c)tD7hRC_SqKM9P*4L)7|> zcW3F&HUT0@3csl`5DdV#W=*Q$VxVb$I1O2;()MsoT?zK{-0!c)%b|;tuNS-6nVbxE zxQ;E z{|NKQg#4h4()<<%Ac?aLWU76F<`Bi4kKn@8K^v&j@Y7e-Xzq|2v$2x)Pc}yM7Q-{x zC|Nc+iG^;Z$$ttMr1%VcB(fnvP2UM0MMJ?)gCCd5$+I9ITw|f1_Nsr~5A1y5lYYa` zt-<#^`nqp@>@mOYC8v5#2J}k#AA^l@$vWfMAU6cVmmQ`FDYMT){Cd-ME6nTLgktg$ zI9?Y;ElzVZ(0POm&c=(2lZmFu3#k-ZJEuIl8C`eMa-gNo9ys3!39_18n)P<~bM$<^ z9-6!O3l_SOas&Kk8`~Mw=!+CbuWj#bPeJJ@i89UBG@T@r^H4ClMZ0WP*igL&LMDm{M z$uwpomzx!AlVZJrb6bR}>G2A?KDy}?xs*TmMAk2Hw<8E?f$sb$jvyWYw(Va%7!MB@6OFJK4dRjjy${{;MQY1 zZOm=AUb!}~s&y1EMySU2%x``ySkx&V5xosW**3)nClA_n4U(n~ORTJ{rc2}lfcnM9 zsP_Dv*=j~73*tedW@i)u&^xJfQojeRZf*E=-U0soJuxHFsYDVd1A-7hbc(FPooux4 zbCd=h>tA~2&g3~fyn6UwD?*MhU{=%y7qPtf%=T5)#rh)4FCM>8)hDj1w8p7ENT=D! z!M~=~tpc?gb8^8k=;vM?;td{L(@v4w!V9LH5b-+uh9Mn3h?s(*uh-gQ^=Fn9uFo=- z;7{L+t0@2I3W$Ur`%yjYMb4bvEtJoce3Ml!;5%)QVvi{9tfEF=YQM)74_|q|=lc4_ zX8MQI%;jyy&&SMuByg+Q_eDv6buK6&!AZpPTIKuw<@eA{|$h=#n}lAjjL z@$M681i2MxpCNbxa$_ef6n3xJoDWWf!W53WPy>m!vH%|X+n}V$ClJ{S#jt`KXVT^0 z6I*Eo^($&A@g`j70pY$afvnKSItJZs>^Su~^DPw%hu%)<8Z5j zqqHm|wu#r>sP@bpo6Ks3IyL>ZN4LVU_j^))T9}K`45C5Z7AT_wgjKvSl>5ZzoF}H6D>Dxdto2f^dEc&wenYkCFzbKJlW)!?))o7mP@%(c6{$UB2Dco`HOKc4u2oMB zr{qzvz#Gw46gP>J2QGJqm{vR7z2XL~6v3Z$Gg$yXbOo z{~7sgQG2XV&Ogi5UU1`;a>^JoNElg*<9<0NqZaR@NVZP4g`j==J$H9?*x;-^d{@f_ zO#ExTsv^;9ICB1wKE@%FZE+%n|C{}IZJl+%PfT1dgJ=!(Sjdwz;|q@mDu94OqM?ct z_W7s&))xi32N+t#uYD~b3YrreRq0}NE&FPT%%g?&-TG>#p{L$4^9vECwM6cX{T2^O z5%|Z=}?3VVA^{J~5?O`r8` zWH5TY2$a3N3623Kd@+Sg*1HFk&}fobdjX1T>6l#FUSp7E+xsXD{LlHJ9DLQxUwt!6 zu$d#ne{Vb254mqM03@7Ahvyf)#w>tKtJDb~22i@>t{ZVmDFE)3Eqs8Tp82^ha5HN1q4M2b>#RY`8^@ z*N4h@JeD~6564#cp99a$Jn3}09-xQz@R5ke z{QlF#WnHoX=TXwI!WOpK6GjGy)4Z{VB_XClkQ4bA?3FtIQd;?|va=M5B17@8y$Tv# zbC9vVS9E&$^Ipd9WSQ`~542wxp&e3x`OZXlp7d z|9NIKjAl7O{wR-6=tk0$5(Mm<~ISPFDlp)ImF3ufFDb1G&(Jx zn%Wd+rnZ=j4-}KK;vY7sYUBr-Bqz#C)!4D4#zR%^Ns2@H7M^u!)vs^ihS!fpOn~h^ zkYI-~a$A*24( zPz0StHDI+eSati-mxU_bSU%v-BrUBxIOv9hsUj%5WLjlF+uCi2YM5A-9mPC(*E2J) zFsB|>PrSDTCHyy{ycGcQS^1 zMgLhGf3$efa;mS`+_i}<#7QTr{+(3&S0_g&{Tey+BSAjRjFr{hgtfgXyTwV#UKxU9 z9w8QFIwF5GCKWx83ND&xu(rCuXo!08(b6$Q zuhdcfLp7#lxa(7;W#jmWNNK{FbHkL?R5CYT--SkgxrrB3rWbciE$IiI-5c5u z06}yRJ>F8kfBOsHjH~q*z}L^KV-28ox3%#N5JVbb3;M+_IAZ>fuB)7i4mO#`{Fgj9 z{X=tV3CP}olpwsuy2PGnlL}Rn%6490!P|a?nmA0|#oJ1c?{eD4arGV$%SBH|qs_rj zRG~)$K=o#Qwwb1Um8yZ5(mpClfNdAp%v1-!@sFjCnSJ9!!~>$6>3C{-fEcQ0=dJ7f zEO$x4HKp#y50+EV(fBwnxs4WB>g4rTZdN6n+6Nwo^VI$pU%@Fv%)FRn?{q3=nM4|2 zOi3M*gz{Tawwj_p;r6zlIu2q?Y)nqSv$pr@;A4q_WW7Oee9N#Yv9;yrMK5UZLzn%% zvcgCyNS#@y$hUs=-0+t-5bq%L+iZs>$d`7YbIk<+CXo5#x5g+`k3-pHLcLC5(bl(2WZD0ouNRIwEFTFn zmF+k|zg@6O%Xp9)4TIbOD3ZsbL-UKNqFQRa_9J1Za*Rf)q_S!tCScu!T_quF!>swh zrY>MRjw{?7Pc68hv^z%rIZY=D*B2J8eEAR7Iv>@@cV0k>gjH+Lk^{UA(S16Gz5X6e zCzTi3wh(2tA{YJ^Xo-CyF}+cx>-JKBJ^Ri%n1uwUX)^ob3_G+71<*zJ|CQpG?+05f3t3btjWx*PM@1G73tKeb!~f>Sf8zwse)1!~ zy7Qm@cDHQ*d9T5LtN09kU$D+$;1nFV0j7cw8z)wPKidqB*p4NZ{7w7jJi@_&_VoL( zfu#l2TqBfu3zfBL-=QTqH7#om(&7X4Q8-ssT5kZjFgp(~;dU_JVoAB<&xH@u(DIL zwN6zeT4*G!(LgWPGsP$If&*dr(O%M6%+SWW4~OCB*s$_Bb+wsUj0HFCq2ae?;CKAy z-v2WbcynNYn*D#b+bx~{^!mMK|6j$&DS1uJ=|K{{Q8p35{_|S9nhyJkxhYwd=%m8l zMYfV`a6D|7Uv#@UUw@&Q=gfn7;b)*YxhXmcqGC|xc?CpI_Bw-0`zo>UZA z*SY~IlG*`6ool1w?96>!$LF$k1=yXk4a&+&?9ig=qp4ZV4Xdj488@#8fU3V{YSZdFo~vD5^Oep~4K@`+|*`Jj^`Ea}-oHPXOrWp=OhP9Q%-! z`YSt9T60&7z~9V1%E6FL5?=aBJr%YubgvW0#?-`5?aXhR5vFIXlC>3p641tT_knbo zSt_eA(DEBV{~YB;m?~;E_XVqf+*h#`T+KR)pKx_u3eCwiu?zMth0e+?byE@o?7Rd#yMDcm>P#gf1I`9Be!yirO?I-?1=TK?a+{Xg&S?KS*=C12t6 zxyA#{1jix6L|l|%g0QgZEu3c=3T2u+jyZW$NDx&eW&FQT$kJHsH`4 zbJ_6xd-&$(|8$O*-}0-||LQ&K+xUOC+t~kE$#=6tdyn=ra?6>U#{R|H7iRx9iz{_& zK0rQGG4-H5A>lTl0i25*6W&5Hs58w4R01?BBA>f02B3{nOz5%6IQ5`r(n zK)5L%V!}646jKDk7z8L`d`iQ31P+qqfJ`s}1hO%&eGFr8 z@=BCdU00eU5#r)Ej7)=~D(SnFGLige84BZ3ZV&Vcc#X**3;>4*@klgE^igc-omO_5 zHwoi?(ADzT1YYffy?%eE-vVGrrRlttR!R^IKE%=WF~&^1<-ev3O|nUD;J`A!x%YqS z`^!H6d)6=Q|2uol{=bsXvHzEi8kb}0{c2i!z0>+}F7Z`62AzHTuQYSKY&E2rhGB`1 z0gF3+puJ)214dHRss;pk^lzB5!;nai3P({o3bSY>6d%-}iYI*I=X_l#xERLLb@6%! zml<@pv0>{l;Z>>}n(yQy3HqHRl;3~%c6S^8 zzlzUk)=Swfe%N;|&g+GYI-|-HwE3TPXJy{Q^KQu=43Q_Ri)jTv^MKaS4lX40l-cSx zN8Sclx0Co;q*7wlznwMf*b0qTL!!>Ipeu$*45K$kYG8~2=K-KAW4$Gra99`t+fLC& z&Gdz_tn4F!!(r%~&O)70m2buC8djW|w-^R5U<6|y5pQF96C)$7Q0A4Z#15U>vf6@A z#2cSuA+?ptHzOA^_oIIpN+a!DEo7QjNcOTw>M0s7Fw6cdLD^5{OzY%54ul${Ynj9< z&hRsr1v#6ZMOG*w-_*I>EbCd9(^R|LG>57jTjyRgsn8|02C<~X;~2%-VXGOZX*5*4 zonGWBdbc?ynEow_|Dg0yu#EfP?S4uA-`(pq@4r^^xwmxXfuhP2hz%4o2NyHr#VL+1 zzRk-btKUn6dN&W-gy^E{U)|s@WBu>+_R9Jnjs4Hne9PAVP~`DWk;h{CA18%E+-$|E zf-s{3$Q1n#tNqK>|EOj>S$)*-T{@@an#q)Z;DJCw!stWiJRKksBZh>Uei&26aXy#P z;vm6cY-4m2c8fP@#;|#rzh-5W!O#Wssyd{6epIOgTB(H~b^#5fnuXLn_?uy5eF78o zF`OWr8d$UteCJB!z#Jn%17SQe6dg-ZmR+rqyXBIOWWs4+ZHgTz>+x3w?aX04s=mX- z)m2jNWThYa` zWUpVn#JQ3*%8s=Os_xIY<&UGWNUwa`p*~;Rpro!F5r(Re-lb*323|#`t_6@IIG<+X0FY zL+wNzU5hZ9`aVJd3N$vkdxeJkB+@BDio3trRa6T}%1%f{wCx8mFC#J;hnL+GdBtSOXrrXIAG{obqX-3=IYDaj?Na1ExT{P!wK)@bbs&|84B1(*fl z*Ujd!>uGFj;)YxmIo}GoskW-f`zejYn|rhwQenSfS|RGkI~-`r%R`E%GF7*QjdZxs zH{<@_;b1cy#h9S^>!`i|+p+Key4(GoX8&Ks_XHe4#t@NjOymrM%Q1?ajy+FcHu(iXt$I@IW5)gz;!ooc^)!0*UiuBOJy->j{X_NNjOnJrP#QP$0R) ze{Xcahd8Xxle7$CSMlN0<$t8+np9H6c5ff3EP{*PZB9KL(inFNc} zQFs5>eP*BkY=2VnNCHFvCP3&?Z-Nge|4+odI5`AEI0^ac zPY{8Oa~-L_O*^gDlPBO!Oc{)%MtZCD5d#Uv zw{)<`ArpgQZ#d%^rvol=@YHQd{5-#?DU?+M$Qc`#69kkBvYPqo`0A)L%a*57e;TVw z*74*C5V#u%o1?T)3LWE16+R{VC-J`QZ3;o=INO+0p-E3cD)_tL=|Fmg<-aF52#42V zB*WOp6J8EMJ|+QGuhyhRZ1CAAy_3^JfPLCAiDGL4;}k|)Nthtcxwn)D+N~&s8@#bk zz;F1HSDjL1Hd*chjBhV5B>}z;31yq$8mD|1(nvfh<3C9uAH(2l?)v2`2xB#1;*?@j zyjMh5aFRqw&JYi7kN)I0ShOHGB`;#)rf6q zc;9Y=BxSOxQ3?Uy48Rx>B%i;bIFM6!Fl6!_AlnpEP#lgZ0s|!8!d;80Lwv{r0I6yV z?SK;uVsym-WhmjRk>Pxd6bW#``z$leFl|1LeUvb|#RcoOHHjNytc#w7XB}@}-;$|} z*#Lij{pRh*SI1xU;0fsy06uU!>>URY0D!;op0ybq{ar`N@dCiytkf88m8Vr_^ZD8Y zz?@bwC!gX#ObG&Gi~~6>f+2_r*>APMOQ{kCfp!k0R*Ff5omLwh1Ob5R!RtGm#*E6b zf&REK(=s=&NC2j9d+{GPRag%Z+owi|5ox9E@1YBUXVDSRj&hE5gxro7bw~X z0UD&ERx5yXJiw3yw454%05L=+VH{Ex`kz8wOwRw6Z+XrCKYaRl^yy>H|6|GjDGPAQ zjQ8PU2YA|QX`gfD>c90^evjXb>p!1$Bee4e#qkVvOkMxo?ryjD%+~+h-fr~2SMuGo z0Pu{P2X7zTh*jrBA!Xh^_?(?c>$T`{DT2L;y?xN#%tJY3lt=u!3>Cw!6CUOn>VlhQ zd>RHkdhPJ!?CAKzyH_87d-drgw<>#UR3aC=Cl|aCJ`Mv%Xz1hkC5|xR#gowhu5WLG zUbnjmdj0+;=x!M8@WcW5KJZLmjQ2p4P#`S!A zBN7Hjm=;SOTXe^J(AzZvuCifLzpwf4QrV&fyM6sT@A?-VCNEy7u?j$uvMXu6p6cuA zzCif^cmxf_oO=hx+E9NPZ4Q|rB=3YlX4I+ML-3`{uS&mJ$y8d4A#c_&L{T91A&b>2 z^&e_;lL<_cFdh}BN;Y&%5FO(vu=DU-IFOytj>hcS;6ik9s>>6ulKL4>c@pgI!X0Q8 zCDI72td^e@duu13T1D^IU;W*^X;p077PaB1tcL{9YO|=gw883V_TQ%;iR~mNwT@01Hlr~KIZVN2I5-RAA-?$o51yZTJgiL_WT>rPE}VLsS_$O&skf$ooO+w`&0BCbxIQ}%Ho*%QNxJ>RkBLyWJ?ZYlz7NiWSDf2|A;Qgvf$E+4F|-EM_sUq0a;-=oX?QG(vIk zTGw#ki#xX?99T7oC&ndcRJEOKKD^0{ae&UK*v!s+97}EYX)~)6JwNq?^Y>G4Q|5nu zD%`r2XR3M{>~R(ag7-MEXR>~rUp&@v{_p!3==V5o-(j#{PQ^$B#x#Kh#mp*1gB{)v zH%$8OvG8sM7T$ThSlE!g-bwm>D7biT_?rR4Dtrqg#29M(5u`Nqt4B~??3D|GS0Q~V z&Vo?z{}I79m7B>-JjEe}J*hmAc+OK=?ke&qg1)es+Hq&9)UC5O{gQ zuH__0VO#?B!!U|IB+w5Tr~JO1e?(xwkz3XYfrPCz<1j$4iDRfV({c|Hrw}cVyW=my zze+F-lm!IQvp3>+4o4bf@<2%J*7DH*8wPA_BL=>#IZ{3e|KeTk+tQB@BXk&2dHo}+ zD0MfZcvbb(Hq$Y_)X?0E(FH2Cl_oHLpGFa_pfSpze;&poS5t&JQON*G!#dHkNhUCw z-ppKP%;>08RaP7x4z02(7gt$Y0e2T&;eg^OWlIrUM+BkxKE+mof@#J7a#xx4MM9S= zvhG>iXt!6hjrQH!Xm6Ux>J&>g6PZ;ce2*DlHr0}05Yi-qT3Qt+xrGFY$#(~s@tvTM zfUn-S1?J`)?3}?Bk7!iIlQd$XRWK%^(g0Dvymf#7@+W}|t!veavoKClb~dlP?<^U~moA6C7-co3Z&NOm#9|;;+jSJ#TKn*PELJ2e-FdH#fYM zx3^!x?d@IU&}u06hL0$C!ABGvNfom&9)V+MFh;#m2_+plC=Il0pj`v)8ffP%?mnU2 zB#dqHZs6X-hkJ#9DR}SK2Jba^PvbqQ-E@`^>^G=yiKy>`7}o~&HL$ONeMVdZ`yMRp zYarg75RZKG6TNPQ7Rv5+gYg=SXPxP(XL1d`TjHtSv3h#g06ZtnHUO^yct%_U@S1bI z2K3$eT(1)KbsN;zpg!lxUW5IXiT%{so&1btk{C_?R}Q zy=FNL+H25W9``uW-nTmKZFDvt?z5a<<~_je?YO~PfA>yE?Ylc+{XtNeve$lZPitnk zPqbR3^jex>7w=yhkfh3KKvDye^0)>hH6W>Cj2@YeV=YK39dT8nq<({v9xqC|M>WL< zj+4|wk_IWMavG%6Af-I6K}ro$su-h3rx7^!fznRZbC6#esPuTD5)6VL7%m+IK?9i@ z$kafl1~N5}Nv3EZ6R3ntrQ@Ya%=E0mOpg~c1q3HQFlag?I9VJuefM5Oc7AoQ`Tiqz zBl3@QfJlrOqKzAotegfXH#j+udz?7=+q_F?Q1a4Ha)5lHGBg8FZdJ9|6F$gMnQ3M_ zpEtNZMk92Ulu0Q^zus2oDIn<26j8P{ovIRo6GT{8YFD{!MkC~yP8rN{PQl==P@r8U zKY87I`EvWEqZEU{L>8}<;&uT#P%Rf*Nyp^1Q@Sw+NwnGI@M8E%jD)YyN*ALE4xaz#)LTn%ApI!HlAoV?xpVzf z?^cUP%Mgqn&#P>BG0NS%=F>uuY{!}6u5=;quf?eWRdchM)%N_wZ$JTz5o~0Z>o>AoEIJ@uJr|IM@B#@z-ha&5M1P2RRD!QYz^UdaQ zh4RoW;*g)Dcaen()0k~L_h)$O<x6bV`!3aSZe@1$s%)JVlZAmB}6jH0$EZNA)l4+5@mO|Y_bo$BR5^@-N{Vg zpdTvv;v zBKr5NuVzd>o83a_2wmAp>3NuZBGJip>^oYX5stwn?5)I@OBhAEIcvi-ig?G{ou_7f zcbV@4%Zy|`(Q9XC(6l*YPI4uOtK)BV+^tXv8isMGWjRhKXQFj9XJlbK#0zmOpCHIk za5lI;I}hXF`KfmZ(FDgQ;!Mtn<(UfF3b&iq{!>fxMedVRe1d~{NZm?aL*Rv3(NzC% zN6}=;Igj}rk`N9e^iSS_S7NJc7sA}6lP}iZ-4?6De&*CNI1($8{xcg?uNYeqp%#Xm z3iA|F8pLTj`7}&BrliFlN&j z+O$rnR=B!A%2Aaa_$Funo>{P2OK;cwk#lD6Szxv|YfEAH{^F8k z#vgK&IXfY!ZS>{_FhpWVxKKpGm<@q<>itAdJrAtYG3?TGlE?f++domywbs{bs@>Sw z*Z^LfMv(_PlKFK4NXG*VNkBURVu(z_IHWA}KZP8Ui$-2(oGUauy-S47PZ*TawAZ?A d{a5ofU-LCz^F8eEe*ypi|Njlt`zHXR1ps_Mp$Py0 literal 0 HcmV?d00001 diff --git a/helm/Chart.yaml b/helm/Chart.yaml index 296b7c1cf..32ffceac7 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: parseable description: Helm chart for Parseable OSS version. Columnar data lake platform - purpose built for observability type: application -version: 2.8.2 +version: 2.8.3 appVersion: "v2.8.0" icon: "https://raw.githubusercontent.com/parseablehq/.github/main/images/logo.svg" diff --git a/helm/templates/ingestor-statefulset.yaml b/helm/templates/ingestor-statefulset.yaml index 73185cd2b..e3835c69c 100644 --- a/helm/templates/ingestor-statefulset.yaml +++ b/helm/templates/ingestor-statefulset.yaml @@ -56,7 +56,7 @@ spec: done echo "Query service did not become ready in time. Exiting." exit 1 - terminationGracePeriodSeconds: 10 + terminationGracePeriodSeconds: {{ .Values.parseable.highAvailability.ingestor.terminationGracePeriodSeconds | default 30 }} serviceAccountName: {{ include "parseable.serviceAccountName" . }} {{- with .Values.parseable.highAvailability.ingestor.nodeSelector }} nodeSelector: diff --git a/helm/templates/querier-statefulset.yaml b/helm/templates/querier-statefulset.yaml index de1f28298..378f23937 100644 --- a/helm/templates/querier-statefulset.yaml +++ b/helm/templates/querier-statefulset.yaml @@ -37,7 +37,7 @@ spec: {{- .Values.parseable.podLabels | toYaml | nindent 8 }} {{- include "parseable.querierLabelsSelector" . | nindent 8 }} spec: - terminationGracePeriodSeconds: 10 + terminationGracePeriodSeconds: {{ .Values.parseable.terminationGracePeriodSeconds | default 30 }} serviceAccountName: {{ include "parseable.serviceAccountName" . }} {{- with .Values.parseable.toleration }} tolerations: diff --git a/helm/values.yaml b/helm/values.yaml index 7448d93a1..c105e55bc 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -28,6 +28,7 @@ parseable: app: parseable component: ingestor count: 3 + terminationGracePeriodSeconds: 30 env: RUST_LOG: warn ## Use this endpoint to send events to ingestors @@ -50,6 +51,7 @@ parseable: ## Add environment variables to the Parseable Deployment env: RUST_LOG: warn + terminationGracePeriodSeconds: 30 ## Enable to create a log stream and then add retention configuration ## for that log stream # logstream: diff --git a/index.yaml b/index.yaml index fa5bccbcd..1f90da482 100644 --- a/index.yaml +++ b/index.yaml @@ -3,7 +3,7 @@ entries: pai: - apiVersion: v2 appVersion: 0.2.0 - created: "2026-06-08T13:03:45.109378+08:00" + created: "2026-06-11T00:47:35.334268+08:00" description: Parseable Auto Instrumentation (PAI) operator for Kubernetes digest: fd04518e8fc9e25d3fa876971a38b4c65005a207ba9440ae8ce981cd070646d9 home: https://github.com/parseablehq/pai @@ -21,7 +21,7 @@ entries: version: 0.2.0 - apiVersion: v2 appVersion: 0.1.0 - created: "2026-06-08T13:03:45.109233+08:00" + created: "2026-06-11T00:47:35.334125+08:00" description: Parseable Auto Instrumentation (PAI) operator for Kubernetes digest: 9443e75bef96a424fd5063ddd02cee18e6050207d73e505f66be467287c9f7f7 home: https://github.com/parseablehq/pai @@ -40,7 +40,32 @@ entries: parseable: - apiVersion: v2 appVersion: v2.8.0 - created: "2026-06-08T13:03:45.221758+08:00" + created: "2026-06-11T00:47:35.451186+08:00" + dependencies: + - condition: vector.enabled + name: vector + repository: https://helm.vector.dev + version: 0.20.1 + - condition: fluent-bit.enabled + name: fluent-bit + repository: https://fluent.github.io/helm-charts + version: 0.48.0 + description: Helm chart for Parseable OSS version. Columnar data lake platform + - purpose built for observability + digest: acb81cac5d18686383b3146274172a38f4279e7da7d7834550693ccb71743ca0 + icon: https://raw.githubusercontent.com/parseablehq/.github/main/images/logo.svg + maintainers: + - email: hi@parseable.com + name: Parseable Team + url: https://parseable.com + name: parseable + type: application + urls: + - https://charts.parseable.com/helm-releases/parseable-2.8.3.tgz + version: 2.8.3 + - apiVersion: v2 + appVersion: v2.8.0 + created: "2026-06-11T00:47:35.449456+08:00" dependencies: - condition: vector.enabled name: vector @@ -65,7 +90,7 @@ entries: version: 2.8.2 - apiVersion: v2 appVersion: v2.8.1 - created: "2026-06-08T13:03:45.22012+08:00" + created: "2026-06-11T00:47:35.447523+08:00" dependencies: - condition: vector.enabled name: vector @@ -90,7 +115,7 @@ entries: version: 2.8.1 - apiVersion: v2 appVersion: v2.8.0 - created: "2026-06-08T13:03:45.218159+08:00" + created: "2026-06-11T00:47:35.445865+08:00" dependencies: - condition: vector.enabled name: vector @@ -115,7 +140,7 @@ entries: version: 2.8.0 - apiVersion: v2 appVersion: v2.7.2 - created: "2026-06-08T13:03:45.216492+08:00" + created: "2026-06-11T00:47:35.443868+08:00" dependencies: - condition: vector.enabled name: vector @@ -140,7 +165,7 @@ entries: version: 2.7.2 - apiVersion: v2 appVersion: v2.7.1 - created: "2026-06-08T13:03:45.21479+08:00" + created: "2026-06-11T00:47:35.442097+08:00" dependencies: - condition: vector.enabled name: vector @@ -165,7 +190,7 @@ entries: version: 2.7.1 - apiVersion: v2 appVersion: v2.6.6 - created: "2026-06-08T13:03:45.212753+08:00" + created: "2026-06-11T00:47:35.439976+08:00" dependencies: - condition: vector.enabled name: vector @@ -190,7 +215,7 @@ entries: version: 2.6.6 - apiVersion: v2 appVersion: v2.6.5 - created: "2026-06-08T13:03:45.211094+08:00" + created: "2026-06-11T00:47:35.43822+08:00" dependencies: - condition: vector.enabled name: vector @@ -215,7 +240,7 @@ entries: version: 2.6.5 - apiVersion: v2 appVersion: v2.5.13 - created: "2026-06-08T13:03:45.200212+08:00" + created: "2026-06-11T00:47:35.427212+08:00" dependencies: - condition: vector.enabled name: vector @@ -240,7 +265,7 @@ entries: version: 2.5.13 - apiVersion: v2 appVersion: v2.5.7 - created: "2026-06-08T13:03:45.20911+08:00" + created: "2026-06-11T00:47:35.436261+08:00" dependencies: - condition: vector.enabled name: vector @@ -265,7 +290,7 @@ entries: version: 2.5.7 - apiVersion: v2 appVersion: v2.5.6 - created: "2026-06-08T13:03:45.207505+08:00" + created: "2026-06-11T00:47:35.434632+08:00" dependencies: - condition: vector.enabled name: vector @@ -290,7 +315,7 @@ entries: version: 2.5.6 - apiVersion: v2 appVersion: v2.5.5 - created: "2026-06-08T13:03:45.205892+08:00" + created: "2026-06-11T00:47:35.43296+08:00" dependencies: - condition: vector.enabled name: vector @@ -315,7 +340,7 @@ entries: version: 2.5.5 - apiVersion: v2 appVersion: v2.5.4 - created: "2026-06-08T13:03:45.203943+08:00" + created: "2026-06-11T00:47:35.430915+08:00" dependencies: - condition: vector.enabled name: vector @@ -339,7 +364,7 @@ entries: version: 2.5.4 - apiVersion: v2 appVersion: v2.5.3 - created: "2026-06-08T13:03:45.20227+08:00" + created: "2026-06-11T00:47:35.429297+08:00" dependencies: - condition: vector.enabled name: vector @@ -363,7 +388,7 @@ entries: version: 2.5.3 - apiVersion: v2 appVersion: v2.4.0 - created: "2026-06-08T13:03:45.198621+08:00" + created: "2026-06-11T00:47:35.425463+08:00" dependencies: - condition: vector.enabled name: vector @@ -387,7 +412,7 @@ entries: version: 2.4.0 - apiVersion: v2 appVersion: v2.3.3 - created: "2026-06-08T13:03:45.196995+08:00" + created: "2026-06-11T00:47:35.423789+08:00" dependencies: - condition: vector.enabled name: vector @@ -411,7 +436,7 @@ entries: version: 2.3.3 - apiVersion: v2 appVersion: v2.3.2 - created: "2026-06-08T13:03:45.195026+08:00" + created: "2026-06-11T00:47:35.421822+08:00" dependencies: - condition: vector.enabled name: vector @@ -435,7 +460,7 @@ entries: version: 2.3.2 - apiVersion: v2 appVersion: v2.3.1 - created: "2026-06-08T13:03:45.19334+08:00" + created: "2026-06-11T00:47:35.420155+08:00" dependencies: - condition: vector.enabled name: vector @@ -459,7 +484,7 @@ entries: version: 2.3.1 - apiVersion: v2 appVersion: v2.3.0 - created: "2026-06-08T13:03:45.191377+08:00" + created: "2026-06-11T00:47:35.418125+08:00" dependencies: - condition: vector.enabled name: vector @@ -483,7 +508,7 @@ entries: version: 2.3.0 - apiVersion: v2 appVersion: v2.1.0 - created: "2026-06-08T13:03:45.18972+08:00" + created: "2026-06-11T00:47:35.416474+08:00" dependencies: - condition: vector.enabled name: vector @@ -507,7 +532,7 @@ entries: version: 2.1.0 - apiVersion: v2 appVersion: v1.7.5 - created: "2026-06-08T13:03:45.187602+08:00" + created: "2026-06-11T00:47:35.414676+08:00" dependencies: - condition: vector.enabled name: vector @@ -531,7 +556,7 @@ entries: version: 2.0.0 - apiVersion: v2 appVersion: v1.7.5 - created: "2026-06-08T13:03:45.185956+08:00" + created: "2026-06-11T00:47:35.412841+08:00" dependencies: - condition: vector.enabled name: vector @@ -555,7 +580,7 @@ entries: version: 1.7.5 - apiVersion: v2 appVersion: v1.7.3 - created: "2026-06-08T13:03:45.184297+08:00" + created: "2026-06-11T00:47:35.411224+08:00" dependencies: - condition: vector.enabled name: vector @@ -579,7 +604,7 @@ entries: version: 1.7.3 - apiVersion: v2 appVersion: v1.7.2 - created: "2026-06-08T13:03:45.182331+08:00" + created: "2026-06-11T00:47:35.409166+08:00" dependencies: - condition: vector.enabled name: vector @@ -603,7 +628,7 @@ entries: version: 1.7.2 - apiVersion: v2 appVersion: v1.7.1 - created: "2026-06-08T13:03:45.18069+08:00" + created: "2026-06-11T00:47:35.407443+08:00" dependencies: - condition: vector.enabled name: vector @@ -627,7 +652,7 @@ entries: version: 1.7.1 - apiVersion: v2 appVersion: v1.7.0 - created: "2026-06-08T13:03:45.178724+08:00" + created: "2026-06-11T00:47:35.405303+08:00" dependencies: - condition: vector.enabled name: vector @@ -650,7 +675,7 @@ entries: version: 1.7.0 - apiVersion: v2 appVersion: v1.6.3 - created: "2026-06-08T13:03:45.17705+08:00" + created: "2026-06-11T00:47:35.403607+08:00" dependencies: - condition: vector.enabled name: vector @@ -673,7 +698,7 @@ entries: version: 1.6.8 - apiVersion: v2 appVersion: v1.6.3 - created: "2026-06-08T13:03:45.175007+08:00" + created: "2026-06-11T00:47:35.401972+08:00" dependencies: - condition: vector.enabled name: vector @@ -696,7 +721,7 @@ entries: version: 1.6.7 - apiVersion: v2 appVersion: v1.6.3 - created: "2026-06-08T13:03:45.173404+08:00" + created: "2026-06-11T00:47:35.400047+08:00" dependencies: - condition: vector.enabled name: vector @@ -719,7 +744,7 @@ entries: version: 1.6.6 - apiVersion: v2 appVersion: v1.6.3 - created: "2026-06-08T13:03:45.171846+08:00" + created: "2026-06-11T00:47:35.398534+08:00" dependencies: - condition: vector.enabled name: vector @@ -742,7 +767,7 @@ entries: version: 1.6.5 - apiVersion: v2 appVersion: v1.6.3 - created: "2026-06-08T13:03:45.169949+08:00" + created: "2026-06-11T00:47:35.396737+08:00" dependencies: - condition: vector.enabled name: vector @@ -765,7 +790,7 @@ entries: version: 1.6.4 - apiVersion: v2 appVersion: v1.6.3 - created: "2026-06-08T13:03:45.168407+08:00" + created: "2026-06-11T00:47:35.3952+08:00" dependencies: - condition: vector.enabled name: vector @@ -788,7 +813,7 @@ entries: version: 1.6.3 - apiVersion: v2 appVersion: v1.6.2 - created: "2026-06-08T13:03:45.166634+08:00" + created: "2026-06-11T00:47:35.393619+08:00" dependencies: - condition: vector.enabled name: vector @@ -811,7 +836,7 @@ entries: version: 1.6.2 - apiVersion: v2 appVersion: v1.6.1 - created: "2026-06-08T13:03:45.165135+08:00" + created: "2026-06-11T00:47:35.391842+08:00" dependencies: - condition: vector.enabled name: vector @@ -834,7 +859,7 @@ entries: version: 1.6.1 - apiVersion: v2 appVersion: v1.6.0 - created: "2026-06-08T13:03:45.163611+08:00" + created: "2026-06-11T00:47:35.390374+08:00" dependencies: - condition: vector.enabled name: vector @@ -857,7 +882,7 @@ entries: version: 1.6.0 - apiVersion: v2 appVersion: v1.5.5 - created: "2026-06-08T13:03:45.161756+08:00" + created: "2026-06-11T00:47:35.388475+08:00" dependencies: - condition: vector.enabled name: vector @@ -880,7 +905,7 @@ entries: version: 1.5.5 - apiVersion: v2 appVersion: v1.5.4 - created: "2026-06-08T13:03:45.160224+08:00" + created: "2026-06-11T00:47:35.386943+08:00" dependencies: - condition: vector.enabled name: vector @@ -903,7 +928,7 @@ entries: version: 1.5.4 - apiVersion: v2 appVersion: v1.5.3 - created: "2026-06-08T13:03:45.158678+08:00" + created: "2026-06-11T00:47:35.385385+08:00" dependencies: - condition: vector.enabled name: vector @@ -926,7 +951,7 @@ entries: version: 1.5.3 - apiVersion: v2 appVersion: v1.5.2 - created: "2026-06-08T13:03:45.156791+08:00" + created: "2026-06-11T00:47:35.383487+08:00" dependencies: - condition: vector.enabled name: vector @@ -949,7 +974,7 @@ entries: version: 1.5.2 - apiVersion: v2 appVersion: v1.5.1 - created: "2026-06-08T13:03:45.155314+08:00" + created: "2026-06-11T00:47:35.381946+08:00" dependencies: - condition: vector.enabled name: vector @@ -972,7 +997,7 @@ entries: version: 1.5.1 - apiVersion: v2 appVersion: v1.5.0 - created: "2026-06-08T13:03:45.153649+08:00" + created: "2026-06-11T00:47:35.379743+08:00" dependencies: - condition: vector.enabled name: vector @@ -995,7 +1020,7 @@ entries: version: 1.5.0 - apiVersion: v2 appVersion: v1.4.0 - created: "2026-06-08T13:03:45.151903+08:00" + created: "2026-06-11T00:47:35.37826+08:00" dependencies: - condition: vector.enabled name: vector @@ -1018,7 +1043,7 @@ entries: version: 1.4.1 - apiVersion: v2 appVersion: v1.4.0 - created: "2026-06-08T13:03:45.150445+08:00" + created: "2026-06-11T00:47:35.376818+08:00" dependencies: - condition: vector.enabled name: vector @@ -1041,7 +1066,7 @@ entries: version: 1.4.0 - apiVersion: v2 appVersion: v1.3.0 - created: "2026-06-08T13:03:45.148661+08:00" + created: "2026-06-11T00:47:35.374898+08:00" dependencies: - condition: vector.enabled name: vector @@ -1064,7 +1089,7 @@ entries: version: 1.3.1 - apiVersion: v2 appVersion: v1.3.0 - created: "2026-06-08T13:03:45.14726+08:00" + created: "2026-06-11T00:47:35.373321+08:00" dependencies: - condition: vector.enabled name: vector @@ -1087,7 +1112,7 @@ entries: version: 1.3.0 - apiVersion: v2 appVersion: v1.2.0 - created: "2026-06-08T13:03:45.145808+08:00" + created: "2026-06-11T00:47:35.371428+08:00" dependencies: - condition: vector.enabled name: vector @@ -1110,7 +1135,7 @@ entries: version: 1.2.0 - apiVersion: v2 appVersion: v1.1.0 - created: "2026-06-08T13:03:45.14406+08:00" + created: "2026-06-11T00:47:35.369967+08:00" dependencies: - condition: vector.enabled name: vector @@ -1133,7 +1158,7 @@ entries: version: 1.1.0 - apiVersion: v2 appVersion: v1.0.0 - created: "2026-06-08T13:03:45.142627+08:00" + created: "2026-06-11T00:47:35.368496+08:00" dependencies: - condition: vector.enabled name: vector @@ -1156,7 +1181,7 @@ entries: version: 1.0.0 - apiVersion: v2 appVersion: v0.9.0 - created: "2026-06-08T13:03:45.141059+08:00" + created: "2026-06-11T00:47:35.366647+08:00" dependencies: - condition: vector.enabled name: vector @@ -1179,7 +1204,7 @@ entries: version: 0.9.0 - apiVersion: v2 appVersion: v0.8.1 - created: "2026-06-08T13:03:45.139568+08:00" + created: "2026-06-11T00:47:35.365297+08:00" dependencies: - condition: vector.enabled name: vector @@ -1202,7 +1227,7 @@ entries: version: 0.8.1 - apiVersion: v2 appVersion: v0.8.0 - created: "2026-06-08T13:03:45.138147+08:00" + created: "2026-06-11T00:47:35.363933+08:00" dependencies: - condition: vector.enabled name: vector @@ -1225,7 +1250,7 @@ entries: version: 0.8.0 - apiVersion: v2 appVersion: v0.7.3 - created: "2026-06-08T13:03:45.136413+08:00" + created: "2026-06-11T00:47:35.361855+08:00" dependencies: - condition: vector.enabled name: vector @@ -1248,7 +1273,7 @@ entries: version: 0.7.3 - apiVersion: v2 appVersion: v0.7.2 - created: "2026-06-08T13:03:45.135012+08:00" + created: "2026-06-11T00:47:35.360371+08:00" dependencies: - condition: vector.enabled name: vector @@ -1271,7 +1296,7 @@ entries: version: 0.7.2 - apiVersion: v2 appVersion: v0.7.1 - created: "2026-06-08T13:03:45.133544+08:00" + created: "2026-06-11T00:47:35.358445+08:00" dependencies: - condition: vector.enabled name: vector @@ -1294,7 +1319,7 @@ entries: version: 0.7.1 - apiVersion: v2 appVersion: v0.7.0 - created: "2026-06-08T13:03:45.13154+08:00" + created: "2026-06-11T00:47:35.35704+08:00" dependencies: - condition: vector.enabled name: vector @@ -1317,7 +1342,7 @@ entries: version: 0.7.0 - apiVersion: v2 appVersion: v0.6.2 - created: "2026-06-08T13:03:45.130069+08:00" + created: "2026-06-11T00:47:35.355534+08:00" dependencies: - condition: vector.enabled name: vector @@ -1340,7 +1365,7 @@ entries: version: 0.6.2 - apiVersion: v2 appVersion: v0.6.1 - created: "2026-06-08T13:03:45.128581+08:00" + created: "2026-06-11T00:47:35.353619+08:00" dependencies: - condition: vector.enabled name: vector @@ -1363,7 +1388,7 @@ entries: version: 0.6.1 - apiVersion: v2 appVersion: v0.6.0 - created: "2026-06-08T13:03:45.126687+08:00" + created: "2026-06-11T00:47:35.352214+08:00" dependencies: - condition: vector.enabled name: vector @@ -1386,7 +1411,7 @@ entries: version: 0.6.0 - apiVersion: v2 appVersion: v0.5.1 - created: "2026-06-08T13:03:45.125232+08:00" + created: "2026-06-11T00:47:35.350755+08:00" dependencies: - condition: vector.enabled name: vector @@ -1409,7 +1434,7 @@ entries: version: 0.5.1 - apiVersion: v2 appVersion: v0.5.0 - created: "2026-06-08T13:03:45.123443+08:00" + created: "2026-06-11T00:47:35.348834+08:00" dependencies: - condition: vector.enabled name: vector @@ -1432,7 +1457,7 @@ entries: version: 0.5.0 - apiVersion: v2 appVersion: v0.4.4 - created: "2026-06-08T13:03:45.122052+08:00" + created: "2026-06-11T00:47:35.347435+08:00" dependencies: - condition: vector.enabled name: vector @@ -1455,7 +1480,7 @@ entries: version: 0.4.5 - apiVersion: v2 appVersion: v0.4.3 - created: "2026-06-08T13:03:45.120645+08:00" + created: "2026-06-11T00:47:35.345914+08:00" dependencies: - condition: vector.enabled name: vector @@ -1478,7 +1503,7 @@ entries: version: 0.4.4 - apiVersion: v2 appVersion: v0.4.2 - created: "2026-06-08T13:03:45.11885+08:00" + created: "2026-06-11T00:47:35.344101+08:00" dependencies: - condition: vector.enabled name: vector @@ -1501,7 +1526,7 @@ entries: version: 0.4.3 - apiVersion: v2 appVersion: v0.4.1 - created: "2026-06-08T13:03:45.117488+08:00" + created: "2026-06-11T00:47:35.342695+08:00" dependencies: - condition: vector.enabled name: vector @@ -1524,7 +1549,7 @@ entries: version: 0.4.2 - apiVersion: v2 appVersion: v0.4.0 - created: "2026-06-08T13:03:45.116037+08:00" + created: "2026-06-11T00:47:35.341191+08:00" dependencies: - condition: vector.enabled name: vector @@ -1547,7 +1572,7 @@ entries: version: 0.4.1 - apiVersion: v2 appVersion: v0.4.0 - created: "2026-06-08T13:03:45.113982+08:00" + created: "2026-06-11T00:47:35.339193+08:00" dependencies: - condition: vector.enabled name: vector @@ -1570,7 +1595,7 @@ entries: version: 0.4.0 - apiVersion: v2 appVersion: v0.3.1 - created: "2026-06-08T13:03:45.112619+08:00" + created: "2026-06-11T00:47:35.3377+08:00" dependencies: - condition: vector.enabled name: vector @@ -1593,7 +1618,7 @@ entries: version: 0.3.1 - apiVersion: v2 appVersion: v0.3.0 - created: "2026-06-08T13:03:45.111179+08:00" + created: "2026-06-11T00:47:35.336287+08:00" description: Helm chart for Parseable Server digest: ff30739229b727dc637f62fd4481c886a6080ce4556bae10cafe7642ddcfd937 name: parseable @@ -1603,7 +1628,7 @@ entries: version: 0.3.0 - apiVersion: v2 appVersion: v0.2.2 - created: "2026-06-08T13:03:45.111004+08:00" + created: "2026-06-11T00:47:35.336106+08:00" description: Helm chart for Parseable Server digest: 477d0dc2f0c07d4f4c32e105d4bdd70c71113add5c2a75ac5f1cb42aa0276db7 name: parseable @@ -1613,7 +1638,7 @@ entries: version: 0.2.2 - apiVersion: v2 appVersion: v0.2.1 - created: "2026-06-08T13:03:45.110512+08:00" + created: "2026-06-11T00:47:35.33558+08:00" description: Helm chart for Parseable Server digest: 84826fcd1b4c579f301569f43b0309c07e8082bad76f5cdd25f86e86ca2e8192 name: parseable @@ -1623,7 +1648,7 @@ entries: version: 0.2.1 - apiVersion: v2 appVersion: v0.2.0 - created: "2026-06-08T13:03:45.110391+08:00" + created: "2026-06-11T00:47:35.33538+08:00" description: Helm chart for Parseable Server digest: 7a759f7f9809f3935cba685e904c021a0b645f217f4e45b9be185900c467edff name: parseable @@ -1633,7 +1658,7 @@ entries: version: 0.2.0 - apiVersion: v2 appVersion: v0.1.1 - created: "2026-06-08T13:03:45.110273+08:00" + created: "2026-06-11T00:47:35.335254+08:00" description: Helm chart for Parseable Server digest: 37993cf392f662ec7b1fbfc9a2ba00ec906d98723e38f3c91ff1daca97c3d0b3 name: parseable @@ -1643,7 +1668,7 @@ entries: version: 0.1.1 - apiVersion: v2 appVersion: v0.1.0 - created: "2026-06-08T13:03:45.110158+08:00" + created: "2026-06-11T00:47:35.335125+08:00" description: Helm chart for Parseable Server digest: 1d580d072af8d6b1ebcbfee31c2e16c907d08db754780f913b5f0032b403789b name: parseable @@ -1653,7 +1678,7 @@ entries: version: 0.1.0 - apiVersion: v2 appVersion: v0.0.8 - created: "2026-06-08T13:03:45.110047+08:00" + created: "2026-06-11T00:47:35.334996+08:00" description: Helm chart for Parseable Server digest: c805254ffa634f96ecec448bcfff9973339aa9487dd8199b21b17b79a4de9345 name: parseable @@ -1663,7 +1688,7 @@ entries: version: 0.0.8 - apiVersion: v2 appVersion: v0.0.7 - created: "2026-06-08T13:03:45.109937+08:00" + created: "2026-06-11T00:47:35.334882+08:00" description: Helm chart for Parseable Server digest: c591f617ed1fe820bb2c72a4c976a78126f1d1095d552daa07c4700f46c4708a name: parseable @@ -1673,7 +1698,7 @@ entries: version: 0.0.7 - apiVersion: v2 appVersion: v0.0.6 - created: "2026-06-08T13:03:45.10983+08:00" + created: "2026-06-11T00:47:35.334754+08:00" description: Helm chart for Parseable Server digest: f9ae56a6fcd6a59e7bee0436200ddbedeb74ade6073deb435b8fcbaf08dda795 name: parseable @@ -1683,7 +1708,7 @@ entries: version: 0.0.6 - apiVersion: v2 appVersion: v0.0.5 - created: "2026-06-08T13:03:45.109722+08:00" + created: "2026-06-11T00:47:35.334637+08:00" description: Helm chart for Parseable Server digest: 4d6b08a064fba36e16feeb820b77e1e8e60fb6de48dbf7ec8410d03d10c26ad0 name: parseable @@ -1693,7 +1718,7 @@ entries: version: 0.0.5 - apiVersion: v2 appVersion: v0.0.2 - created: "2026-06-08T13:03:45.109605+08:00" + created: "2026-06-11T00:47:35.334502+08:00" description: Helm chart for Parseable Server digest: 38a0a3e4c498afbbcc76ebfcb9cb598fa2ca843a53cc93b3cb4f135b85c10844 name: parseable @@ -1703,7 +1728,7 @@ entries: version: 0.0.2 - apiVersion: v2 appVersion: v0.0.1 - created: "2026-06-08T13:03:45.109495+08:00" + created: "2026-06-11T00:47:35.334379+08:00" description: Helm chart for Parseable Server digest: 1f1142db092b9620ee38bb2294ccbb1c17f807b33bf56da43816af7fe89f301e name: parseable @@ -1713,8 +1738,8 @@ entries: version: 0.0.1 parseable-enterprise: - apiVersion: v2 - appVersion: v2.8.1 - created: "2026-06-08T13:03:45.235107+08:00" + appVersion: v2.8.0 + created: "2026-06-11T00:47:35.464421+08:00" dependencies: - condition: vector.enabled name: vector @@ -1726,8 +1751,33 @@ entries: version: 0.48.0 description: Helm chart for Parseable Enterprise version - Needs a license to run. Please contact sales@parseable.com for more details. - digest: 562470cd36439ec088aadee756e92efd543787e1e9172da183df827ef5e0657a - icon: https://raw.githubusercontent.com/parseablehq/.github/main/images/logo.svg + digest: f3eda4c6f8ebbdc247f3a100f3d63ce4c208eec32a140883110736b7ba68ec86 + icon: https://raw.githubusercontent.com/parseablehq/.github/main/images/new-logo.svg + maintainers: + - email: hi@parseable.com + name: Parseable Team + url: https://parseable.com + name: parseable-enterprise + type: application + urls: + - https://charts.parseable.com/helm-releases/parseable-enterprise-2.8.3.tgz + version: 2.8.3 + - apiVersion: v2 + appVersion: v2.8.0 + created: "2026-06-11T00:45:13.339133+08:00" + dependencies: + - condition: vector.enabled + name: vector + repository: https://helm.vector.dev + version: 0.20.1 + - condition: fluent-bit.enabled + name: fluent-bit + repository: https://fluent.github.io/helm-charts + version: 0.48.0 + description: Helm chart for Parseable Enterprise version - Needs a license to + run. Please contact sales@parseable.com for more details. + digest: 116c1c16481a1751d3babc6c1c62d91a571f5ba7f545d5599e37b1141a79f990 + icon: https://raw.githubusercontent.com/parseablehq/.github/main/images/new-logo.svg maintainers: - email: hi@parseable.com name: Parseable Team @@ -1739,7 +1789,7 @@ entries: version: 2.8.1 - apiVersion: v2 appVersion: v2.8.0 - created: "2026-06-08T13:03:45.233405+08:00" + created: "2026-06-11T00:47:35.462472+08:00" dependencies: - condition: vector.enabled name: vector @@ -1764,7 +1814,7 @@ entries: version: 2.8.0 - apiVersion: v2 appVersion: v2.7.3 - created: "2026-06-08T13:03:45.231589+08:00" + created: "2026-06-11T00:47:35.460611+08:00" dependencies: - condition: vector.enabled name: vector @@ -1789,7 +1839,7 @@ entries: version: 2.7.3 - apiVersion: v2 appVersion: v2.7.2 - created: "2026-06-08T13:03:45.22962+08:00" + created: "2026-06-11T00:47:35.45885+08:00" dependencies: - condition: vector.enabled name: vector @@ -1814,7 +1864,7 @@ entries: version: 2.7.2 - apiVersion: v2 appVersion: v2.7.1 - created: "2026-06-08T13:03:45.227818+08:00" + created: "2026-06-11T00:47:35.456778+08:00" dependencies: - condition: vector.enabled name: vector @@ -1839,7 +1889,7 @@ entries: version: 2.7.1 - apiVersion: v2 appVersion: v2.6.6 - created: "2026-06-08T13:03:45.225599+08:00" + created: "2026-06-11T00:47:35.455015+08:00" dependencies: - condition: vector.enabled name: vector @@ -1864,7 +1914,7 @@ entries: version: 2.6.7 - apiVersion: v2 appVersion: v2.6.6 - created: "2026-06-08T13:03:45.223825+08:00" + created: "2026-06-11T00:47:35.452962+08:00" dependencies: - condition: vector.enabled name: vector @@ -1887,4 +1937,4 @@ entries: urls: - https://charts.parseable.com/helm-releases/parseable-enterprise-2.6.6.tgz version: 2.6.6 -generated: "2026-06-08T13:03:45.109001+08:00" +generated: "2026-06-11T00:47:35.333907+08:00" From 8f65159d8630db5ea26c2ea60b73072724492dcb Mon Sep 17 00:00:00 2001 From: parmesant Date: Thu, 11 Jun 2026 16:36:49 +0530 Subject: [PATCH 24/47] fix: deadlock in metric pruning (#1674) * fix: deadlock in metric pruning in case schema hashes grow beyond NUM_CPU, deadlock can occur during sort and prune * chunk arrow files for conversion * coderabbit suggestion --- src/handlers/http/ingest.rs | 8 +++ src/parseable/streams.rs | 118 +++++++++++++++++++++++++++++------- 2 files changed, 104 insertions(+), 22 deletions(-) diff --git a/src/handlers/http/ingest.rs b/src/handlers/http/ingest.rs index 6f5543783..a98e8ab3a 100644 --- a/src/handlers/http/ingest.rs +++ b/src/handlers/http/ingest.rs @@ -24,6 +24,8 @@ use actix_web::{HttpRequest, HttpResponse, http::header::ContentType}; use arrow_array::RecordBatch; use bytes::Bytes; use chrono::Utc; +use once_cell::sync::Lazy; +use rayon::{ThreadPool, ThreadPoolBuilder}; use tokio::sync::oneshot; use tracing::error; @@ -55,6 +57,12 @@ use super::modal::utils::ingest_utils::{flatten_and_push_logs, get_custom_fields use super::users::dashboards::DashboardError; use super::users::filters::FiltersError; +pub static INGESTION_THREADPOOL: Lazy = Lazy::new(|| { + ThreadPoolBuilder::new() + .build() + .expect("Unable to create Rayon thread pool") +}); + // Handler for POST /api/v1/ingest // ingests events by extracting stream name from header // creates if stream does not exist diff --git a/src/parseable/streams.rs b/src/parseable/streams.rs index d9a9ae1b8..cca7c95c1 100644 --- a/src/parseable/streams.rs +++ b/src/parseable/streams.rs @@ -56,7 +56,7 @@ use crate::{ DEFAULT_TIMESTAMP_KEY, format::{LogSource, LogSourceEntry}, }, - handlers::DatasetTag, + handlers::{DatasetTag, http::ingest::INGESTION_THREADPOOL}, hottier::StreamHotTier, metadata::{LogStreamMetadata, SchemaVersion}, metrics, @@ -86,7 +86,71 @@ static DISK_WRITE_BATCH_ROWS: Lazy = Lazy::new(|| { }); const INPROCESS_DIR_PREFIX: &str = "processing_"; -const METRIC_ROW_GROUP_PREP_IN_FLIGHT: usize = 1; +const METRIC_ROW_GROUP_PREP_IN_FLIGHT_VAR: &str = "METRIC_ROW_GROUP_PREP_IN_FLIGHT"; +/// Caps how many arrow files feed a single parquet conversion group. A +/// minute with heavy schema-key churn can stage thousands of small arrow +/// files; converting them as one group means one kmerge holding an open +/// reader (and a decoded batch) per file. Chunking bounds that memory +/// while still collapsing thousands of files into a handful of parquets. +static METRIC_ROW_GROUP_PREP_IN_FLIGHT: Lazy = Lazy::new(|| { + if let Ok(var) = std::env::var(METRIC_ROW_GROUP_PREP_IN_FLIGHT_VAR) + && let Ok(var) = var.parse::() + && var > 0 + { + var + } else { + 1 + } +}); + +const MAX_ARROW_FILES_PER_PARQUET_VAR: &str = "MAX_ARROW_FILES_PER_PARQUET"; +/// Caps how many arrow files feed a single parquet conversion group. A +/// minute with heavy schema-key churn can stage thousands of small arrow +/// files; converting them as one group means one kmerge holding an open +/// reader (and a decoded batch) per file. Chunking bounds that memory +/// while still collapsing thousands of files into a handful of parquets. +static MAX_ARROW_FILES_PER_PARQUET: Lazy = Lazy::new(|| { + if let Ok(var) = std::env::var(MAX_ARROW_FILES_PER_PARQUET_VAR) + && let Ok(var) = var.parse::() + && var > 0 + { + var + } else { + 512 + } +}); + +/// Splits any conversion group holding more than MAX_ARROW_FILES_PER_PARQUET +/// arrow files into chunks, giving chunk 1.. a "-{i}" suffix on the parquet +/// file stem so each chunk converts to its own parquet file. +fn chunk_arrow_file_groups( + grouped: HashMap>, +) -> HashMap> { + let max_files = *MAX_ARROW_FILES_PER_PARQUET; + let mut chunked = HashMap::with_capacity(grouped.len()); + for (parquet_path, arrow_files) in grouped { + if arrow_files.len() <= max_files { + chunked.insert(parquet_path, arrow_files); + continue; + } + for (i, chunk) in arrow_files.chunks(max_files).enumerate() { + let chunk_path = if i == 0 { + parquet_path.clone() + } else { + let mut path = parquet_path.clone(); + if let (Some(stem), Some(ext)) = ( + parquet_path.file_stem().and_then(|s| s.to_str()), + parquet_path.extension().and_then(|e| e.to_str()), + ) { + path.set_file_name(format!("{stem}-{i}.{ext}")); + } + path + }; + chunked.insert(chunk_path, chunk.to_vec()); + } + } + chunked +} struct PreparedMetricRowGroup { batch: RecordBatch, @@ -100,10 +164,20 @@ fn arrow_path_to_parquet( ) -> Option { let filename = path.file_stem()?.to_str()?; let (_, front) = filename.split_once('.')?; - if !front.contains('.') { - warn!("Skipping unexpected arrow file without `.`: {}", filename); + // Writers may suffix the filename with a per-file ULID after the + // ".data" marker (ONE_PARQUET_PER_ARROW). Truncate at the marker so + // the parquet grouping key stays per-minute: with high schema-key + // churn, keying on the full name made every arrow file its own + // conversion group (thousands per minute), which exploded conversion + // parallelism/memory and the object-store file count. + let Some(idx) = front.rfind(".data") else { + warn!( + "Skipping unexpected arrow file without `.data`: {}", + filename + ); return None; - } + }; + let front = &front[..idx + ".data".len()]; let filename_with_random_number = format!("{front}.{random_string}.parquet"); let mut parquet_path = stream_staging_path.to_owned(); parquet_path.push(filename_with_random_number); @@ -352,7 +426,7 @@ impl Stream { } } } - grouped + chunk_arrow_file_groups(grouped) } /// Returns a mapping for inprocess arrow files (init_signal=true). @@ -370,7 +444,7 @@ impl Stream { warn!("Unexpected arrow file: {}", inprocess_file.display()); } } - grouped + chunk_arrow_file_groups(grouped) } /// Returns arrow files for conversion, filtering by time and removing invalid files. @@ -757,7 +831,7 @@ impl Stream { time_partition_field: String, ) -> std::sync::mpsc::Receiver> { let (tx, rx) = std::sync::mpsc::sync_channel(1); - rayon::spawn(move || { + INGESTION_THREADPOOL.spawn(move || { let _ = tx.send(Self::prepare_metric_row_group( schema, buffer, @@ -856,7 +930,7 @@ impl Stream { let merged_schema = record_reader.merged_schema(); let props = self.parquet_writer_props(&merged_schema, time_partition, custom_partition); - // schemas.push(merged_schema.clone()); + let schema = Arc::new(merged_schema.clone()); let part_path = parquet_path.with_extension("part"); @@ -938,7 +1012,7 @@ impl Stream { // each page actually carries. let target = self.options.row_group_size; let buffer_capacity = record_reader.readers.len(); - let mut pending_row_groups = VecDeque::with_capacity(METRIC_ROW_GROUP_PREP_IN_FLIGHT); + let mut pending_row_groups = VecDeque::with_capacity(*METRIC_ROW_GROUP_PREP_IN_FLIGHT); let mut buffer: Vec = Vec::with_capacity(buffer_capacity); let mut buffered_rows: usize = 0; let mut merged_iter = @@ -951,6 +1025,12 @@ impl Stream { buffered_rows += record_rows; buffer.push(record); if buffered_rows >= target { + if pending_row_groups.len() >= *METRIC_ROW_GROUP_PREP_IN_FLIGHT + && let Some(rx) = pending_row_groups.pop_front() + { + let prepared = Self::receive_prepared_metric_row_group(rx)?; + writer.write(&prepared.batch)?; + } let row_group_buffer = std::mem::replace(&mut buffer, Vec::with_capacity(buffer_capacity)); let next_row_group = Self::spawn_metric_row_group_prepare( @@ -959,29 +1039,23 @@ impl Stream { time_partition_field.clone(), ); pending_row_groups.push_back(next_row_group); - if pending_row_groups.len() > METRIC_ROW_GROUP_PREP_IN_FLIGHT - && let Some(rx) = pending_row_groups.pop_front() - { - let prepared = Self::receive_prepared_metric_row_group(rx)?; - writer.write(&prepared.batch)?; - } buffer.clear(); buffered_rows = 0; } } if !buffer.is_empty() { + if pending_row_groups.len() >= *METRIC_ROW_GROUP_PREP_IN_FLIGHT + && let Some(rx) = pending_row_groups.pop_front() + { + let prepared = Self::receive_prepared_metric_row_group(rx)?; + writer.write(&prepared.batch)?; + } let next_row_group = Self::spawn_metric_row_group_prepare( schema.clone(), buffer, time_partition_field.clone(), ); pending_row_groups.push_back(next_row_group); - if pending_row_groups.len() > METRIC_ROW_GROUP_PREP_IN_FLIGHT - && let Some(rx) = pending_row_groups.pop_front() - { - let prepared = Self::receive_prepared_metric_row_group(rx)?; - writer.write(&prepared.batch)?; - } } while let Some(rx) = pending_row_groups.pop_front() { let prepared = Self::receive_prepared_metric_row_group(rx)?; From fe8aceacaa7450ae362a2e0bc9b8935b8d04c84f Mon Sep 17 00:00:00 2001 From: Nikhil Sinha <131262146+nikhilsinhaparseable@users.noreply.github.com> Date: Thu, 11 Jun 2026 19:34:05 +0530 Subject: [PATCH 25/47] fix: field stats parquet aggregation (#1675) calculate field stats directly from parquet record batches remove old datafusion table path logic add bounded high-cardinality handling per-file stats IDs for correct flattened-row aggregation improve logging and keep partial stats when individual parquet batches fail --- src/storage/field_stats.rs | 1169 ++++++++++++++++-------------------- 1 file changed, 512 insertions(+), 657 deletions(-) diff --git a/src/storage/field_stats.rs b/src/storage/field_stats.rs index 83ad1f626..a9586f52f 100644 --- a/src/storage/field_stats.rs +++ b/src/storage/field_stats.rs @@ -33,7 +33,6 @@ use crate::metadata::SchemaVersion; use crate::option::Mode; use crate::parseable::DEFAULT_TENANT; use crate::parseable::PARSEABLE; -use crate::query::QUERY_SESSION_STATE; use crate::storage::ObjectStorageError; use crate::storage::StreamType; use crate::tenants::TENANT_METADATA; @@ -65,25 +64,40 @@ use arrow_schema::TimeUnit; use chrono::DateTime; use chrono::NaiveDateTime; use chrono::Utc; -use datafusion::prelude::ParquetReadOptions; -use datafusion::prelude::SessionContext; -use futures::StreamExt; use indexmap::IndexMap; +use once_cell::sync::Lazy; +use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder; +use rayon::prelude::*; +use rayon::{ThreadPool, ThreadPoolBuilder}; use regex::Regex; use serde::Deserialize; use serde::Serialize; -use std::collections::HashMap; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::fmt::Debug; +use std::fs::File; use std::path::Path; -use tracing::error; -use tracing::trace; -use tracing::warn; -use ulid::Ulid; +use std::sync::Arc; +use std::time::Instant; +use tokio::sync::Semaphore; +use tracing::{debug, error, warn}; pub const DATASET_STATS_STREAM_NAME: &str = "pstats"; const DATASET_STATS_CUSTOM_PARTITION: &str = "dataset_name"; -const MAX_CONCURRENT_FIELD_STATS: usize = 10; +const MAX_CONCURRENT_FIELD_STATS: usize = 4; +const PARALLEL_FIELD_STATS_MIN_FIELDS: usize = 16; +const MIN_TRACKED_DISTINCT_VALUES_PER_FIELD: usize = 1024; +const MAX_TRACKED_DISTINCT_VALUES_PER_FIELD: usize = 10_000; +const HLL_PRECISION_BITS: u32 = 12; +const HLL_REGISTER_COUNT: usize = 1 << HLL_PRECISION_BITS; +static FIELD_STATS_QUERY_SEMAPHORE: Lazy> = + Lazy::new(|| Arc::new(Semaphore::new(MAX_CONCURRENT_FIELD_STATS))); +static FIELD_STATS_RAYON_POOL: Lazy = Lazy::new(|| { + ThreadPoolBuilder::new() + .num_threads(MAX_CONCURRENT_FIELD_STATS) + .thread_name(|index| format!("field-stats-{index}")) + .build() + .expect("field stats rayon pool should initialize") +}); #[derive(Serialize, Debug)] struct DistinctStat { @@ -102,6 +116,7 @@ struct FieldStat { #[derive(Serialize, Debug)] struct DatasetStats { dataset_name: String, + stats_id: String, field_stats: Vec, } @@ -114,6 +129,27 @@ pub async fn calculate_field_stats( schema: &Schema, max_field_statistics: usize, tenant_id: &Option, +) -> Result { + let started_at = Instant::now(); + let result = calculate_field_stats_inner( + stream_name, + parquet_path, + schema, + max_field_statistics, + tenant_id, + ) + .await; + log_stats_calculation_time(stream_name, parquet_path, started_at, result.is_ok()); + + result +} + +async fn calculate_field_stats_inner( + stream_name: &str, + parquet_path: &Path, + schema: &Schema, + max_field_statistics: usize, + tenant_id: &Option, ) -> Result { // create datetime from timestamp present in parquet path let parquet_ts = extract_datetime_from_parquet_path_regex(parquet_path).map_err(|e| { @@ -122,26 +158,17 @@ pub async fn calculate_field_stats( e )) })?; - let field_stats = { - let session_state = QUERY_SESSION_STATE.clone(); - - let ctx = SessionContext::new_with_state(session_state); - let table_name = Ulid::new().to_string(); - ctx.register_parquet( - &table_name, - parquet_path - .to_str() - .ok_or_else(|| PostError::Invalid(anyhow::anyhow!("Invalid UTF-8 in path")))?, - ParquetReadOptions::default(), - ) - .await - .map_err(|e| PostError::Invalid(e.into()))?; - - collect_all_field_stats(&table_name, &ctx, schema, max_field_statistics).await - }; + let field_stats = collect_all_field_stats_from_parquet( + stream_name, + parquet_path, + schema, + max_field_statistics, + ) + .await?; let mut stats_calculated = false; let stats = DatasetStats { dataset_name: stream_name.to_string(), + stats_id: parquet_path.to_string_lossy().to_string(), field_stats, }; if stats.field_stats.is_empty() { @@ -150,19 +177,7 @@ pub async fn calculate_field_stats( stats_calculated = true; let stats_value = serde_json::to_value(&stats).map_err(|e| ObjectStorageError::Invalid(e.into()))?; - let log_source_entry = LogSourceEntry::new(LogSource::Json, HashSet::new()); - PARSEABLE - .create_stream_if_not_exists( - DATASET_STATS_STREAM_NAME, - StreamType::Internal, - Some(&DATASET_STATS_CUSTOM_PARTITION.to_string()), - vec![log_source_entry], - TelemetryType::Logs, - tenant_id, - vec![], - vec![], - ) - .await?; + ensure_dataset_stats_stream(tenant_id).await?; let vec_json = apply_generic_flattening_for_partition( stats_value, None, @@ -199,136 +214,360 @@ pub async fn calculate_field_stats( Ok(stats_calculated) } -/// Collects statistics for all fields in the stream. -/// Returns a vector of `FieldStat` for each field with non-zero count. -/// Uses `buffer_unordered` to run up to `MAX_CONCURRENT_FIELD_STATS` queries concurrently. -async fn collect_all_field_stats( +fn log_stats_calculation_time( + stream_name: &str, + parquet_path: &Path, + started_at: Instant, + success: bool, +) { + let parquet_file = parquet_path + .file_name() + .and_then(|filename| filename.to_str()) + .unwrap_or(""); + let elapsed_ms = started_at.elapsed().as_millis(); + if success { + debug!( + "Field stats calculation completed for parquet file {parquet_file} in {elapsed_ms} ms. stream={stream_name}" + ); + } else { + warn!( + "Field stats calculation failed for parquet file {parquet_file} after {elapsed_ms} ms. stream={stream_name}" + ); + } +} + +async fn ensure_dataset_stats_stream(tenant_id: &Option) -> Result<(), PostError> { + let log_source_entry = LogSourceEntry::new(LogSource::Json, HashSet::new()); + PARSEABLE + .create_stream_if_not_exists( + DATASET_STATS_STREAM_NAME, + StreamType::Internal, + Some(&DATASET_STATS_CUSTOM_PARTITION.to_string()), + vec![log_source_entry], + TelemetryType::Logs, + tenant_id, + vec![], + vec![], + ) + .await?; + + Ok(()) +} + +async fn collect_all_field_stats_from_parquet( + stream_name: &str, + parquet_path: &Path, + schema: &Schema, + max_field_statistics: usize, +) -> Result, PostError> { + let permit = FIELD_STATS_QUERY_SEMAPHORE + .clone() + .acquire_owned() + .await + .map_err(|e| { + PostError::Invalid(anyhow::anyhow!( + "Failed to acquire field stats query permit: {}", + e + )) + })?; + let stream_name = stream_name.to_string(); + let parquet_path = parquet_path.to_path_buf(); + let schema = schema.clone(); + + tokio::task::spawn_blocking(move || { + let _permit = permit; + collect_all_field_stats_from_parquet_blocking( + &stream_name, + &parquet_path, + &schema, + max_field_statistics, + ) + }) + .await + .map_err(|e| PostError::Invalid(anyhow::anyhow!("Field stats task failed: {}", e)))? +} + +fn collect_all_field_stats_from_parquet_blocking( stream_name: &str, - ctx: &SessionContext, + parquet_path: &Path, schema: &Schema, max_field_statistics: usize, -) -> Vec { - // Collect field names into an owned Vec to avoid lifetime issues +) -> Result, PostError> { + let file = File::open(parquet_path).map_err(|e| { + PostError::Invalid(anyhow::anyhow!( + "Failed to open parquet file for field stats: {}", + e + )) + })?; + let mut reader = ParquetRecordBatchReaderBuilder::try_new(file) + .map_err(|e| PostError::Invalid(anyhow::anyhow!(e)))? + .build() + .map_err(|e| PostError::Invalid(anyhow::anyhow!(e)))?; let field_names: Vec = schema .fields() .iter() .map(|field| field.name().clone()) .collect(); - let field_futures = field_names.into_iter().map(|field_name| { - let ctx = ctx.clone(); - async move { - calculate_single_field_stats(ctx, stream_name, &field_name, max_field_statistics).await + let mut field_counts: HashMap = field_names + .iter() + .map(|field_name| { + ( + field_name.clone(), + FieldCountState::new( + stream_name.to_string(), + field_name.clone(), + max_field_statistics, + ), + ) + }) + .collect(); + + for batch in &mut reader { + let batch = match batch { + Ok(batch) => batch, + Err(e) => { + warn!( + "Skipping undecodable parquet batch while calculating field stats for {}: {}", + parquet_path.display(), + e + ); + continue; + } + }; + + let batch_counts = if field_names.len() >= PARALLEL_FIELD_STATS_MIN_FIELDS { + collect_batch_field_counts_parallel(&batch, &field_names) + } else { + collect_batch_field_counts_serial(&batch, &field_names) + }; + + for (field_name, counts) in batch_counts { + merge_field_counts(&mut field_counts, &field_name, counts); } - }); + } - futures::stream::iter(field_futures) - .buffer_unordered(MAX_CONCURRENT_FIELD_STATS) - .filter_map(std::future::ready) - .collect::>() - .await + Ok(field_names + .into_iter() + .filter_map(|field_name| { + let state = field_counts.remove(&field_name)?; + if state.total_count == 0 { + return None; + } + + Some(FieldStat { + field_name, + count: state.total_count, + distinct_count: state.distinct_count(), + distinct_stats: state.into_distinct_stats(max_field_statistics), + }) + }) + .collect()) } -/// This function is used to fetch distinct values and their counts for a field in the stream. -/// Returns a vector of `DistinctStat` containing distinct values and their counts. -/// The query groups by the field and orders by the count in descending order, limiting the results to `PARSEABLE.options.max_field_statistics`. -async fn calculate_single_field_stats( - ctx: SessionContext, - stream_name: &str, +fn collect_batch_field_counts_serial( + batch: &arrow_array::RecordBatch, + field_names: &[String], +) -> Vec<(String, HashMap)> { + field_names + .iter() + .filter_map(|field_name| collect_field_counts(batch, field_name)) + .collect() +} + +fn collect_batch_field_counts_parallel( + batch: &arrow_array::RecordBatch, + field_names: &[String], +) -> Vec<(String, HashMap)> { + FIELD_STATS_RAYON_POOL.install(|| { + field_names + .par_iter() + .filter_map(|field_name| collect_field_counts(batch, field_name)) + .collect() + }) +} + +fn collect_field_counts( + batch: &arrow_array::RecordBatch, field_name: &str, - max_field_statistics: usize, -) -> Option { - let mut total_count = 0; - let mut distinct_count = 0; - let mut distinct_stats = Vec::new(); - - let combined_sql = get_stats_sql(stream_name, field_name, max_field_statistics); - match ctx.sql(&combined_sql).await { - Ok(df) => { - let mut stream = match df.execute_stream().await { - Ok(stream) => stream, - Err(e) => { - trace!("Failed to execute distinct stats query: {e}"); - return None; // Return empty if query fails - } - }; - while let Some(batch_result) = stream.next().await { - let rb = match batch_result { - Ok(batch) => batch, - Err(e) => { - trace!("Failed to fetch batch in distinct stats query: {e}"); - continue; // Skip this batch if there's an error - } - }; - let total_count_array = rb.column(0).as_any().downcast_ref::()?; - let distinct_count_array = rb.column(1).as_any().downcast_ref::()?; - - total_count = total_count_array.value(0); - distinct_count = distinct_count_array.value(0); - if distinct_count == 0 { - return None; - } +) -> Option<(String, HashMap)> { + let array = batch.column_by_name(field_name)?; + let mut counts = HashMap::new(); + + for row_index in 0..array.len() { + let value = format_arrow_value(array.as_ref(), row_index); + *counts.entry(value).or_default() += 1; + } - let field_value_array = rb.column(2).as_ref(); - let value_count_array = rb.column(3).as_any().downcast_ref::()?; + Some((field_name.to_string(), counts)) +} - for i in 0..rb.num_rows() { - let value = format_arrow_value(field_value_array, i); - let count = value_count_array.value(i); +fn merge_field_counts( + field_counts: &mut HashMap, + field_name: &str, + counts: HashMap, +) { + let field_total = field_counts + .get_mut(field_name) + .expect("field_counts initialized for each field"); - distinct_stats.push(DistinctStat { - distinct_value: value, - count, - }); - } + field_total.merge_counts(counts); +} + +struct FieldCountState { + stream_name: String, + field_name: String, + total_count: i64, + counts: HashMap, + hll: HyperLogLog, + max_tracked_values: usize, + approximate: bool, +} + +impl FieldCountState { + fn new(stream_name: String, field_name: String, max_field_statistics: usize) -> Self { + Self { + stream_name, + field_name, + total_count: 0, + counts: HashMap::new(), + hll: HyperLogLog::new(), + max_tracked_values: tracked_distinct_value_limit(max_field_statistics), + approximate: false, + } + } + + fn merge_counts(&mut self, counts: HashMap) { + for (value, count) in counts { + self.hll.add(&value); + self.total_count += count; + + if let Some(existing_count) = self.counts.get_mut(&value) { + *existing_count += count; + continue; + } + + if self.counts.len() < self.max_tracked_values { + self.counts.insert(value, count); + continue; + } + + if !self.approximate { + self.approximate = true; + warn!( + "Field stats cardinality cap reached for stream {} field {}. Tracking bounded top-value candidates with max_tracked_values={}", + self.stream_name, self.field_name, self.max_tracked_values + ); + } + + if let Some((min_value, min_count)) = self.current_min_value() + && count > min_count + { + self.counts.remove(&min_value); + self.counts.insert(value, count); } } - Err(e) => { - trace!("Failed to execute distinct stats query for field: {field_name}, error: {e}"); - return None; + } + + fn current_min_value(&self) -> Option<(String, i64)> { + self.counts + .iter() + .min_by(|(left_value, left_count), (right_value, right_count)| { + left_count + .cmp(right_count) + .then_with(|| right_value.cmp(left_value)) + }) + .map(|(value, count)| (value.clone(), *count)) + } + + fn distinct_count(&self) -> i64 { + if self.approximate { + (self.hll.estimate().round() as i64).max(self.counts.len() as i64) + } else { + self.counts.len() as i64 } } - Some(FieldStat { - field_name: field_name.to_string(), - count: total_count, - distinct_count, - distinct_stats, - }) -} -fn get_stats_sql(stream_name: &str, field_name: &str, max_field_statistics: usize) -> String { - let escaped_field_name = field_name.replace('"', "\"\""); - let escaped_stream_name = stream_name.replace('"', "\"\""); + fn into_distinct_stats(self, max_field_statistics: usize) -> Vec { + let mut distinct_stats = self + .counts + .into_iter() + .map(|(distinct_value, count)| DistinctStat { + distinct_value, + count, + }) + .collect::>(); + distinct_stats.sort_by(|left, right| { + right + .count + .cmp(&left.count) + .then_with(|| left.distinct_value.cmp(&right.distinct_value)) + }); + distinct_stats.truncate(max_field_statistics); + distinct_stats + } +} - format!( - r#" - WITH field_groups AS ( - SELECT - "{escaped_field_name}" as field_value, - COUNT(*) as value_count - FROM "{escaped_stream_name}" - GROUP BY "{escaped_field_name}" - ), - field_summary AS ( - SELECT - field_value, - value_count, - SUM(value_count) OVER () as total_count, - COUNT(*) OVER () as distinct_count, - ROW_NUMBER() OVER (ORDER BY value_count DESC) as rn - FROM field_groups - ) - SELECT - total_count, - distinct_count, - field_value, - value_count - FROM field_summary - WHERE rn <= {max_field_statistics} - ORDER BY value_count DESC - "# +fn tracked_distinct_value_limit(max_field_statistics: usize) -> usize { + max_field_statistics.clamp( + MIN_TRACKED_DISTINCT_VALUES_PER_FIELD, + MAX_TRACKED_DISTINCT_VALUES_PER_FIELD, ) } +struct HyperLogLog { + registers: Vec, +} + +impl HyperLogLog { + fn new() -> Self { + Self { + registers: vec![0; HLL_REGISTER_COUNT], + } + } + + fn add(&mut self, value: &str) { + let hash = hash_field_value(value); + let register_index = (hash >> (u64::BITS - HLL_PRECISION_BITS)) as usize; + let rank = ((hash << HLL_PRECISION_BITS).leading_zeros() + 1) + .min(u64::BITS - HLL_PRECISION_BITS + 1) as u8; + self.registers[register_index] = self.registers[register_index].max(rank); + } + + fn estimate(&self) -> f64 { + let register_count = HLL_REGISTER_COUNT as f64; + let zero_registers = self + .registers + .iter() + .filter(|register| **register == 0) + .count(); + let harmonic_sum = self + .registers + .iter() + .map(|register| 2_f64.powi(-(*register as i32))) + .sum::(); + let raw_estimate = hll_alpha(register_count) * register_count.powi(2) / harmonic_sum; + + if raw_estimate <= 2.5 * register_count && zero_registers > 0 { + register_count * (register_count / zero_registers as f64).ln() + } else { + raw_estimate + } + } +} + +fn hll_alpha(register_count: f64) -> f64 { + match HLL_REGISTER_COUNT { + 16 => 0.673, + 32 => 0.697, + 64 => 0.709, + _ => 0.7213 / (1.0 + 1.079 / register_count), + } +} + +fn hash_field_value(value: &str) -> u64 { + xxhash_rust::xxh3::xxh3_64(value.as_bytes()) +} + macro_rules! try_downcast { ($ty:ty, $arr:expr, $body:expr) => { if let Some(arr) = $arr.as_any().downcast_ref::<$ty>() { @@ -706,11 +945,7 @@ pub fn build_stats_sql( ORDER BY SUM(field_stats_distinct_stats_count) DESC, field_stats_distinct_stats_distinct_value ASC - ) AS rn, - COUNT(*) OVER ( - PARTITION BY - field_stats_field_name - ) AS distinct_count + ) AS rn FROM {DATASET_STATS_STREAM_NAME} WHERE @@ -730,26 +965,49 @@ pub fn build_stats_sql( rn > {rn_start} AND rn <= {rn_end} ), + field_distincts AS ( + SELECT + field_name, + COUNT(*) AS total_distinct_count + FROM + ranked_values + GROUP BY + field_name + ), field_totals AS ( SELECT field_stats_field_name, - SUM(field_stats_count) AS total_field_count + SUM(field_count) AS total_field_count FROM - {DATASET_STATS_STREAM_NAME} - WHERE - dataset_name = '{dataset_name}' + ( + SELECT + field_stats_field_name, + stats_id, + p_timestamp, + MAX(field_stats_count) AS field_count + FROM + {DATASET_STATS_STREAM_NAME} + WHERE + dataset_name = '{dataset_name}' + {fields_filter} + GROUP BY + field_stats_field_name, + stats_id, + p_timestamp + ) field_file_totals GROUP BY field_stats_field_name ) SELECT tv.field_name, ft.total_field_count AS field_count, - tv.distinct_count, + fd.total_distinct_count AS distinct_count, tv.distinct_value, tv.distinct_value_count FROM top_values tv JOIN field_totals ft ON tv.field_name = ft.field_stats_field_name + JOIN field_distincts fd ON tv.field_name = fd.field_name ORDER BY tv.field_name, tv.distinct_value_count DESC" @@ -757,7 +1015,7 @@ ORDER BY } #[cfg(test)] mod tests { - use std::{fs::OpenOptions, sync::Arc}; + use std::{collections::HashMap, fs::OpenOptions, sync::Arc}; use arrow::buffer::OffsetBuffer; use arrow_array::{ @@ -765,12 +1023,12 @@ mod tests { TimestampMillisecondArray, }; use arrow_schema::{DataType, Field, Schema, TimeUnit}; - use datafusion::prelude::{ParquetReadOptions, SessionContext}; use parquet::{arrow::ArrowWriter, file::properties::WriterProperties}; use temp_dir::TempDir; - use ulid::Ulid; - use crate::storage::field_stats::calculate_single_field_stats; + use crate::storage::field_stats::{ + FieldCountState, build_stats_sql, collect_all_field_stats_from_parquet_blocking, + }; async fn create_test_parquet_with_data() -> (TempDir, std::path::PathBuf) { let temp_dir = TempDir::new().unwrap(); @@ -923,523 +1181,120 @@ mod tests { ]) } - #[tokio::test] - async fn test_calculate_single_field_stats_with_multiple_values() { - let (_temp_dir, parquet_path) = create_test_parquet_with_data().await; - - let random_suffix = Ulid::new().to_string(); - let ctx = SessionContext::new(); - ctx.register_parquet( - &random_suffix, - parquet_path.to_str().expect("valid path"), - ParquetReadOptions::default(), - ) - .await - .unwrap(); - - // Test name field with multiple distinct values and different frequencies - let result = calculate_single_field_stats(ctx.clone(), &random_suffix, "name", 50).await; - assert!(result.is_some()); - let stats = result.unwrap(); - - assert_eq!(stats.field_name, "name"); - assert_eq!(stats.count, 10); - assert_eq!(stats.distinct_count, 7); - assert_eq!(stats.distinct_stats.len(), 7); - - // Verify ordering by count (descending) - assert!(stats.distinct_stats[0].count >= stats.distinct_stats[1].count); - assert!(stats.distinct_stats[1].count >= stats.distinct_stats[2].count); - - // Verify specific counts - let alice_stat = stats - .distinct_stats - .iter() - .find(|s| s.distinct_value == "Alice"); - assert!(alice_stat.is_some()); - assert_eq!(alice_stat.unwrap().count, 3); - - let bob_stat = stats - .distinct_stats - .iter() - .find(|s| s.distinct_value == "Bob"); - assert!(bob_stat.is_some()); - assert_eq!(bob_stat.unwrap().count, 2); - - let charlie_stat = stats - .distinct_stats - .iter() - .find(|s| s.distinct_value == "Charlie"); - assert!(charlie_stat.is_some()); - assert_eq!(charlie_stat.unwrap().count, 1); - } - - #[tokio::test] - async fn test_calculate_single_field_stats_with_numeric_field() { - let (_temp_dir, parquet_path) = create_test_parquet_with_data().await; - - let ctx = SessionContext::new(); - let table_name = ulid::Ulid::new().to_string(); - - ctx.register_parquet( - &table_name, - parquet_path.to_str().unwrap(), - ParquetReadOptions::default(), - ) - .await - .unwrap(); - - // Test score field (Float64) - let result = calculate_single_field_stats(ctx.clone(), &table_name, "score", 50).await; - - assert!(result.is_some()); - let stats = result.unwrap(); - - assert_eq!(stats.field_name, "score"); - assert_eq!(stats.count, 10); - assert_eq!(stats.distinct_count, 9); - - // Verify that 95.5 appears twice (should be first due to highest count) - let highest_count_stat = &stats.distinct_stats[0]; - assert_eq!(highest_count_stat.distinct_value, "95.5"); - assert_eq!(highest_count_stat.count, 2); - } - - #[tokio::test] - async fn test_calculate_single_field_stats_with_boolean_field() { - let (_temp_dir, parquet_path) = create_test_parquet_with_data().await; - - let ctx = SessionContext::new(); - let table_name = ulid::Ulid::new().to_string(); - ctx.register_parquet( - &table_name, - parquet_path.to_str().unwrap(), - ParquetReadOptions::default(), - ) - .await - .unwrap(); - - // Test active field (Boolean) - let result = calculate_single_field_stats(ctx.clone(), &table_name, "active", 50).await; - - assert!(result.is_some()); - let stats = result.unwrap(); - - assert_eq!(stats.field_name, "active"); - assert_eq!(stats.count, 10); - assert_eq!(stats.distinct_count, 3); - assert_eq!(stats.distinct_stats.len(), 3); - - assert_eq!(stats.distinct_stats[0].distinct_value, "true"); - assert_eq!(stats.distinct_stats[0].count, 6); - assert_eq!(stats.distinct_stats[1].distinct_value, "false"); - assert_eq!(stats.distinct_stats[1].count, 3); - } - - #[tokio::test] - async fn test_calculate_single_field_stats_with_timestamp_field() { - let (_temp_dir, parquet_path) = create_test_parquet_with_data().await; - - let ctx = SessionContext::new(); - let table_name = ulid::Ulid::new().to_string(); - ctx.register_parquet( - &table_name, - parquet_path.to_str().unwrap(), - ParquetReadOptions::default(), - ) - .await - .unwrap(); - - // Test created_at field (Timestamp) - let result = calculate_single_field_stats(ctx.clone(), &table_name, "created_at", 50).await; - - assert!(result.is_some()); - let stats = result.unwrap(); - - assert_eq!(stats.field_name, "created_at"); - assert_eq!(stats.count, 10); - assert_eq!(stats.distinct_count, 9); - - // Verify that the duplicate timestamp appears twice - let duplicate_timestamp = &stats.distinct_stats[0]; - assert_eq!(duplicate_timestamp.count, 2); - } - - #[tokio::test] - async fn test_calculate_single_field_stats_single_value_field() { - let (_temp_dir, parquet_path) = create_test_parquet_with_data().await; - - let ctx = SessionContext::new(); - let table_name = ulid::Ulid::new().to_string(); - ctx.register_parquet( - &table_name, - parquet_path.to_str().unwrap(), - ParquetReadOptions::default(), - ) - .await - .unwrap(); - - // Test field with single distinct value - let result = - calculate_single_field_stats(ctx.clone(), &table_name, "single_value", 50).await; - - assert!(result.is_some()); - let stats = result.unwrap(); - - assert_eq!(stats.field_name, "single_value"); - assert_eq!(stats.count, 10); - assert_eq!(stats.distinct_count, 1); - assert_eq!(stats.distinct_stats.len(), 1); - assert_eq!(stats.distinct_stats[0].distinct_value, "constant"); - assert_eq!(stats.distinct_stats[0].count, 10); - } - - #[tokio::test] - async fn test_calculate_single_field_stats_nonexistent_table() { - let ctx = SessionContext::new(); - - // Test with non-existent table - let result = - calculate_single_field_stats(ctx.clone(), "non_existent_table", "field", 50).await; - - // Should return None due to SQL execution failure - assert!(result.is_none()); - } - - #[tokio::test] - async fn test_calculate_single_field_stats_nonexistent_field() { - let (_temp_dir, parquet_path) = create_test_parquet_with_data().await; - - let ctx = SessionContext::new(); - let table_name = ulid::Ulid::new().to_string(); - ctx.register_parquet( - &table_name, - parquet_path.to_str().unwrap(), - ParquetReadOptions::default(), - ) - .await - .unwrap(); - - // Test with non-existent field - let result = - calculate_single_field_stats(ctx.clone(), &table_name, "non_existent_field", 50).await; - - // Should return None due to SQL execution failure - assert!(result.is_none()); - } - - #[tokio::test] - async fn test_calculate_single_field_stats_with_special_characters() { - // Create a schema with field names containing special characters - let schema = Arc::new(Schema::new(vec![ - Field::new("field with spaces", DataType::Utf8, true), - Field::new("field\"with\"quotes", DataType::Utf8, true), - Field::new("field'with'apostrophes", DataType::Utf8, true), - ])); - - let temp_dir = TempDir::new().unwrap(); - let file_path = temp_dir.path().join("special_chars.parquet"); - - use parquet::arrow::AsyncArrowWriter; - use tokio::fs::File; - - let space_array = StringArray::from(vec![Some("value1"), Some("value2"), Some("value1")]); - let quote_array = StringArray::from(vec![Some("quote1"), Some("quote2"), Some("quote1")]); - let apostrophe_array = StringArray::from(vec![Some("apos1"), Some("apos2"), Some("apos1")]); - - let batch = RecordBatch::try_new( - schema.clone(), - vec![ - Arc::new(space_array), - Arc::new(quote_array), - Arc::new(apostrophe_array), - ], - ) - .unwrap(); - - let file = File::create(&file_path).await.unwrap(); - let mut writer = AsyncArrowWriter::try_new(file, schema, None).unwrap(); - writer.write(&batch).await.unwrap(); - writer.close().await.unwrap(); - - let ctx = SessionContext::new(); - let table_name = ulid::Ulid::new().to_string(); - ctx.register_parquet( - &table_name, - file_path.to_str().unwrap(), - ParquetReadOptions::default(), - ) - .await - .unwrap(); - - // Test field with spaces - let result = - calculate_single_field_stats(ctx.clone(), &table_name, "field with spaces", 50).await; - assert!(result.is_some()); - let stats = result.unwrap(); - assert_eq!(stats.field_name, "field with spaces"); - assert_eq!(stats.count, 3); - assert_eq!(stats.distinct_count, 2); - - // Test field with quotes - let result = - calculate_single_field_stats(ctx.clone(), &table_name, "field\"with\"quotes", 50).await; - assert!(result.is_some()); - let stats = result.unwrap(); - assert_eq!(stats.field_name, "field\"with\"quotes"); - assert_eq!(stats.count, 3); - assert_eq!(stats.distinct_count, 2); - } - - #[tokio::test] - async fn test_calculate_single_field_stats_empty_table() { - // Create empty table - let schema = Arc::new(create_test_schema()); - let temp_dir = TempDir::new().unwrap(); - let file_path = temp_dir.path().join("empty_data.parquet"); - - use parquet::arrow::AsyncArrowWriter; - use tokio::fs::File; - - let file = File::create(&file_path).await.unwrap(); - let mut writer = AsyncArrowWriter::try_new(file, schema.clone(), None).unwrap(); - - // Create empty batch - let empty_batch = RecordBatch::new_empty(schema.clone()); - writer.write(&empty_batch).await.unwrap(); - writer.close().await.unwrap(); - - let ctx = SessionContext::new(); - let table_name = ulid::Ulid::new().to_string(); - ctx.register_parquet( - &table_name, - file_path.to_str().unwrap(), - ParquetReadOptions::default(), - ) - .await - .unwrap(); - - let result = calculate_single_field_stats(ctx.clone(), &table_name, "name", 50).await; - assert!(result.unwrap().distinct_stats.is_empty()); - } - - #[tokio::test] - async fn test_calculate_single_field_stats_streaming_behavior() { - let (_temp_dir, parquet_path) = create_test_parquet_with_data().await; - - let ctx = SessionContext::new(); - let table_name = ulid::Ulid::new().to_string(); - ctx.register_parquet( - &table_name, - parquet_path.to_str().unwrap(), - ParquetReadOptions::default(), - ) - .await - .unwrap(); - - // Test that the function handles streaming properly by checking - // that all data is collected correctly across multiple batches - let result = calculate_single_field_stats(ctx.clone(), &table_name, "name", 50).await; - - assert!(result.is_some()); - let stats = result.unwrap(); - - // Verify that the streaming collected all the data - let total_distinct_count: i64 = stats.distinct_stats.iter().map(|s| s.count).sum(); - assert_eq!(total_distinct_count, stats.count); - - // Verify that distinct_stats are properly ordered by count - for i in 1..stats.distinct_stats.len() { - assert!(stats.distinct_stats[i - 1].count >= stats.distinct_stats[i].count); - } - } - - #[tokio::test] - async fn test_calculate_single_field_stats_large_dataset() { - // Create a larger dataset to test streaming behavior - let schema = Arc::new(Schema::new(vec![ - Field::new("id", DataType::Int64, false), - Field::new("category", DataType::Utf8, true), - ])); - - let temp_dir = TempDir::new().unwrap(); - let file_path = temp_dir.path().join("large_data.parquet"); - - use parquet::arrow::AsyncArrowWriter; - use tokio::fs::File; - - // Create 1000 rows with 10 distinct categories - let ids: Vec = (0..1000).collect(); - let categories: Vec> = (0..1000) - .map(|i| { - Some(match i % 10 { - 0 => "cat_0", - 1 => "cat_1", - 2 => "cat_2", - 3 => "cat_3", - 4 => "cat_4", - 5 => "cat_5", - 6 => "cat_6", - 7 => "cat_7", - 8 => "cat_8", - _ => "cat_9", - }) - }) - .collect(); - - let id_array = Int64Array::from(ids); - let category_array = StringArray::from(categories); - - let batch = RecordBatch::try_new( - schema.clone(), - vec![Arc::new(id_array), Arc::new(category_array)], - ) - .unwrap(); - - let file = File::create(&file_path).await.unwrap(); - let mut writer = AsyncArrowWriter::try_new(file, schema, None).unwrap(); - writer.write(&batch).await.unwrap(); - writer.close().await.unwrap(); - - let ctx = SessionContext::new(); - let table_name = ulid::Ulid::new().to_string(); - ctx.register_parquet( - &table_name, - file_path.to_str().unwrap(), - ParquetReadOptions::default(), - ) - .await - .unwrap(); - - let result = calculate_single_field_stats(ctx.clone(), &table_name, "category", 50).await; - - assert!(result.is_some()); - let stats = result.unwrap(); - - assert_eq!(stats.count, 1000); - assert_eq!(stats.distinct_count, 10); - assert_eq!(stats.distinct_stats.len(), 10); + #[test] + fn test_build_stats_sql_uses_file_ids_and_deduped_distinct_values() { + let sql = build_stats_sql( + "astronomy-shop-logs", + Some(&["p_log_category".to_string()]), + 0, + 5, + ); - // Each category should appear 100 times - for distinct_stat in &stats.distinct_stats { - assert_eq!(distinct_stat.count, 100); - } + assert!(sql.contains("COUNT(*) AS total_distinct_count")); + assert!(sql.contains("stats_id")); + assert!(sql.contains("MAX(field_stats_count) AS field_count")); + assert!(sql.contains("SUM(field_count) AS total_field_count")); + assert!(sql.contains("AND field_stats_distinct_stats_distinct_value IS NOT NULL")); + assert!(sql.contains("field_stats_field_name IN ('p_log_category')")); + assert!(sql.contains("fd.total_distinct_count AS distinct_count")); + assert!(!sql.contains("SUM(field_stats_distinct_stats_count) AS total_field_count")); + assert!(!sql.contains("SUM(field_stats_distinct_count)")); } - #[tokio::test] - async fn test_calculate_single_field_stats_with_int_list_field() { - let (_temp_dir, parquet_path) = create_test_parquet_with_data().await; - - let ctx = SessionContext::new(); - let table_name = ulid::Ulid::new().to_string(); + #[test] + fn test_collect_all_field_stats_from_parquet_single_pass() { + let (_temp_dir, parquet_path) = tokio::runtime::Runtime::new() + .unwrap() + .block_on(create_test_parquet_with_data()); + let schema = create_test_schema(); - ctx.register_parquet( - &table_name, - parquet_path.to_str().unwrap(), - ParquetReadOptions::default(), + let field_stats = collect_all_field_stats_from_parquet_blocking( + "test_stream", + &parquet_path, + &schema, + 50, ) - .await .unwrap(); - - // Test int_list field (List) - let result = calculate_single_field_stats(ctx.clone(), &table_name, "int_list", 50).await; - - assert!(result.is_some()); - let stats = result.unwrap(); - - assert_eq!(stats.field_name, "int_list"); - assert_eq!(stats.count, 10); - - // Verify we have the expected distinct lists - // Expected: [1, 2, 3], [4, 5], [6, 7, 8, 9], [1], [10, 11], [], [12, 13, 14], [1, 2] - assert_eq!(stats.distinct_count, 8); - - // Check for duplicate lists - [1, 2, 3] appears twice, [4, 5] appears twice - let list_123_stat = stats - .distinct_stats + let name_stats = field_stats .iter() - .find(|s| s.distinct_value == "[1, 2, 3]"); - assert!(list_123_stat.is_some()); - assert_eq!(list_123_stat.unwrap().count, 2); - - let list_45_stat = stats - .distinct_stats - .iter() - .find(|s| s.distinct_value == "[4, 5]"); - assert!(list_45_stat.is_some()); - assert_eq!(list_45_stat.unwrap().count, 2); + .find(|stats| stats.field_name == "name") + .unwrap(); + assert_eq!(name_stats.count, 10); + assert_eq!(name_stats.distinct_count, 7); + assert_eq!(name_stats.distinct_stats[0].distinct_value, "Alice"); + assert_eq!(name_stats.distinct_stats[0].count, 3); + assert!( + name_stats + .distinct_stats + .iter() + .any(|stat| stat.distinct_value == "NULL" && stat.count == 1) + ); - // Check single occurrence lists - let list_6789_stat = stats - .distinct_stats + let active_stats = field_stats .iter() - .find(|s| s.distinct_value == "[6, 7, 8, 9]"); - assert!(list_6789_stat.is_some()); - assert_eq!(list_6789_stat.unwrap().count, 1); + .find(|stats| stats.field_name == "active") + .unwrap(); + assert_eq!(active_stats.count, 10); + assert_eq!(active_stats.distinct_count, 3); + assert_eq!(active_stats.distinct_stats[0].distinct_value, "true"); + assert_eq!(active_stats.distinct_stats[0].count, 6); - let empty_list_stat = stats - .distinct_stats + let int_list_stats = field_stats .iter() - .find(|s| s.distinct_value == "[]"); - assert!(empty_list_stat.is_some()); - assert_eq!(empty_list_stat.unwrap().count, 1); + .find(|stats| stats.field_name == "int_list") + .unwrap(); + assert_eq!(int_list_stats.count, 10); + assert_eq!(int_list_stats.distinct_count, 8); + assert!( + int_list_stats + .distinct_stats + .iter() + .any(|stat| stat.distinct_value == "[]" && stat.count == 1) + ); } - #[tokio::test] - async fn test_calculate_single_field_stats_with_float_list_field() { - let (_temp_dir, parquet_path) = create_test_parquet_with_data().await; + #[test] + fn test_field_count_state_caps_high_cardinality_values() { + let mut state = + FieldCountState::new("test_stream".to_string(), "request_id".to_string(), 2); + state.max_tracked_values = 8; - let ctx = SessionContext::new(); - let table_name = ulid::Ulid::new().to_string(); - - ctx.register_parquet( - &table_name, - parquet_path.to_str().unwrap(), - ParquetReadOptions::default(), - ) - .await - .unwrap(); + for index in 0..100 { + let mut counts = HashMap::new(); + counts.insert(format!("uuid-{index}"), 1); + state.merge_counts(counts); + } - // Test float_list field (List) - let result = calculate_single_field_stats(ctx.clone(), &table_name, "float_list", 50).await; + assert!(state.approximate); + assert_eq!(state.total_count, 100); + assert_eq!(state.counts.len(), 8); + assert!(state.distinct_count() > 80); + assert_eq!(state.into_distinct_stats(50).len(), 8); + } - assert!(result.is_some()); - let stats = result.unwrap(); + #[test] + fn test_field_count_state_only_evicts_for_stronger_candidate() { + let mut state = FieldCountState::new("test_stream".to_string(), "status".to_string(), 3); + state.max_tracked_values = 3; - assert_eq!(stats.field_name, "float_list"); - assert_eq!(stats.count, 10); + let mut initial_counts = HashMap::new(); + initial_counts.insert("A".to_string(), 100); + initial_counts.insert("B".to_string(), 50); + initial_counts.insert("C".to_string(), 30); + state.merge_counts(initial_counts); - // Expected distinct lists: [1.1, 2.2], [3.3, 4.4, 5.5], [6.6], [7.7, 8.8, 9.9], [10.0], [], [11.1, 12.2], [13.3] - assert_eq!(stats.distinct_count, 8); + let mut weak_candidate = HashMap::new(); + weak_candidate.insert("D".to_string(), 1); + state.merge_counts(weak_candidate); - // Check for duplicate lists - [1.1, 2.2] appears twice, [3.3, 4.4, 5.5] appears twice - let list_11_22_stat = stats - .distinct_stats - .iter() - .find(|s| s.distinct_value == "[1.1, 2.2]"); - assert!(list_11_22_stat.is_some()); - assert_eq!(list_11_22_stat.unwrap().count, 2); + assert!(state.counts.contains_key("C")); + assert!(!state.counts.contains_key("D")); - let list_33_44_55_stat = stats - .distinct_stats - .iter() - .find(|s| s.distinct_value == "[3.3, 4.4, 5.5]"); - assert!(list_33_44_55_stat.is_some()); - assert_eq!(list_33_44_55_stat.unwrap().count, 2); - - // Check single occurrence lists - let list_66_stat = stats - .distinct_stats - .iter() - .find(|s| s.distinct_value == "[6.6]"); - assert!(list_66_stat.is_some()); - assert_eq!(list_66_stat.unwrap().count, 1); + let mut strong_candidate = HashMap::new(); + strong_candidate.insert("E".to_string(), 31); + state.merge_counts(strong_candidate); - let empty_list_stat = stats - .distinct_stats - .iter() - .find(|s| s.distinct_value == "[]"); - assert!(empty_list_stat.is_some()); - assert_eq!(empty_list_stat.unwrap().count, 1); + assert!(!state.counts.contains_key("C")); + assert!(state.counts.contains_key("E")); } } From b0d115403e6c8169c89424e33c6c72a644c16b9d Mon Sep 17 00:00:00 2001 From: Nikhil Sinha <131262146+nikhilsinhaparseable@users.noreply.github.com> Date: Fri, 12 Jun 2026 08:12:27 +0530 Subject: [PATCH 26/47] Ingest api key (#1667) * update ingestion script to use tenant id and api key * update ps1 script --- scripts/ingest.ps1 | 197 ++++++++++++++++++++++++++++++--------------- scripts/ingest.sh | 117 ++++++++++++++------------- 2 files changed, 191 insertions(+), 123 deletions(-) mode change 100644 => 100755 scripts/ingest.sh diff --git a/scripts/ingest.ps1 b/scripts/ingest.ps1 index 5647f4185..4421bc7b9 100644 --- a/scripts/ingest.ps1 +++ b/scripts/ingest.ps1 @@ -5,7 +5,10 @@ param( [string]$Param1, [Parameter(Position=1)] - [string]$Param2 + [string]$Param2, + + [Parameter(Position=2)] + [string]$Param3 ) $ProgressPreference = 'SilentlyContinue' @@ -15,6 +18,8 @@ $BIN_DIR = "$INSTALL_DIR\bin" $FLUENT_BIT_EXE = "$BIN_DIR\fluent-bit.exe" $CONFIG_FILE = "$PSScriptRoot\fluent-bit.conf" $PID_FILE = "$PSScriptRoot\fluent-bit.pid" +$LOG_FILE = "$PSScriptRoot\fluent-bit.log" +$ERROR_LOG_FILE = "$PSScriptRoot\fluent-bit.err.log" $SUPPORTED_ARCH = @("AMD64", "ARM64") @@ -79,8 +84,11 @@ function Show-Status { Get-Process -Id $processId | Format-Table Id, ProcessName, CPU, WS, StartTime -AutoSize Write-Host "" Write-Info "Config file: $CONFIG_FILE" + Write-Info "Log file: $LOG_FILE" + Write-Info "Error log file: $ERROR_LOG_FILE" Write-Host "" - Write-Info "To see output, run: .\fluent-bit.ps1 debug" + Write-Info "To see logs: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 logs" + Write-Info "To stop: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 stop" } else { Write-Warning "Fluent Bit is not running" @@ -91,6 +99,25 @@ function Show-Status { } } +function Show-Logs { + if (Test-Path $LOG_FILE) { + Write-Info "Showing last 80 stdout log lines from $LOG_FILE" + Get-Content -Path $LOG_FILE -Tail 80 + } + else { + Write-Warning "Stdout log file not found: $LOG_FILE" + } + + if (Test-Path $ERROR_LOG_FILE) { + Write-Host "" + Write-Info "Showing last 80 stderr log lines from $ERROR_LOG_FILE" + Get-Content -Path $ERROR_LOG_FILE -Tail 80 + } + else { + Write-Warning "Stderr log file not found: $ERROR_LOG_FILE" + } +} + function Get-Architecture { $arch = $env:PROCESSOR_ARCHITECTURE if ($arch -eq "AMD64") { @@ -172,7 +199,7 @@ function Start-FluentBit { if (Test-FluentBitRunning) { $processId = Get-Content $PID_FILE Write-Warning "Fluent Bit is already running (PID: $processId)" - Write-Info "Use 'fluent-bit.ps1 stop' to stop it first" + Write-Info "Use 'ingest.ps1 stop' to stop it first" return } @@ -189,11 +216,15 @@ function Start-FluentBit { $version = & $FLUENT_BIT_EXE --version 2>$null | Select-Object -First 1 - # Start Fluent Bit process in background (no logging) + Remove-Item $LOG_FILE, $ERROR_LOG_FILE -ErrorAction SilentlyContinue + + # Start Fluent Bit process in background and capture logs $process = Start-Process -FilePath $FLUENT_BIT_EXE ` -ArgumentList "-c", "`"$CONFIG_FILE`"" ` -WorkingDirectory $BIN_DIR ` -WindowStyle Hidden ` + -RedirectStandardOutput $LOG_FILE ` + -RedirectStandardError $ERROR_LOG_FILE ` -PassThru # Save PID @@ -207,16 +238,17 @@ function Start-FluentBit { if (-not $stillRunning) { Write-ErrorMsg "Fluent Bit exited immediately" - Write-ErrorMsg "Run '.\fluent-bit.ps1 debug' to see error details" + Write-ErrorMsg "Run '.\ingest.ps1 debug' to see error details" Remove-Item $PID_FILE -ErrorAction SilentlyContinue exit 1 } else { Write-Info "Fluent Bit started successfully (PID: $($process.Id))" Write-Host "" - Write-Info "To debug: .\fluent-bit.ps1 debug" - Write-Info "To check status: .\fluent-bit.ps1 status" - Write-Info "To stop: .\fluent-bit.ps1 stop" + Write-Info "To debug: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 debug" + Write-Info "To check status: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 status" + Write-Info "To see logs: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 logs" + Write-Info "To stop: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 stop" } } @@ -229,60 +261,86 @@ function Restart-FluentBit { function Setup-FluentBit { param( [string]$IngestorHost, - [string]$CredentialsBase64 + [string]$ApiKey, + [string]$TenantId ) - try { - $credBytes = [Convert]::FromBase64String($CredentialsBase64) - $credentials = [Text.Encoding]::UTF8.GetString($credBytes) - } - catch { - Write-ErrorMsg "Failed to decode base64 credentials" + if ([string]::IsNullOrWhiteSpace($IngestorHost) -or [string]::IsNullOrWhiteSpace($ApiKey)) { + Write-ErrorMsg "Invalid setup parameters" exit 1 } - - $parts = $credentials -split ':', 2 - if ($parts.Length -ne 2) { - Write-ErrorMsg "Invalid credentials format" + + $tlsSetting = "On" + $defaultPort = "443" + if ($IngestorHost -like "https://*") { + $IngestorHost = $IngestorHost.Substring("https://".Length) + $tlsSetting = "On" + $defaultPort = "443" + } + elseif ($IngestorHost -like "http://*") { + $IngestorHost = $IngestorHost.Substring("http://".Length) + $tlsSetting = "Off" + $defaultPort = "80" + } + $IngestorHost = ($IngestorHost -split '/', 2)[0] + + if ($IngestorHost -match '^(.*):([0-9]+)$') { + $Port = $Matches[2] + $IngestorHost = $Matches[1] + } + else { + $Port = $defaultPort + } + + if ([string]::IsNullOrWhiteSpace($IngestorHost)) { + Write-ErrorMsg "Invalid host" exit 1 } - - $username = $parts[0] - $password = $parts[1] - - if ([string]::IsNullOrWhiteSpace($IngestorHost) -or [string]::IsNullOrWhiteSpace($username) -or [string]::IsNullOrWhiteSpace($password)) { - Write-ErrorMsg "Invalid credentials" + + $portNumber = 0 + if (-not [int]::TryParse($Port, [ref]$portNumber) -or $portNumber -lt 1 -or $portNumber -gt 65535) { + Write-ErrorMsg "Invalid port: $Port" + Write-ErrorMsg "Port must be a number between 1 and 65535" exit 1 } - + Install-FluentBit - - $configContent = @" -[SERVICE] - flush 1 - log_level info - -[INPUT] - Name windows_exporter_metrics - Tag node_metrics - Scrape_interval 1 - # Collect only essential metrics - metrics cpu - -[OUTPUT] - Name opentelemetry - Match node_metrics - Host $IngestorHost - Port 443 - Metrics_uri /v1/metrics - Log_response_payload False - TLS On - Http_User $username - Http_Passwd $password - Header X-P-Stream node-metrics - Header X-P-Log-Source otel-metrics - Compress gzip -"@ + + $configLines = @( + "[SERVICE]", + " flush 1", + " log_level info", + "", + "[INPUT]", + " Name windows_exporter_metrics", + " Tag node_metrics", + " Scrape_interval 1", + " # Collect only essential metrics", + " metrics cpu", + "", + "[OUTPUT]", + " Name opentelemetry", + " Match node_metrics", + " Host $IngestorHost", + " Port $Port", + " Metrics_uri /v1/metrics", + " Log_response_payload True", + " TLS $tlsSetting", + " Grpc Off", + " Http2 Off", + " Header X-API-Key $ApiKey" + ) + + if (-not [string]::IsNullOrWhiteSpace($TenantId)) { + $configLines += " Header X-P-Tenant $TenantId" + } + + $configLines += @( + " Header X-P-Stream node-metrics", + " Header X-P-Log-Source otel-metrics" + ) + + $configContent = ($configLines -join [Environment]::NewLine) + [Environment]::NewLine # Use UTF8 without BOM (important for Fluent Bit) $utf8NoBom = New-Object System.Text.UTF8Encoding $false @@ -297,16 +355,18 @@ function Show-Help { Fluent Bit Setup and Management Script for Windows Usage: - Setup: .\fluent-bit.ps1 [host] [base64_credentials] - Stop: .\fluent-bit.ps1 stop - Start: .\fluent-bit.ps1 start - Restart: .\fluent-bit.ps1 restart - Status: .\fluent-bit.ps1 status - Debug: .\fluent-bit.ps1 debug - Run in foreground to see output - -To encode credentials: - `$creds = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("username:password")) - .\fluent-bit.ps1 your-host.com `$creds + Setup: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 [host[:port]] [api_key] [tenant_id] + Stop: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 stop + Start: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 start + Restart: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 restart + Status: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 status + Logs: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 logs + Debug: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 debug + +Example: + powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 https://your-host.com:443 px_api_key + powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 http://localhost:8000 px_api_key tenant-id + "@ } @@ -344,6 +404,9 @@ switch ($Param1.ToLower()) { "status" { Show-Status } + "logs" { + Show-Logs + } "debug" { Debug-FluentBit } @@ -358,10 +421,10 @@ switch ($Param1.ToLower()) { } default { if ([string]::IsNullOrWhiteSpace($Param2)) { - Write-ErrorMsg "Usage: .\fluent-bit.ps1 [host] [base64_credentials]" - Write-ErrorMsg " Or: .\fluent-bit.ps1 [start|stop|restart|status|debug|help]" + Write-ErrorMsg "Usage: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 [host[:port]] [api_key] [tenant_id]" + Write-ErrorMsg " Or: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 [start|stop|restart|status|logs|debug|help]" exit 1 } - Setup-FluentBit -IngestorHost $Param1 -CredentialsBase64 $Param2 + Setup-FluentBit -IngestorHost $Param1 -ApiKey $Param2 -TenantId $Param3 } -} \ No newline at end of file +} diff --git a/scripts/ingest.sh b/scripts/ingest.sh old mode 100644 new mode 100755 index 738b75979..95d69b84d --- a/scripts/ingest.sh +++ b/scripts/ingest.sh @@ -2,15 +2,11 @@ # Fluent Bit Setup and Management Script # Usage: -# Setup: ./fluent-bit.sh -# Where credentials = base64(username:password) -# Stop: ./fluent-bit.sh stop -# Restart: ./fluent-bit.sh restart -# Status: ./fluent-bit.sh status -# Logs: ./fluent-bit.sh logs -# -# To encode credentials: -# echo -n 'username:password' | base64 +# Setup: ./ingest.sh [tenant_id] +# Stop: ./ingest.sh stop +# Restart: ./ingest.sh restart +# Status: ./ingest.sh status +# Logs: ./ingest.sh logs set -e @@ -135,10 +131,7 @@ start_fluent_bit() { if [ ! -f "$CONFIG_FILE" ]; then print_error "Configuration file not found: $CONFIG_FILE" - print_error "Please run setup first with: $0 " - print_error "" - print_error "To encode credentials:" - print_error " echo -n 'username:password' | base64" + print_error "Please run setup first with: $0 [tenant_id]" exit 1 fi @@ -248,30 +241,52 @@ install_fluent_bit() { # Setup function setup_fluent_bit() { local INGESTOR_HOST="$1" - local CREDENTIALS_B64="$2" + local API_KEY="$2" + local TENANT_ID="${3:-}" + local TENANT_HEADER="" + local TLS_SETTING="On" + local DEFAULT_PORT="443" + local PORT="" - # Decode base64 credentials - if ! CREDENTIALS=$(echo "$CREDENTIALS_B64" | base64 -d 2>/dev/null); then - print_error "Failed to decode base64 credentials" - print_error "Please provide credentials in base64 format: username:password" - print_error "" - print_error "To encode your credentials, run:" - print_error " echo -n 'username:password' | base64" + # Validate all fields are present + if [ -z "$INGESTOR_HOST" ] || [ -z "$API_KEY" ]; then + print_error "Invalid setup parameters" + print_error "Expected format: $0 [tenant_id]" exit 1 fi - - # Split credentials by colon - IFS=':' read -r INGESTOR_USERNAME INGESTOR_PASSWORD <<< "$CREDENTIALS" - - # Validate all fields are present - if [ -z "$INGESTOR_HOST" ] || [ -z "$INGESTOR_USERNAME" ] || [ -z "$INGESTOR_PASSWORD" ]; then - print_error "Invalid credentials format" - print_error "Expected format: username:password" - print_error "" - print_error "To encode your credentials, run:" - print_error " echo -n 'username:password' | base64" + + if [[ "$INGESTOR_HOST" =~ ^[Hh][Tt][Tt][Pp][Ss]:// ]]; then + INGESTOR_HOST="${INGESTOR_HOST#*://}" + TLS_SETTING="On" + DEFAULT_PORT="443" + elif [[ "$INGESTOR_HOST" =~ ^[Hh][Tt][Tt][Pp]:// ]]; then + INGESTOR_HOST="${INGESTOR_HOST#*://}" + TLS_SETTING="Off" + DEFAULT_PORT="80" + fi + INGESTOR_HOST="${INGESTOR_HOST%%/*}" + + if [[ "$INGESTOR_HOST" == *:* ]]; then + PORT="${INGESTOR_HOST##*:}" + INGESTOR_HOST="${INGESTOR_HOST%:*}" + else + PORT="$DEFAULT_PORT" + fi + + if [ -z "$INGESTOR_HOST" ]; then + print_error "Invalid host" exit 1 fi + + if ! [[ "$PORT" =~ ^[0-9]+$ ]] || [ "$PORT" -lt 1 ] || [ "$PORT" -gt 65535 ]; then + print_error "Invalid port: $PORT" + print_error "Port must be a number between 1 and 65535" + exit 1 + fi + + if [ -n "$TENANT_ID" ]; then + TENANT_HEADER=" Header X-P-Tenant $TENANT_ID" + fi OS=$(detect_os) @@ -309,17 +324,17 @@ setup_fluent_bit() { Name opentelemetry Match node_metrics Host $INGESTOR_HOST - Port 443 + Port $PORT Metrics_uri /v1/metrics Log_response_payload True - TLS On - Http_User $INGESTOR_USERNAME - Http_Passwd $INGESTOR_PASSWORD + TLS $TLS_SETTING + Header X-API-Key $API_KEY +${TENANT_HEADER} Header X-P-Stream node-metrics Header X-P-Log-Source otel-metrics EOF chmod 600 "$CONFIG_FILE" - sed "s/Http_Passwd.*/Http_Passwd [REDACTED]/" "$CONFIG_FILE" + sed "s/Header X-API-Key.*/Header X-API-Key [REDACTED]/" "$CONFIG_FILE" # Start Fluent Bit echo "" @@ -348,9 +363,7 @@ case "${1:-}" in echo "" echo "Usage:" echo " Setup and start:" - echo " $0 " - echo "" - echo " Credentials format: base64(username:password)" + echo " $0 [tenant_id]" echo "" echo " Management commands:" echo " $0 start - Start Fluent Bit (if config exists)" @@ -359,27 +372,19 @@ case "${1:-}" in echo " $0 status - Show Fluent Bit status" echo " $0 logs - Show Fluent Bit logs" echo "" - echo "To encode your credentials:" - echo " echo -n 'username:password' | base64" - echo "" echo "Example:" - echo " CREDS=\$(echo -n 'user@email.com:password123' | base64)" - echo " $0 example.parseable.com \$CREDS" + echo " $0 https://example.parseable.com:443 px_api_key" + echo " $0 http://localhost:8000 px_api_key tenant-id" ;; *) # If not a command, treat as setup parameters - if [ $# -ne 2 ]; then - print_error "Usage: $0 " + if [ $# -lt 2 ] || [ $# -gt 3 ]; then + print_error "Usage: $0 [tenant_id]" print_error " Or: $0 [start|stop|restart|status|logs|help]" print_error "" - print_error "Credentials format: base64(username:password)" - print_error "" - print_error "To encode your credentials:" - print_error " echo -n 'username:password' | base64" - print_error "" print_error "Example:" - print_error " CREDS=\$(echo -n 'hello@parseable.com:NH7oCUju' | base64)" - print_error " $0 ec9cfee0-2fd4-45eb-8209-d7cd992c4bcc-ingestor.workspace-staging.parseable.com \$CREDS" + print_error " $0 https://ec9cfee0-2fd4-45eb-8209-d7cd992c4bcc-ingestor.workspace-staging.parseable.com:443 px_api_key" + print_error " $0 http://localhost:8000 px_api_key tenant-id" print_error "" print_error "Management commands:" print_error " $0 status - Check if running" @@ -388,6 +393,6 @@ case "${1:-}" in print_error " $0 logs - View logs" exit 1 fi - setup_fluent_bit "$1" "$2" + setup_fluent_bit "$1" "$2" "${3:-}" ;; -esac \ No newline at end of file +esac From 41db1e18eb370d2d45a7b38829be204d217946f9 Mon Sep 17 00:00:00 2001 From: Nikhil Sinha <131262146+nikhilsinhaparseable@users.noreply.github.com> Date: Fri, 12 Jun 2026 08:46:37 +0530 Subject: [PATCH 27/47] chore: reuse time bin logic across apis (#1660) * chore: reuse time bin logic across apis remove binning logic in counts api make reusable component reuse in counts api, errors api, agent-observability related apis * add validations * fix bin logic for the last bin --- src/query/mod.rs | 41 ++++------- src/utils/time.rs | 168 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 182 insertions(+), 27 deletions(-) diff --git a/src/query/mod.rs b/src/query/mod.rs index 14955518e..8ed86b1f3 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -74,7 +74,7 @@ use crate::option::Mode; use crate::parseable::{DEFAULT_TENANT, PARSEABLE}; use crate::query::stream_schema_provider::GlobalSchemaProvider; use crate::storage::{ObjectStorage, ObjectStorageProvider, ObjectStoreFormat}; -use crate::utils::time::TimeRange; +use crate::utils::time::{DATE_BIN_EPOCH_ANCHOR, TimeRange, count_api_bin_interval}; /// Boxed record-batch stream used as the streaming half of query results. type BoxedBatchStream = SendableRecordBatchStream; @@ -563,7 +563,7 @@ impl CountsRequest { let time_range = TimeRange::parse_human_time(&self.start_time, &self.end_time)?; let all_manifest_files = get_manifest_list(&self.stream, &time_range, tenant_id).await?; // get bounds - let counts = self.get_bounds(&time_range); + let counts = self.get_bounds(&time_range)?; // we have start and end times for each bin // we also have all the manifest files for the given time range @@ -606,7 +606,7 @@ impl CountsRequest { } /// Calculate the end time for each bin based on the number of bins - fn get_bounds(&self, time_range: &TimeRange) -> Vec { + fn get_bounds(&self, time_range: &TimeRange) -> Result, QueryError> { let total_minutes = time_range .end .signed_duration_since(time_range.start) @@ -631,6 +631,12 @@ impl CountsRequest { } }; + if num_bins == 0 { + return Err(QueryError::CustomError( + "numBins must be greater than 0".to_string(), + )); + } + // divide minutes by num bins to get minutes per bin let quotient = total_minutes / num_bins; let remainder = total_minutes % num_bins; @@ -668,7 +674,7 @@ impl CountsRequest { }); } - bounds + Ok(bounds) } /// This function will get executed only if self.conditions is some @@ -678,32 +684,13 @@ impl CountsRequest { let time_range = TimeRange::parse_human_time(&self.start_time, &self.end_time)?; - let dur = time_range.end.signed_duration_since(time_range.start); - let table_name = &self.stream; let start_time_col_name = "_bin_start_time_"; let end_time_col_name = "_bin_end_time_"; - let date_bin = if dur.num_minutes() <= 60 * 5 { - // less than 5 hour = 1 min bin - format!( - "CAST(DATE_BIN('1m', \"{table_name}\".\"{time_column}\", TIMESTAMP '1970-01-01 00:00:00+00') AS TEXT) as {start_time_col_name}, DATE_BIN('1m', \"{table_name}\".\"{time_column}\", TIMESTAMP '1970-01-01 00:00:00+00') + INTERVAL '1m' as {end_time_col_name}" - ) - } else if dur.num_minutes() <= 60 * 24 { - // 1 day = 5 min bin - format!( - "CAST(DATE_BIN('5m', \"{table_name}\".\"{time_column}\", TIMESTAMP '1970-01-01 00:00:00+00') AS TEXT) as {start_time_col_name}, DATE_BIN('5m', \"{table_name}\".\"{time_column}\", TIMESTAMP '1970-01-01 00:00:00+00') + INTERVAL '5m' as {end_time_col_name}" - ) - } else if dur.num_minutes() < 60 * 24 * 10 { - // 10 days = 1 hour bin - format!( - "CAST(DATE_BIN('1h', \"{table_name}\".\"{time_column}\", TIMESTAMP '1970-01-01 00:00:00+00') AS TEXT) as {start_time_col_name}, DATE_BIN('1h', \"{table_name}\".\"{time_column}\", TIMESTAMP '1970-01-01 00:00:00+00') + INTERVAL '1h' as {end_time_col_name}" - ) - } else { - // 1 day - format!( - "CAST(DATE_BIN('1d', \"{table_name}\".\"{time_column}\", TIMESTAMP '1970-01-01 00:00:00+00') AS TEXT) as {start_time_col_name}, DATE_BIN('1d', \"{table_name}\".\"{time_column}\", TIMESTAMP '1970-01-01 00:00:00+00') + INTERVAL '1d' as {end_time_col_name}" - ) - }; + let bin_interval = count_api_bin_interval(&time_range.start, &time_range.end); + let date_bin = format!( + "CAST(DATE_BIN('{bin_interval}', \"{table_name}\".\"{time_column}\", TIMESTAMP '{DATE_BIN_EPOCH_ANCHOR}') AS TEXT) as {start_time_col_name}, DATE_BIN('{bin_interval}', \"{table_name}\".\"{time_column}\", TIMESTAMP '{DATE_BIN_EPOCH_ANCHOR}') + INTERVAL '{bin_interval}' as {end_time_col_name}" + ); let group_by_cols = count_conditions .group_by diff --git a/src/utils/time.rs b/src/utils/time.rs index a57dac631..35e8c9b53 100644 --- a/src/utils/time.rs +++ b/src/utils/time.rs @@ -18,6 +18,8 @@ use chrono::{DateTime, Datelike, NaiveDate, NaiveDateTime, TimeDelta, TimeZone, Timelike, Utc}; +pub const DATE_BIN_EPOCH_ANCHOR: &str = "1970-01-01 00:00:00+00"; + #[derive(Debug, thiserror::Error)] pub enum TimeParseError { #[error("Parsing humantime")] @@ -30,6 +32,14 @@ pub enum TimeParseError { StartTimeAfterEndTime, } +#[derive(Debug, thiserror::Error)] +pub enum TimeBinError { + #[error("num_bins must be greater than 0")] + InvalidNumBins, + #[error("time bin interval is out of range")] + IntervalOutOfRange, +} + type Prefix = String; #[derive(Clone, Copy)] @@ -55,6 +65,107 @@ pub struct TimeRange { pub end: DateTime, } +pub fn count_api_bin_interval(start: &DateTime, end: &DateTime) -> &'static str { + let dur = end.signed_duration_since(*start); + + if dur.num_minutes() <= 60 * 5 { + "1m" + } else if dur.num_minutes() <= 60 * 24 { + "5m" + } else if dur.num_minutes() < 60 * 24 * 10 { + "1h" + } else { + "1d" + } +} + +pub fn interval_for_num_bins( + start: &DateTime, + end: &DateTime, + num_bins: u64, +) -> Result { + if num_bins == 0 { + return Err(TimeBinError::InvalidNumBins); + } + + let total_seconds = end.signed_duration_since(*start).num_seconds().max(1) as u64; + let bin_seconds = (total_seconds / num_bins).max(1); + Ok(format!("{bin_seconds} seconds")) +} + +pub fn expected_time_bins( + time_range: &TimeRange, + num_bins: u64, +) -> Result, TimeBinError> { + if num_bins == 0 { + return Err(TimeBinError::InvalidNumBins); + } + + let total_seconds = time_range + .end + .signed_duration_since(time_range.start) + .num_seconds() + .max(1) as u64; + let bin_seconds = (total_seconds / num_bins).max(1); + let bin_seconds_i64 = + i64::try_from(bin_seconds).map_err(|_| TimeBinError::IntervalOutOfRange)?; + let first_bin_start = + time_range.start.timestamp().div_euclid(bin_seconds_i64) * bin_seconds_i64; + let first_bin_start = + DateTime::from_timestamp(first_bin_start, 0).ok_or(TimeBinError::IntervalOutOfRange)?; + + let seconds_to_cover = time_range.end.timestamp() - first_bin_start.timestamp(); + let bins_to_cover_range = (seconds_to_cover + bin_seconds_i64 - 1) / bin_seconds_i64; + let bins_to_cover_range = + u64::try_from(bins_to_cover_range).map_err(|_| TimeBinError::IntervalOutOfRange)?; + + (0..bins_to_cover_range) + .map(|i| { + let offset = i64::try_from(i) + .ok() + .and_then(|i| i.checked_mul(bin_seconds_i64)) + .ok_or(TimeBinError::IntervalOutOfRange)?; + let bin_start = first_bin_start + .checked_add_signed(chrono::Duration::seconds(offset)) + .ok_or(TimeBinError::IntervalOutOfRange)?; + let next_bin_start = bin_start + .checked_add_signed(chrono::Duration::seconds(bin_seconds_i64)) + .ok_or(TimeBinError::IntervalOutOfRange)?; + let bin_end = next_bin_start.min(time_range.end); + Ok((bin_start.to_rfc3339(), bin_end.to_rfc3339())) + }) + .collect() +} + +pub fn match_time_bin_key(sql_bin_start: &str, expected_bins: &[(String, String)]) -> String { + let has_timezone = sql_bin_start.ends_with('Z') + || DateTime::parse_from_rfc3339(sql_bin_start).is_ok() + || sql_bin_start + .rsplit_once(['+', '-']) + .is_some_and(|(_, suffix)| { + suffix.len() == 5 && suffix.as_bytes().get(2) == Some(&b':') + }); + let normalized = if !has_timezone { + format!("{sql_bin_start}+00:00") + } else { + sql_bin_start.to_string() + }; + + if let Ok(sql_ts) = DateTime::parse_from_rfc3339(&normalized) { + let sql_ts = sql_ts.with_timezone(&Utc); + for (bs, _) in expected_bins { + if let Ok(expected_ts) = DateTime::parse_from_rfc3339(bs) { + let expected_ts = expected_ts.with_timezone(&Utc); + if (sql_ts - expected_ts).num_seconds().abs() <= 1 { + return bs.clone(); + } + } + } + } + + sql_bin_start.to_string() +} + impl TimeRange { pub fn new(start: DateTime, end: DateTime) -> Self { TimeRange { start, end } @@ -442,6 +553,63 @@ mod tests { assert!(matches!(result, Err(TimeParseError::HumanTime(_)))); } + #[test] + fn interval_for_num_bins_rejects_zero_bins() { + let start = Utc.with_ymd_and_hms(2023, 1, 1, 12, 0, 0).unwrap(); + let end = Utc.with_ymd_and_hms(2023, 1, 1, 12, 10, 0).unwrap(); + + let result = interval_for_num_bins(&start, &end, 0); + + assert!(matches!(result, Err(TimeBinError::InvalidNumBins))); + } + + #[test] + fn expected_time_bins_rejects_zero_bins() { + let time_range = time_period_from_str("2023-01-01T12:00:00Z", "2023-01-01T12:10:00Z"); + + let result = expected_time_bins(&time_range, 0); + + assert!(matches!(result, Err(TimeBinError::InvalidNumBins))); + } + + #[test] + fn expected_time_bins_aligns_to_epoch_anchor() { + let time_range = time_period_from_str("2023-01-01T12:02:00Z", "2023-01-01T12:12:00Z"); + + let bins = expected_time_bins(&time_range, 2).unwrap(); + + assert_eq!(bins.len(), 3); + assert_eq!(bins[0].0, "2023-01-01T12:00:00+00:00"); + assert_eq!(bins[0].1, "2023-01-01T12:05:00+00:00"); + assert_eq!(bins[1].0, "2023-01-01T12:05:00+00:00"); + assert_eq!(bins[1].1, "2023-01-01T12:10:00+00:00"); + assert_eq!(bins[2].0, "2023-01-01T12:10:00+00:00"); + assert_eq!(bins[2].1, "2023-01-01T12:12:00+00:00"); + } + + #[test] + fn expected_time_bins_covers_full_range_when_start_is_mid_bin() { + let time_range = time_period_from_str("2026-06-01T05:56:00Z", "2026-06-04T05:56:00Z"); + + let bins = expected_time_bins(&time_range, 60).unwrap(); + + assert_eq!(bins.len(), 61); + assert_eq!(bins.last().unwrap().0, "2026-06-04T04:48:00+00:00"); + assert_eq!(bins.last().unwrap().1, "2026-06-04T05:56:00+00:00"); + } + + #[test] + fn match_time_bin_key_handles_negative_timezone_offsets() { + let expected_bins = vec![( + "2023-01-01T05:00:00+00:00".to_string(), + "2023-01-01T06:00:00+00:00".to_string(), + )]; + + let matched = match_time_bin_key("2023-01-01T00:00:00-05:00", &expected_bins); + + assert_eq!(matched, "2023-01-01T05:00:00+00:00"); + } + fn time_period_from_str(start: &str, end: &str) -> TimeRange { TimeRange { start: DateTime::parse_from_rfc3339(start).unwrap().into(), From 8c40db1cd428c3b684a395046efd47be5522c925 Mon Sep 17 00:00:00 2001 From: Nikhil Sinha <131262146+nikhilsinhaparseable@users.noreply.github.com> Date: Fri, 12 Jun 2026 11:24:29 +0530 Subject: [PATCH 28/47] chore: remove unwanted error and warning logs (#1676) 1. on fresh deployment - server startup - when alerts and targets load 2. on delete stream - at query node - delete stats 3. on delete stream - at query node - delete staging 4. get_objects in object store --- .../http/modal/ingest/ingestor_logstream.rs | 8 +++++- src/handlers/http/modal/mod.rs | 2 +- .../metastores/object_store_metastore.rs | 27 ++++++++++++++++--- src/stats.rs | 8 +++++- src/storage/azure_blob.rs | 12 +++------ src/storage/field_stats.rs | 2 +- src/storage/gcs.rs | 12 +++------ src/storage/s3.rs | 12 +++------ 8 files changed, 51 insertions(+), 32 deletions(-) diff --git a/src/handlers/http/modal/ingest/ingestor_logstream.rs b/src/handlers/http/modal/ingest/ingestor_logstream.rs index 9f7135068..9f7414baa 100644 --- a/src/handlers/http/modal/ingest/ingestor_logstream.rs +++ b/src/handlers/http/modal/ingest/ingestor_logstream.rs @@ -26,6 +26,7 @@ use actix_web::{ use bytes::Bytes; use tracing::warn; +use crate::option::Mode; use crate::{ catalog::remove_manifest_from_snapshot, handlers::http::logstream::error::StreamError, @@ -77,7 +78,12 @@ pub async fn delete( let tenant_id = get_tenant_id_from_request(&req); // Delete from staging let stream_dir = PARSEABLE.get_stream(&stream_name, &tenant_id)?; - if let Err(err) = fs::remove_dir_all(&stream_dir.data_path) { + + // delete staging only for ingest server or standalone server + // else skip + if (PARSEABLE.options.mode == Mode::Ingest || PARSEABLE.options.mode == Mode::All) + && let Err(err) = fs::remove_dir_all(&stream_dir.data_path) + { warn!( "failed to delete local data for stream {} with error {err}. Clean {} manually", stream_name, diff --git a/src/handlers/http/modal/mod.rs b/src/handlers/http/modal/mod.rs index 6e6bb370a..176d4cefe 100644 --- a/src/handlers/http/modal/mod.rs +++ b/src/handlers/http/modal/mod.rs @@ -130,7 +130,7 @@ pub trait ParseableServer { .backlog(PARSEABLE.options.connection_backlog) .max_connections(PARSEABLE.options.max_connections) .shutdown_timeout(60); - tracing::warn!( + tracing::info!( "Starting server with-\nNum workers: {}\nKeep Alive: {}\nRequest timeout: {}\nConnection backlog: {}\nMax connections: {}", PARSEABLE.options.num_workers, PARSEABLE.options.keep_alive, diff --git a/src/metastore/metastores/object_store_metastore.rs b/src/metastore/metastores/object_store_metastore.rs index d22f87226..5ae01d508 100644 --- a/src/metastore/metastores/object_store_metastore.rs +++ b/src/metastore/metastores/object_store_metastore.rs @@ -65,6 +65,14 @@ pub struct ObjectStoreMetastore { pub storage: Arc, } +fn is_missing_optional_dir(err: &ObjectStorageError) -> bool { + match err { + ObjectStorageError::NoSuchKey(_) => true, + ObjectStorageError::IoError(err) => err.kind() == std::io::ErrorKind::NotFound, + _ => false, + } +} + #[async_trait] impl Metastore for ObjectStoreMetastore { /// Since Parseable already starts with a connection to an object store, no need to implement this @@ -263,7 +271,7 @@ impl Metastore for ObjectStoreMetastore { let mut all_alerts = HashMap::new(); for mut tenant in base_paths { let alerts_path = RelativePathBuf::from_iter([&tenant, ALERTS_ROOT_DIRECTORY]); - let alerts = self + let alerts = match self .storage .get_objects( Some(&alerts_path), @@ -272,7 +280,12 @@ impl Metastore for ObjectStoreMetastore { }), &Some(tenant.clone()), ) - .await?; + .await + { + Ok(alerts) => alerts, + Err(err) if is_missing_optional_dir(&err) => Vec::new(), + Err(err) => return Err(MetastoreError::ObjectStorageError(err)), + }; if tenant.is_empty() { tenant.clone_from(&DEFAULT_TENANT.to_string()); } @@ -1122,14 +1135,20 @@ impl Metastore for ObjectStoreMetastore { SETTINGS_ROOT_DIRECTORY, TARGETS_ROOT_DIRECTORY, ]); - let targets = self + let target_bytes = match self .storage .get_objects( Some(&targets_path), Box::new(|file_name| file_name.ends_with(".json")), &Some(tenant.clone()), ) - .await? + .await + { + Ok(targets) => targets, + Err(err) if is_missing_optional_dir(&err) => Vec::new(), + Err(err) => return Err(MetastoreError::ObjectStorageError(err)), + }; + let targets = target_bytes .iter() .filter_map(|bytes| { serde_json::from_slice(bytes) diff --git a/src/stats.rs b/src/stats.rs index 6d7673339..9101fbce9 100644 --- a/src/stats.rs +++ b/src/stats.rs @@ -31,7 +31,8 @@ use crate::metrics::{ EVENTS_STORAGE_SIZE_DATE, LIFETIME_EVENTS_INGESTED, LIFETIME_EVENTS_INGESTED_SIZE, LIFETIME_EVENTS_STORAGE_SIZE, STORAGE_SIZE, }; -use crate::parseable::DEFAULT_TENANT; +use crate::option::Mode; +use crate::parseable::{DEFAULT_TENANT, PARSEABLE}; use crate::storage::{ObjectStorage, ObjectStorageError, ObjectStoreFormat}; /// Helper struct type created by copying stats values from metadata @@ -187,6 +188,11 @@ pub fn delete_stats( format: &'static str, tenant_id: &Option, ) -> prometheus::Result<()> { + // delete stats only for ingest server or standalone server + // else skip + if PARSEABLE.options.mode != Mode::Ingest && PARSEABLE.options.mode != Mode::All { + return Ok(()); + } let event_labels = event_labels(stream_name, format, tenant_id); let storage_size_labels = storage_size_labels(stream_name, tenant_id); diff --git a/src/storage/azure_blob.rs b/src/storage/azure_blob.rs index 1482f2333..db0b98c62 100644 --- a/src/storage/azure_blob.rs +++ b/src/storage/azure_blob.rs @@ -320,8 +320,7 @@ impl BlobStore { #[tracing::instrument( name = "azure.get_object", skip(self), - fields(path = %path, tenant = ?tenant_id, bytes = tracing::field::Empty), - err + fields(path = %path, tenant = ?tenant_id, bytes = tracing::field::Empty) )] async fn _get_object( &self, @@ -446,8 +445,7 @@ impl BlobStore { #[tracing::instrument( name = "azure.list_dates", skip(self), - fields(stream = %stream, tenant = ?tenant_id, dates = tracing::field::Empty), - err + fields(stream = %stream, tenant = ?tenant_id, dates = tracing::field::Empty) )] async fn _list_dates( &self, @@ -651,8 +649,7 @@ impl ObjectStorage for BlobStore { #[tracing::instrument( name = "azure.head", skip(self), - fields(path = %path, tenant = ?tenant_id), - err + fields(path = %path, tenant = ?tenant_id) )] async fn head( &self, @@ -827,8 +824,7 @@ impl ObjectStorage for BlobStore { #[tracing::instrument( name = "azure.check", skip(self), - fields(tenant = ?tenant_id, ok = tracing::field::Empty), - err + fields(tenant = ?tenant_id, ok = tracing::field::Empty) )] async fn check(&self, tenant_id: &Option) -> Result<(), ObjectStorageError> { let result = self diff --git a/src/storage/field_stats.rs b/src/storage/field_stats.rs index a9586f52f..a68c5f357 100644 --- a/src/storage/field_stats.rs +++ b/src/storage/field_stats.rs @@ -453,7 +453,7 @@ impl FieldCountState { if !self.approximate { self.approximate = true; - warn!( + debug!( "Field stats cardinality cap reached for stream {} field {}. Tracking bounded top-value candidates with max_tracked_values={}", self.stream_name, self.field_name, self.max_tracked_values ); diff --git a/src/storage/gcs.rs b/src/storage/gcs.rs index 01c98848f..2ef85568b 100644 --- a/src/storage/gcs.rs +++ b/src/storage/gcs.rs @@ -287,8 +287,7 @@ impl Gcs { #[tracing::instrument( name = "gcs.get_object", skip(self), - fields(path = %path, tenant = ?tenant_id, bytes = tracing::field::Empty), - err + fields(path = %path, tenant = ?tenant_id, bytes = tracing::field::Empty) )] async fn _get_object( &self, @@ -412,8 +411,7 @@ impl Gcs { #[tracing::instrument( name = "gcs.list_dates", skip(self), - fields(stream = %stream, tenant = ?tenant_id, dates = tracing::field::Empty), - err + fields(stream = %stream, tenant = ?tenant_id, dates = tracing::field::Empty) )] async fn _list_dates( &self, @@ -633,8 +631,7 @@ impl ObjectStorage for Gcs { #[tracing::instrument( name = "gcs.head", skip(self), - fields(path = %path, tenant = ?tenant_id), - err + fields(path = %path, tenant = ?tenant_id) )] async fn head( &self, @@ -809,8 +806,7 @@ impl ObjectStorage for Gcs { #[tracing::instrument( name = "gcs.check", skip(self), - fields(tenant = ?tenant_id, ok = tracing::field::Empty), - err + fields(tenant = ?tenant_id, ok = tracing::field::Empty) )] async fn check(&self, tenant_id: &Option) -> Result<(), ObjectStorageError> { let result = self diff --git a/src/storage/s3.rs b/src/storage/s3.rs index 589d71b94..0a1d2f752 100644 --- a/src/storage/s3.rs +++ b/src/storage/s3.rs @@ -477,8 +477,7 @@ impl S3 { #[tracing::instrument( name = "s3.get_object", skip(self), - fields(path = %path, tenant = ?tenant_id, bytes = tracing::field::Empty), - err + fields(path = %path, tenant = ?tenant_id, bytes = tracing::field::Empty) )] async fn _get_object( &self, @@ -615,8 +614,7 @@ impl S3 { #[tracing::instrument( name = "s3.list_dates", skip(self), - fields(stream = %stream, tenant = ?tenant_id, dates = tracing::field::Empty), - err + fields(stream = %stream, tenant = ?tenant_id, dates = tracing::field::Empty) )] async fn _list_dates( &self, @@ -848,8 +846,7 @@ impl ObjectStorage for S3 { #[tracing::instrument( name = "s3.head", skip(self), - fields(path = %path, tenant = ?tenant_id), - err + fields(path = %path, tenant = ?tenant_id) )] async fn head( &self, @@ -1046,8 +1043,7 @@ impl ObjectStorage for S3 { #[tracing::instrument( name = "s3.check", skip(self), - fields(tenant = ?tenant_id, ok = tracing::field::Empty), - err + fields(tenant = ?tenant_id, ok = tracing::field::Empty) )] async fn check(&self, tenant_id: &Option) -> Result<(), ObjectStorageError> { let tenant_str = tenant_id.as_deref().unwrap_or(DEFAULT_TENANT); From 832f3ca9980be9e8797a365428b974b1b9bd62f0 Mon Sep 17 00:00:00 2001 From: Nikhil Sinha <131262146+nikhilsinhaparseable@users.noreply.github.com> Date: Sat, 13 Jun 2026 23:00:53 +0530 Subject: [PATCH 29/47] update Cargo.toml for release v2.9.0 (#1677) --- Cargo.lock | 2 +- Cargo.toml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d038a589f..7cb2eb42e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4056,7 +4056,7 @@ dependencies = [ [[package]] name = "parseable" -version = "2.8.0" +version = "2.9.0" dependencies = [ "actix-cors", "actix-web", diff --git a/Cargo.toml b/Cargo.toml index 88613953a..4c5cffe03 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "parseable" -version = "2.8.0" +version = "2.9.0" authors = ["Parseable Team "] edition = "2024" rust-version = "1.91.0" @@ -215,8 +215,8 @@ arrow = "58.0.0" temp-dir = "0.1.14" [package.metadata.parseable_ui] -assets-url = "https://parseable-prism-build.s3.us-east-2.amazonaws.com/v2.8.0/build.zip" -assets-sha1 = "a7523ef819d38678275ae165c443564b2f9a3fc1" +assets-url = "https://parseable-prism-build.s3.us-east-2.amazonaws.com/v2.9.0/build.zip" +assets-sha1 = "3d54e032e1211f50b1770763f057a97354080954" [features] debug = [] From 29a0f8948b65a3edbc94bfe84ab06402b2132f9a Mon Sep 17 00:00:00 2001 From: Nikhil Sinha <131262146+nikhilsinhaparseable@users.noreply.github.com> Date: Mon, 15 Jun 2026 17:39:21 +0530 Subject: [PATCH 30/47] perf: field stats more performant for high volume ingestion (#1679) --- src/storage/field_stats.rs | 162 ++++++++++++++++++++-------------- src/storage/object_storage.rs | 139 +++++++++++++++++++---------- 2 files changed, 191 insertions(+), 110 deletions(-) diff --git a/src/storage/field_stats.rs b/src/storage/field_stats.rs index a68c5f357..0b7e9fa7d 100644 --- a/src/storage/field_stats.rs +++ b/src/storage/field_stats.rs @@ -76,24 +76,17 @@ use std::collections::{HashMap, HashSet}; use std::fmt::Debug; use std::fs::File; use std::path::Path; -use std::sync::Arc; use std::time::Instant; -use tokio::sync::Semaphore; use tracing::{debug, error, warn}; pub const DATASET_STATS_STREAM_NAME: &str = "pstats"; const DATASET_STATS_CUSTOM_PARTITION: &str = "dataset_name"; -const MAX_CONCURRENT_FIELD_STATS: usize = 4; -const PARALLEL_FIELD_STATS_MIN_FIELDS: usize = 16; const MIN_TRACKED_DISTINCT_VALUES_PER_FIELD: usize = 1024; const MAX_TRACKED_DISTINCT_VALUES_PER_FIELD: usize = 10_000; const HLL_PRECISION_BITS: u32 = 12; const HLL_REGISTER_COUNT: usize = 1 << HLL_PRECISION_BITS; -static FIELD_STATS_QUERY_SEMAPHORE: Lazy> = - Lazy::new(|| Arc::new(Semaphore::new(MAX_CONCURRENT_FIELD_STATS))); static FIELD_STATS_RAYON_POOL: Lazy = Lazy::new(|| { ThreadPoolBuilder::new() - .num_threads(MAX_CONCURRENT_FIELD_STATS) .thread_name(|index| format!("field-stats-{index}")) .build() .expect("field stats rayon pool should initialize") @@ -139,7 +132,7 @@ pub async fn calculate_field_stats( tenant_id, ) .await; - log_stats_calculation_time(stream_name, parquet_path, started_at, result.is_ok()); + log_stats_calculation_time(stream_name, parquet_path, started_at, &result); result } @@ -218,20 +211,21 @@ fn log_stats_calculation_time( stream_name: &str, parquet_path: &Path, started_at: Instant, - success: bool, + result: &Result, ) { let parquet_file = parquet_path .file_name() .and_then(|filename| filename.to_str()) .unwrap_or(""); let elapsed_ms = started_at.elapsed().as_millis(); - if success { + if result.is_ok() { debug!( "Field stats calculation completed for parquet file {parquet_file} in {elapsed_ms} ms. stream={stream_name}" ); } else { + let err = result.as_ref().expect_err("result checked as error"); warn!( - "Field stats calculation failed for parquet file {parquet_file} after {elapsed_ms} ms. stream={stream_name}" + "Field stats calculation failed for parquet file {parquet_file} after {elapsed_ms} ms. stream={stream_name}: {err}" ); } } @@ -260,22 +254,11 @@ async fn collect_all_field_stats_from_parquet( schema: &Schema, max_field_statistics: usize, ) -> Result, PostError> { - let permit = FIELD_STATS_QUERY_SEMAPHORE - .clone() - .acquire_owned() - .await - .map_err(|e| { - PostError::Invalid(anyhow::anyhow!( - "Failed to acquire field stats query permit: {}", - e - )) - })?; let stream_name = stream_name.to_string(); let parquet_path = parquet_path.to_path_buf(); let schema = schema.clone(); tokio::task::spawn_blocking(move || { - let _permit = permit; collect_all_field_stats_from_parquet_blocking( &stream_name, &parquet_path, @@ -335,14 +318,15 @@ fn collect_all_field_stats_from_parquet_blocking( } }; - let batch_counts = if field_names.len() >= PARALLEL_FIELD_STATS_MIN_FIELDS { - collect_batch_field_counts_parallel(&batch, &field_names) - } else { - collect_batch_field_counts_serial(&batch, &field_names) - }; + let batch_field_counts = collect_batch_field_counts_parallel( + stream_name, + &batch, + &field_names, + max_field_statistics, + ); - for (field_name, counts) in batch_counts { - merge_field_counts(&mut field_counts, &field_name, counts); + for (field_name, batch_field_count) in batch_field_counts { + merge_field_counts(&mut field_counts, &field_name, batch_field_count); } } @@ -364,53 +348,53 @@ fn collect_all_field_stats_from_parquet_blocking( .collect()) } -fn collect_batch_field_counts_serial( - batch: &arrow_array::RecordBatch, - field_names: &[String], -) -> Vec<(String, HashMap)> { - field_names - .iter() - .filter_map(|field_name| collect_field_counts(batch, field_name)) - .collect() -} - fn collect_batch_field_counts_parallel( + stream_name: &str, batch: &arrow_array::RecordBatch, field_names: &[String], -) -> Vec<(String, HashMap)> { + max_field_statistics: usize, +) -> Vec<(String, FieldCountState)> { FIELD_STATS_RAYON_POOL.install(|| { field_names .par_iter() - .filter_map(|field_name| collect_field_counts(batch, field_name)) + .filter_map(|field_name| { + collect_field_counts(stream_name, batch, field_name, max_field_statistics) + }) .collect() }) } fn collect_field_counts( + stream_name: &str, batch: &arrow_array::RecordBatch, field_name: &str, -) -> Option<(String, HashMap)> { + max_field_statistics: usize, +) -> Option<(String, FieldCountState)> { let array = batch.column_by_name(field_name)?; - let mut counts = HashMap::new(); + let mut field_count = FieldCountState::new_for_batch( + stream_name.to_string(), + field_name.to_string(), + max_field_statistics, + ); for row_index in 0..array.len() { let value = format_arrow_value(array.as_ref(), row_index); - *counts.entry(value).or_default() += 1; + field_count.record_value(value); } - Some((field_name.to_string(), counts)) + Some((field_name.to_string(), field_count)) } fn merge_field_counts( field_counts: &mut HashMap, field_name: &str, - counts: HashMap, + batch_field_count: FieldCountState, ) { let field_total = field_counts .get_mut(field_name) .expect("field_counts initialized for each field"); - field_total.merge_counts(counts); + field_total.merge_state(batch_field_count); } struct FieldCountState { @@ -421,10 +405,24 @@ struct FieldCountState { hll: HyperLogLog, max_tracked_values: usize, approximate: bool, + log_cardinality_cap: bool, } impl FieldCountState { fn new(stream_name: String, field_name: String, max_field_statistics: usize) -> Self { + Self::new_with_logging(stream_name, field_name, max_field_statistics, true) + } + + fn new_for_batch(stream_name: String, field_name: String, max_field_statistics: usize) -> Self { + Self::new_with_logging(stream_name, field_name, max_field_statistics, false) + } + + fn new_with_logging( + stream_name: String, + field_name: String, + max_field_statistics: usize, + log_cardinality_cap: bool, + ) -> Self { Self { stream_name, field_name, @@ -433,38 +431,68 @@ impl FieldCountState { hll: HyperLogLog::new(), max_tracked_values: tracked_distinct_value_limit(max_field_statistics), approximate: false, + log_cardinality_cap, } } + fn record_value(&mut self, value: String) { + self.hll.add(&value); + self.total_count += 1; + self.track_value(value, 1); + } + + #[cfg(test)] fn merge_counts(&mut self, counts: HashMap) { for (value, count) in counts { self.hll.add(&value); self.total_count += count; + self.track_value(value, count); + } + } - if let Some(existing_count) = self.counts.get_mut(&value) { - *existing_count += count; - continue; - } + fn merge_state(&mut self, state: FieldCountState) { + self.hll.merge(&state.hll); + self.total_count += state.total_count; - if self.counts.len() < self.max_tracked_values { - self.counts.insert(value, count); - continue; - } + if state.approximate { + self.mark_approximate(); + } - if !self.approximate { - self.approximate = true; + for (value, count) in state.counts { + self.track_value(value, count); + } + } + + fn track_value(&mut self, value: String, count: i64) { + if let Some(existing_count) = self.counts.get_mut(&value) { + *existing_count += count; + return; + } + + if self.counts.len() < self.max_tracked_values { + self.counts.insert(value, count); + return; + } + + self.mark_approximate(); + + if let Some((min_value, min_count)) = self.current_min_value() + && count > min_count + { + self.counts.remove(&min_value); + self.counts.insert(value, count); + } + } + + fn mark_approximate(&mut self) { + if !self.approximate { + self.approximate = true; + if self.log_cardinality_cap { debug!( "Field stats cardinality cap reached for stream {} field {}. Tracking bounded top-value candidates with max_tracked_values={}", self.stream_name, self.field_name, self.max_tracked_values ); } - - if let Some((min_value, min_count)) = self.current_min_value() - && count > min_count - { - self.counts.remove(&min_value); - self.counts.insert(value, count); - } } } @@ -533,6 +561,12 @@ impl HyperLogLog { self.registers[register_index] = self.registers[register_index].max(rank); } + fn merge(&mut self, other: &Self) { + for (register, other_register) in self.registers.iter_mut().zip(other.registers.iter()) { + *register = (*register).max(*other_register); + } + } + fn estimate(&self) -> f64 { let register_count = HLL_REGISTER_COUNT as f64; let zero_registers = self diff --git a/src/storage/object_storage.rs b/src/storage/object_storage.rs index 9c9089faa..b9087c91c 100644 --- a/src/storage/object_storage.rs +++ b/src/storage/object_storage.rs @@ -98,10 +98,15 @@ pub(crate) struct UploadResult { manifest_file: Option, } +struct UploadedParquetFile { + file_path: std::path::PathBuf, + manifest_file: catalog::manifest::File, +} + /// Handles the upload of a single parquet file #[tracing::instrument( name = "object_store.upload_single_parquet", - skip(store, schema), + skip(store), fields(stream = %stream_name, tenant = ?tenant_id, path = %path.display(), key = %stream_relative_path) )] async fn upload_single_parquet_file( @@ -109,7 +114,6 @@ async fn upload_single_parquet_file( path: std::path::PathBuf, stream_relative_path: String, stream_name: String, - schema: Arc, tenant_id: Option, ) -> Result { let filename = path @@ -185,11 +189,6 @@ async fn upload_single_parquet_file( let manifest = catalog::create_from_parquet_file(absolute_path, &path) .map_err(|e| (path.clone(), ObjectStorageError::from(e)))?; - if PARSEABLE.options.collect_dataset_stats { - // collect field stats if enabled - calculate_stats_if_enabled(&stream_name, &path, &schema, tenant_id).await; - } - Ok(UploadResult { file_path: path, manifest_file: Some(manifest), @@ -1038,11 +1037,22 @@ pub trait ObjectStorage: Debug + Send + Sync + 'static { let upload_context = UploadContext::new(stream); // Process parquet files concurrently and collect results - let manifest_files = + let uploaded_files = process_parquet_files(&upload_context, stream_name, tenant_id.clone()).await?; // Update snapshot with collected manifest files - update_snapshot_with_manifests(stream_name, manifest_files, &tenant_id).await?; + update_snapshot_with_manifests(stream_name, &uploaded_files, &tenant_id).await?; + + // Calculate stats after snapshot update so uploads are visible before stats work starts. + calculate_stats_for_uploaded_files( + &uploaded_files, + stream_name, + &upload_context, + &tenant_id, + ) + .await; + + cleanup_uploaded_staged_files(uploaded_files).await; // Process schema files process_schema_files(&upload_context, stream_name, &tenant_id).await?; @@ -1068,7 +1078,7 @@ async fn process_parquet_files( upload_context: &UploadContext, stream_name: &str, tenant_id: Option, -) -> Result, ObjectStorageError> { +) -> Result, ObjectStorageError> { let semaphore = Arc::new(tokio::sync::Semaphore::new(100)); let mut join_set = JoinSet::new(); let object_store = PARSEABLE.storage().get_object_store(); @@ -1154,21 +1164,13 @@ async fn spawn_parquet_upload_task( ); let stream_name = stream_name.to_string(); - let schema = upload_context.schema.clone(); let handle = FLUSH_AND_CONVERT_RUNTIME.handle(); join_set.spawn_on( async move { let _permit = semaphore.acquire().await.expect("semaphore is not closed"); - upload_single_parquet_file( - store, - path, - stream_relative_path, - stream_name, - schema, - tenant_id, - ) - .await + upload_single_parquet_file(store, path, stream_relative_path, stream_name, tenant_id) + .await }, handle, ); @@ -1177,14 +1179,17 @@ async fn spawn_parquet_upload_task( /// Collects results from all upload tasks async fn collect_upload_results( mut join_set: JoinSet>, -) -> Result, ObjectStorageError> { +) -> Result, ObjectStorageError> { let mut uploaded_files = Vec::new(); while let Some(result) = join_set.join_next().await { match result { Ok(Ok(upload_result)) => { if let Some(manifest_file) = upload_result.manifest_file { - uploaded_files.push((upload_result.file_path, manifest_file)); + uploaded_files.push(UploadedParquetFile { + file_path: upload_result.file_path, + manifest_file, + }); } else { // File failed in upload size validation, preserve staging file for retry error!( @@ -1212,10 +1217,69 @@ async fn collect_upload_results( } } - // successfully uploaded files, remove from in-mem hashset + Ok(uploaded_files) +} + +/// Updates snapshot with collected manifest files +async fn update_snapshot_with_manifests( + stream_name: &str, + uploaded_files: &[UploadedParquetFile], + tenant_id: &Option, +) -> Result<(), ObjectStorageError> { + let manifest_files: Vec<_> = uploaded_files + .iter() + .map(|uploaded_file| uploaded_file.manifest_file.clone()) + .collect(); + + if !manifest_files.is_empty() { + catalog::update_snapshot(stream_name, manifest_files, tenant_id).await?; + } + Ok(()) +} + +async fn calculate_stats_for_uploaded_files( + uploaded_files: &[UploadedParquetFile], + stream_name: &str, + upload_context: &UploadContext, + tenant_id: &Option, +) { + if !PARSEABLE.options.collect_dataset_stats { + return; + } + + let mut join_set = JoinSet::new(); + let handle = FLUSH_AND_CONVERT_RUNTIME.handle(); + + for uploaded_file in uploaded_files { + let stream_name = stream_name.to_string(); + let path = uploaded_file.file_path.clone(); + let schema = upload_context.schema.clone(); + let tenant_id = tenant_id.clone(); + + join_set.spawn_on( + async move { + calculate_stats_if_enabled(&stream_name, &path, &schema, tenant_id).await; + }, + handle, + ); + } + + while let Some(result) = join_set.join_next().await { + if let Err(err) = result { + warn!("Field stats task join failed after parquet upload: {err}"); + } + } +} + +async fn cleanup_uploaded_staged_files(uploaded_files: Vec) { + let paths: Vec<_> = uploaded_files + .into_iter() + .map(|uploaded_file| uploaded_file.file_path) + .collect(); + { let mut guard = ACTIVE_OBJECT_STORE_SYNC_FILES.write().await; - for (path, _) in uploaded_files.iter() { + for path in paths.iter() { guard.remove(path); } @@ -1226,29 +1290,12 @@ async fn collect_upload_results( .is_ok_and(|ts| (now - ts).num_minutes() >= 5) }); } - let manifest_files: Vec<_> = uploaded_files - .into_par_iter() - .map(|(path, manifest_file)| { - if let Err(e) = remove_file(&path) { - warn!("Failed to remove staged file: {e}"); - } - manifest_file - }) - .collect(); - - Ok(manifest_files) -} -/// Updates snapshot with collected manifest files -async fn update_snapshot_with_manifests( - stream_name: &str, - manifest_files: Vec, - tenant_id: &Option, -) -> Result<(), ObjectStorageError> { - if !manifest_files.is_empty() { - catalog::update_snapshot(stream_name, manifest_files, tenant_id).await?; - } - Ok(()) + paths.into_par_iter().for_each(|path| { + if let Err(e) = remove_file(&path) { + warn!("Failed to remove staged file: {e}"); + } + }); } /// Processes schema files From f26f59483a51f50be49869e662efbb67113afbe6 Mon Sep 17 00:00:00 2001 From: Nikhil Sinha <131262146+nikhilsinhaparseable@users.noreply.github.com> Date: Mon, 15 Jun 2026 18:36:12 +0530 Subject: [PATCH 31/47] update Cargo.toml for release v2.9.1 (#1680) --- Cargo.lock | 2 +- Cargo.toml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7cb2eb42e..523ef5519 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4056,7 +4056,7 @@ dependencies = [ [[package]] name = "parseable" -version = "2.9.0" +version = "2.9.1" dependencies = [ "actix-cors", "actix-web", diff --git a/Cargo.toml b/Cargo.toml index 4c5cffe03..dafe5880f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "parseable" -version = "2.9.0" +version = "2.9.1" authors = ["Parseable Team "] edition = "2024" rust-version = "1.91.0" @@ -215,8 +215,8 @@ arrow = "58.0.0" temp-dir = "0.1.14" [package.metadata.parseable_ui] -assets-url = "https://parseable-prism-build.s3.us-east-2.amazonaws.com/v2.9.0/build.zip" -assets-sha1 = "3d54e032e1211f50b1770763f057a97354080954" +assets-url = "https://parseable-prism-build.s3.us-east-2.amazonaws.com/v2.9.1/build.zip" +assets-sha1 = "c87f5f5187e3d880bc311a8935fd844a5bbcc476" [features] debug = [] From 5d155ce32a0226b8115f956677b19bfc91965876 Mon Sep 17 00:00:00 2001 From: Nikhil Sinha <131262146+nikhilsinhaparseable@users.noreply.github.com> Date: Tue, 16 Jun 2026 11:21:12 +0530 Subject: [PATCH 32/47] update release wf to run linux builds on self hosted runner (#1681) --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4302fd80d..6b6ffd2b3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ on: jobs: build-linux: name: Build for ${{matrix.target}} - runs-on: ubuntu-22.04 # or ubuntu-24.04, ubuntu-20.04 + runs-on: self-hosted permissions: id-token: write contents: write @@ -172,7 +172,7 @@ jobs: build-kafka-linux: name: Build Kafka for x86_64-unknown-linux-gnu - runs-on: ubuntu-22.04 # or ubuntu-24.04 ubuntu-20.04 + runs-on: self-hosted permissions: id-token: write contents: write From 960915d8a181a26438f68cda1f8d4bf7471dbc9f Mon Sep 17 00:00:00 2001 From: YGN <149811989+ygndotgg@users.noreply.github.com> Date: Wed, 17 Jun 2026 10:59:47 +0530 Subject: [PATCH 33/47] track insertion time instead of data time for eviction (#1650) * track insertion time instead of data time for eviction * Use DashMap for concurrent cache access --- src/storage/object_storage.rs | 121 +++++++++++++++------------------- src/sync.rs | 9 ++- 2 files changed, 57 insertions(+), 73 deletions(-) diff --git a/src/storage/object_storage.rs b/src/storage/object_storage.rs index b9087c91c..35f1d7eba 100644 --- a/src/storage/object_storage.rs +++ b/src/storage/object_storage.rs @@ -16,10 +16,32 @@ * */ +use crate::catalog::{self, snapshot::Snapshot}; +use crate::event::format::LogSource; +use crate::event::format::LogSourceEntry; +use crate::handlers::DatasetTag; +use crate::handlers::http::fetch_schema; +use crate::handlers::http::modal::ingest_server::INGESTOR_EXPECT; +use crate::handlers::http::modal::ingest_server::INGESTOR_META; +use crate::handlers::http::users::{FILTER_DIR, USERS_ROOT_DIR}; +use crate::metrics::increment_parquets_stored_by_date; +use crate::metrics::increment_parquets_stored_size_by_date; +use crate::metrics::{EVENTS_STORAGE_SIZE_DATE, LIFETIME_EVENTS_STORAGE_SIZE, STORAGE_SIZE}; +use crate::option::Mode; +use crate::parseable::DEFAULT_TENANT; +use crate::parseable::{LogStream, PARSEABLE, Stream}; +use crate::stats::FullStats; +use crate::storage::SETTINGS_ROOT_DIRECTORY; +use crate::storage::TARGETS_ROOT_DIRECTORY; +use crate::storage::field_stats::DATASET_STATS_STREAM_NAME; +use crate::storage::field_stats::calculate_field_stats; +use crate::storage::field_stats::extract_datetime_from_parquet_path_regex; +use crate::sync::{ACTIVE_OBJECT_STORE_SYNC_FILES, FLUSH_AND_CONVERT_RUNTIME}; use arrow_schema::Schema; use async_trait::async_trait; use bytes::Bytes; use chrono::{DateTime, Utc}; +use dashmap::mapref::entry::Entry; use datafusion::{datasource::listing::ListingTableUrl, execution::runtime_env::RuntimeEnvBuilder}; use itertools::Itertools; use object_store::ListResult; @@ -43,29 +65,6 @@ use tokio::task::JoinSet; use tracing::{Instrument, error, info, info_span, warn}; use ulid::Ulid; -use crate::catalog::{self, snapshot::Snapshot}; -use crate::event::format::LogSource; -use crate::event::format::LogSourceEntry; -use crate::handlers::DatasetTag; -use crate::handlers::http::fetch_schema; -use crate::handlers::http::modal::ingest_server::INGESTOR_EXPECT; -use crate::handlers::http::modal::ingest_server::INGESTOR_META; -use crate::handlers::http::users::{FILTER_DIR, USERS_ROOT_DIR}; -use crate::metrics::increment_parquets_stored_by_date; -use crate::metrics::increment_parquets_stored_size_by_date; -use crate::metrics::{EVENTS_STORAGE_SIZE_DATE, LIFETIME_EVENTS_STORAGE_SIZE, STORAGE_SIZE}; -use crate::option::Mode; -use crate::parseable::DEFAULT_TENANT; -use crate::parseable::{LogStream, PARSEABLE, Stream}; -use crate::stats::FullStats; -use crate::storage::SETTINGS_ROOT_DIRECTORY; -use crate::storage::TARGETS_ROOT_DIRECTORY; -use crate::storage::field_stats::DATASET_STATS_STREAM_NAME; -use crate::storage::field_stats::calculate_field_stats; -use crate::storage::field_stats::extract_datetime_from_parquet_path_regex; -use crate::sync::ACTIVE_OBJECT_STORE_SYNC_FILES; -use crate::sync::FLUSH_AND_CONVERT_RUNTIME; - use super::{ ALERTS_ROOT_DIRECTORY, MANIFEST_FILE, ObjectStorageError, ObjectStoreFormat, PARSEABLE_METADATA_FILE_NAME, PARSEABLE_ROOT_DIRECTORY, SCHEMA_FILE_NAME, @@ -1084,22 +1083,20 @@ async fn process_parquet_files( let object_store = PARSEABLE.storage().get_object_store(); // collect all parquet files to upload - let parquet_paths = { - let mut guard = ACTIVE_OBJECT_STORE_SYNC_FILES.write().await; - - let parquet_paths: Vec = upload_context - .stream - .parquet_files() - .into_iter() - .filter(|p| !guard.contains(p)) - .collect(); - - let mut ret = Vec::with_capacity(parquet_paths.len()); - ret.clone_from(&parquet_paths); - guard.extend(parquet_paths); - tracing::info!(ACTIVE_OBJECT_STORE_SYNC_FILES=?ACTIVE_OBJECT_STORE_SYNC_FILES); - ret - }; + let parquet_paths: Vec = upload_context + .stream + .parquet_files() + .into_par_iter() + .filter_map( + |path| match ACTIVE_OBJECT_STORE_SYNC_FILES.entry(path.clone()) { + Entry::Vacant(entry) => { + entry.insert(Instant::now()); + Some(path) + } + Entry::Occupied(_) => None, + }, + ) + .collect(); let mut total_size: u64 = 0; let mut min_dt: Option> = None; @@ -1196,18 +1193,12 @@ async fn collect_upload_results( "Parquet file upload size validation failed for {:?}, preserving in staging for retry", upload_result.file_path ); - { - let mut guard = ACTIVE_OBJECT_STORE_SYNC_FILES.write().await; - guard.remove(&upload_result.file_path); - } + ACTIVE_OBJECT_STORE_SYNC_FILES.remove(&upload_result.file_path); } } Ok(Err((path, e))) => { error!("Error uploading parquet file: {e}"); - { - let mut guard = ACTIVE_OBJECT_STORE_SYNC_FILES.write().await; - guard.remove(&path); - } + ACTIVE_OBJECT_STORE_SYNC_FILES.remove(&path); return Err(e); } Err(e) => { @@ -1272,30 +1263,24 @@ async fn calculate_stats_for_uploaded_files( } async fn cleanup_uploaded_staged_files(uploaded_files: Vec) { - let paths: Vec<_> = uploaded_files - .into_iter() - .map(|uploaded_file| uploaded_file.file_path) - .collect(); - - { - let mut guard = ACTIVE_OBJECT_STORE_SYNC_FILES.write().await; - for path in paths.iter() { - guard.remove(path); - } - - // check if file has been in hashset for more than 5 minutes - let now = Utc::now(); - guard.retain(|f| { - !extract_datetime_from_parquet_path_regex(f) - .is_ok_and(|ts| (now - ts).num_minutes() >= 5) - }); + // successfully uploaded files, remove from DashMap + for uploaded_parquet_file in uploaded_files.iter() { + ACTIVE_OBJECT_STORE_SYNC_FILES.remove(&uploaded_parquet_file.file_path); } - paths.into_par_iter().for_each(|path| { - if let Err(e) = remove_file(&path) { - warn!("Failed to remove staged file: {e}"); - } + // Use monotonic time to ensure the 5-minute eviction window(cleanup) is immune to system clock adjustments. + let now = Instant::now(); + ACTIVE_OBJECT_STORE_SYNC_FILES.retain(|_, tracked_instant| { + now.duration_since(*tracked_instant) < Duration::from_secs(300) }); + + uploaded_files + .into_par_iter() + .for_each(|uploaded_parquet_file| { + if let Err(e) = remove_file(&uploaded_parquet_file.file_path) { + warn!("Failed to remove staged file: {e}"); + } + }); } /// Processes schema files diff --git a/src/sync.rs b/src/sync.rs index f6f20be48..b6ffe21db 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -17,17 +17,16 @@ */ use chrono::{TimeDelta, Timelike}; -use datafusion::common::HashSet; +use dashmap::DashMap; use futures::FutureExt; use once_cell::sync::Lazy; use std::collections::HashMap; use std::future::Future; use std::panic::AssertUnwindSafe; use std::path::PathBuf; -use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use tokio::runtime::Runtime; -use tokio::sync::{RwLock, mpsc, oneshot}; +use tokio::sync::{mpsc, oneshot}; use tokio::task::JoinSet; use tokio::time::{Duration, Instant, interval_at, sleep}; use tokio::{select, task}; @@ -39,8 +38,8 @@ pub static FLUSH_AND_CONVERT_RUNTIME: Lazy = static LOCAL_SYNC_RUNNING: AtomicBool = AtomicBool::new(false); static REMOTE_SYNC_RUNNING: AtomicBool = AtomicBool::new(false); -pub static ACTIVE_OBJECT_STORE_SYNC_FILES: Lazy>>> = - Lazy::new(|| Arc::new(RwLock::new(HashSet::new()))); +pub static ACTIVE_OBJECT_STORE_SYNC_FILES: Lazy> = + Lazy::new(DashMap::new); /// RAII guard that clears a sync-running flag on drop, so a panic inside the /// sync body cannot leave the flag stuck at `true` and wedge future ticks. struct SyncRunningGuard(&'static AtomicBool); From 9d0643c7b288c16140be45105963400ca9258505 Mon Sep 17 00:00:00 2001 From: parmesant Date: Wed, 17 Jun 2026 14:32:29 +0530 Subject: [PATCH 34/47] chore: Update deps (#1683) - Bump versions of multiple crates - Introduce new env var `P_SQL_TIMEOUT` to let users control timeout for SQL execution (defaults to 300s) --- Cargo.lock | 939 ++++++++++++++++++++++++++++++++--------------- Cargo.toml | 78 ++-- src/cli.rs | 10 + src/main.rs | 8 + src/query/mod.rs | 12 +- 5 files changed, 716 insertions(+), 331 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 523ef5519..3ca5af152 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -203,7 +203,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "smallvec", - "socket2 0.6.1", + "socket2 0.6.4", "time", "tracing", "url", @@ -448,15 +448,6 @@ dependencies = [ "object 0.32.2", ] -[[package]] -name = "arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" -dependencies = [ - "derive_arbitrary", -] - [[package]] name = "arc-swap" version = "1.8.0" @@ -496,19 +487,40 @@ version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d441fdda254b65f3e9025910eb2c2066b6295d9c8ed409522b8d2ace1ff8574c" dependencies = [ - "arrow-arith", - "arrow-array", - "arrow-buffer", - "arrow-cast", - "arrow-csv", - "arrow-data", - "arrow-ipc", - "arrow-json", - "arrow-ord", - "arrow-row", - "arrow-schema", - "arrow-select", - "arrow-string", + "arrow-arith 58.1.0", + "arrow-array 58.1.0", + "arrow-buffer 58.1.0", + "arrow-cast 58.1.0", + "arrow-csv 58.1.0", + "arrow-data 58.1.0", + "arrow-ipc 58.1.0", + "arrow-json 58.1.0", + "arrow-ord 58.1.0", + "arrow-row 58.1.0", + "arrow-schema 58.1.0", + "arrow-select 58.1.0", + "arrow-string 58.1.0", +] + +[[package]] +name = "arrow" +version = "59.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffaaa3e009861fd829d0a24dd6f115aa8e4634324bb092147d43baafe69ca4a7" +dependencies = [ + "arrow-arith 59.0.0", + "arrow-array 59.0.0", + "arrow-buffer 59.0.0", + "arrow-cast 59.0.0", + "arrow-csv 59.0.0", + "arrow-data 59.0.0", + "arrow-ipc 59.0.0", + "arrow-json 59.0.0", + "arrow-ord 59.0.0", + "arrow-row 59.0.0", + "arrow-schema 59.0.0", + "arrow-select 59.0.0", + "arrow-string 59.0.0", ] [[package]] @@ -517,10 +529,24 @@ version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced5406f8b720cc0bc3aa9cf5758f93e8593cda5490677aa194e4b4b383f9a59" dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-schema", + "arrow-array 58.1.0", + "arrow-buffer 58.1.0", + "arrow-data 58.1.0", + "arrow-schema 58.1.0", + "chrono", + "num-traits", +] + +[[package]] +name = "arrow-arith" +version = "59.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ac95125e1d71c4a252b5a9c729aef111e80418f08aaa6dbabd1ba66918247fc" +dependencies = [ + "arrow-array 59.0.0", + "arrow-buffer 59.0.0", + "arrow-data 59.0.0", + "arrow-schema 59.0.0", "chrono", "num-traits", ] @@ -532,9 +558,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "772bd34cacdda8baec9418d80d23d0fb4d50ef0735685bd45158b83dfeb6e62d" dependencies = [ "ahash", - "arrow-buffer", - "arrow-data", - "arrow-schema", + "arrow-buffer 58.1.0", + "arrow-data 58.1.0", + "arrow-schema 58.1.0", "chrono", "chrono-tz", "half", @@ -544,6 +570,24 @@ dependencies = [ "num-traits", ] +[[package]] +name = "arrow-array" +version = "59.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c60c79628e9a97cb90d7a0dc3e944f216a902f837d4ecabc14d524bddbbc137" +dependencies = [ + "ahash", + "arrow-buffer 59.0.0", + "arrow-data 59.0.0", + "arrow-schema 59.0.0", + "chrono", + "half", + "hashbrown 0.17.1", + "num-complex", + "num-integer", + "num-traits", +] + [[package]] name = "arrow-buffer" version = "58.1.0" @@ -556,18 +600,30 @@ dependencies = [ "num-traits", ] +[[package]] +name = "arrow-buffer" +version = "59.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6026f638c400e9878c1b1cc05c3cfd46fbf381285916ab408678701c1df46c1a" +dependencies = [ + "bytes", + "half", + "num-bigint", + "num-traits", +] + [[package]] name = "arrow-cast" version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0127816c96533d20fc938729f48c52d3e48f99717e7a0b5ade77d742510736d" dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-ord", - "arrow-schema", - "arrow-select", + "arrow-array 58.1.0", + "arrow-buffer 58.1.0", + "arrow-data 58.1.0", + "arrow-ord 58.1.0", + "arrow-schema 58.1.0", + "arrow-select 58.1.0", "atoi", "base64", "chrono", @@ -578,15 +634,51 @@ dependencies = [ "ryu", ] +[[package]] +name = "arrow-cast" +version = "59.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c82c236c3caf8df5664284f3f1fbe89938852163998c3fdbf37e84ac220445e9" +dependencies = [ + "arrow-array 59.0.0", + "arrow-buffer 59.0.0", + "arrow-data 59.0.0", + "arrow-ord 59.0.0", + "arrow-schema 59.0.0", + "arrow-select 59.0.0", + "atoi", + "base64", + "chrono", + "half", + "lexical-core", + "num-traits", + "ryu", +] + [[package]] name = "arrow-csv" version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca025bd0f38eeecb57c2153c0123b960494138e6a957bbda10da2b25415209fe" dependencies = [ - "arrow-array", - "arrow-cast", - "arrow-schema", + "arrow-array 58.1.0", + "arrow-cast 58.1.0", + "arrow-schema 58.1.0", + "chrono", + "csv", + "csv-core", + "regex", +] + +[[package]] +name = "arrow-csv" +version = "59.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12714e5fb7954159af1e26d4e0d37108bcf1a2ad5ee5c5bf02a944d564d588b7" +dependencies = [ + "arrow-array 59.0.0", + "arrow-cast 59.0.0", + "arrow-schema 59.0.0", "chrono", "csv", "csv-core", @@ -599,8 +691,21 @@ version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42d10beeab2b1c3bb0b53a00f7c944a178b622173a5c7bcabc3cb45d90238df4" dependencies = [ - "arrow-buffer", - "arrow-schema", + "arrow-buffer 58.1.0", + "arrow-schema 58.1.0", + "half", + "num-integer", + "num-traits", +] + +[[package]] +name = "arrow-data" +version = "59.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd568aa70c4ec5947027b0d5caee94877433b661a0bb9e8ddceeeb5f0c9b1ab" +dependencies = [ + "arrow-buffer 59.0.0", + "arrow-schema 59.0.0", "half", "num-integer", "num-traits", @@ -612,11 +717,11 @@ version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "302b2e036335f3f04d65dad3f74ff1f2aae6dc671d6aa04dc6b61193761e16fb" dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-cast", - "arrow-ipc", - "arrow-schema", + "arrow-array 58.1.0", + "arrow-buffer 58.1.0", + "arrow-cast 58.1.0", + "arrow-ipc 58.1.0", + "arrow-schema 58.1.0", "base64", "bytes", "futures", @@ -632,27 +737,66 @@ version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "609a441080e338147a84e8e6904b6da482cefb957c5cdc0f3398872f69a315d0" dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-schema", - "arrow-select", + "arrow-array 58.1.0", + "arrow-buffer 58.1.0", + "arrow-data 58.1.0", + "arrow-schema 58.1.0", + "arrow-select 58.1.0", "flatbuffers", "lz4_flex", "zstd", ] +[[package]] +name = "arrow-ipc" +version = "59.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57ee4d470eab1a021bc4b63fa2b2c15d572892bf227b0a982d3b755a6c662b5" +dependencies = [ + "arrow-array 59.0.0", + "arrow-buffer 59.0.0", + "arrow-data 59.0.0", + "arrow-schema 59.0.0", + "arrow-select 59.0.0", + "flatbuffers", +] + [[package]] name = "arrow-json" version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ead0914e4861a531be48fe05858265cf854a4880b9ed12618b1d08cba9bebc8" dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-cast", - "arrow-data", - "arrow-schema", + "arrow-array 58.1.0", + "arrow-buffer 58.1.0", + "arrow-cast 58.1.0", + "arrow-data 58.1.0", + "arrow-schema 58.1.0", + "chrono", + "half", + "indexmap", + "itoa", + "lexical-core", + "memchr", + "num-traits", + "ryu", + "serde_core", + "serde_json", + "simdutf8", +] + +[[package]] +name = "arrow-json" +version = "59.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f47e0e7a284e1f3707a780dc8cd5451b1614e9e398ea2d9ca03c7a2fe9a9ed" +dependencies = [ + "arrow-array 59.0.0", + "arrow-buffer 59.0.0", + "arrow-cast 59.0.0", + "arrow-ord 59.0.0", + "arrow-schema 59.0.0", + "arrow-select 59.0.0", "chrono", "half", "indexmap", @@ -672,11 +816,24 @@ version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "763a7ba279b20b52dad300e68cfc37c17efa65e68623169076855b3a9e941ca5" dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-schema", - "arrow-select", + "arrow-array 58.1.0", + "arrow-buffer 58.1.0", + "arrow-data 58.1.0", + "arrow-schema 58.1.0", + "arrow-select 58.1.0", +] + +[[package]] +name = "arrow-ord" +version = "59.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a79cf73ad2eba8686ec2aa9bbf8671208e509025f166afc040cedbd94ffe4983" +dependencies = [ + "arrow-array 59.0.0", + "arrow-buffer 59.0.0", + "arrow-data 59.0.0", + "arrow-schema 59.0.0", + "arrow-select 59.0.0", ] [[package]] @@ -685,10 +842,23 @@ version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e14fe367802f16d7668163ff647830258e6e0aeea9a4d79aaedf273af3bdcd3e" dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-schema", + "arrow-array 58.1.0", + "arrow-buffer 58.1.0", + "arrow-data 58.1.0", + "arrow-schema 58.1.0", + "half", +] + +[[package]] +name = "arrow-row" +version = "59.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea0f7d8ed6182f14952761e2c0f989852d5aa334fcbc49f73a9f2247c25b879" +dependencies = [ + "arrow-array 59.0.0", + "arrow-buffer 59.0.0", + "arrow-data 59.0.0", + "arrow-schema 59.0.0", "half", ] @@ -703,6 +873,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "arrow-schema" +version = "59.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80b3e786a0dd9103acd583a6fb486dbf2f3268466cc0bd571dcf34cef231c1f1" + [[package]] name = "arrow-select" version = "58.1.0" @@ -710,10 +886,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78694888660a9e8ac949853db393af2a8b8fc82c19ce333132dfa2e72cc1a7fe" dependencies = [ "ahash", - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-schema", + "arrow-array 58.1.0", + "arrow-buffer 58.1.0", + "arrow-data 58.1.0", + "arrow-schema 58.1.0", + "num-traits", +] + +[[package]] +name = "arrow-select" +version = "59.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "067a67e0361f6c31f4a7248759f36ca4ca71b187a941ed4d49da1c7d3d4db624" +dependencies = [ + "ahash", + "arrow-array 59.0.0", + "arrow-buffer 59.0.0", + "arrow-data 59.0.0", + "arrow-schema 59.0.0", "num-traits", ] @@ -723,11 +913,28 @@ version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61e04a01f8bb73ce54437514c5fd3ee2aa3e8abe4c777ee5cc55853b1652f79e" dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-schema", - "arrow-select", + "arrow-array 58.1.0", + "arrow-buffer 58.1.0", + "arrow-data 58.1.0", + "arrow-schema 58.1.0", + "arrow-select 58.1.0", + "memchr", + "num-traits", + "regex", + "regex-syntax", +] + +[[package]] +name = "arrow-string" +version = "59.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99bc95847f3ff62a2b03d6f8ce2e3e78f01362060549a2a311898dd442f6256d" +dependencies = [ + "arrow-array 59.0.0", + "arrow-buffer 59.0.0", + "arrow-data 59.0.0", + "arrow-schema 59.0.0", + "arrow-select 59.0.0", "memchr", "num-traits", "regex", @@ -861,7 +1068,7 @@ dependencies = [ "miniz_oxide", "object 0.37.3", "rustc-demangle", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -1025,18 +1232,19 @@ dependencies = [ [[package]] name = "cargo-platform" -version = "0.1.9" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +checksum = "dd0061da739915fae12ea00e16397555ed4371a6bb285431aab930f61b0aa4ba" dependencies = [ "serde", + "serde_core", ] [[package]] name = "cargo_metadata" -version = "0.19.2" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9" dependencies = [ "camino", "cargo-platform", @@ -1048,9 +1256,9 @@ dependencies = [ [[package]] name = "cargo_toml" -version = "0.21.0" +version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fbd1fe9db3ebf71b89060adaf7b0504c2d6a425cf061313099547e382c2e472" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" dependencies = [ "serde", "toml", @@ -1112,7 +1320,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -1403,15 +1611,17 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crossterm" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ "bitflags 2.10.0", "crossterm_winapi", + "derive_more 2.1.1", + "document-features", "mio", "parking_lot", - "rustix 0.38.44", + "rustix 1.1.3", "signal-hook", "signal-hook-mio", "winapi", @@ -1509,9 +1719,9 @@ dependencies = [ [[package]] name = "dashmap" -version = "6.1.0" +version = "6.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" dependencies = [ "cfg-if", "crossbeam-utils", @@ -1533,8 +1743,8 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93db0e623840612f7f2cd757f7e8a8922064192363732c88692e0870016e141b" dependencies = [ - "arrow", - "arrow-schema", + "arrow 58.1.0", + "arrow-schema 58.1.0", "async-trait", "bytes", "bzip2", @@ -1588,7 +1798,7 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37cefde60b26a7f4ff61e9d2ff2833322f91df2b568d7238afe67bde5bdffb66" dependencies = [ - "arrow", + "arrow 58.1.0", "async-trait", "dashmap", "datafusion-common", @@ -1613,7 +1823,7 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17e112307715d6a7a331111a4c2330ff54bc237183511c319e3708a4cff431fb" dependencies = [ - "arrow", + "arrow 58.1.0", "async-trait", "datafusion-catalog", "datafusion-common", @@ -1637,8 +1847,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d72a11ca44a95e1081870d3abb80c717496e8a7acb467a1d3e932bb636af5cc2" dependencies = [ "ahash", - "arrow", - "arrow-ipc", + "arrow 58.1.0", + "arrow-ipc 58.1.0", "chrono", "half", "hashbrown 0.16.1", @@ -1672,7 +1882,7 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9fb386e1691355355a96419978a0022b7947b44d4a24a6ea99f00b6b485cbb6" dependencies = [ - "arrow", + "arrow 58.1.0", "async-compression", "async-trait", "bytes", @@ -1707,8 +1917,8 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffa6c52cfed0734c5f93754d1c0175f558175248bf686c944fb05c373e5fc096" dependencies = [ - "arrow", - "arrow-ipc", + "arrow 58.1.0", + "arrow-ipc 58.1.0", "async-trait", "bytes", "datafusion-common", @@ -1731,7 +1941,7 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "503f29e0582c1fc189578d665ff57d9300da1f80c282777d7eb67bb79fb8cdca" dependencies = [ - "arrow", + "arrow 58.1.0", "async-trait", "bytes", "datafusion-common", @@ -1754,7 +1964,7 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e33804749abc8d0c8cb7473228483cb8070e524c6f6086ee1b85a64debe2b3d2" dependencies = [ - "arrow", + "arrow 58.1.0", "async-trait", "bytes", "datafusion-common", @@ -1778,7 +1988,7 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a8e0365e0e08e8ff94d912f0ababcf9065a1a304018ba90b1fc83c855b4997" dependencies = [ - "arrow", + "arrow 58.1.0", "async-trait", "bytes", "datafusion-common", @@ -1814,8 +2024,8 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c03c7fbdaefcca4ef6ffe425a5fc2325763bfb426599bb0bf4536466efabe709" dependencies = [ - "arrow", - "arrow-buffer", + "arrow 58.1.0", + "arrow-buffer 58.1.0", "async-trait", "chrono", "dashmap", @@ -1837,7 +2047,7 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "574b9b6977fedbd2a611cbff12e5caf90f31640ad9dc5870f152836d94bad0dd" dependencies = [ - "arrow", + "arrow 58.1.0", "async-trait", "chrono", "datafusion-common", @@ -1860,7 +2070,7 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d7c3adf3db8bf61e92eb90cb659c8e8b734593a8f7c8e12a843c7ddba24b87e" dependencies = [ - "arrow", + "arrow 58.1.0", "datafusion-common", "indexmap", "itertools 0.14.0", @@ -1873,8 +2083,8 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f28aa4e10384e782774b10e72aca4d93ef7b31aa653095d9d4536b0a3dbc51b6" dependencies = [ - "arrow", - "arrow-buffer", + "arrow 58.1.0", + "arrow-buffer 58.1.0", "base64", "blake2", "blake3", @@ -1906,7 +2116,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00aa6217e56098ba84e0a338176fe52f0a84cca398021512c6c8c5eff806d0ad" dependencies = [ "ahash", - "arrow", + "arrow 58.1.0", "datafusion-common", "datafusion-doc", "datafusion-execution", @@ -1928,7 +2138,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b511250349407db7c43832ab2de63f5557b19a20dfd236b39ca2c04468b50d47" dependencies = [ "ahash", - "arrow", + "arrow 58.1.0", "datafusion-common", "datafusion-expr-common", "datafusion-physical-expr-common", @@ -1940,8 +2150,8 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef13a858e20d50f0a9bb5e96e7ac82b4e7597f247515bccca4fdd2992df0212a" dependencies = [ - "arrow", - "arrow-ord", + "arrow 58.1.0", + "arrow-ord 58.1.0", "datafusion-common", "datafusion-doc", "datafusion-execution", @@ -1965,7 +2175,7 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b40d3f5bbb3905f9ccb1ce9485a9595c77b69758a7c24d3ba79e334ff51e7e" dependencies = [ - "arrow", + "arrow 58.1.0", "async-trait", "datafusion-catalog", "datafusion-common", @@ -1981,7 +2191,7 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e88ec9d57c9b685d02f58bfee7be62d72610430ddcedb82a08e5d9925dbfb6" dependencies = [ - "arrow", + "arrow 58.1.0", "datafusion-common", "datafusion-doc", "datafusion-expr", @@ -2020,7 +2230,7 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e929015451a67f77d9d8b727b2bf3a40c4445fdef6cdc53281d7d97c76888ace" dependencies = [ - "arrow", + "arrow 58.1.0", "chrono", "datafusion-common", "datafusion-expr", @@ -2041,7 +2251,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b1e68aba7a4b350401cfdf25a3d6f989ad898a7410164afe9ca52080244cb59" dependencies = [ "ahash", - "arrow", + "arrow 58.1.0", "datafusion-common", "datafusion-expr", "datafusion-expr-common", @@ -2064,7 +2274,7 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea22315f33cf2e0adc104e8ec42e285f6ed93998d565c65e82fec6a9ee9f9db4" dependencies = [ - "arrow", + "arrow 58.1.0", "datafusion-common", "datafusion-expr", "datafusion-functions", @@ -2080,7 +2290,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b04b45ea8ad3ac2d78f2ea2a76053e06591c9629c7a603eda16c10649ecf4362" dependencies = [ "ahash", - "arrow", + "arrow 58.1.0", "chrono", "datafusion-common", "datafusion-expr-common", @@ -2096,7 +2306,7 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cb13397809a425918f608dfe8653f332015a3e330004ab191b4404187238b95" dependencies = [ - "arrow", + "arrow 58.1.0", "datafusion-common", "datafusion-execution", "datafusion-expr", @@ -2116,9 +2326,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5edc023675791af9d5fb4cc4c24abf5f7bd3bd4dcf9e5bd90ea1eff6976dcc79" dependencies = [ "ahash", - "arrow", - "arrow-ord", - "arrow-schema", + "arrow 58.1.0", + "arrow-ord 58.1.0", + "arrow-schema 58.1.0", "async-trait", "datafusion-common", "datafusion-common-runtime", @@ -2186,7 +2396,7 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac8c76860e355616555081cab5968cec1af7a80701ff374510860bcd567e365a" dependencies = [ - "arrow", + "arrow 58.1.0", "datafusion-common", "datafusion-datasource", "datafusion-expr-common", @@ -2217,7 +2427,7 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa0d133ddf8b9b3b872acac900157f783e7b879fe9a6bccf389abebbfac45ec1" dependencies = [ - "arrow", + "arrow 58.1.0", "bigdecimal", "chrono", "datafusion-common", @@ -2239,17 +2449,6 @@ dependencies = [ "powerfmt", ] -[[package]] -name = "derive_arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "derive_builder" version = "0.20.2" @@ -2393,6 +2592,15 @@ dependencies = [ "syn", ] +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "duct" version = "0.13.7" @@ -2540,9 +2748,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -2555,9 +2763,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -2565,15 +2773,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -2582,9 +2790,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-lite" @@ -2603,9 +2811,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -2614,15 +2822,15 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-timer" @@ -2632,9 +2840,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -2644,7 +2852,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -2787,6 +2994,12 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + [[package]] name = "hdrhistogram" version = "7.5.4" @@ -2829,7 +3042,7 @@ checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd" dependencies = [ "cfg-if", "libc", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -3053,7 +3266,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.1", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -3211,12 +3424,12 @@ checksum = "35a84fd5aa25fae5c0f4a33d9cac2ca017fc622cbd089be2229993514990f870" [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -3287,6 +3500,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.17" @@ -3465,6 +3687,12 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "local-channel" version = "0.1.5" @@ -3580,9 +3808,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.1" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "log", @@ -3708,6 +3936,16 @@ dependencies = [ "bitflags 2.10.0", ] +[[package]] +name = "objc2-io-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +dependencies = [ + "libc", + "objc2-core-foundation", +] + [[package]] name = "object" version = "0.32.2" @@ -3777,9 +4015,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -3851,8 +4089,9 @@ dependencies = [ [[package]] name = "opentelemetry" -version = "0.31.0" -source = "git+https://github.com/open-telemetry/opentelemetry-rust/?rev=b096b70b2ffe9beb65a716cf47d5e5db80a9e930#b096b70b2ffe9beb65a716cf47d5e5db80a9e930" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0142c63252a9e054e68a4c61a5778f7b14f576274d593f8ce883d191a099682" dependencies = [ "futures-core", "futures-sink", @@ -3871,7 +4110,7 @@ dependencies = [ "async-trait", "bytes", "http 1.4.0", - "opentelemetry 0.31.0 (registry+https://github.com/rust-lang/crates.io-index)", + "opentelemetry 0.31.0", "reqwest", ] @@ -3882,10 +4121,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f69cd6acbb9af919df949cd1ec9e5e7fdc2ef15d234b6b795aaa525cc02f71f" dependencies = [ "http 1.4.0", - "opentelemetry 0.31.0 (registry+https://github.com/rust-lang/crates.io-index)", + "opentelemetry 0.31.0", "opentelemetry-http", - "opentelemetry-proto 0.31.0 (registry+https://github.com/rust-lang/crates.io-index)", - "opentelemetry_sdk 0.31.0 (registry+https://github.com/rust-lang/crates.io-index)", + "opentelemetry-proto 0.31.0", + "opentelemetry_sdk 0.31.0", "prost 0.14.1", "reqwest", "serde_json", @@ -3903,8 +4142,8 @@ checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f" dependencies = [ "base64", "const-hex", - "opentelemetry 0.31.0 (registry+https://github.com/rust-lang/crates.io-index)", - "opentelemetry_sdk 0.31.0 (registry+https://github.com/rust-lang/crates.io-index)", + "opentelemetry 0.31.0", + "opentelemetry_sdk 0.31.0", "prost 0.14.1", "serde", "serde_json", @@ -3914,13 +4153,14 @@ dependencies = [ [[package]] name = "opentelemetry-proto" -version = "0.31.0" -source = "git+https://github.com/open-telemetry/opentelemetry-rust/?rev=b096b70b2ffe9beb65a716cf47d5e5db80a9e930#b096b70b2ffe9beb65a716cf47d5e5db80a9e930" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d658ba1faf63f7b9c492cfbe6e0ec365440a16132d3270c1065f7b33f1b638" dependencies = [ "base64", "const-hex", - "opentelemetry 0.31.0 (git+https://github.com/open-telemetry/opentelemetry-rust/?rev=b096b70b2ffe9beb65a716cf47d5e5db80a9e930)", - "opentelemetry_sdk 0.31.0 (git+https://github.com/open-telemetry/opentelemetry-rust/?rev=b096b70b2ffe9beb65a716cf47d5e5db80a9e930)", + "opentelemetry 0.32.0", + "opentelemetry_sdk 0.32.1", "prost 0.14.1", "serde", "tonic", @@ -3934,8 +4174,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc8887887e169414f637b18751487cce4e095be787d23fad13c454e2fb1b3811" dependencies = [ "chrono", - "opentelemetry 0.31.0 (registry+https://github.com/rust-lang/crates.io-index)", - "opentelemetry_sdk 0.31.0 (registry+https://github.com/rust-lang/crates.io-index)", + "opentelemetry 0.31.0", + "opentelemetry_sdk 0.31.0", ] [[package]] @@ -3947,7 +4187,7 @@ dependencies = [ "futures-channel", "futures-executor", "futures-util", - "opentelemetry 0.31.0 (registry+https://github.com/rust-lang/crates.io-index)", + "opentelemetry 0.31.0", "percent-encoding", "rand 0.9.4", "thiserror 2.0.17", @@ -3957,13 +4197,14 @@ dependencies = [ [[package]] name = "opentelemetry_sdk" -version = "0.31.0" -source = "git+https://github.com/open-telemetry/opentelemetry-rust/?rev=b096b70b2ffe9beb65a716cf47d5e5db80a9e930#b096b70b2ffe9beb65a716cf47d5e5db80a9e930" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b59f80e1ac4d5ff7a2db8fb6c80badb7f0f3f858211fba08dd9aaec750894f9" dependencies = [ "futures-channel", "futures-executor", "futures-util", - "opentelemetry 0.31.0 (git+https://github.com/open-telemetry/opentelemetry-rust/?rev=b096b70b2ffe9beb65a716cf47d5e5db80a9e930)", + "opentelemetry 0.32.0", "percent-encoding", "portable-atomic", "rand 0.9.4", @@ -4015,7 +4256,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -4025,12 +4266,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d3f9f2205199603564127932b89695f52b62322f541d0fc7179d57c2e1c9877" dependencies = [ "ahash", - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-ipc", - "arrow-schema", - "arrow-select", + "arrow-array 58.1.0", + "arrow-buffer 58.1.0", + "arrow-data 58.1.0", + "arrow-ipc 58.1.0", + "arrow-schema 58.1.0", + "arrow-select 58.1.0", "base64", "brotli", "bytes", @@ -4066,13 +4307,14 @@ dependencies = [ "actix-web-static-files", "anyhow", "argon2", - "arrow", - "arrow-array", + "arrow 58.1.0", + "arrow 59.0.0", + "arrow-array 58.1.0", "arrow-flight", - "arrow-ipc", - "arrow-json", - "arrow-schema", - "arrow-select", + "arrow-ipc 58.1.0", + "arrow-json 58.1.0", + "arrow-schema 58.1.0", + "arrow-select 58.1.0", "async-trait", "base64", "byteorder", @@ -4102,17 +4344,17 @@ dependencies = [ "humantime", "humantime-serde", "indexmap", - "itertools 0.14.0", + "itertools 0.15.0", "lazy_static", "num_cpus", "object_store", "once_cell", "openid", - "opentelemetry 0.31.0 (registry+https://github.com/rust-lang/crates.io-index)", + "opentelemetry 0.31.0", "opentelemetry-otlp", - "opentelemetry-proto 0.31.0 (git+https://github.com/open-telemetry/opentelemetry-rust/?rev=b096b70b2ffe9beb65a716cf47d5e5db80a9e930)", + "opentelemetry-proto 0.32.0", "opentelemetry-stdout", - "opentelemetry_sdk 0.31.0 (registry+https://github.com/rust-lang/crates.io-index)", + "opentelemetry_sdk 0.31.0", "parking_lot", "parquet", "path-clean", @@ -4123,7 +4365,7 @@ dependencies = [ "rayon", "rdkafka", "regex", - "relative-path", + "relative-path 2.0.1", "reqwest", "rstest", "rustc-hash", @@ -4137,10 +4379,12 @@ dependencies = [ "sha1_smol", "sha2", "static-files 0.2.5", - "sysinfo 0.33.1", + "sysinfo 0.34.2", "temp-dir", "tempfile", "thiserror 2.0.17", + "tikv-jemalloc-ctl", + "tikv-jemallocator", "tokio", "tokio-stream", "tokio-util", @@ -4339,7 +4583,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit 0.23.10+spec-1.0.0", + "toml_edit", ] [[package]] @@ -4558,7 +4802,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.1", + "socket2 0.5.10", "thiserror 2.0.17", "tokio", "tracing", @@ -4595,7 +4839,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.1", + "socket2 0.5.10", "tracing", "windows-sys 0.60.2", ] @@ -4726,9 +4970,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -4858,6 +5102,12 @@ name = "relative-path" version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + +[[package]] +name = "relative-path" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca40a312222d8ba74837cb474edef44b37f561da5f773981007a10bbaa992b0" dependencies = [ "serde", ] @@ -4922,21 +5172,20 @@ dependencies = [ [[package]] name = "rstest" -version = "0.23.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2c585be59b6b5dd66a9d2084aa1d8bd52fbdb806eafdeffb52791147862035" +checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49" dependencies = [ - "futures", "futures-timer", + "futures-util", "rstest_macros", - "rustc_version", ] [[package]] name = "rstest_macros" -version = "0.23.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "825ea780781b15345a146be27eaefb05085e337e869bff01b4306a4fd4a9ad5a" +checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" dependencies = [ "cfg-if", "glob", @@ -4944,7 +5193,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "relative-path", + "relative-path 1.9.3", "rustc_version", "syn", "unicode-ident", @@ -4958,9 +5207,9 @@ checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustc_version" @@ -4999,9 +5248,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "aws-lc-rs", "log", @@ -5188,9 +5437,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.148" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "indexmap", "itoa", @@ -5224,11 +5473,11 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.9" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -5387,12 +5636,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -5503,29 +5752,30 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.33.1" +version = "0.34.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01" +checksum = "a4b93974b3d3aeaa036504b8eefd4c039dced109171c1ae973f1dc63b2c7e4b2" dependencies = [ - "core-foundation-sys", "libc", "memchr", "ntapi", + "objc2-core-foundation", "rayon", - "windows", + "windows 0.57.0", ] [[package]] name = "sysinfo" -version = "0.34.2" +version = "0.37.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4b93974b3d3aeaa036504b8eefd4c039dced109171c1ae973f1dc63b2c7e4b2" +checksum = "16607d5caffd1c07ce073528f9ed972d88db15dd44023fa57142963be3feb11f" dependencies = [ "libc", "memchr", "ntapi", "objc2-core-foundation", - "windows", + "objc2-io-kit", + "windows 0.61.3", ] [[package]] @@ -5618,6 +5868,37 @@ dependencies = [ "ordered-float", ] +[[package]] +name = "tikv-jemalloc-ctl" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a184c43b8ab2f41df2733b55556e3f5f632f4aeaa205b1bb018f574b7f5f142" +dependencies = [ + "libc", + "paste", + "tikv-jemalloc-sys", +] + +[[package]] +name = "tikv-jemalloc-sys" +version = "0.7.1+5.3.1-0-g81034ce1f1373e37dc865038e1bc8eeecf559ce8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a2825c78386b4ae0314074867860ba9577875de945f05992c38815cbec327f0" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "tikv-jemallocator" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "249f09e49ab1609436f34c776e84231bead18d6a955f119f939bdc1d847561bd" +dependencies = [ + "libc", + "tikv-jemalloc-sys", +] + [[package]] name = "time" version = "0.3.47" @@ -5699,9 +5980,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.48.0" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -5709,16 +5990,16 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.1", + "socket2 0.6.4", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -5737,9 +6018,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -5749,9 +6030,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -5762,23 +6043,17 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.23" +version = "0.9.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" dependencies = [ - "serde", + "indexmap", + "serde_core", "serde_spanned", - "toml_datetime 0.6.11", - "toml_edit 0.22.27", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", ] [[package]] @@ -5790,20 +6065,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime 0.6.11", - "toml_write", - "winnow", -] - [[package]] name = "toml_edit" version = "0.23.10+spec-1.0.0" @@ -5811,7 +6072,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ "indexmap", - "toml_datetime 0.7.5+spec-1.1.0", + "toml_datetime", "toml_parser", "winnow", ] @@ -5826,10 +6087,10 @@ dependencies = [ ] [[package]] -name = "toml_write" -version = "0.1.2" +name = "toml_writer" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "tonic" @@ -5852,7 +6113,7 @@ dependencies = [ "percent-encoding", "pin-project", "rustls-native-certs", - "socket2 0.6.1", + "socket2 0.6.4", "sync_wrapper", "tokio", "tokio-rustls", @@ -6011,7 +6272,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ac28f2d093c6c477eaa76b23525478f38de514fa9aeb1285738d4b97a9552fc" dependencies = [ "js-sys", - "opentelemetry 0.31.0 (registry+https://github.com/rust-lang/crates.io-index)", + "opentelemetry 0.31.0", "smallvec", "tracing", "tracing-core", @@ -6051,6 +6312,12 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" +[[package]] +name = "typed-path" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" + [[package]] name = "typenum" version = "1.20.0" @@ -6124,7 +6391,7 @@ checksum = "9e64b558561f12a171bbea5325c3f24f129db371adee1d7ae93b6e310bd69192" dependencies = [ "libc", "thiserror 1.0.69", - "windows", + "windows 0.57.0", ] [[package]] @@ -6169,9 +6436,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.1" +version = "1.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -6222,9 +6489,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vergen" -version = "9.0.6" +version = "9.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b2bf58be11fc9414104c6d3a2e464163db5ef74b12296bda593cac37b6e4777" +checksum = "b849a1f6d8639e8de261e81ee0fc881e3e3620db1af9f2e0da015d4382ceaf75" dependencies = [ "anyhow", "cargo_metadata", @@ -6232,16 +6499,16 @@ dependencies = [ "regex", "rustc_version", "rustversion", - "sysinfo 0.34.2", + "sysinfo 0.37.2", "time", "vergen-lib", ] [[package]] name = "vergen-gitcl" -version = "1.0.8" +version = "9.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9dfc1de6eb2e08a4ddf152f1b179529638bedc0ea95e6d667c014506377aefe" +checksum = "77ff3b5300a085d6bcd8fc96a507f706a28ae3814693236c9b409db71a1d15b9" dependencies = [ "anyhow", "derive_builder", @@ -6253,9 +6520,9 @@ dependencies = [ [[package]] name = "vergen-lib" -version = "0.1.6" +version = "9.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b07e6010c0f3e59fcb164e0163834597da68d1f864e2b8ca49f74de01e9c166" +checksum = "b34a29ba7e9c59e62f229ae1932fb1b8fb8a6fdcc99215a641913f5f5a59a569" dependencies = [ "anyhow", "derive_builder", @@ -6507,6 +6774,28 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + [[package]] name = "windows-core" version = "0.57.0" @@ -6519,6 +6808,19 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -6527,9 +6829,20 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement 0.60.2", "windows-interface 0.59.3", - "windows-link", + "windows-link 0.2.1", "windows-result 0.4.1", - "windows-strings", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", ] [[package]] @@ -6576,12 +6889,28 @@ dependencies = [ "syn", ] +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + [[package]] name = "windows-result" version = "0.1.2" @@ -6591,13 +6920,31 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + [[package]] name = "windows-result" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", ] [[package]] @@ -6606,7 +6953,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -6642,7 +6989,7 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -6667,7 +7014,7 @@ version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link", + "windows-link 0.2.1", "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", @@ -6678,6 +7025,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -6994,18 +7350,15 @@ dependencies = [ [[package]] name = "zip" -version = "2.4.2" +version = "8.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" dependencies = [ - "arbitrary", "crc32fast", - "crossbeam-utils", - "displaydoc", "flate2", "indexmap", "memchr", - "thiserror 2.0.17", + "typed-path", "zopfli", ] diff --git a/Cargo.toml b/Cargo.toml index dafe5880f..b28e30c66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,7 +64,7 @@ base64 = "0.22.0" cookie = "0.18.1" hex = "0.4" openid = { version = "0.18.3", default-features = false, features = ["rustls"] } -rustls = { version = "0.23", default-features = false, features = [ +rustls = { version = "0.23.40", default-features = false, features = [ "ring", "std", ] } @@ -72,44 +72,35 @@ rustls-pemfile = "2.1.2" sha2 = "0.10.8" # Serialization and Data Formats -byteorder = "1.4.3" +byteorder = "1.5.0" erased-serde = "=0.3.16" -serde = { version = "1.0", features = ["rc", "derive"] } -serde_json = "1.0" -serde_repr = "0.1.17" +serde = { version = "1.0.228", features = ["rc", "derive"] } +serde_json = "1.0.150" +serde_repr = "0.1.20" # Async and Runtime -async-trait = "0.1" -futures = "0.3" -futures-util = "0.3" -tokio = { version = "^1.43", default-features = false, features = [ +async-trait = "0.1.89" +futures = "0.3.32" +futures-util = "0.3.32" +tokio = { version = "^1.52.3", default-features = false, features = [ "sync", "macros", "fs", "rt-multi-thread", ] } -tokio-stream = { version = "0.1.17", features = ["fs"] } -tokio-util = { version = "0.7" } +tokio-stream = { version = "0.1.18", features = ["fs"] } +tokio-util = { version = "0.7.18" } # perf hotpath = { version = "0.16.0", optional = true, features = [ "hotpath", "hotpath-cpu", "hotpath-alloc", - "tokio" + "tokio", ] } # # Logging and Metrics -# opentelemetry-proto = { version = "0.30.0", features = [ -# "gen-tonic", -# "with-serde", -# "logs", -# "metrics", -# "trace", -# ] } - -# add custom branch with fix until it gets merged -opentelemetry-proto = { git = "https://github.com/open-telemetry/opentelemetry-rust/", rev = "b096b70b2ffe9beb65a716cf47d5e5db80a9e930", features = [ +opentelemetry-proto = { version = "0.32.0", features = [ "gen-tonic", "with-serde", "logs", @@ -148,7 +139,7 @@ humantime-serde = "1.1" # File System and I/O fs_extra = "1.3" path-clean = "1.0.1" -relative-path = { version = "1.7", features = ["serde"] } +relative-path = { version = "2.0.1", features = ["serde"] } # CLI and System clap = { version = "4.5", default-features = false, features = [ @@ -160,11 +151,11 @@ clap = { version = "4.5", default-features = false, features = [ "cargo", "error-context", ] } -crossterm = "0.28.1" +crossterm = "0.29.0" hostname = "0.4.0" human-size = "0.4" num_cpus = "1.15" -sysinfo = "0.33.1" +sysinfo = { version = "0.34.2", features = ["multithread"] } uptime_lib = "0.3.0" # Utility Libraries @@ -172,9 +163,9 @@ anyhow = { version = "1.0", features = ["backtrace"] } bytes = "1.11" clokwerk = "0.4" derive_more = { version = "1", features = ["full"] } -itertools = "0.14" +itertools = "0.15.0" once_cell = "1.20" -rayon = "1.8" +rayon = "1.12.0" rand = "0.8.5" regex = "1.12.2" reqwest = { version = "0.12", default-features = false, features = [ @@ -187,30 +178,43 @@ reqwest = { version = "0.12", default-features = false, features = [ semver = "1.0" static-files = "0.2" thiserror = "2.0" -ulid = { version = "1.0", features = ["serde"] } -uuid = { version = "1", features = ["v4"] } -xxhash-rust = { version = "0.8", features = ["xxh3"] } -rustc-hash = "2" +ulid = { version = "1.2.1", features = ["serde"] } +uuid = { version = "1.23.3", features = ["v4"] } +xxhash-rust = { version = "0.8.15", features = ["xxh3"] } +rustc-hash = "2.1.2" futures-core = "0.3.31" tempfile = "3.20.0" lazy_static = "1.4.0" prost = "0.13.1" -dashmap = "6.1.0" +dashmap = "6.2.1" parking_lot = "0.12.5" -indexmap = { version = "2.13.0", features = ["serde"] } +indexmap = { version = "2.14.0", features = ["serde"] } + +[target.'cfg(not(target_env = "msvc"))'.dependencies] +tikv-jemallocator = { version = "0.7.0", features = [ + "profiling", + "stats", + "background_threads", +] } +tikv-jemalloc-ctl = { version = "0.7.0", features = ["stats"] } [build-dependencies] -cargo_toml = "0.21" +cargo_toml = "0.22.3" sha1_smol = { version = "1.0", features = ["std"] } static-files = "0.2" ureq = "2.12" url = "2.5" -vergen-gitcl = { version = "1.0", features = ["build", "cargo", "rustc", "si"] } -zip = { version = "2.3", default-features = false, features = ["deflate"] } +vergen-gitcl = { version = "9.1.0", features = [ + "build", + "cargo", + "rustc", + "si", +] } +zip = { version = "8.6.0", default-features = false, features = ["deflate"] } anyhow = "1.0" [dev-dependencies] -rstest = "0.23.0" +rstest = "0.26.1" arrow = "58.0.0" temp-dir = "0.1.14" diff --git a/src/cli.rs b/src/cli.rs index bd9e3c957..3b48fe0b4 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -196,6 +196,16 @@ pub struct Options { )] pub max_connections: usize, + // SQL timeout in seconds + #[arg( + long, + env = "P_SQL_TIMEOUT", + default_value = "300", + value_parser = value_parser!(u64).range(5..), + help = "SQL execution timeout" + )] + pub sql_timeout: u64, + // DataFusion target partitions #[arg( long, diff --git a/src/main.rs b/src/main.rs index 4d07c1608..34a16e07f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,6 +35,14 @@ use tracing_subscriber::{EnvFilter, Registry, fmt}; #[cfg_attr(feature = "hotpath", hotpath::main)] async fn main() -> anyhow::Result<()> { init_logger(); + + #[cfg(all(not(target_env = "msvc"), not(feature = "hotpath")))] + use tikv_jemallocator::Jemalloc; + + #[cfg(all(not(target_env = "msvc"), not(feature = "hotpath")))] + #[global_allocator] + static GLOBAL: Jemalloc = Jemalloc; + // Install the rustls crypto provider before any TLS operations. // This is required for rustls 0.23+ which needs an explicit crypto provider. // If the installation fails, log a warning but continue execution. diff --git a/src/query/mod.rs b/src/query/mod.rs index 8ed86b1f3..09316baf6 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -166,7 +166,14 @@ impl InMemorySessionContext { pub async fn execute(query: Query, is_streaming: bool, tenant_id: &Option) -> QueryResult { let id = tenant_id.clone(); QUERY_RUNTIME - .spawn(async move { query.execute(is_streaming, &id).await }) + .spawn(async move { + tokio::time::timeout( + std::time::Duration::from_secs(PARSEABLE.options.sql_timeout), + query.execute(is_streaming, &id), + ) + .await + .map_err(|e| ExecuteError::Timeout(e, PARSEABLE.options.sql_timeout))? + }) .await .expect("The Join should have been successful") } @@ -943,6 +950,7 @@ pub fn flatten_objects_for_count(objects: Vec) -> Vec { pub mod error { use crate::{parseable::StreamNotFound, storage::ObjectStorageError}; use datafusion::error::DataFusionError; + use tokio::time::error::Elapsed; #[derive(Debug, thiserror::Error)] pub enum ExecuteError { @@ -952,6 +960,8 @@ pub mod error { Datafusion(#[from] DataFusionError), #[error("{0}")] StreamNotFound(#[from] StreamNotFound), + #[error("{0}: {1}s")] + Timeout(Elapsed, u64), } } From 236d7ef18159a8fe0f149aee4f6c69b235fa676e Mon Sep 17 00:00:00 2001 From: Nikhil Sinha <131262146+nikhilsinhaparseable@users.noreply.github.com> Date: Wed, 17 Jun 2026 15:03:51 +0530 Subject: [PATCH 35/47] dashboard: allow optional fields in dashboard json (#1684) * dashboard: allow optional fields in dashboard json currently we allow optional fields for each tile in the dashboard this change allows fields to be added from caller in the dashboard section also useful when client adds more feature to dashboard like - settings, variables etc * update dashboard fix --- src/handlers/http/users/dashboards.rs | 4 +--- src/users/dashboards.rs | 3 +++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/handlers/http/users/dashboards.rs b/src/handlers/http/users/dashboards.rs index e0b05e96c..4eaab2169 100644 --- a/src/handlers/http/users/dashboards.rs +++ b/src/handlers/http/users/dashboards.rs @@ -127,9 +127,7 @@ pub async fn update_dashboard( // Validate: either query params OR body, not both let has_query_params = !query_map.is_empty(); - let has_body_update = dashboard - .as_ref() - .is_some_and(|d| d.title != existing_dashboard.title || d.tiles.is_some()); + let has_body_update = dashboard.is_some(); if has_query_params && has_body_update { return Err(DashboardError::Metadata( diff --git a/src/users/dashboards.rs b/src/users/dashboards.rs index 04e35825e..5e0446bb3 100644 --- a/src/users/dashboards.rs +++ b/src/users/dashboards.rs @@ -68,6 +68,9 @@ pub struct Dashboard { dashboard_type: Option, pub tiles: Option>, pub tenant_id: Option, + /// all other fields are variable and can be added as needed + #[serde(flatten)] + pub other_fields: Option>, } impl MetastoreObject for Dashboard { From 831cea40cf9d70784c7b96cb7514d00f6885c871 Mon Sep 17 00:00:00 2001 From: parmesant Date: Thu, 18 Jun 2026 15:16:52 +0530 Subject: [PATCH 36/47] bugfix: schema updation issue (#1686) in standalone mode, the `.schema` file from staging wasn't getting pushed to storage due to the improper usage of `path().extension()` function. It was always returning `None`. Instead of checking whether the extension is `schema`, check if the file path ends with `.schema` --- src/parseable/streams.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/parseable/streams.rs b/src/parseable/streams.rs index cca7c95c1..8ebcdaea0 100644 --- a/src/parseable/streams.rs +++ b/src/parseable/streams.rs @@ -529,7 +529,11 @@ impl Stream { dir.flatten() .map(|file| file.path()) - .filter(|file| file.extension().is_some_and(|ext| ext.eq("schema"))) + .filter(|file| { + file.file_name() + .and_then(|n| n.to_str()) + .is_some_and(|n| n.ends_with(".schema")) + }) .collect() } @@ -539,8 +543,11 @@ impl Stream { let mut schemas: Vec = Vec::new(); for file in dir.flatten() { - if let Some(ext) = file.path().extension() - && ext.eq("schema") + if file + .path() + .file_name() + .and_then(|n| n.to_str()) + .is_some_and(|n| n.ends_with(".schema")) { let file = File::open(file.path())?; From ea262fc92011db13f920912cbc7ecce85e27d754 Mon Sep 17 00:00:00 2001 From: Nikhil Sinha <131262146+nikhilsinhaparseable@users.noreply.github.com> Date: Fri, 19 Jun 2026 15:35:41 +0530 Subject: [PATCH 37/47] feat: log context api (#1687) * feat: log context api request - ``` { "stream": "teststream", "contextWindow": "1m", "pTimestamp": "2026-06-18T07:39:59.995Z", "pageSize": 500, "message": "Application started", "conditions": { "operator": "and", "groups": [ { "operator": "or", "conditionConfig": [ { "column": "level", "operator": "=", "value": "warn", "type": "text" } ] } ] } } ``` response - ``` { "scope": "contextWindow", "contextStartTime": "2026-06-18T07:38:00Z", "contextEndTime": "2026-06-18T07:40:00Z", "limit": 500, "anchorIndex": 10, "duplicateAnchorCount": 15, "anchoredDuplicate": "first", "records": [ { "app_meta": "okcequedfmkqlgzheaidrcce", "device_id": 126.0, "host": "172.162.1.120", "level": "warn", "location": "uqwetjbuvjameflh", "message": "Application is failing", "meta-containerimage": "ghcr.io/parseablehq/quest", "meta-containername": "log-generator", "meta-host": "10.116.0.3", "meta-namespace": "go-apasdp", "meta-podlabels": "app=go-app,pod-template-hash=6c87bc9cc9", "meta-source": "quest-test", "os": "Windows", "p_src_ip": "127.0.0.1", "p_timestamp": "2026-06-18T07:39:59.995", "p_user_agent": "Grafana k6/1.6.1", "request_body": "vlywlgkpmciorkiklfruxcfnzaspahyscsazpmnqgquqrtahrzhmtojwvackzcqngscesuadnupwpdsryfrvlifembjotnftzuwx", "session_id": "pqr", "source_time": "2026-06-18T07:39:59.991", "status_code": 500.0, "user_id": 98513.0, "uuid": "169fa593-fa27-4625-8576-1faab8b9cc71", "version": "1.2.0" } ], "queries": { "previous": { "query": "SELECT * FROM (SELECT * FROM \"teststream\" WHERE ((\"p_timestamp\" >= TIMESTAMP '2026-06-18 07:38:00.000' AND \"p_timestamp\" < TIMESTAMP '2026-06-18 07:40:00.000') AND ((\"level\" = 'warn'))) AND (\"p_timestamp\" > TIMESTAMP '2026-06-18 07:39:59.995' OR (\"p_timestamp\" = TIMESTAMP '2026-06-18 07:39:59.995' AND \"message\" < 'Application is failing')) ORDER BY \"p_timestamp\" ASC, \"message\" DESC LIMIT 500) AS log_context_seek_page ORDER BY \"p_timestamp\" DESC, \"message\" ASC", "startTime": "2026-06-18T07:38:00Z", "endTime": "2026-06-18T07:40:00Z", "sendNull": false }, "next": { "query": "SELECT * FROM \"teststream\" WHERE ((\"p_timestamp\" >= TIMESTAMP '2026-06-18 07:38:00.000' AND \"p_timestamp\" < TIMESTAMP '2026-06-18 07:40:00.000') AND ((\"level\" = 'warn'))) AND (\"p_timestamp\" < TIMESTAMP '2026-06-18 07:39:59.662' OR (\"p_timestamp\" = TIMESTAMP '2026-06-18 07:39:59.662' AND \"message\" > 'Logging a request')) ORDER BY \"p_timestamp\" DESC, \"message\" ASC LIMIT 500", "startTime": "2026-06-18T07:38:00Z", "endTime": "2026-06-18T07:40:00Z", "sendNull": false } } } ``` * query string to have limit and offset * clippy fix --- Cargo.lock | 437 ++------ src/handlers/http/mod.rs | 1 + src/handlers/http/modal/query_server.rs | 1 + src/handlers/http/modal/server.rs | 12 + src/handlers/http/query.rs | 39 + src/handlers/http/query_context.rs | 1227 +++++++++++++++++++++++ 6 files changed, 1390 insertions(+), 327 deletions(-) create mode 100644 src/handlers/http/query_context.rs diff --git a/Cargo.lock b/Cargo.lock index 3ca5af152..0ee3d1a05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -487,40 +487,19 @@ version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d441fdda254b65f3e9025910eb2c2066b6295d9c8ed409522b8d2ace1ff8574c" dependencies = [ - "arrow-arith 58.1.0", - "arrow-array 58.1.0", - "arrow-buffer 58.1.0", - "arrow-cast 58.1.0", - "arrow-csv 58.1.0", - "arrow-data 58.1.0", - "arrow-ipc 58.1.0", - "arrow-json 58.1.0", - "arrow-ord 58.1.0", - "arrow-row 58.1.0", - "arrow-schema 58.1.0", - "arrow-select 58.1.0", - "arrow-string 58.1.0", -] - -[[package]] -name = "arrow" -version = "59.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffaaa3e009861fd829d0a24dd6f115aa8e4634324bb092147d43baafe69ca4a7" -dependencies = [ - "arrow-arith 59.0.0", - "arrow-array 59.0.0", - "arrow-buffer 59.0.0", - "arrow-cast 59.0.0", - "arrow-csv 59.0.0", - "arrow-data 59.0.0", - "arrow-ipc 59.0.0", - "arrow-json 59.0.0", - "arrow-ord 59.0.0", - "arrow-row 59.0.0", - "arrow-schema 59.0.0", - "arrow-select 59.0.0", - "arrow-string 59.0.0", + "arrow-arith", + "arrow-array", + "arrow-buffer", + "arrow-cast", + "arrow-csv", + "arrow-data", + "arrow-ipc", + "arrow-json", + "arrow-ord", + "arrow-row", + "arrow-schema", + "arrow-select", + "arrow-string", ] [[package]] @@ -529,24 +508,10 @@ version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced5406f8b720cc0bc3aa9cf5758f93e8593cda5490677aa194e4b4b383f9a59" dependencies = [ - "arrow-array 58.1.0", - "arrow-buffer 58.1.0", - "arrow-data 58.1.0", - "arrow-schema 58.1.0", - "chrono", - "num-traits", -] - -[[package]] -name = "arrow-arith" -version = "59.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ac95125e1d71c4a252b5a9c729aef111e80418f08aaa6dbabd1ba66918247fc" -dependencies = [ - "arrow-array 59.0.0", - "arrow-buffer 59.0.0", - "arrow-data 59.0.0", - "arrow-schema 59.0.0", + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", "chrono", "num-traits", ] @@ -558,9 +523,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "772bd34cacdda8baec9418d80d23d0fb4d50ef0735685bd45158b83dfeb6e62d" dependencies = [ "ahash", - "arrow-buffer 58.1.0", - "arrow-data 58.1.0", - "arrow-schema 58.1.0", + "arrow-buffer", + "arrow-data", + "arrow-schema", "chrono", "chrono-tz", "half", @@ -570,24 +535,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "arrow-array" -version = "59.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c60c79628e9a97cb90d7a0dc3e944f216a902f837d4ecabc14d524bddbbc137" -dependencies = [ - "ahash", - "arrow-buffer 59.0.0", - "arrow-data 59.0.0", - "arrow-schema 59.0.0", - "chrono", - "half", - "hashbrown 0.17.1", - "num-complex", - "num-integer", - "num-traits", -] - [[package]] name = "arrow-buffer" version = "58.1.0" @@ -600,30 +547,18 @@ dependencies = [ "num-traits", ] -[[package]] -name = "arrow-buffer" -version = "59.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6026f638c400e9878c1b1cc05c3cfd46fbf381285916ab408678701c1df46c1a" -dependencies = [ - "bytes", - "half", - "num-bigint", - "num-traits", -] - [[package]] name = "arrow-cast" version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0127816c96533d20fc938729f48c52d3e48f99717e7a0b5ade77d742510736d" dependencies = [ - "arrow-array 58.1.0", - "arrow-buffer 58.1.0", - "arrow-data 58.1.0", - "arrow-ord 58.1.0", - "arrow-schema 58.1.0", - "arrow-select 58.1.0", + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-ord", + "arrow-schema", + "arrow-select", "atoi", "base64", "chrono", @@ -634,51 +569,15 @@ dependencies = [ "ryu", ] -[[package]] -name = "arrow-cast" -version = "59.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c82c236c3caf8df5664284f3f1fbe89938852163998c3fdbf37e84ac220445e9" -dependencies = [ - "arrow-array 59.0.0", - "arrow-buffer 59.0.0", - "arrow-data 59.0.0", - "arrow-ord 59.0.0", - "arrow-schema 59.0.0", - "arrow-select 59.0.0", - "atoi", - "base64", - "chrono", - "half", - "lexical-core", - "num-traits", - "ryu", -] - [[package]] name = "arrow-csv" version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca025bd0f38eeecb57c2153c0123b960494138e6a957bbda10da2b25415209fe" dependencies = [ - "arrow-array 58.1.0", - "arrow-cast 58.1.0", - "arrow-schema 58.1.0", - "chrono", - "csv", - "csv-core", - "regex", -] - -[[package]] -name = "arrow-csv" -version = "59.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12714e5fb7954159af1e26d4e0d37108bcf1a2ad5ee5c5bf02a944d564d588b7" -dependencies = [ - "arrow-array 59.0.0", - "arrow-cast 59.0.0", - "arrow-schema 59.0.0", + "arrow-array", + "arrow-cast", + "arrow-schema", "chrono", "csv", "csv-core", @@ -691,21 +590,8 @@ version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42d10beeab2b1c3bb0b53a00f7c944a178b622173a5c7bcabc3cb45d90238df4" dependencies = [ - "arrow-buffer 58.1.0", - "arrow-schema 58.1.0", - "half", - "num-integer", - "num-traits", -] - -[[package]] -name = "arrow-data" -version = "59.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd568aa70c4ec5947027b0d5caee94877433b661a0bb9e8ddceeeb5f0c9b1ab" -dependencies = [ - "arrow-buffer 59.0.0", - "arrow-schema 59.0.0", + "arrow-buffer", + "arrow-schema", "half", "num-integer", "num-traits", @@ -717,11 +603,11 @@ version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "302b2e036335f3f04d65dad3f74ff1f2aae6dc671d6aa04dc6b61193761e16fb" dependencies = [ - "arrow-array 58.1.0", - "arrow-buffer 58.1.0", - "arrow-cast 58.1.0", - "arrow-ipc 58.1.0", - "arrow-schema 58.1.0", + "arrow-array", + "arrow-buffer", + "arrow-cast", + "arrow-ipc", + "arrow-schema", "base64", "bytes", "futures", @@ -737,66 +623,27 @@ version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "609a441080e338147a84e8e6904b6da482cefb957c5cdc0f3398872f69a315d0" dependencies = [ - "arrow-array 58.1.0", - "arrow-buffer 58.1.0", - "arrow-data 58.1.0", - "arrow-schema 58.1.0", - "arrow-select 58.1.0", + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", "flatbuffers", "lz4_flex", "zstd", ] -[[package]] -name = "arrow-ipc" -version = "59.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57ee4d470eab1a021bc4b63fa2b2c15d572892bf227b0a982d3b755a6c662b5" -dependencies = [ - "arrow-array 59.0.0", - "arrow-buffer 59.0.0", - "arrow-data 59.0.0", - "arrow-schema 59.0.0", - "arrow-select 59.0.0", - "flatbuffers", -] - [[package]] name = "arrow-json" version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ead0914e4861a531be48fe05858265cf854a4880b9ed12618b1d08cba9bebc8" dependencies = [ - "arrow-array 58.1.0", - "arrow-buffer 58.1.0", - "arrow-cast 58.1.0", - "arrow-data 58.1.0", - "arrow-schema 58.1.0", - "chrono", - "half", - "indexmap", - "itoa", - "lexical-core", - "memchr", - "num-traits", - "ryu", - "serde_core", - "serde_json", - "simdutf8", -] - -[[package]] -name = "arrow-json" -version = "59.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f47e0e7a284e1f3707a780dc8cd5451b1614e9e398ea2d9ca03c7a2fe9a9ed" -dependencies = [ - "arrow-array 59.0.0", - "arrow-buffer 59.0.0", - "arrow-cast 59.0.0", - "arrow-ord 59.0.0", - "arrow-schema 59.0.0", - "arrow-select 59.0.0", + "arrow-array", + "arrow-buffer", + "arrow-cast", + "arrow-data", + "arrow-schema", "chrono", "half", "indexmap", @@ -816,24 +663,11 @@ version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "763a7ba279b20b52dad300e68cfc37c17efa65e68623169076855b3a9e941ca5" dependencies = [ - "arrow-array 58.1.0", - "arrow-buffer 58.1.0", - "arrow-data 58.1.0", - "arrow-schema 58.1.0", - "arrow-select 58.1.0", -] - -[[package]] -name = "arrow-ord" -version = "59.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a79cf73ad2eba8686ec2aa9bbf8671208e509025f166afc040cedbd94ffe4983" -dependencies = [ - "arrow-array 59.0.0", - "arrow-buffer 59.0.0", - "arrow-data 59.0.0", - "arrow-schema 59.0.0", - "arrow-select 59.0.0", + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", ] [[package]] @@ -842,23 +676,10 @@ version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e14fe367802f16d7668163ff647830258e6e0aeea9a4d79aaedf273af3bdcd3e" dependencies = [ - "arrow-array 58.1.0", - "arrow-buffer 58.1.0", - "arrow-data 58.1.0", - "arrow-schema 58.1.0", - "half", -] - -[[package]] -name = "arrow-row" -version = "59.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea0f7d8ed6182f14952761e2c0f989852d5aa334fcbc49f73a9f2247c25b879" -dependencies = [ - "arrow-array 59.0.0", - "arrow-buffer 59.0.0", - "arrow-data 59.0.0", - "arrow-schema 59.0.0", + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", "half", ] @@ -873,12 +694,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "arrow-schema" -version = "59.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80b3e786a0dd9103acd583a6fb486dbf2f3268466cc0bd571dcf34cef231c1f1" - [[package]] name = "arrow-select" version = "58.1.0" @@ -886,24 +701,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78694888660a9e8ac949853db393af2a8b8fc82c19ce333132dfa2e72cc1a7fe" dependencies = [ "ahash", - "arrow-array 58.1.0", - "arrow-buffer 58.1.0", - "arrow-data 58.1.0", - "arrow-schema 58.1.0", - "num-traits", -] - -[[package]] -name = "arrow-select" -version = "59.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "067a67e0361f6c31f4a7248759f36ca4ca71b187a941ed4d49da1c7d3d4db624" -dependencies = [ - "ahash", - "arrow-array 59.0.0", - "arrow-buffer 59.0.0", - "arrow-data 59.0.0", - "arrow-schema 59.0.0", + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", "num-traits", ] @@ -913,28 +714,11 @@ version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61e04a01f8bb73ce54437514c5fd3ee2aa3e8abe4c777ee5cc55853b1652f79e" dependencies = [ - "arrow-array 58.1.0", - "arrow-buffer 58.1.0", - "arrow-data 58.1.0", - "arrow-schema 58.1.0", - "arrow-select 58.1.0", - "memchr", - "num-traits", - "regex", - "regex-syntax", -] - -[[package]] -name = "arrow-string" -version = "59.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99bc95847f3ff62a2b03d6f8ce2e3e78f01362060549a2a311898dd442f6256d" -dependencies = [ - "arrow-array 59.0.0", - "arrow-buffer 59.0.0", - "arrow-data 59.0.0", - "arrow-schema 59.0.0", - "arrow-select 59.0.0", + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", "memchr", "num-traits", "regex", @@ -1743,8 +1527,8 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93db0e623840612f7f2cd757f7e8a8922064192363732c88692e0870016e141b" dependencies = [ - "arrow 58.1.0", - "arrow-schema 58.1.0", + "arrow", + "arrow-schema", "async-trait", "bytes", "bzip2", @@ -1798,7 +1582,7 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37cefde60b26a7f4ff61e9d2ff2833322f91df2b568d7238afe67bde5bdffb66" dependencies = [ - "arrow 58.1.0", + "arrow", "async-trait", "dashmap", "datafusion-common", @@ -1823,7 +1607,7 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17e112307715d6a7a331111a4c2330ff54bc237183511c319e3708a4cff431fb" dependencies = [ - "arrow 58.1.0", + "arrow", "async-trait", "datafusion-catalog", "datafusion-common", @@ -1847,8 +1631,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d72a11ca44a95e1081870d3abb80c717496e8a7acb467a1d3e932bb636af5cc2" dependencies = [ "ahash", - "arrow 58.1.0", - "arrow-ipc 58.1.0", + "arrow", + "arrow-ipc", "chrono", "half", "hashbrown 0.16.1", @@ -1882,7 +1666,7 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9fb386e1691355355a96419978a0022b7947b44d4a24a6ea99f00b6b485cbb6" dependencies = [ - "arrow 58.1.0", + "arrow", "async-compression", "async-trait", "bytes", @@ -1917,8 +1701,8 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffa6c52cfed0734c5f93754d1c0175f558175248bf686c944fb05c373e5fc096" dependencies = [ - "arrow 58.1.0", - "arrow-ipc 58.1.0", + "arrow", + "arrow-ipc", "async-trait", "bytes", "datafusion-common", @@ -1941,7 +1725,7 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "503f29e0582c1fc189578d665ff57d9300da1f80c282777d7eb67bb79fb8cdca" dependencies = [ - "arrow 58.1.0", + "arrow", "async-trait", "bytes", "datafusion-common", @@ -1964,7 +1748,7 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e33804749abc8d0c8cb7473228483cb8070e524c6f6086ee1b85a64debe2b3d2" dependencies = [ - "arrow 58.1.0", + "arrow", "async-trait", "bytes", "datafusion-common", @@ -1988,7 +1772,7 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a8e0365e0e08e8ff94d912f0ababcf9065a1a304018ba90b1fc83c855b4997" dependencies = [ - "arrow 58.1.0", + "arrow", "async-trait", "bytes", "datafusion-common", @@ -2024,8 +1808,8 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c03c7fbdaefcca4ef6ffe425a5fc2325763bfb426599bb0bf4536466efabe709" dependencies = [ - "arrow 58.1.0", - "arrow-buffer 58.1.0", + "arrow", + "arrow-buffer", "async-trait", "chrono", "dashmap", @@ -2047,7 +1831,7 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "574b9b6977fedbd2a611cbff12e5caf90f31640ad9dc5870f152836d94bad0dd" dependencies = [ - "arrow 58.1.0", + "arrow", "async-trait", "chrono", "datafusion-common", @@ -2070,7 +1854,7 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d7c3adf3db8bf61e92eb90cb659c8e8b734593a8f7c8e12a843c7ddba24b87e" dependencies = [ - "arrow 58.1.0", + "arrow", "datafusion-common", "indexmap", "itertools 0.14.0", @@ -2083,8 +1867,8 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f28aa4e10384e782774b10e72aca4d93ef7b31aa653095d9d4536b0a3dbc51b6" dependencies = [ - "arrow 58.1.0", - "arrow-buffer 58.1.0", + "arrow", + "arrow-buffer", "base64", "blake2", "blake3", @@ -2116,7 +1900,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00aa6217e56098ba84e0a338176fe52f0a84cca398021512c6c8c5eff806d0ad" dependencies = [ "ahash", - "arrow 58.1.0", + "arrow", "datafusion-common", "datafusion-doc", "datafusion-execution", @@ -2138,7 +1922,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b511250349407db7c43832ab2de63f5557b19a20dfd236b39ca2c04468b50d47" dependencies = [ "ahash", - "arrow 58.1.0", + "arrow", "datafusion-common", "datafusion-expr-common", "datafusion-physical-expr-common", @@ -2150,8 +1934,8 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef13a858e20d50f0a9bb5e96e7ac82b4e7597f247515bccca4fdd2992df0212a" dependencies = [ - "arrow 58.1.0", - "arrow-ord 58.1.0", + "arrow", + "arrow-ord", "datafusion-common", "datafusion-doc", "datafusion-execution", @@ -2175,7 +1959,7 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b40d3f5bbb3905f9ccb1ce9485a9595c77b69758a7c24d3ba79e334ff51e7e" dependencies = [ - "arrow 58.1.0", + "arrow", "async-trait", "datafusion-catalog", "datafusion-common", @@ -2191,7 +1975,7 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e88ec9d57c9b685d02f58bfee7be62d72610430ddcedb82a08e5d9925dbfb6" dependencies = [ - "arrow 58.1.0", + "arrow", "datafusion-common", "datafusion-doc", "datafusion-expr", @@ -2230,7 +2014,7 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e929015451a67f77d9d8b727b2bf3a40c4445fdef6cdc53281d7d97c76888ace" dependencies = [ - "arrow 58.1.0", + "arrow", "chrono", "datafusion-common", "datafusion-expr", @@ -2251,7 +2035,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b1e68aba7a4b350401cfdf25a3d6f989ad898a7410164afe9ca52080244cb59" dependencies = [ "ahash", - "arrow 58.1.0", + "arrow", "datafusion-common", "datafusion-expr", "datafusion-expr-common", @@ -2274,7 +2058,7 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea22315f33cf2e0adc104e8ec42e285f6ed93998d565c65e82fec6a9ee9f9db4" dependencies = [ - "arrow 58.1.0", + "arrow", "datafusion-common", "datafusion-expr", "datafusion-functions", @@ -2290,7 +2074,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b04b45ea8ad3ac2d78f2ea2a76053e06591c9629c7a603eda16c10649ecf4362" dependencies = [ "ahash", - "arrow 58.1.0", + "arrow", "chrono", "datafusion-common", "datafusion-expr-common", @@ -2306,7 +2090,7 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cb13397809a425918f608dfe8653f332015a3e330004ab191b4404187238b95" dependencies = [ - "arrow 58.1.0", + "arrow", "datafusion-common", "datafusion-execution", "datafusion-expr", @@ -2326,9 +2110,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5edc023675791af9d5fb4cc4c24abf5f7bd3bd4dcf9e5bd90ea1eff6976dcc79" dependencies = [ "ahash", - "arrow 58.1.0", - "arrow-ord 58.1.0", - "arrow-schema 58.1.0", + "arrow", + "arrow-ord", + "arrow-schema", "async-trait", "datafusion-common", "datafusion-common-runtime", @@ -2396,7 +2180,7 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac8c76860e355616555081cab5968cec1af7a80701ff374510860bcd567e365a" dependencies = [ - "arrow 58.1.0", + "arrow", "datafusion-common", "datafusion-datasource", "datafusion-expr-common", @@ -2427,7 +2211,7 @@ version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa0d133ddf8b9b3b872acac900157f783e7b879fe9a6bccf389abebbfac45ec1" dependencies = [ - "arrow 58.1.0", + "arrow", "bigdecimal", "chrono", "datafusion-common", @@ -4266,12 +4050,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d3f9f2205199603564127932b89695f52b62322f541d0fc7179d57c2e1c9877" dependencies = [ "ahash", - "arrow-array 58.1.0", - "arrow-buffer 58.1.0", - "arrow-data 58.1.0", - "arrow-ipc 58.1.0", - "arrow-schema 58.1.0", - "arrow-select 58.1.0", + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-ipc", + "arrow-schema", + "arrow-select", "base64", "brotli", "bytes", @@ -4307,14 +4091,13 @@ dependencies = [ "actix-web-static-files", "anyhow", "argon2", - "arrow 58.1.0", - "arrow 59.0.0", - "arrow-array 58.1.0", + "arrow", + "arrow-array", "arrow-flight", - "arrow-ipc 58.1.0", - "arrow-json 58.1.0", - "arrow-schema 58.1.0", - "arrow-select 58.1.0", + "arrow-ipc", + "arrow-json", + "arrow-schema", + "arrow-select", "async-trait", "base64", "byteorder", diff --git a/src/handlers/http/mod.rs b/src/handlers/http/mod.rs index 993a40b10..86c2be877 100644 --- a/src/handlers/http/mod.rs +++ b/src/handlers/http/mod.rs @@ -44,6 +44,7 @@ pub mod oidc; pub mod prism_home; pub mod prism_logstream; pub mod query; +pub mod query_context; pub mod rbac; pub mod resource_check; pub mod role; diff --git a/src/handlers/http/modal/query_server.rs b/src/handlers/http/modal/query_server.rs index 0d6941cb8..3da7d6e53 100644 --- a/src/handlers/http/modal/query_server.rs +++ b/src/handlers/http/modal/query_server.rs @@ -58,6 +58,7 @@ impl ParseableServer for QueryServer { web::scope(&base_path()) .service(Server::get_correlation_webscope()) .service(Server::get_query_factory()) + .service(Server::get_query_context_factory()) .service(Server::get_liveness_factory()) .service(Server::get_readiness_factory()) .service(Server::get_about_factory()) diff --git a/src/handlers/http/modal/server.rs b/src/handlers/http/modal/server.rs index 6a0309075..06703ad7d 100644 --- a/src/handlers/http/modal/server.rs +++ b/src/handlers/http/modal/server.rs @@ -29,6 +29,7 @@ use crate::handlers::http::max_event_payload_size; use crate::handlers::http::middleware::IntraClusterRequest; use crate::handlers::http::prism_base_path; use crate::handlers::http::query; +use crate::handlers::http::query_context; use crate::handlers::http::targets; use crate::handlers::http::users::dashboards; use crate::handlers::http::users::filters; @@ -80,6 +81,7 @@ impl ParseableServer for Server { web::scope(&base_path()) .service(Self::get_correlation_webscope()) .service(Self::get_query_factory()) + .service(Self::get_query_context_factory()) .service(Self::get_ingest_factory()) .service(Self::get_liveness_factory()) .service(Self::get_readiness_factory()) @@ -424,6 +426,16 @@ impl Server { ) } + // POST "/query/context" ==> Get a page of logs around a selected log line + pub fn get_query_context_factory() -> Resource { + web::resource("/query/context").route( + web::post() + .to(query_context::query_context) + .authorize(Action::Query) + .wrap(IntraClusterRequest), + ) + } + // get the logstream web scope pub fn get_logstream_webscope() -> Scope { web::scope("/logstream") diff --git a/src/handlers/http/query.rs b/src/handlers/http/query.rs index c7641eaac..c7baf8cba 100644 --- a/src/handlers/http/query.rs +++ b/src/handlers/http/query.rs @@ -115,6 +115,45 @@ pub async fn get_records_and_fields( Ok((Some(records), Some(fields))) } +/// Executes a query after the caller has already loaded and authorized the streams. +/// +/// This keeps structured APIs that fan out into multiple internal SQL queries from repeating +/// distributed stream loading and RBAC checks for every generated query. +pub async fn get_records_and_fields_for_authorized_query( + query_request: &Query, + authorized_tables: &[String], + tenant_id: &Option, +) -> Result<(Option>, Option>), QueryError> { + let mut session_state = QUERY_SESSION.get_ctx().state(); + let time_range = + TimeRange::parse_human_time(&query_request.start_time, &query_request.end_time)?; + let tables = resolve_stream_names(&query_request.query)?; + if tables + .iter() + .any(|table| !authorized_tables.contains(table)) + { + return Err(QueryError::Unauthorized); + } + + session_state + .config_mut() + .options_mut() + .catalog + .default_schema = tenant_id.as_deref().unwrap_or("public").to_owned(); + + let query: LogicalQuery = into_query(query_request, &session_state, time_range).await?; + let (records, fields) = execute(query, false, tenant_id).await?; + + let records = match records { + Either::Left(vec_rb) => vec_rb, + Either::Right(_) => { + return Err(QueryError::CustomError("Reject streaming response".into())); + } + }; + + Ok((Some(records), Some(fields))) +} + pub async fn query(req: HttpRequest, query_request: Query) -> Result { let mut session_state = QUERY_SESSION.get_ctx().state(); let time_range = diff --git a/src/handlers/http/query_context.rs b/src/handlers/http/query_context.rs new file mode 100644 index 000000000..422d4d85e --- /dev/null +++ b/src/handlers/http/query_context.rs @@ -0,0 +1,1227 @@ +/* + * Parseable Server (C) 2022 - 2025 Parseable, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +use actix_web::web::{self, Json}; +use actix_web::{HttpRequest, Responder}; +use arrow_schema::DataType; +use chrono::{DateTime, NaiveDateTime, SecondsFormat, Utc}; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; +use tracing::{Span, debug, info, warn}; + +use crate::alerts::{alert_structs::Conditions, alerts_utils::get_filter_string}; +use crate::event::DEFAULT_TIMESTAMP_KEY; +use crate::handlers::http::query::{ + Query, QueryError, create_streams_for_distributed, get_records_and_fields_for_authorized_query, +}; +use crate::metrics::increment_query_calls_by_date; +use crate::parseable::{DEFAULT_TENANT, PARSEABLE}; +use crate::rbac::Users; +use crate::utils::actix::extract_session_key_from_req; +use crate::utils::arrow::record_batches_to_json; +use crate::utils::time::truncate_to_minute; +use crate::utils::{get_tenant_id_from_request, user_auth_for_datasets}; + +const DEFAULT_LOG_CONTEXT_PAGE_SIZE: u64 = 500; +const LOG_CONTEXT_ANCHORED_DUPLICATE: &str = "first"; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct LogContextRequest { + pub dataset: String, + pub context_window: String, + pub p_timestamp: String, + pub log: Option, + pub body: Option, + pub message: Option, + pub conditions: Option, + pub page_size: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LogContextResponse { + pub scope: LogContextScope, + pub context_start_time: String, + pub context_end_time: String, + pub limit: u64, + pub anchor_index: u64, + pub duplicate_anchor_count: u64, + pub anchored_duplicate: &'static str, + pub records: Vec, + pub queries: LogContextQueries, +} + +#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub enum LogContextScope { + ContextWindow, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LogContextQueries { + pub previous: Option, + pub next: Option, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LogContextQueryPayload { + pub query: String, + pub start_time: String, + pub end_time: String, + pub send_null: bool, + pub reverse_records: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct LogContextMatchField { + name: String, + value: String, +} + +#[derive(Debug, Clone)] +struct LogContextCursor { + timestamp: DateTime, + match_field: LogContextMatchField, +} + +#[tracing::instrument( + name = "query_context", + skip(req, context_request), + fields( + dataset = tracing::field::Empty, + tenant = tracing::field::Empty, + page_size = tracing::field::Empty, + scope = tracing::field::Empty + ) +)] +pub async fn query_context( + req: HttpRequest, + context_request: Json, +) -> Result { + let context_request = context_request.into_inner(); + let tenant_id = get_tenant_id_from_request(&req); + let span = Span::current(); + span.record("dataset", tracing::field::display(&context_request.dataset)); + span.record("tenant", tracing::field::debug(&tenant_id)); + info!( + has_log = context_request.log.is_some(), + has_body = context_request.body.is_some(), + has_message = context_request.message.is_some(), + has_conditions = context_request.conditions.is_some(), + "query context request received" + ); + + let creds = extract_session_key_from_req(&req)?; + let permissions = Users.get_permissions(&creds); + + create_streams_for_distributed(vec![context_request.dataset.clone()], &tenant_id).await?; + let authorized_datasets = vec![context_request.dataset.clone()]; + user_auth_for_datasets( + &permissions, + std::slice::from_ref(&context_request.dataset), + &tenant_id, + ) + .await?; + + let page_size = normalize_log_context_page_size(context_request.page_size)?; + span.record("page_size", page_size); + let anchor_timestamp = parse_log_context_timestamp(&context_request.p_timestamp)?; + let (context_start_time, context_end_time) = + log_context_bounds(anchor_timestamp, &context_request.context_window)?; + debug!( + page_size, + anchor_timestamp = %anchor_timestamp, + context_start_time = %context_start_time, + context_end_time = %context_end_time, + "query context request normalized" + ); + + let dataset = PARSEABLE.get_stream(&context_request.dataset, &tenant_id)?; + let schema = dataset.get_schema(); + validate_log_context_schema(schema.as_ref(), DEFAULT_TIMESTAMP_KEY)?; + let match_fields = normalize_log_context_match_fields( + &context_request.log, + &context_request.body, + &context_request.message, + schema.as_ref(), + )?; + let additional_filter = log_context_additional_filter(&context_request.conditions)?; + + let context_start_time_str = format_log_context_api_time(context_start_time); + let context_end_time_str = format_log_context_api_time(context_end_time); + + let anchor_count_query = build_log_context_anchor_count_query( + &context_request.dataset, + anchor_timestamp, + context_start_time, + context_end_time, + &match_fields, + additional_filter.as_deref(), + ); + let duplicate_anchor_count = execute_log_context_anchor_count( + &anchor_count_query, + &authorized_datasets, + &context_start_time_str, + &context_end_time_str, + &tenant_id, + ) + .await?; + debug!( + duplicate_anchor_count, + "query context anchor count resolved" + ); + + let scope = LogContextScope::ContextWindow; + span.record("scope", tracing::field::debug(&scope)); + info!( + scope = ?scope, + duplicate_anchor_count, + "query context scope selected" + ); + + if duplicate_anchor_count == 0 { + warn!( + scope = ?scope, + "query context anchor row not found" + ); + return Err(QueryError::CustomError( + "No log row matched the provided pTimestamp and log/body/message value".to_string(), + )); + } + + let fetch_limit = page_size; + let newer_payload = build_log_context_newer_query_payload( + &context_request.dataset, + anchor_timestamp, + context_start_time, + context_end_time, + &match_fields, + additional_filter.as_deref(), + &context_start_time_str, + &context_end_time_str, + fetch_limit, + ); + let older_payload = build_log_context_anchor_and_older_query_payload( + &context_request.dataset, + anchor_timestamp, + context_start_time, + context_end_time, + &match_fields, + additional_filter.as_deref(), + &context_start_time_str, + &context_end_time_str, + fetch_limit, + ); + + let (newer_records, older_records) = tokio::try_join!( + execute_log_context_rows(&newer_payload, &authorized_datasets, &tenant_id), + execute_log_context_rows(&older_payload, &authorized_datasets, &tenant_id), + )?; + let (records, anchor_index) = + build_log_context_records_window(newer_records, older_records, page_size)?; + + let queries = build_log_context_cursor_queries( + &context_request.dataset, + context_start_time, + context_end_time, + &match_fields, + additional_filter.as_deref(), + &context_start_time_str, + &context_end_time_str, + page_size, + &records, + )?; + + info!( + scope = ?scope, + anchor_index, + duplicate_anchor_count, + record_count = records.len(), + "query context rows fetched" + ); + + let current_date = chrono::Utc::now().date_naive().to_string(); + increment_query_calls_by_date( + ¤t_date, + tenant_id.as_deref().unwrap_or(DEFAULT_TENANT), + ); + + Ok(web::Json(LogContextResponse { + scope, + context_start_time: context_start_time_str, + context_end_time: context_end_time_str, + limit: page_size, + anchor_index, + duplicate_anchor_count, + anchored_duplicate: LOG_CONTEXT_ANCHORED_DUPLICATE, + records, + queries, + })) +} + +fn normalize_log_context_page_size(page_size: Option) -> Result { + let page_size = page_size.unwrap_or(DEFAULT_LOG_CONTEXT_PAGE_SIZE); + if page_size == 0 { + return Err(QueryError::CustomError( + "pageSize must be greater than 0".to_string(), + )); + } + + Ok(page_size.min(DEFAULT_LOG_CONTEXT_PAGE_SIZE)) +} + +fn parse_log_context_timestamp(raw: &str) -> Result, QueryError> { + let raw = raw.trim(); + let timestamp = DateTime::parse_from_rfc3339(raw) + .map(|timestamp| timestamp.with_timezone(&Utc)) + .or_else(|rfc3339_err| { + parse_log_context_naive_utc_timestamp(raw) + .map(|timestamp| DateTime::from_naive_utc_and_offset(timestamp, Utc)) + .map_err(|_| QueryError::CustomError(format!("Invalid pTimestamp: {rfc3339_err}"))) + })?; + + DateTime::from_timestamp_millis(timestamp.timestamp_millis()).ok_or_else(|| { + QueryError::CustomError("pTimestamp is outside the supported range".to_string()) + }) +} + +fn parse_log_context_naive_utc_timestamp(raw: &str) -> Result { + NaiveDateTime::parse_from_str(raw, "%Y-%m-%dT%H:%M:%S%.f") + .or_else(|_| NaiveDateTime::parse_from_str(raw, "%Y-%m-%d %H:%M:%S%.f")) +} + +fn log_context_bounds( + anchor_timestamp: DateTime, + context_window: &str, +) -> Result<(DateTime, DateTime), QueryError> { + let duration = humantime::parse_duration(context_window) + .map_err(|err| QueryError::CustomError(format!("Invalid contextWindow: {err}")))?; + if duration.is_zero() { + return Err(QueryError::CustomError( + "contextWindow must be greater than 0".to_string(), + )); + } + + let duration = chrono::Duration::from_std(duration) + .map_err(|err| QueryError::CustomError(format!("Invalid contextWindow: {err}")))?; + let start = truncate_to_minute(anchor_timestamp - duration); + let mut end = truncate_to_minute(anchor_timestamp + duration); + + if start >= end { + end = start + chrono::Duration::minutes(1); + } + + Ok((start, end)) +} + +fn normalize_log_context_match_fields( + log: &Option, + body: &Option, + message: &Option, + schema: &arrow_schema::Schema, +) -> Result, QueryError> { + let fields = [("log", log), ("body", body), ("message", message)] + .into_iter() + .filter_map(|(field_name, value)| value.as_ref().map(|value| (field_name, value))) + .map(|(name, value)| { + validate_log_context_match_field_schema(schema, name)?; + Ok(LogContextMatchField { + name: name.to_string(), + value: value.clone(), + }) + }) + .collect::, QueryError>>()?; + + if fields.is_empty() { + return Err(QueryError::CustomError( + "Request must include exactly one of log, body, or message".to_string(), + )); + } + + if fields.len() > 1 { + return Err(QueryError::CustomError( + "Request must include exactly one of log, body, or message".to_string(), + )); + } + + Ok(fields) +} + +fn validate_log_context_schema( + schema: &arrow_schema::Schema, + field_name: &str, +) -> Result<(), QueryError> { + if schema + .fields() + .iter() + .any(|field| field.name() == field_name) + { + return Ok(()); + } + + Err(QueryError::CustomError(format!( + "Field '{field_name}' does not exist in dataset schema" + ))) +} + +fn validate_log_context_match_field_schema( + schema: &arrow_schema::Schema, + field_name: &str, +) -> Result<(), QueryError> { + let field = schema + .fields() + .iter() + .find(|field| field.name() == field_name) + .ok_or_else(|| { + QueryError::CustomError(format!( + "Field '{field_name}' does not exist in dataset schema" + )) + })?; + + match field.data_type() { + DataType::Utf8 | DataType::LargeUtf8 | DataType::Utf8View => Ok(()), + data_type => Err(QueryError::CustomError(format!( + "Field '{field_name}' must be a string column for log context matching; found {data_type}" + ))), + } +} + +fn log_context_additional_filter( + conditions: &Option, +) -> Result, QueryError> { + conditions + .as_ref() + .map(|conditions| { + get_filter_string(conditions) + .map(|filter| format!("({filter})")) + .map_err(|err| QueryError::CustomError(format!("Invalid conditions: {err}"))) + }) + .transpose() +} + +fn build_log_context_anchor_count_query( + dataset: &str, + anchor_timestamp: DateTime, + context_start_time: DateTime, + context_end_time: DateTime, + match_fields: &[LogContextMatchField], + additional_filter: Option<&str>, +) -> String { + let scope_filter = + build_log_context_scope_filter(context_start_time, context_end_time, additional_filter); + let anchor_match_predicate = + build_log_context_anchor_match_predicate(anchor_timestamp, match_fields); + let dataset = quote_sql_identifier(dataset); + + format!( + r#"SELECT + COUNT(*) AS duplicate_anchor_count +FROM {dataset} +WHERE {scope_filter} AND ({anchor_match_predicate})"# + ) +} + +#[allow(clippy::too_many_arguments)] +fn build_log_context_newer_query_payload( + dataset: &str, + anchor_timestamp: DateTime, + context_start_time: DateTime, + context_end_time: DateTime, + match_fields: &[LogContextMatchField], + additional_filter: Option<&str>, + start_time: &str, + end_time: &str, + limit: u64, +) -> LogContextQueryPayload { + LogContextQueryPayload { + query: build_log_context_neighbor_query( + dataset, + context_start_time, + context_end_time, + &LogContextCursor { + timestamp: anchor_timestamp, + match_field: match_fields[0].clone(), + }, + LogContextSeekDirection::Newer, + match_fields, + additional_filter, + Some(limit), + None, + ), + start_time: start_time.to_string(), + end_time: end_time.to_string(), + send_null: false, + reverse_records: false, + } +} + +#[allow(clippy::too_many_arguments)] +fn build_log_context_anchor_and_older_query_payload( + dataset: &str, + anchor_timestamp: DateTime, + context_start_time: DateTime, + context_end_time: DateTime, + match_fields: &[LogContextMatchField], + additional_filter: Option<&str>, + start_time: &str, + end_time: &str, + limit: u64, +) -> LogContextQueryPayload { + LogContextQueryPayload { + query: build_log_context_anchor_and_older_query( + dataset, + anchor_timestamp, + context_start_time, + context_end_time, + match_fields, + additional_filter, + limit, + ), + start_time: start_time.to_string(), + end_time: end_time.to_string(), + send_null: false, + reverse_records: false, + } +} + +#[derive(Debug, Clone, Copy)] +enum LogContextSeekDirection { + Newer, + Older, +} + +fn build_log_context_anchor_and_older_query( + dataset: &str, + anchor_timestamp: DateTime, + context_start_time: DateTime, + context_end_time: DateTime, + match_fields: &[LogContextMatchField], + additional_filter: Option<&str>, + limit: u64, +) -> String { + let anchor_cursor = LogContextCursor { + timestamp: anchor_timestamp, + match_field: match_fields[0].clone(), + }; + let scope_filter = + build_log_context_scope_filter(context_start_time, context_end_time, additional_filter); + let predicate = build_log_context_anchor_and_older_predicate(&anchor_cursor); + let order_by = build_log_context_order_by(match_fields); + let dataset = quote_sql_identifier(dataset); + + format!( + "SELECT * FROM {dataset} WHERE ({scope_filter}) AND ({predicate}) {order_by} LIMIT {limit}" + ) +} + +#[allow(clippy::too_many_arguments)] +fn build_log_context_neighbor_query( + dataset: &str, + context_start_time: DateTime, + context_end_time: DateTime, + cursor: &LogContextCursor, + direction: LogContextSeekDirection, + match_fields: &[LogContextMatchField], + additional_filter: Option<&str>, + limit: Option, + offset: Option, +) -> String { + let scope_filter = + build_log_context_scope_filter(context_start_time, context_end_time, additional_filter); + let predicate = build_log_context_cursor_predicate(cursor, direction); + let order_by = match direction { + LogContextSeekDirection::Newer => build_log_context_reverse_order_by(match_fields), + LogContextSeekDirection::Older => build_log_context_order_by(match_fields), + }; + let dataset = quote_sql_identifier(dataset); + let limit_clause = limit + .map(|limit| format!(" LIMIT {limit}")) + .unwrap_or_default(); + let offset_clause = offset + .map(|offset| format!(" OFFSET {offset}")) + .unwrap_or_default(); + + format!( + "SELECT * FROM {dataset} WHERE ({scope_filter}) AND ({predicate}) {order_by}{limit_clause}{offset_clause}" + ) +} + +#[allow(clippy::too_many_arguments)] +fn build_log_context_cursor_query_payload( + dataset: &str, + context_start_time: DateTime, + context_end_time: DateTime, + match_fields: &[LogContextMatchField], + additional_filter: Option<&str>, + start_time: &str, + end_time: &str, + limit: u64, + cursor: &LogContextCursor, + direction: LogContextSeekDirection, +) -> LogContextQueryPayload { + let reverse_records = matches!(direction, LogContextSeekDirection::Newer); + LogContextQueryPayload { + query: build_log_context_neighbor_query( + dataset, + context_start_time, + context_end_time, + cursor, + direction, + match_fields, + additional_filter, + Some(limit), + Some(0), + ), + start_time: start_time.to_string(), + end_time: end_time.to_string(), + send_null: false, + reverse_records, + } +} + +#[allow(clippy::too_many_arguments)] +fn build_log_context_cursor_queries( + dataset: &str, + context_start_time: DateTime, + context_end_time: DateTime, + match_fields: &[LogContextMatchField], + additional_filter: Option<&str>, + start_time: &str, + end_time: &str, + limit: u64, + records: &[Value], +) -> Result { + let previous = records + .first() + .map(|record| log_context_cursor_from_record(record, &match_fields[0])) + .transpose()? + .map(|cursor| { + build_log_context_cursor_query_payload( + dataset, + context_start_time, + context_end_time, + match_fields, + additional_filter, + start_time, + end_time, + limit, + &cursor, + LogContextSeekDirection::Newer, + ) + }); + let next = records + .last() + .map(|record| log_context_cursor_from_record(record, &match_fields[0])) + .transpose()? + .map(|cursor| { + build_log_context_cursor_query_payload( + dataset, + context_start_time, + context_end_time, + match_fields, + additional_filter, + start_time, + end_time, + limit, + &cursor, + LogContextSeekDirection::Older, + ) + }); + + Ok(LogContextQueries { previous, next }) +} + +fn build_log_context_records_window( + newer_records: Vec, + older_records: Vec, + page_size: u64, +) -> Result<(Vec, u64), QueryError> { + if older_records.is_empty() { + return Err(QueryError::CustomError( + "Anchor row was not returned by the context query".to_string(), + )); + } + + let page_size = usize::try_from(page_size) + .map_err(|_| QueryError::CustomError("pageSize is too large".to_string()))?; + let (newer_take, older_take) = + log_context_window_counts(newer_records.len(), older_records.len(), page_size); + let mut newer_records = newer_records + .into_iter() + .take(newer_take) + .collect::>(); + newer_records.reverse(); + + let anchor_index = newer_records.len() as u64; + let mut records = newer_records; + records.extend(older_records.into_iter().take(older_take)); + + Ok((records, anchor_index)) +} + +fn log_context_window_counts( + newer_len: usize, + older_len: usize, + page_size: usize, +) -> (usize, usize) { + let target_newer = page_size / 2; + let mut newer_take = newer_len.min(target_newer); + let older_take = older_len.min(page_size.saturating_sub(newer_take)); + if newer_take + older_take < page_size { + newer_take = newer_len.min(newer_take + page_size - newer_take - older_take); + } + + (newer_take, older_take) +} + +fn build_log_context_scope_filter( + context_start_time: DateTime, + context_end_time: DateTime, + additional_filter: Option<&str>, +) -> String { + let timestamp_column = quote_sql_identifier(DEFAULT_TIMESTAMP_KEY); + let time_filter = format!( + "{timestamp_column} >= {} AND {timestamp_column} < {}", + timestamp_sql_literal(context_start_time), + timestamp_sql_literal(context_end_time) + ); + + match additional_filter { + Some(filter) => format!("({time_filter}) AND {filter}"), + None => time_filter, + } +} + +fn build_log_context_anchor_match_predicate( + anchor_timestamp: DateTime, + match_fields: &[LogContextMatchField], +) -> String { + let mut predicates = vec![format!( + "{} = {}", + quote_sql_identifier(DEFAULT_TIMESTAMP_KEY), + timestamp_sql_literal(anchor_timestamp) + )]; + + predicates.extend(match_fields.iter().map(|field| { + format!( + "{} = {}", + quote_sql_identifier(&field.name), + quote_sql_string_literal(&field.value) + ) + })); + + predicates.join(" AND ") +} + +fn build_log_context_anchor_and_older_predicate(cursor: &LogContextCursor) -> String { + let timestamp_column = quote_sql_identifier(DEFAULT_TIMESTAMP_KEY); + let timestamp = timestamp_sql_literal(cursor.timestamp); + let field_column = quote_sql_identifier(&cursor.match_field.name); + let field_value = quote_sql_string_literal(&cursor.match_field.value); + + format!( + "{timestamp_column} < {timestamp} OR ({timestamp_column} = {timestamp} AND {field_column} >= {field_value})" + ) +} + +fn build_log_context_cursor_predicate( + cursor: &LogContextCursor, + direction: LogContextSeekDirection, +) -> String { + let timestamp_column = quote_sql_identifier(DEFAULT_TIMESTAMP_KEY); + let timestamp = timestamp_sql_literal(cursor.timestamp); + let field_column = quote_sql_identifier(&cursor.match_field.name); + let field_value = quote_sql_string_literal(&cursor.match_field.value); + + match direction { + LogContextSeekDirection::Newer => format!( + "{timestamp_column} > {timestamp} OR ({timestamp_column} = {timestamp} AND {field_column} < {field_value})" + ), + LogContextSeekDirection::Older => format!( + "{timestamp_column} < {timestamp} OR ({timestamp_column} = {timestamp} AND {field_column} > {field_value})" + ), + } +} + +fn build_log_context_order_by(match_fields: &[LogContextMatchField]) -> String { + let mut order_columns = vec![format!( + "{} DESC", + quote_sql_identifier(DEFAULT_TIMESTAMP_KEY) + )]; + order_columns.extend( + match_fields + .iter() + .map(|field| format!("{} ASC", quote_sql_identifier(&field.name))), + ); + + format!("ORDER BY {}", order_columns.into_iter().join(", ")) +} + +fn build_log_context_reverse_order_by(match_fields: &[LogContextMatchField]) -> String { + let mut order_columns = vec![format!( + "{} ASC", + quote_sql_identifier(DEFAULT_TIMESTAMP_KEY) + )]; + order_columns.extend( + match_fields + .iter() + .map(|field| format!("{} DESC", quote_sql_identifier(&field.name))), + ); + + format!("ORDER BY {}", order_columns.into_iter().join(", ")) +} + +#[tracing::instrument( + name = "query_context.execute_anchor_count", + skip(sql, authorized_datasets, tenant_id), + fields( + tenant = tracing::field::Empty, + start_time = %start_time, + end_time = %end_time, + sql_len = sql.len(), + authorized_dataset_count = authorized_datasets.len() + ) +)] +async fn execute_log_context_anchor_count( + sql: &str, + authorized_datasets: &[String], + start_time: &str, + end_time: &str, + tenant_id: &Option, +) -> Result { + Span::current().record("tenant", tracing::field::debug(tenant_id)); + debug!("query context anchor count query starting"); + + let query_request = Query { + query: sql.to_string(), + start_time: start_time.to_string(), + end_time: end_time.to_string(), + send_null: false, + fields: false, + streaming: false, + filter_tags: None, + }; + + let (records, _) = + get_records_and_fields_for_authorized_query(&query_request, authorized_datasets, tenant_id) + .await?; + let records = records.unwrap_or_default(); + let rows = record_batches_to_json(&records)?; + let row = rows + .first() + .ok_or_else(|| QueryError::CustomError("No anchor count returned".to_string()))?; + let duplicate_anchor_count = json_u64_field(row, "duplicate_anchor_count")?; + debug!( + duplicate_anchor_count, + "query context anchor count query completed" + ); + + Ok(duplicate_anchor_count) +} + +#[tracing::instrument( + name = "query_context.execute_rows", + skip(payload, authorized_datasets, tenant_id), + fields( + tenant = tracing::field::Empty, + start_time = %payload.start_time, + end_time = %payload.end_time, + sql_len = payload.query.len(), + authorized_dataset_count = authorized_datasets.len() + ) +)] +async fn execute_log_context_rows( + payload: &LogContextQueryPayload, + authorized_datasets: &[String], + tenant_id: &Option, +) -> Result, QueryError> { + Span::current().record("tenant", tracing::field::debug(tenant_id)); + debug!("query context row query starting"); + + let query_request = Query { + query: payload.query.clone(), + start_time: payload.start_time.clone(), + end_time: payload.end_time.clone(), + send_null: payload.send_null, + fields: false, + streaming: false, + filter_tags: None, + }; + + let (records, _) = + get_records_and_fields_for_authorized_query(&query_request, authorized_datasets, tenant_id) + .await?; + let records = records.unwrap_or_default(); + let records: Vec = record_batches_to_json(&records)? + .into_iter() + .map(Value::Object) + .collect(); + + debug!( + record_count = records.len(), + "query context row query completed" + ); + + Ok(records) +} + +fn json_u64_field(row: &Map, field: &str) -> Result { + let value = row + .get(field) + .ok_or_else(|| QueryError::CustomError(format!("Missing field '{field}'")))?; + + match value { + Value::Number(number) => { + if let Some(value) = number.as_u64() { + return Ok(value); + } + if let Some(value) = number.as_i64() + && value >= 0 + { + return Ok(value as u64); + } + Err(QueryError::CustomError(format!( + "Field '{field}' is not a valid unsigned integer" + ))) + } + Value::Null => Ok(0), + _ => Err(QueryError::CustomError(format!( + "Field '{field}' is not a valid unsigned integer" + ))), + } +} + +fn log_context_cursor_from_record( + record: &Value, + match_field: &LogContextMatchField, +) -> Result { + let record = record.as_object().ok_or_else(|| { + QueryError::CustomError("Log context record is not an object".to_string()) + })?; + let timestamp = record + .get(DEFAULT_TIMESTAMP_KEY) + .and_then(Value::as_str) + .ok_or_else(|| { + QueryError::CustomError(format!("Missing field '{DEFAULT_TIMESTAMP_KEY}' in record")) + }) + .and_then(parse_log_context_timestamp)?; + let value = record + .get(&match_field.name) + .and_then(Value::as_str) + .ok_or_else(|| { + QueryError::CustomError(format!("Missing field '{}' in record", match_field.name)) + })? + .to_string(); + + Ok(LogContextCursor { + timestamp, + match_field: LogContextMatchField { + name: match_field.name.clone(), + value, + }, + }) +} + +fn format_log_context_api_time(timestamp: DateTime) -> String { + timestamp.to_rfc3339_opts(SecondsFormat::Secs, true) +} + +fn timestamp_sql_literal(timestamp: DateTime) -> String { + format!( + "TIMESTAMP '{}'", + timestamp.naive_utc().format("%Y-%m-%d %H:%M:%S%.3f") + ) +} + +fn quote_sql_identifier(identifier: &str) -> String { + format!("\"{}\"", identifier.replace('"', "\"\"")) +} + +fn quote_sql_string_literal(value: &str) -> String { + format!("'{}'", value.replace('\'', "''")) +} + +#[cfg(test)] +mod tests { + use super::*; + use arrow_schema::{DataType, Field, Schema, TimeUnit}; + + fn schema_with(fields: &[&str]) -> Schema { + Schema::new( + fields + .iter() + .map(|field| { + if *field == DEFAULT_TIMESTAMP_KEY { + Field::new( + *field, + DataType::Timestamp(TimeUnit::Millisecond, None), + true, + ) + } else { + Field::new(*field, DataType::Utf8, true) + } + }) + .collect::>(), + ) + } + + fn anchor_timestamp() -> DateTime { + parse_log_context_timestamp("2026-06-17T10:15:42.123456Z").unwrap() + } + + #[test] + fn log_context_timestamp_parser_accepts_request_and_query_result_formats() { + assert_eq!( + parse_log_context_timestamp("2026-06-18T07:39:59.995Z").unwrap(), + parse_log_context_timestamp("2026-06-18T07:39:59.995").unwrap() + ); + assert_eq!( + parse_log_context_timestamp("2026-06-18T07:39:59.995Z").unwrap(), + parse_log_context_timestamp("2026-06-18 07:39:59.995").unwrap() + ); + } + + #[test] + fn log_context_window_counts_center_anchor_and_fill_edges() { + assert_eq!(log_context_window_counts(250, 250, 500), (250, 250)); + assert_eq!(log_context_window_counts(10, 1000, 500), (10, 490)); + assert_eq!(log_context_window_counts(1000, 10, 500), (490, 10)); + assert_eq!(log_context_window_counts(10, 10, 500), (10, 10)); + } + + #[test] + fn log_context_page_size_defaults_and_clamps() { + assert_eq!( + normalize_log_context_page_size(None).unwrap(), + DEFAULT_LOG_CONTEXT_PAGE_SIZE + ); + assert_eq!(normalize_log_context_page_size(Some(1)).unwrap(), 1); + assert_eq!( + normalize_log_context_page_size(Some(DEFAULT_LOG_CONTEXT_PAGE_SIZE + 1)).unwrap(), + DEFAULT_LOG_CONTEXT_PAGE_SIZE + ); + assert!(normalize_log_context_page_size(Some(0)).is_err()); + } + + #[test] + fn log_context_bounds_apply_window_and_truncate_to_minute() { + let (start, end) = log_context_bounds(anchor_timestamp(), "1m").unwrap(); + assert_eq!(format_log_context_api_time(start), "2026-06-17T10:14:00Z"); + assert_eq!(format_log_context_api_time(end), "2026-06-17T10:16:00Z"); + + let (start, end) = log_context_bounds(anchor_timestamp(), "5s").unwrap(); + assert_eq!(format_log_context_api_time(start), "2026-06-17T10:15:00Z"); + assert_eq!(format_log_context_api_time(end), "2026-06-17T10:16:00Z"); + } + + #[test] + fn log_context_match_fields_accept_exactly_one_anchor_field() { + let schema = schema_with(&[DEFAULT_TIMESTAMP_KEY, "body", "log", "message"]); + + let log_fields = normalize_log_context_match_fields( + &Some("log value".to_string()), + &None, + &None, + &schema, + ) + .unwrap(); + assert_eq!( + log_fields, + vec![LogContextMatchField { + name: "log".to_string(), + value: "log value".to_string(), + }] + ); + + let body_fields = normalize_log_context_match_fields( + &None, + &Some("body value".to_string()), + &None, + &schema, + ) + .unwrap(); + assert_eq!( + body_fields, + vec![LogContextMatchField { + name: "body".to_string(), + value: "body value".to_string(), + }] + ); + + let message_fields = normalize_log_context_match_fields( + &None, + &None, + &Some("message value".to_string()), + &schema, + ) + .unwrap(); + assert_eq!( + message_fields, + vec![LogContextMatchField { + name: "message".to_string(), + value: "message value".to_string(), + }] + ); + } + + #[test] + fn log_context_match_fields_reject_missing_multiple_and_absent_schema_fields() { + let full_schema = schema_with(&[DEFAULT_TIMESTAMP_KEY, "body", "log", "message"]); + let log_only_schema = schema_with(&[DEFAULT_TIMESTAMP_KEY, "log"]); + + assert!(normalize_log_context_match_fields(&None, &None, &None, &full_schema).is_err()); + assert!( + normalize_log_context_match_fields( + &Some("log value".to_string()), + &Some("body value".to_string()), + &None, + &full_schema, + ) + .is_err() + ); + assert!( + normalize_log_context_match_fields( + &None, + &Some("body value".to_string()), + &None, + &log_only_schema + ) + .is_err() + ); + + let numeric_message_schema = Schema::new(vec![ + Field::new( + DEFAULT_TIMESTAMP_KEY, + DataType::Timestamp(TimeUnit::Millisecond, None), + true, + ), + Field::new("message", DataType::Int64, true), + ]); + assert!( + normalize_log_context_match_fields( + &None, + &None, + &Some("message value".to_string()), + &numeric_message_schema, + ) + .is_err() + ); + } + + #[test] + fn log_context_anchor_count_sql_only_counts_anchor_duplicates() { + let anchor = anchor_timestamp(); + let (start, end) = log_context_bounds(anchor, "1m").unwrap(); + let match_fields = vec![LogContextMatchField { + name: "message".to_string(), + value: "alpha".to_string(), + }]; + + let sql = + build_log_context_anchor_count_query("logs", anchor, start, end, &match_fields, None); + + assert!(sql.contains("COUNT(*) AS duplicate_anchor_count")); + assert!(!sql.contains("rows_before")); + assert!(!sql.contains("COUNT(*) AS total")); + assert!(sql.contains("\"p_timestamp\" = TIMESTAMP '2026-06-17 10:15:42.123'")); + assert!(sql.contains("\"message\" = 'alpha'")); + assert!(!sql.contains("\"log\" = 'alpha' AND \"body\"")); + } + + #[test] + fn log_context_cursor_sql_builds_previous_and_next_pages() { + let anchor = anchor_timestamp(); + let (start, end) = log_context_bounds(anchor, "1m").unwrap(); + let match_fields = vec![LogContextMatchField { + name: "message".to_string(), + value: "alpha".to_string(), + }]; + let cursor = LogContextCursor { + timestamp: anchor, + match_field: match_fields[0].clone(), + }; + + let previous_sql = build_log_context_neighbor_query( + "logs", + start, + end, + &cursor, + LogContextSeekDirection::Newer, + &match_fields, + None, + None, + None, + ); + assert!(previous_sql.contains("\"p_timestamp\" > TIMESTAMP '2026-06-17 10:15:42.123'")); + assert!(previous_sql.contains("\"message\" < 'alpha'")); + assert!(previous_sql.ends_with("ORDER BY \"p_timestamp\" ASC, \"message\" DESC")); + assert!(!previous_sql.contains("LIMIT")); + assert!(!previous_sql.contains("OFFSET")); + + let next_sql = build_log_context_neighbor_query( + "logs", + start, + end, + &cursor, + LogContextSeekDirection::Older, + &match_fields, + None, + None, + None, + ); + assert!(next_sql.contains("\"p_timestamp\" < TIMESTAMP '2026-06-17 10:15:42.123'")); + assert!(next_sql.contains("\"message\" > 'alpha'")); + assert!(next_sql.ends_with("ORDER BY \"p_timestamp\" DESC, \"message\" ASC")); + assert!(!next_sql.contains("LIMIT")); + assert!(!next_sql.contains("OFFSET")); + + let previous_payload = build_log_context_cursor_query_payload( + "logs", + start, + end, + &match_fields, + None, + "2026-06-17T10:14:00Z", + "2026-06-17T10:16:00Z", + 500, + &cursor, + LogContextSeekDirection::Newer, + ); + assert!(previous_payload.query.ends_with("LIMIT 500 OFFSET 0")); + assert!(previous_payload.reverse_records); + + let next_payload = build_log_context_cursor_query_payload( + "logs", + start, + end, + &match_fields, + None, + "2026-06-17T10:14:00Z", + "2026-06-17T10:16:00Z", + 500, + &cursor, + LogContextSeekDirection::Older, + ); + assert!(next_payload.query.ends_with("LIMIT 500 OFFSET 0")); + assert!(!next_payload.reverse_records); + } +} From 80ead6bd4d92d0e2fc4a265c515efc7be90d1ebb Mon Sep 17 00:00:00 2001 From: Nitish Tiwari Date: Fri, 19 Jun 2026 21:37:35 +0530 Subject: [PATCH 38/47] chore: update to latest pai helm chart v0.3.0 (#1692) --- helm-releases/pai-0.3.0.tgz | Bin 0 -> 6412 bytes index.yaml | 196 ++++++++++++++++++++---------------- 2 files changed, 107 insertions(+), 89 deletions(-) create mode 100644 helm-releases/pai-0.3.0.tgz diff --git a/helm-releases/pai-0.3.0.tgz b/helm-releases/pai-0.3.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..a6985ee119a666bdd9f7e595265cfd54d6ddf2ac GIT binary patch literal 6412 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 aERW@}JeGg6@;?Cp0RR68?t_*9mH+@;{FdSX literal 0 HcmV?d00001 diff --git a/index.yaml b/index.yaml index 1f90da482..164da5829 100644 --- a/index.yaml +++ b/index.yaml @@ -1,9 +1,27 @@ apiVersion: v1 entries: pai: + - apiVersion: v2 + appVersion: 0.3.0 + created: "2026-06-19T20:59:06.159116+05:30" + description: Parseable Auto Instrumentation (PAI) operator for Kubernetes + digest: 210963f69aacb386e39bebe516727a6f5d310176e4c06daca73552398278dd4c + home: https://github.com/parseablehq/pai + keywords: + - observability + - opentelemetry + - auto-instrumentation + - parseable + maintainers: + - name: Parseable + name: pai + type: application + urls: + - https://charts.parseable.com/helm-releases/pai-0.3.0.tgz + version: 0.3.0 - apiVersion: v2 appVersion: 0.2.0 - created: "2026-06-11T00:47:35.334268+08:00" + created: "2026-06-19T20:59:06.158919+05:30" description: Parseable Auto Instrumentation (PAI) operator for Kubernetes digest: fd04518e8fc9e25d3fa876971a38b4c65005a207ba9440ae8ce981cd070646d9 home: https://github.com/parseablehq/pai @@ -21,7 +39,7 @@ entries: version: 0.2.0 - apiVersion: v2 appVersion: 0.1.0 - created: "2026-06-11T00:47:35.334125+08:00" + created: "2026-06-19T20:59:06.158577+05:30" description: Parseable Auto Instrumentation (PAI) operator for Kubernetes digest: 9443e75bef96a424fd5063ddd02cee18e6050207d73e505f66be467287c9f7f7 home: https://github.com/parseablehq/pai @@ -40,7 +58,7 @@ entries: parseable: - apiVersion: v2 appVersion: v2.8.0 - created: "2026-06-11T00:47:35.451186+08:00" + created: "2026-06-19T20:59:06.30473+05:30" dependencies: - condition: vector.enabled name: vector @@ -65,7 +83,7 @@ entries: version: 2.8.3 - apiVersion: v2 appVersion: v2.8.0 - created: "2026-06-11T00:47:35.449456+08:00" + created: "2026-06-19T20:59:06.303052+05:30" dependencies: - condition: vector.enabled name: vector @@ -90,7 +108,7 @@ entries: version: 2.8.2 - apiVersion: v2 appVersion: v2.8.1 - created: "2026-06-11T00:47:35.447523+08:00" + created: "2026-06-19T20:59:06.30071+05:30" dependencies: - condition: vector.enabled name: vector @@ -115,7 +133,7 @@ entries: version: 2.8.1 - apiVersion: v2 appVersion: v2.8.0 - created: "2026-06-11T00:47:35.445865+08:00" + created: "2026-06-19T20:59:06.29864+05:30" dependencies: - condition: vector.enabled name: vector @@ -140,7 +158,7 @@ entries: version: 2.8.0 - apiVersion: v2 appVersion: v2.7.2 - created: "2026-06-11T00:47:35.443868+08:00" + created: "2026-06-19T20:59:06.296428+05:30" dependencies: - condition: vector.enabled name: vector @@ -165,7 +183,7 @@ entries: version: 2.7.2 - apiVersion: v2 appVersion: v2.7.1 - created: "2026-06-11T00:47:35.442097+08:00" + created: "2026-06-19T20:59:06.294163+05:30" dependencies: - condition: vector.enabled name: vector @@ -190,7 +208,7 @@ entries: version: 2.7.1 - apiVersion: v2 appVersion: v2.6.6 - created: "2026-06-11T00:47:35.439976+08:00" + created: "2026-06-19T20:59:06.292157+05:30" dependencies: - condition: vector.enabled name: vector @@ -215,7 +233,7 @@ entries: version: 2.6.6 - apiVersion: v2 appVersion: v2.6.5 - created: "2026-06-11T00:47:35.43822+08:00" + created: "2026-06-19T20:59:06.289611+05:30" dependencies: - condition: vector.enabled name: vector @@ -240,7 +258,7 @@ entries: version: 2.6.5 - apiVersion: v2 appVersion: v2.5.13 - created: "2026-06-11T00:47:35.427212+08:00" + created: "2026-06-19T20:59:06.276685+05:30" dependencies: - condition: vector.enabled name: vector @@ -265,7 +283,7 @@ entries: version: 2.5.13 - apiVersion: v2 appVersion: v2.5.7 - created: "2026-06-11T00:47:35.436261+08:00" + created: "2026-06-19T20:59:06.287557+05:30" dependencies: - condition: vector.enabled name: vector @@ -290,7 +308,7 @@ entries: version: 2.5.7 - apiVersion: v2 appVersion: v2.5.6 - created: "2026-06-11T00:47:35.434632+08:00" + created: "2026-06-19T20:59:06.28513+05:30" dependencies: - condition: vector.enabled name: vector @@ -315,7 +333,7 @@ entries: version: 2.5.6 - apiVersion: v2 appVersion: v2.5.5 - created: "2026-06-11T00:47:35.43296+08:00" + created: "2026-06-19T20:59:06.283134+05:30" dependencies: - condition: vector.enabled name: vector @@ -340,7 +358,7 @@ entries: version: 2.5.5 - apiVersion: v2 appVersion: v2.5.4 - created: "2026-06-11T00:47:35.430915+08:00" + created: "2026-06-19T20:59:06.280708+05:30" dependencies: - condition: vector.enabled name: vector @@ -364,7 +382,7 @@ entries: version: 2.5.4 - apiVersion: v2 appVersion: v2.5.3 - created: "2026-06-11T00:47:35.429297+08:00" + created: "2026-06-19T20:59:06.278648+05:30" dependencies: - condition: vector.enabled name: vector @@ -388,7 +406,7 @@ entries: version: 2.5.3 - apiVersion: v2 appVersion: v2.4.0 - created: "2026-06-11T00:47:35.425463+08:00" + created: "2026-06-19T20:59:06.274165+05:30" dependencies: - condition: vector.enabled name: vector @@ -412,7 +430,7 @@ entries: version: 2.4.0 - apiVersion: v2 appVersion: v2.3.3 - created: "2026-06-11T00:47:35.423789+08:00" + created: "2026-06-19T20:59:06.272243+05:30" dependencies: - condition: vector.enabled name: vector @@ -436,7 +454,7 @@ entries: version: 2.3.3 - apiVersion: v2 appVersion: v2.3.2 - created: "2026-06-11T00:47:35.421822+08:00" + created: "2026-06-19T20:59:06.269897+05:30" dependencies: - condition: vector.enabled name: vector @@ -460,7 +478,7 @@ entries: version: 2.3.2 - apiVersion: v2 appVersion: v2.3.1 - created: "2026-06-11T00:47:35.420155+08:00" + created: "2026-06-19T20:59:06.26786+05:30" dependencies: - condition: vector.enabled name: vector @@ -484,7 +502,7 @@ entries: version: 2.3.1 - apiVersion: v2 appVersion: v2.3.0 - created: "2026-06-11T00:47:35.418125+08:00" + created: "2026-06-19T20:59:06.265951+05:30" dependencies: - condition: vector.enabled name: vector @@ -508,7 +526,7 @@ entries: version: 2.3.0 - apiVersion: v2 appVersion: v2.1.0 - created: "2026-06-11T00:47:35.416474+08:00" + created: "2026-06-19T20:59:06.263393+05:30" dependencies: - condition: vector.enabled name: vector @@ -532,7 +550,7 @@ entries: version: 2.1.0 - apiVersion: v2 appVersion: v1.7.5 - created: "2026-06-11T00:47:35.414676+08:00" + created: "2026-06-19T20:59:06.261357+05:30" dependencies: - condition: vector.enabled name: vector @@ -556,7 +574,7 @@ entries: version: 2.0.0 - apiVersion: v2 appVersion: v1.7.5 - created: "2026-06-11T00:47:35.412841+08:00" + created: "2026-06-19T20:59:06.258947+05:30" dependencies: - condition: vector.enabled name: vector @@ -580,7 +598,7 @@ entries: version: 1.7.5 - apiVersion: v2 appVersion: v1.7.3 - created: "2026-06-11T00:47:35.411224+08:00" + created: "2026-06-19T20:59:06.256892+05:30" dependencies: - condition: vector.enabled name: vector @@ -604,7 +622,7 @@ entries: version: 1.7.3 - apiVersion: v2 appVersion: v1.7.2 - created: "2026-06-11T00:47:35.409166+08:00" + created: "2026-06-19T20:59:06.254358+05:30" dependencies: - condition: vector.enabled name: vector @@ -628,7 +646,7 @@ entries: version: 1.7.2 - apiVersion: v2 appVersion: v1.7.1 - created: "2026-06-11T00:47:35.407443+08:00" + created: "2026-06-19T20:59:06.252378+05:30" dependencies: - condition: vector.enabled name: vector @@ -652,7 +670,7 @@ entries: version: 1.7.1 - apiVersion: v2 appVersion: v1.7.0 - created: "2026-06-11T00:47:35.405303+08:00" + created: "2026-06-19T20:59:06.250136+05:30" dependencies: - condition: vector.enabled name: vector @@ -675,7 +693,7 @@ entries: version: 1.7.0 - apiVersion: v2 appVersion: v1.6.3 - created: "2026-06-11T00:47:35.403607+08:00" + created: "2026-06-19T20:59:06.248071+05:30" dependencies: - condition: vector.enabled name: vector @@ -698,7 +716,7 @@ entries: version: 1.6.8 - apiVersion: v2 appVersion: v1.6.3 - created: "2026-06-11T00:47:35.401972+08:00" + created: "2026-06-19T20:59:06.246002+05:30" dependencies: - condition: vector.enabled name: vector @@ -721,7 +739,7 @@ entries: version: 1.6.7 - apiVersion: v2 appVersion: v1.6.3 - created: "2026-06-11T00:47:35.400047+08:00" + created: "2026-06-19T20:59:06.243577+05:30" dependencies: - condition: vector.enabled name: vector @@ -744,7 +762,7 @@ entries: version: 1.6.6 - apiVersion: v2 appVersion: v1.6.3 - created: "2026-06-11T00:47:35.398534+08:00" + created: "2026-06-19T20:59:06.241598+05:30" dependencies: - condition: vector.enabled name: vector @@ -767,7 +785,7 @@ entries: version: 1.6.5 - apiVersion: v2 appVersion: v1.6.3 - created: "2026-06-11T00:47:35.396737+08:00" + created: "2026-06-19T20:59:06.239187+05:30" dependencies: - condition: vector.enabled name: vector @@ -790,7 +808,7 @@ entries: version: 1.6.4 - apiVersion: v2 appVersion: v1.6.3 - created: "2026-06-11T00:47:35.3952+08:00" + created: "2026-06-19T20:59:06.237129+05:30" dependencies: - condition: vector.enabled name: vector @@ -813,7 +831,7 @@ entries: version: 1.6.3 - apiVersion: v2 appVersion: v1.6.2 - created: "2026-06-11T00:47:35.393619+08:00" + created: "2026-06-19T20:59:06.234711+05:30" dependencies: - condition: vector.enabled name: vector @@ -836,7 +854,7 @@ entries: version: 1.6.2 - apiVersion: v2 appVersion: v1.6.1 - created: "2026-06-11T00:47:35.391842+08:00" + created: "2026-06-19T20:59:06.232702+05:30" dependencies: - condition: vector.enabled name: vector @@ -859,7 +877,7 @@ entries: version: 1.6.1 - apiVersion: v2 appVersion: v1.6.0 - created: "2026-06-11T00:47:35.390374+08:00" + created: "2026-06-19T20:59:06.230742+05:30" dependencies: - condition: vector.enabled name: vector @@ -882,7 +900,7 @@ entries: version: 1.6.0 - apiVersion: v2 appVersion: v1.5.5 - created: "2026-06-11T00:47:35.388475+08:00" + created: "2026-06-19T20:59:06.228301+05:30" dependencies: - condition: vector.enabled name: vector @@ -905,7 +923,7 @@ entries: version: 1.5.5 - apiVersion: v2 appVersion: v1.5.4 - created: "2026-06-11T00:47:35.386943+08:00" + created: "2026-06-19T20:59:06.226408+05:30" dependencies: - condition: vector.enabled name: vector @@ -928,7 +946,7 @@ entries: version: 1.5.4 - apiVersion: v2 appVersion: v1.5.3 - created: "2026-06-11T00:47:35.385385+08:00" + created: "2026-06-19T20:59:06.224326+05:30" dependencies: - condition: vector.enabled name: vector @@ -951,7 +969,7 @@ entries: version: 1.5.3 - apiVersion: v2 appVersion: v1.5.2 - created: "2026-06-11T00:47:35.383487+08:00" + created: "2026-06-19T20:59:06.22239+05:30" dependencies: - condition: vector.enabled name: vector @@ -974,7 +992,7 @@ entries: version: 1.5.2 - apiVersion: v2 appVersion: v1.5.1 - created: "2026-06-11T00:47:35.381946+08:00" + created: "2026-06-19T20:59:06.220414+05:30" dependencies: - condition: vector.enabled name: vector @@ -997,7 +1015,7 @@ entries: version: 1.5.1 - apiVersion: v2 appVersion: v1.5.0 - created: "2026-06-11T00:47:35.379743+08:00" + created: "2026-06-19T20:59:06.217769+05:30" dependencies: - condition: vector.enabled name: vector @@ -1020,7 +1038,7 @@ entries: version: 1.5.0 - apiVersion: v2 appVersion: v1.4.0 - created: "2026-06-11T00:47:35.37826+08:00" + created: "2026-06-19T20:59:06.215815+05:30" dependencies: - condition: vector.enabled name: vector @@ -1043,7 +1061,7 @@ entries: version: 1.4.1 - apiVersion: v2 appVersion: v1.4.0 - created: "2026-06-11T00:47:35.376818+08:00" + created: "2026-06-19T20:59:06.214107+05:30" dependencies: - condition: vector.enabled name: vector @@ -1066,7 +1084,7 @@ entries: version: 1.4.0 - apiVersion: v2 appVersion: v1.3.0 - created: "2026-06-11T00:47:35.374898+08:00" + created: "2026-06-19T20:59:06.212076+05:30" dependencies: - condition: vector.enabled name: vector @@ -1089,7 +1107,7 @@ entries: version: 1.3.1 - apiVersion: v2 appVersion: v1.3.0 - created: "2026-06-11T00:47:35.373321+08:00" + created: "2026-06-19T20:59:06.210336+05:30" dependencies: - condition: vector.enabled name: vector @@ -1112,7 +1130,7 @@ entries: version: 1.3.0 - apiVersion: v2 appVersion: v1.2.0 - created: "2026-06-11T00:47:35.371428+08:00" + created: "2026-06-19T20:59:06.208591+05:30" dependencies: - condition: vector.enabled name: vector @@ -1135,7 +1153,7 @@ entries: version: 1.2.0 - apiVersion: v2 appVersion: v1.1.0 - created: "2026-06-11T00:47:35.369967+08:00" + created: "2026-06-19T20:59:06.20636+05:30" dependencies: - condition: vector.enabled name: vector @@ -1158,7 +1176,7 @@ entries: version: 1.1.0 - apiVersion: v2 appVersion: v1.0.0 - created: "2026-06-11T00:47:35.368496+08:00" + created: "2026-06-19T20:59:06.204514+05:30" dependencies: - condition: vector.enabled name: vector @@ -1181,7 +1199,7 @@ entries: version: 1.0.0 - apiVersion: v2 appVersion: v0.9.0 - created: "2026-06-11T00:47:35.366647+08:00" + created: "2026-06-19T20:59:06.202418+05:30" dependencies: - condition: vector.enabled name: vector @@ -1204,7 +1222,7 @@ entries: version: 0.9.0 - apiVersion: v2 appVersion: v0.8.1 - created: "2026-06-11T00:47:35.365297+08:00" + created: "2026-06-19T20:59:06.20061+05:30" dependencies: - condition: vector.enabled name: vector @@ -1227,7 +1245,7 @@ entries: version: 0.8.1 - apiVersion: v2 appVersion: v0.8.0 - created: "2026-06-11T00:47:35.363933+08:00" + created: "2026-06-19T20:59:06.198815+05:30" dependencies: - condition: vector.enabled name: vector @@ -1250,7 +1268,7 @@ entries: version: 0.8.0 - apiVersion: v2 appVersion: v0.7.3 - created: "2026-06-11T00:47:35.361855+08:00" + created: "2026-06-19T20:59:06.196692+05:30" dependencies: - condition: vector.enabled name: vector @@ -1273,7 +1291,7 @@ entries: version: 0.7.3 - apiVersion: v2 appVersion: v0.7.2 - created: "2026-06-11T00:47:35.360371+08:00" + created: "2026-06-19T20:59:06.194932+05:30" dependencies: - condition: vector.enabled name: vector @@ -1296,7 +1314,7 @@ entries: version: 0.7.2 - apiVersion: v2 appVersion: v0.7.1 - created: "2026-06-11T00:47:35.358445+08:00" + created: "2026-06-19T20:59:06.193091+05:30" dependencies: - condition: vector.enabled name: vector @@ -1319,7 +1337,7 @@ entries: version: 0.7.1 - apiVersion: v2 appVersion: v0.7.0 - created: "2026-06-11T00:47:35.35704+08:00" + created: "2026-06-19T20:59:06.190768+05:30" dependencies: - condition: vector.enabled name: vector @@ -1342,7 +1360,7 @@ entries: version: 0.7.0 - apiVersion: v2 appVersion: v0.6.2 - created: "2026-06-11T00:47:35.355534+08:00" + created: "2026-06-19T20:59:06.188838+05:30" dependencies: - condition: vector.enabled name: vector @@ -1365,7 +1383,7 @@ entries: version: 0.6.2 - apiVersion: v2 appVersion: v0.6.1 - created: "2026-06-11T00:47:35.353619+08:00" + created: "2026-06-19T20:59:06.186974+05:30" dependencies: - condition: vector.enabled name: vector @@ -1388,7 +1406,7 @@ entries: version: 0.6.1 - apiVersion: v2 appVersion: v0.6.0 - created: "2026-06-11T00:47:35.352214+08:00" + created: "2026-06-19T20:59:06.185087+05:30" dependencies: - condition: vector.enabled name: vector @@ -1411,7 +1429,7 @@ entries: version: 0.6.0 - apiVersion: v2 appVersion: v0.5.1 - created: "2026-06-11T00:47:35.350755+08:00" + created: "2026-06-19T20:59:06.182766+05:30" dependencies: - condition: vector.enabled name: vector @@ -1434,7 +1452,7 @@ entries: version: 0.5.1 - apiVersion: v2 appVersion: v0.5.0 - created: "2026-06-11T00:47:35.348834+08:00" + created: "2026-06-19T20:59:06.180818+05:30" dependencies: - condition: vector.enabled name: vector @@ -1457,7 +1475,7 @@ entries: version: 0.5.0 - apiVersion: v2 appVersion: v0.4.4 - created: "2026-06-11T00:47:35.347435+08:00" + created: "2026-06-19T20:59:06.17848+05:30" dependencies: - condition: vector.enabled name: vector @@ -1480,7 +1498,7 @@ entries: version: 0.4.5 - apiVersion: v2 appVersion: v0.4.3 - created: "2026-06-11T00:47:35.345914+08:00" + created: "2026-06-19T20:59:06.176487+05:30" dependencies: - condition: vector.enabled name: vector @@ -1503,7 +1521,7 @@ entries: version: 0.4.4 - apiVersion: v2 appVersion: v0.4.2 - created: "2026-06-11T00:47:35.344101+08:00" + created: "2026-06-19T20:59:06.174486+05:30" dependencies: - condition: vector.enabled name: vector @@ -1526,7 +1544,7 @@ entries: version: 0.4.3 - apiVersion: v2 appVersion: v0.4.1 - created: "2026-06-11T00:47:35.342695+08:00" + created: "2026-06-19T20:59:06.171958+05:30" dependencies: - condition: vector.enabled name: vector @@ -1549,7 +1567,7 @@ entries: version: 0.4.2 - apiVersion: v2 appVersion: v0.4.0 - created: "2026-06-11T00:47:35.341191+08:00" + created: "2026-06-19T20:59:06.16998+05:30" dependencies: - condition: vector.enabled name: vector @@ -1572,7 +1590,7 @@ entries: version: 0.4.1 - apiVersion: v2 appVersion: v0.4.0 - created: "2026-06-11T00:47:35.339193+08:00" + created: "2026-06-19T20:59:06.167889+05:30" dependencies: - condition: vector.enabled name: vector @@ -1595,7 +1613,7 @@ entries: version: 0.4.0 - apiVersion: v2 appVersion: v0.3.1 - created: "2026-06-11T00:47:35.3377+08:00" + created: "2026-06-19T20:59:06.165031+05:30" dependencies: - condition: vector.enabled name: vector @@ -1618,7 +1636,7 @@ entries: version: 0.3.1 - apiVersion: v2 appVersion: v0.3.0 - created: "2026-06-11T00:47:35.336287+08:00" + created: "2026-06-19T20:59:06.162998+05:30" description: Helm chart for Parseable Server digest: ff30739229b727dc637f62fd4481c886a6080ce4556bae10cafe7642ddcfd937 name: parseable @@ -1628,7 +1646,7 @@ entries: version: 0.3.0 - apiVersion: v2 appVersion: v0.2.2 - created: "2026-06-11T00:47:35.336106+08:00" + created: "2026-06-19T20:59:06.16276+05:30" description: Helm chart for Parseable Server digest: 477d0dc2f0c07d4f4c32e105d4bdd70c71113add5c2a75ac5f1cb42aa0276db7 name: parseable @@ -1638,7 +1656,7 @@ entries: version: 0.2.2 - apiVersion: v2 appVersion: v0.2.1 - created: "2026-06-11T00:47:35.33558+08:00" + created: "2026-06-19T20:59:06.162465+05:30" description: Helm chart for Parseable Server digest: 84826fcd1b4c579f301569f43b0309c07e8082bad76f5cdd25f86e86ca2e8192 name: parseable @@ -1648,7 +1666,7 @@ entries: version: 0.2.1 - apiVersion: v2 appVersion: v0.2.0 - created: "2026-06-11T00:47:35.33538+08:00" + created: "2026-06-19T20:59:06.162135+05:30" description: Helm chart for Parseable Server digest: 7a759f7f9809f3935cba685e904c021a0b645f217f4e45b9be185900c467edff name: parseable @@ -1658,7 +1676,7 @@ entries: version: 0.2.0 - apiVersion: v2 appVersion: v0.1.1 - created: "2026-06-11T00:47:35.335254+08:00" + created: "2026-06-19T20:59:06.161182+05:30" description: Helm chart for Parseable Server digest: 37993cf392f662ec7b1fbfc9a2ba00ec906d98723e38f3c91ff1daca97c3d0b3 name: parseable @@ -1668,7 +1686,7 @@ entries: version: 0.1.1 - apiVersion: v2 appVersion: v0.1.0 - created: "2026-06-11T00:47:35.335125+08:00" + created: "2026-06-19T20:59:06.160953+05:30" description: Helm chart for Parseable Server digest: 1d580d072af8d6b1ebcbfee31c2e16c907d08db754780f913b5f0032b403789b name: parseable @@ -1678,7 +1696,7 @@ entries: version: 0.1.0 - apiVersion: v2 appVersion: v0.0.8 - created: "2026-06-11T00:47:35.334996+08:00" + created: "2026-06-19T20:59:06.160689+05:30" description: Helm chart for Parseable Server digest: c805254ffa634f96ecec448bcfff9973339aa9487dd8199b21b17b79a4de9345 name: parseable @@ -1688,7 +1706,7 @@ entries: version: 0.0.8 - apiVersion: v2 appVersion: v0.0.7 - created: "2026-06-11T00:47:35.334882+08:00" + created: "2026-06-19T20:59:06.160426+05:30" description: Helm chart for Parseable Server digest: c591f617ed1fe820bb2c72a4c976a78126f1d1095d552daa07c4700f46c4708a name: parseable @@ -1698,7 +1716,7 @@ entries: version: 0.0.7 - apiVersion: v2 appVersion: v0.0.6 - created: "2026-06-11T00:47:35.334754+08:00" + created: "2026-06-19T20:59:06.160207+05:30" description: Helm chart for Parseable Server digest: f9ae56a6fcd6a59e7bee0436200ddbedeb74ade6073deb435b8fcbaf08dda795 name: parseable @@ -1708,7 +1726,7 @@ entries: version: 0.0.6 - apiVersion: v2 appVersion: v0.0.5 - created: "2026-06-11T00:47:35.334637+08:00" + created: "2026-06-19T20:59:06.159978+05:30" description: Helm chart for Parseable Server digest: 4d6b08a064fba36e16feeb820b77e1e8e60fb6de48dbf7ec8410d03d10c26ad0 name: parseable @@ -1718,7 +1736,7 @@ entries: version: 0.0.5 - apiVersion: v2 appVersion: v0.0.2 - created: "2026-06-11T00:47:35.334502+08:00" + created: "2026-06-19T20:59:06.15969+05:30" description: Helm chart for Parseable Server digest: 38a0a3e4c498afbbcc76ebfcb9cb598fa2ca843a53cc93b3cb4f135b85c10844 name: parseable @@ -1728,7 +1746,7 @@ entries: version: 0.0.2 - apiVersion: v2 appVersion: v0.0.1 - created: "2026-06-11T00:47:35.334379+08:00" + created: "2026-06-19T20:59:06.159458+05:30" description: Helm chart for Parseable Server digest: 1f1142db092b9620ee38bb2294ccbb1c17f807b33bf56da43816af7fe89f301e name: parseable @@ -1739,7 +1757,7 @@ entries: parseable-enterprise: - apiVersion: v2 appVersion: v2.8.0 - created: "2026-06-11T00:47:35.464421+08:00" + created: "2026-06-19T20:59:06.320973+05:30" dependencies: - condition: vector.enabled name: vector @@ -1789,7 +1807,7 @@ entries: version: 2.8.1 - apiVersion: v2 appVersion: v2.8.0 - created: "2026-06-11T00:47:35.462472+08:00" + created: "2026-06-19T20:59:06.319178+05:30" dependencies: - condition: vector.enabled name: vector @@ -1814,7 +1832,7 @@ entries: version: 2.8.0 - apiVersion: v2 appVersion: v2.7.3 - created: "2026-06-11T00:47:35.460611+08:00" + created: "2026-06-19T20:59:06.316793+05:30" dependencies: - condition: vector.enabled name: vector @@ -1839,7 +1857,7 @@ entries: version: 2.7.3 - apiVersion: v2 appVersion: v2.7.2 - created: "2026-06-11T00:47:35.45885+08:00" + created: "2026-06-19T20:59:06.314558+05:30" dependencies: - condition: vector.enabled name: vector @@ -1864,7 +1882,7 @@ entries: version: 2.7.2 - apiVersion: v2 appVersion: v2.7.1 - created: "2026-06-11T00:47:35.456778+08:00" + created: "2026-06-19T20:59:06.31234+05:30" dependencies: - condition: vector.enabled name: vector @@ -1889,7 +1907,7 @@ entries: version: 2.7.1 - apiVersion: v2 appVersion: v2.6.6 - created: "2026-06-11T00:47:35.455015+08:00" + created: "2026-06-19T20:59:06.309672+05:30" dependencies: - condition: vector.enabled name: vector @@ -1914,7 +1932,7 @@ entries: version: 2.6.7 - apiVersion: v2 appVersion: v2.6.6 - created: "2026-06-11T00:47:35.452962+08:00" + created: "2026-06-19T20:59:06.307344+05:30" dependencies: - condition: vector.enabled name: vector @@ -1937,4 +1955,4 @@ entries: urls: - https://charts.parseable.com/helm-releases/parseable-enterprise-2.6.6.tgz version: 2.6.6 -generated: "2026-06-11T00:47:35.333907+08:00" +generated: "2026-06-19T20:59:06.158077+05:30" From 8d79243ff779997739b76db00f1bb46dd6989549 Mon Sep 17 00:00:00 2001 From: parmesant Date: Mon, 22 Jun 2026 16:22:53 +0530 Subject: [PATCH 39/47] update: remove historic sync from hottier (#1694) * update: remove historic sync from hottier - Removed historic hottier sync task - Removed env vars `P_HISTORIC_PER_TICK_CAP` and `P_HOT_TIER_HISTORIC_SYNC_MINUTES` - Resolved a TOCTOU in hottier tasks addition * remove --- src/cli.rs | 20 +-- src/hottier.rs | 415 ++++++++++++------------------------------------- 2 files changed, 102 insertions(+), 333 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 3b48fe0b4..b49928d8f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -368,28 +368,10 @@ pub struct Options { env = "P_HOT_TIER_LATEST_MINUTES", value_parser = clap::value_parser!(u64).range(1..), default_value = "10", - help = "Files whose timestamp is within the last N minutes are 'latest'; rest are 'historic'." + help = "Files whose timestamp is within the last N minutes are 'latest'" )] pub hot_tier_latest_minutes: u64, - #[arg( - long = "hot-tier-per-tick-cap", - env = "P_HISTORIC_PER_TICK_CAP", - value_parser = clap::value_parser!(u32).range(10..), - default_value = "100", - help = "Maximum files to download per historic tick." - )] - pub historic_per_tick_cap: u32, - - #[arg( - long = "hot-tier-historic-sync-minutes", - env = "P_HOT_TIER_HISTORIC_SYNC_MINUTES", - value_parser = clap::value_parser!(u32).range(1..), - default_value = "5", - help = "Interval (minutes) at which the historic hot-tier sync runs." - )] - pub hot_tier_historic_sync_minutes: u32, - //TODO: remove this when smart cache is implemented #[arg( long = "index-storage-path", diff --git a/src/hottier.rs b/src/hottier.rs index 78b610738..6a0c99087 100644 --- a/src/hottier.rs +++ b/src/hottier.rs @@ -17,6 +17,8 @@ */ use datafusion::common::HashSet; +use itertools::Itertools; +use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use std::{ collections::{BTreeMap, HashMap}, io, @@ -29,7 +31,7 @@ use crate::{ catalog::manifest::{File, Manifest}, handlers::http::cluster::PMETA_STREAM_NAME, parseable::PARSEABLE, - storage::{ObjectStorageError, StreamType, field_stats::DATASET_STATS_STREAM_NAME}, + storage::{ObjectStorageError, field_stats::DATASET_STATS_STREAM_NAME}, tenants::TENANT_METADATA, utils::{extract_datetime, human_size::bytes_to_human_size}, validator::error::HotTierValidationError, @@ -38,7 +40,6 @@ use chrono::{DateTime, NaiveDate, Timelike, Utc}; use futures::{StreamExt, TryStreamExt, stream::FuturesUnordered}; use futures_util::TryFutureExt; use object_store::{ObjectStoreExt, local::LocalFileSystem}; -use once_cell::sync::OnceCell; use parquet::errors::ParquetError; use relative_path::RelativePathBuf; use std::time::Duration; @@ -56,11 +57,6 @@ pub enum HotTierMessage { pub static GLOBAL_HOTTIER: OnceLock = OnceLock::new(); -pub static HOTTIER_RUNTIME: OnceCell<( - mpsc::UnboundedSender, - mpsc::UnboundedReceiver, -)> = OnceCell::new(); - /// Floor a timestamp to the start of its minute (seconds + sub-second zeroed). /// Used to produce a stable per-tick anchor so all spans within one tick share /// the same cutoff value. @@ -92,20 +88,6 @@ pub struct StreamHotTier { /// commit, and per-date manifest writes. Downloads run outside the lock. struct StreamSyncState { sht: AsyncMutex, - /// Past-date keys (e.g. `date=2026-05-11`) whose local file count is - /// known to match the S3 manifest count. Historic phase skips fetching - /// these. Populated after a tick observes `local_count >= s3_count`. - /// In-memory only; rebuilt on restart. - completed_dates: AsyncRwLock>, -} - -/// Hot-tier sync runs in two phases. Latest pulls files newer than -/// `hot_tier_latest_minutes` ago and may evict historic to make room. -/// Historic pulls older files, runs less often, never triggers eviction. -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -enum SyncPhase { - Latest, - Historic, } pub type StreamKey = (Option, String); @@ -113,7 +95,6 @@ pub type HotTierResponse = Result<(), HotTierError>; struct StreamTasks { latest: tokio::task::JoinHandle<()>, - historic: tokio::task::JoinHandle<()>, } pub struct HotTierManager { @@ -263,7 +244,6 @@ impl HotTierManager { let sht = self.reconcile_stream(stream, tenant_id).await?; let state = Arc::new(StreamSyncState { sht: AsyncMutex::new(sht), - completed_dates: AsyncRwLock::new(HashSet::new()), }); { @@ -738,7 +718,6 @@ impl HotTierManager { let guard = self.tasks.write().await; for (streamkey, task) in guard.iter() { task.latest.abort(); - task.historic.abort(); info!("aborted hot tier tasks for- {streamkey:?}"); } } @@ -759,23 +738,19 @@ impl HotTierManager { .await; } - /// Spawn (Latest, Historic) loops for a single stream. Idempotent: + /// Spawn Latest loop for a single stream. Idempotent: /// if tasks already exist for this (tenant, stream), no-op. async fn spawn_stream_task_inner(&'static self, stream: String, tenant_id: Option) { let key: StreamKey = (tenant_id.clone(), stream.clone()); + + let mut tasks = self.tasks.write().await; + if let Some(existing) = tasks.get(&key) + && !existing.latest.is_finished() { - let tasks = self.tasks.read().await; - if let Some(existing) = tasks.get(&key) - && !existing.latest.is_finished() - && !existing.historic.is_finished() - { - return; - } + return; } let latest_interval = Duration::from_secs(30); - let historic_interval = - Duration::from_secs(PARSEABLE.options.hot_tier_historic_sync_minutes as u64 * 60); info!(stream = %stream, tenant = ?tenant_id, "spawning per-stream hot tier tasks"); @@ -788,14 +763,10 @@ impl HotTierManager { "hottier.tick", stream = %s, tenant = ?t, - phase = "latest", anchor = %anchor ); async { - if let Err(err) = self - .process_stream(s.clone(), t.clone(), SyncPhase::Latest, anchor) - .await - { + if let Err(err) = self.process_stream(s.clone(), t.clone(), anchor).await { error!("latest sync error: {err:?}"); } } @@ -805,48 +776,8 @@ impl HotTierManager { } }); - let s = stream.clone(); - let t = tenant_id.clone(); - let historic = tokio::spawn(async move { - loop { - if let Ok(stream) = PARSEABLE.get_stream(&s, &t) - && stream.get_stream_type().ne(&StreamType::Internal) - { - info!( - stream = ?s, - tenant = ?t, - "skipping historic phase for user-defined stream" - ); - } else { - let anchor = floor_to_minute(Utc::now()); - let tick_span = tracing::info_span!( - "hottier.tick", - stream = %s, - tenant = ?t, - phase = "historic", - anchor = %anchor - ); - async { - if let Err(err) = self - .process_stream(s.clone(), t.clone(), SyncPhase::Historic, anchor) - .await - { - error!("historic sync error: {err:?}"); - } - } - .instrument(tick_span) - .await; - } - tokio::time::sleep(historic_interval).await; - } - }); - - { - let mut tasks = self.tasks.write().await; - if let Some(old) = tasks.insert(key, StreamTasks { latest, historic }) { - old.latest.abort(); - old.historic.abort(); - } + if let Some(old) = tasks.insert(key, StreamTasks { latest }) { + old.latest.abort(); } } @@ -857,7 +788,6 @@ impl HotTierManager { { if let Some(t) = self.tasks.write().await.remove(&key) { t.latest.abort(); - t.historic.abort(); info!(stream = %stream, tenant = ?tenant_id, "aborted per-stream hot tier tasks"); } } @@ -868,24 +798,22 @@ impl HotTierManager { #[tracing::instrument( name = "hottier.process_stream", skip(self), - fields(stream = %stream, tenant = ?tenant_id, phase = ?phase, anchor = %anchor), + fields(stream = %stream, tenant = ?tenant_id, anchor = %anchor), err )] async fn process_stream( &self, stream: String, tenant_id: Option, - phase: SyncPhase, anchor: DateTime, ) -> Result<(), HotTierError> { let stream_start = std::time::Instant::now(); - self.process_manifest(&stream, &tenant_id, phase, anchor) + self.process_manifest(&stream, &tenant_id, anchor) .await .map_err(|e| { error!( stream = %stream, tenant = ?tenant_id, - phase = ?phase, error = ?e ); e @@ -894,15 +822,15 @@ impl HotTierManager { info!( stream = %stream, tenant = ?tenant_id, - phase = ?phase, - elapsed_ms = stream_start.elapsed().as_millis() as u64, + elapsed_seconds = stream_start.elapsed().as_secs_f64(), + delayed = stream_start.elapsed().as_secs() > 30, "stream sync done" ); Ok(()) } /// process the hot tier files for the stream - /// Determine the candidate dates for the current phase, fetch only those + /// Determine the candidate dates, fetch only those /// manifests from the metastore, build a work list sorted newest-first by /// file timestamp, then download via the existing reserve/commit flow. #[tracing::instrument( @@ -911,7 +839,6 @@ impl HotTierManager { fields( stream = %stream, tenant = ?tenant_id, - phase = ?phase, anchor = %anchor, candidate_dates = tracing::field::Empty, work_count = tracing::field::Empty, @@ -923,71 +850,68 @@ impl HotTierManager { &self, stream: &str, tenant_id: &Option, - phase: SyncPhase, anchor: DateTime, ) -> Result<(), HotTierError> { let state = self.get_or_load_state(stream, tenant_id).await?; let latest_minutes = PARSEABLE.options.hot_tier_latest_minutes; - let historic_cutoff = anchor - chrono::Duration::minutes(latest_minutes as i64); - let today_date_key = format!("date={}", anchor.date_naive()); + let mut cutoff = anchor - chrono::Duration::minutes(latest_minutes as i64); - let candidate_dates = match phase { - SyncPhase::Latest => Self::latest_candidate_dates(historic_cutoff, anchor), - SyncPhase::Historic => { - self.historic_candidate_dates(stream, tenant_id, &state, &today_date_key) - .await - } - }; + let candidate_dates = Self::latest_candidate_dates(cutoff, anchor); if candidate_dates.is_empty() { - info!(stream = %stream, tenant = ?tenant_id, phase = ?phase, "no candidate dates this tick"); + info!(stream = %stream, tenant = ?tenant_id, "no candidate dates this tick"); return Ok(()); } let s3_manifests = self - .fetch_manifests(stream, tenant_id, phase, &candidate_dates) + .fetch_manifests(stream, tenant_id, &candidate_dates) .await?; - let mut work = self.build_work_list(&s3_manifests, historic_cutoff.naive_utc(), phase); + // take the latest date from the manifests + // and modify cutoff accordingly + let latest_date = s3_manifests + .values() + .flatten() + .flat_map(|m| &m.files) + .collect_vec() + .par_iter() + .map(|f| extract_datetime(&f.file_path).map(|d| d.and_utc())) + .collect::>() + .par_iter() + .max() + .cloned(); + + cutoff = if let Some(d) = latest_date + && let Some(d) = d + { + d - chrono::Duration::minutes(latest_minutes as i64) + } else { + cutoff + }; + + let mut work = self.build_work_list(&s3_manifests, cutoff.naive_utc()); work.sort_by_key(|b| std::cmp::Reverse(b.1)); - let truncated = Self::cap_historic_work(&mut work, phase); let total_bytes: u64 = work.iter().map(|(_, _, f, _)| f.file_size).sum(); tracing::Span::current() .record("work_count", work.len()) .record("total_bytes", total_bytes); - if truncated > 0 { - info!( - stream = %stream, tenant = ?tenant_id, phase = ?phase, - cap = PARSEABLE.options.historic_per_tick_cap as usize, - deferred = truncated, - "historic per-tick cap hit; deferring rest to next tick" - ); - } + if work.is_empty() { - info!(stream = %stream, tenant = ?tenant_id, phase = ?phase, "no files to download this tick"); + info!(stream = %stream, tenant = ?tenant_id, "no files to download this tick"); return Ok(()); } - self.download_work(stream, tenant_id, phase, &state, work) - .await?; - - if matches!(phase, SyncPhase::Historic) && truncated == 0 { - self.mark_complete_dates(stream, tenant_id, &state, &candidate_dates, &s3_manifests) - .await; - } + self.download_work(stream, tenant_id, &state, work).await?; Ok(()) } - /// Dates covered by `[historic_cutoff, anchor]`. Usually today, or + /// Dates covered by `[cutoff, anchor]`. Usually today, or /// today + yesterday when latest window crosses midnight. - fn latest_candidate_dates( - historic_cutoff: DateTime, - anchor: DateTime, - ) -> Vec { + fn latest_candidate_dates(cutoff: DateTime, anchor: DateTime) -> Vec { let mut out = Vec::new(); - let mut d = historic_cutoff.date_naive(); + let mut d = cutoff.date_naive(); let end = anchor.date_naive(); while d <= end { out.push(format!("date={d}")); @@ -996,44 +920,10 @@ impl HotTierManager { out } - /// Union of local hot-tier dates and S3 dates, minus today and dates - /// already marked complete. Newest-first. - async fn historic_candidate_dates( - &self, - stream: &str, - tenant_id: &Option, - state: &Arc, - today_date_key: &str, - ) -> Vec { - let local = self - .fetch_hot_tier_dates(stream, tenant_id) - .await - .unwrap_or_default() - .into_iter() - .map(|d| format!("date={d}")); - let s3 = PARSEABLE - .hottier_connection_pool - .list_dates(stream, tenant_id) - .await - .unwrap_or_default(); - let mut union: std::collections::BTreeSet = local.collect(); - union.extend(s3); - - let completed = state.completed_dates.read().await; - let mut out: Vec = union - .into_iter() - .filter(|d| d.as_str() < today_date_key && !completed.contains(d)) - .collect(); - out.sort(); - out.reverse(); - out - } - async fn fetch_manifests( &self, stream: &str, tenant_id: &Option, - phase: SyncPhase, candidate_dates: &[String], ) -> Result>, HotTierError> { PARSEABLE @@ -1042,7 +932,7 @@ impl HotTierManager { .await .map_err(|e| { error!( - stream = %stream, tenant = ?tenant_id, phase = ?phase, + stream = %stream, tenant = ?tenant_id, error = ?e, "manifest fetch failed" ); HotTierError::ObjectStorageError(ObjectStorageError::MetastoreError(Box::new( @@ -1051,13 +941,11 @@ impl HotTierManager { }) } - /// Flatten manifests into work list. Keep only files matching this - /// phase's cutoff and not already on disk. + /// Flatten manifests into work list. Keep only files not already on disk. fn build_work_list( &self, s3_manifests: &BTreeMap>, - historic_cutoff_naive: chrono::NaiveDateTime, - phase: SyncPhase, + cutoff_naive: chrono::NaiveDateTime, ) -> Vec<(NaiveDate, chrono::NaiveDateTime, File, PathBuf)> { let mut work = Vec::new(); for (str_date, manifest_files) in s3_manifests.iter() { @@ -1066,9 +954,7 @@ impl HotTierManager { }; for manifest in manifest_files { for parquet_file in &manifest.files { - if let Some(item) = - self.work_item_for(parquet_file, date, historic_cutoff_naive, phase) - { + if let Some(item) = self.work_item_for(parquet_file, date, cutoff_naive) { work.push(item); } } @@ -1091,44 +977,22 @@ impl HotTierManager { &self, parquet_file: &File, date: NaiveDate, - historic_cutoff_naive: chrono::NaiveDateTime, - phase: SyncPhase, + cutoff_naive: chrono::NaiveDateTime, ) -> Option<(NaiveDate, chrono::NaiveDateTime, File, PathBuf)> { let parquet_path = self.hot_tier_path.join(&parquet_file.file_path); if parquet_path.exists() { return None; } - let dt = extract_datetime(&parquet_file.file_path)?; - let is_latest = dt >= historic_cutoff_naive; - let keep = match phase { - SyncPhase::Latest => is_latest, - SyncPhase::Historic => !is_latest, - }; - keep.then(|| (date, dt, parquet_file.clone(), parquet_path)) - } - /// Historic ticks cap per-tick work. Returns count truncated (0 for Latest). - fn cap_historic_work( - work: &mut Vec<(NaiveDate, chrono::NaiveDateTime, File, PathBuf)>, - phase: SyncPhase, - ) -> usize { - if !matches!(phase, SyncPhase::Historic) { - return 0; - } - let cap = PARSEABLE.options.historic_per_tick_cap as usize; - if work.len() <= cap { - return 0; - } - let dropped = work.len() - cap; - work.truncate(cap); - dropped + let dt = extract_datetime(&parquet_file.file_path)?; + let is_latest = dt >= cutoff_naive; + is_latest.then(|| (date, dt, parquet_file.clone(), parquet_path)) } async fn download_work( &self, stream: &str, tenant_id: &Option, - phase: SyncPhase, state: &Arc, work: Vec<(NaiveDate, chrono::NaiveDateTime, File, PathBuf)>, ) -> Result<(), HotTierError> { @@ -1139,7 +1003,7 @@ impl HotTierManager { let reserved = { let mut sht = state.sht.lock().await; - self.reserve_disk_budget(stream, &work, tenant_id, phase, &mut sht) + self.reserve_disk_budget(stream, &work, tenant_id, &mut sht) .await? }; if !reserved { @@ -1160,16 +1024,15 @@ impl HotTierManager { .process_parquet_file_concurrent( &stream, &file, - parquet_path, + &parquet_path, date, &tenant_id, &state, - phase, ) .await?; if !processed && !stop.swap(true, std::sync::atomic::Ordering::Relaxed) { info!( - stream = %stream, tenant = ?tenant_id, phase = ?phase, + stream = %stream, tenant = ?tenant_id, "sticky stop: halting further reservations this tick" ); } @@ -1186,65 +1049,6 @@ impl HotTierManager { Ok(()) } - /// For each candidate date where local manifest caught up to S3, - /// add it to `completed_dates` so future Historic ticks skip it. - async fn mark_complete_dates( - &self, - stream: &str, - tenant_id: &Option, - state: &Arc, - candidate_dates: &[String], - s3_manifests: &BTreeMap>, - ) { - let mut newly_complete = Vec::new(); - for date_key in candidate_dates { - let s3_count: usize = s3_manifests - .get(date_key) - .map_or(0, |v| v.iter().map(|m| m.files.len()).sum()); - if s3_count == 0 { - continue; - } - let local_count = self - .local_manifest_file_count(stream, date_key, tenant_id) - .await; - if local_count >= s3_count { - newly_complete.push(date_key.clone()); - } - } - if newly_complete.is_empty() { - return; - } - let mut completed = state.completed_dates.write().await; - for d in newly_complete { - info!(stream = %stream, tenant = ?tenant_id, date = %d, "marking date locally complete"); - completed.insert(d); - } - } - - /// Count files recorded in a date's local `hottier.manifest.json`. - /// Returns 0 if the manifest is missing or fails to parse. - async fn local_manifest_file_count( - &self, - stream: &str, - date_key: &str, - tenant_id: &Option, - ) -> usize { - let date_dir = if let Some(tenant) = tenant_id.as_ref() { - self.hot_tier_path.join(tenant).join(stream).join(date_key) - } else { - self.hot_tier_path.join(stream).join(date_key) - }; - let manifest_path = date_dir.join("hottier.manifest.json"); - if !manifest_path.exists() { - return 0; - } - match fs::read(&manifest_path).await { - Ok(bytes) => serde_json::from_slice::(&bytes) - .map(|m| m.files.len()) - .unwrap_or(0), - Err(_) => 0, - } - } #[allow(clippy::too_many_arguments)] #[tracing::instrument( name = "hottier.reserve_disk_budget", @@ -1252,69 +1056,49 @@ impl HotTierManager { fields( stream = %stream, tenant = ?tenant_id, - phase = ?phase, ), err )] async fn reserve_disk_budget( &self, stream: &str, - // parquet_file: &File, - // parquet_path: PathBuf, - // date: NaiveDate, work: &Vec<(NaiveDate, chrono::NaiveDateTime, File, PathBuf)>, tenant_id: &Option, - phase: SyncPhase, sht: &mut StreamHotTier, ) -> Result { // RESERVE for (_, _, parquet_file, parquet_path) in work { // let mut sht = state.sht.lock().await; - if !self.is_disk_available(parquet_file.file_size).await? + if !self.is_disk_available(parquet_file.file_size).await || sht.available_size < parquet_file.file_size { - match phase { - SyncPhase::Latest => { - info!( - stream = %stream, - tenant = ?tenant_id, - file = %parquet_file.file_path, - file_size = parquet_file.file_size, - available = sht.available_size, - "tight on space; triggering eviction" - ); - if !self - .cleanup_hot_tier_old_data( - stream, - sht, - parquet_path, - parquet_file.file_size, - tenant_id, - ) - .await? - { - info!( - stream = %stream, - tenant = ?tenant_id, - file = %parquet_file.file_path, - file_size = parquet_file.file_size, - "eviction freed nothing, skipping file" - ); - return Ok(false); - } - } - SyncPhase::Historic => { - info!( - stream = %stream, - tenant = ?tenant_id, - file = %parquet_file.file_path, - file_size = parquet_file.file_size, - available = sht.available_size, - "historic phase: full, skipping file" - ); - return Ok(false); - } + info!( + stream = %stream, + tenant = ?tenant_id, + file = %parquet_file.file_path, + file_size = parquet_file.file_size, + available = sht.available_size, + "tight on space; triggering eviction" + ); + if !self + .cleanup_hot_tier_old_data( + stream, + sht, + parquet_path, + parquet_file.file_size, + tenant_id, + ) + .await? + { + info!( + stream = %stream, + tenant = ?tenant_id, + file = %parquet_file.file_path, + file_size = parquet_file.file_size, + "eviction freed nothing, skipping file" + ); + return Ok(false); } } if sht.available_size < parquet_file.file_size { @@ -1356,11 +1140,10 @@ impl HotTierManager { &self, stream: &str, parquet_file: &File, - parquet_path: PathBuf, + parquet_path: &Path, date: NaiveDate, tenant_id: &Option, state: &Arc, - phase: SyncPhase, ) -> Result { // DOWNLOAD (no lock held) let parquet_file_path = RelativePathBuf::from(parquet_file.file_path.clone()); @@ -1370,13 +1153,17 @@ impl HotTierManager { tenant = ?tenant_id, file = %parquet_file.file_path, file_size = parquet_file.file_size, - phase = ?phase, + "download starting" ); let dl_start = std::time::Instant::now(); let download_result = PARSEABLE .hottier_connection_pool - .parallel_chunked_download(&parquet_file_path, tenant_id, parquet_path.clone()) + .parallel_chunked_download( + &parquet_file_path, + tenant_id, + parquet_path.to_path_buf().clone(), + ) .await; let dl_elapsed = dl_start.elapsed(); @@ -1387,7 +1174,7 @@ impl HotTierManager { file = %parquet_file.file_path, elapsed_ms = dl_elapsed.as_millis() as u64, err = %e, - phase = ?phase, + "download failed, refunding reservation" ); // refund reservation @@ -1411,7 +1198,7 @@ impl HotTierManager { file = %parquet_file.file_path, file_size = parquet_file.file_size, elapsed_ms, - phase = ?phase, + mbps = format!("{mbps:.1}"), "download finished, committing" ); @@ -1806,7 +1593,7 @@ impl HotTierManager { /// check if the disk is available to download the parquet file /// check if the disk usage is above the threshold - pub async fn is_disk_available(&self, size_to_download: u64) -> Result { + pub async fn is_disk_available(&self, size_to_download: u64) -> bool { if let Some(DiskUtil { total_space, available_space, @@ -1814,17 +1601,17 @@ impl HotTierManager { }) = self.get_disk_usage() { if available_space < size_to_download { - return Ok(false); + return false; } if ((used_space + size_to_download) as f64 * 100.0 / total_space as f64) > PARSEABLE.options.max_disk_usage { - return Ok(false); + return false; } } - Ok(true) + true } pub async fn get_oldest_date_time_entry( From 07a09b91a3f1edf90db6f98f074f8cd3a465e3d1 Mon Sep 17 00:00:00 2001 From: Nikhil Sinha <131262146+nikhilsinhaparseable@users.noreply.github.com> Date: Mon, 22 Jun 2026 16:54:51 +0530 Subject: [PATCH 40/47] add optional ingestion quota to tenant metadata (#1691) * add optional ingestion quota to tenant metadata * clippy fix --- src/storage/mod.rs | 3 ++- src/storage/store_metadata.rs | 28 ++++++++++++++++++++++++++++ src/tenants/mod.rs | 10 +++++++++- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/storage/mod.rs b/src/storage/mod.rs index e40100195..ac010edf6 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -93,7 +93,8 @@ pub use localfs::FSConfig; pub use object_storage::{ObjectStorage, ObjectStorageProvider}; pub use s3::S3Config; pub use store_metadata::{ - StorageMetadata, put_remote_metadata, put_staging_metadata, resolve_parseable_metadata, + IngestionQuota, IngestionQuotaType, QuotaPeriod, StorageMetadata, put_remote_metadata, + put_staging_metadata, resolve_parseable_metadata, }; // metadata file names in a Stream prefix diff --git a/src/storage/store_metadata.rs b/src/storage/store_metadata.rs index a04008a61..71a308317 100644 --- a/src/storage/store_metadata.rs +++ b/src/storage/store_metadata.rs @@ -52,6 +52,28 @@ pub struct StaticStorageMetadata { } // Type for serialization and deserialization +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct IngestionQuota { + #[serde(rename = "type")] + pub quota_type: IngestionQuotaType, + pub limit: u64, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum IngestionQuotaType { + SizeBytes, + EventCount, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum QuotaPeriod { + Monthly, + Yearly, + Lifetime, +} + #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct StorageMetadata { pub version: String, @@ -79,6 +101,10 @@ pub struct StorageMetadata { #[serde(default, skip_serializing_if = "Option::is_none")] pub plan: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + pub ingestion_quota: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub quota_period: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub owner: Option, } @@ -102,6 +128,8 @@ impl Default for StorageMetadata { start_date: None, end_date: None, plan: None, + ingestion_quota: None, + quota_period: None, owner: None, } } diff --git a/src/tenants/mod.rs b/src/tenants/mod.rs index 442c44c3b..e0c983203 100644 --- a/src/tenants/mod.rs +++ b/src/tenants/mod.rs @@ -23,7 +23,10 @@ use itertools::Itertools; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; -use crate::{rbac::role::Action, storage::StorageMetadata}; +use crate::{ + rbac::role::Action, + storage::{IngestionQuota, QuotaPeriod, StorageMetadata}, +}; pub static TENANT_METADATA: Lazy> = Lazy::new(|| Arc::new(TenantMetadata::default())); @@ -63,6 +66,7 @@ impl TenantMetadata { self.tenants.get(tenant_id).map(|t| t.meta.clone()) } + #[allow(clippy::too_many_arguments)] pub fn update_tenant_meta( &self, tenant_id: &str, @@ -70,12 +74,16 @@ impl TenantMetadata { start_date: Option, end_date: Option, plan: Option, + ingestion_quota: Option, + quota_period: Option, ) -> bool { if let Some(mut tenant) = self.tenants.get_mut(tenant_id) { tenant.meta.customer_name = customer_name; tenant.meta.start_date = start_date; tenant.meta.end_date = end_date; tenant.meta.plan = plan; + tenant.meta.ingestion_quota = ingestion_quota; + tenant.meta.quota_period = quota_period; true } else { false From 955f53b224f07b74fec986d6c2066138806f5617 Mon Sep 17 00:00:00 2001 From: Nikhil Sinha <131262146+nikhilsinhaparseable@users.noreply.github.com> Date: Tue, 23 Jun 2026 07:43:55 +0530 Subject: [PATCH 41/47] add stream name to the ingest script param (#1696) --- scripts/ingest.ps1 | 22 +++++++++++++--------- scripts/ingest.sh | 29 +++++++++++++++-------------- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/scripts/ingest.ps1 b/scripts/ingest.ps1 index 4421bc7b9..9037c47a4 100644 --- a/scripts/ingest.ps1 +++ b/scripts/ingest.ps1 @@ -8,7 +8,10 @@ param( [string]$Param2, [Parameter(Position=2)] - [string]$Param3 + [string]$Param3, + + [Parameter(Position=3)] + [string]$Param4 ) $ProgressPreference = 'SilentlyContinue' @@ -261,11 +264,12 @@ function Restart-FluentBit { function Setup-FluentBit { param( [string]$IngestorHost, + [string]$StreamName, [string]$ApiKey, [string]$TenantId ) - if ([string]::IsNullOrWhiteSpace($IngestorHost) -or [string]::IsNullOrWhiteSpace($ApiKey)) { + if ([string]::IsNullOrWhiteSpace($IngestorHost) -or [string]::IsNullOrWhiteSpace($StreamName) -or [string]::IsNullOrWhiteSpace($ApiKey)) { Write-ErrorMsg "Invalid setup parameters" exit 1 } @@ -336,7 +340,7 @@ function Setup-FluentBit { } $configLines += @( - " Header X-P-Stream node-metrics", + " Header X-P-Stream $StreamName", " Header X-P-Log-Source otel-metrics" ) @@ -355,7 +359,7 @@ function Show-Help { Fluent Bit Setup and Management Script for Windows Usage: - Setup: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 [host[:port]] [api_key] [tenant_id] + Setup: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 [host[:port]] [stream] [api_key] [tenant_id] Stop: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 stop Start: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 start Restart: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 restart @@ -364,8 +368,8 @@ Usage: Debug: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 debug Example: - powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 https://your-host.com:443 px_api_key - powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 http://localhost:8000 px_api_key tenant-id + powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 https://your-host.com:443 node-metrics px_api_key + powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 http://localhost:8000 node-metrics px_api_key tenant-id "@ } @@ -420,11 +424,11 @@ switch ($Param1.ToLower()) { Show-Help } default { - if ([string]::IsNullOrWhiteSpace($Param2)) { - Write-ErrorMsg "Usage: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 [host[:port]] [api_key] [tenant_id]" + if ([string]::IsNullOrWhiteSpace($Param2) -or [string]::IsNullOrWhiteSpace($Param3)) { + Write-ErrorMsg "Usage: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 [host[:port]] [stream] [api_key] [tenant_id]" Write-ErrorMsg " Or: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 [start|stop|restart|status|logs|debug|help]" exit 1 } - Setup-FluentBit -IngestorHost $Param1 -ApiKey $Param2 -TenantId $Param3 + Setup-FluentBit -IngestorHost $Param1 -StreamName $Param2 -ApiKey $Param3 -TenantId $Param4 } } diff --git a/scripts/ingest.sh b/scripts/ingest.sh index 95d69b84d..54372633a 100755 --- a/scripts/ingest.sh +++ b/scripts/ingest.sh @@ -2,7 +2,7 @@ # Fluent Bit Setup and Management Script # Usage: -# Setup: ./ingest.sh [tenant_id] +# Setup: ./ingest.sh [tenant_id] # Stop: ./ingest.sh stop # Restart: ./ingest.sh restart # Status: ./ingest.sh status @@ -241,17 +241,18 @@ install_fluent_bit() { # Setup function setup_fluent_bit() { local INGESTOR_HOST="$1" - local API_KEY="$2" - local TENANT_ID="${3:-}" + local STREAM_NAME="$2" + local API_KEY="$3" + local TENANT_ID="${4:-}" local TENANT_HEADER="" local TLS_SETTING="On" local DEFAULT_PORT="443" local PORT="" # Validate all fields are present - if [ -z "$INGESTOR_HOST" ] || [ -z "$API_KEY" ]; then + if [ -z "$INGESTOR_HOST" ] || [ -z "$STREAM_NAME" ] || [ -z "$API_KEY" ]; then print_error "Invalid setup parameters" - print_error "Expected format: $0 [tenant_id]" + print_error "Expected format: $0 [tenant_id]" exit 1 fi @@ -330,7 +331,7 @@ setup_fluent_bit() { TLS $TLS_SETTING Header X-API-Key $API_KEY ${TENANT_HEADER} - Header X-P-Stream node-metrics + Header X-P-Stream $STREAM_NAME Header X-P-Log-Source otel-metrics EOF chmod 600 "$CONFIG_FILE" @@ -363,7 +364,7 @@ case "${1:-}" in echo "" echo "Usage:" echo " Setup and start:" - echo " $0 [tenant_id]" + echo " $0 [tenant_id]" echo "" echo " Management commands:" echo " $0 start - Start Fluent Bit (if config exists)" @@ -373,18 +374,18 @@ case "${1:-}" in echo " $0 logs - Show Fluent Bit logs" echo "" echo "Example:" - echo " $0 https://example.parseable.com:443 px_api_key" - echo " $0 http://localhost:8000 px_api_key tenant-id" + echo " $0 https://example.parseable.com:443 node-metrics px_api_key" + echo " $0 http://localhost:8000 node-metrics px_api_key tenant-id" ;; *) # If not a command, treat as setup parameters - if [ $# -lt 2 ] || [ $# -gt 3 ]; then - print_error "Usage: $0 [tenant_id]" + if [ $# -lt 3 ] || [ $# -gt 4 ]; then + print_error "Usage: $0 [tenant_id]" print_error " Or: $0 [start|stop|restart|status|logs|help]" print_error "" print_error "Example:" - print_error " $0 https://ec9cfee0-2fd4-45eb-8209-d7cd992c4bcc-ingestor.workspace-staging.parseable.com:443 px_api_key" - print_error " $0 http://localhost:8000 px_api_key tenant-id" + print_error " $0 https://ec9cfee0-2fd4-45eb-8209-d7cd992c4bcc-ingestor.workspace-staging.parseable.com:443 node-metrics px_api_key" + print_error " $0 http://localhost:8000 node-metrics px_api_key tenant-id" print_error "" print_error "Management commands:" print_error " $0 status - Check if running" @@ -393,6 +394,6 @@ case "${1:-}" in print_error " $0 logs - View logs" exit 1 fi - setup_fluent_bit "$1" "$2" "${3:-}" + setup_fluent_bit "$1" "$2" "$3" "${4:-}" ;; esac From 12eb6c6b6caf67dd27f8d8d1be7a0e7cdba23ce1 Mon Sep 17 00:00:00 2001 From: Nikhil Sinha <131262146+nikhilsinhaparseable@users.noreply.github.com> Date: Tue, 23 Jun 2026 07:48:22 +0530 Subject: [PATCH 42/47] update: context api support for start and end time (#1695) context api request to have support to send either of - 1. context window (1m, 5m, 10m etc) 2. context start and end time add validations - 1. p_timestamp of the reference log should be within the context window 2. window and start/end time - both should not be present in the request 3. start <= end time --- src/handlers/http/query_context.rs | 173 +++++++++++++++++++++++++++-- 1 file changed, 161 insertions(+), 12 deletions(-) diff --git a/src/handlers/http/query_context.rs b/src/handlers/http/query_context.rs index 422d4d85e..be5269306 100644 --- a/src/handlers/http/query_context.rs +++ b/src/handlers/http/query_context.rs @@ -45,7 +45,9 @@ const LOG_CONTEXT_ANCHORED_DUPLICATE: &str = "first"; #[serde(rename_all = "camelCase")] pub struct LogContextRequest { pub dataset: String, - pub context_window: String, + pub context_window: Option, + pub context_start_time: Option, + pub context_end_time: Option, pub p_timestamp: String, pub log: Option, pub body: Option, @@ -145,8 +147,13 @@ pub async fn query_context( let page_size = normalize_log_context_page_size(context_request.page_size)?; span.record("page_size", page_size); let anchor_timestamp = parse_log_context_timestamp(&context_request.p_timestamp)?; - let (context_start_time, context_end_time) = - log_context_bounds(anchor_timestamp, &context_request.context_window)?; + let (context_start_time, context_end_time) = resolve_log_context_bounds( + anchor_timestamp, + context_request.context_window.as_deref(), + context_request.context_start_time.as_deref(), + context_request.context_end_time.as_deref(), + )?; + validate_log_context_anchor_in_bounds(anchor_timestamp, context_start_time, context_end_time)?; debug!( page_size, anchor_timestamp = %anchor_timestamp, @@ -290,17 +297,23 @@ fn normalize_log_context_page_size(page_size: Option) -> Result Result, QueryError> { + parse_log_context_time_field(raw, "pTimestamp") +} + +fn parse_log_context_time_field(raw: &str, field_name: &str) -> Result, QueryError> { let raw = raw.trim(); let timestamp = DateTime::parse_from_rfc3339(raw) .map(|timestamp| timestamp.with_timezone(&Utc)) .or_else(|rfc3339_err| { parse_log_context_naive_utc_timestamp(raw) .map(|timestamp| DateTime::from_naive_utc_and_offset(timestamp, Utc)) - .map_err(|_| QueryError::CustomError(format!("Invalid pTimestamp: {rfc3339_err}"))) + .map_err(|_| { + QueryError::CustomError(format!("Invalid {field_name}: {rfc3339_err}")) + }) })?; DateTime::from_timestamp_millis(timestamp.timestamp_millis()).ok_or_else(|| { - QueryError::CustomError("pTimestamp is outside the supported range".to_string()) + QueryError::CustomError(format!("{field_name} is outside the supported range")) }) } @@ -309,7 +322,33 @@ fn parse_log_context_naive_utc_timestamp(raw: &str) -> Result, + context_window: Option<&str>, + context_start_time: Option<&str>, + context_end_time: Option<&str>, +) -> Result<(DateTime, DateTime), QueryError> { + match (context_window, context_start_time, context_end_time) { + (Some(_), Some(_), _) | (Some(_), _, Some(_)) => Err(QueryError::CustomError( + "Request must include either contextWindow or contextStartTime/contextEndTime, not both" + .to_string(), + )), + (Some(context_window), None, None) => { + log_context_window_bounds(anchor_timestamp, context_window) + } + (None, Some(context_start_time), Some(context_end_time)) => { + log_context_explicit_bounds(context_start_time, context_end_time) + } + (None, Some(_), None) | (None, None, Some(_)) => Err(QueryError::CustomError( + "contextStartTime and contextEndTime must be provided together".to_string(), + )), + (None, None, None) => Err(QueryError::CustomError( + "Request must include either contextWindow or contextStartTime/contextEndTime".to_string(), + )), + } +} + +fn log_context_window_bounds( anchor_timestamp: DateTime, context_window: &str, ) -> Result<(DateTime, DateTime), QueryError> { @@ -333,6 +372,37 @@ fn log_context_bounds( Ok((start, end)) } +fn log_context_explicit_bounds( + context_start_time: &str, + context_end_time: &str, +) -> Result<(DateTime, DateTime), QueryError> { + let start = parse_log_context_time_field(context_start_time, "contextStartTime")?; + let end = parse_log_context_time_field(context_end_time, "contextEndTime")?; + + if start >= end { + return Err(QueryError::CustomError( + "contextStartTime must be before contextEndTime".to_string(), + )); + } + + Ok((start, end)) +} + +fn validate_log_context_anchor_in_bounds( + anchor_timestamp: DateTime, + context_start_time: DateTime, + context_end_time: DateTime, +) -> Result<(), QueryError> { + if anchor_timestamp >= context_start_time && anchor_timestamp < context_end_time { + return Ok(()); + } + + Err(QueryError::CustomError( + "pTimestamp must be greater than or equal to contextStartTime and less than contextEndTime" + .to_string(), + )) +} + fn normalize_log_context_match_fields( log: &Option, body: &Option, @@ -942,7 +1012,7 @@ fn log_context_cursor_from_record( } fn format_log_context_api_time(timestamp: DateTime) -> String { - timestamp.to_rfc3339_opts(SecondsFormat::Secs, true) + timestamp.to_rfc3339_opts(SecondsFormat::AutoSi, true) } fn timestamp_sql_literal(timestamp: DateTime) -> String { @@ -1023,16 +1093,93 @@ mod tests { } #[test] - fn log_context_bounds_apply_window_and_truncate_to_minute() { - let (start, end) = log_context_bounds(anchor_timestamp(), "1m").unwrap(); + fn log_context_explicit_bounds_accept_start_and_end_times() { + let (start, end) = + log_context_explicit_bounds("2026-06-17T10:14:00Z", "2026-06-17T10:16:00Z").unwrap(); assert_eq!(format_log_context_api_time(start), "2026-06-17T10:14:00Z"); assert_eq!(format_log_context_api_time(end), "2026-06-17T10:16:00Z"); - let (start, end) = log_context_bounds(anchor_timestamp(), "5s").unwrap(); + let (start, end) = + log_context_explicit_bounds("2026-06-17T10:15:42.100Z", "2026-06-17T10:15:42.900Z") + .unwrap(); + assert_eq!( + format_log_context_api_time(start), + "2026-06-17T10:15:42.100Z" + ); + assert_eq!(format_log_context_api_time(end), "2026-06-17T10:15:42.900Z"); + + assert!( + log_context_explicit_bounds("2026-06-17T10:16:00Z", "2026-06-17T10:16:00Z").is_err() + ); + assert!( + log_context_explicit_bounds("2026-06-17T10:17:00Z", "2026-06-17T10:16:00Z").is_err() + ); + } + + #[test] + fn log_context_window_bounds_apply_window_and_truncate_to_minute() { + let (start, end) = log_context_window_bounds(anchor_timestamp(), "1m").unwrap(); + assert_eq!(format_log_context_api_time(start), "2026-06-17T10:14:00Z"); + assert_eq!(format_log_context_api_time(end), "2026-06-17T10:16:00Z"); + + let (start, end) = log_context_window_bounds(anchor_timestamp(), "5s").unwrap(); assert_eq!(format_log_context_api_time(start), "2026-06-17T10:15:00Z"); assert_eq!(format_log_context_api_time(end), "2026-06-17T10:16:00Z"); } + #[test] + fn log_context_bounds_resolver_accepts_one_mode_only() { + let anchor = anchor_timestamp(); + assert!(resolve_log_context_bounds(anchor, Some("1m"), None, None).is_ok()); + assert!( + resolve_log_context_bounds( + anchor, + None, + Some("2026-06-17T10:14:00Z"), + Some("2026-06-17T10:16:00Z"), + ) + .is_ok() + ); + assert!( + resolve_log_context_bounds(anchor, Some("1m"), Some("2026-06-17T10:14:00Z"), None,) + .is_err() + ); + assert!( + resolve_log_context_bounds(anchor, None, Some("2026-06-17T10:14:00Z"), None).is_err() + ); + assert!(resolve_log_context_bounds(anchor, None, None, None).is_err()); + } + + #[test] + fn log_context_anchor_must_be_inside_context_bounds() { + let (start, end) = + log_context_explicit_bounds("2026-06-17T10:14:00Z", "2026-06-17T10:16:00Z").unwrap(); + + validate_log_context_anchor_in_bounds( + parse_log_context_timestamp("2026-06-17T10:14:00Z").unwrap(), + start, + end, + ) + .unwrap(); + validate_log_context_anchor_in_bounds(anchor_timestamp(), start, end).unwrap(); + assert!( + validate_log_context_anchor_in_bounds( + parse_log_context_timestamp("2026-06-17T10:13:59.999Z").unwrap(), + start, + end, + ) + .is_err() + ); + assert!( + validate_log_context_anchor_in_bounds( + parse_log_context_timestamp("2026-06-17T10:16:00Z").unwrap(), + start, + end, + ) + .is_err() + ); + } + #[test] fn log_context_match_fields_accept_exactly_one_anchor_field() { let schema = schema_with(&[DEFAULT_TIMESTAMP_KEY, "body", "log", "message"]); @@ -1130,7 +1277,8 @@ mod tests { #[test] fn log_context_anchor_count_sql_only_counts_anchor_duplicates() { let anchor = anchor_timestamp(); - let (start, end) = log_context_bounds(anchor, "1m").unwrap(); + let (start, end) = + log_context_explicit_bounds("2026-06-17T10:14:00Z", "2026-06-17T10:16:00Z").unwrap(); let match_fields = vec![LogContextMatchField { name: "message".to_string(), value: "alpha".to_string(), @@ -1150,7 +1298,8 @@ mod tests { #[test] fn log_context_cursor_sql_builds_previous_and_next_pages() { let anchor = anchor_timestamp(); - let (start, end) = log_context_bounds(anchor, "1m").unwrap(); + let (start, end) = + log_context_explicit_bounds("2026-06-17T10:14:00Z", "2026-06-17T10:16:00Z").unwrap(); let match_fields = vec![LogContextMatchField { name: "message".to_string(), value: "alpha".to_string(), From 2cea7b0d6dc92ff41e273471fb28334c1b7a3e5b Mon Sep 17 00:00:00 2001 From: YGN <149811989+ygndotgg@users.noreply.github.com> Date: Tue, 23 Jun 2026 14:06:53 +0530 Subject: [PATCH 43/47] Restore and harden credential masking for /targets (#1698) This patch re-enables masking with a few backend changes - Add mask_url() using url based parsing - Apply consistent masking across Slack, Webhook, and AlertManager types. - Add strict unit tests enforcing that secrets never hit the serializer. --- src/alerts/target.rs | 70 ++++++++++++++++++++++++------------ src/handlers/http/targets.rs | 14 +++----- 2 files changed, 52 insertions(+), 32 deletions(-) diff --git a/src/alerts/target.rs b/src/alerts/target.rs index ba700ae2a..abe8792a5 100644 --- a/src/alerts/target.rs +++ b/src/alerts/target.rs @@ -198,48 +198,36 @@ impl Target { pub fn mask(self) -> Value { match self.target { TargetType::Slack(slack_web_hook) => { - let endpoint = slack_web_hook.endpoint.to_string(); - let masked_endpoint = if endpoint.len() > 20 { - format!("{}********", &endpoint[..20]) - } else { - "********".to_string() - }; json!({ "name":self.name, "type":"slack", - "endpoint":masked_endpoint, + "endpoint":format!("{}://********", slack_web_hook.endpoint.scheme()), "id":self.id }) } TargetType::Other(other_web_hook) => { - let endpoint = other_web_hook.endpoint.to_string(); - let masked_endpoint = if endpoint.len() > 20 { - format!("{}********", &endpoint[..20]) - } else { - "********".to_string() - }; + let safe_headers: HashMap = other_web_hook + .headers + .into_keys() + .map(|k| (k, "********".to_string())) + .collect(); json!({ "name":self.name, "type":"webhook", - "endpoint":masked_endpoint, - "headers":other_web_hook.headers, + "endpoint": format!("{}://********", other_web_hook.endpoint.scheme()), + "headers":safe_headers, "skipTlsCheck":other_web_hook.skip_tls_check, "id":self.id }) } TargetType::AlertManager(alert_manager) => { - let endpoint = alert_manager.endpoint.to_string(); - let masked_endpoint = if endpoint.len() > 20 { - format!("{}********", &endpoint[..20]) - } else { - "********".to_string() - }; + let endpoint = format!("{}://********", alert_manager.endpoint.scheme()); if let Some(auth) = alert_manager.auth { let password = "********"; json!({ "name":self.name, "type":"webhook", - "endpoint":masked_endpoint, + "endpoint":endpoint, "username":auth.username, "password":password, "skipTlsCheck":alert_manager.skip_tls_check, @@ -249,7 +237,7 @@ impl Target { json!({ "name":self.name, "type":"webhook", - "endpoint":masked_endpoint, + "endpoint":endpoint, "username":Value::Null, "password":Value::Null, "skipTlsCheck":alert_manager.skip_tls_check, @@ -652,3 +640,39 @@ pub struct Auth { username: String, password: String, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_masked_target_hides_secrets() { + let target = Target { + name: "test-target".into(), + id: Ulid::new(), + tenant: None, + target: TargetType::AlertManager(AlertManager { + endpoint: "https://internal.corp/api".parse().unwrap(), + auth: Some(Auth { + username: "admin".into(), + password: "SuperSecretPassword123".into(), + }), + skip_tls_check: false, + }), + }; + + let masked = target.mask(); + + let masked_str = serde_json::to_string(&masked).unwrap(); + + // These assertions MUST pass, or the build fails + assert!( + !masked_str.contains("SuperSecretPassword123"), + "Password leaked in masked response!" + ); + assert!( + masked_str.contains("********"), + "Expected redaction marker not found!" + ); + } +} diff --git a/src/handlers/http/targets.rs b/src/handlers/http/targets.rs index 919a40df5..db91c1e3d 100644 --- a/src/handlers/http/targets.rs +++ b/src/handlers/http/targets.rs @@ -42,8 +42,7 @@ pub async fn post( // add to the map TARGETS.update(target.clone()).await?; - // Ok(web::Json(target.mask())) - Ok(web::Json(target)) + Ok(web::Json(target.mask())) } // GET /targets @@ -54,7 +53,7 @@ pub async fn list(req: HttpRequest) -> Result { .list(&tenant_id) .await? .into_iter() - // .map(|t| t.mask()) + .map(|t| t.mask()) .collect_vec(); Ok(web::Json(list)) @@ -66,8 +65,7 @@ pub async fn get(req: HttpRequest, target_id: Path) -> Result) -> Result Date: Tue, 23 Jun 2026 14:43:43 +0530 Subject: [PATCH 44/47] fix windows ingest script (#1699) * fix windows ingest script * use PSScriptRoot join path --- scripts/ingest.ps1 | 46 +++++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/scripts/ingest.ps1 b/scripts/ingest.ps1 index 9037c47a4..b7f27963d 100644 --- a/scripts/ingest.ps1 +++ b/scripts/ingest.ps1 @@ -23,6 +23,14 @@ $CONFIG_FILE = "$PSScriptRoot\fluent-bit.conf" $PID_FILE = "$PSScriptRoot\fluent-bit.pid" $LOG_FILE = "$PSScriptRoot\fluent-bit.log" $ERROR_LOG_FILE = "$PSScriptRoot\fluent-bit.err.log" +$SCRIPT_PATH = $PSCommandPath +if ([string]::IsNullOrWhiteSpace($SCRIPT_PATH)) { + $SCRIPT_PATH = $MyInvocation.MyCommand.Path +} +if ([string]::IsNullOrWhiteSpace($SCRIPT_PATH)) { + $SCRIPT_PATH = Join-Path $PSScriptRoot "ingest.ps1" +} +$SCRIPT_CMD = "powershell -NoProfile -ExecutionPolicy Bypass -File '$SCRIPT_PATH'" $SUPPORTED_ARCH = @("AMD64", "ARM64") @@ -90,8 +98,8 @@ function Show-Status { Write-Info "Log file: $LOG_FILE" Write-Info "Error log file: $ERROR_LOG_FILE" Write-Host "" - Write-Info "To see logs: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 logs" - Write-Info "To stop: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 stop" + Write-Info "To see logs: $SCRIPT_CMD logs" + Write-Info "To stop: $SCRIPT_CMD stop" } else { Write-Warning "Fluent Bit is not running" @@ -202,7 +210,7 @@ function Start-FluentBit { if (Test-FluentBitRunning) { $processId = Get-Content $PID_FILE Write-Warning "Fluent Bit is already running (PID: $processId)" - Write-Info "Use 'ingest.ps1 stop' to stop it first" + Write-Info "Use '$SCRIPT_CMD stop' to stop it first" return } @@ -241,17 +249,17 @@ function Start-FluentBit { if (-not $stillRunning) { Write-ErrorMsg "Fluent Bit exited immediately" - Write-ErrorMsg "Run '.\ingest.ps1 debug' to see error details" + Write-ErrorMsg "Run '$SCRIPT_CMD debug' to see error details" Remove-Item $PID_FILE -ErrorAction SilentlyContinue exit 1 } else { Write-Info "Fluent Bit started successfully (PID: $($process.Id))" Write-Host "" - Write-Info "To debug: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 debug" - Write-Info "To check status: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 status" - Write-Info "To see logs: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 logs" - Write-Info "To stop: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 stop" + Write-Info "To debug: $SCRIPT_CMD debug" + Write-Info "To check status: $SCRIPT_CMD status" + Write-Info "To see logs: $SCRIPT_CMD logs" + Write-Info "To stop: $SCRIPT_CMD stop" } } @@ -359,17 +367,17 @@ function Show-Help { Fluent Bit Setup and Management Script for Windows Usage: - Setup: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 [host[:port]] [stream] [api_key] [tenant_id] - Stop: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 stop - Start: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 start - Restart: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 restart - Status: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 status - Logs: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 logs - Debug: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 debug + Setup: $SCRIPT_CMD [host[:port]] [stream] [api_key] [tenant_id] + Stop: $SCRIPT_CMD stop + Start: $SCRIPT_CMD start + Restart: $SCRIPT_CMD restart + Status: $SCRIPT_CMD status + Logs: $SCRIPT_CMD logs + Debug: $SCRIPT_CMD debug Example: - powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 https://your-host.com:443 node-metrics px_api_key - powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 http://localhost:8000 node-metrics px_api_key tenant-id + $SCRIPT_CMD https://your-host.com:443 node-metrics px_api_key + $SCRIPT_CMD http://localhost:8000 node-metrics px_api_key tenant-id "@ } @@ -425,8 +433,8 @@ switch ($Param1.ToLower()) { } default { if ([string]::IsNullOrWhiteSpace($Param2) -or [string]::IsNullOrWhiteSpace($Param3)) { - Write-ErrorMsg "Usage: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 [host[:port]] [stream] [api_key] [tenant_id]" - Write-ErrorMsg " Or: powershell -NoProfile -ExecutionPolicy Bypass -File .\ingest.ps1 [start|stop|restart|status|logs|debug|help]" + Write-ErrorMsg "Usage: $SCRIPT_CMD [host[:port]] [stream] [api_key] [tenant_id]" + Write-ErrorMsg " Or: $SCRIPT_CMD [start|stop|restart|status|logs|debug|help]" exit 1 } Setup-FluentBit -IngestorHost $Param1 -StreamName $Param2 -ApiKey $Param3 -TenantId $Param4 From dd1431493cd7144b55a4000a7413f8bce3de4e3d Mon Sep 17 00:00:00 2001 From: Nikhil Sinha <131262146+nikhilsinhaparseable@users.noreply.github.com> Date: Tue, 23 Jun 2026 15:38:37 +0530 Subject: [PATCH 45/47] update Cargo.toml for release v2.9.2 (#1700) --- Cargo.lock | 2 +- Cargo.toml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0ee3d1a05..e34b911e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4081,7 +4081,7 @@ dependencies = [ [[package]] name = "parseable" -version = "2.9.1" +version = "2.9.2" dependencies = [ "actix-cors", "actix-web", diff --git a/Cargo.toml b/Cargo.toml index b28e30c66..7745f05da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "parseable" -version = "2.9.1" +version = "2.9.2" authors = ["Parseable Team "] edition = "2024" rust-version = "1.91.0" @@ -219,8 +219,8 @@ arrow = "58.0.0" temp-dir = "0.1.14" [package.metadata.parseable_ui] -assets-url = "https://parseable-prism-build.s3.us-east-2.amazonaws.com/v2.9.1/build.zip" -assets-sha1 = "c87f5f5187e3d880bc311a8935fd844a5bbcc476" +assets-url = "https://parseable-prism-build.s3.us-east-2.amazonaws.com/v2.9.2/build.zip" +assets-sha1 = "bbda31faaae161269c24f2efca16f09d62359403" [features] debug = [] From 67681e544ddd40d4b0cad34b79448d18f2b300cd Mon Sep 17 00:00:00 2001 From: Nikhil Sinha <131262146+nikhilsinhaparseable@users.noreply.github.com> Date: Wed, 24 Jun 2026 14:20:17 +0530 Subject: [PATCH 46/47] fix: create checksum release action (#1701) the download artifacts should look at Parseable_OSS* binaries only so adding a filter to download only Parseable_OSS_* files and skip other intermediate files like *.dockerbuild --- .github/workflows/release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6b6ffd2b3..2f91d08eb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -327,6 +327,8 @@ jobs: steps: - name: Download artifacts created uses: actions/download-artifact@v4.1.7 + with: + pattern: Parseable_OSS_* - name: Run shasum command run: | From 89a07f30b1117a029eae60df900a0d1069d965c5 Mon Sep 17 00:00:00 2001 From: Anant Vindal Date: Wed, 24 Jun 2026 14:40:09 +0530 Subject: [PATCH 47/47] cargo fmt, rebase oss --- src/lib.rs | 2 +- src/query/mod.rs | 2 -- src/query/stream_schema_provider.rs | 2 +- src/utils/arrow/flight.rs | 6 +++--- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 648b252ed..fede3bae5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -73,9 +73,9 @@ use once_cell::sync::Lazy; pub use openid; use parseable::PARSEABLE; use reqwest::{Client, ClientBuilder}; +pub use utils as parseable_utils; pub use {opentelemetry, opentelemetry_otlp, opentelemetry_proto, opentelemetry_sdk}; pub use {tracing_actix_web, tracing_opentelemetry, tracing_subscriber}; -pub use utils as parseable_utils; // It is very unlikely that panic will occur when dealing with locks. pub const LOCK_EXPECT: &str = "Thread shouldn't panic while holding a lock"; diff --git a/src/query/mod.rs b/src/query/mod.rs index 09316baf6..6a553cc69 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -82,8 +82,6 @@ type BoxedBatchStream = SendableRecordBatchStream; /// Result type returned by query execution: either collected batches or a streaming adapter, plus field names. type QueryResult = Result<(Either, BoxedBatchStream>, Vec), ExecuteError>; -// pub static QUERY_SESSION: Lazy = -// Lazy::new(|| Query::create_session_context(PARSEABLE.storage())); pub static SCHEMA_PROVIDER: OnceCell> = OnceCell::new(); /// Additional physical optimizer rules registered by enterprise/plugins. diff --git a/src/query/stream_schema_provider.rs b/src/query/stream_schema_provider.rs index 09dbd2b43..b85595bcc 100644 --- a/src/query/stream_schema_provider.rs +++ b/src/query/stream_schema_provider.rs @@ -55,11 +55,11 @@ use crate::{ snapshot::{ManifestItem, Snapshot}, }, event::DEFAULT_TIMESTAMP_KEY, + hottier::{GLOBAL_HOTTIER, HotTierManager}, metrics::{ QUERY_CACHE_HIT, increment_files_scanned_in_hottier_by_date, increment_files_scanned_in_query_by_date, }, - hottier::{GLOBAL_HOTTIER, HotTierManager}, option::Mode, parseable::{DEFAULT_TENANT, PARSEABLE, STREAM_EXISTS}, storage::{ObjectStorage, ObjectStoreFormat}, diff --git a/src/utils/arrow/flight.rs b/src/utils/arrow/flight.rs index 787afae5a..9456ca6da 100644 --- a/src/utils/arrow/flight.rs +++ b/src/utils/arrow/flight.rs @@ -149,9 +149,9 @@ pub fn into_flight_data_stream( stream: datafusion::execution::SendableRecordBatchStream, ) -> Result, Box> { let record_stream = stream.map_err(|e| { - arrow_flight::error::FlightError::Arrow(arrow_schema::ArrowError::ExternalError( - Box::new(e), - )) + arrow_flight::error::FlightError::Arrow(arrow_schema::ArrowError::ExternalError(Box::new( + e, + ))) }); let write_options = IpcWriteOptions::default()