package store import ( "fmt" "os" "strings" "time" ) const ( // lockTimeout — сколько ждём освобождения хранилища другим процессом. lockTimeout = 5 * time.Second // lockStaleAfter — после этого возраста замок считается брошенным // (процесс убит, не успев его снять). lockStaleAfter = 60 * time.Second ) // Lock — межпроцессная блокировка хранилища на основе файла, создаваемого // с O_EXCL. В отличие от flock работает одинаково на Unix и Windows. type Lock struct { path string warn func(string) } // acquireLock захватывает замок для файла хранилища path. // warn получает предупреждения (обычно печатается в stderr); может быть nil. func acquireLock(path string, warn func(string)) (*Lock, error) { if warn == nil { warn = func(string) {} } l := &Lock{path: path + ".lock", warn: warn} deadline := time.Now().Add(lockTimeout) delay := 10 * time.Millisecond brokeStale := false for { f, err := os.OpenFile(l.path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600) if err == nil { fmt.Fprintf(f, "pid %d, %s\n", os.Getpid(), time.Now().Format(time.RFC3339)) f.Close() return l, nil } if !os.IsExist(err) { return nil, fmt.Errorf("не удалось создать файл блокировки %s: %w", l.path, err) } // Замок занят: возможно, его владелец давно умер. if !brokeStale { if fi, statErr := os.Stat(l.path); statErr == nil && time.Since(fi.ModTime()) > lockStaleAfter { l.warn(fmt.Sprintf("снимаю брошенный файл блокировки %s (возраст %s)", l.path, time.Since(fi.ModTime()).Round(time.Second))) os.Remove(l.path) brokeStale = true continue } } if time.Now().After(deadline) { return nil, fmt.Errorf("хранилище занято другим процессом: файл блокировки %s.\n"+ "Если процесс уже завершён, удалите этот файл вручную", l.path) } time.Sleep(delay) if delay < 200*time.Millisecond { delay *= 2 } } } // release снимает блокировку. func (l *Lock) release() error { if l == nil { return nil } if err := os.Remove(l.path); err != nil && !os.IsNotExist(err) { return fmt.Errorf("не удалось снять блокировку %s: %w", l.path, err) } return nil } // IsLockPath сообщает, является ли путь служебным файлом блокировки. func IsLockPath(p string) bool { return strings.HasSuffix(p, ".lock") }