git_policy_cli/real_index_lock.rs
1//! What:
2//! The real `index.lock` a landing holds,
3//! created exclusively,
4//! identified by device,
5//! inode and
6//! filesystem,
7//! announced through `index~pid.lock`,
8//! and released only while it is still the file
9//! that was created.
10//! Why:
11//! The landing's index install depends on the exact lock file it created;
12//! a replaced lock must
13//! never be deleted,
14//! and a lock this process did not create must never be released
15//! (`src/policy-engine/commit-landing-index-lock.ts`).
16//!
17//! In TS you'd write (pseudocode):
18//! ```ts
19//! await using lock = await openRealIndexLock({ realIndexPath, timeoutMs });
20//! ```
21
22/// The lock identity record the transaction journal stores, and the numbered record's name.
23use super::transaction_journal::{IndexLockRecord, LockIdentity, index_lock_record_filename};
24/// The record's exact bytes.
25use super::transaction_journal_encode::encode_index_lock;
26/// Debug and warning text to the diagnostic log.
27use super::diagnostic_log::{debug, warn};
28/// The recovery install of a recorded post-index through the exact owned lock, and the release of that lock.
29use super::recovery_files::{LockRelease, install_recovered_index, lock_release_name, release_owned_lock};
30/// Recovery failures, which the install reports.
31use super::recovery_error::RecoveryError;
32#[cfg(unix)]
33use super::transaction_journal::FileIdentity;
34/// The waiting coordination and its failure.
35use super::index_writer_coordination::{CoordinationFailure, wait_for_lock};
36/// The filesystem identity of the worktree's volume.
37#[cfg(unix)]
38use super::filesystem_identity::filesystem_identity;
39/// Announcing this process through the PID file, the journal record, and the lock's mode.
40use super::journal_record::{RecordSync, write_record};
41use super::private_storage::{protect_path, write_private_file_unsynced};
42/// `Path` and `PathBuf` are borrowed and owned filesystem paths.
43use std::path::{Path, PathBuf};
44
45/// Module tag for diagnostics.
46const TAG: &str = "acquireRealIndexLock";
47
48/// What:
49/// A real `index.lock` created by this process.
50/// Why:
51/// Holding the identity lets the release prove the file is still the one created.
52///
53/// In TS you'd write (pseudocode):
54/// ```ts
55/// type RealIndexLock = { lockPath: string; identity: LockIdentity; installIndex(source): Promise<void> };
56/// ```
57#[derive(Debug, Eq, PartialEq)]
58pub struct RealIndexLock {
59 /// The lock path,
60 /// `<real index>.lock`.
61 lock_path: PathBuf,
62 /// The real index the lock guards,
63 /// which an install replaces.
64 real_index: PathBuf,
65 /// The PID file path,
66 /// `<real index>~pid.lock`.
67 pid_path: PathBuf,
68 /// The identity recorded when the lock was created.
69 identity: LockIdentity,
70}
71
72/// What:
73/// The lock path of a real index.
74/// Why:
75/// Git names the lock beside the index.
76///
77/// In TS you'd write (pseudocode):
78/// ```ts
79/// `${realIndexPath}.lock`
80/// ```
81pub fn real_lock_path(real_index: &Path) -> PathBuf {
82 let mut name: std::ffi::OsString = real_index.as_os_str().to_owned();
83 name.push(".lock");
84 return PathBuf::from(name);
85}
86
87/// What:
88/// The PID file path of a real index.
89/// Why:
90/// Git's `core.lockfilePid` file lives beside the lock.
91///
92/// In TS you'd write (pseudocode):
93/// ```ts
94/// lockPidPath(realIndexPath) // `${realIndexPath}~pid.lock`
95/// ```
96pub fn real_pid_path(real_index: &Path) -> PathBuf {
97 let mut name: std::ffi::OsString = real_index.as_os_str().to_owned();
98 name.push("~pid.lock");
99 return PathBuf::from(name);
100}
101
102impl RealIndexLock {
103 /// What:
104 /// The identity recorded for the lock.
105 /// Why:
106 /// The landing journal stores it.
107 ///
108 /// In TS you'd write (pseudocode):
109 /// ```ts
110 /// lock.identity
111 /// ```
112 pub fn identity(&self) -> &LockIdentity {
113 return &self.identity;
114 }
115
116 /// What:
117 /// Whether the lock path still names the file this process created.
118 /// Why:
119 /// A release or install must not act on a replaced lock.
120 ///
121 /// In TS you'd write (pseudocode):
122 /// ```ts
123 /// await assertLockIdentity({ lockPath, identity });
124 /// ```
125 pub fn assert_identity(&self) -> Result<(), String> {
126 return same_file_identity(self.lock_path.as_path(), &self.identity);
127 }
128
129 /// What:
130 /// Release the lock and this process's PID file,
131 /// refusing a lock that was replaced.
132 /// Why:
133 /// A replaced lock belongs to another process and stays in place.
134 ///
135 /// In TS you'd write (pseudocode):
136 /// ```ts
137 /// await lock[Symbol.asyncDispose]();
138 /// ```
139 pub fn release(self) -> Result<(), String> {
140 self.assert_identity()?;
141 if let Err(error) = std::fs::remove_file(&self.lock_path) {
142 return Err(error.to_string());
143 }
144 return remove_own_pid_file(self.pid_path.as_path());
145 }
146
147 /// What:
148 /// Install a computed index through the held lock,
149 /// consuming the lock,
150 /// then remove this process's PID file.
151 /// Why:
152 /// `installIndex`:
153 /// the bytes are written into the lock this process created,
154 /// the lock is linked under a private name beside the post-index,
155 /// and that name is renamed over the real index,
156 /// so the new index keeps the lock's owner.
157 /// The lock is not released here on failure:
158 /// once the landing record is durable,
159 /// recovery completes the install through the same lock.
160 ///
161 /// In TS you'd write (pseudocode):
162 /// ```ts
163 /// await lock.installIndex(postIndexPath);
164 /// ```
165 pub fn install(self, post_index: &Path) -> Result<(), RecoveryError> {
166 install_recovered_index(
167 self.lock_path.as_path(),
168 self.real_index.as_path(),
169 post_index,
170 &self.identity,
171 )?;
172 debug(TAG, format!("installed {} through {}", self.real_index.display(), self.lock_path.display()).as_str());
173 return remove_own_pid_file(self.pid_path.as_path()).map_err(RecoveryError);
174 }
175
176 /// What:
177 /// Release a lock a landing leaves without installing:
178 /// the lock is removed only while it is still the file this process created,
179 /// and the PID file goes either way.
180 /// Why:
181 /// The incumbent's disposal warns when the lock is gone or another owner's now
182 /// and still removes its own PID file,
183 /// and throws only for an operating-system error.
184 ///
185 /// In TS you'd write (pseudocode):
186 /// ```ts
187 /// await lock[Symbol.asyncDispose]();
188 /// ```
189 pub fn release_after_landing(self) -> Result<(), String> {
190 match release_owned_lock(&self.identity, self.lock_path.as_path()) {
191 Ok(LockRelease::Released) => {}
192 Ok(other) => warn(
193 TAG,
194 format!("index lock was not this transaction's at release ({}): {}", lock_release_name(other), self.lock_path.display()).as_str(),
195 ),
196 Err(error) => return Err(error.0),
197 }
198 return remove_own_pid_file(self.pid_path.as_path());
199 }
200}
201
202/// What:
203/// Whether a path is a regular file with the recorded device and inode,
204/// without following links.
205/// Why:
206/// Following a link would compare another file's identity.
207///
208/// In TS you'd write (pseudocode):
209/// ```ts
210/// const metadata = await lstat(path, { bigint: true });
211/// ```
212#[cfg(unix)]
213fn same_file_identity(path: &Path, identity: &LockIdentity) -> Result<(), String> {
214 use std::os::unix::fs::MetadataExt;
215 let metadata: std::fs::Metadata = match std::fs::symlink_metadata(path) {
216 Ok(found) => found,
217 Err(error) => return Err(error.to_string()),
218 };
219 if !metadata.is_file()
220 || metadata.dev().to_string() != identity.file.device
221 || metadata.ino().to_string() != identity.file.inode
222 {
223 return Err(format!("the index lock identity changed: {}", path.display()));
224 }
225 return Ok(());
226}
227
228/// What:
229/// On platforms without an inode,
230/// the identity cannot be checked,
231/// so the lock is never trusted.
232/// Why:
233/// The incumbent refuses a landing without a filesystem identity too.
234///
235/// In TS you'd write (pseudocode):
236/// ```ts
237/// throw new TypeError('filesystem identity unavailable');
238/// ```
239// UNTESTED ON WINDOWS: no gate compiles or runs this branch (the crate does not build for Windows; README, Requirements).
240#[cfg(not(unix))]
241fn same_file_identity(path: &Path, _identity: &LockIdentity) -> Result<(), String> {
242 return Err(format!("the index lock identity cannot be checked here: {}", path.display()));
243}
244
245/// What:
246/// Create the lock file exclusively with private permissions.
247/// Why:
248/// `O_EXCL` is the mutual exclusion;
249/// an existing file means another owner holds the lock.
250///
251/// In TS you'd write (pseudocode):
252/// ```ts
253/// open(lockPath, O_CREAT | O_EXCL | O_RDWR, 0o600);
254/// ```
255#[cfg(unix)]
256fn create_exclusive(path: &Path) -> Result<bool, String> {
257 use std::os::unix::fs::OpenOptionsExt;
258 let mut options: std::fs::OpenOptions = std::fs::OpenOptions::new();
259 options.write(true).create_new(true).mode(0o600);
260 match options.open(path) {
261 Ok(_) => return Ok(true),
262 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => return Ok(false),
263 Err(error) => return Err(error.to_string()),
264 }
265}
266
267/// What:
268/// Create the lock file exclusively,
269/// on platforms without Unix modes.
270/// Why:
271/// Private permissions are not applied there;
272/// the identity check still refuses the lock.
273///
274/// In TS you'd write (pseudocode):
275/// ```ts
276/// open(lockPath, O_CREAT | O_EXCL | O_RDWR);
277/// ```
278// UNTESTED ON WINDOWS: no gate compiles or runs this branch (the crate does not build for Windows; README, Requirements).
279#[cfg(not(unix))]
280fn create_exclusive(path: &Path) -> Result<bool, String> {
281 let mut options: std::fs::OpenOptions = std::fs::OpenOptions::new();
282 options.write(true).create_new(true);
283 match options.open(path) {
284 Ok(_) => return Ok(true),
285 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => return Ok(false),
286 Err(error) => return Err(error.to_string()),
287 }
288}
289
290/// What:
291/// The identity of a freshly created lock file.
292/// Why:
293/// The journal records it;
294/// a platform without device and inode numbers records nothing.
295///
296/// In TS you'd write (pseudocode):
297/// ```ts
298/// { file: { device: String(stat.dev), inode: String(stat.ino) }, fsId: await resolveFsId(dirname(path)) };
299/// ```
300#[cfg(unix)]
301fn created_identity(lock_path: &Path, real_index: &Path) -> Result<LockIdentity, String> {
302 use std::os::unix::fs::MetadataExt;
303 let metadata: std::fs::Metadata = match std::fs::symlink_metadata(lock_path) {
304 Ok(found) => found,
305 Err(error) => return Err(error.to_string()),
306 };
307 let directory: &Path = real_index.parent().unwrap_or(Path::new("."));
308 let fs_id: String = match filesystem_identity(directory) {
309 Some(found) => found,
310 None => return Err(String::from("the filesystem identity of the worktree is unavailable")),
311 };
312 return Ok(LockIdentity {
313 file: FileIdentity {
314 device: metadata.dev().to_string(),
315 inode: metadata.ino().to_string(),
316 },
317 fs_id,
318 });
319}
320
321/// What:
322/// On platforms without device and inode numbers,
323/// no identity is recorded and the landing stops.
324/// Why:
325/// The Windows volume serial is not exposed by the standard library,
326/// as the design notes say.
327///
328/// In TS you'd write (pseudocode):
329/// ```ts
330/// throw new TypeError('filesystem identity unavailable');
331/// ```
332// UNTESTED ON WINDOWS: no gate compiles or runs this branch (the crate does not build for Windows; README, Requirements).
333#[cfg(not(unix))]
334fn created_identity(lock_path: &Path, _real_index: &Path) -> Result<LockIdentity, String> {
335 return Err(format!("the index lock identity cannot be recorded here: {}", lock_path.display()));
336}
337
338/// What:
339/// Remove the PID file when it still names this process.
340/// Why:
341/// Another process may have announced itself meanwhile.
342///
343/// In TS you'd write (pseudocode):
344/// ```ts
345/// if ((await readFile(pidPath, 'utf8')) === `pid ${process.pid}\n`) await rm(pidPath);
346/// ```
347fn remove_own_pid_file(pid_path: &Path) -> Result<(), String> {
348 let own: String = format!("pid {}\n", std::process::id());
349 match std::fs::read_to_string(pid_path) {
350 Ok(text) => {
351 if text == own && let Err(error) = std::fs::remove_file(pid_path) {
352 return Err(error.to_string());
353 }
354 return Ok(());
355 }
356 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
357 Err(error) => return Err(error.to_string()),
358 }
359}
360
361/// What:
362/// Create the lock exclusively,
363/// waiting for a foreign holder under the classification rules,
364/// and read the identity of the file created.
365/// Why:
366/// `openLockWaiting` plus the identity step of the landing;
367/// a lock whose identity cannot be recorded is removed again,
368/// because nothing proves it is ours.
369///
370/// In TS you'd write (pseudocode):
371/// ```ts
372/// const handle = await openLockWaiting({ realIndexPath, timeoutMs });
373/// ```
374fn create_identified_lock(
375 real_index: &Path,
376 timeout_ms: u64,
377 consequence: &str,
378) -> Result<(PathBuf, LockIdentity), CoordinationFailure> {
379 let lock_path: PathBuf = real_lock_path(real_index);
380 loop {
381 match create_exclusive(lock_path.as_path()) {
382 Ok(true) => break,
383 Ok(false) => wait_for_lock(real_index, timeout_ms, consequence)?,
384 Err(reason) => return Err(CoordinationFailure::Stop(reason)),
385 }
386 }
387 match created_identity(lock_path.as_path(), real_index) {
388 Ok(identity) => return Ok((lock_path, identity)),
389 Err(reason) => {
390 let _ = std::fs::remove_file(lock_path.as_path());
391 return Err(CoordinationFailure::Stop(reason));
392 }
393 }
394}
395
396/// What:
397/// Take the real index lock for a landing so that its journal record exists before the lock does:
398/// create a candidate file in the transaction directory,
399/// journal the candidate's identity,
400/// then publish the candidate as `index.lock` by a hard link,
401/// which is as exclusive as `O_EXCL` and gives the lock the candidate's inode.
402/// `Ok(None)` means the filesystem refused the link and nothing was left behind,
403/// so the caller creates the lock in place and journals it afterwards.
404/// Why:
405/// A kill between the creation of `index.lock` and the write of its record leaves a lock that nothing proves
406/// ours, which every later command refuses to remove (the lock may be Git's); no window like that remains when the
407/// record comes first. A candidate that outlives a kill lies in the transaction directory, which recovery removes.
408///
409/// In TS you'd write (pseudocode):
410/// ```ts
411/// await writeCandidate(); await journalIdentity(candidate); await link(candidate, indexLock); await rm(candidate);
412/// ```
413fn create_journaled_lock(
414 real_index: &Path,
415 transaction_directory: &Path,
416 attempt: i64,
417 timeout_ms: u64,
418 consequence: &str,
419) -> Result<Option<(PathBuf, LockIdentity)>, CoordinationFailure> {
420 let lock_path: PathBuf = real_lock_path(real_index);
421 let candidate: PathBuf = transaction_directory.join(format!("index-lock-{attempt}.candidate"));
422 match create_exclusive(candidate.as_path()) {
423 Ok(true) => {}
424 Ok(false) => return Err(CoordinationFailure::Stop(format!("{} already exists", candidate.display()))),
425 Err(reason) => return Err(CoordinationFailure::Stop(reason)),
426 }
427 let identity: LockIdentity = match created_identity(candidate.as_path(), real_index) {
428 Ok(found) => found,
429 Err(reason) => {
430 let _ = std::fs::remove_file(candidate.as_path());
431 return Err(CoordinationFailure::Stop(reason));
432 }
433 };
434 if let Err(reason) = write_lock_record(transaction_directory, attempt, &identity) {
435 let _ = std::fs::remove_file(candidate.as_path());
436 return Err(CoordinationFailure::Stop(reason));
437 }
438 loop {
439 match std::fs::hard_link(candidate.as_path(), lock_path.as_path()) {
440 Ok(()) => break,
441 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
442 if let Err(failure) = wait_for_lock(real_index, timeout_ms, consequence) {
443 // The lock was never taken, so nothing is journaled for it.
444 let _ = std::fs::remove_file(candidate.as_path());
445 let _ = std::fs::remove_file(transaction_directory.join(index_lock_record_filename(attempt)));
446 return Err(failure);
447 }
448 }
449 Err(error) => {
450 debug(
451 TAG,
452 format!("the filesystem refused to link the lock candidate ({error}); creating the lock in place").as_str(),
453 );
454 let _ = std::fs::remove_file(candidate.as_path());
455 let record: PathBuf = transaction_directory.join(index_lock_record_filename(attempt));
456 if let Err(removal) = std::fs::remove_file(record.as_path()) {
457 return Err(CoordinationFailure::Stop(format!("removing {} failed: {removal}", record.display())));
458 }
459 return Ok(None);
460 }
461 }
462 }
463 // The lock now has its own name; the candidate's name was only the way in.
464 if let Err(error) = std::fs::remove_file(candidate.as_path()) {
465 debug(TAG, format!("removing the lock candidate {} failed: {error}", candidate.display()).as_str());
466 }
467 return Ok(Some((lock_path, identity)));
468}
469
470/// What:
471/// Take the real index lock,
472/// waiting for a foreign holder under the classification rules,
473/// then
474/// record its identity and announce this process through the PID file.
475/// Why:
476/// `openLockWaiting` plus the identity and PID steps of the landing.
477///
478/// In TS you'd write (pseudocode):
479/// ```ts
480/// const handle = await openLockWaiting({ realIndexPath, timeoutMs });
481/// ```
482pub fn acquire_real_index_lock(
483 real_index: &Path,
484 timeout_ms: u64,
485 consequence: &str,
486) -> Result<RealIndexLock, CoordinationFailure> {
487 let (lock_path, identity): (PathBuf, LockIdentity) = create_identified_lock(real_index, timeout_ms, consequence)?;
488 let pid_path: PathBuf = real_pid_path(real_index);
489 // The PID file is unsynced: a torn or lost one completes or discards in the tearing table.
490 if let Err(error) = write_private_file_unsynced(pid_path.as_path(), format!("pid {}\n", std::process::id()).as_bytes()) {
491 let _ = std::fs::remove_file(lock_path.as_path());
492 return Err(CoordinationFailure::Stop(error.to_string()));
493 }
494 return Ok(RealIndexLock {
495 lock_path,
496 real_index: real_index.to_path_buf(),
497 pid_path,
498 identity,
499 });
500}
501
502/// What:
503/// Write `index-lock-<n>.json` for a lock just created and sync the file, not its directory.
504/// Why:
505/// Recovery can prove ownership of the lock before any landing record exists,
506/// so the bytes must be durable before the PID file announces the lock.
507/// The transaction directory's entry is made durable by the directory sync of `landing-<n>.json`, before the branch
508/// moves; a name lost earlier leaves the state of a kill between creating the lock and writing its record.
509///
510/// In TS you'd write (pseudocode):
511/// ```ts
512/// await writeJournalRecord({ directory: transactionDirectory, filename: indexLockRecordFilename(attempt), record, sync: 'file' });
513/// ```
514fn write_lock_record(transaction_directory: &Path, attempt: i64, identity: &LockIdentity) -> Result<(), String> {
515 let record: IndexLockRecord = IndexLockRecord {
516 attempt,
517 lock: identity.clone(),
518 };
519 return write_record(
520 transaction_directory,
521 index_lock_record_filename(attempt).as_str(),
522 encode_index_lock(&record).as_bytes(),
523 RecordSync::FileOnly,
524 );
525}
526
527/// What:
528/// Take the real index lock for a landing attempt:
529/// create it,
530/// journal its identity in `index-lock-<n>.json`,
531/// fix its mode,
532/// and only then announce this process through the PID file.
533/// Why:
534/// `acquireRealIndexLock`:
535/// the journal comes before the PID file,
536/// so a crash never leaves a PID file naming a lock that recovery cannot prove ours;
537/// an existing PID file is left alone,
538/// as the incumbent does.
539/// A failure after the lock exists removes the lock again.
540///
541/// In TS you'd write (pseudocode):
542/// ```ts
543/// await using lock = await acquireRealIndexLock({ realIndexPath, transactionDirectory, attempt, timeoutMs });
544/// ```
545pub fn acquire_landing_index_lock(
546 real_index: &Path,
547 transaction_directory: &Path,
548 attempt: i64,
549 timeout_ms: u64,
550 consequence: &str,
551) -> Result<RealIndexLock, CoordinationFailure> {
552 if let Some((lock_path, identity)) = create_journaled_lock(real_index, transaction_directory, attempt, timeout_ms, consequence)? {
553 let announced: Result<(), String> = announce_held_lock(real_index, lock_path.as_path());
554 if let Err(reason) = announced {
555 let _ = std::fs::remove_file(lock_path.as_path());
556 return Err(CoordinationFailure::Stop(reason));
557 }
558 return Ok(RealIndexLock {
559 lock_path,
560 real_index: real_index.to_path_buf(),
561 pid_path: real_pid_path(real_index),
562 identity,
563 });
564 }
565 let (lock_path, identity): (PathBuf, LockIdentity) = create_identified_lock(real_index, timeout_ms, consequence)?;
566 let announced: Result<(), String> = announce_landing_lock(real_index, transaction_directory, attempt, lock_path.as_path(), &identity);
567 if let Err(reason) = announced {
568 let _ = std::fs::remove_file(lock_path.as_path());
569 return Err(CoordinationFailure::Stop(reason));
570 }
571 return Ok(RealIndexLock {
572 lock_path,
573 real_index: real_index.to_path_buf(),
574 pid_path: real_pid_path(real_index),
575 identity,
576 });
577}
578
579/// What:
580/// The steps between creating a landing's lock and holding it:
581/// journal record,
582/// mode,
583/// PID file.
584/// Why:
585/// One failure path removes the lock,
586/// whichever step failed.
587///
588/// In TS you'd write (pseudocode):
589/// ```ts
590/// await writeJournalRecord(...); await protectPath({ path: lockPath, directory: false }); await writeOwnPidFile(pidPath);
591/// ```
592fn announce_landing_lock(
593 real_index: &Path,
594 transaction_directory: &Path,
595 attempt: i64,
596 lock_path: &Path,
597 identity: &LockIdentity,
598) -> Result<(), String> {
599 write_lock_record(transaction_directory, attempt, identity)?;
600 return announce_held_lock(real_index, lock_path);
601}
602
603/// What:
604/// The steps between holding a landing's lock and announcing it:
605/// mode,
606/// PID file.
607/// Why:
608/// The journal record is already durable, whether it was written before the lock was published or after it was
609/// created.
610///
611/// In TS you'd write (pseudocode):
612/// ```ts
613/// await protectPath({ path: lockPath, directory: false }); await writeOwnPidFile(pidPath);
614/// ```
615fn announce_held_lock(real_index: &Path, lock_path: &Path) -> Result<(), String> {
616 if let Err(error) = protect_path(lock_path, 0o600) {
617 return Err(format!("protecting {} failed: {error}", lock_path.display()));
618 }
619 let pid_path: PathBuf = real_pid_path(real_index);
620 // Unsynced, as in `acquire_real_index_lock`: a torn or lost PID file completes or discards in the tearing table.
621 match write_private_file_unsynced(pid_path.as_path(), format!("pid {}\n", std::process::id()).as_bytes()) {
622 Ok(()) => return Ok(()),
623 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
624 debug(TAG, format!("a PID file already exists beside the lock: {}", pid_path.display()).as_str());
625 return Ok(());
626 }
627 Err(error) => return Err(format!("writing {} failed: {error}", pid_path.display())),
628 }
629}
630
631/// Real-lock controls stay out of the release executable.
632#[cfg(test)]
633#[path = "real_index_lock_tests.rs"]
634mod tests;
635
636/// Controls for failures of the exclusive creation and for the lock created in place after a refused link.
637#[cfg(test)]
638#[path = "real_index_lock_fallback_tests.rs"]
639mod fallback_tests;