From 30a59f99e83cd16f5a7c37bc4296f7501f59fd36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Louren=C3=A7o?= Date: Mon, 16 Sep 2024 22:20:25 -0300 Subject: [PATCH] feat(aws-stream-handler): add flag to customize callbackWaitsForEmptyEventLoop (#264) --- src/handlers/aws/aws-stream.handler.ts | 33 ++++++++++ test/handlers/aws-stream.handler.spec.ts | 76 ++++++++++++++++++++++++ www/docs/main/handlers/aws.mdx | 25 ++++++++ 3 files changed, 134 insertions(+) diff --git a/src/handlers/aws/aws-stream.handler.ts b/src/handlers/aws/aws-stream.handler.ts index e657687e..d974d480 100644 --- a/src/handlers/aws/aws-stream.handler.ts +++ b/src/handlers/aws/aws-stream.handler.ts @@ -58,6 +58,22 @@ declare const awslambda: { }; }; +/** + * The interface that customizes the {@link AwsStreamHandler} + * + * @breadcrumb Handlers / AwsStreamHandler + * @public + */ +export type AwsStreamHandlerOptions = { + /** + * Set the value of the property `callbackWaitsForEmptyEventLoop`, you can set to `false` to fix issues with long execution due to not cleaning the event loop ([ref](https://github.com/H4ad/serverless-adapter/issues/264)). + * In the next release, this value will be changed to `false`. + * + * @defaultValue undefined + */ + callbackWaitsForEmptyEventLoop?: boolean; +}; + /** * The interface that describes the internal context used by the {@link AwsStreamHandler} * @@ -89,6 +105,17 @@ export class AwsStreamHandler extends BaseHandler< AWSStreamResponseMetadata, void > { + //#region Constructor + + /** + * Construtor padrĂ£o + */ + constructor(private readonly options?: AwsStreamHandlerOptions) { + super(); + } + + //#endregion + //#region Public Methods /** @@ -114,6 +141,12 @@ export class AwsStreamHandler extends BaseHandler< log: ILogger, ): ServerlessHandler> { return awslambda.streamifyResponse(async (event, response, context) => { + if (this.options?.callbackWaitsForEmptyEventLoop !== undefined) { + // TODO(h4ad): Set the following property to false by default + context.callbackWaitsForEmptyEventLoop = + this.options.callbackWaitsForEmptyEventLoop; + } + const streamContext = { response, context }; this.onReceiveRequest( diff --git a/test/handlers/aws-stream.handler.spec.ts b/test/handlers/aws-stream.handler.spec.ts index c289676a..69f6dcdb 100644 --- a/test/handlers/aws-stream.handler.spec.ts +++ b/test/handlers/aws-stream.handler.spec.ts @@ -435,4 +435,80 @@ describe('AwsStreamHandler', () => { }), ); }); + + it('callbackWaitsForEmptyEventLoop should not be modified', async () => { + const app = express(); + + app.get('/', (_, res) => { + res.json({ ok: true }); + }); + + const expressFramework = new ExpressFramework(); + + const handler = awsStreamHandler.getHandler( + app, + expressFramework, + adapters, + resolver, + binarySettings, + respondWithErrors, + logger, + ); + + const event = createApiGatewayV2('GET', '/', {}, { test: 'true' }); + const defaultValueForCallback = Symbol('1'); + const context = { + test: Symbol('unique'), + callbackWaitsForEmptyEventLoop: defaultValueForCallback, + }; + + const writable = new WritableMock(); + + await handler(event, writable, context); + + expect(context).toHaveProperty( + 'callbackWaitsForEmptyEventLoop', + defaultValueForCallback, + ); + }); + + describe('callbackWaitsForEmptyEventLoop should be changed', () => { + for (const value of [true, false]) { + it(`to ${value}`, async () => { + const app = express(); + + app.get('/', (_, res) => { + res.json({ ok: true }); + }); + + const expressFramework = new ExpressFramework(); + const customAwsHandler = new AwsStreamHandler({ + callbackWaitsForEmptyEventLoop: value, + }); + + const handler = customAwsHandler.getHandler( + app, + expressFramework, + adapters, + resolver, + binarySettings, + respondWithErrors, + logger, + ); + + const event = createApiGatewayV2('GET', '/', {}, { test: 'true' }); + const defaultValueForCallback = Symbol('test'); + const context = { + test: Symbol('unique'), + callbackWaitsForEmptyEventLoop: defaultValueForCallback, + }; + + const writable = new WritableMock(); + + await handler(event, writable, context); + + expect(context).toHaveProperty('callbackWaitsForEmptyEventLoop', value); + }); + } + }); }); diff --git a/www/docs/main/handlers/aws.mdx b/www/docs/main/handlers/aws.mdx index e34aefa5..f794488e 100644 --- a/www/docs/main/handlers/aws.mdx +++ b/www/docs/main/handlers/aws.mdx @@ -83,6 +83,31 @@ export const handler = ServerlessAdapter.new(app) As you see, we only have to change the `setHandler`, `setResolver` and `setAdapter` methods, that's all! +### My execution is taking too long + +Well, maybe is caused because your event loop is not empty when the request finishes, in this case, you can set `callbackWaitsForEmptyEventLoop=false` on [AwsStreamHandler](../../api/handlers/AwsStreamHandler). + + +```ts title="index.ts" +import { ServerlessAdapter } from '@h4ad/serverless-adapter'; +import { AwsStreamHandler } from '@h4ad/serverless-adapter/handlers/aws'; +import { DummyResolver } from '@h4ad/serverless-adapter/resolvers/dummy'; +import { ApiGatewayV2Adapter } from '@h4ad/serverless-adapter/adapters/aws'; +import app from './app'; + +export const handler = ServerlessAdapter.new(app) + .setHandler(new AwsStreamHandler({ callbackWaitsForEmptyEventLoop: false })) + .setResolver(new DummyResolver()) + .setAdapter(new ApiGatewayV2Adapter()) + // continue to set the other options here. + //.addAdapter(new AlbAdapter()) + //.addAdapter(new SQSAdapter()) + //.addAdapter(new SNSAdapter()) + //.setFramework(new ExpressFramework()) + // after put all methods necessary, just call the build method. + .build(); +``` + ### Can I use SQS and other services? Well, I don't know, but you can try. At least with `SQS` I was able to get the messages, but I couldn't use the `partial batch response` feature.