Skip to content

Commit 515f0cf

Browse files
authored
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
1 parent cefe210 commit 515f0cf

4 files changed

Lines changed: 98 additions & 44 deletions

File tree

src/event/mod.rs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -86,19 +86,22 @@ impl Event {
8686
}
8787
}
8888

89+
let stream = PARSEABLE.get_or_create_stream(&self.stream_name, &self.tenant_id);
90+
8991
if self.is_first_event {
9092
commit_schema(&self.stream_name, self.rb.schema(), &self.tenant_id)?;
93+
if !stream.get_static_schema_flag() {
94+
stream.stage_schema_file(stream.get_schema().as_ref().clone())?;
95+
}
9196
}
9297

93-
PARSEABLE
94-
.get_or_create_stream(&self.stream_name, &self.tenant_id)
95-
.push(
96-
&key,
97-
&self.rb,
98-
self.parsed_timestamp,
99-
&self.custom_partition_values,
100-
self.stream_type,
101-
)?;
98+
stream.push(
99+
&key,
100+
&self.rb,
101+
self.parsed_timestamp,
102+
&self.custom_partition_values,
103+
self.stream_type,
104+
)?;
102105

103106
let tenant = self.tenant_id.as_deref().unwrap_or(DEFAULT_TENANT);
104107
update_stats(

src/parseable/staging/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,6 @@ pub enum StagingError {
4040
TenantNotFound(#[from] TenantNotFound),
4141
#[error("{0}")]
4242
PoisonError(#[from] PoisonError<String>),
43+
#[error("JSON Error {0}")]
44+
Json(#[from] serde_json::Error),
4345
}

src/parseable/staging/writer.rs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ use arrow_array::RecordBatch;
2929
use arrow_ipc::writer::StreamWriter;
3030
use arrow_schema::Schema;
3131
use arrow_select::concat::concat_batches;
32-
use chrono::Utc;
32+
use chrono::{TimeDelta, Utc};
3333
use itertools::Itertools;
34+
use once_cell::sync::Lazy;
3435
use rand::distributions::{Alphanumeric, DistString};
3536
use tracing::error;
3637

@@ -41,7 +42,16 @@ use crate::{
4142

4243
use super::StagingError;
4344

44-
const DISK_WRITE_BATCH_ROWS: usize = 32_768;
45+
const DISK_WRITE_BATCH_MAX_AGE_SECS_VAR: &str = "DISK_WRITE_BATCH_MAX_AGE_SECS";
46+
static DISK_WRITE_BATCH_MAX_AGE_SECS: Lazy<i64> = Lazy::new(|| {
47+
if let Ok(var) = std::env::var(DISK_WRITE_BATCH_MAX_AGE_SECS_VAR)
48+
&& let Ok(var) = var.parse::<i64>()
49+
{
50+
var
51+
} else {
52+
1
53+
}
54+
});
4555

4656
#[derive(Default)]
4757
pub struct Writer {
@@ -58,13 +68,18 @@ impl Writer {
5868
rb: &RecordBatch,
5969
file_path: PathBuf,
6070
range: TimeRange,
71+
batch_rows: usize,
6172
) -> Result<(), StagingError> {
73+
let now = Utc::now();
6274
let pending = self.disk_pending.entry(filename.clone()).or_default();
6375
pending.rows += rb.num_rows();
6476
pending.range.get_or_insert(range);
77+
pending.first_seen.get_or_insert(now);
6578
pending.batches.push(rb.clone());
6679

67-
if pending.rows >= DISK_WRITE_BATCH_ROWS {
80+
let should_flush = pending.rows >= batch_rows.max(1)
81+
|| pending.is_older_than(now, TimeDelta::seconds(*DISK_WRITE_BATCH_MAX_AGE_SECS));
82+
if should_flush {
6883
self.flush_pending_disk(&filename, file_path)?;
6984
}
7085

@@ -104,8 +119,12 @@ impl Writer {
104119

105120
let mut flushable_pending = HashMap::new();
106121
let old_pending = std::mem::take(&mut self.disk_pending);
122+
let now = Utc::now();
107123
for (filename, pending) in old_pending {
108-
if !forced && pending.is_current() {
124+
if !forced
125+
&& pending.is_current()
126+
&& !pending.is_older_than(now, TimeDelta::seconds(*DISK_WRITE_BATCH_MAX_AGE_SECS))
127+
{
109128
self.disk_pending.insert(filename, pending);
110129
} else {
111130
flushable_pending.insert(filename, pending);
@@ -164,6 +183,7 @@ struct PendingDiskBatch {
164183
rows: usize,
165184
batches: Vec<RecordBatch>,
166185
range: Option<TimeRange>,
186+
first_seen: Option<chrono::DateTime<Utc>>,
167187
}
168188

169189
impl PendingDiskBatch {
@@ -172,6 +192,11 @@ impl PendingDiskBatch {
172192
.as_ref()
173193
.is_some_and(|range| range.contains(Utc::now()))
174194
}
195+
196+
fn is_older_than(&self, now: chrono::DateTime<Utc>, age: TimeDelta) -> bool {
197+
self.first_seen
198+
.is_some_and(|first_seen| now.signed_duration_since(first_seen) >= age)
199+
}
175200
}
176201

177202
pub struct DiskWriter {

src/parseable/streams.rs

Lines changed: 55 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use arrow_schema::{Field, Fields, Schema};
2323
use chrono::{NaiveDateTime, Timelike, Utc};
2424
use derive_more::derive::{Deref, DerefMut};
2525
use itertools::Itertools;
26+
use once_cell::sync::Lazy;
2627
use parquet::{
2728
arrow::ArrowWriter,
2829
basic::Encoding,
@@ -69,6 +70,17 @@ use super::{
6970
staging::{StagingError, reader::MergedReverseRecordReader, writer::Writer},
7071
};
7172

73+
const DISK_WRITE_BATCH_ROWS_VAR: &str = "DISK_WRITE_BATCH_ROWS";
74+
static DISK_WRITE_BATCH_ROWS: Lazy<usize> = Lazy::new(|| {
75+
if let Ok(var) = std::env::var(DISK_WRITE_BATCH_ROWS_VAR)
76+
&& let Ok(var) = var.parse::<usize>()
77+
{
78+
var
79+
} else {
80+
1
81+
}
82+
});
83+
7284
const INPROCESS_DIR_PREFIX: &str = "processing_";
7385
const METRIC_ROW_GROUP_PREP_IN_FLIGHT: usize = 1;
7486

@@ -115,6 +127,7 @@ pub struct Stream {
115127
pub data_path: PathBuf,
116128
pub options: Arc<Options>,
117129
pub writer: Mutex<Writer>,
130+
schema_writer: Mutex<()>,
118131
pub ingestor_id: Option<String>,
119132
}
120133
impl Stream {
@@ -134,6 +147,7 @@ impl Stream {
134147
data_path,
135148
options,
136149
writer: Mutex::new(Writer::default()),
150+
schema_writer: Mutex::new(()),
137151
ingestor_id,
138152
})
139153
}
@@ -181,7 +195,7 @@ impl Stream {
181195
);
182196
let file_path = self.data_path.join(&filename);
183197

184-
guard.push_disk(filename, record, file_path, range)?;
198+
guard.push_disk(filename, record, file_path, range, *DISK_WRITE_BATCH_ROWS)?;
185199
}
186200

187201
guard.mem.push(schema_key, record)?;
@@ -435,32 +449,22 @@ impl Stream {
435449
.collect()
436450
}
437451

438-
pub fn get_schemas_if_present(&self) -> Option<Vec<Schema>> {
439-
let Ok(dir) = self.data_path.read_dir() else {
440-
return None;
441-
};
452+
pub fn get_schemas_if_present(&self) -> Result<Vec<Schema>, StagingError> {
453+
let dir = self.data_path.read_dir()?;
442454

443455
let mut schemas: Vec<Schema> = Vec::new();
444456

445457
for file in dir.flatten() {
446458
if let Some(ext) = file.path().extension()
447459
&& ext.eq("schema")
448460
{
449-
let file = File::open(file.path()).expect("Schema File should exist");
461+
let file = File::open(file.path())?;
450462

451-
let schema = match serde_json::from_reader(file) {
452-
Ok(schema) => schema,
453-
Err(_) => continue,
454-
};
455-
schemas.push(schema);
463+
schemas.push(serde_json::from_reader(file)?);
456464
}
457465
}
458466

459-
if !schemas.is_empty() {
460-
Some(schemas)
461-
} else {
462-
None
463-
}
467+
Ok(schemas)
464468
}
465469

466470
/// 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 {
497501
// check if there is already a schema file in staging pertaining to this stream
498502
// if yes, then merge them and save
499503

500-
if let Some(mut schema) = schema {
504+
if let Some(schema) = schema {
501505
let static_schema_flag = self.get_static_schema_flag();
502506
if !static_schema_flag {
503-
// schema is dynamic, read from staging and merge if present
507+
self.stage_schema_file(schema)?;
508+
}
509+
}
510+
511+
Ok(())
512+
}
504513

505-
// need to add something before .schema to make the file have an extension of type `schema`
506-
let path = RelativePathBuf::from_iter([format!("{}.schema", self.stream_name)])
507-
.to_path(&self.data_path);
514+
pub fn stage_schema_file(&self, mut schema: Schema) -> Result<(), StagingError> {
515+
let _schema_writer = self.schema_writer.lock().map_err(|poisoned| {
516+
StagingError::PoisonError(PoisonError::new(format!(
517+
"Schema writer lock poisoned while staging schema for stream {} - {}",
518+
self.stream_name, poisoned
519+
)))
520+
})?;
508521

509-
let staging_schemas = self.get_schemas_if_present();
510-
if let Some(mut staging_schemas) = staging_schemas {
511-
staging_schemas.push(schema);
512-
schema = Schema::try_merge(staging_schemas)?;
513-
}
522+
// schema is dynamic, read from staging and merge if present
523+
fs::create_dir_all(&self.data_path)?;
514524

515-
// save the merged schema on staging disk
516-
// the path should be stream/.ingestor.{id}.schema
517-
info!("writing schema to path - {path:?}");
518-
write(path, to_bytes(&schema))?;
519-
}
525+
// need to add something before .schema to make the file have an extension of type `schema`
526+
let file_name = self.ingestor_id.as_ref().map_or_else(
527+
|| ".schema".to_owned(),
528+
|id| format!(".ingestor.{id}.schema"),
529+
);
530+
let path = RelativePathBuf::from_iter([file_name]).to_path(&self.data_path);
531+
let tmp_path = path.with_extension("schema.tmp");
532+
533+
let staging_schemas = self.get_schemas_if_present()?;
534+
if !staging_schemas.is_empty() {
535+
let mut staging_schemas = staging_schemas;
536+
staging_schemas.push(schema);
537+
schema = Schema::try_merge(staging_schemas)?;
520538
}
521539

540+
// save the merged schema on staging disk
541+
// the path should be stream/.ingestor.{id}.schema
542+
info!("writing schema to path - {path:?}");
543+
write(&tmp_path, to_bytes(&schema))?;
544+
fs::rename(tmp_path, path)?;
545+
522546
Ok(())
523547
}
524548

0 commit comments

Comments
 (0)