Files
hotp_calc/internal/store/lock.go
T

83 lines
2.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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") }