fix: 修复QQBot插件worker超时恢复
This commit is contained in:
parent
e53db705aa
commit
eb255a0ac6
@ -236,21 +236,19 @@ export class QqbotPluginWorkerRuntime {
|
|||||||
error instanceof QqbotPluginWorkerExpiredRequestError ||
|
error instanceof QqbotPluginWorkerExpiredRequestError ||
|
||||||
isNamedError(error, 'QqbotPluginWorkerExpiredRequestError')
|
isNamedError(error, 'QqbotPluginWorkerExpiredRequestError')
|
||||||
) {
|
) {
|
||||||
|
const safeSummary = {
|
||||||
|
correlationId: message.correlationId,
|
||||||
|
operationId: message.operationId,
|
||||||
|
timeoutMs,
|
||||||
|
type,
|
||||||
|
};
|
||||||
const runtimeError = new QqbotPluginRuntimeError(
|
const runtimeError = new QqbotPluginRuntimeError(
|
||||||
'PLUGIN_WORKER_TIMEOUT',
|
'PLUGIN_WORKER_TIMEOUT',
|
||||||
this.options.pluginKey,
|
this.options.pluginKey,
|
||||||
'QQBot plugin worker queue request expired.',
|
'QQBot plugin worker queue request expired.',
|
||||||
{
|
safeSummary,
|
||||||
correlationId: message.correlationId,
|
|
||||||
operationId: message.operationId,
|
|
||||||
timeoutMs,
|
|
||||||
type,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
this.recordRuntimeEvent(
|
|
||||||
'worker-request-expired',
|
|
||||||
runtimeError.safeSummary,
|
|
||||||
);
|
);
|
||||||
|
await this.markWorkerFailed('worker-request-expired', safeSummary);
|
||||||
throw runtimeError;
|
throw runtimeError;
|
||||||
}
|
}
|
||||||
if (error instanceof QqbotPluginWorkerResponseError) {
|
if (error instanceof QqbotPluginWorkerResponseError) {
|
||||||
|
|||||||
@ -90,7 +90,7 @@
|
|||||||
"plugin.config.read",
|
"plugin.config.read",
|
||||||
"plugin.storage.read"
|
"plugin.storage.read"
|
||||||
],
|
],
|
||||||
"timeoutMs": 30000
|
"timeoutMs": 120000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "bangdream.card.search",
|
"key": "bangdream.card.search",
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import type { ConfigService } from '@nestjs/config';
|
|||||||
import {
|
import {
|
||||||
createQqbotBullmqWorkerQueueOptions,
|
createQqbotBullmqWorkerQueueOptions,
|
||||||
QqbotPluginRuntimeError,
|
QqbotPluginRuntimeError,
|
||||||
|
QqbotPluginWorkerExpiredRequestError,
|
||||||
QqbotPluginWorkerResponseError,
|
QqbotPluginWorkerResponseError,
|
||||||
QqbotPluginWorkerRuntime,
|
QqbotPluginWorkerRuntime,
|
||||||
resolveQqbotPluginQueueConnection,
|
resolveQqbotPluginQueueConnection,
|
||||||
@ -111,6 +112,28 @@ class TimeoutAwareRecordingRequestQueue extends RecordingRequestQueue {
|
|||||||
readonly queueWaitTimeoutMs = 50;
|
readonly queueWaitTimeoutMs = 50;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ExpiringOnceRequestQueue implements QqbotPluginWorkerRequestQueue {
|
||||||
|
constructor(private readonly driver: QqbotPluginWorkerDriver) {}
|
||||||
|
|
||||||
|
async close() {
|
||||||
|
await this.driver.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
async request(message: QqbotPluginWorkerRequest): Promise<unknown> {
|
||||||
|
if (message.operationId === 'op-expire') {
|
||||||
|
await this.reset();
|
||||||
|
throw new QqbotPluginWorkerExpiredRequestError(
|
||||||
|
'worker-request-execution-timeout',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.driver.request(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
async reset() {
|
||||||
|
await this.driver.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const createRuntime = (driver = new RecordingDriver()) => {
|
const createRuntime = (driver = new RecordingDriver()) => {
|
||||||
const runtime = new QqbotPluginWorkerRuntime(new RecordingRequestQueue(driver), {
|
const runtime = new QqbotPluginWorkerRuntime(new RecordingRequestQueue(driver), {
|
||||||
defaultTimeoutMs: 50,
|
defaultTimeoutMs: 50,
|
||||||
@ -349,6 +372,66 @@ describe('QQBot plugin worker runtime', () => {
|
|||||||
expect(runtime.status).toBe('active');
|
expect(runtime.status).toBe('active');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('recovers after the queue expires and disposes a slow worker request', async () => {
|
||||||
|
const requestTypes: string[] = [];
|
||||||
|
const driver: QqbotPluginWorkerDriver = {
|
||||||
|
dispose: jest.fn(async () => undefined),
|
||||||
|
request: jest.fn(async (message) => {
|
||||||
|
requestTypes.push(message.operationId || message.type);
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
operationId: message.operationId,
|
||||||
|
type: message.type,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const runtime = new QqbotPluginWorkerRuntime(
|
||||||
|
new ExpiringOnceRequestQueue(driver),
|
||||||
|
{
|
||||||
|
defaultTimeoutMs: 50,
|
||||||
|
installationId: 'install-expired-recover',
|
||||||
|
pluginKey: 'demo-plugin',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await runtime.load({
|
||||||
|
entry: 'src/index.ts',
|
||||||
|
pluginKey: 'demo-plugin',
|
||||||
|
version: '0.1.0',
|
||||||
|
});
|
||||||
|
await runtime.activate();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
runtime.executeOperation({
|
||||||
|
input: { text: 'slow' },
|
||||||
|
operationId: 'op-expire',
|
||||||
|
operationKey: 'demo-plugin.echo',
|
||||||
|
}),
|
||||||
|
).rejects.toMatchObject({
|
||||||
|
code: 'PLUGIN_WORKER_TIMEOUT',
|
||||||
|
});
|
||||||
|
expect(runtime.status).toBe('failed');
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
runtime.executeOperation({
|
||||||
|
input: { text: 'after expired' },
|
||||||
|
operationId: 'op-after-expire',
|
||||||
|
operationKey: 'demo-plugin.echo',
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
operationId: 'op-after-expire',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(requestTypes).toEqual([
|
||||||
|
'load',
|
||||||
|
'activate',
|
||||||
|
'load',
|
||||||
|
'activate',
|
||||||
|
'op-after-expire',
|
||||||
|
]);
|
||||||
|
expect(runtime.status).toBe('active');
|
||||||
|
});
|
||||||
|
|
||||||
it('sends lifecycle and execution RPC messages with correlation IDs and safe input summaries', async () => {
|
it('sends lifecycle and execution RPC messages with correlation IDs and safe input summaries', async () => {
|
||||||
const { driver, runtime } = createRuntime();
|
const { driver, runtime } = createRuntime();
|
||||||
driver.responses.set('executeOperation', { replyText: 'ok' });
|
driver.responses.set('executeOperation', { replyText: 'ok' });
|
||||||
|
|||||||
@ -52,4 +52,15 @@ describe('BangDream operation manifest', () => {
|
|||||||
});
|
});
|
||||||
expect(byKey.get('bangdream.unknown')).toBeUndefined();
|
expect(byKey.get('bangdream.unknown')).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps heavyweight list renderers on an extended timeout budget', () => {
|
||||||
|
const manifest = readManifest();
|
||||||
|
const byKey = new Map(
|
||||||
|
manifest.operations.map((operation) => [operation.key, operation]),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(byKey.get('bangdream.song.meta')?.timeoutMs).toBeGreaterThanOrEqual(
|
||||||
|
120000,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user