Skip to content
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

use official ZIO-lambda library in product-move-api #2115

Merged
merged 7 commits into from
Dec 11, 2023

Conversation

johnduffell
Copy link
Member

@johnduffell johnduffell commented Nov 23, 2023

I noticed that the docs endpoint for product-move-api was returning a 500 error

Failed with: Die(scala.MatchError: InputStreamRangeBody (of class sttp.tapir.RawBodyType$InputStreamRangeBody$),Stack trace for thread "zio-fiber-227": at com.gu.productmove.framework.TIOInterpreter.given_MonadError_TIO.$anon.flatMap(TIOInterpreter.scala:39) at com.gu.productmove.framework.TIOInterpreter.given_MonadError_TIO.$anon.handleWrappedError(TIOInterpreter.scala:46) at com.gu.productmove.framework.TIOInterpreter.given_MonadError_TIO.$anon.map(TIOInterpreter.scala:37) at com.gu.productmove.framework.TIOInterpreter.given_MonadError_TIO.$anon.flatMap(TIOInterpreter.scala:39) at com.gu.productmove.framework.TIOInterpreter.given_MonadError_TIO.$anon.map(TIOInterpreter.scala:37) at com.gu.productmove.framework.ZIOApiGatewayRequestHandler.handleWithLoggerAndErrorHandling(ZIOApiGatewayRequestHandler.scala:120) at com.gu.productmove.framework.ZIOApiGatewayRequestHandler.handleWithLoggerAndErrorHandling(ZIOApiGatewayRequestHandler.scala:121) at com.gu.productmove.framework.ZIOApiGatewayRequestHandler.handleWithLoggerAndErrorHandling(ZIOApiGatewayRequestHandler.scala:122))

When I looked into it, it was because I hadn't covered all the cases in the custom ZIO/aws lambda interpreter I wrote for tapir early last year.

Rather than adding the one case, I realised that a few months ago they added an official interpreter if we bumped to a later version of tapir. softwaremill/tapir#2975

So I switched over to the new one, which worked fine. I also had to change to the more standard ZIO Task rather than using our own custom TIO, which meant changing the error type from Any to Throwable. This might help our error stack traces, which can often be a bit unclear with other types.

Tested in CODE and working again
Feel free to take a look yourself at the docs https://product-move-api-code.support.guardianapis.com/docs/
Also tested the available moves endpoint
{{product-movement-baseurl}}/available-product-moves/A-S00737090
response data looks ok

[
    {
        "id": "2c92c0f84bbfec8b014bc655f4852d9d",
        "name": "Digital Pack",
        "billing": {
            "amount": 1499,
            "currency": {
                "code": "GBP",
                "symbol": "£"
            },
            "frequency": {
                "name": {
                    "month": {}
                },
                "count": 1
            },
            "startDate": "2024-03-27"
        },
        "trial": {
            "dayCount": 14
        },
        "introOffer": {
            "billing": {
                "percentage": 50,
                "startDate": "2023-12-28"
            },
            "duration": {
                "name": {
                    "month": {}
                },
                "count": 3
            }
        }
    }
]

@@ -568,6 +568,7 @@ lazy val `product-move-api` = lambdaProject(
"com.softwaremill.sttp.tapir" %% "tapir-core" % tapirVersion,
"com.softwaremill.sttp.tapir" %% "tapir-json-zio" % tapirVersion,
"com.softwaremill.sttp.tapir" %% "tapir-aws-lambda" % tapirVersion,
"com.softwaremill.sttp.tapir" %% "tapir-aws-lambda-zio" % tapirVersion,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the new library added in softwaremill/tapir#2975

Comment on lines +16 to +17
val layer: Layer[Throwable, AwsCredentialsProvider] =
ZLayer.scoped(ZIO.fromAutoCloseable(ZIO.attempt(impl)))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the official zio interpreter expects Throwable as the error, rather than Any, so I had to make everything comply. That is the majority of the changes.

Comment on lines 68 to 76
@main
// this will output the HTML to the console
def testDocsHtml(): Unit = {
Handler.runTest(
"GET",
"/docs/",
None,
)
}
Copy link
Member Author

@johnduffell johnduffell Nov 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new manual test so I could reproduce the issue locally.

ZLayer.scoped(for {
stage <- ZIO.service[Stage]
sqsClient <- initializeSQSClient().mapError(ex =>
InternalServerError(s"Failed to initialize SQS Client with error: $ex"),
new Throwable(s"Failed to initialize SQS Client with error", ex),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we were returning InternalServerError but I don't know if it was actually getting used directly, or just converted into an internal server error later anyway

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you check? We obvs want to maintain the existing behaviour

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes that's a really good point I will check that for sure

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, existing code we end up with Any as the failure type in the ZIO, and that ends up here in the code


So no matter whether we called it InternalServerError or BadRequest etc, it it would still be an internal server error.

If/where we map the error to a ZIO.success then we could use it as a response.
I think the new (official) way of using Throwable rather than my way of using Any as the error type is better as it's pretty clear that a Throwable will be a 500 error, and if we try to use a BadRequest or something, expecting it will really be a bad request, it won't compile unless we recover it into a success.

Comment on lines +25 to +33
object Handler
extends ZIOApiGatewayRequestHandler(
List(
AvailableProductMovesEndpoint.server,
ProductMoveEndpoint.server,
UpdateSupporterPlusAmountEndpoint.server,
SubscriptionCancelEndpoint.server,
),
)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed all the test stuff from the handler and made the endpoints a constructor parameter.

This was because with inheritance and vals I was getting issues with initialisation order, and server was coming out as null.
Rather than just going down the lazy val option (which worked) I decided to separate/simplify appropriately.

"POST",
"/product-move/A-S123",
Some(ProductMoveEndpointTypes.ExpectedInput(49.99, false, None, None).toJson),
"/product-move/recurring-contribution-to-supporter-plus/" + devSubscriptionNumber,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was not matching the actual routes definiton.

)
}

// for testing
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this functions moved out of the superclass ZIOApiGatewayRequestHandler


val credentialsProvider = AwsCredentialsProviderChain
private lazy val secretsClient = SecretsManagerClient.builder().credentialsProvider(credentialsProvider).build()
lazy val credentialsProvider = AwsCredentialsProviderChain
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in another PR this should probably use the credentials provider layer like the other AWS layers

str = new String(allBytes, StandardCharsets.UTF_8)
_ <- ZIO.log("input: " + str)
loggedOutputStream = new ByteArrayOutputStream()
_ <- handler.process[AwsRequestV1](new ByteArrayInputStream(allBytes), loggedOutputStream)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have to use V1 because we are still using the 1.0 integration
https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html

I'm not sure if we should be updating or not, it doesn't seem to encourage it in the documentation.

Comment on lines -120 to -121
.provideLayer(Runtime.removeDefaultLoggers)
.provideLayer(Runtime.addLogger(new AwsLambdaLogger(context.getLogger))),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the lambda logger support, this means we use the default logging.
I'm not sure this was actually still working anyway, as looking in PROD I can't see the timestamps being generated.
Ultimately everything that goes to stdout will end up in the cloudwatch logs, it might just not look nice.


object ZIOApiGatewayRequestHandler {
abstract class ZIOApiGatewayRequestHandler(val server: List[ServerEndpoint[Any, Task]]) extends RequestStreamHandler {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this class has basically been rewritten according to the example
https://github.com/softwaremill/tapir/blob/master/serverless/aws/lambda-zio-tests/src/main/scala/sttp/tapir/serverless/aws/ziolambda/tests/ZioLambdaHandlerImpl.scala
with the addition of request and response stream logging

@@ -65,20 +62,11 @@ object Secrets {
def getSalesforceSSLSecrets: RIO[Secrets, SalesforceSSLSecrets] =
ZIO.environmentWithZIO[Secrets](_.get.getSalesforceSSLSecrets)
}
object SecretsLive extends Secrets {
class SecretsLive(secretsClient: SecretsManagerClient) extends Secrets {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is updated to match how the S3 client works, the credentials come from the AwsCredentialsLive.layer

ZLayer.scoped {
for {
creds <- ZIO.service[AwsCredentialsProvider]
s3Client <- ZIO.fromAutoCloseable(ZIO.attempt(impl(creds)))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is an important change - the SecretsManagerClient needs to be .close()d after use

@@ -84,7 +85,7 @@ object InvoiceItemAdjustmentSpec extends ZIOSpecDefault {
),
)
assert(adjustments.length)(equalTo(2)) &&
assert(adjustments.head.AdjustmentDate.getDayOfMonth)(equalTo(15))
assert(adjustments.head.AdjustmentDate.getDayOfMonth)(equalTo(15))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't agree with this, but scalafmt complains otherwise

[warn] scalafmt: ~/code/support-service-lambdas/handlers/product-move-api/src/test/scala/com/gu/productmove/zuora/InvoiceItemAdjustmentSpec.scala isn't formatted properly!

Copy link
Member

@rupertbates rupertbates left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just one question about error types

run(subscriptionName).tapEither(result => ZIO.log("result tapped: " + result)).map(Right.apply)
}
}

// sub to test on: "A-S00334930"
private def run(subscriptionName: String): TIO[OutputBody] =
runWithEnvironment(SubscriptionName("A-S00334930")).provide(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this shouldn't have been hard coded, it should have used the one passed from the client!

@johnduffell johnduffell merged commit fd0c598 into main Dec 11, 2023
@johnduffell johnduffell deleted the jd-fix-docs-endpoint branch December 11, 2023 12:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants