fix: 修复QQBot插件worker超时恢复
This commit is contained in:
parent
e53db705aa
commit
eb255a0ac6
@ -236,21 +236,19 @@ export class QqbotPluginWorkerRuntime {
|
||||
error instanceof QqbotPluginWorkerExpiredRequestError ||
|
||||
isNamedError(error, 'QqbotPluginWorkerExpiredRequestError')
|
||||
) {
|
||||
const safeSummary = {
|
||||
correlationId: message.correlationId,
|
||||
operationId: message.operationId,
|
||||
timeoutMs,
|
||||
type,
|
||||
};
|
||||
const runtimeError = new QqbotPluginRuntimeError(
|
||||
'PLUGIN_WORKER_TIMEOUT',
|
||||
this.options.pluginKey,
|
||||
'QQBot plugin worker queue request expired.',
|
||||
{
|
||||
correlationId: message.correlationId,
|
||||
operationId: message.operationId,
|
||||
timeoutMs,
|
||||
type,
|
||||
},
|
||||
);
|
||||
this.recordRuntimeEvent(
|
||||
'worker-request-expired',
|
||||
runtimeError.safeSummary,
|
||||
safeSummary,
|
||||
);
|
||||
await this.markWorkerFailed('worker-request-expired', safeSummary);
|
||||
throw runtimeError;
|
||||
}
|
||||
if (error instanceof QqbotPluginWorkerResponseError) {
|
||||
|
||||
@ -90,7 +90,7 @@
|
||||
"plugin.config.read",
|
||||
"plugin.storage.read"
|
||||
],
|
||||
"timeoutMs": 30000
|
||||
"timeoutMs": 120000
|
||||
},
|
||||
{
|
||||
"key": "bangdream.card.search",
|
||||
|
||||
@ -2,6 +2,7 @@ import type { ConfigService } from '@nestjs/config';
|
||||
import {
|
||||
createQqbotBullmqWorkerQueueOptions,
|
||||
QqbotPluginRuntimeError,
|
||||
QqbotPluginWorkerExpiredRequestError,
|
||||
QqbotPluginWorkerResponseError,
|
||||
QqbotPluginWorkerRuntime,
|
||||
resolveQqbotPluginQueueConnection,
|
||||
@ -111,6 +112,28 @@ class TimeoutAwareRecordingRequestQueue extends RecordingRequestQueue {
|
||||
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 runtime = new QqbotPluginWorkerRuntime(new RecordingRequestQueue(driver), {
|
||||
defaultTimeoutMs: 50,
|
||||
@ -349,6 +372,66 @@ describe('QQBot plugin worker runtime', () => {
|
||||
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 () => {
|
||||
const { driver, runtime } = createRuntime();
|
||||
driver.responses.set('executeOperation', { replyText: 'ok' });
|
||||
|
||||
@ -52,4 +52,15 @@ describe('BangDream operation manifest', () => {
|
||||
});
|
||||
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