mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
fix(search): stop leaking meilisearch task ids on the api (#2206)
This commit is contained in:
1 parent
7c1c8b2749
commit
4357d5ec5d
2 files changed
+82
-25
No files matched your search
@@ -4,6 +4,7 @@ import type {SearchableMessage} from '@fluxer/schema/src/contracts/search/Search
|
|||||||
import {describe, expect, it} from 'vitest';
|
import {describe, expect, it} from 'vitest';
|
||||||
import type {MeilisearchClient, MeilisearchTask} from './MeilisearchClient';
|
import type {MeilisearchClient, MeilisearchTask} from './MeilisearchClient';
|
||||||
import {MeilisearchMessageAdapter} from './MeilisearchDomainAdapters';
|
import {MeilisearchMessageAdapter} from './MeilisearchDomainAdapters';
|
||||||
|
import {MEILISEARCH_MAX_TRACKED_BULK_TASKS} from './MeilisearchIndexAdapter';
|
||||||
|
|
||||||
interface RecordedMeilisearchRequest {
|
interface RecordedMeilisearchRequest {
|
||||||
method: string;
|
method: string;
|
||||||
@@ -35,6 +36,9 @@ class FakeMeilisearchClient implements MeilisearchClient {
|
|||||||
if (method === 'POST' && path.endsWith('/documents')) {
|
if (method === 'POST' && path.endsWith('/documents')) {
|
||||||
return this.nextTask() as TResponse;
|
return this.nextTask() as TResponse;
|
||||||
}
|
}
|
||||||
|
if (method === 'POST' && path.endsWith('/documents/delete-batch')) {
|
||||||
|
return this.nextTask() as TResponse;
|
||||||
|
}
|
||||||
if (method === 'POST' && path.endsWith('/search')) {
|
if (method === 'POST' && path.endsWith('/search')) {
|
||||||
return {
|
return {
|
||||||
hits: [{id: 'message-1'}],
|
hits: [{id: 'message-1'}],
|
||||||
@@ -120,17 +124,66 @@ describe('MeilisearchMessageAdapter', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('waits for queued write tasks when refreshed', async () => {
|
it('waits for queued bulk index tasks when refreshed', async () => {
|
||||||
const client = new FakeMeilisearchClient();
|
const client = new FakeMeilisearchClient();
|
||||||
client.indexExists = true;
|
client.indexExists = true;
|
||||||
const adapter = new MeilisearchMessageAdapter({client});
|
const adapter = new MeilisearchMessageAdapter({client});
|
||||||
await adapter.initialize();
|
await adapter.initialize();
|
||||||
client.clear();
|
client.clear();
|
||||||
|
|
||||||
await adapter.indexDocument({id: 'message-1'} as SearchableMessage);
|
await adapter.bulkIndexDocuments([{id: 'message-1'} as SearchableMessage]);
|
||||||
|
|
||||||
expect(client.waitedTaskUids).toEqual([]);
|
expect(client.waitedTaskUids).toEqual([]);
|
||||||
await adapter.refreshIndex();
|
await adapter.refreshIndex();
|
||||||
expect(client.waitedTaskUids).toEqual([5]);
|
expect(client.waitedTaskUids).toEqual([5]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not retain task state for per-document writes', async () => {
|
||||||
|
const client = new FakeMeilisearchClient();
|
||||||
|
client.indexExists = true;
|
||||||
|
const adapter = new MeilisearchMessageAdapter({client});
|
||||||
|
await adapter.initialize();
|
||||||
|
client.clear();
|
||||||
|
|
||||||
|
for (let index = 0; index < 500; index++) {
|
||||||
|
await adapter.indexDocument({id: `message-${index}`} as SearchableMessage);
|
||||||
|
await adapter.updateDocument({id: `message-${index}`} as SearchableMessage);
|
||||||
|
await adapter.deleteDocument(`message-${index}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(client.requests).toHaveLength(1500);
|
||||||
|
await adapter.refreshIndex();
|
||||||
|
expect(client.waitedTaskUids).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('bounds the tracked bulk task set', async () => {
|
||||||
|
const client = new FakeMeilisearchClient();
|
||||||
|
client.indexExists = true;
|
||||||
|
const adapter = new MeilisearchMessageAdapter({client});
|
||||||
|
await adapter.initialize();
|
||||||
|
client.clear();
|
||||||
|
|
||||||
|
const batches = MEILISEARCH_MAX_TRACKED_BULK_TASKS + 100;
|
||||||
|
for (let index = 0; index < batches; index++) {
|
||||||
|
await adapter.bulkIndexDocuments([{id: `message-${index}`} as SearchableMessage]);
|
||||||
|
}
|
||||||
|
|
||||||
|
await adapter.refreshIndex();
|
||||||
|
expect(client.waitedTaskUids).toHaveLength(MEILISEARCH_MAX_TRACKED_BULK_TASKS);
|
||||||
|
expect(client.waitedTaskUids[0]).toBe(105);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('drops tracked bulk tasks on shutdown', async () => {
|
||||||
|
const client = new FakeMeilisearchClient();
|
||||||
|
client.indexExists = true;
|
||||||
|
const adapter = new MeilisearchMessageAdapter({client});
|
||||||
|
await adapter.initialize();
|
||||||
|
client.clear();
|
||||||
|
|
||||||
|
await adapter.bulkIndexDocuments([{id: 'message-1'} as SearchableMessage]);
|
||||||
|
await adapter.shutdown();
|
||||||
|
await adapter.refreshIndex();
|
||||||
|
|
||||||
|
expect(client.waitedTaskUids).toEqual([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -9,6 +9,8 @@ import type {MeilisearchIndexDefinition} from './MeilisearchIndexDefinitions';
|
|||||||
const MAX_SEARCH_LIMIT = 1000;
|
const MAX_SEARCH_LIMIT = 1000;
|
||||||
const MAX_TOTAL_HITS = 10000;
|
const MAX_TOTAL_HITS = 10000;
|
||||||
|
|
||||||
|
export const MEILISEARCH_MAX_TRACKED_BULK_TASKS = 4096;
|
||||||
|
|
||||||
interface MeilisearchIndexAdapterOptions<TFilters> {
|
interface MeilisearchIndexAdapterOptions<TFilters> {
|
||||||
client: MeilisearchClient;
|
client: MeilisearchClient;
|
||||||
index: MeilisearchIndexDefinition;
|
index: MeilisearchIndexDefinition;
|
||||||
@@ -37,7 +39,7 @@ export class MeilisearchIndexAdapter<
|
|||||||
protected readonly buildFilters: (filters: TFilters) => Array<MeilisearchFilter | undefined>;
|
protected readonly buildFilters: (filters: TFilters) => Array<MeilisearchFilter | undefined>;
|
||||||
protected readonly buildSort: ((filters: TFilters) => Array<string> | undefined) | undefined;
|
protected readonly buildSort: ((filters: TFilters) => Array<string> | undefined) | undefined;
|
||||||
protected readonly buildQuery: ((query: string, filters: TFilters) => string) | undefined;
|
protected readonly buildQuery: ((query: string, filters: TFilters) => string) | undefined;
|
||||||
private readonly pendingTaskIds = new Set<number>();
|
private readonly pendingBulkTaskIds = new Set<number>();
|
||||||
private initialized = false;
|
private initialized = false;
|
||||||
|
|
||||||
constructor(options: MeilisearchIndexAdapterOptions<TFilters>) {
|
constructor(options: MeilisearchIndexAdapterOptions<TFilters>) {
|
||||||
@@ -56,7 +58,7 @@ export class MeilisearchIndexAdapter<
|
|||||||
uid,
|
uid,
|
||||||
primaryKey: this.indexDefinition.primaryKey,
|
primaryKey: this.indexDefinition.primaryKey,
|
||||||
});
|
});
|
||||||
await this.waitForTask(task.taskUid);
|
await this.client.waitForTask(task.taskUid);
|
||||||
}
|
}
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.applySetting('PUT', 'searchable-attributes', this.indexDefinition.searchableAttributes),
|
this.applySetting('PUT', 'searchable-attributes', this.indexDefinition.searchableAttributes),
|
||||||
@@ -68,7 +70,7 @@ export class MeilisearchIndexAdapter<
|
|||||||
}
|
}
|
||||||
|
|
||||||
async shutdown(): Promise<void> {
|
async shutdown(): Promise<void> {
|
||||||
this.pendingTaskIds.clear();
|
this.pendingBulkTaskIds.clear();
|
||||||
this.initialized = false;
|
this.initialized = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,40 +99,41 @@ export class MeilisearchIndexAdapter<
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.assertInitialised();
|
this.assertInitialised();
|
||||||
const task = await this.client.request<MeilisearchTask>(
|
await this.client.request<MeilisearchTask>(
|
||||||
'POST',
|
'POST',
|
||||||
`/indexes/${encodeURIComponent(this.indexDefinition.uid)}/documents/delete-batch`,
|
`/indexes/${encodeURIComponent(this.indexDefinition.uid)}/documents/delete-batch`,
|
||||||
ids,
|
ids,
|
||||||
);
|
);
|
||||||
this.trackTask(task.taskUid);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteByFilter(filter: MeilisearchFilter): Promise<void> {
|
async deleteByFilter(filter: MeilisearchFilter): Promise<void> {
|
||||||
this.assertInitialised();
|
this.assertInitialised();
|
||||||
const task = await this.client.request<MeilisearchTask>(
|
await this.client.request<MeilisearchTask>(
|
||||||
'POST',
|
'POST',
|
||||||
`/indexes/${encodeURIComponent(this.indexDefinition.uid)}/documents/delete`,
|
`/indexes/${encodeURIComponent(this.indexDefinition.uid)}/documents/delete`,
|
||||||
{filter},
|
{filter},
|
||||||
);
|
);
|
||||||
this.trackTask(task.taskUid);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteAllDocuments(): Promise<void> {
|
async deleteAllDocuments(): Promise<void> {
|
||||||
this.assertInitialised();
|
this.assertInitialised();
|
||||||
const task = await this.client.request<MeilisearchTask>(
|
await this.client.request<MeilisearchTask>(
|
||||||
'DELETE',
|
'DELETE',
|
||||||
`/indexes/${encodeURIComponent(this.indexDefinition.uid)}/documents`,
|
`/indexes/${encodeURIComponent(this.indexDefinition.uid)}/documents`,
|
||||||
);
|
);
|
||||||
this.trackTask(task.taskUid);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async bulkIndexDocuments(docs: Array<TResult>): Promise<void> {
|
async bulkIndexDocuments(docs: Array<TResult>): Promise<void> {
|
||||||
await this.addDocuments(docs);
|
const taskUid = await this.addDocuments(docs);
|
||||||
|
if (taskUid === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.trackBulkTask(taskUid);
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshIndex(): Promise<void> {
|
async refreshIndex(): Promise<void> {
|
||||||
const taskIds = Array.from(this.pendingTaskIds);
|
const taskIds = Array.from(this.pendingBulkTaskIds);
|
||||||
this.pendingTaskIds.clear();
|
this.pendingBulkTaskIds.clear();
|
||||||
await Promise.all(taskIds.map((taskId) => this.client.waitForTask(taskId)));
|
await Promise.all(taskIds.map((taskId) => this.client.waitForTask(taskId)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,12 +196,12 @@ export class MeilisearchIndexAdapter<
|
|||||||
`/indexes/${encodeURIComponent(this.indexDefinition.uid)}/settings/${setting}`,
|
`/indexes/${encodeURIComponent(this.indexDefinition.uid)}/settings/${setting}`,
|
||||||
value,
|
value,
|
||||||
);
|
);
|
||||||
await this.waitForTask(task.taskUid);
|
await this.client.waitForTask(task.taskUid);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async addDocuments(docs: Array<TResult>): Promise<void> {
|
private async addDocuments(docs: Array<TResult>): Promise<number | undefined> {
|
||||||
if (docs.length === 0) {
|
if (docs.length === 0) {
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
this.assertInitialised();
|
this.assertInitialised();
|
||||||
const task = await this.client.request<MeilisearchTask>(
|
const task = await this.client.request<MeilisearchTask>(
|
||||||
@@ -206,15 +209,16 @@ export class MeilisearchIndexAdapter<
|
|||||||
`/indexes/${encodeURIComponent(this.indexDefinition.uid)}/documents`,
|
`/indexes/${encodeURIComponent(this.indexDefinition.uid)}/documents`,
|
||||||
docs,
|
docs,
|
||||||
);
|
);
|
||||||
this.trackTask(task.taskUid);
|
return task.taskUid;
|
||||||
}
|
}
|
||||||
|
|
||||||
private trackTask(taskUid: number): void {
|
private trackBulkTask(taskUid: number): void {
|
||||||
this.pendingTaskIds.add(taskUid);
|
if (this.pendingBulkTaskIds.size >= MEILISEARCH_MAX_TRACKED_BULK_TASKS) {
|
||||||
}
|
const oldest = this.pendingBulkTaskIds.values().next().value;
|
||||||
|
if (oldest !== undefined) {
|
||||||
private async waitForTask(taskUid: number): Promise<void> {
|
this.pendingBulkTaskIds.delete(oldest);
|
||||||
this.pendingTaskIds.delete(taskUid);
|
}
|
||||||
await this.client.waitForTask(taskUid);
|
}
|
||||||
|
this.pendingBulkTaskIds.add(taskUid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user