Skip to main content

velesdb_memory/migration/state/
lock.rs

1use 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
7/// The file that marks a migration in progress.
8pub const LOCK_FILE: &str = "migration.lock";
9
10/// The persistent sibling whose OS lock serializes every canonical lock check.
11///
12/// Unlike [`LOCK_FILE`], this file is never removed. Its inode must stay stable:
13/// the advisory lock on its open handle closes the delete/recreate ABA window
14/// around the human-readable canonical record.
15pub(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/// Exclusive possession of a migration workspace.
21///
22/// The persistent OS guard is held before the canonical record is inspected
23/// and remains held until explicit release or drop. A canonical record is
24/// deliberately retained after drop or panic as fail-closed evidence.
25#[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    /// Take the lock in `workspace` on behalf of `holder`.
41    ///
42    /// # Errors
43    /// The OS guard is held, a canonical record remains, or the workspace is
44    /// unwritable. Neither an active nor a dead lock is stolen automatically.
45    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    /// Who holds the lock in `workspace`, as recorded, or `None` when free.
54    #[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    /// Release the lock.
102    ///
103    /// # Errors
104    /// The canonical lock identity changed, the lock file cannot be removed,
105    /// or the OS guard cannot be unlocked.
106    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}