Name the scan lock file from the database's resolved path (closes #101)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
The lock file was the configured database path with .lock appended, so a scan reaching the database through a symlink to it locked a different file and ran alongside a scan holding the lock through the real path. Before it takes the lock, scan now creates the database file, empty, if it is missing, following a symlink to its target as SQLite would, and names the lock file from that file's path with symlinks resolved. A scan started before the database exists therefore takes the same lock file as every later scan through the same path. testDBPath resolves its temp dir, so lock errors name the expected path where the temp dir is itself reached through a symlink. Model: opus-5-5
This commit is contained in:
@@ -35,6 +35,10 @@ const schemaVersion = 1
|
||||
// scan.
|
||||
const dbDirPerm = 0o755
|
||||
|
||||
// dbFilePerm is the mode for a database file created by scan, the mode
|
||||
// SQLite gives a database file it creates.
|
||||
const dbFilePerm = 0o644
|
||||
|
||||
// lockFilePerm is the mode for the scan lock file. Anyone who can open
|
||||
// the file can hold the lock and keep every scan from running, so it
|
||||
// is open to its owner only.
|
||||
@@ -140,19 +144,37 @@ func openDB(path, params string) (*sql.DB, error) {
|
||||
|
||||
// lockScanDatabase takes the lock that keeps a second scan off the
|
||||
// database at path: an exclusive flock(2) on the file beside it named
|
||||
// path with ".lock" appended, created along with the database's parent
|
||||
// directory if missing. A lock held by another scan fails at once
|
||||
// instead of waiting. The lock lasts until the returned file is closed
|
||||
// or the process ends. The file is never deleted: a scan that deleted
|
||||
// it would let the next scan lock a new file while another still holds
|
||||
// the old one.
|
||||
// by the database's path with symlinks resolved and ".lock" appended,
|
||||
// so every path to one database names the same lock file. If missing,
|
||||
// the database's parent directory and an empty database file are
|
||||
// created first, so the lock file is named the same way before the
|
||||
// database is set up as after. A lock held by another scan fails at
|
||||
// once instead of waiting. The lock lasts until the returned file is
|
||||
// closed or the process ends. The lock file is never deleted: a scan
|
||||
// that deleted it would let the next scan lock a new file while
|
||||
// another still holds the old one.
|
||||
func lockScanDatabase(path string) (*os.File, error) {
|
||||
err := os.MkdirAll(filepath.Dir(path), dbDirPerm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create database directory: %w", err)
|
||||
}
|
||||
|
||||
lockPath := path + ".lock"
|
||||
// Like SQLite, this follows a symlink at path and creates its
|
||||
// target. SQLite opens an empty file as a new database.
|
||||
//nolint:gosec // the operator chooses the database path
|
||||
dbFile, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, dbFilePerm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create database: %w", err)
|
||||
}
|
||||
|
||||
_ = dbFile.Close()
|
||||
|
||||
resolved, err := filepath.EvalSymlinks(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolve database path: %w", err)
|
||||
}
|
||||
|
||||
lockPath := resolved + ".lock"
|
||||
|
||||
//nolint:gosec // the operator chooses the database path
|
||||
f, err := os.OpenFile(lockPath, os.O_RDWR|os.O_CREATE, lockFilePerm)
|
||||
|
||||
Reference in New Issue
Block a user