feat: transaction implement
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
package processlock
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
// ErrAlreadyLocked 表示另一个 serve 进程已经持有内核锁。
|
||||
ErrAlreadyLocked = errors.New("process lock is already held")
|
||||
// ErrUnsupported 表示当前操作系统不能运行服务端进程锁。
|
||||
ErrUnsupported = errors.New("process lock is not supported on this operating system")
|
||||
)
|
||||
@@ -0,0 +1,84 @@
|
||||
//go:build linux
|
||||
|
||||
package processlock
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// Lock 持有与文件描述符绑定的 Linux advisory flock。
|
||||
// 锁文件可以永久存在;只有内核锁状态表示当前所有权。
|
||||
type Lock struct {
|
||||
mu sync.Mutex
|
||||
file *os.File
|
||||
}
|
||||
|
||||
// Acquire 以非阻塞方式获取排他锁,并把当前 PID 写入文件用于诊断。
|
||||
func Acquire(path string) (*Lock, error) {
|
||||
if path == "" {
|
||||
return nil, errors.New("process lock path is required")
|
||||
}
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolve process lock path: %w", err)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(absPath), 0o750); err != nil {
|
||||
return nil, fmt.Errorf("create process lock directory: %w", err)
|
||||
}
|
||||
file, err := os.OpenFile(absPath, os.O_CREATE|os.O_RDWR, 0o640)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open process lock: %w", err)
|
||||
}
|
||||
if err := unix.Flock(int(file.Fd()), unix.LOCK_EX|unix.LOCK_NB); err != nil {
|
||||
_ = file.Close()
|
||||
if errors.Is(err, unix.EWOULDBLOCK) || errors.Is(err, unix.EAGAIN) {
|
||||
return nil, ErrAlreadyLocked
|
||||
}
|
||||
return nil, fmt.Errorf("acquire process lock: %w", err)
|
||||
}
|
||||
if err := writeOwnerPID(file); err != nil {
|
||||
_ = unix.Flock(int(file.Fd()), unix.LOCK_UN)
|
||||
_ = file.Close()
|
||||
return nil, err
|
||||
}
|
||||
return &Lock{file: file}, nil
|
||||
}
|
||||
|
||||
func writeOwnerPID(file *os.File) error {
|
||||
if err := file.Truncate(0); err != nil {
|
||||
return fmt.Errorf("truncate process lock metadata: %w", err)
|
||||
}
|
||||
if _, err := file.Seek(0, 0); err != nil {
|
||||
return fmt.Errorf("seek process lock metadata: %w", err)
|
||||
}
|
||||
if _, err := fmt.Fprintf(file, "%d\n", os.Getpid()); err != nil {
|
||||
return fmt.Errorf("write process lock metadata: %w", err)
|
||||
}
|
||||
if err := file.Sync(); err != nil {
|
||||
return fmt.Errorf("flush process lock metadata: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close 释放内核锁并关闭文件描述符,不删除锁文件。
|
||||
func (l *Lock) Close() error {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if l.file == nil {
|
||||
return nil
|
||||
}
|
||||
file := l.file
|
||||
l.file = nil
|
||||
unlockErr := unix.Flock(int(file.Fd()), unix.LOCK_UN)
|
||||
closeErr := file.Close()
|
||||
return errors.Join(unlockErr, closeErr)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//go:build linux
|
||||
|
||||
package processlock
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLockUsesKernelOwnershipAndLeavesFileInPlace(t *testing.T) {
|
||||
t.Parallel()
|
||||
path := filepath.Join(t.TempDir(), "serve.lock")
|
||||
first, err := Acquire(path)
|
||||
if err != nil {
|
||||
t.Fatalf("acquire first lock: %v", err)
|
||||
}
|
||||
second, err := Acquire(path)
|
||||
if !errors.Is(err, ErrAlreadyLocked) || second != nil {
|
||||
t.Fatalf("expected second acquire to fail with lock ownership, lock=%v err=%v", second, err)
|
||||
}
|
||||
metadata, err := os.ReadFile(path)
|
||||
if err != nil || len(metadata) == 0 {
|
||||
t.Fatalf("read diagnostic PID: data=%q err=%v", metadata, err)
|
||||
}
|
||||
if err := first.Close(); err != nil {
|
||||
t.Fatalf("release first lock: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
t.Fatalf("lock file should remain after release: %v", err)
|
||||
}
|
||||
third, err := Acquire(path)
|
||||
if err != nil {
|
||||
t.Fatalf("reacquire existing lock file: %v", err)
|
||||
}
|
||||
if err := third.Close(); err != nil {
|
||||
t.Fatalf("release reacquired lock: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//go:build !linux
|
||||
|
||||
package processlock
|
||||
|
||||
// Lock 仅用于让共享代码在非 Linux client 构建中保持可编译。
|
||||
type Lock struct{}
|
||||
|
||||
// Acquire 明确拒绝在非 Linux 系统启动服务端进程锁。
|
||||
func Acquire(string) (*Lock, error) {
|
||||
return nil, ErrUnsupported
|
||||
}
|
||||
|
||||
// Close 对未获取的非 Linux 锁不执行操作。
|
||||
func (l *Lock) Close() error {
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user