-
Notifications
You must be signed in to change notification settings - Fork 529
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
Fiber locals #1393
Fiber locals #1393
Conversation
@@ -101,6 +102,8 @@ private final class IOFiber[A]( | |||
/* true when semantically blocking (ensures that we only unblock *once*) */ | |||
private[this] val suspended: AtomicBoolean = new AtomicBoolean(true) | |||
|
|||
private[this] var localState: scala.collection.immutable.Map[Int, Any] = initLocalState |
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.
There are two options here for holding local state:
- Immutable map/array: Spawning fibers just passes along the current local state reference (constant-time). Manipulating local state means inserting into the map and reassigning the variable (constant-time, worst case linear time IIRC?). This is the current approach
- Mutable hashmap/array: Spawning fibers has to perform a shallow copy of the local state (linear time). Manipulating local state is constant-time.
@djspiewak I'm going to think about the typeclass in a separate PR but I think this is ready |
Just curious what the known use-cases are for this? Is it just when you need mutable state but don't need to share it and hence don't need to pay the price of all the |
Tracing and telemetry are classic use cases. |
👍 to those use cases Conceptually, Personally I'm not that enthusiastic about introducing this for a few reasons. It's hard to define a typeclass for There are also alternative, less invasive ways to achieve this kind of scoping. MTL/Kleisli is one solution though the complaint here is that the type signature permeates throughout a codebase, even though the use cases are typically cross cutting concerns. Another interesting way I've thought about doing this is to basically break apart your application dependency graph and scope components as necessary. I've been wanting to write a blog post about this. |
@RaasAhsan Okay I'm pretty convinced we want this :-D |
Will merge mainline back in today/tomorrow |
Yes that's right, MTL offers the correct abstractions for this. I've used |
So here's how it's intended to work: Given a I took a look at the implementation again, and I see it doesn't reflect the above description 😓 Going to add a failing test and then refine. I think this may call for an effectful default value
what do you think about
How would this work ? The purpose of |
@RaasAhsan |
I'm good with either :)
Yeah, well, I meant that Anyway, some abstraction for creating locals would be nice to have - sth like |
As I understand, |
@kubukoz So imagine you've got a HTTP service and you want to collect traces for requests. The de facto way to do this today is via some API that involve The problem with the old If |
I might be wrong... but isn't that possible with just a plain // Assuming this is in FiberLocal
def locallyF(f: A => F[A])(fa: F[A]): F[A] = get.bracket(f(_) *> fa)(set)
//...
//could probably work if it's Resource[F, Span[F]] too
def makeNewSpan[F[_]]: Span[F] => F[Span[F]] = ???
// You get a local back, might as well be Trace[F] or something instead
def middleware[F[_]: FiberLocal.Make /* yes, I'm still trying to make Make happen */]:
F[(FiberLocal[F, Traces], HttpRoutes[F] => HttpRoutes[F])] = FiberLocal.Make[F](emptySpan).map { local =>
def wrap(routes: HttpRoutes[F]): HttpRoutes[F] =
HttpRoutes { req => OptionT(local.locallyF(makeNewSpan)(routes.run(req).value)) }
(local, wrap)
} Since every call to the route will run in a different fiber, they won't see each other's changes, so each will start with the I'm also wondering whether it would be a problem if you forked inside the processing of the HTTP call - then the FiberLocals will no longer share their state with each other, which might or might not be an issue. ...And I guess that's pretty much what |
It is! It may also be a reasonable idea to replace // divorces reference for the duration of this call, reverts after
def reset[A](fa: F[A]): F[A] something like that probably makes sense for both |
btw. my point isn't that Also, had an idea just now that Just saw your comment, maybe we really should make it a separate discussion - sounds like we could do more for |
I'd still consider doing FiberRef in another PR but I'm 👍 if y'all are :) |
@RaasAhsan let's merge this when you're ready, I think we can tackle all the others in follow-ups (even |
aaaaaaaaaaaa I'm so excited 🎉 🎉 🎉 🎉 |
back to semantics... quoting @djspiewak
hold on... why would it be 5? It's 3 after the first round of updates and then we get the state from the RHS of |
Fiber locals for CE3, the implementation is absurdly simple.
TODO:
IO.clearLocals
method to reset all local values, this makes it easier to achieve "isolated locals" semanticsCloses #1266