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

Properly propogate Writer and State effects in bracketCase #374

Merged
merged 7 commits into from
Nov 22, 2018

Conversation

LukaJCB
Copy link
Member

@LukaJCB LukaJCB commented Sep 24, 2018

Should fix #371.

With this change:

scala> WriterT(IO(1 -> "asd"))
res0: cats.data.WriterT[cats.effect.IO,Int,String] = WriterT(IO$2102705681)

scala> Sync[WriterT[IO, Int, ?]].bracket(res0)(s => WriterT(IO(2 -> "2")))(s => WriterT(IO( (7, ()) )))
res1: cats.data.WriterT[cats.effect.IO,Int,String] = WriterT(IO$1115955315)

scala> res1.run
res2: cats.effect.IO[(Int, String)] = IO$1115955315

scala> res2.unsafeRunSync
res2: (Int, String) = (10,2)

Without this change:

scala> WriterT(IO(1 -> "asd"))
res0: cats.data.WriterT[cats.effect.IO,Int,String] = WriterT(IO$62720203982)

scala> Sync[WriterT[IO, Int, ?]].bracket(res0)(s => WriterT(IO(2 -> "2")))(s => WriterT(IO( (7, ()) )))
res1: cats.data.WriterT[cats.effect.IO,Int,String] = WriterT(IO$31215753453)

scala> res1.run
res2: cats.effect.IO[(Int, String)] = IO$297120624

scala> res2.unsafeRunSync
res2: (Int, String) = (3,2)

@codecov-io
Copy link

codecov-io commented Sep 24, 2018

Codecov Report

Merging #374 into master will increase coverage by 0.91%.
The diff coverage is 97.87%.

@@            Coverage Diff             @@
##           master     #374      +/-   ##
==========================================
+ Coverage   88.56%   89.48%   +0.91%     
==========================================
  Files          69       69              
  Lines        1802     1845      +43     
  Branches      197      192       -5     
==========================================
+ Hits         1596     1651      +55     
+ Misses        206      194      -12

@catostrophe
Copy link
Contributor

The same technique can be applied to the other transformers: EitherT, OptionT and IorT (#365).

@catostrophe
Copy link
Contributor

Async.asyncF has the same issue.

release(a, res).value
(release: (A, ExitCase[Throwable]) => WriterT[F, L, Unit]): WriterT[F, L, B] =
WriterT.liftF(Ref.of[F, Option[L]](None)).flatMap { ref =>
uncancelable(acquire).flatMap { a =>
Copy link
Member

Choose a reason for hiding this comment

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

This construct is not correct: uncancelable(acquire).flatMap

The gist is this: a task that allocates a resource, like acquire, always has to be in the context of a bracket. In this case it is not and that flatMap can be cancelled via the auto-cancelation mechanisms. The problem is that it can't be reproduced with cats.effect.IO, only because cats.effect.IO does not auto-fork.

If we cannot fix it somehow, then this instance needs to be scheduled for deletion in version 2.0 because it is broken.

@oleg-py can you also take a look when you've got some time?

Copy link
Member

@alexandru alexandru Sep 25, 2018

Choose a reason for hiding this comment

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

Also, worse case scenario we add a @deprecated tag to it warning people that it is broken.

Copy link
Contributor

Choose a reason for hiding this comment

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

Good catch, this is certainly broken.

I think we could make release idempotent here and use bracket second time instead of uncancelable, if we want to keep this instance.

@alexandru
Copy link
Member

alexandru commented Sep 26, 2018

As a manifestation of what I was saying, the problem with uncancelable(acquire).flatMap is now reproducible in PR #376

I had to deactivate WriterT's instance tests because WriterT is now unlawful.

@LukaJCB
Copy link
Member Author

LukaJCB commented Sep 26, 2018

Shall we merge #376 first and then fix the instance with this PR?

@alexandru
Copy link
Member

Yes, that order sounds good as now we can really see the problem.

F.bracketCase(F.pure(a))(use.andThen(_.run)){ (a, res) =>
release(a, res).value
(release: (A, ExitCase[Throwable]) => WriterT[F, L, Unit]): WriterT[F, L, B] =
WriterT.liftF(Ref.of[F, Option[L]](None)).flatMap { ref =>
Copy link

Choose a reason for hiding this comment

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

Since we have a Monoid, can't we just use Ref.of[F, L](L.empty) and save on Option ?

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually I don't think this will work, given the fact that we couldn't check if an L is empty, like we can with Option (atleast without requiring an extra Eq[L]).

Copy link

Choose a reason for hiding this comment

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

You mean that getOrElse in the end?
I see. Didn't notice that, thanks for clearing this up for me 👍

@alexandru
Copy link
Member

I have merged #376 in master and would like to do a release soon.

Any progress on this?

@catostrophe
Copy link
Contributor

Have we come to a consensus on the transformer effect propagation in bracket's release and Async.asyncF? Why does this PR fix only WriterT and StateT instances and only for Sync? For any transformer, its own effect can be temporarily stored into a Ref and added (flatTapped) to the result later.

I'm working on #365 and it's not clear for me what strategy for IorT effect propagation should be chosen in bracket's release and asyncF. Current PR just ignores it as in other instances. But I prepared a draft (not pushed yet) where I take it into account.

Can we clarify sematics?

@LukaJCB
Copy link
Member Author

LukaJCB commented Oct 8, 2018

@catostrophe
I haven't looked at asyncF yet, but we need Sync in order to use Ref. I think we could probably use Ref for EitherT as well, but I haven't tried yet. I'll try to work on it soon :)

@LukaJCB
Copy link
Member Author

LukaJCB commented Oct 8, 2018

Added OptionT and EitherT:

scala> OptionT(IO(Option(1))) ^
res0: cats.data.OptionT[cats.effect.IO,Int] = OptionT(IO$1900219430)

scala> Sync[OptionT[IO, ?]].bracket(res0)(_ => OptionT(IO(Option(2))))(_ => OptionT.none[IO, Unit])
res1: cats.data.OptionT[cats.effect.IO,Int] = OptionT(IO$1768249421)

scala> res1.value.unsafeRunSync
res2: Option[Int] = None
scala> EitherT(IO(Either.right[String, Int](1)))
res0: cats.data.EitherT[cats.effect.IO,String,Int] = EitherT(IO$983210249)

scala> Sync[EitherT[IO, String, ?]].bracket(res10)(_ => EitherT(IO(Either.right[String, Int](2))))(_ => EitherT(IO(Either.left[String, Unit]("Hi"))))
res1: cats.data.EitherT[cats.effect.IO,String,Int] = EitherT(IO$111852734)

scala> res1.value.unsafeRunSync
res2: Either[String,Int] = Left(Hi)

F.unit // nothing to release
}
})
EitherT.liftF(Ref.of[F, Option[L]](None)).flatMap { ref =>
Copy link
Contributor

Choose a reason for hiding this comment

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

flatMapF

F.unit
})
//Boolean represents if release returned None
OptionT.liftF(Ref.of[F, Boolean](false)).flatMap { ref =>
Copy link
Contributor

Choose a reason for hiding this comment

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

And here

} { case ((s, a), br) =>
release(a, br).run(s).void
(release: (A, ExitCase[Throwable]) => StateT[F, S, Unit]): StateT[F, S, B] =
StateT.liftF(Ref.of[F, Option[S]](None)).flatMap { ref =>
Copy link
Contributor

Choose a reason for hiding this comment

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

And here

(release: (A, ExitCase[Throwable]) => WriterT[F, L, Unit]): WriterT[F, L, B] =
WriterT.liftF(Ref.of[F, Option[L]](None)).flatMap { ref =>
acquire.flatMap { a =>
WriterT(
Copy link
Contributor

Choose a reason for hiding this comment

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

And here

@alexandru
Copy link
Member

Guys, no progress on these instances?

I my PR I introduced this:

@deprecated("WARNING: currently the Async[WriterT[F, L, ?]] instance is broken!", "1.1.0")

For a 1.1.0 release we need to make a decision — do we fix these instances, or do we deprecate them?

Can we fix them by any means? Can you make progress on it?

@catostrophe
Copy link
Contributor

This issues seems to be fixed. We only need to finish the review.

@LukaJCB
Copy link
Member Author

LukaJCB commented Nov 9, 2018

I'm not sure its fixed unfortunately, the issue with propogation in release is fixed, but the underlying issue with making acquire uncancelable is still there as can be seen by the failing tests in CI :(

@catostrophe
Copy link
Contributor

This should fix the failing tests:

import cats.instances.tuple._

def bracketCase[A, B](acquire: WriterT[F, L, A])
  (use: A => WriterT[F, L, B])
  (release: (A, ExitCase[Throwable]) => WriterT[F, L, Unit]): WriterT[F, L, B] =
  WriterT(
    Ref[F].of(none[L]).flatMap { ref =>
      F.bracketCase(acquire.run) { la =>
        WriterT(la.pure[F]).flatMap(use).run
      } { case ((_, a), ec) =>
        val r = release(a, ec).run
        if (ec == ExitCase.Completed)
          r flatMap { case (l, _) => ref.set(l.some) }
        else
          r.void
      }.flatMap { lb =>
        ref.get.map(_.fold(lb)(l => lb.leftMap(_ |+| l)))
      }
    }
  )

@catostrophe
Copy link
Contributor

Oh, and Option[L] can be replaced with L here, as @vpavkin noticed. I don't compare it with empty.

Something like:

import cats.instances.tuple._

def bracketCase[A, B](acquire: WriterT[F, L, A])
  (use: A => WriterT[F, L, B])
  (release: (A, ExitCase[Throwable]) => WriterT[F, L, Unit]): WriterT[F, L, B] =
  WriterT(
    Ref[F].of(L.empty).flatMap { ref =>
      F.bracketCase(acquire.run) { la =>
        WriterT(la.pure[F]).flatMap(use).run
      } { case ((_, a), ec) =>
        val r = release(a, ec).run
        if (ec == ExitCase.Completed)
          r flatMap { case (l, _) => ref.set(l) }
        else
          r.void
      }.flatMap { lb =>
        ref.get.map(l => lb.leftMap(_ |+| l))
      }
    }
  )

@LukaJCB
Copy link
Member Author

LukaJCB commented Nov 19, 2018

CI seems to be happy and the release part still propogates, thanks so much @catostrophe!

@alexandru
Copy link
Member

🎉

@alexandru alexandru merged commit 9b7dba1 into typelevel:master Nov 22, 2018
@LukaJCB LukaJCB deleted the concurrent-state branch November 22, 2018 08:45
This was referenced Nov 25, 2018
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.

Bracket implementations for all transformers ignore aux effects in release
6 participants