返回 DeepSeek-Reasonix
roots.go
1 package workspacelease
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "os"
8 "sort"
9 "strings"
10 "sync"
11
12 "reasonix/internal/filelock"
13 )
14
15 type rootLockDomain struct {
16 path string
17 key string
18 mode filelock.Mode
19 }
20
21 var workspaceRootsAfterAcquire = func() {}
22
23 // HoldWriteRoots acquires one exclusive hold spanning several workspace roots.
24 // It preserves the legacy ancestor locks and coalesces tree-stripe collisions
25 // before acquisition, so a group cannot wait for a stripe it already owns.
26 // Callers must not hold separate leases for these roots while acquiring a group.
27 func HoldWriteRoots(ctx context.Context, lockDir string, roots ...string) (func(), error) {
28 if ctx == nil {
29 ctx = context.Background()
30 }
31 expected, err := snapshotWorkspaceRoots(roots)
32 if err != nil {
33 return nil, err
34 }
35 owner, domains, err := rootLockDomains(lockDir, roots)
36 if err != nil {
37 return nil, err
38 }
39 releases, err := acquireRootDomains(ctx, owner, domains)
40 if err != nil {
41 return nil, err
42 }
43 workspaceRootsAfterAcquire()
44 for _, snapshot := range expected {
45 canonical, _, identityErr := workspaceIdentities(snapshot.path)
46 currentInfo, statErr := os.Stat(snapshot.path)
47 currentExists := statErr == nil
48 if statErr != nil && !os.IsNotExist(statErr) && identityErr == nil {
49 identityErr = statErr
50 }
51 changed := identityErr == nil && (canonical != snapshot.key || currentExists != snapshot.exists || (currentExists && !os.SameFile(snapshot.info, currentInfo)))
52 if identityErr != nil || changed {
53 runReleases(releases)
54 if identityErr != nil {
55 return nil, fmt.Errorf("revalidate workspace root: %w", identityErr)
56 }
57 return nil, errors.New("workspace root identity changed while waiting")
58 }
59 }
60 if err := ctx.Err(); err != nil {
61 runReleases(releases)
62 return nil, err
63 }
64 var once sync.Once
65 return func() { once.Do(func() { runReleases(releases) }) }, nil
66 }
67
68 type workspaceRootSnapshot struct {
69 path string
70 key string
71 info os.FileInfo
72 exists bool
73 }
74
75 func snapshotWorkspaceRoots(roots []string) ([]workspaceRootSnapshot, error) {
76 snapshots := make([]workspaceRootSnapshot, 0, len(roots))
77 for _, root := range roots {
78 if strings.TrimSpace(root) == "" {
79 continue
80 }
81 key, _, err := workspaceIdentities(root)
82 if err != nil {
83 return nil, err
84 }
85 info, statErr := os.Stat(root)
86 exists := statErr == nil
87 if statErr != nil && !os.IsNotExist(statErr) {
88 return nil, statErr
89 }
90 snapshots = append(snapshots, workspaceRootSnapshot{path: root, key: key, info: info, exists: exists})
91 }
92 return snapshots, nil
93 }
94
95 func acquireRootDomains(ctx context.Context, owner *Owner, domains []rootLockDomain) ([]func(), error) {
96 notified := false
97 for {
98 releases := make([]func(), 0, len(domains))
99 var blocked *rootLockDomain
100 for i := range domains {
101 domain := &domains[i]
102 release, err := filelock.TryAcquireModeWithKey(domain.path, domain.key, domain.mode)
103 if err == nil {
104 releases = append(releases, release)
105 continue
106 }
107 runReleases(releases)
108 if !errors.Is(err, filelock.ErrHeld) {
109 return nil, err
110 }
111 blocked = domain
112 break
113 }
114 if blocked == nil {
115 return releases, nil
116 }
117 waitRelease, err := owner.acquireQueuedMode(ctx, blocked.path, blocked.mode, &notified)
118 if err != nil {
119 return nil, err
120 }
121 waitRelease()
122 }
123 }
124
125 func rootLockDomains(lockDir string, roots []string) (*Owner, []rootLockDomain, error) {
126 lockDir = strings.TrimSpace(lockDir)
127 var coordinator *Owner
128 var compatibilityRoots []string
129 exclusive := map[string]bool{}
130 trees := map[string]bool{}
131 for _, root := range roots {
132 if strings.TrimSpace(root) == "" {
133 continue
134 }
135 owner, err := New(root, lockDir, nil)
136 if err != nil {
137 return nil, nil, err
138 }
139 if coordinator == nil {
140 coordinator = owner
141 }
142 exclusive[owner.canonical] = true
143 compatibilityRoots = append(compatibilityRoots, ancestorDirectories(owner.canonical)...)
144 compatibilityRoots = append(compatibilityRoots, ancestorDirectories(owner.compatibility)...)
145 trees[owner.treeLockPath(owner.canonical)] = true
146 }
147 // Match the single-root protocol: all compatibility ancestors first, then
148 // tree stripes. Promote an ancestor requested by this group to exclusive.
149 var domains []rootLockDomain
150 for _, root := range orderedWorkspaceRoots(compatibilityRoots) {
151 mode := filelock.ModeShared
152 if exclusive[normalizeIdentityPath(root)] {
153 mode = filelock.ModeExclusive
154 }
155 domain, err := makeRootLockDomain(workspaceLockPath(lockDir, root), mode)
156 if err != nil {
157 return nil, nil, err
158 }
159 domains = append(domains, domain)
160 }
161 paths := make([]string, 0, len(trees))
162 for path := range trees {
163 paths = append(paths, path)
164 }
165 sort.Strings(paths)
166 for _, path := range paths {
167 domain, err := makeRootLockDomain(path, filelock.ModeExclusive)
168 if err != nil {
169 return nil, nil, err
170 }
171 domains = append(domains, domain)
172 }
173 return coordinator, domains, nil
174 }
175
176 func makeRootLockDomain(path string, mode filelock.Mode) (rootLockDomain, error) {
177 key, err := lockIdentityKey(path)
178 if err != nil {
179 return rootLockDomain{}, err
180 }
181 return rootLockDomain{path: path, key: key, mode: mode}, nil
182 }
183
183 lines GO