Skip to content

Commit d3fa5a5

Browse files
fix(ohh): buffer all OHH content to apend atomicly (#277)
1 parent f9e6276 commit d3fa5a5

2 files changed

Lines changed: 34 additions & 10 deletions

File tree

src/bin/rsp/ohh/reader.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,32 @@ use rs_poker::open_hand_history::{HandHistory, OpenHandHistoryWrapper};
77
pub enum ReaderError {
88
#[error("failed to read file: {0}")]
99
Io(#[from] std::io::Error),
10-
#[error("failed to parse line {line}: {source}")]
10+
#[error("failed to parse {path}:{line} ({source})\n line preview: {preview}")]
1111
Parse {
12+
path: std::path::PathBuf,
1213
line: usize,
14+
preview: String,
1315
source: serde_json::Error,
1416
},
1517
}
1618

19+
/// Maximum number of characters of a malformed line to include in the
20+
/// error message. Long enough to spot interleaves or truncation, short
21+
/// enough not to dump a whole hand record into the terminal.
22+
const PREVIEW_LEN: usize = 160;
23+
24+
fn line_preview(line: &str) -> String {
25+
let mut out = String::new();
26+
for (i, c) in line.chars().enumerate() {
27+
if i >= PREVIEW_LEN {
28+
out.push('…');
29+
break;
30+
}
31+
out.push(c);
32+
}
33+
out
34+
}
35+
1736
/// Read all `.ohh` files from a directory (sorted by filename).
1837
///
1938
/// Files that do not have an `.ohh` extension are skipped. Without this
@@ -57,7 +76,9 @@ pub fn read_ohh_file(path: &Path) -> Result<Vec<HandHistory>, ReaderError> {
5776
}
5877
let wrapper: OpenHandHistoryWrapper =
5978
serde_json::from_str(trimmed).map_err(|e| ReaderError::Parse {
79+
path: path.to_path_buf(),
6080
line: line_num + 1,
81+
preview: line_preview(trimmed),
6182
source: e,
6283
})?;
6384
hands.push(wrapper.ohh);

src/open_hand_history/writer.rs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,21 @@ use std::path::Path;
44

55
use super::hand_history::{HandHistory, OpenHandHistoryWrapper};
66

7-
/// Appends a hand history to a file in JSON Lines format
7+
/// Appends a hand history to a file in JSON Lines format.
8+
///
9+
/// Serializes into an in-memory buffer and then issues a single
10+
/// `write_all` under `O_APPEND`. On Linux regular files the kernel
11+
/// locks the inode around seek-to-EOF + write, so a single `write(2)`
12+
/// is atomic at EOF — concurrent writers cannot interleave their
13+
/// JSON mid-record as long as each record is emitted in one syscall.
814
pub fn append_hand(path: &Path, hand: HandHistory) -> io::Result<()> {
9-
// Create file if it doesn't exist, append if it does
10-
let mut file = OpenOptions::new().create(true).append(true).open(path)?;
11-
12-
// Wrap the hand history in the OHH wrapper
1315
let wrapped = OpenHandHistoryWrapper { ohh: hand };
1416

15-
// Serialize to JSON and append newline
16-
serde_json::to_writer(&mut file, &wrapped)?;
17-
writeln!(file)?; // Newline at the end
18-
writeln!(file)?; // Extra newline for separation
17+
let mut buf = serde_json::to_vec(&wrapped)?;
18+
buf.extend_from_slice(b"\n\n");
19+
20+
let mut file = OpenOptions::new().create(true).append(true).open(path)?;
21+
file.write_all(&buf)?;
1922
Ok(())
2023
}
2124

0 commit comments

Comments
 (0)