Skip to content

Commit e77d3c6

Browse files
authored
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
1 parent cbfbfa1 commit e77d3c6

18 files changed

Lines changed: 516 additions & 271 deletions

File tree

Cargo.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,14 @@ tokio = { version = "^1.43", default-features = false, features = [
9090
tokio-stream = { version = "0.1.17", features = ["fs"] }
9191
tokio-util = { version = "0.7" }
9292

93+
# perf
94+
hotpath = { version = "0.16.0", optional = true, features = [
95+
"hotpath",
96+
"hotpath-cpu",
97+
"hotpath-alloc",
98+
"tokio"
99+
] }
100+
93101
# # Logging and Metrics
94102
# opentelemetry-proto = { version = "0.30.0", features = [
95103
# "gen-tonic",
@@ -211,6 +219,15 @@ assets-sha1 = "a7523ef819d38678275ae165c443564b2f9a3fc1"
211219

212220
[features]
213221
debug = []
222+
hotpath = [
223+
"dep:hotpath",
224+
"hotpath/hotpath",
225+
"hotpath/hotpath-cpu",
226+
"hotpath/hotpath-alloc",
227+
"hotpath/tokio",
228+
]
229+
hotpath-alloc = ["hotpath"]
230+
hotpath-cpu = ["hotpath"]
214231
kafka = [
215232
"rdkafka",
216233
"rdkafka/ssl-vendored",

src/cli.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,15 @@ pub struct Options {
559559
)]
560560
pub max_field_statistics: usize,
561561

562+
// collect statistics for dataset fields
563+
#[arg(
564+
long,
565+
env = "P_COLLECT_DATASET_STATS",
566+
default_value = "true",
567+
help = "Collect statistics for dataset fields"
568+
)]
569+
pub collect_dataset_stats: bool,
570+
562571
#[arg(
563572
long,
564573
env = "P_MAX_EVENT_PAYLOAD_SIZE",

src/event/format/json.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ impl EventFormat for Event {
6060

6161
// convert the incoming json to a vector of json values
6262
// also extract the arrow schema, tags and metadata from the incoming json
63+
#[cfg_attr(feature = "hotpath", hotpath::measure)]
6364
fn to_data(
6465
self,
6566
schema: &HashMap<String, Arc<Field>>,
@@ -156,7 +157,7 @@ impl EventFormat for Event {
156157
infer_schema.clone(),
157158
]).map_err(|err| anyhow!("Could not merge schema of this event with that of the existing stream. {:?}", err))?;
158159
is_first = true;
159-
let schema = infer_schema
160+
let schema: Vec<Arc<Field>> = infer_schema
160161
.fields
161162
.iter()
162163
.filter(|field| !field.data_type().is_null())
@@ -327,6 +328,10 @@ fn rename_json_keys(values: Vec<Value>) -> Result<Vec<Value>, anyhow::Error> {
327328
.into_iter()
328329
.map(|value| {
329330
if let Value::Object(map) = value {
331+
if !map.keys().any(|key| key.starts_with('@')) {
332+
return Ok(Value::Object(map));
333+
}
334+
330335
// Collect original keys to check for collisions
331336
let original_keys: HashSet<String> = map.keys().cloned().collect();
332337

src/event/format/mod.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ pub trait EventFormat: Sized {
162162
/// Returns the UTC time at ingestion
163163
fn get_p_timestamp(&self) -> DateTime<Utc>;
164164

165+
#[cfg_attr(feature = "hotpath", hotpath::measure)]
165166
fn into_recordbatch(
166167
self,
167168
storage_schema: &HashMap<String, Arc<Field>>,
@@ -606,6 +607,28 @@ pub fn rename_per_record_type_mismatches(
606607
let Value::Object(map) = value else {
607608
return value;
608609
};
610+
let needs_rename = map.iter().any(|(key, val)| {
611+
if val.is_null() {
612+
return false;
613+
}
614+
let target_type = existing_schema
615+
.get(key)
616+
.map(|f| f.data_type())
617+
.or_else(|| inferred_types.get(key.as_str()).copied());
618+
let Some(target_type) = target_type else {
619+
return false;
620+
};
621+
if (val.is_array() || val.is_object())
622+
&& (target_type.is_list()
623+
|| matches!(target_type, DataType::Struct(_) | DataType::Map(_, _)))
624+
{
625+
return false;
626+
}
627+
!value_compatible_with_type(val, target_type, schema_version)
628+
});
629+
if !needs_rename {
630+
return Value::Object(map);
631+
}
609632
let new_map: serde_json::Map<String, Value> = map
610633
.into_iter()
611634
.map(|(key, val)| {

src/event/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ impl Event {
7272
is_first_event = self.is_first_event
7373
)
7474
)]
75+
#[cfg_attr(feature = "hotpath", hotpath::measure)]
7576
pub fn process(self) -> Result<(), EventError> {
7677
let mut key = get_schema_key(&self.rb.schema().fields);
7778
if self.time_partition.is_some() {

src/handlers/http/health_check.rs

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ use once_cell::sync::Lazy;
3232
use tokio::{sync::Mutex, task::JoinSet};
3333
use tracing::{error, info};
3434

35+
use crate::sync::shutdown_local_sync_flush_and_convert;
3536
use crate::utils::get_tenant_id_from_request;
3637
use crate::{parseable::PARSEABLE, storage::object_storage::sync_all_streams};
3738

@@ -84,20 +85,7 @@ async fn perform_sync_operations() {
8485
}
8586

8687
async fn perform_local_sync() {
87-
let mut local_sync_joinset = JoinSet::new();
88-
89-
// Sync staging
90-
PARSEABLE
91-
.streams
92-
.flush_and_convert(&mut local_sync_joinset, false, true);
93-
94-
while let Some(res) = local_sync_joinset.join_next().await {
95-
match res {
96-
Ok(Ok(_)) => info!("Successfully converted arrow files to parquet."),
97-
Ok(Err(err)) => error!("Failed to convert arrow files to parquet. {err:?}"),
98-
Err(err) => error!("Failed to join async task: {err}"),
99-
}
100-
}
88+
shutdown_local_sync_flush_and_convert().await;
10189
}
10290

10391
async fn perform_object_store_sync() {

src/handlers/http/modal/utils/ingest_utils.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ pub async fn flatten_and_push_logs(
156156
skip(json, log_source, p_custom_fields, time_partition, telemetry_type, tenant_id),
157157
fields(stream_name, record_count = tracing::field::Empty)
158158
)]
159+
#[cfg_attr(feature = "hotpath", hotpath::measure)]
159160
pub fn push_logs(
160161
stream_name: &str,
161162
json: Value,

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ pub use datafusion;
6262
pub use handlers::http::modal::{
6363
ParseableServer, ingest_server::IngestServer, query_server::QueryServer, server::Server,
6464
};
65+
#[cfg(feature = "hotpath")]
66+
pub use hotpath;
6567
use once_cell::sync::Lazy;
6668
pub use openid;
6769
use parseable::PARSEABLE;

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ use tracing_subscriber::util::SubscriberInitExt;
3232
use tracing_subscriber::{EnvFilter, Registry, fmt};
3333

3434
#[actix_web::main]
35+
#[cfg_attr(feature = "hotpath", hotpath::main)]
3536
async fn main() -> anyhow::Result<()> {
3637
init_logger();
3738
// Install the rustls crypto provider before any TLS operations.

src/otel/logs.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ pub fn flatten_log_record(log_record: &LogRecord) -> Map<String, Value> {
138138

139139
if log_record.body.is_some() {
140140
let body = &log_record.body;
141-
let body_json = collect_json_from_values(body, &"body".to_string());
141+
let body_json = collect_json_from_values(body, "body");
142142
for (key, value) in &body_json {
143143
// Always insert the original body field as is
144144
log_record_json.insert(key.clone(), value.clone());

0 commit comments

Comments
 (0)