返回 DeepSeek-Reasonix
lock.go
1 package bootstrap
2
3 import (
4 "context"
5 "fmt"
6 "os"
7 "strconv"
8 "strings"
9 "time"
10 )
11
12 const (
13 serveLockPoll = 100 * time.Millisecond
14 serveLockStaleAfter = 60 * time.Second
15 )
16
17 type serveLock struct {
18 fs serveLockFS
19 paths StatePaths
20 owner string
21 }
22
23 // acquireServeLock serializes the short launch/publish critical section across
24 // CLI processes, desktop windows, and reconnect generations. The expensive
25 // locate/install phase stays outside the lock. A crashed owner's directory is
26 // reclaimed only after a minute; the guarded health check itself is bounded to
27 // 20 seconds, so a live owner cannot legitimately age past that threshold.
28 func acquireServeLock(ctx context.Context, fs serveLockFS, paths StatePaths, clock func() time.Time) (*serveLock, error) {
29 if err := fs.MkdirAll(ctx, paths.Dir); err != nil {
30 return nil, err
31 }
32 token, err := generateToken()
33 if err != nil {
34 return nil, err
35 }
36 owner := strconv.FormatInt(clock().Unix(), 10) + ":" + token
37 retriedMissing := false
38 for {
39 mkdirErr := fs.MkdirExclusive(ctx, paths.LockDir)
40 if mkdirErr == nil {
41 if err := fs.WriteFileAtomic(ctx, paths.LockOwner, []byte(owner+"\n"), 0o600); err != nil {
42 _ = fs.Remove(context.Background(), paths.LockDir, true)
43 return nil, fmt.Errorf("bootstrap: write serve lock owner: %w", err)
44 }
45 return &serveLock{fs: fs, paths: paths, owner: owner}, nil
46 }
47 if err := ctx.Err(); err != nil {
48 return nil, fmt.Errorf("bootstrap: wait for serve lock: %w", err)
49 }
50 if !lockCreationMayContend(mkdirErr) {
51 return nil, fmt.Errorf("bootstrap: create serve lock: %w", mkdirErr)
52 }
53
54 lockInfo, statErr := fs.Stat(ctx, paths.LockDir)
55 // The owner may release between mkdir and Stat. Recompete once per
56 // observed lock: SFTP v3 generic failures cannot prove contention, so
57 // repeated missing observations must not spin on permanent failures.
58 if os.IsNotExist(statErr) && !retriedMissing {
59 retriedMissing = true
60 continue
61 }
62 if statErr != nil || !lockInfo.IsDir {
63 return nil, fmt.Errorf("bootstrap: create serve lock: %w", mkdirErr)
64 }
65 retriedMissing = false
66 data, _, _, readErr := fs.ReadFile(ctx, paths.LockOwner, 512)
67 if readErr == nil {
68 observed := strings.TrimSpace(string(data))
69 parts := strings.SplitN(observed, ":", 2)
70 created, parseErr := strconv.ParseInt(parts[0], 10, 64)
71 if parseErr == nil && len(parts) == 2 && clock().Sub(time.Unix(created, 0)) > serveLockStaleAfter {
72 // Compare the owner again immediately before removal. A new owner never
73 // inherits the old random token, so we cannot delete a replacement lock.
74 current, _, _, currentErr := fs.ReadFile(ctx, paths.LockOwner, 512)
75 if currentErr == nil && strings.TrimSpace(string(current)) == observed {
76 _ = fs.Remove(ctx, paths.LockDir, true)
77 continue
78 }
79 }
80 } else if clock().Sub(time.Unix(lockInfo.ModTime, 0)) > serveLockStaleAfter {
81 // The creator may have crashed between mkdir and writing owner. The
82 // critical section cannot legitimately leave an owner-less directory
83 // this old, so reclaim it.
84 if _, _, _, currentErr := fs.ReadFile(ctx, paths.LockOwner, 512); currentErr != nil {
85 _ = fs.Remove(ctx, paths.LockDir, true)
86 continue
87 }
88 }
89
90 select {
91 case <-ctx.Done():
92 return nil, fmt.Errorf("bootstrap: wait for serve lock: %w", ctx.Err())
93 case <-time.After(serveLockPoll):
94 }
95 }
96 }
97
98 func (l *serveLock) release() {
99 if l == nil {
100 return
101 }
102 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
103 defer cancel()
104 data, _, _, err := l.fs.ReadFile(ctx, l.paths.LockOwner, 512)
105 if err == nil && strings.TrimSpace(string(data)) == l.owner {
106 _ = l.fs.Remove(ctx, l.paths.LockDir, true)
107 }
108 }
109
109 lines GO