Skip to content

Commit 68980c2

Browse files
authored
feat: add timeout metadata for http controller (#301)
<!-- Thank you for your pull request. Please review below requirements. Bug fixes and new features should include tests and possibly benchmarks. Contributors guide: https://github.com/eggjs/egg/blob/master/CONTRIBUTING.md 感谢您贡献代码。请确认下列 checklist 的完成情况。 Bug 修复和新功能必须包含测试,必要时请附上性能测试。 Contributors guide: https://github.com/eggjs/egg/blob/master/CONTRIBUTING.md --> ##### Checklist <!-- Remove items that do not apply. For completed items, change [ ] to [x]. --> - [x] `npm test` passes - [x] tests and/or benchmarks are included - [x] documentation is changed or added - [x] commit message follows commit guidelines ##### Affected core subsystem(s) <!-- Provide affected core subsystem(s). --> ##### Description of change <!-- Provide a description of the change below this comment. --> <!-- - any feature? - close https://github.com/eggjs/egg/ISSUE_URL --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added support for specifying timeout values at both the controller and method levels in HTTP controllers and methods, enforceable during method execution. - Introduced timeout handling in HTTP request processing, returning errors when execution exceeds configured limits. - Added example controllers and endpoints demonstrating timeout behavior. - **Refactor** - Simplified qualifier implementation and decorator utilities by removing the force replacement parameter, allowing unconditional overwriting. - **Tests** - Added comprehensive tests verifying timeout behavior and inheritance in HTTP controllers and methods. - **Documentation** - Updated documentation to reflect removal of force replacement logic in qualifier decorators. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 7f1f4b3 commit 68980c2

26 files changed

Lines changed: 248 additions & 31 deletions

File tree

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -968,7 +968,6 @@ import { ContextHelloType } from '../FooType';
968968
import { AbstractContextHello } from '../AbstractHello';
969969

970970
@ContextProto()
971-
// 需要注意的是,对应枚举如果已经被实现,则会报错,若你已经知晓此情况,想覆盖此枚举类型,则增加参数 true 来强制覆盖,例:@Hello(HelloType.BAR, true)
972971
@Hello(HelloType.BAR)
973972
export class BarHello extends AbstractHello {
974973
hello(): string {

core/common-util/src/TimerUtil.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,32 @@
1+
class TimeoutError extends Error {
2+
constructor(message: string) {
3+
super(message);
4+
this.name = 'TimeoutError';
5+
}
6+
}
7+
18
export class TimerUtil {
9+
static TimeoutError = TimeoutError;
10+
211
static async sleep(ms: number) {
312
await new Promise(resolve => setTimeout(resolve, ms));
413
}
14+
15+
static async timeout<T>(fn: () => Promise<T>, ms?: number): Promise<T> {
16+
if (!ms) {
17+
return await fn();
18+
}
19+
20+
let timer: NodeJS.Timeout;
21+
const promise = new Promise<T>((resolve, reject) => {
22+
timer = setTimeout(() => reject(new TimeoutError('timeout')), ms);
23+
fn().then(resolve).catch(reject);
24+
});
25+
26+
return await promise.finally(() => {
27+
if (timer) {
28+
clearTimeout(timer);
29+
}
30+
});
31+
}
532
}

core/common-util/test/TimerUtil.test.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import assert from 'node:assert';
1+
import { strict as assert } from 'node:assert';
22
import { TimerUtil } from '..';
33

44
describe('test/TimerUtil.test.ts', () => {
@@ -8,4 +8,30 @@ describe('test/TimerUtil.test.ts', () => {
88
const use = Date.now() - start;
99
assert(use > 1, `use time ${use}ms`);
1010
});
11+
12+
describe('timeout', () => {
13+
const fixture = Symbol.for('timeout#res');
14+
const delay = (ms: number) => () => new Promise(resolve => setTimeout(() => resolve(fixture), ms));
15+
16+
it('should work without ms', async () => {
17+
const res = await TimerUtil.timeout(delay(50));
18+
assert.equal(res, fixture);
19+
});
20+
21+
it('should work with ms', async () => {
22+
const res = await TimerUtil.timeout(delay(50), 100);
23+
assert.equal(res, fixture);
24+
});
25+
26+
it('should timeout', async () => {
27+
await assert.rejects(TimerUtil.timeout(delay(100), 50), (e: any) => {
28+
return e instanceof TimerUtil.TimeoutError && e.message === 'timeout';
29+
});
30+
});
31+
32+
it('should throw error', async () => {
33+
const e = new Error('test');
34+
await assert.rejects(TimerUtil.timeout(async () => { throw e; }, 100), (err: any) => err === e);
35+
});
36+
});
1137
});

core/controller-decorator/src/decorator/http/HTTPController.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ export function HTTPController(param?: HTTPControllerParams) {
1111
if (param?.controllerName) {
1212
ControllerInfoUtil.setControllerName(constructor, param.controllerName);
1313
}
14+
if (param?.timeout) {
15+
ControllerInfoUtil.setControllerTimeout(param.timeout, constructor);
16+
}
1417
if (param?.path) {
1518
HTTPInfoUtil.setHTTPPath(param.path, constructor);
1619
}

core/controller-decorator/src/decorator/http/HTTPMethod.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ export function HTTPMethod(param: HTTPMethodParams) {
1313
MethodInfoUtil.setMethodControllerType(controllerClazz, methodName, ControllerType.HTTP);
1414
HTTPInfoUtil.setHTTPMethodPath(param.path, controllerClazz, methodName);
1515
HTTPInfoUtil.setHTTPMethodMethod(param.method, controllerClazz, methodName);
16+
if (param.timeout) {
17+
MethodInfoUtil.setMethodTimeout(param.timeout, controllerClazz, methodName);
18+
}
1619
if (param.priority !== undefined) {
1720
HTTPInfoUtil.setHTTPMethodPriority(param.priority, controllerClazz, methodName);
1821
}

core/controller-decorator/src/impl/http/HTTPControllerMetaBuilder.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,9 @@ export class HTTPControllerMetaBuilder {
4646
const needAcl = ControllerInfoUtil.hasControllerAcl(this.clazz);
4747
const aclCode = ControllerInfoUtil.getControllerAcl(this.clazz);
4848
const hosts = ControllerInfoUtil.getControllerHosts(this.clazz);
49+
const timeout = ControllerInfoUtil.getControllerTimeout(this.clazz);
4950
const metadata = new HTTPControllerMeta(
50-
clazzName, protoName, controllerName, httpPath, httpMiddlewares, methods, needAcl, aclCode, hosts);
51+
clazzName, protoName, controllerName, httpPath, httpMiddlewares, methods, needAcl, aclCode, hosts, timeout);
5152
ControllerMetadataUtil.setControllerMetadata(this.clazz, metadata);
5253
for (const method of metadata.methods) {
5354
const realPath = metadata.getMethodRealPath(method);

core/controller-decorator/src/impl/http/HTTPControllerMethodMetaBuilder.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,12 +106,13 @@ export class HTTPControllerMethodMetaBuilder {
106106
const needAcl = MethodInfoUtil.hasMethodAcl(this.clazz, this.methodName);
107107
const aclCode = MethodInfoUtil.getMethodAcl(this.clazz, this.methodName);
108108
const hosts = MethodInfoUtil.getMethodHosts(this.clazz, this.methodName);
109+
const timeout = MethodInfoUtil.getMethodTimeout(this.clazz, this.methodName);
109110
const realPath = parentPath
110111
? path.posix.join(parentPath, httpPath)
111112
: httpPath;
112113
const paramTypeMap = this.buildParamType(realPath);
113114
const priority = this.getPriority();
114115
return new HTTPMethodMeta(
115-
this.methodName, httpPath!, httpMethod!, middlewares, contextIndex, paramTypeMap, priority, needAcl, aclCode, hosts);
116+
this.methodName, httpPath!, httpMethod!, middlewares, contextIndex, paramTypeMap, priority, needAcl, aclCode, hosts, timeout);
116117
}
117118
}

core/controller-decorator/src/model/HTTPControllerMeta.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export class HTTPControllerMeta implements ControllerMetadata {
1414
public readonly needAcl: boolean;
1515
public readonly aclCode?: string;
1616
public readonly hosts?: string[];
17+
public readonly timeout?: number;
1718

1819
constructor(
1920
className: string,
@@ -25,6 +26,7 @@ export class HTTPControllerMeta implements ControllerMetadata {
2526
needAcl: boolean,
2627
aclCode: string | undefined,
2728
hosts: string[] | undefined,
29+
timeout: number | undefined,
2830
) {
2931
this.protoName = protoName;
3032
this.controllerName = controllerName;
@@ -35,6 +37,7 @@ export class HTTPControllerMeta implements ControllerMetadata {
3537
this.needAcl = needAcl;
3638
this.aclCode = aclCode;
3739
this.hosts = hosts;
40+
this.timeout = timeout;
3841
}
3942

4043
getMethodRealPath(method: HTTPMethodMeta) {
@@ -75,4 +78,8 @@ export class HTTPControllerMeta implements ControllerMetadata {
7578
}
7679
return this.aclCode;
7780
}
81+
82+
getMethodTimeout(method: HTTPMethodMeta): number | undefined {
83+
return method.timeout || this.timeout;
84+
}
7885
}

core/controller-decorator/src/model/HTTPMethodMeta.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export class HTTPMethodMeta implements MethodMeta {
9898
public readonly needAcl: boolean;
9999
public readonly aclCode: string | undefined;
100100
public readonly hosts: string[] | undefined;
101+
public readonly timeout: number | undefined;
101102

102103
constructor(
103104
name: string,
@@ -110,6 +111,7 @@ export class HTTPMethodMeta implements MethodMeta {
110111
needAcl: boolean,
111112
aclCode: string | undefined,
112113
hosts: string[] | undefined,
114+
timeout: number | undefined,
113115
) {
114116
this.name = name;
115117
this.path = path;
@@ -121,6 +123,7 @@ export class HTTPMethodMeta implements MethodMeta {
121123
this.needAcl = needAcl;
122124
this.aclCode = aclCode;
123125
this.hosts = hosts;
126+
this.timeout = timeout;
124127
}
125128
}
126129

core/controller-decorator/src/util/ControllerInfoUtil.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import {
2-
CONTROLLER_ACL, CONTROLLER_AOP_MIDDLEWARES,
2+
CONTROLLER_ACL,
3+
CONTROLLER_AOP_MIDDLEWARES,
34
CONTROLLER_HOST,
45
CONTROLLER_MIDDLEWARES,
56
CONTROLLER_NAME,
6-
CONTROLLER_TYPE, IAdvice,
7+
CONTROLLER_TIMEOUT_METADATA,
8+
CONTROLLER_TYPE,
9+
IAdvice,
710
} from '@eggjs/tegg-types';
811
import type { ControllerTypeLike, EggProtoImplClass, MiddlewareFunc } from '@eggjs/tegg-types';
912
import { MetadataUtil } from '@eggjs/core-decorator';
@@ -62,4 +65,12 @@ export default class ControllerInfoUtil {
6265
static getControllerHosts(clazz: EggProtoImplClass): string[] | undefined {
6366
return MetadataUtil.getMetaData(CONTROLLER_HOST, clazz);
6467
}
68+
69+
static setControllerTimeout(timeout: number, clazz: EggProtoImplClass) {
70+
MetadataUtil.defineMetaData(CONTROLLER_TIMEOUT_METADATA, timeout, clazz);
71+
}
72+
73+
static getControllerTimeout(clazz: EggProtoImplClass): number | undefined {
74+
return MetadataUtil.getMetaData(CONTROLLER_TIMEOUT_METADATA, clazz);
75+
}
6576
}

0 commit comments

Comments
 (0)