Skip to content

Conversation

@mrdziuban
Copy link
Contributor

Following up on #723, this adds a new cats-effect module with ioLocalTrace helper methods for constructing a Trace[IO] backed by an IOLocal[Span[IO]]

@catostrophe catostrophe added the enhancement New feature or request label Apr 11, 2022
@janstenpickle
Copy link
Collaborator

Thanks for this @mrdziuban!

Last time I looked at this, the IOLocal instance had to be shared by all threads using Trace. If that is the case the parent span could be overwritten by any thread using the Trace instance. Have you tried this in a multi-threaded app?

Also for this to work properly with the Http4s module, you need to have a Provide instance.

@ybasket
Copy link
Contributor

ybasket commented Apr 13, 2022

Thanks for this @mrdziuban!

Yes, absolutely love this PR and the fact someone else is excited about this possibility. I have played with IOLocal for tracing recently, glad to see someone invested the time to make a proper PR out of it! I didn't have the time for that until now. Tested your changes locally and implemented a bit more on top (hope you don't mind me sharing it here), see https://github.com/ybasket/trace4cats/commits/trace-iolocal – explanations below. Feel free to copy over whatever you like, I'll send a PR with the leftovers once this one is merged.

Last time I looked at this, the IOLocal instance had to be shared by all threads using Trace. If that is the case the parent span could be overwritten by any thread using the Trace instance. Have you tried this in a multi-threaded app?

IOLocal works per CE Fiber, so if you have things running in parallel, they will all only modify their own copy (created on first modification within that fiber), so ideal for tracing. Natchez has an implementation already. The only artefact you have is a root span that lives for as long as your Trace instance lives. But this can be mitigated by a special kind of span, see the DischargeSpan commit on my branch. It's a PoC only, its ergonomics can probably be optimised, but it works and is remotely inspired by https://github.com/armanbilge/bayou. I tested the combination of IOLocal tracing from this PR with this special span on two linked http4s services of mine and a dummy Fibonacci app that does use massive parallelism, Jaeger showed all spans as expected.

Also for this to work properly with the Http4s module, you need to have a Provide instance.

I implemented a Provide instance and also instances for an IOLocal[Ctx] where Ctx has a Lens to a Span[IO] on my branch.

@janstenpickle
Copy link
Collaborator

Thanks for the explanation @ybasket! All we should need is a Provide instance on Ctx then, with helpers providing Lens

@mrdziuban
Copy link
Contributor Author

Thanks all for the feedback, and thank you @ybasket for the enhancements! I've just added your commits here. @janstenpickle there were a couple Provide instances in @ybasket's work, do either of them match what you're thinking of? If not, I'm happy to add them, just would need a bit more guidance on the exact types to target.

@catostrophe
Copy link
Member

Sorry for short and shallow comments. I will return later with concrete suggestions.

@mrdziuban
Copy link
Contributor Author

No problem! I may not be super active or responsive the next few days but will keep my eye on this and add relevant changes (including the context bound changes) next week

@catostrophe catostrophe force-pushed the trace-iolocal branch 2 times, most recently from 7b13b5a to eca8723 Compare April 16, 2022 08:26
@catostrophe
Copy link
Member

@mrdziuban I rebased your branch on master and added 2 new commits with fixes to base(-laws) and cio modules.

The review is not finished though as I have a few questions for @ybasket and want to try the IOLocal instances in different use-cases to make sure it compiles and works as expected.

Copy link
Member

@catostrophe catostrophe left a comment

Choose a reason for hiding this comment

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

I left some comments for @mrdziuban and @ybasket


class IdTests extends BaseSuite {
implicit val cogenOption2Option: Cogen[Option ~> Option] =
Cogen(k => k(Some(0)).size.toLong)
Copy link
Member

Choose a reason for hiding this comment

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

@mrdziuban I changed this line as there may be 2 instances of Option ~> Option

import io.janstenpickle.trace4cats.base.context.laws.discipline.UnliftTests
import org.scalacheck.Cogen

class IdTests extends BaseSuite {
Copy link
Member

Choose a reason for hiding this comment

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

I renamed the file as it tests laws for F <~> F which we call identity.

implicit val cogenOption2Option: Cogen[Option ~> Option] =
Cogen(k => k(Some(0)).size.toLong)

checkAll("Option <~> Option", UnliftTests[Option, Option].unlift[String, Int])
Copy link
Member

Choose a reason for hiding this comment

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

@mrdziuban UnliftTests extends LiftTests, so we need only 1 file and 1 test invokation, not two.

def provide[A](fa: Kleisli[F, R, A])(r: R): F[A] = fa.run(r)
}

implicit def idInstance[F[_]: Monad]: Unlift[F, F] = new Unlift[F, F] {
Copy link
Member

Choose a reason for hiding this comment

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

@mrdziuban ContextRoot is the root trait for the whole hierarchy, so it is best to put instances in its companion - it will be summonable for any descendant typeclass.

@@ -0,0 +1,9 @@
package io.janstenpickle.trace4cats.base.context

object IdInstanceSummonTest {
Copy link
Member

Choose a reason for hiding this comment

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

@mrdziuban Summon tests - we just check that our instance is discoverable by the compiler

import cats.effect.{IO, IOLocal}
import io.janstenpickle.trace4cats.base.context.Provide

private[trace4cats] class ProvideForIOLocal[Ctx](rootCtx: IOLocal[Ctx]) extends Provide[IO, IO, Ctx] {
Copy link
Member

Choose a reason for hiding this comment

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

@mrdziuban We need just one implementation, no more need for IdLift/IdUnlift mixins.


def lift[A](la: IO[A]): IO[A] = la
def ask[R1 >: Ctx]: IO[R1] = rootCtx.get
def local[A](fa: IO[A])(f: Ctx => Ctx): IO[A] = rootCtx.get.flatMap { parent =>
Copy link
Member

Choose a reason for hiding this comment

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

@mrdziuban This instance seems fine but it must be tested against ProvideLaws. So we need tests just as we have them in the zio module.

import io.janstenpickle.trace4cats.base.optics.Lens
import io.janstenpickle.trace4cats.inject.Trace

package object effect {
Copy link
Member

Choose a reason for hiding this comment

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

@mrdziuban I simplified this file as I could. Still need time to check how these instances are usable


def zoom[R1](g: Getter[R, R1]): Ask[F, R1] = new Ask[F, R1] {
def F: Monad[F] = self.F
val F: Monad[F] = self.F
Copy link
Member

Choose a reason for hiding this comment

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

Ignore all these def to val changes. It's my old debt.

@catostrophe catostrophe marked this pull request as draft April 18, 2022 07:00
import io.janstenpickle.trace4cats.base.optics.Lens
import io.janstenpickle.trace4cats.inject.Trace

package object effect {
Copy link
Member

Choose a reason for hiding this comment

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

@ybasket please double check my changes here. I hope I didn't break anything

@catostrophe
Copy link
Member

I think I'll be ready to merge this PR when we have law tests for the Provide instance. Does anyone want to add them? I'll have some spare time in a few days only.

@ybasket
Copy link
Contributor

ybasket commented Apr 19, 2022

I think I'll be ready to merge this PR when we have law tests for the Provide instance. Does anyone want to add them? I'll have some spare time in a few days only.

@catostrophe I tried to add them in ybasket@330fa65. The test passes, but I'm afraid it's at least partially non-sensical. I had to implement an instance of Eq[IO[IO ~> IO]], a type for which I don't see any reasonable meaning nor implementation. As a placeholder, I always return false (true also works) – that hints towards not being used well. I'm no expert in law testing, wdyt could be done here? Extract the one law that requires the nonsensical instance and not check it for IOLocal?

@catostrophe
Copy link
Member

@ybasket hey! You should have got an ability to push to contributors fork branches. Please check and push your commits straight to this PR. I will review them a bit later and apply fix if any needed.

@ybasket
Copy link
Contributor

ybasket commented Apr 20, 2022

@ybasket hey! You should have got an ability to push to contributors fork branches. Please check and push your commits straight to this PR. I will review them a bit later and apply fix if any needed.

Oh, thank you for the honour 😊
Just pushed the commit.

@catostrophe
Copy link
Member

Will review it tonight. Sorry for the delay

mrdziuban and others added 7 commits April 25, 2022 11:48
Instances of Provide and a Trace instance for an IOLocal that contains a context from which a Span[IO] can be extracted.
DischargeSpan is a root span that spans new (non-discharge) root spans as children. Useful for situations where you have to construct resources in a traced type and need to "discharge" the tracing without having all children being linked to this long-lived artifact span.
By using Lens.focus, both classes can be merged into one.
mrdziuban and others added 4 commits April 25, 2022 11:48
1. Move one single instance to ContextRoot
2. Remove IdLift, IdUnlift traits
3. Add summon tests for idInstance
4. Fix law tests for idInstance
5. Override defs with vals where needed
1. Change bounds on F in DischargeSpan
2. Replace LocalForIOLocal with ProvideForIOLocal
3. Simplify instance providers in package object
Provide a law test for Provide on IOLocal – somewhat dummy implementation as not all signatures make sense for IO. Test passes, but its value is questionable.
@catostrophe catostrophe force-pushed the trace-iolocal branch 2 times, most recently from d768950 to c34da14 Compare April 25, 2022 12:02
@catostrophe
Copy link
Member

I refactored the module. I did it in the same way as we have it in the zio integration.

Still no law tests. Still working on it.


import _root_.io.janstenpickle.trace4cats.base.context.io.IOLocalContextInstances

package object io extends IOLocalTraceInstances with IOLocalContextInstances
Copy link
Member

Choose a reason for hiding this comment

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

The usage will be as follows:

package my.app

import cats.effect.{IO, IOApp, IOLocal}
import io.janstenpickle.trace4cats.Span
import io.janstenpickle.trace4cats.base.context.Provide
import io.janstenpickle.trace4cats.inject.{EntryPoint, Trace}
import io.janstenpickle.trace4cats.kernel.{SpanCompleter, SpanSampler}

// this imports both provide and trace instances for IOLocal
import io.janstenpickle.trace4cats.inject.io._ 

object Test extends IOApp.Simple {
  def run: IO[Unit] = {
    val spanCompleter = SpanCompleter.empty[IO]
    Span.discharge(SpanSampler.always[IO], spanCompleter).flatMap { span =>
      IOLocal(span).map { ioLocal =>
        implicit val trace: Trace[IO] = ioLocalTrace(ioLocal)
        implicit val provide: Provide[IO, IO, Span[IO]] = ioLocalProvide(ioLocal)
        Fibonacci(EntryPoint[IO](SpanSampler.always[IO], spanCompleter))
      }
    }
  }
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Looks good, I'll try it on the demo repo later!

Once this PR is merged I'd like to experiment with adding some helper for this as good parts of this snippet will always be the same, so we can make user code nicer by introducing some IOLocalHelper (better name to be found) that creates the discharge span and the IOLocal and returns type class instances ready to use. Especially on Scala 3 (context functions), this could simplify usage noticeably.

Copy link
Member

Choose a reason for hiding this comment

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

@ybasket We can include a syntax extension for Span.type. So it may look as:

val x = Span.dischargeIOLocal(sampler, completer) : IO[IOLocal[Span[IO]]

Copy link
Member

Choose a reason for hiding this comment

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

If you don't mind, let's put it in a separate PR? I just want to release something we can play with.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, definitely follow-up PR(s) for anything that's not in here yet.

@ybasket ybasket mentioned this pull request Apr 25, 2022
@catostrophe
Copy link
Member

I improved tests a bit (by adding more sensical arb instances), but I still can't get why Eq[IO ~> IO] works. Been busy at $WORK and can't dig deeper atm. Let's keep it as is and return to it later.

@catostrophe catostrophe marked this pull request as ready for review April 26, 2022 05:38
@catostrophe catostrophe merged commit 973c327 into trace4cats:master Apr 26, 2022
@mrdziuban mrdziuban deleted the trace-iolocal branch September 23, 2022 14:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants