Skip to content

Commit 6515070

Browse files
committed
fix: correct historical bounds and segmented storage recovery
1 parent f050fa6 commit 6515070

12 files changed

Lines changed: 332 additions & 144 deletions

File tree

src/api/janus_api/tests.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -718,7 +718,7 @@ fn test_sliding_query_defined_baseline_snapshots_change_with_live_evaluation_tim
718718
StreamingSegmentedStorage::new(config).expect("Failed to create segmented storage"),
719719
);
720720

721-
for (timestamp, value) in [(86_400_002, "10"), (86_460_000, "20")] {
721+
for (timestamp, value) in [(86_340_002, "10"), (86_400_000, "20")] {
722722
storage
723723
.write_rdf(
724724
timestamp,
@@ -730,7 +730,7 @@ fn test_sliding_query_defined_baseline_snapshots_change_with_live_evaluation_tim
730730
.expect("Failed to write historical RDF event");
731731
}
732732
storage.flush().expect("Failed to flush storage");
733-
for (timestamp, value) in [(86_460_002, "30"), (86_520_000, "50")] {
733+
for (timestamp, value) in [(86_400_002, "30"), (86_460_000, "50")] {
734734
storage
735735
.write_rdf(
736736
timestamp,
@@ -784,14 +784,14 @@ HAVING(AVG(?value) > ?yesterdayAvgValue)
784784
let latest_rows = Arc::new(RwLock::new(HashMap::new()));
785785
assert_eq!(
786786
storage
787-
.query_rdf(86_400_001, 86_460_001)
787+
.query_rdf(86_340_001, 86_400_001)
788788
.expect("first historical range should query")
789789
.len(),
790790
2
791791
);
792792
assert_eq!(
793793
storage
794-
.query_rdf(86_460_001, 86_520_001)
794+
.query_rdf(86_400_001, 86_460_001)
795795
.expect("second historical range should query")
796796
.len(),
797797
2
@@ -910,8 +910,8 @@ HAVING(AVG(?value) > ?yesterdayAvgValue)
910910
let second_snapshot = baseline_registry
911911
.get_snapshot("http://example.org/yesterdayBaseline", 172_860_001)
912912
.expect("expected snapshot at second evaluation time");
913-
assert_eq!(first_snapshot.window_start, 86_400_001);
914-
assert_eq!(first_snapshot.window_end, 86_460_001);
915-
assert_eq!(second_snapshot.window_start, 86_460_001);
916-
assert_eq!(second_snapshot.window_end, 86_520_001);
913+
assert_eq!(first_snapshot.window_start, 86_340_001);
914+
assert_eq!(first_snapshot.window_end, 86_400_001);
915+
assert_eq!(second_snapshot.window_start, 86_400_001);
916+
assert_eq!(second_snapshot.window_end, 86_460_001);
917917
}

src/execution/historical_executor.rs

Lines changed: 24 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ use crate::parsing::janusql_parser::WindowDefinition;
1919
use crate::querying::oxigraph_adapter::OxigraphAdapter;
2020
use crate::storage::segmented_storage::StreamingSegmentedStorage;
2121
use crate::stream::operators::historical_fixed_window::HistoricalFixedWindowOperator;
22-
use crate::stream::operators::historical_sliding_window::HistoricalSlidingWindowOperator;
2322
use oxigraph::model::{GraphName, NamedNode, Quad};
2423
use rsp_rs::QuadContainer;
2524
use std::collections::{HashMap, HashSet};
@@ -166,23 +165,16 @@ impl HistoricalExecutor {
166165
window: &WindowDefinition,
167166
sparql_query: &'a str,
168167
) -> impl Iterator<Item = Result<Vec<HashMap<String, String>>, JanusApiError>> + 'a {
169-
let offset = window.offset.unwrap_or(0);
170-
let width = window.width;
171-
let slide = window.slide;
172-
173168
let now = std::time::SystemTime::now()
174169
.duration_since(std::time::UNIX_EPOCH)
175170
.unwrap_or_default()
176171
.as_millis() as u64;
177172

178-
let start_time = now.saturating_sub(offset);
179-
180173
SlidingWindowIterator {
181174
executor: self,
182-
current_start: start_time,
183-
evaluation_time: now,
184-
width,
185-
slide,
175+
window: window.clone(),
176+
current_evaluation_time: now,
177+
latest_evaluation_time: now,
186178
sparql_query: sparql_query.to_string(),
187179
}
188180
}
@@ -397,47 +389,40 @@ impl HistoricalExecutor {
397389
&self,
398390
window: &WindowDefinition,
399391
) -> Result<(u64, u64), JanusApiError> {
400-
// For fixed windows: use explicit start/end
401-
if let (Some(start), Some(end)) = (window.start, window.end) {
402-
return Ok((start, end));
403-
}
404-
405-
// For sliding windows: calculate from offset and width
406-
if let Some(offset) = window.offset {
407-
let now = std::time::SystemTime::now()
392+
let evaluation_time = if window.offset.is_some() {
393+
std::time::SystemTime::now()
408394
.duration_since(std::time::UNIX_EPOCH)
409395
.map_err(|e| JanusApiError::ExecutionError(format!("System time error: {}", e)))?
410-
.as_millis() as u64;
411-
412-
let start = now.saturating_sub(offset);
413-
let end = start + window.width;
414-
return Ok((start, end));
415-
}
396+
.as_millis() as u64
397+
} else {
398+
window.end.unwrap_or_default()
399+
};
416400

417-
Err(JanusApiError::ExecutionError(
418-
"Window definition must have either (start, end) or (offset, width)".to_string(),
419-
))
401+
window.resolve_historical_bounds(evaluation_time).ok_or_else(|| {
402+
JanusApiError::ExecutionError(
403+
"Window definition cannot resolve complete historical bounds".to_string(),
404+
)
405+
})
420406
}
421407
}
422408

423409
/// Iterator for sliding windows that queries storage directly
424410
struct SlidingWindowIterator<'a> {
425411
executor: &'a HistoricalExecutor,
426-
current_start: u64,
427-
evaluation_time: u64,
428-
width: u64,
429-
slide: u64,
412+
window: WindowDefinition,
413+
current_evaluation_time: u64,
414+
latest_evaluation_time: u64,
430415
sparql_query: String,
431416
}
432417

433418
impl<'a> Iterator for SlidingWindowIterator<'a> {
434419
type Item = Result<Vec<HashMap<String, String>>, JanusApiError>;
435420

436421
fn next(&mut self) -> Option<Self::Item> {
437-
let window_start = self.current_start;
438-
let window_end = window_start.checked_add(self.width)?;
422+
let (window_start, window_end) =
423+
self.window.resolve_historical_bounds(self.current_evaluation_time)?;
439424

440-
if window_end > self.evaluation_time {
425+
if window_end > self.latest_evaluation_time {
441426
return None;
442427
}
443428

@@ -453,7 +438,8 @@ impl<'a> Iterator for SlidingWindowIterator<'a> {
453438
let result = self.executor.execute_sparql_on_events(&events, &self.sparql_query);
454439

455440
// Advance window
456-
self.current_start += self.slide;
441+
self.current_evaluation_time =
442+
self.current_evaluation_time.checked_add(self.window.slide)?;
457443

458444
Some(result)
459445
}
@@ -557,7 +543,7 @@ mod tests {
557543
.execute_sliding_windows(&window, "SELECT ?s WHERE { ?s ?p ?o }")
558544
.collect::<Vec<_>>();
559545

560-
assert_eq!(results.len(), 4);
546+
assert_eq!(results.len(), 6);
561547
assert!(results.iter().all(|result| result.is_ok()));
562548
}
563549

@@ -586,7 +572,7 @@ mod tests {
586572
.execute_sliding_windows(&window, "SELECT ?s WHERE { ?s ?p ?o }")
587573
.collect::<Vec<_>>();
588574

589-
assert_eq!(results.len(), 2);
575+
assert_eq!(results.len(), 3);
590576
assert!(results.iter().all(|result| result.is_ok()));
591577
}
592578

src/parsing/janusql_parser/ast.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,9 @@ impl WindowDefinition {
7474
return None;
7575
}
7676

77-
let historical_start = evaluation_time.saturating_sub(offset);
78-
let historical_end = historical_start.checked_add(range)?;
77+
// Historical sliding intervals are [T - OFFSET - RANGE, T - OFFSET].
78+
let historical_end = evaluation_time.checked_sub(offset)?;
79+
let historical_start = historical_end.checked_sub(range)?;
7980
Some((historical_start, historical_end))
8081
}
8182
}

src/storage/segmented_storage/background.rs

Lines changed: 8 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -77,55 +77,26 @@ impl StreamingSegmentedStorage {
7777
events
7878
};
7979

80-
let events_ref = &mut events_to_flush;
8180
let flush_result = (|| -> std::io::Result<()> {
82-
let new_segment = Self::write_segment_files(&config, events_ref)?;
83-
84-
{
85-
let mut segments = segments.write().unwrap();
86-
segments.push(new_segment);
87-
segments.sort_by_key(|s| s.start_timstamp);
88-
}
89-
81+
// The dictionary must be durable before a segment referencing its IDs is committed.
9082
let dict_path = std::path::Path::new(&config.segment_base_path).join("dictionary.bin");
9183
let dict = dictionary.read().unwrap();
9284
dict.save_to_file(&dict_path)?;
9385

86+
let new_segment = Self::write_segment_files(&config, &mut events_to_flush)?;
87+
88+
let mut segments = segments.write().unwrap();
89+
segments.push(new_segment);
90+
segments.sort_by_key(|s| s.start_timstamp);
91+
9492
Ok(())
9593
})();
9694

9795
if let Err(err) = flush_result {
98-
Self::restore_failed_background_flush(&batch_buffer, &events_to_flush);
96+
Self::restore_failed_flush(&batch_buffer, &events_to_flush);
9997
return Err(err);
10098
}
10199

102100
Ok(())
103101
}
104-
105-
fn restore_failed_background_flush(batch_buffer: &Arc<RwLock<BatchBuffer>>, events: &[Event]) {
106-
if events.is_empty() {
107-
return;
108-
}
109-
110-
let mut buffer = batch_buffer.write().unwrap();
111-
for event in events.iter().rev().cloned() {
112-
buffer.events.push_front(event);
113-
buffer.total_bytes += std::mem::size_of::<Event>();
114-
}
115-
116-
let restored_oldest = events.first().map(|event| event.timestamp);
117-
let restored_newest = events.last().map(|event| event.timestamp);
118-
119-
buffer.oldest_timestamp_bound = match (buffer.oldest_timestamp_bound, restored_oldest) {
120-
(Some(existing), Some(restored)) => Some(existing.min(restored)),
121-
(None, restored) => restored,
122-
(existing, None) => existing,
123-
};
124-
125-
buffer.newest_timestamp_bound = match (buffer.newest_timestamp_bound, restored_newest) {
126-
(Some(existing), Some(restored)) => Some(existing.max(restored)),
127-
(None, restored) => restored,
128-
(existing, None) => existing,
129-
};
130-
}
131102
}

src/storage/segmented_storage/mod.rs

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,39 @@ impl StreamingSegmentedStorage {
3535

3636
// Load or create dictionary
3737
let dict_path = std::path::Path::new(&config.segment_base_path).join("dictionary.bin");
38+
let has_persisted_segments = std::fs::read_dir(&config.segment_base_path)?.any(|entry| {
39+
entry.ok().is_some_and(|entry| {
40+
entry.file_type().map(|kind| kind.is_file()).unwrap_or(false)
41+
&& entry
42+
.file_name()
43+
.to_str()
44+
.is_some_and(|name| name.starts_with("segment-") && name.ends_with(".log"))
45+
})
46+
});
3847
let dictionary = if dict_path.exists() {
3948
match Dictionary::load_from_file(&dict_path) {
4049
Ok(dict) => dict,
4150
Err(e) => {
42-
eprintln!("Warning: Failed to load dictionary: {}, creating new one", e);
51+
if has_persisted_segments {
52+
return Err(std::io::Error::new(
53+
std::io::ErrorKind::InvalidData,
54+
format!(
55+
"Cannot open persisted segment data without a readable dictionary '{}': {e}",
56+
dict_path.display()
57+
),
58+
));
59+
}
4360
Dictionary::new()
4461
}
4562
}
63+
} else if has_persisted_segments {
64+
return Err(std::io::Error::new(
65+
std::io::ErrorKind::NotFound,
66+
format!(
67+
"Cannot open persisted segment data because dictionary '{}' is missing",
68+
dict_path.display()
69+
),
70+
));
4671
} else {
4772
Dictionary::new()
4873
};
@@ -158,9 +183,7 @@ impl StreamingSegmentedStorage {
158183
/// This is useful when you need to ensure data is persisted immediately.
159184
pub fn flush(&self) -> std::io::Result<()> {
160185
self.ensure_background_flush_healthy()?;
161-
self.flush_batch_buffer_to_segment()?;
162-
self.save_dictionary()?;
163-
Ok(())
186+
self.flush_batch_buffer_to_segment()
164187
}
165188

166189
/// Shutdown the storage system gracefully, ensuring all data is flushed to disk.

0 commit comments

Comments
 (0)