Skip to content

Commit bae7be8

Browse files
committed
revert(code-index): keep watcher restart at PR 1821 scope
1 parent 770f0eb commit bae7be8

3 files changed

Lines changed: 9 additions & 186 deletions

File tree

‎src/services/code-index/interfaces/file-processor.ts‎

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,6 @@ export interface IFileWatcher extends vscode.Disposable {
5656
*/
5757
initialize(): Promise<void>
5858

59-
/**
60-
* Waits for all started watcher batches to settle, including vector writes and hash updates.
61-
* Call dispose() first to stop intake and discard queued events. Does not drain scans
62-
* or the cache manager's independent debounced disk saves.
63-
*/
64-
waitForIdle(): Promise<void>
65-
6659
/**
6760
* Event emitted when a batch of files begins processing.
6861
* The event payload is an array of file paths included in the batch.

‎src/services/code-index/processors/__tests__/file-watcher.spec.ts‎

Lines changed: 2 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
import * as vscode from "vscode"
44

55
import { FileWatcher } from "../file-watcher"
6-
import type { PointStruct } from "../../interfaces"
7-
import { createHash } from "crypto"
86

97
import { clearAllMocks } from "../../../../test-utils/reset"
108

@@ -93,136 +91,6 @@ vi.mock("vscode", () => ({
9391
}))
9492

9593
describe("FileWatcher", () => {
96-
const deferred = () => {
97-
let resolve!: () => void
98-
const promise = new Promise<void>((done) => {
99-
resolve = done
100-
})
101-
return { promise, resolve }
102-
}
103-
104-
it("drains delayed persistence before concurrent restarts can write newer points and hashes", async () => {
105-
await fileWatcher.initialize()
106-
vi.mocked(vscode.workspace.createFileSystemWatcher).mockClear()
107-
const blocked = deferred()
108-
const entered = deferred()
109-
const persisted = new Map<string | number, PointStruct>()
110-
const hashes = new Map<string, string>()
111-
mockCacheManager.updateHash.mockImplementation((path: string, hash: string) => hashes.set(path, hash))
112-
mockVectorStore.upsertPoints.mockImplementation(async (points: PointStruct[]) => {
113-
entered.resolve()
114-
await blocked.promise
115-
for (const point of points) persisted.set(point.id, point)
116-
})
117-
const path = "/mock/workspace/same.ts"
118-
await mockOnDidCreate(vscode.Uri.file(path))
119-
await flushBatch()
120-
await entered.promise
121-
fileWatcher.dispose()
122-
const restarted = vi.fn()
123-
const restarts = [fileWatcher.initialize().then(restarted), fileWatcher.initialize()]
124-
await Promise.resolve()
125-
expect(restarted).not.toHaveBeenCalled()
126-
expect(vscode.workspace.createFileSystemWatcher).not.toHaveBeenCalled()
127-
expect(hashes.size).toBe(0)
128-
blocked.resolve()
129-
await Promise.all(restarts)
130-
expect(vscode.workspace.createFileSystemWatcher).toHaveBeenCalledOnce()
131-
vi.spyOn(fileWatcher, "processFile").mockResolvedValueOnce({
132-
path,
133-
status: "processed_for_batching",
134-
newHash: "new-hash",
135-
pointsToUpsert: [...persisted.values()].map((point) => ({ ...point, vector: [9, 9, 9] })),
136-
})
137-
await mockOnDidChange(vscode.Uri.file(path))
138-
await flushBatch()
139-
await fileWatcher.waitForIdle()
140-
expect([...persisted.values()].map((point) => point.vector)).toEqual([[9, 9, 9]])
141-
expect(hashes.get(path)).toBe("new-hash")
142-
expect(mockCacheManager.updateHash.mock.calls.map((call: string[]) => call[1])).toEqual([
143-
createHash("sha256").update("test content").digest("hex"),
144-
"new-hash",
145-
])
146-
})
147-
148-
it("waits for every overlapping batch, not only the latest, and releases all idle callers", async () => {
149-
await fileWatcher.initialize()
150-
const first = deferred()
151-
const second = deferred()
152-
mockVectorStore.upsertPoints
153-
.mockImplementationOnce(() => first.promise)
154-
.mockImplementationOnce(() => second.promise)
155-
await mockOnDidCreate(vscode.Uri.file("/mock/workspace/first.ts"))
156-
await flushBatch()
157-
await mockOnDidCreate(vscode.Uri.file("/mock/workspace/second.ts"))
158-
await flushBatch()
159-
expect(mockVectorStore.upsertPoints).toHaveBeenCalledTimes(2)
160-
fileWatcher.dispose()
161-
const idle = vi.fn()
162-
const waiters = [fileWatcher.waitForIdle().then(idle), fileWatcher.waitForIdle().then(idle)]
163-
second.resolve()
164-
await flushBatch()
165-
expect(idle).not.toHaveBeenCalled()
166-
first.resolve()
167-
await Promise.all(waiters)
168-
expect(idle).toHaveBeenCalledTimes(2)
169-
expect(fileWatcher["activeBatches"].size).toBe(0)
170-
await fileWatcher.waitForIdle()
171-
})
172-
173-
it("does not revive a pending restart after another disposal or accept stale native callbacks", async () => {
174-
await fileWatcher.initialize()
175-
const staleCreate = mockOnDidCreate
176-
const blocked = deferred()
177-
mockVectorStore.upsertPoints.mockImplementationOnce(() => blocked.promise)
178-
await mockOnDidCreate(vscode.Uri.file("/mock/workspace/old.ts"))
179-
await flushBatch()
180-
fileWatcher.dispose()
181-
vi.mocked(vscode.workspace.createFileSystemWatcher).mockClear()
182-
const restart = fileWatcher.initialize()
183-
fileWatcher.dispose()
184-
blocked.resolve()
185-
await restart
186-
expect(vscode.workspace.createFileSystemWatcher).not.toHaveBeenCalled()
187-
await fileWatcher.initialize()
188-
await staleCreate(vscode.Uri.file("/mock/workspace/stale.ts"))
189-
await flushBatch()
190-
expect(mockVectorStore.upsertPoints).toHaveBeenCalledOnce()
191-
expect(vscode.workspace.createFileSystemWatcher).toHaveBeenCalledOnce()
192-
})
193-
194-
it("registers work before a batch-start listener restarts the watcher", async () => {
195-
await fileWatcher.initialize()
196-
const blocked = deferred()
197-
mockVectorStore.upsertPoints.mockImplementationOnce(() => blocked.promise)
198-
let restart: Promise<void> | undefined
199-
fileWatcher.onDidStartBatchProcessing(() => {
200-
fileWatcher.dispose()
201-
restart = fileWatcher.initialize()
202-
})
203-
vi.mocked(vscode.workspace.createFileSystemWatcher).mockClear()
204-
await mockOnDidCreate(vscode.Uri.file("/mock/workspace/old.ts"))
205-
await flushBatch()
206-
expect(restart).toBeDefined()
207-
expect(vscode.workspace.createFileSystemWatcher).not.toHaveBeenCalled()
208-
blocked.resolve()
209-
await restart
210-
expect(vscode.workspace.createFileSystemWatcher).toHaveBeenCalledOnce()
211-
})
212-
213-
it("handles timer batch rejection and removes rejected work from the drain set", async () => {
214-
await fileWatcher.initialize()
215-
const error = new Error("unexpected batch failure")
216-
const log = vi.spyOn(console, "error").mockImplementation(() => {})
217-
fileWatcher["processBatch"] = vi.fn<FileWatcher["processBatch"]>().mockRejectedValueOnce(error)
218-
await mockOnDidCreate(vscode.Uri.file("/mock/workspace/error.ts"))
219-
await flushBatch()
220-
await fileWatcher.waitForIdle()
221-
expect(log).toHaveBeenCalledWith("[FileWatcher] Unhandled batch processing error:", error)
222-
expect(fileWatcher["activeBatches"].size).toBe(0)
223-
log.mockRestore()
224-
})
225-
22694
it("does not deliver an old batch into subscriptions created after restart", async () => {
22795
await fileWatcher.initialize()
22896
let release!: () => void
@@ -242,13 +110,12 @@ describe("FileWatcher", () => {
242110
const processing = flushBatch()
243111
await started
244112
fileWatcher.dispose()
245-
const restarting = fileWatcher.initialize()
246-
release()
247-
await restarting
113+
await fileWatcher.initialize()
248114
const progress = vi.fn()
249115
const finished = vi.fn()
250116
fileWatcher.onBatchProgressUpdate(progress)
251117
fileWatcher.onDidFinishBatchProcessing(finished)
118+
release()
252119
await processing
253120
expect(progress).not.toHaveBeenCalled()
254121
expect(finished).not.toHaveBeenCalled()

‎src/services/code-index/processors/file-watcher.ts‎

Lines changed: 7 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,6 @@ export class FileWatcher implements IFileWatcher {
4242
private readonly batchSegmentThreshold: number
4343

4444
private eventsDisposed = false
45-
private sessionGeneration = 0
46-
private readonly activeBatches = new Set<Promise<void>>()
4745
private _onDidStartBatchProcessing = new vscode.EventEmitter<string[]>()
4846
private _onBatchProgressUpdate = new vscode.EventEmitter<{
4947
processedInBatch: number
@@ -116,10 +114,6 @@ export class FileWatcher implements IFileWatcher {
116114
*/
117115
async initialize(): Promise<void> {
118116
if (this.fileWatcher) return
119-
const generation = this.sessionGeneration
120-
await this.waitForIdle()
121-
// A stop invalidates pending restarts; concurrent initializers share the same watcher.
122-
if (generation !== this.sessionGeneration || this.fileWatcher) return
123117
if (this.eventsDisposed) {
124118
this._onDidStartBatchProcessing = new vscode.EventEmitter<string[]>()
125119
this._onBatchProgressUpdate = new vscode.EventEmitter<{
@@ -138,32 +132,15 @@ export class FileWatcher implements IFileWatcher {
138132
this.fileWatcher = vscode.workspace.createFileSystemWatcher(filePattern)
139133

140134
// Register event handlers
141-
this.fileWatcher.onDidCreate((uri) => {
142-
if (generation === this.sessionGeneration) void this.handleFileCreated(uri)
143-
})
144-
this.fileWatcher.onDidChange((uri) => {
145-
if (generation === this.sessionGeneration) void this.handleFileChanged(uri)
146-
})
147-
this.fileWatcher.onDidDelete((uri) => {
148-
if (generation === this.sessionGeneration) void this.handleFileDeleted(uri)
149-
})
150-
}
151-
152-
/** Waits for all started batches, including persistence and hash updates, to settle.
153-
* Dispose first to discard queued events and prevent more work from arriving.
154-
* This does not drain independent cache-save timers or directory scans.
155-
*/
156-
async waitForIdle(): Promise<void> {
157-
while (this.activeBatches.size > 0) {
158-
await Promise.allSettled([...this.activeBatches])
159-
}
135+
this.fileWatcher.onDidCreate(this.handleFileCreated.bind(this))
136+
this.fileWatcher.onDidChange(this.handleFileChanged.bind(this))
137+
this.fileWatcher.onDidDelete(this.handleFileDeleted.bind(this))
160138
}
161139

162140
/**
163141
* Disposes the file watcher
164142
*/
165143
dispose(): void {
166-
this.sessionGeneration++
167144
this.fileWatcher?.dispose()
168145
this.fileWatcher = undefined
169146
if (this.batchProcessDebounceTimer) {
@@ -211,12 +188,7 @@ export class FileWatcher implements IFileWatcher {
211188
if (this.batchProcessDebounceTimer) {
212189
clearTimeout(this.batchProcessDebounceTimer)
213190
}
214-
this.batchProcessDebounceTimer = setTimeout(() => {
215-
this.batchProcessDebounceTimer = undefined
216-
void this.triggerBatchProcessing().catch((error: unknown) => {
217-
console.error("[FileWatcher] Unhandled batch processing error:", error)
218-
})
219-
}, this.BATCH_DEBOUNCE_DELAY_MS)
191+
this.batchProcessDebounceTimer = setTimeout(() => this.triggerBatchProcessing(), this.BATCH_DEBOUNCE_DELAY_MS)
220192
}
221193

222194
/**
@@ -233,22 +205,13 @@ export class FileWatcher implements IFileWatcher {
233205
// Capture this session's emitters before notifying listeners or awaiting work.
234206
// Disposed emitters drop late events instead of forwarding them to a restarted session.
235207
const batchEvents = {
236-
started: this._onDidStartBatchProcessing,
237208
progress: this._onBatchProgressUpdate,
238209
finished: this._onDidFinishBatchProcessing,
239210
}
240211
const filePathsInBatch = Array.from(eventsToProcess.keys())
241-
// Register before invoking listeners: a listener may synchronously stop/restart us.
242-
const batch = Promise.resolve().then(async () => {
243-
batchEvents.started.fire(filePathsInBatch)
244-
await this.processBatch(eventsToProcess, batchEvents)
245-
})
246-
this.activeBatches.add(batch)
247-
try {
248-
await batch
249-
} finally {
250-
this.activeBatches.delete(batch)
251-
}
212+
this._onDidStartBatchProcessing.fire(filePathsInBatch)
213+
214+
await this.processBatch(eventsToProcess, batchEvents)
252215
}
253216

254217
/**

0 commit comments

Comments
 (0)