fix: prevent race conditions during vault index rebuild and rate-limit embeddings
- Add AbortController to cancel ongoing indexing before clear/rebuild - Track currentIndexingPromise to await cancellation before clearing collection - Batch background indexing (5 files at a time) with 100ms delays between batches - Skip incremental event-based indexing while a full rebuild is in progress - Cancel indexing on plugin unload - Fix nginx CORS headers on OPTIONS preflight responses
This commit is contained in:
@@ -9958,6 +9958,7 @@ var OllamaPlugin = class extends import_obsidian4.Plugin {
|
|||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
onunload() {
|
onunload() {
|
||||||
|
this.cancelBackgroundIndexing();
|
||||||
if (this.semanticCache) {
|
if (this.semanticCache) {
|
||||||
void this.semanticCache.clearCache();
|
void this.semanticCache.clearCache();
|
||||||
}
|
}
|
||||||
@@ -9970,45 +9971,92 @@ var OllamaPlugin = class extends import_obsidian4.Plugin {
|
|||||||
await this.saveData(this.settings);
|
await this.saveData(this.settings);
|
||||||
}
|
}
|
||||||
async initializeVaultVectorStore() {
|
async initializeVaultVectorStore() {
|
||||||
|
this.cancelBackgroundIndexing();
|
||||||
|
await this.awaitBackgroundIndexing();
|
||||||
this.vaultVectorStore = new VaultVectorStore(
|
this.vaultVectorStore = new VaultVectorStore(
|
||||||
this.settings.ollamaUrl,
|
this.settings.ollamaUrl,
|
||||||
this.settings.vaultIndexConfig
|
this.settings.vaultIndexConfig
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
await this.vaultVectorStore.initialize();
|
await this.vaultVectorStore.initialize();
|
||||||
void this.backgroundIndexVault().catch((err) => {
|
this.currentIndexingPromise = this.backgroundIndexVault();
|
||||||
|
void this.currentIndexingPromise.catch((err) => {
|
||||||
Logger.warn(`Background vault indexing failed: ${String(err)}`, "main");
|
Logger.warn(`Background vault indexing failed: ${String(err)}`, "main");
|
||||||
|
}).finally(() => {
|
||||||
|
this.currentIndexingPromise = void 0;
|
||||||
|
this.indexingAbortController = void 0;
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
new import_obsidian4.Notice("Vault vector store initialization failed. Check console for details.");
|
new import_obsidian4.Notice("Vault vector store initialization failed. Check console for details.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
cancelBackgroundIndexing() {
|
||||||
|
if (this.indexingAbortController) {
|
||||||
|
this.indexingAbortController.abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async awaitBackgroundIndexing() {
|
||||||
|
if (this.currentIndexingPromise) {
|
||||||
|
try {
|
||||||
|
await this.currentIndexingPromise;
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
async backgroundIndexVault() {
|
async backgroundIndexVault() {
|
||||||
if (!this.vaultVectorStore) return;
|
if (!this.vaultVectorStore) return;
|
||||||
|
this.indexingAbortController = new AbortController();
|
||||||
|
const signal = this.indexingAbortController.signal;
|
||||||
const files = this.app.vault.getMarkdownFiles();
|
const files = this.app.vault.getMarkdownFiles();
|
||||||
Logger.info(`Starting background vault indexing for ${files.length} files...`, "main");
|
Logger.info(`Starting background vault indexing for ${files.length} files...`, "main");
|
||||||
let indexed = 0;
|
let indexed = 0;
|
||||||
for (const file of files) {
|
const BATCH_SIZE = 5;
|
||||||
try {
|
const DELAY_MS = 100;
|
||||||
const content = await this.app.vault.read(file);
|
for (let i = 0; i < files.length; i += BATCH_SIZE) {
|
||||||
await this.vaultVectorStore.indexFile(file, content);
|
if (signal.aborted) {
|
||||||
indexed++;
|
Logger.info("Vault indexing cancelled.", "main");
|
||||||
} catch (error) {
|
return;
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
}
|
||||||
Logger.warn(`Failed to index ${file.path}: ${errorMessage}`, "main");
|
const batch = files.slice(i, i + BATCH_SIZE);
|
||||||
|
await Promise.all(
|
||||||
|
batch.map(async (file) => {
|
||||||
|
if (signal.aborted) return;
|
||||||
|
try {
|
||||||
|
const content = await this.app.vault.read(file);
|
||||||
|
if (signal.aborted) return;
|
||||||
|
await this.vaultVectorStore.indexFile(file, content);
|
||||||
|
indexed++;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
Logger.warn(`Failed to index ${file.path}: ${errorMessage}`, "main");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
if (i + BATCH_SIZE < files.length) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, DELAY_MS));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.settings.lastIndexTime = Date.now();
|
if (!signal.aborted) {
|
||||||
await this.saveSettings();
|
this.settings.lastIndexTime = Date.now();
|
||||||
Logger.info(`Vault indexing complete: ${indexed}/${files.length} files indexed.`, "main");
|
await this.saveSettings();
|
||||||
new import_obsidian4.Notice(`Vault index updated: ${indexed} files indexed.`);
|
Logger.info(`Vault indexing complete: ${indexed}/${files.length} files indexed.`, "main");
|
||||||
|
new import_obsidian4.Notice(`Vault index updated: ${indexed} files indexed.`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
async rebuildVaultIndex() {
|
async rebuildVaultIndex() {
|
||||||
|
this.cancelBackgroundIndexing();
|
||||||
|
await this.awaitBackgroundIndexing();
|
||||||
if (this.vaultVectorStore) {
|
if (this.vaultVectorStore) {
|
||||||
await this.vaultVectorStore.clearIndex();
|
await this.vaultVectorStore.clearIndex();
|
||||||
await this.vaultVectorStore.initialize();
|
await this.vaultVectorStore.initialize();
|
||||||
}
|
}
|
||||||
await this.backgroundIndexVault();
|
this.currentIndexingPromise = this.backgroundIndexVault();
|
||||||
|
void this.currentIndexingPromise.catch((err) => {
|
||||||
|
Logger.warn(`Rebuild vault indexing failed: ${String(err)}`, "main");
|
||||||
|
}).finally(() => {
|
||||||
|
this.currentIndexingPromise = void 0;
|
||||||
|
this.indexingAbortController = void 0;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
async clearVaultIndex() {
|
async clearVaultIndex() {
|
||||||
if (this.vaultVectorStore) {
|
if (this.vaultVectorStore) {
|
||||||
@@ -10020,7 +10068,9 @@ var OllamaPlugin = class extends import_obsidian4.Plugin {
|
|||||||
this.app.vault.on("create", (file) => {
|
this.app.vault.on("create", (file) => {
|
||||||
if (file instanceof import_obsidian4.TFile && file.extension === "md" && this.vaultVectorStore) {
|
if (file instanceof import_obsidian4.TFile && file.extension === "md" && this.vaultVectorStore) {
|
||||||
void this.app.vault.read(file).then((content) => {
|
void this.app.vault.read(file).then((content) => {
|
||||||
void this.vaultVectorStore?.indexFile(file, content);
|
if (!this.currentIndexingPromise) {
|
||||||
|
void this.vaultVectorStore?.indexFile(file, content);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -10029,7 +10079,9 @@ var OllamaPlugin = class extends import_obsidian4.Plugin {
|
|||||||
this.app.vault.on("modify", (file) => {
|
this.app.vault.on("modify", (file) => {
|
||||||
if (file instanceof import_obsidian4.TFile && file.extension === "md" && this.vaultVectorStore) {
|
if (file instanceof import_obsidian4.TFile && file.extension === "md" && this.vaultVectorStore) {
|
||||||
void this.app.vault.read(file).then((content) => {
|
void this.app.vault.read(file).then((content) => {
|
||||||
void this.vaultVectorStore?.indexFile(file, content);
|
if (!this.currentIndexingPromise) {
|
||||||
|
void this.vaultVectorStore?.indexFile(file, content);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -10046,7 +10098,9 @@ var OllamaPlugin = class extends import_obsidian4.Plugin {
|
|||||||
if (file instanceof import_obsidian4.TFile && file.extension === "md" && this.vaultVectorStore) {
|
if (file instanceof import_obsidian4.TFile && file.extension === "md" && this.vaultVectorStore) {
|
||||||
void this.vaultVectorStore.deleteFile(oldPath);
|
void this.vaultVectorStore.deleteFile(oldPath);
|
||||||
void this.app.vault.read(file).then((content) => {
|
void this.app.vault.read(file).then((content) => {
|
||||||
void this.vaultVectorStore?.indexFile(file, content);
|
if (!this.currentIndexingPromise) {
|
||||||
|
void this.vaultVectorStore?.indexFile(file, content);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
+4
-1
@@ -8,7 +8,7 @@ http {
|
|||||||
server_name localhost;
|
server_name localhost;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
# CORS headers for every response (including 4xx/5xx from upstream)
|
# CORS headers must be set for every response, including OPTIONS preflight
|
||||||
add_header 'Access-Control-Allow-Origin' '*' always;
|
add_header 'Access-Control-Allow-Origin' '*' always;
|
||||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
|
||||||
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
|
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
|
||||||
@@ -16,6 +16,9 @@ http {
|
|||||||
|
|
||||||
# Preflight OPTIONS
|
# Preflight OPTIONS
|
||||||
if ($request_method = 'OPTIONS') {
|
if ($request_method = 'OPTIONS') {
|
||||||
|
add_header 'Access-Control-Allow-Origin' '*' always;
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
|
||||||
|
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
|
||||||
add_header 'Access-Control-Max-Age' 1728000;
|
add_header 'Access-Control-Max-Age' 1728000;
|
||||||
add_header 'Content-Type' 'text/plain; charset=utf-8';
|
add_header 'Content-Type' 'text/plain; charset=utf-8';
|
||||||
add_header 'Content-Length' 0;
|
add_header 'Content-Length' 0;
|
||||||
|
|||||||
+95
-19
@@ -10,6 +10,8 @@ export default class OllamaPlugin extends Plugin {
|
|||||||
settings: PluginSettings = DEFAULT_SETTINGS;
|
settings: PluginSettings = DEFAULT_SETTINGS;
|
||||||
semanticCache?: SemanticCacheService;
|
semanticCache?: SemanticCacheService;
|
||||||
vaultVectorStore?: VaultVectorStore;
|
vaultVectorStore?: VaultVectorStore;
|
||||||
|
private indexingAbortController?: AbortController;
|
||||||
|
private currentIndexingPromise?: Promise<void>;
|
||||||
|
|
||||||
async onload() {
|
async onload() {
|
||||||
await this.loadSettings();
|
await this.loadSettings();
|
||||||
@@ -92,6 +94,9 @@ export default class OllamaPlugin extends Plugin {
|
|||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
onunload() {
|
onunload() {
|
||||||
|
// Cancel any ongoing indexing
|
||||||
|
this.cancelBackgroundIndexing();
|
||||||
|
|
||||||
// Clean up any active semantic cache resources on plugin unload
|
// Clean up any active semantic cache resources on plugin unload
|
||||||
// Using fire-and-forget pattern since onunload cannot be async per Obsidian API
|
// Using fire-and-forget pattern since onunload cannot be async per Obsidian API
|
||||||
if (this.semanticCache) {
|
if (this.semanticCache) {
|
||||||
@@ -110,6 +115,10 @@ export default class OllamaPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async initializeVaultVectorStore(): Promise<void> {
|
async initializeVaultVectorStore(): Promise<void> {
|
||||||
|
// Abort any ongoing indexing before re-initializing
|
||||||
|
this.cancelBackgroundIndexing();
|
||||||
|
await this.awaitBackgroundIndexing();
|
||||||
|
|
||||||
this.vaultVectorStore = new VaultVectorStore(
|
this.vaultVectorStore = new VaultVectorStore(
|
||||||
this.settings.ollamaUrl,
|
this.settings.ollamaUrl,
|
||||||
this.settings.vaultIndexConfig
|
this.settings.vaultIndexConfig
|
||||||
@@ -117,44 +126,104 @@ export default class OllamaPlugin extends Plugin {
|
|||||||
try {
|
try {
|
||||||
await this.vaultVectorStore.initialize();
|
await this.vaultVectorStore.initialize();
|
||||||
// Run background indexing
|
// Run background indexing
|
||||||
void this.backgroundIndexVault().catch((err) => {
|
this.currentIndexingPromise = this.backgroundIndexVault();
|
||||||
Logger.warn(`Background vault indexing failed: ${String(err)}`, 'main');
|
void this.currentIndexingPromise
|
||||||
});
|
.catch((err) => {
|
||||||
|
Logger.warn(`Background vault indexing failed: ${String(err)}`, 'main');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.currentIndexingPromise = undefined;
|
||||||
|
this.indexingAbortController = undefined;
|
||||||
|
});
|
||||||
} catch {
|
} catch {
|
||||||
new Notice('Vault vector store initialization failed. Check console for details.');
|
new Notice('Vault vector store initialization failed. Check console for details.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private cancelBackgroundIndexing(): void {
|
||||||
|
if (this.indexingAbortController) {
|
||||||
|
this.indexingAbortController.abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async awaitBackgroundIndexing(): Promise<void> {
|
||||||
|
if (this.currentIndexingPromise) {
|
||||||
|
try {
|
||||||
|
await this.currentIndexingPromise;
|
||||||
|
} catch {
|
||||||
|
// ignore errors from cancelled indexing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async backgroundIndexVault(): Promise<void> {
|
async backgroundIndexVault(): Promise<void> {
|
||||||
if (!this.vaultVectorStore) return;
|
if (!this.vaultVectorStore) return;
|
||||||
|
|
||||||
|
this.indexingAbortController = new AbortController();
|
||||||
|
const signal = this.indexingAbortController.signal;
|
||||||
|
|
||||||
const files = this.app.vault.getMarkdownFiles();
|
const files = this.app.vault.getMarkdownFiles();
|
||||||
Logger.info(`Starting background vault indexing for ${files.length} files...`, 'main');
|
Logger.info(`Starting background vault indexing for ${files.length} files...`, 'main');
|
||||||
|
|
||||||
let indexed = 0;
|
let indexed = 0;
|
||||||
for (const file of files) {
|
const BATCH_SIZE = 5;
|
||||||
try {
|
const DELAY_MS = 100;
|
||||||
const content = await this.app.vault.read(file);
|
|
||||||
await this.vaultVectorStore.indexFile(file, content);
|
for (let i = 0; i < files.length; i += BATCH_SIZE) {
|
||||||
indexed++;
|
if (signal.aborted) {
|
||||||
} catch (error) {
|
Logger.info('Vault indexing cancelled.', 'main');
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
return;
|
||||||
Logger.warn(`Failed to index ${file.path}: ${errorMessage}`, 'main');
|
}
|
||||||
|
|
||||||
|
const batch = files.slice(i, i + BATCH_SIZE);
|
||||||
|
await Promise.all(
|
||||||
|
batch.map(async (file) => {
|
||||||
|
if (signal.aborted) return;
|
||||||
|
try {
|
||||||
|
const content = await this.app.vault.read(file);
|
||||||
|
if (signal.aborted) return;
|
||||||
|
await this.vaultVectorStore!.indexFile(file, content);
|
||||||
|
indexed++;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
Logger.warn(`Failed to index ${file.path}: ${errorMessage}`, 'main');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Small delay between batches to avoid overloading Ollama
|
||||||
|
if (i + BATCH_SIZE < files.length) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, DELAY_MS));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.settings.lastIndexTime = Date.now();
|
if (!signal.aborted) {
|
||||||
await this.saveSettings();
|
this.settings.lastIndexTime = Date.now();
|
||||||
Logger.info(`Vault indexing complete: ${indexed}/${files.length} files indexed.`, 'main');
|
await this.saveSettings();
|
||||||
new Notice(`Vault index updated: ${indexed} files indexed.`);
|
Logger.info(`Vault indexing complete: ${indexed}/${files.length} files indexed.`, 'main');
|
||||||
|
new Notice(`Vault index updated: ${indexed} files indexed.`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async rebuildVaultIndex(): Promise<void> {
|
async rebuildVaultIndex(): Promise<void> {
|
||||||
|
// Cancel and await any ongoing indexing before clearing
|
||||||
|
this.cancelBackgroundIndexing();
|
||||||
|
await this.awaitBackgroundIndexing();
|
||||||
|
|
||||||
if (this.vaultVectorStore) {
|
if (this.vaultVectorStore) {
|
||||||
await this.vaultVectorStore.clearIndex();
|
await this.vaultVectorStore.clearIndex();
|
||||||
await this.vaultVectorStore.initialize();
|
await this.vaultVectorStore.initialize();
|
||||||
}
|
}
|
||||||
await this.backgroundIndexVault();
|
|
||||||
|
this.currentIndexingPromise = this.backgroundIndexVault();
|
||||||
|
void this.currentIndexingPromise
|
||||||
|
.catch((err) => {
|
||||||
|
Logger.warn(`Rebuild vault indexing failed: ${String(err)}`, 'main');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.currentIndexingPromise = undefined;
|
||||||
|
this.indexingAbortController = undefined;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearVaultIndex(): Promise<void> {
|
async clearVaultIndex(): Promise<void> {
|
||||||
@@ -169,7 +238,10 @@ export default class OllamaPlugin extends Plugin {
|
|||||||
this.app.vault.on('create', (file) => {
|
this.app.vault.on('create', (file) => {
|
||||||
if (file instanceof TFile && file.extension === 'md' && this.vaultVectorStore) {
|
if (file instanceof TFile && file.extension === 'md' && this.vaultVectorStore) {
|
||||||
void this.app.vault.read(file).then((content) => {
|
void this.app.vault.read(file).then((content) => {
|
||||||
void this.vaultVectorStore?.indexFile(file, content);
|
// Skip if a full rebuild is in progress to avoid race conditions
|
||||||
|
if (!this.currentIndexingPromise) {
|
||||||
|
void this.vaultVectorStore?.indexFile(file, content);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -180,7 +252,9 @@ export default class OllamaPlugin extends Plugin {
|
|||||||
this.app.vault.on('modify', (file) => {
|
this.app.vault.on('modify', (file) => {
|
||||||
if (file instanceof TFile && file.extension === 'md' && this.vaultVectorStore) {
|
if (file instanceof TFile && file.extension === 'md' && this.vaultVectorStore) {
|
||||||
void this.app.vault.read(file).then((content) => {
|
void this.app.vault.read(file).then((content) => {
|
||||||
void this.vaultVectorStore?.indexFile(file, content);
|
if (!this.currentIndexingPromise) {
|
||||||
|
void this.vaultVectorStore?.indexFile(file, content);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -201,7 +275,9 @@ export default class OllamaPlugin extends Plugin {
|
|||||||
if (file instanceof TFile && file.extension === 'md' && this.vaultVectorStore) {
|
if (file instanceof TFile && file.extension === 'md' && this.vaultVectorStore) {
|
||||||
void this.vaultVectorStore.deleteFile(oldPath);
|
void this.vaultVectorStore.deleteFile(oldPath);
|
||||||
void this.app.vault.read(file).then((content) => {
|
void this.app.vault.read(file).then((content) => {
|
||||||
void this.vaultVectorStore?.indexFile(file, content);
|
if (!this.currentIndexingPromise) {
|
||||||
|
void this.vaultVectorStore?.indexFile(file, content);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user