This is the seventh major release in the Cats Effect 2.x lineage. It is fully binary compatible with all 2.x.y releases. As previously noted, this is the first Cats Effect release to be exclusively published for ScalaJS 1.x; there are no 0.6.x cross-releases.
This is a huge release packed full with a large number of new features (most visibly, high-performance tracing for the IO
monad!) and a massive number of bugfixes. It's impossible to distill this immense effort down to just a few small thank-yous and change descriptions, but I still want to make special mention of @RaasAhsan, who worked tirelessly to bring rich, asynchronous tracing to IO
, on top of a host of other major aspects of this release.
Notable New Features
Asynchronous Tracing
IO
tracing is probably the single most-requested Cats Effect feature, and it's finally here! IO
now has a basic, high-performance asynchronous tracing mechanism. Note that this mechanism has very different internals from the async tracing already available in Akka and ZIO, and thus comes with a different set of tradeoffs. Also please remember that no async tracing on the JVM is perfectly accurate, and you may see misleading trace frames depending on your program. Despite all this, the added information is very welcome and sometimes quite helpful in tracking down issues!
This is what it looks like:
IOTrace: 19 frames captured
├ flatMap @ org.simpleapp.examples.Main$.program (Main.scala:53)
├ map @ org.simpleapp.examples.Main$.foo (Main.scala:46)
├ flatMap @ org.simpleapp.examples.Main$.foo (Main.scala:45)
├ flatMap @ org.simpleapp.examples.Main$.foo (Main.scala:44)
├ flatMap @ org.simpleapp.examples.Main$.foo (Main.scala:43)
├ flatMap @ org.simpleapp.examples.Main$.foo (Main.scala:42)
├ flatMap @ org.simpleapp.examples.Main$.foo (Main.scala:41)
├ flatMap @ org.simpleapp.examples.Main$.foo (Main.scala:40)
├ flatMap @ org.simpleapp.examples.Main$.foo (Main.scala:39)
├ flatMap @ org.simpleapp.examples.Main$.foo (Main.scala:38)
├ flatMap @ org.simpleapp.examples.Main$.foo (Main.scala:37)
├ flatMap @ org.simpleapp.examples.Main$.foo (Main.scala:36)
├ flatMap @ org.simpleapp.examples.Main$.foo (Main.scala:35)
├ flatMap @ org.simpleapp.examples.Main$.foo (Main.scala:34)
├ flatMap @ org.simpleapp.examples.Main$.foo (Main.scala:33)
├ flatMap @ org.simpleapp.examples.Main$.foo (Main.scala:32)
├ flatMap @ org.simpleapp.examples.Main$.program (Main.scala:53)
╰ ... (3 frames omitted)
The number of actions to retain in the trace can be configured by the system property cats.effect.traceBufferLogSize
, which defaults to 4 (representing a default buffer size of 16 frames). Backtraces like the above can be accessed using the IO.traced
function. In addition to this, and (in practice) even more usefully, we have added support for enriched exceptions. As an example:
java.lang.Throwable: A runtime exception has occurred
at org.simpleapp.examples.Main$.b(Main.scala:28)
at org.simpleapp.examples.Main$.a(Main.scala:25)
at org.simpleapp.examples.Main$.$anonfun$foo$11(Main.scala:37)
at map @ org.simpleapp.examples.Main$.$anonfun$foo$10(Main.scala:37)
at flatMap @ org.simpleapp.examples.Main$.$anonfun$foo$8(Main.scala:36)
at flatMap @ org.simpleapp.examples.Main$.$anonfun$foo$6(Main.scala:35)
at flatMap @ org.simpleapp.examples.Main$.$anonfun$foo$4(Main.scala:34)
at flatMap @ org.simpleapp.examples.Main$.$anonfun$foo$2(Main.scala:33)
at flatMap @ org.simpleapp.examples.Main$.foo(Main.scala:32)
at flatMap @ org.simpleapp.examples.Main$.program(Main.scala:42)
at as @ org.simpleapp.examples.Main$.run(Main.scala:48)
at main$ @ org.simpleapp.examples.Main$.main(Main.scala:22)
Any time an exception is caught within the IO
mechanism (either via automatic try
/catch
, or via raiseError
), the stack trace is inspected to determine whether or not a prefix of that stack trace is simply implementation details of the IO
runloop itself. If that is the case, those frames are removed from the stack and replaced with a synthetic stack trace based on the asynchronous tracing information, as shown above.
Note that this feature may interact incorrectly with some poorly-written parsing logic. For example, a log parser tool which uses regular expressions to extract stack trace information, where those regular expressions are intolerant of spaces. If you wish to disable enhanced exceptions specifically without disabling tracing, simply set the cats.effect.enhancedExceptions
system property to false
.
It's important to understand that this feature is very new and we have a lot of things we want to improve about it! We need user feedback to guide this process. Please file issues and/or chat us up in Gitter if you have ideas, and especially if you see problems!
Tracing has three modes:
- Disabled
- Cached (the default)
- Full
These are all governed by the cats.effect.stackTracingMode
system property and are global to the JVM. As a rough performance reference, at present, Cached tracing imposes a roughly 10% performance penalty in synthetic benchmarks on asynchronous IO
s. This difference should be entirely impossible to observe outside of microbenchmarks (though we would love to hear otherwise if you see evidence to the contrary!). Full tracing imposes an exceptionally high performance cost, and is expected to be used only in development environments when specifically attempting to track down bugs. Disabled tracing imposes an extremely negligible penalty, and should be used in production if Cached tracing imposes a noticeable performance hit. It is recommended that you stick with the default tracing mode in most cases.
Cached tracing produces a single linear trace of the actions an IO
program takes. This tracing mode uses heuristics to determine call-site information on functions like flatMap
and async
, and those heuristics can be misleading, particularly when used with monad transformers or types like Resource
or Stream
. If you have ideas for how to improve these heuristics, please let us know!
Full tracing captures a full JVM stack-trace for every call into IO
, which results in an extremely comprehensive picture of your asynchronous control flow. This imposes a significant performance cost, but it makes it possible to see through complex compositions such as monad transformers or third-party code. This is an appropriate mode for development when the heuristics which generate a Cached trace are insufficient or misleading.
We want your feedback! There are so many different things that we can do with this functionality, and we want your opinions on how it should work and how it can be improved. We have many more features coming, but please file issues, talk to us, try it out, tweet angrily, you know the drill.
Bracket
Forwarders
This is a rather subtle issue, but on 2.1.x (and earlier), it is impossible to write something like the following:
def foo[F[_]: Sync] =
Bracket[OptionT[F, *], Throwable].bracket(...)
The Bracket
instance would fail to resolve, despite the fact that there's a Sync
in scope, since Bracket
cannot be inductively defined (see #860 for some discussion of the issues surrounding inductive Bracket
instances). This change adds implicit forwarders to the Bracket
companion object so that the inductive Sync
instances, defined on the Sync
companion object, could be resolved via the Bracket
companion. Basically, this just moves the Sync
instances into a scope wherein they can be addressed by implicit search starting from either Sync
or Bracket
.
There is a very small chance this will cause implicit ambiguities in some code bases. We are not aware of any specific cases where this can happen, but it's worth keeping in mind as you upgrade.
Performance Improvements in Deferred
The Deferred
primitive is one of the most powerful and most-used utilities in the Cats Effect library outside of IO
itself. It is, for example, the foundation of most of fs2's concurrency mechanisms. This release contains significant performance improvements within this abstraction, with a particular focus on lowering memory pressure. This should result in a very significant, user-visible jump in performance across most real-world applications.
User-Facing Pull Requests
- #1133 - Add
onFinalize
/onFinalizeCase
methods toResource
(@bplommer) - #1163 - Update cats-core, cats-laws to 2.2.0 (@scala-steward)
- #1131 - Further (significant!) optimizations in
Deferred
andMVar
(@SystemFw) - #1080 - Add
Clock
extension method for creatingjava.time.Instant
(@jbwheatley) - #1103 - Add
IO
.whenA
and related convenience methods (@Daenyth) - #1060, #1030 - Change
SemigroupK
instance forResource
to lift behaviour from underlying effect (@bplommer, @BalmungSan) - #1077 - Enhanced exception stack traces with async trace information (@RaasAhsan)
- #1034 - Fixed
StackOverflowError
inbracket
(@RaasAhsan) - #1006 - Fix
Semaphore#tryAcquireN
when acquiring exactly all available permits (@RaasAhsan) - #938 - Added
IO#map2Eval
(@johnynek) - #912 - Added some additional functions to
MVar2
(@vasilmkd) - #981 - Improved semantic specificity of tracing on combinators (e.g.
*>
) (@RaasAhsan) - #854 Fiber tracing: asynchronous stack tracing (@RaasAhsan)
- #918 Improved performance of
Deferred
andMVarConcurrent
(@RaasAhsan) - #847 Added
Ref#modifyOr
andRef#updateOr
(@RaasAhsan) - #886 Improved performance of
SyncIO
(and certainIO
cases) by 20-30% (@RaasAhsan) - #827 Add
Ref.lens
constructor (@jwojnowski) - #849 Workaround for scalac bugs when using
Resource
withF[+_]
(@djspiewak) - #739 Added
MVar#tryRead
(@kubum) - #814 Add implicit proxies for transformers in
Bracket
(@ybasket) - #844 Fix resolution of
Clock
implicits (e.g.Clock[Resource[IO, *]]
) (@joroKr21) - #846 Optimize
redeem
/redeemWith
in typeclass instances (@alexandru) - #1160, #1130, #1023, #930, #915, #906, #872, #884, #864, #851, #833, #839 - Documentation updates (@d-ogxwx, @Avasil, @RaasAhsan, @cb372, @bplommer, @december32, @ronanM, @justinhj, @jgogstad, @azolotko, @arosien, @Slakah)
Special thanks to each and every one of you!