Name the scan lock file from the database's resolved path (closes #101)
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.
The lock file is now named from the database's path with symlinks
resolved. Before the database exists, its directory is resolved and the
file name joined to it, so the error names the same lock file either
way. 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:
clawbot
2026-10-14 17:46:38 +00:00
parent 5900feb515
commit 6ccb9d7899
5 changed files with 99 additions and 11 deletions
+21 -6
View File
@@ -140,11 +140,13 @@ 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
// by the database's path with symlinks resolved and ".lock" appended,
// so every path to one database names the same lock file. The lock
// file is 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.
func lockScanDatabase(path string) (*os.File, error) {
err := os.MkdirAll(filepath.Dir(path), dbDirPerm)
@@ -152,7 +154,20 @@ func lockScanDatabase(path string) (*os.File, error) {
return nil, fmt.Errorf("create database directory: %w", err)
}
lockPath := path + ".lock"
resolved, err := filepath.EvalSymlinks(path)
if errors.Is(err, fs.ErrNotExist) {
// No database yet: resolve its directory, which exists now.
var dir string
dir, err = filepath.EvalSymlinks(filepath.Dir(path))
resolved = filepath.Join(dir, filepath.Base(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)