velesdb_memory/migration/state/
lock.rs1use super::STATE_FILE;
2use std::io::Write;
3use std::path::{Path, PathBuf};
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::time::{SystemTime, UNIX_EPOCH};
6
7pub const LOCK_FILE: &str = "migration.lock";
9
10pub(in crate::migration) const LOCK_GUARD_FILE: &str = "migration.lock.guard";
16
17const LOCK_FORMAT_VERSION: u32 = 1;
18static LOCK_SEQUENCE: AtomicU64 = AtomicU64::new(0);
19
20#[derive(Debug)]
26pub struct MigrationLock {
27 path: PathBuf,
28 token: String,
29 guard: std::fs::File,
30}
31
32#[derive(Debug, serde::Serialize, serde::Deserialize)]
33struct LockRecord {
34 format_version: u32,
35 held_by: String,
36 token: String,
37}
38
39impl MigrationLock {
40 pub fn acquire(workspace: &Path, holder: &str) -> Result<Self, String> {
46 let path = workspace.join(LOCK_FILE);
47 let guard = open_and_lock_guard(workspace)?;
48 ensure_lock_record_absent(workspace, &path)?;
49 let token = create_lock_record(&path, holder)?;
50 Ok(Self { path, token, guard })
51 }
52
53 #[must_use]
55 pub fn holder(workspace: &Path) -> Option<String> {
56 std::fs::read_to_string(workspace.join(LOCK_FILE))
57 .ok()
58 .map(|body| {
59 serde_json::from_str::<LockRecord>(&body).map_or_else(
60 |_| body.trim().to_owned(),
61 |record| format!("held_by={}", record.held_by),
62 )
63 })
64 }
65
66 pub(super) fn verify_workspace(&self, workspace: &Path) -> Result<(), String> {
67 let expected = workspace.join(LOCK_FILE);
68 if self.path != expected || !self.owns_current_lock() {
69 return Err(format!(
70 "cannot write {STATE_FILE} without the exact live migration lock identity for {}; acquire MigrationLock for this exact workspace first",
71 workspace.display()
72 ));
73 }
74 Ok(())
75 }
76
77 fn owns_current_lock(&self) -> bool {
78 let is_live_regular_file = std::fs::symlink_metadata(&self.path)
79 .is_ok_and(|metadata| metadata.is_file() && !metadata.file_type().is_symlink());
80 if !is_live_regular_file {
81 return false;
82 }
83 std::fs::read_to_string(&self.path)
84 .ok()
85 .and_then(|body| serde_json::from_str::<LockRecord>(&body).ok())
86 .is_some_and(|record| {
87 record.format_version == LOCK_FORMAT_VERSION && record.token == self.token
88 })
89 }
90
91 fn remove_if_owned(&self) -> Result<(), String> {
92 if !self.owns_current_lock() {
93 return Err(format!(
94 "cannot release {LOCK_FILE}: the lock at {} is absent, invalid, or belongs to a later acquisition",
95 self.path.display()
96 ));
97 }
98 std::fs::remove_file(&self.path).map_err(|err| format!("cannot release {LOCK_FILE}: {err}"))
99 }
100
101 pub fn release(self) -> Result<(), String> {
107 self.remove_if_owned()?;
108 fs2::FileExt::unlock(&self.guard).map_err(|err| {
109 format!("removed {LOCK_FILE} but cannot unlock {LOCK_GUARD_FILE}: {err}")
110 })
111 }
112}
113
114fn open_and_lock_guard(workspace: &Path) -> Result<std::fs::File, String> {
115 let guard_path = workspace.join(LOCK_GUARD_FILE);
116 let guard = std::fs::OpenOptions::new()
117 .read(true)
118 .write(true)
119 .create(true)
120 .truncate(false)
121 .open(&guard_path)
122 .map_err(|err| format!("cannot open persistent {LOCK_GUARD_FILE}: {err}"))?;
123 validate_guard_file(&guard_path, &guard)?;
124 lock_guard(workspace, &guard)?;
125 Ok(guard)
126}
127
128fn validate_guard_file(path: &Path, guard: &std::fs::File) -> Result<(), String> {
129 let path_metadata = std::fs::symlink_metadata(path)
130 .map_err(|err| format!("cannot inspect {LOCK_GUARD_FILE}: {err}"))?;
131 let handle_is_file = guard.metadata().is_ok_and(|metadata| metadata.is_file());
132 if !path_metadata.file_type().is_symlink() && handle_is_file {
133 return Ok(());
134 }
135 Err(format!(
136 "refusing {LOCK_GUARD_FILE} at {}: the persistent guard must be a regular, non-symlink file",
137 path.display()
138 ))
139}
140
141fn lock_guard(workspace: &Path, guard: &std::fs::File) -> Result<(), String> {
142 match fs2::FileExt::try_lock_exclusive(guard) {
143 Ok(()) => Ok(()),
144 Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => Err(format!(
145 "a live migration still holds this workspace guard ({}). The OS guard is NOT stolen and deleting {LOCK_FILE} cannot release it; wait for the owner or stop it explicitly.",
146 MigrationLock::holder(workspace).unwrap_or_else(|| "holder record missing".to_owned()),
147 )),
148 Err(err) => Err(format!("cannot lock {LOCK_GUARD_FILE}: {err}")),
149 }
150}
151
152fn ensure_lock_record_absent(workspace: &Path, path: &Path) -> Result<(), String> {
153 match std::fs::symlink_metadata(path) {
154 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
155 Err(err) => Err(format!("cannot inspect {LOCK_FILE}: {err}")),
156 Ok(_) => Err(format!(
157 "a migration lock record remains in this workspace ({}). It is NOT stolen automatically: a dead process releases the OS guard but leaves this evidence behind. If you are certain no migration is running, delete {} yourself.",
158 MigrationLock::holder(workspace).unwrap_or_else(|| "holder unknown".to_owned()),
159 path.display()
160 )),
161 }
162}
163
164fn create_lock_record(path: &Path, holder: &str) -> Result<String, String> {
165 let token = next_lock_token();
166 let record = LockRecord {
167 format_version: LOCK_FORMAT_VERSION,
168 held_by: holder.to_owned(),
169 token: token.clone(),
170 };
171 let body = serde_json::to_vec_pretty(&record)
172 .map_err(|err| format!("cannot serialise {LOCK_FILE}: {err}"))?;
173 let mut file = std::fs::OpenOptions::new()
174 .write(true)
175 .create_new(true)
176 .open(path)
177 .map_err(|err| format!("cannot create {LOCK_FILE}: {err}"))?;
178 file.write_all(&body)
179 .map_err(|err| format!("cannot write {LOCK_FILE}: {err}"))?;
180 file.flush()
181 .and_then(|()| file.sync_all())
182 .map_err(|err| format!("cannot persist {LOCK_FILE}: {err}"))?;
183 Ok(token)
184}
185
186fn next_lock_token() -> String {
187 let sequence = LOCK_SEQUENCE.fetch_add(1, Ordering::Relaxed);
188 let nanos = SystemTime::now()
189 .duration_since(UNIX_EPOCH)
190 .map_or(0, |elapsed| elapsed.as_nanos());
191 format!(
192 "lock-v{LOCK_FORMAT_VERSION}-{:08x}-{nanos:032x}-{sequence:016x}",
193 std::process::id()
194 )
195}