Skip to content

Add Support for Differentiating Cached and Regular Responses in Idempotency Handling for Accurate Metrics #1780

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

Closed
fernando-mendonca1 opened this issue Feb 27, 2025 · 16 comments · Fixed by #1814
Assignees
Labels
feature-parity Feature parity with python version feature-request New feature or request idempotency v2 Version 2

Comments

@fernando-mendonca1
Copy link

fernando-mendonca1 commented Feb 27, 2025

Is your feature request related to a problem? Please describe.

In the Account Security team at Booking, we rely on the Java Powertools library's idempotency support to manage both idempotent and non-idempotent downstream API calls. This functionality is crucial for ensuring that our security remediations are executed without unintended duplication or unnecessary load on our systems.

However, there is currently no mechanism to distinguish between a regular response and one that has been cached and returned by the Powertools library. While this behavior is technically correct and functions as intended, it presents a challenge when it comes to accurately tracking business metrics. Specifically, if we are unable to differentiate between regular and cached responses, our count of successful executions becomes inaccurate. This discrepancy impacts our projects on a business level, as we depend on these metrics to monitor key goals and milestones effectively.

Describe the solution you'd like

We propose two potential solutions, in decreasing order of preference:

  1. Callback Method for Response Manipulation: Introduce support for manipulating responses through a callback method during idempotency configuration. This would allow us to modify the response object to include additional information indicating whether it was served from cache.
Idempotency.config().withPersistenceStore(
  DynamoDBPersistenceStore.builder()
    .withTableName(System.getenv("TABLE_NAME"))
    .withEventHandler(subscriptionResult -> {
      return new SubscriptionResult(subscriptionResult.getPaymentId(), "noop", 200);
    })
    .build()
).configure();
  1. Key-Value Pair in Cached Responses: Allow the specification of a key-value pair that would be appended to cached responses. This would provide a straightforward way to identify cached responses in our metrics.
Idempotency.config().withPersistenceStore(
  DynamoDBPersistenceStore.builder()
    .withTableName(System.getenv("TABLE_NAME"))
    .withCachedResponseKey("x-idempotent-key")
    .withCachedResponseValue("true")
    .build()
).configure();

Implementing either of these solutions would bring the Java implementation in line with the capabilities already available in the Python version of Powertools, which supports response manipulation as documented here: Powertools Python Documentation.

Describe alternatives you've considered

  • Moving the @Idempotent Annotation: Shifting the annotation to a secondary method rather than the handler method was deemed unviable, as idempotency execution is independent of the method itself.

  • Custom Idempotency Annotation: Creating a custom annotation that inherits from Powertools was rejected due to the increased complexity in maintenance and the need to track upstream changes continuously.

  • Implementing Functionality In-House: Developing similar functionality within our project using DynamoDB would lead to duplication of existing features and an increased maintenance burden, which we aim to avoid.

@phipag
Copy link
Contributor

phipag commented Feb 27, 2025

Hey @fernando-mendonca1, thank you for your detailed feature request and for describing your use-case in so much detail. This is awesome! 🚀

We have recently published our roadmap for 2025 and feature parity between the Java library and Powertools libraries in other languages is something we are planning to work on. I believe that your feature request fits well in this context.

There was already some work done regarding a new major version release and I can image that the feature you described could become part of this (see v2 branch). In v2, the idempotency module was already refactored into multiple submodules where the DynamoDB persistency layer is no longer coupled to the core idempotency logic: #1467.

Regarding your proposed options (1 and 2), I believe both the response hook solution as well as your withCachedResponse proposal should be independent of the persistence layer. Instead of being part of the DynamoDBPersistenceStore.build()... they should be part of the IdempotencyConfig. To sketch roughly how a response hook could look like (Option 1 in your proposal):

Idempotency.config()
        .withPersistenceStore(
                DynamoDBPersistenceStore.builder()
                        .withTableName(System.getenv("TABLE_NAME"))
                        .build())
        // Make it part of IdempotencyConfig rather than persistence store
        .withConfig(
                IdempotencyConfig.builder()
                        .withEventKeyJMESPath("messageId")
                        // Logic to manipulate original response can be added here
                        .withResponseHook((response, idempotentData) -> {
                            return new SubscriptionResult(response.getPaymentId(), "noop", 200);
                        })
                        .build())
        .configure();

I will triage your request further with the Powertools team.

In the meantime, some questions:

  • Would a response hook similar to Powertools in Python help solve your use-case?
  • Would you be interested in working on this feature and contributing it to Powertools in Java?

@phipag
Copy link
Contributor

phipag commented Mar 10, 2025

Hey @fernando-mendonca1, we will pick this up for version 2.0.0 of Powertools.

I prefer the response hook option since it is similar to the Python and TypeScript implementation. @hjgraca I assume you will go for the response hook option in .NET as well?

@fernando-mendonca1, let me know what you think.

@fernando-mendonca1
Copy link
Author

Hi, @phipag,

Coming back to this. Apologies for the great delay, I was on sick leave for a good while, and swamped once I came back.

To start by answering your questions, I believe the example you wrote out on this comment will work very well, after having a few meetings with my team to check in with them.

Regarding contribution, I'm happy to do it, I'm just not sure how much time I can allocate towards it and I don't want to slow down or impede anything. Perhaps something that might work and be helpful is if I help review and test the work? I'd be able to quickly plug the new version in and deploy it to out staging environment after reviewing.

From now on I'll be more readily available, so please fell free to reach out if there's anything I can do.

@phipag phipag self-assigned this Mar 20, 2025
@phipag phipag moved this from Backlog to Working on it in Powertools for AWS Lambda (Java) Mar 20, 2025
@phipag
Copy link
Contributor

phipag commented Mar 20, 2025

Hey @fernando-mendonca1,

thanks for getting back to me. I hope you feel better now! 🚀

No problem, I understand that you have a busy schedule. We can work on this together where I do the implementation work with your help in reviewing it.

In this branch, I created a first implementation draft and updated our idempotency example to reflect the use-case you described. Currently, the interface looks like this where the response hook will be called with a Java Object containing the de-serialized response data fetched from the persistence store in addition to a DataRecord object which can be used to retrieve auxiliary idempotency information, such as expiration timestamp.

    public App(DynamoDbClient client) {
        Idempotency.config().withConfig(
                IdempotencyConfig.builder()
                        .withEventKeyJMESPath("powertools_json(body).address")
                        // This is the new response hook option
                        .withResponseHook((responseData, dataRecord) -> {
                            if (responseData instanceof APIGatewayProxyResponseEvent) {
                                APIGatewayProxyResponseEvent proxyResponse = (APIGatewayProxyResponseEvent) responseData;
                                final Map<String, String> headers = new HashMap<>();
                                headers.putAll(proxyResponse.getHeaders());
                                headers.put("x-idempotency-response", "true");
                                headers.put("x-idempotency-expiration",
                                        String.valueOf(dataRecord.getExpiryTimestamp()));

                                proxyResponse.setHeaders(headers);

                                return proxyResponse;
                            }

                            return responseData;
                        })
                        .build())
                .withPersistenceStore(
                        DynamoDBPersistenceStore.builder()
                                .withDynamoDbClient(client)
                                .withTableName(System.getenv("IDEMPOTENCY_TABLE"))
                                .build())
                .configure();
    }

Based on the use-case you described, I updated the example to append two x-idempotency headers to the response object that will be sent via API Gateway to the client.

Non-idempotent response:

< HTTP/2 200 
< content-type: application/json
< content-length: 55
< date: Thu, 20 Mar 2025 14:55:57 GMT
< x-amzn-trace-id: Root=1-67dc2c7c-5349e77540360acb395f40ae;Sampled=1;Lineage=1:b0015d10:0
< x-amzn-requestid: 5836eb2b-db6a-438c-83fe-b4719edf2c53
< access-control-allow-origin: *
< access-control-allow-headers: *
< x-amz-apigw-id: HuvjlHivvHcEf-Q=
< access-control-allow-methods: GET, OPTIONS
< x-cache: Miss from cloudfront
< via: 1.1 25c72aca03a56915d393638f26b2b73e.cloudfront.net (CloudFront)
< x-amz-cf-pop: ZRH55-P2
< x-amz-cf-id: nbfPl2vw6HcBWOXrv8zhoKvmuXH-Rn3kK7cIA2tz34XSAkb2U_kPQg==

Idempotent response:

< HTTP/2 200 
< content-type: application/json
< content-length: 55
< date: Thu, 20 Mar 2025 14:55:39 GMT
< x-amzn-trace-id: Root=1-67dc2c6b-22596f0b037292377ced0fe6;Sampled=1;Lineage=1:b0015d10:0
< x-amzn-requestid: 951efb49-e6c4-4b06-b597-dba345266722
< x-idempotency-expiration: 1742485871
< access-control-allow-origin: *
< access-control-allow-headers: *
< x-amz-apigw-id: HuvgyFr8vHcEIPQ=
< x-idempotency-response: true
< access-control-allow-methods: GET, OPTIONS
< x-cache: Miss from cloudfront
< via: 1.1 f7fd0095deab06cf8fa6a7365f5ec6e8.cloudfront.net (CloudFront)
< x-amz-cf-pop: ZRH55-P2
< x-amz-cf-id: jDK9fNscWSFAPT-vCS8jNfqbfQP6aS9AdHNgrZPiLXyh-EHnyrL5fA==

What do you think about this? Does this address your use-case of generating client-side metrics regarding idempotent responses?

Let me know if you have any suggestions for improvement.

@phipag phipag linked a pull request Mar 28, 2025 that will close this issue
4 tasks
@phipag
Copy link
Contributor

phipag commented Mar 28, 2025

Hey @fernando-mendonca1, I just merged a PR with this feature and also updated our official example with an example of how to append HTTP Headers to idempotent responses: https://github.com/aws-powertools/powertools-lambda-java/blob/v2/examples/powertools-examples-idempotency/src/main/java/helloworld/App.java#L50-L65.

The preview documentation website was also updated: https://docs.powertools.aws.dev/lambda/java/preview/utilities/idempotency/#manipulating-the-idempotent-response.

I would love for you and your team to review and test this. Would you be able to pull the repository and test the 2.0.0-SNAPSHOT in a dev environment of yours?

I am happy to help – please let me know if you want to jump on a call for this and we can arrange this.

@phipag phipag moved this from Working on it to Coming soon in Powertools for AWS Lambda (Java) Mar 28, 2025
@phipag phipag added this to the v2 milestone Mar 28, 2025
@phipag phipag closed this as completed Mar 28, 2025
@fernando-mendonca1
Copy link
Author

Hi, @phipag,

Amazing, thank you for working on this and for the update.

I'd be very happy review and test it. I just finished my previous task last Friday, so this will be my priority this week. I will keep you updated through here.

Regarding a call, let me give it a try first and see if I encounter any blockers, then we can set it up, it would be very appreciated.

@phipag
Copy link
Contributor

phipag commented Mar 31, 2025

Awesome, let me know if you have any questions 🚀

@fernando-mendonca1
Copy link
Author

Hey back, @phipag,

Wanted to circle back and confirm that my tests with v2 have been very successful. I wanted to take some extra time also to validate it in our staging environment, as well as confirm that it would work with our unit tests.

If there is anything else that I can do to help please feel free to ask.

Also, what are the necessary steps until this version of the library is pushed to the official assets repository?

@phipag
Copy link
Contributor

phipag commented Apr 8, 2025

Hey @fernando-mendonca1, welcome back! 👋

I am very happy to hear that your testing was successful. Let me know how you progress with your unit tests.

Also, what are the necessary steps until this version of the library is pushed to the official assets repository?

I am currently working on preparations for the v2 release. We are targeting (tentative date) to release a stable v2 by the end of Q2 2025. Until then, I will work on setting up 2.0.0-SNAPSHOT releases as well as doing regular pre-releases with comprehensive documentation and release notes (2.0.0-alpha-1, 2.0.0-alpha-2, ...).

This week, I will create public documentation about this process and will send you an update with more info once this is done. Additionally, I will let you know once the SNAPSHOT as well as potential pre-release assets are published to Maven Central. This will happen very soon as well.

@fernando-mendonca1
Copy link
Author

Great, thank you @phipag.

Everything is clear. I'm done with the testing on my side. We've deployed a version of our code that uses v2 to our integration environment, and it's working great. As get closer to a stable release we'll keep in touch and I'll deploy to production as well.

I have some availability to help out, so please let me know if there's anything I can help with.

@phipag
Copy link
Contributor

phipag commented Apr 9, 2025

Thanks @fernando-mendonca1, I am happy to hear that. I will keep you posted as we progress with the first v2 releases.

Regarding this specific feature, there is nothing more to be done but you can have a look at our open issues especially those with the v2 label if you would like to contribute on any open issues. I am happy to assist you with that if you need any pointers.

@fernando-mendonca1
Copy link
Author

Hi, @phipag,

I have a quick question from my time. When you publish pre-release versions, such as alpha and beta, do you do it on the main Maven repository, or somewhere else? Additionally, are these pre-release versions labeled SNAPSHOT, or release?

@phipag
Copy link
Contributor

phipag commented Apr 14, 2025

Hi @fernando-mendonca1,

we used to publish SNAPSHOT versions to https://aws.oss.sonatype.org:

<distributionManagement>
<snapshotRepository>
<id>ossrh</id>
<url>https://aws.oss.sonatype.org/content/repositories/snapshots</url>
</snapshotRepository>
</distributionManagement>

Caution: These are not up-to-date.

Based on this migration guide it looks like we need to migrate to Maven Central soon (https://central.sonatype.org/faq/what-is-different-between-central-portal-and-legacy-ossrh/) which means that those SNAPSHOT releases will end up in the main Maven Repository once we completed this migration.

Additionally, are these pre-release versions labeled SNAPSHOT, or release?

What do you mean by "labeled"?

@phipag
Copy link
Contributor

phipag commented Apr 22, 2025

Hey @fernando-mendonca1,

I have a quick update on the location where we will publish SNAPSHOT and alpha releases. Until further notice, you can expect them to land the AWS OSS Sonatype repository here: https://aws.oss.sonatype.org/content/repositories/snapshots/software/amazon/lambda/.

And you can read the most up-to-date documentation for these releases here: https://docs.powertools.aws.dev/lambda/java/preview/

I will notify you here again once we made the first SNAPSHOT release to this location.

@fernando-mendonca1
Copy link
Author

Hey @phipag ,

Very nice, thank you for the clear information. We're currently running the v2 branch in our staging environment and integrating with the different Step Functions and Lambdas we use, and we're very happy with the results. Just wanted to thank you again.

Will keep an eye out for alpha releases.

@phipag
Copy link
Contributor

phipag commented Apr 22, 2025

This is awesome to hear! Let's keep in touch on the progress you and us make.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature-parity Feature parity with python version feature-request New feature or request idempotency v2 Version 2
Projects
Status: Coming soon
Development

Successfully merging a pull request may close this issue.

3 participants