83 lines
2.8 KiB
Go
83 lines
2.8 KiB
Go
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") }
|