-
Notifications
You must be signed in to change notification settings - Fork 41
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
MTL Lambda #60
MTL Lambda #60
Conversation
Co-authored-by: Brian P. Holt <bholt+github@planetholt.com>
} | ||
} | ||
|
||
def handler(implicit env: LambdaEnv[IO, Event]): Resource[IO, IO[Option[Result]]] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is the most confusing aspect of this change: The Resource
is run once, to create an IO
that will be run many times. The IO
is the handler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, this is unsafe, because it would let you access the event
and context
while building the resource stack, which is not right. I knew those null
s were a bad sign 😅 . Fixing.
This looks interesting! I'll try to publish it locally and pull it into the two Lambdas I've been playing with (one regular one, and one custom CFN resource) and report back. (Hopefully tomorrow.) |
Thanks! No rush, and grateful for your patience with all the churn while we figure this out 😁 having you try these designs out with your lambdas has been invaluable, much appreciated! If it helps, I just published this branch as: "com.armanbilge" %%% "feral-lambda" % "0.1-2f94f32" // and always friends! |
Finally got around to trying this, so I'm going to add some comments on the code in no particular order. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Working with LambdaEnv[F, Input] => F[Option[Output]]
can be a little awkward when you want to define an anonymous function but make the LambdaEnv[F, Input]
available implicitly to the body of the function. I started working around it by defining it as a Kleisli, which makes me wonder if it should actually be a Kleisli in the library after all.
|
||
object TracedLambda { | ||
|
||
def apply[F[_]: MonadCancelThrow, Event: KernelSource, Result](entryPoint: EntryPoint[F])( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think if we tweak this a bit, we'll get better type inference downstream:
def apply[F[_] : MonadCancelThrow, Event, Result](entryPoint: EntryPoint[F])
(lambda: Span[F] => F[Option[Result]])
(implicit
env: LambdaEnv[F, Event],
KS: KernelSource[Event]): F[Option[Result]]
As written now, it seems to get confused about what the Event
type is, I think because it binds the KernelSource[Event]
(using apiGatewayProxyEventV2KernelSource
) before looking for the LambdaEnv[F, Event]
, which, if Event
is not ApiGatewayProxyEventV2
, won't be available.
Explicitly putting the implicit LambdaEnv[F, Event]
parameter first lets it use that to bind Event
, and then it correctly finds the emptyKernelSource
low-priority implicit value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aha! Very nice!
import io.circe.Decoder | ||
import io.circe.Encoder | ||
import cats.effect.IOLocal | ||
|
||
abstract class IOLambda[Event, Result]( | ||
implicit private[lambda] val decoder: Decoder[Event], | ||
private[lambda] val encoder: Encoder[Result] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I noticed that the Encoder[INothing]
is missing from this PR. Was that intentional? I had to add it manually to get things working with a Result
of INothing
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yeah, I didn't merge that branch into this one. We should probably start landing some of these 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, agreed. I would love to get an alpha version of this published under "org.typelevel"
soon!
} | ||
} | ||
|
||
object LambdaEnv { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like to add a summoner and implicit instances for Kleisli and friends:
def apply[F[_], A](implicit env: LambdaEnv[F, A]): LambdaEnv[F, A] = env
implicit def kleisliLambdaEnv[F[_] : Functor, A, B](implicit env: LambdaEnv[F, A]): LambdaEnv[Kleisli[F, B, *], A] =
env.mapK(Kleisli.liftK)
implicit def optionTLambdaEnv[F[_] : Functor, A](implicit env: LambdaEnv[F, A]): LambdaEnv[OptionT[F, *], A] =
env.mapK(OptionT.liftK)
implicit def eitherTLambdaEnv[F[_] : Functor, A, B](implicit env: LambdaEnv[F, A]): LambdaEnv[EitherT[F, B, *], A] =
env.mapK(EitherT.liftK)
implicit def writerTLambdaEnv[F[_] : Applicative, A, B : Monoid](implicit env: LambdaEnv[F, A]): LambdaEnv[WriterT[F, B, *], A] =
env.mapK(WriterT.liftK[F, B])
The Kleisli instance comes in handy when tracing with Kleisli[F, Span[F], *]
. Not sure if the others are needed but I've noticed they get added in a lot of TypeLevel projects and I don't see any reason not to include them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 for sure! Good to have.
client: Client[F], | ||
handler: CloudFormationCustomResource[F, Input, Output])( | ||
implicit | ||
env: LambdaEnv[F, CloudFormationCustomResourceRequest[Input]]): F[Option[Unit]] = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should F[Option[Unit]]
be F[Option[INothing]]
?
FYI, the POC code using this branch is available here for a plain-ol' AWS Lambda and here for a custom CloudFormation resource. |
Do you have an example of this? I was thinking something like this, but maybe it doesn't work so well. { implicit env =>
// use it implicitly
}
Yes, that's the heart of the matter :) I assume you mean like we considered in #51 (comment)? The Kleisli and MTL approach are not mutually exclusive. If you have a Kleisli with So overall, how do you like this MTL approach vs the approach in #51? |
That works fine if the return type is private def resources[F[_] : Async : Console]: Resource[F, LambdaEnv[F, CloudFormationCustomResourceRequest[DatabaseMetadata]] => F[Option[Unit]]] =
Resource.eval(Slf4jLogger.create[F]).flatMap { implicit logger =>
for {
client <- httpClient[F]
entryPoint <- Resource.eval(Random.scalaUtilRandom[F]).flatMap { implicit r => XRay.entryPoint[F]() }
secretsManager <- SecretsManagerAlg.resource[F, Kleisli[F, Span[F], *]]
} yield Kleisli { implicit env: LambdaEnv[F, CloudFormationCustomResourceRequest[DatabaseMetadata]] =>
TracedLambda[F, CloudFormationCustomResourceRequest[DatabaseMetadata], Unit](entryPoint) { span =>
val tracedClient = NatchezMiddleware.client(client.translate(Kleisli.liftK[F, Span[F]])(Kleisli.applyK(span)))
CloudFormationCustomResource(tracedClient, PostgresqlDatabaseInitHandlerImpl(secretsManager)).run(span)
}
}.run
} |
Thanks for that example! So after playing with your RabbitMQ lambda a bit in Dwolla/rabbitmq-topology-backup#77 I think we can avoid issues like above by binding to I think it is very reasonable/realistic/practical to acquire resources and patch together the lambda handler directly in What do you think? |
Co-authored-by: Brian P. Holt <bholt@dwolla.com>
Co-authored-by: Brian P. Holt <bholt@dwolla.com>
I'm not sure what I was doing differently before, but I don't seem to have that problem doing } yield { implicit env => with def handlerF[F[_] : Async]: Resource[F, LambdaEnv[F, RabbitMQConfig] => F[Option[INothing]]] now, so that's good news. (I still don't think I would want to move the resource acquisition stuff into |
Unless anyone sees any blockers, I think we should run with this design, cut an alpha release, and let people start playing with it. As early in the project as we are, I think we have flexibility to change things quite a bit if we discover this approach doesn't work for some reason. |
@armanbilge bring it on :) |
Here we go again 😆
This takes the idea in #51 (comment) about MTL-based middlewares for a spin.
The magic in this abstraction is provided by:
As long as you have a
LambdaEnv
, you can access the event and context from anywhere in the lambda. I think this is fantastic. It improves composability yet one more time: for example, now the http4s and CloudFormation lambdas no longer have to explicitly pass aroundContext[F]
objects so that their routes/handlers have access to them. In general, you can build up your middleware stack however you want without worrying if the inputs of your middlewares line up: every middleware independently has full access to theLambdaEnv
.