返回 DeepSeek-Reasonix
shared.go
根目录 / internal / taskcatalog / shared.go
1 package taskcatalog
2
3 import (
4 "context"
5 "path/filepath"
6 "sync"
7
8 "reasonix/internal/taskmonitor"
9 )
10
11 type sharedManager struct {
12 lifecycleMu sync.Mutex
13 mu sync.RWMutex
14 catalog *Catalog
15 pending map[string]string
16 closing bool
17 rebuilding bool
18 generation uint64
19 opening bool
20 openDone chan struct{}
21 openCancel context.CancelFunc
22 open func(context.Context, string) (*Catalog, error)
23 rebuild func(context.Context, string, []Project) (Status, error)
24 }
25
26 var shared sharedManager
27
28 func ensureShared() {
29 shared.start()
30 }
31
32 func (m *sharedManager) start() {
33 m.mu.Lock()
34 if m.catalog != nil || m.opening || m.closing || m.rebuilding {
35 m.mu.Unlock()
36 return
37 }
38 m.generation++
39 generation := m.generation
40 ctx, cancel := context.WithCancel(context.Background())
41 done := make(chan struct{})
42 m.opening, m.openDone, m.openCancel = true, done, cancel
43 openCatalog := m.open
44 if openCatalog == nil {
45 openCatalog = Open
46 }
47 m.mu.Unlock()
48 go m.openGeneration(ctx, generation, done, openCatalog)
49 }
50
51 func (m *sharedManager) openGeneration(ctx context.Context, generation uint64, done chan struct{}, openCatalog func(context.Context, string) (*Catalog, error)) {
52 catalog, err := openCatalog(ctx, "")
53 seen := map[string]bool{}
54 for err == nil {
55 m.mu.Lock()
56 if m.closing || m.rebuilding || generation != m.generation || ctx.Err() != nil {
57 m.mu.Unlock()
58 _ = catalog.Close(context.Background())
59 catalog = nil
60 break
61 }
62 pending := map[string]string{}
63 for root, label := range m.pending {
64 if !seen[root] {
65 seen[root] = true
66 pending[root] = label
67 }
68 }
69 if len(pending) == 0 {
70 m.catalog = catalog
71 m.pending = nil
72 m.mu.Unlock()
73 break
74 }
75 m.mu.Unlock()
76 for root, label := range pending {
77 _, _ = catalog.RegisterProject(ctx, root, label)
78 }
79 }
80 m.mu.Lock()
81 if m.openDone == done {
82 m.opening = false
83 m.openDone = nil
84 m.openCancel = nil
85 }
86 close(done)
87 m.mu.Unlock()
88 }
89
90 func Shared() *Catalog {
91 ensureShared()
92 shared.mu.RLock()
93 defer shared.mu.RUnlock()
94 return shared.catalog
95 }
96
97 // ShutdownShared drains accepted notifications and cancels every shared task
98 // projection worker. It is only used during process shutdown; authoritative
99 // task snapshots and event logs have already committed before notifications.
100 func ShutdownShared(ctx context.Context) error {
101 return shared.close(ctx)
102 }
103
104 func (m *sharedManager) close(ctx context.Context) error {
105 m.lifecycleMu.Lock()
106 defer m.lifecycleMu.Unlock()
107 return m.closeLocked(ctx, false)
108 }
109
110 func (m *sharedManager) closeLocked(ctx context.Context, rebuild bool) error {
111 m.mu.Lock()
112 m.closing = true
113 m.rebuilding = rebuild
114 m.generation++
115 if m.openCancel != nil {
116 m.openCancel()
117 }
118 done := m.openDone
119 catalog := m.catalog
120 m.catalog = nil
121 if !rebuild {
122 m.pending = nil
123 }
124 m.mu.Unlock()
125 var flushErr, closeErr error
126 if catalog != nil {
127 flushErr = catalog.Flush(ctx)
128 closeErr = catalog.Close(ctx)
129 }
130 if done != nil {
131 select {
132 case <-done:
133 case <-ctx.Done():
134 if closeErr == nil {
135 closeErr = ctx.Err()
136 }
137 }
138 }
139 m.mu.Lock()
140 m.closing = false
141 m.rebuilding = rebuild
142 m.mu.Unlock()
143 if flushErr != nil {
144 return flushErr
145 }
146 return closeErr
147 }
148
149 func RegisterSharedProject(root, label string) string {
150 return shared.registerProject(root, label)
151 }
152
153 func (m *sharedManager) registerProject(root, label string) string {
154 m.start()
155 key := ProjectKey(root)
156 m.mu.Lock()
157 if m.rebuilding {
158 if m.pending == nil {
159 m.pending = map[string]string{}
160 }
161 m.pending[root] = label
162 m.mu.Unlock()
163 return key
164 }
165 if m.closing {
166 m.mu.Unlock()
167 return key
168 }
169 if m.catalog == nil {
170 if m.pending == nil {
171 m.pending = map[string]string{}
172 }
173 m.pending[root] = label
174 m.mu.Unlock()
175 return key
176 }
177 catalog := m.catalog
178 m.mu.Unlock()
179 _, _ = catalog.RegisterProject(context.Background(), root, label)
180 return key
181 }
182
183 type sharedSink struct{}
184
185 func (sharedSink) SnapshotChanged(projectRoot, taskID string) {
186 catalog := sharedCatalogForNotification(projectRoot)
187 if catalog != nil {
188 catalog.SnapshotChanged(projectRoot, taskID)
189 }
190 }
191
192 func (sharedSink) EventsChanged(projectRoot, taskID string) {
193 catalog := sharedCatalogForNotification(projectRoot)
194 if catalog != nil {
195 catalog.EventsChanged(projectRoot, taskID)
196 }
197 }
198
199 // sharedCatalogForNotification is deliberately SQLite-free. ProjectionSink is
200 // called after the authoritative task file lock is released, but task saves
201 // still must never wait for catalog I/O. A notification received while the
202 // catalog is opening is recovered by the pending project's initial reconcile.
203 func sharedCatalogForNotification(projectRoot string) *Catalog {
204 return shared.catalogForNotification(projectRoot)
205 }
206
207 func (m *sharedManager) catalogForNotification(projectRoot string) *Catalog {
208 m.start()
209 m.mu.Lock()
210 defer m.mu.Unlock()
211 if m.rebuilding {
212 if m.pending == nil {
213 m.pending = map[string]string{}
214 }
215 m.pending[projectRoot] = filepath.Base(projectRoot)
216 return nil
217 }
218 if m.closing {
219 return nil
220 }
221 if m.catalog == nil {
222 if m.pending == nil {
223 m.pending = map[string]string{}
224 }
225 m.pending[projectRoot] = filepath.Base(projectRoot)
226 return nil
227 }
228 return m.catalog
229 }
230
231 // ObservedStore remains an authoritative FileStore; only its post-commit sink
232 // is shared with the disposable catalog.
233 func ObservedStore() taskmonitor.WriteStore {
234 ensureShared()
235 return taskmonitor.NewObservedFileStore(filepath.Join(".reasonix", "tasks"), sharedSink{})
236 }
237
237 lines GO