optimization_job_api/
storage.rs1use serde::de::DeserializeOwned;
3use serde::{Deserialize, Serialize};
4use sha2::{Digest, Sha256};
5use std::fs::{self, File, OpenOptions};
6use std::io::{self, BufRead, BufReader, BufWriter, Write};
7use std::path::{Path, PathBuf};
8static TEMP_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
9pub fn append_jsonl<T: Serialize>(path: &Path, value: &T) -> Result<(), io::Error> {
10 let mut file = OpenOptions::new()
11 .create(true)
12 .read(true)
13 .append(true)
14 .open(path)?;
15 seal_tail(&mut file)?;
16 let mut writer = BufWriter::new(file);
17 serde_json::to_writer(&mut writer, value)?;
18 writer.write_all(b"\n")?;
19 writer.flush()?;
20 writer.get_ref().sync_data()?;
21 Ok(())
22}
23
24fn seal_tail(file: &mut File) -> io::Result<()> {
30 use std::io::{Read, Seek, SeekFrom};
31 let len = file.metadata()?.len();
32 if len == 0 {
33 return Ok(());
34 }
35 let mut last = [0u8];
36 file.seek(SeekFrom::Start(len - 1))?;
37 file.read_exact(&mut last)?;
38 if last[0] == b'\n' {
39 return Ok(());
40 }
41 let mut data = Vec::new();
42 file.seek(SeekFrom::Start(0))?;
43 file.read_to_end(&mut data)?;
44 let start = data.iter().rposition(|b| *b == b'\n').map_or(0, |i| i + 1);
45 if serde_json::from_slice::<serde_json::Value>(&data[start..]).is_ok() {
46 file.write_all(b"\n")?;
47 } else {
48 file.set_len(start as u64)?;
49 }
50 file.sync_data()
51}
52
53pub fn read_jsonl_resilient<T: DeserializeOwned>(path: &Path) -> Result<Vec<T>, io::Error> {
62 let file = File::open(path)?;
63 let mut reader = BufReader::new(file);
64 let mut records = Vec::new();
65 let mut line = Vec::new();
66 loop {
67 line.clear();
68 let bytes = reader.read_until(b'\n', &mut line)?;
69 if bytes == 0 {
70 break;
71 }
72 let terminated = line.last() == Some(&b'\n');
73 if terminated {
74 line.pop();
75 if line.last() == Some(&b'\r') {
76 line.pop();
77 }
78 }
79 if line.iter().all(u8::is_ascii_whitespace) {
80 continue;
81 }
82 match serde_json::from_slice(&line) {
83 Ok(record) => records.push(record),
84 Err(_) if !terminated => break,
85 Err(error) => return Err(io::Error::other(error)),
86 }
87 }
88 Ok(records)
89}
90
91pub fn write_atomic_json<T: Serialize>(path: &Path, value: &T) -> Result<(), io::Error> {
98 let parent = path.parent().unwrap_or_else(|| Path::new("."));
99 fs::create_dir_all(parent)?;
100 let filename = path
101 .file_name()
102 .and_then(|name| name.to_str())
103 .ok_or_else(|| io::Error::other("snapshot path has no file name".to_owned()))?;
104 let temporary = temporary_path(parent, filename);
105 let result = (|| {
106 let file = OpenOptions::new()
107 .create_new(true)
108 .write(true)
109 .open(&temporary)?;
110 let mut writer = BufWriter::new(file);
111 serde_json::to_writer_pretty(&mut writer, value)?;
112 writer.write_all(b"\n")?;
113 writer.flush()?;
114 writer.get_ref().sync_all()?;
115 fs::rename(&temporary, path)?;
116 File::open(parent)?.sync_all()?;
117 Ok::<(), io::Error>(())
118 })();
119 if result.is_err() {
120 let _ = fs::remove_file(&temporary);
121 }
122 result
123}
124
125fn temporary_path(parent: &Path, filename: &str) -> PathBuf {
126 parent.join(format!(
127 ".{filename}.{}.{}.tmp",
128 std::process::id(),
129 TEMP_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
130 ))
131}
132
133#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
135#[serde(deny_unknown_fields)]
136pub struct Envelope {
137 pub schema_version: u32,
138 pub checksum: String,
139 pub payload_json: String,
140}
141pub fn digest(bytes: &[u8]) -> String {
142 format!("{:x}", Sha256::digest(bytes))
143}
144pub fn append_checked<T: Serialize>(path: &Path, version: u32, value: &T) -> io::Result<()> {
145 let payload_json = serde_json::to_string(value)?;
146 append_jsonl(
147 path,
148 &Envelope {
149 schema_version: version,
150 checksum: digest(payload_json.as_bytes()),
151 payload_json,
152 },
153 )
154}
155pub fn read_checked<T: DeserializeOwned>(path: &Path, version: u32) -> io::Result<Vec<T>> {
156 let lines: Vec<Envelope> = read_jsonl_resilient(path)?;
157 lines
158 .into_iter()
159 .map(|line| {
160 if line.schema_version != version
161 || line.checksum != digest(line.payload_json.as_bytes())
162 {
163 return Err(io::Error::other("checksum or schema mismatch"));
164 }
165 Ok(serde_json::from_str(&line.payload_json)?)
166 })
167 .collect()
168}
169pub fn repair_tail<T: DeserializeOwned>(path: &Path, version: u32) -> io::Result<Vec<T>> {
172 let records = read_checked(path, version)?;
173 let data = fs::read(path)?;
174 if !data.is_empty() && !data.ends_with(b"\n") {
175 let start = data.iter().rposition(|b| *b == b'\n').map_or(0, |i| i + 1);
176 let mut file = OpenOptions::new().append(true).open(path)?;
177 if serde_json::from_slice::<Envelope>(&data[start..]).is_ok() {
178 file.write_all(b"\n")?;
179 } else {
180 file.set_len(start as u64)?;
181 }
182 file.sync_all()?;
183 }
184 Ok(records)
185}
186pub fn lock_is_free(path: &Path) -> io::Result<bool> {
197 let file = match OpenOptions::new().read(true).write(true).open(path) {
198 Ok(file) => file,
199 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(true),
200 Err(e) => return Err(e),
201 };
202 match fs2::FileExt::try_lock_exclusive(&file) {
203 Ok(()) => Ok(true),
204 Err(e) if e.raw_os_error() == fs2::lock_contended_error().raw_os_error() => Ok(false),
205 Err(e) => Err(e),
206 }
207}
208pub fn lock(path: &Path) -> io::Result<File> {
210 let file = OpenOptions::new()
211 .create(true)
212 .truncate(false)
213 .read(true)
214 .write(true)
215 .open(path)?;
216 fs2::FileExt::lock_exclusive(&file)?;
217 Ok(file)
218}