feat: transaction implement

This commit is contained in:
2026-08-15 02:30:36 +08:00
parent 6515c93378
commit b4e18c6f0c
21 changed files with 3886 additions and 0 deletions
+10
View File
@@ -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")
)
+84
View File
@@ -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)
}
+40
View File
@@ -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)
}
}
+16
View File
@@ -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
}