-
Notifications
You must be signed in to change notification settings - Fork 146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Docs: explain how to cleanup with middy early return #1410
Comments
Hi @quantumew, thank you for taking the time to open this issue. Based on the info you have provided above I don't have enough info to reproduce the issue. Also, asking this to better understand the issue, how did you pinpoint the issue to either Middy or the Tracer? The log that you have provided contains only a method called
Are you referring to this feature of Middy? Does your second custom middleware returns early like this?
Let me break this down:
That's a sensible solution, any chance you could share the bit of code that you use to do this operation? I'd like to see how you're closing the segment.
Not necessarily. Once you close the parent all its children should also be flushed.
I'm not 100% sure, but I doubt that. The function cannot mutate this segment and it's handled by the X-Ray SDK and refreshed at each invocation based on the invocation it/Trace ID present in the environment. I'm not 100% sure I can pinpoint the problem with the current info, but what I can say is that based on the current implementation of the Basically even if you didn't close any of the segment in your early return, the next function invocation should reassign the values of the segments and overwrite its previous values. In this case you might see trace data loss or incorrect segment duration but not stack overflow. |
Thank you for such a thorough response. Here is how I attempt to close the segment:
The increment call is coming from the xray sdk. Based on that stack trace, it looks like the problem is deeply nested segments, eventually causing a stack overflow. I came to this conclusion purely through inference. The Tracer is created on lambda cold start. I assumed that when you called getSegment here, it returned the previous invocation's handler segment, since it was never closed, since we returned early. Resulting in handler segments nested in handler segments. It just occurred to me that I am not doing this step. Could that be the problem? I am closing it but it is still the current segment? |
That's a fair assumption and one that right off the bat, I'm also not able to rule out. Now that I have a clearer idea of what your code is doing I can do more tests and give you a more precise answer.
It could. The issue here is that you won't have access to that segment since it's not stored anywhere in the Tracer class. I need to spend some time thinking about those two points before weighing in, but I see that there's a chance we might not be taking in account this edge case of users returning early in one of the downstream middleware. I'll spend some time looking at this tomorrow and get back to you here. Thanks for the patience and for reporting this interesting case! |
If the above is correct, I'm thinking this might be a good use case for using Middy's internal storage and allow downstream middleware to bail out early while also cleaning up traces, metrics, and other Powertools related actions. |
I've been talking with @willfarrell (maintainer of Middy) and another option that came up would be to place any middleware that you know might return early as first in the middleware chain: const handler = middy()
.use(myCacheMiddleware)
.use(captureLambdaHandler(tracer)) This way if the first middleware returns, the If this is the problem, this should fix your issue. However I'll still continue investigating to see if we can do something for this edge case as I would prefer not having to rely on ordering. |
Definitely, great suggestion, it is one of our workarounds on one of our endpoints where disabling tracing was not an option. However, then we never get any trace data for our cache hits. My hope is to have the best of both worlds. Although, you could argue that a combination of ordering plus manual instrumentation would suffice here. Perhaps that is the tactical path I will pursue for now. |
There are undocumented features in Example use: https://middy.js.org/docs/best-practices/profiling |
I put some simple docs together for the supported hooks. |
@dreamorosi wow, that is exactly what I was missing! Much appreciated. Also, I agree with your assessment about the classification of this issue. @willfarrell TIL, thank you, that'll be super useful. |
Hi @quantumew, I've been noodling on this a bit more on this and I think that there's some room for improvement beyond just documenting. I have come up with a proposed solution and I'd like to hear your opinion as a potential user, if you don't mind. The problem I see with just documenting is that not everyone who uses the early return feature in Middy might be aware of the unintended consequences of not performing the post-request actions related to powertools and so it's unlikely they'd think of looking for this in the docs unless something is broken (like it happened to you). Additionally, as shown by the discussion above, the solution is not immediately obvious and in a way, unnecessarily exposes the internals of our To avoid all this, and to provide a simpler/clearer DX, I'm thinking of exposing a function aimed at developers writing middleware that might return early. As a developer, you'd simply have to import the function and call it. Powertools would handle all cleanup operations and stay up to date with the rest of the utilities: + import { getPowertoolsCloseFunction } from '@aws-lambda-powertools/commons';
const before = (request: middy.Request): string | void => {
cacheKey = (request.event as myEvent).id;
// Cache hit
if (storage.has(cacheKey)) {
- // Get current segment, which if present should always be the main handler segment (`### index.handler`), so we can typecast
- const myLastSegment = tracer.getSegment() as Subsegment | undefined;
- if (myLastSegment) {
- // You can add annotations or metadata here
- myLastSegment.addAnnotation("CacheHit", true);
- // Close it
- myLastSegment.close();
- // Then finally set back to the `facade` segment (the one created by the Lambda service) - which is accessible at the .parent property
- tracer.setSegment(myLastSegment.parent);
- }
- }
+ const powertoolsCloseFn = getPowertoolsCloseFunction(request);
+ powertoolsCloseFn?.(
+ tracer: {
+ annotations: [{ key: "CacheHit", value: true }],
+ },
+ )
// Return from cache
return storage.get(cacheKey);
}
}; Note that the naming and signature are not final, however the idea is that in your middleware, you import the function factory and then before returning in your code, you get the function it and call it. When calling the function factory, you need to pass it the When calling the function, you can also optionally pass extra arguments to customize the cleanup actions. For instance, in the case of Tracer, you might want to set What do you think? Would you find this useful over the previous solution? Would really appreciate your opinion on this. |
@dreamorosi I dig it. I agree, understanding how to properly close and reset context is a bit esoteric so obfuscating that behind a helper is a great idea. One question though, what if the middleware this exposes uses the aforementioned hooks instead? Specifically the |
Apologies for the delayed response. The method you described, with With the hook method: 1/ the library author has to offer the plugins and, 2/ users will need to remember to import and use it in any of their functions that use middleware that might return early, i.e. import { powertoolsCleanupPlugin } from '@aws-lambda-powertools/commons';
import { Tracer, captureLambdaHandler } from '@aws-lambda-powertools/tracer';
import { cacheMiddleware } from './cacheMiddleware'; // see implementation above
export const handler = middy(powertoolsCleanupPlugin()) // this needs to be used every time
.use(captureLambdaHandler(tracer)) // this is the middleware that we need to cleanup
.use(cacheMiddleware) // this is the middleware that someone wrote, and that could return early
.handler(() => {}) // this is the function handler
with the method that I suggested earlier instead, the implementation for end users and people using both Powertools and custom middlewares would look like this: import { Tracer, captureLambdaHandler } from '@aws-lambda-powertools/tracer';
import { cacheMiddleware } from './cacheMiddleware'; // see implementation above
export const handler = middy()
.use(captureLambdaHandler(tracer)) // this is the middleware that we need to cleanup
.use(cacheMiddleware) // this is the middleware that someone wrote, and that could return early
.handler(() => {}) // this is the function handler The difference is that end users don't have to know/care that some middleware need to be cleaned up, because this responsibility is shifted to the middleware author (see here). |
|
This is now released under v1.9.0 version! |
Expected Behaviour
Instructions on how to avoid this segment leak scenario.
Current Behaviour
When middy returns early, the segment is not closed (middy does not run after middleware), eventually this results in a RangeError via the incrementCounter call due to how many open segments exist.
Code snippet
Steps to Reproduce
Possible Solution
Before returning early in middy, I grab the current segment and close it. That does not seem to solve the problem, surprisingly. Do I have to also close all potential sub segments? Is the handler segment not the issue, is it actually the parent lambda segment?
AWS Lambda Powertools for TypeScript version
latest
AWS Lambda function runtime
16.x
Packaging format used
npm
Execution logs
The text was updated successfully, but these errors were encountered: