1use std::collections::{BTreeMap, HashMap, VecDeque};
4use std::fs::File;
5use std::io::{BufRead, Read, Seek, SeekFrom, Write};
6use std::path::{Path, PathBuf};
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::sync::{Arc, Mutex, OnceLock, Weak};
9
10use chrono::Utc;
11use serde::{Deserialize, Serialize};
12use vtcode_commons::VtCodePaths;
13use vtcode_exec_events::{EVENT_SCHEMA_VERSION, ThreadEvent, ThreadItemDetails, VersionedThreadEvent};
14
15use crate::error::SessionStoreError;
16
17pub(crate) const SESSION_LOCK_FILE: &str = "session.lock";
19use crate::manifest::{ManifestStore, PendingCapRewrite};
20use crate::session_dir;
21
22mod append;
23mod eviction;
24mod manifest;
25mod scan;
26#[cfg(test)]
27mod sync_tests;
28
29use eviction::default_eviction_summary_hook;
30pub use manifest::{SessionManifest, TurnIndex, TurnIndexEntry};
31use scan::{
32 count_persisted_event_records, decode_events, infer_scan_turn_base, requires_full_lifecycle_validation,
33 valid_lifecycle_payload,
34};
35
36pub const DEFAULT_MAX_EVENTS: usize = 10_000;
39
40const MAX_WRITE_BUFFER_BYTES: usize = 64 * 1024;
43
44pub type EvictionSummaryHook = Arc<dyn Fn(&[ThreadEvent]) -> Result<(), SessionStoreError> + Send + Sync>;
51
52#[derive(Debug, Deserialize)]
58struct VersionedEventKind<'a> {
59 #[serde(rename = "schema_version", borrow)]
60 _schema_version: &'a str,
61 #[serde(borrow)]
62 event: EventKind<'a>,
63}
64
65#[derive(Debug, Deserialize)]
66struct EventKind<'a> {
67 #[serde(rename = "type", borrow)]
68 kind: &'a str,
69}
70
71#[derive(Serialize)]
78struct BorrowedVersionedEvent<'a> {
79 schema_version: &'a str,
80 event: &'a ThreadEvent,
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88enum LifecycleKind {
89 ThreadStarted,
90 ThreadCompleted,
91 TurnStarted,
92 TurnCompleted,
93 TurnFailed,
94 Other,
95}
96
97impl LifecycleKind {
98 #[inline]
100 fn from_event(event: &ThreadEvent) -> Self {
101 match event {
102 ThreadEvent::ThreadStarted(_) => Self::ThreadStarted,
103 ThreadEvent::ThreadCompleted(_) => Self::ThreadCompleted,
104 ThreadEvent::TurnStarted(_) => Self::TurnStarted,
105 ThreadEvent::TurnCompleted(_) => Self::TurnCompleted,
106 ThreadEvent::TurnFailed(_) => Self::TurnFailed,
107 _ => Self::Other,
108 }
109 }
110
111 #[inline]
113 fn from_kind(kind: &str) -> Self {
114 match kind {
115 "thread.started" => Self::ThreadStarted,
116 "thread.completed" => Self::ThreadCompleted,
117 "turn.started" => Self::TurnStarted,
118 "turn.completed" => Self::TurnCompleted,
119 "turn.failed" => Self::TurnFailed,
120 _ => Self::Other,
121 }
122 }
123}
124
125struct LogState {
128 manifest: SessionManifest,
129 index: TurnIndex,
130 in_turn: bool,
134 next_offset: u64,
138 write_buf: Vec<u8>,
141 needs_sync: bool,
143 write_failed: bool,
145 needs_separator: bool,
147}
148
149#[derive(Debug, Clone, Copy)]
150struct CapEvictionPlan {
151 truncate_offset: u64,
152 evicted_event_count: u64,
153 evicted_turn_count: usize,
154}
155
156impl LogState {
157 fn new(session_id: &str) -> Self {
158 Self {
159 manifest: SessionManifest::new(session_id),
160 index: TurnIndex::default(),
161 in_turn: false,
162 next_offset: 0,
163 write_buf: Vec::with_capacity(65536),
164 needs_sync: false,
165 write_failed: false,
166 needs_separator: false,
167 }
168 }
169
170 fn serialize_event(&mut self, event: &ThreadEvent) -> Result<(u64, u64), SessionStoreError> {
179 let start = self.next_offset;
180 let buf_len_before = self.write_buf.len();
181 if let Err(err) = serde_json::to_writer(
182 &mut self.write_buf,
183 &BorrowedVersionedEvent { schema_version: EVENT_SCHEMA_VERSION, event },
184 ) {
185 self.write_buf.truncate(buf_len_before);
186 return Err(err.into());
187 }
188 self.write_buf.push(b'\n');
189 let written = self.write_buf.len() - buf_len_before;
190 let end = start + written as u64;
191 self.next_offset = end;
192 Ok((start, end))
193 }
194
195 fn apply_lifecycle_event(&mut self, kind: LifecycleKind, start: u64, end: u64) -> bool {
208 let is_boundary = match kind {
209 LifecycleKind::ThreadStarted => {
210 self.manifest.status = "active".to_string();
211 false
212 }
213 LifecycleKind::ThreadCompleted => {
214 self.manifest.status = "completed".to_string();
215 true
216 }
217 LifecycleKind::TurnStarted => {
218 self.manifest.status = "active".to_string();
219 self.in_turn = true;
220 let n = self.manifest.turn_count + 1;
221 self.index.entries.push_back(TurnIndexEntry {
222 turn_number: n,
223 start_offset: start,
224 end_offset: end,
225 event_count: 1,
226 ts: now_rfc3339(),
227 });
228 false
229 }
230 LifecycleKind::TurnCompleted | LifecycleKind::TurnFailed => {
231 if self.in_turn {
232 if let Some(entry) = self.index.entries.back_mut() {
233 entry.end_offset = end;
234 entry.event_count += 1;
235 self.manifest.turn_count = self.manifest.turn_count.max(entry.turn_number);
240 }
241 self.in_turn = false;
242 }
243 true
244 }
245 LifecycleKind::Other => {
246 if self.in_turn
247 && let Some(entry) = self.index.entries.back_mut()
248 {
249 entry.end_offset = end;
250 entry.event_count += 1;
251 }
252 false
253 }
254 };
255 self.manifest.in_turn = Some(self.in_turn);
260 is_boundary
261 }
262
263 fn plan_cap_eviction(&self, max_events: usize) -> Option<CapEvictionPlan> {
270 if max_events == 0 || self.manifest.event_count <= max_events as u64 {
271 return None;
272 }
273 let mut evicted_event_count = 0u64;
274 let mut truncate_offset = 0u64;
275 let mut evicted_turn_count = 0;
276 for (entry_index, oldest) in self.index.entries.iter().enumerate() {
277 if self.in_turn && entry_index + 1 == self.index.entries.len() {
284 break;
285 }
286 if self.manifest.event_count.saturating_sub(evicted_event_count) <= max_events as u64 {
287 break;
288 }
289 truncate_offset = oldest.end_offset;
290 evicted_event_count += oldest.event_count;
291 evicted_turn_count += 1;
292 }
293 if truncate_offset == 0 || evicted_turn_count == 0 {
294 None
295 } else {
296 Some(CapEvictionPlan {
297 truncate_offset,
298 evicted_event_count,
299 evicted_turn_count,
300 })
301 }
302 }
303
304 fn apply_cap_eviction(&mut self, plan: CapEvictionPlan, next_offset: u64) {
305 for _ in 0..plan.evicted_turn_count {
306 let _ = self.index.entries.pop_front();
307 }
308 for entry in &mut self.index.entries {
309 entry.start_offset = entry.start_offset.saturating_sub(plan.truncate_offset);
310 entry.end_offset = entry.end_offset.saturating_sub(plan.truncate_offset);
311 }
312 self.next_offset = next_offset;
313 self.manifest.event_count = self.manifest.event_count.saturating_sub(plan.evicted_event_count);
314 self.manifest.retained_turn_base = Some(self.retained_turn_base_after_eviction(0));
315 }
316
317 fn retained_turn_base_after_eviction(&self, evicted_turn_count: usize) -> u64 {
318 self.index
319 .entries
320 .get(evicted_turn_count)
321 .map(|entry| entry.turn_number)
322 .or_else(|| self.manifest.turn_count.checked_add(1))
323 .unwrap_or(1)
324 .max(1)
325 }
326}
327
328struct SessionShared {
336 file: Mutex<Option<File>>,
337 state: Mutex<LogState>,
338 eviction_lock: Mutex<()>,
339 initialized: AtomicBool,
340 liveness_lock: Mutex<Option<File>>,
347}
348
349#[derive(Debug, Clone, Copy, PartialEq, Eq)]
351enum OpenMode {
352 Shared,
355 Exclusive,
359}
360
361fn shared_session(
366 events_path: &Path,
367 session_id: &str,
368 mode: OpenMode,
369) -> Result<Arc<SessionShared>, SessionStoreError> {
370 static SESSION_SHARED: OnceLock<Mutex<HashMap<PathBuf, Weak<SessionShared>>>> = OnceLock::new();
371
372 let key = events_path
373 .parent()
374 .and_then(|parent| vtcode_commons::paths::canonicalize(parent).ok())
375 .and_then(|parent| events_path.file_name().map(|name| parent.join(name)))
376 .unwrap_or_else(|| events_path.to_path_buf());
377 let registry = SESSION_SHARED.get_or_init(|| Mutex::new(HashMap::new()));
378 let mut shared_by_path = match registry.lock() {
379 Ok(locks) => locks,
380 Err(poisoned) => poisoned.into_inner(),
381 };
382 shared_by_path.retain(|_, shared| shared.strong_count() > 0);
386 if let Some(shared) = shared_by_path.get(&key).and_then(Weak::upgrade) {
387 if mode == OpenMode::Exclusive {
391 shared.ensure_exclusive_liveness(events_path, session_id)?;
392 }
393 return Ok(shared);
394 }
395
396 let file = VtCodePaths::open_private_append_file(events_path)
397 .map_err(|error| SessionStoreError::io(events_path.to_path_buf(), std::io::Error::other(error)))?;
398 let liveness_lock = match mode {
399 OpenMode::Shared => acquire_liveness_lock(events_path, session_id),
400 OpenMode::Exclusive => acquire_liveness_lock_strict(events_path, session_id)?,
401 };
402 let shared = Arc::new(SessionShared {
403 file: Mutex::new(Some(file)),
404 state: Mutex::new(LogState::new(session_id)),
405 eviction_lock: Mutex::new(()),
406 initialized: AtomicBool::new(false),
407 liveness_lock: Mutex::new(liveness_lock),
408 });
409 shared_by_path.insert(key, Arc::downgrade(&shared));
410 Ok(shared)
411}
412
413fn acquire_liveness_lock(events_path: &Path, session_id: &str) -> Option<File> {
422 acquire_liveness_lock_strict(events_path, session_id).unwrap_or(None)
423}
424
425fn acquire_liveness_lock_strict(events_path: &Path, session_id: &str) -> Result<Option<File>, SessionStoreError> {
432 let Some(dir) = events_path.parent() else {
433 return Ok(None);
434 };
435 let lock_path = dir.join(SESSION_LOCK_FILE);
436 if VtCodePaths::write_private_file_atomic_if_absent(&lock_path, b"").is_err() {
437 return Ok(None);
438 }
439 let file = match std::fs::OpenOptions::new().read(true).write(true).open(&lock_path) {
440 Ok(file) => file,
441 Err(_) => return Ok(None),
442 };
443 match file.try_lock().map_err(std::io::Error::from) {
444 Ok(()) => Ok(Some(file)),
445 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
446 Err(SessionStoreError::SessionBusy { session: session_id.to_owned() })
447 }
448 Err(_) => Ok(None),
449 }
450}
451
452impl SessionShared {
453 fn ensure_exclusive_liveness(&self, events_path: &Path, session_id: &str) -> Result<(), SessionStoreError> {
457 let mut slot = self.liveness_lock.lock().map_err(poison)?;
458 if slot.is_none() {
459 *slot = acquire_liveness_lock_strict(events_path, session_id)?;
460 }
461 Ok(())
462 }
463}
464
465pub struct SessionEventLog {
471 events_path: PathBuf,
472 manifest_store: ManifestStore,
473 shared: Arc<SessionShared>,
474 max_events: usize,
475 eviction_summary_hook: EvictionSummaryHook,
476}
477
478impl SessionEventLog {
479 pub(crate) fn open(workspace: &Path, session_id: &str, max_events: usize) -> Result<Self, SessionStoreError> {
482 let dir = session_dir(workspace, session_id);
483 let hook = default_eviction_summary_hook(dir.join(crate::DERIVED_DIR), session_id.to_string());
484 Self::open_inner(workspace, session_id, max_events, hook, OpenMode::Shared)
485 }
486
487 pub(crate) fn open_exclusive(
492 workspace: &Path,
493 session_id: &str,
494 max_events: usize,
495 ) -> Result<Self, SessionStoreError> {
496 let dir = session_dir(workspace, session_id);
497 let hook = default_eviction_summary_hook(dir.join(crate::DERIVED_DIR), session_id.to_string());
498 Self::open_inner(workspace, session_id, max_events, hook, OpenMode::Exclusive)
499 }
500
501 pub fn open_with_eviction_summary(
507 workspace: &Path,
508 session_id: &str,
509 max_events: usize,
510 eviction_summary_hook: EvictionSummaryHook,
511 ) -> Result<Self, SessionStoreError> {
512 Self::open_inner(workspace, session_id, max_events, eviction_summary_hook, OpenMode::Shared)
513 }
514
515 fn open_inner(
516 workspace: &Path,
517 session_id: &str,
518 max_events: usize,
519 eviction_summary_hook: EvictionSummaryHook,
520 mode: OpenMode,
521 ) -> Result<Self, SessionStoreError> {
522 let dir = session_dir(workspace, session_id);
523 crate::ensure_private_directory(&crate::sessions_root(workspace))?;
524 crate::ensure_private_directory(&dir)?;
525 crate::ensure_private_directory(&dir.join(crate::DERIVED_DIR))?;
526 crate::ensure_private_directory(&dir.join("index"))?;
527 let events_path = dir.join("events.jsonl");
528 let manifest_store = ManifestStore::new(dir.clone());
529 let pending_rewrite = manifest_store.load_pending_cap_rewrite()?;
530 let shared = shared_session(&events_path, session_id, mode)?;
531 let log = Self {
532 events_path: events_path.clone(),
533 manifest_store,
534 shared,
535 max_events,
536 eviction_summary_hook,
537 };
538 let _eviction_guard = log.shared.eviction_lock.lock().map_err(poison)?;
539 if !log.shared.initialized.load(Ordering::Acquire) {
542 let manifest_opt = log.manifest_store.load_manifest()?;
543 let index_opt = log.manifest_store.load_turn_index()?;
544 let file_len = log.event_file_metadata_len()?;
545 let needs_separator = if file_len == 0 {
546 false
547 } else {
548 let mut slot = log.shared.file.lock().map_err(poison)?;
549 let file = slot.as_mut().ok_or_else(|| log.event_file_unavailable())?;
550 file.seek(SeekFrom::End(-1))
551 .map_err(|error| SessionStoreError::io(&events_path, error))?;
552 let mut last = [0];
553 file.read_exact(&mut last)
554 .map_err(|error| SessionStoreError::io(&events_path, error))?;
555 last[0] != b'\n'
556 };
557 let pending_rewrite_matches_file = pending_rewrite.as_ref().is_some_and(|pending| {
558 pending.new_file_len == file_len && pending.new_file_len < pending.previous_file_len
559 });
560 match (&manifest_opt, &index_opt) {
561 (Some(manifest), Some(index))
562 if !pending_rewrite_matches_file
563 && manifest.in_turn.is_some()
564 && manifest.persisted_file_len == Some(file_len)
565 && index.is_valid_for_file(file_len)
566 && index.is_consistent_with_manifest(manifest) =>
567 {
568 let mut st = log.shared.state.lock().map_err(poison)?;
569 st.in_turn = manifest.in_turn.unwrap_or(false);
570 st.manifest = manifest.clone();
571 st.index = index.clone();
572 st.next_offset = file_len;
573 }
574 _ => {
575 let scan_turn_base = infer_scan_turn_base(
576 manifest_opt.as_ref(),
577 index_opt.as_ref(),
578 pending_rewrite.as_ref().filter(|_| pending_rewrite_matches_file),
579 file_len,
580 );
581 {
582 let mut st = log.shared.state.lock().map_err(poison)?;
583 if let Some(previous) = manifest_opt.as_ref() {
584 st.manifest = previous.clone();
585 }
586 st.manifest.turn_count = scan_turn_base.saturating_sub(1);
590 st.manifest.event_count = 0;
591 st.manifest.status = "active".to_string();
592 st.manifest.in_turn = Some(false);
593 st.manifest.retained_turn_base = Some(scan_turn_base);
594 st.index = TurnIndex::default();
595 st.in_turn = false;
596 }
597 log.scan()?;
598 let mut st = log.shared.state.lock().map_err(poison)?;
599 st.next_offset = file_len;
600 st.needs_sync = file_len != 0;
604 log.persist_meta_locked(&mut st)?;
605 }
606 }
607 if pending_rewrite.is_some() {
608 log.manifest_store.clear_pending_cap_rewrite()?;
614 }
615 log.shared.state.lock().map_err(poison)?.needs_separator = needs_separator;
616 log.shared.initialized.store(true, Ordering::Release);
617 }
618 drop(_eviction_guard);
619 Ok(log)
620 }
621
622 pub(crate) fn reconstruct_turn(&self, turn: u64) -> Result<Vec<ThreadEvent>, SessionStoreError> {
624 let _eviction_guard = self.shared.eviction_lock.lock().map_err(poison)?;
628 let entry = {
629 let st = self.shared.state.lock().map_err(poison)?;
630 st.index
631 .entries
632 .iter()
633 .find(|e| e.turn_number == turn)
634 .cloned()
635 .ok_or(SessionStoreError::TurnNotFound { session: st.manifest.session_id.clone(), turn })?
636 };
637 {
638 let mut st = self.shared.state.lock().map_err(poison)?;
639 self.flush_write_buf_locked(&mut st)?;
640 }
641 let buf = {
642 let mut file_slot = self.shared.file.lock().map_err(poison)?;
643 let file = file_slot.as_mut().ok_or_else(|| self.event_file_unavailable())?;
644 file.seek(SeekFrom::Start(entry.start_offset))
645 .map_err(|e| SessionStoreError::io(&self.events_path, e))?;
646 let len = usize::try_from(entry.end_offset.checked_sub(entry.start_offset).ok_or_else(|| {
647 SessionStoreError::io(
648 &self.events_path,
649 std::io::Error::new(std::io::ErrorKind::InvalidData, "turn index offsets are out of order"),
650 )
651 })?)
652 .map_err(|error| {
653 SessionStoreError::io(&self.events_path, std::io::Error::new(std::io::ErrorKind::InvalidData, error))
654 })?;
655 let mut buf = vec![0u8; len];
656 file.read_exact(&mut buf)
657 .map_err(|e| SessionStoreError::io(&self.events_path, e))?;
658 buf
659 };
660 let text = String::from_utf8_lossy(&buf);
661 let mut events = Vec::new();
662 for line in text.lines() {
663 let line = line.trim();
664 if line.is_empty() {
665 continue;
666 }
667 let v: VersionedThreadEvent = match serde_json::from_str(line) {
673 Ok(v) => v,
674 Err(_) => continue,
675 };
676 events.push(v.into_event());
677 }
678 Ok(events)
679 }
680
681 #[must_use]
683 pub(crate) fn turn_count(&self) -> u64 {
684 self.shared.state.lock().map_err(poison).map_or(0, |s| s.manifest.turn_count)
685 }
686
687 #[must_use]
689 pub fn event_count(&self) -> u64 {
690 self.shared.state.lock().map_err(poison).map_or(0, |s| s.manifest.event_count)
691 }
692
693 pub fn visit_snapshot<F>(&self, mut visitor: F) -> Result<SessionManifest, SessionStoreError>
697 where
698 F: FnMut(u64, &[u8]),
699 {
700 use std::io::{BufRead, BufReader};
701 let _eviction_guard = self.shared.eviction_lock.lock().map_err(poison)?;
702 let mut st = self.shared.state.lock().map_err(poison)?;
703 self.persist_meta_locked(&mut st)?;
704 let mut file_slot = self.shared.file.lock().map_err(poison)?;
705 let file = file_slot.as_mut().ok_or_else(|| self.event_file_unavailable())?;
706 file.seek(SeekFrom::Start(0))
707 .map_err(|e| SessionStoreError::io(&self.events_path, e))?;
708 let mut reader = BufReader::new(file.take(st.next_offset));
709 let mut offset = 0;
710 let mut line = Vec::new();
711 loop {
712 line.clear();
713 let length = reader
714 .read_until(b'\n', &mut line)
715 .map_err(|e| SessionStoreError::io(&self.events_path, e))?;
716 if length == 0 {
717 break;
718 }
719 visitor(offset, &line);
720 offset += length as u64;
721 }
722 Ok(st.manifest.clone())
723 }
724
725 pub fn flush(&self) -> Result<(), SessionStoreError> {
727 let _eviction_guard = self.shared.eviction_lock.lock().map_err(poison)?;
728 let mut st = self.shared.state.lock().map_err(poison)?;
729 self.persist_meta_locked(&mut st)
730 }
731
732 #[must_use]
734 pub fn manifest(&self) -> SessionManifest {
735 self.shared
736 .state
737 .lock()
738 .map_err(poison)
739 .map(|s| s.manifest.clone())
740 .unwrap_or_else(|_| SessionManifest::new(""))
741 }
742
743 #[must_use]
745 pub fn turn_index(&self) -> TurnIndex {
746 self.shared
747 .state
748 .lock()
749 .map_err(poison)
750 .map(|s| s.index.clone())
751 .unwrap_or_default()
752 }
753
754 pub(crate) fn complete(&self) -> Result<(), SessionStoreError> {
760 let _eviction_guard = self.shared.eviction_lock.lock().map_err(poison)?;
761 let mut st = self.shared.state.lock().map_err(poison)?;
762 st.manifest.updated_at = now_rfc3339();
763 self.persist_meta_locked(&mut st)
764 }
765
766 fn scan(&self) -> Result<(), SessionStoreError> {
772 let mut st = self.shared.state.lock().map_err(poison)?;
773 let file = self
774 .shared
775 .file
776 .lock()
777 .map_err(poison)?
778 .as_ref()
779 .ok_or_else(|| self.event_file_unavailable())?
780 .try_clone()
781 .map_err(|e| SessionStoreError::io(&self.events_path, e))?;
782 let mut reader = std::io::BufReader::new(file);
783 reader
784 .seek(SeekFrom::Start(0))
785 .map_err(|e| SessionStoreError::io(&self.events_path, e))?;
786 let mut buf = Vec::new();
787 let mut pos = 0u64;
788 let mut first_ts: Option<String> = None;
789 loop {
790 buf.clear();
791 let n = reader
792 .read_until(b'\n', &mut buf)
793 .map_err(|e| SessionStoreError::io(&self.events_path, e))?;
794 if n == 0 {
795 break;
796 }
797 let line_end = pos + n as u64;
798 let trimmed = std::str::from_utf8(&buf).unwrap_or("").trim();
799 if !trimmed.is_empty()
800 && let Ok(v) = serde_json::from_str::<VersionedEventKind<'_>>(trimmed)
801 {
802 let kind = v.event.kind;
803 if requires_full_lifecycle_validation(kind) && !valid_lifecycle_payload(kind, trimmed) {
804 pos = line_end;
805 continue;
806 }
807 st.manifest.event_count += 1;
808 if kind == "thread.started" && first_ts.is_none() {
811 first_ts = Some(now_rfc3339());
812 }
813 st.apply_lifecycle_event(LifecycleKind::from_kind(kind), pos, line_end);
816 }
817 pos = line_end;
818 }
819 st.manifest.in_turn = Some(st.in_turn);
823 if let Some(ts) = first_ts
824 && st.manifest.created_at.is_empty()
825 {
826 st.manifest.created_at = ts;
827 }
828 Ok(())
829 }
830
831 fn persist_meta_locked(&self, st: &mut LogState) -> Result<(), SessionStoreError> {
832 self.flush_write_buf_locked(st)?;
833 st.manifest.persisted_file_len = Some(st.next_offset);
838 self.manifest_store.write_turn_index(&st.index)?;
843 self.manifest_store.write_manifest(&st.manifest)?;
844 Ok(())
845 }
846
847 fn flush_write_buf_locked(&self, st: &mut LogState) -> Result<(), SessionStoreError> {
849 self.flush_write_buf_with_sync_locked(st, File::sync_data)
850 }
851
852 fn flush_write_buf_with_sync_locked(
853 &self,
854 st: &mut LogState,
855 sync: impl FnOnce(&File) -> std::io::Result<()>,
856 ) -> Result<(), SessionStoreError> {
857 self.flush_write_buf_with_ops_locked(st, |file, bytes| file.write_all(bytes), File::set_len, sync)
858 }
859
860 fn flush_write_buf_with_ops_locked(
861 &self,
862 st: &mut LogState,
863 write: impl FnOnce(&mut File, &[u8]) -> std::io::Result<()>,
864 rollback: impl FnOnce(&File, u64) -> std::io::Result<()>,
865 sync: impl FnOnce(&File) -> std::io::Result<()>,
866 ) -> Result<(), SessionStoreError> {
867 if st.write_failed {
868 return Err(self.event_write_failed());
869 }
870 if st.write_buf.is_empty() && !st.needs_sync {
871 return Ok(());
872 }
873 let mut file_slot = self.shared.file.lock().map_err(poison)?;
874 let file = file_slot.as_mut().ok_or_else(|| self.event_file_unavailable())?;
875 if !st.write_buf.is_empty() {
876 let previous_len = file.metadata().map_err(|e| SessionStoreError::io(&self.events_path, e))?.len();
877 if let Err(error) = write(file, &st.write_buf) {
878 if let Err(rollback_error) = rollback(file, previous_len) {
879 st.write_buf.clear();
880 st.write_failed = true;
881 return Err(SessionStoreError::io(
882 &self.events_path,
883 std::io::Error::other(format!(
884 "event write failed: {error}; rollback failed: {rollback_error}"
885 )),
886 ));
887 }
888 return Err(SessionStoreError::io(&self.events_path, error));
889 }
890 st.write_buf.clear();
891 st.needs_sync = true;
892 }
893 sync(file).map_err(|error| SessionStoreError::io(&self.events_path, error))?;
894 st.needs_sync = false;
895 Ok(())
896 }
897
898 fn event_file_metadata_len(&self) -> Result<u64, SessionStoreError> {
899 let file_slot = self.shared.file.lock().map_err(poison)?;
900 file_slot
901 .as_ref()
902 .ok_or_else(|| self.event_file_unavailable())?
903 .metadata()
904 .map(|metadata| metadata.len())
905 .map_err(|error| SessionStoreError::io(&self.events_path, error))
906 }
907
908 fn open_event_file(&self) -> Result<File, SessionStoreError> {
909 VtCodePaths::open_private_append_file(&self.events_path)
910 .map_err(|error| SessionStoreError::io(&self.events_path, std::io::Error::other(error)))
911 }
912
913 fn replace_event_file_contents(&self, contents: &[u8]) -> Result<u64, SessionStoreError> {
914 let old_file = {
915 let mut file_slot = self.shared.file.lock().map_err(poison)?;
916 file_slot.take().ok_or_else(|| self.event_file_unavailable())?
917 };
918 drop(old_file);
919
920 if let Err(error) = VtCodePaths::write_private_file_atomic(&self.events_path, contents)
921 .map_err(|error| SessionStoreError::io(&self.events_path, std::io::Error::other(error)))
922 {
923 let restored = self.open_event_file();
924 if let Ok(file) = restored {
925 let mut file_slot = self.shared.file.lock().map_err(poison)?;
926 *file_slot = Some(file);
927 return Err(error);
928 }
929 return Err(SessionStoreError::io(
930 &self.events_path,
931 std::io::Error::other(format!("{error}; failed to restore event log handle")),
932 ));
933 }
934
935 let replacement = self.open_event_file()?;
936 let next_offset = replacement
937 .metadata()
938 .map_err(|error| SessionStoreError::io(&self.events_path, error))?
939 .len();
940 let mut file_slot = self.shared.file.lock().map_err(poison)?;
941 *file_slot = Some(replacement);
942 Ok(next_offset)
943 }
944
945 fn event_file_unavailable(&self) -> SessionStoreError {
946 SessionStoreError::io(&self.events_path, std::io::Error::other("event log file is unavailable"))
947 }
948
949 fn event_write_failed(&self) -> SessionStoreError {
950 SessionStoreError::io(
951 &self.events_path,
952 std::io::Error::other("event log write rollback failed; reopen required"),
953 )
954 }
955}
956
957impl Drop for SessionEventLog {
958 fn drop(&mut self) {
959 if let Ok(_eviction_guard) = self.shared.eviction_lock.lock()
960 && let Ok(mut st) = self.shared.state.lock()
961 {
962 let _ = self.flush_write_buf_locked(&mut st);
968 }
969 }
970}
971
972fn poison<T>(_e: std::sync::PoisonError<T>) -> SessionStoreError {
974 SessionStoreError::Io {
975 path: PathBuf::new(),
976 source: std::io::Error::other("session store lock poisoned"),
977 }
978}
979
980fn now_rfc3339() -> String {
981 Utc::now().to_rfc3339()
982}
983
984#[cfg(test)]
985mod borrowed_envelope_tests;
986#[cfg(test)]
987mod cap_eviction_tests;
988#[cfg(test)]
989mod lifecycle_state_machine_tests;