Skip to content

Commit 1adf368

Browse files
authored
Merge branch 'series/3.x' into topic/thread-local-iolocal
2 parents af84973 + ccae9c7 commit 1adf368

File tree

21 files changed

+416
-96
lines changed

21 files changed

+416
-96
lines changed

README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@
1111

1212
## Getting Started
1313

14-
- Wired: **3.5.5**
14+
- Wired: **3.5.6**
1515
- Tired: **2.5.5** (end of life)
1616

1717
```scala
18-
libraryDependencies += "org.typelevel" %% "cats-effect" % "3.5.5"
18+
libraryDependencies += "org.typelevel" %% "cats-effect" % "3.5.6"
1919
```
2020

2121
The above represents the core, stable dependency which brings in the entirety of Cats Effect. This is *most likely* what you want. All current Cats Effect releases are published for Scala 2.12, 2.13, 3.2, and Scala.js 1.13.
@@ -30,22 +30,22 @@ Depending on your use-case, you may want to consider one of the several other mo
3030

3131
```scala
3232
libraryDependencies ++= Seq(
33-
"org.typelevel" %% "cats-effect-kernel" % "3.5.5",
34-
"org.typelevel" %% "cats-effect-laws" % "3.5.5" % Test)
33+
"org.typelevel" %% "cats-effect-kernel" % "3.5.6",
34+
"org.typelevel" %% "cats-effect-laws" % "3.5.6" % Test)
3535
```
3636

3737
If you're a middleware framework (like [Fs2](https://fs2.io/)), you probably want to depend on **std**, which gives you access to `Queue`, `Semaphore`, and much more without introducing a hard-dependency on `IO` outside of your tests:
3838

3939
```scala
4040
libraryDependencies ++= Seq(
41-
"org.typelevel" %% "cats-effect-std" % "3.5.5",
42-
"org.typelevel" %% "cats-effect" % "3.5.5" % Test)
41+
"org.typelevel" %% "cats-effect-std" % "3.5.6",
42+
"org.typelevel" %% "cats-effect" % "3.5.6" % Test)
4343
```
4444

4545
You may also find some utility in the **testkit** and **kernel-testkit** projects, which contain `TestContext`, generators for `IO`, and a few other things:
4646

4747
```scala
48-
libraryDependencies += "org.typelevel" %% "cats-effect-testkit" % "3.5.5" % Test
48+
libraryDependencies += "org.typelevel" %% "cats-effect-testkit" % "3.5.6" % Test
4949
```
5050

5151
Cats Effect provides backward binary compatibility within the 2.x and 3.x version lines, and both forward and backward compatibility within any major/minor line. This is analogous to the versioning scheme used by Cats itself, as well as other major projects such as Scala.js. Thus, any project depending upon Cats Effect 2.2.1 can be used with libraries compiled against Cats Effect 2.0.0 or 2.2.3, but *not* with libraries compiled against 2.3.0 or higher.

core/shared/src/main/scala/cats/effect/IO.scala

Lines changed: 50 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import cats.data.Ior
4040
import cats.effect.instances.spawn
4141
import cats.effect.kernel.CancelScope
4242
import cats.effect.kernel.GenTemporal.handleDuration
43-
import cats.effect.std.{Backpressure, Console, Env, Supervisor, UUIDGen}
43+
import cats.effect.std.{Backpressure, Console, Env, Supervisor, SystemProperties, UUIDGen}
4444
import cats.effect.tracing.{Tracing, TracingEvent}
4545
import cats.effect.unsafe.IORuntime
4646
import cats.syntax._
@@ -786,17 +786,27 @@ sealed abstract class IO[+A] private () extends IOPlatform[A] {
786786
andWait(duration: Duration)
787787

788788
/**
789-
* Returns an IO that either completes with the result of the source within the specified time
790-
* `duration` or otherwise raises a `TimeoutException`.
789+
* Returns an IO that either completes with the result of the source or otherwise raises a
790+
* `TimeoutException`.
791791
*
792-
* The source is canceled in the event that it takes longer than the specified time duration
793-
* to complete. Once the source has been successfully canceled (and has completed its
794-
* finalizers), the `TimeoutException` will be raised. If the source is uncancelable, the
795-
* resulting effect will wait for it to complete before raising the exception.
792+
* The source is raced against the timeout `duration`, and its cancelation is triggered if the
793+
* source doesn't complete within the specified time. The resulting effect will always wait
794+
* for the source effect to complete (and to complete its finalizers), and will return the
795+
* source's outcome over raising a `TimeoutException`.
796+
*
797+
* In case source and timeout complete simultaneously, the result of the source will be
798+
* returned over raising a `TimeoutException`.
799+
*
800+
* If the source effect is uncancelable, a `TimeoutException` will never be raised.
796801
*
797802
* @param duration
798-
* is the time span for which we wait for the source to complete; in the event that the
799-
* specified time has passed without the source completing, a `TimeoutException` is raised
803+
* is the time span for which we wait for the source to complete before triggering its
804+
* cancelation; in the event that the specified time has passed without the source
805+
* completing, a `TimeoutException` is raised
806+
*
807+
* @see
808+
* [[timeoutAndForget]] for a variant which does not wait for cancelation of the source
809+
* effect to complete.
800810
*/
801811
def timeout[A2 >: A](duration: Duration): IO[A2] =
802812
handleDuration(duration, this) { finiteDuration =>
@@ -809,26 +819,35 @@ sealed abstract class IO[+A] private () extends IOPlatform[A] {
809819
timeout(duration: Duration)
810820

811821
/**
812-
* Returns an IO that either completes with the result of the source within the specified time
813-
* `duration` or otherwise evaluates the `fallback`.
822+
* Returns an IO that either completes with the result of the source or otherwise evaluates
823+
* the `fallback`.
814824
*
815-
* The source is canceled in the event that it takes longer than the specified time duration
816-
* to complete. Once the source has been successfully canceled (and has completed its
817-
* finalizers), the fallback will be sequenced. If the source is uncancelable, the resulting
818-
* effect will wait for it to complete before evaluating the fallback.
825+
* The source is raised against the timeout `duration`, and its cancelation is triggered if
826+
* the source doesn't complete within the specified time. The resulting effect will always
827+
* wait for the source effect to complete (and to complete its finalizers), and will return
828+
* the source's outcome over sequencing the `fallback`.
829+
*
830+
* In case source and timeout complete simultaneously, the result of the source will be
831+
* returned over sequencing the `fallback`.
832+
*
833+
* If the source in uncancelable, `fallback` will never be evaluated.
819834
*
820835
* @param duration
821-
* is the time span for which we wait for the source to complete; in the event that the
822-
* specified time has passed without the source completing, the `fallback` gets evaluated
836+
* is the time span for which we wait for the source to complete before triggering its
837+
* cancelation; in the event that the specified time has passed without the source
838+
* completing, the `fallback` gets evaluated
823839
*
824840
* @param fallback
825841
* is the task evaluated after the duration has passed and the source canceled
826842
*/
827843
def timeoutTo[A2 >: A](duration: Duration, fallback: IO[A2]): IO[A2] = {
828844
handleDuration[IO[A2]](duration, this) { finiteDuration =>
829-
race(IO.sleep(finiteDuration)).flatMap {
830-
case Right(_) => fallback
831-
case Left(value) => IO.pure(value)
845+
IO.uncancelable { poll =>
846+
poll(racePair(IO.sleep(finiteDuration))) flatMap {
847+
case Left((oc, f)) => f.cancel *> oc.embed(poll(IO.canceled) *> IO.never)
848+
case Right((f, _)) =>
849+
f.cancel *> f.join.flatMap { oc => oc.fold(fallback, IO.raiseError, identity) }
850+
}
832851
}
833852
}
834853
}
@@ -1098,21 +1117,16 @@ sealed abstract class IO[+A] private () extends IOPlatform[A] {
10981117
val fiber = new IOFiber[A](
10991118
if (IOFiberConstants.ioLocalPropagation) IOLocal.getThreadLocalState()
11001119
else IOLocalState.empty,
1101-
oc =>
1120+
{ oc =>
1121+
if (registerCallback) {
1122+
runtime.fiberErrorCbs.remove(failure)
1123+
}
11021124
oc.fold(
1103-
{
1104-
runtime.fiberErrorCbs.remove(failure)
1105-
canceled
1106-
},
1107-
{ t =>
1108-
runtime.fiberErrorCbs.remove(failure)
1109-
failure(t)
1110-
},
1111-
{ ioa =>
1112-
runtime.fiberErrorCbs.remove(failure)
1113-
success(ioa.asInstanceOf[IO.Pure[A]].value)
1114-
}
1115-
),
1125+
canceled,
1126+
failure,
1127+
{ ioa => success(ioa.asInstanceOf[IO.Pure[A]].value) }
1128+
)
1129+
},
11161130
this,
11171131
runtime.compute,
11181132
runtime
@@ -2164,6 +2178,8 @@ object IO extends IOCompanionPlatform with IOLowPriorityImplicits with TuplePara
21642178

21652179
implicit val envForIO: Env[IO] = Env.make
21662180

2181+
implicit val systemPropertiesForIO: SystemProperties[IO] = SystemProperties.make
2182+
21672183
// This is cached as a val to save allocations, but it uses ops from the Async
21682184
// instance which is also cached as a val, and therefore needs to appear
21692185
// later in the file

docs/core/native-image.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ ThisBuild / scalaVersion := "2.13.8"
3333

3434
lazy val root = (project in file(".")).enablePlugins(NativeImagePlugin).settings(
3535
name := "cats-effect-3-hello-world",
36-
libraryDependencies += "org.typelevel" %% "cats-effect" % "3.5.5",
36+
libraryDependencies += "org.typelevel" %% "cats-effect" % "3.5.6",
3737
Compile / mainClass := Some("com.example.Main"),
3838
nativeImageOptions += "--no-fallback",
3939
nativeImageVersion := "22.1.0" // It should be at least version 21.0.0

docs/core/scala-native.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ lazy val root = project.in(file("."))
2222
.enablePlugins(ScalaNativePlugin)
2323
.settings(
2424
name := "cats-effect-3-hello-world",
25-
libraryDependencies += "org.typelevel" %%% "cats-effect" % "3.5.5",
25+
libraryDependencies += "org.typelevel" %%% "cats-effect" % "3.5.6",
2626
Compile / mainClass := Some("com.example.Main")
2727
)
2828

docs/core/test-runtime.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ For those migrating code from Cats Effect 2, `TestControl` is a considerably mor
2828
In order to use `TestControl`, you will need to bring in the **cats-effect-testkit** dependency:
2929

3030
```scala
31-
libraryDependencies += "org.typelevel" %% "cats-effect-testkit" % "3.5.5" % Test
31+
libraryDependencies += "org.typelevel" %% "cats-effect-testkit" % "3.5.6" % Test
3232
```
3333

3434
## Example

docs/faq.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ title: FAQ
99

1010
```scala-cli
1111
//> using scala "2.13.8"
12-
//> using dep "org.typelevel::cats-effect::3.5.5"
12+
//> using dep "org.typelevel::cats-effect::3.5.6"
1313
1414
import cats.effect._
1515

docs/getting-started.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ title: Getting Started
66
Add the following to your **build.sbt**:
77

88
```scala
9-
libraryDependencies += "org.typelevel" %% "cats-effect" % "3.5.5"
9+
libraryDependencies += "org.typelevel" %% "cats-effect" % "3.5.6"
1010
```
1111

1212
Naturally, if you're using ScalaJS, you should replace the double `%%` with a triple `%%%`. If you're on Scala 2, it is *highly* recommended that you enable the [better-monadic-for](https://github.com/oleg-py/better-monadic-for) plugin, which fixes a number of surprising elements of the `for`-comprehension syntax in the Scala language:
@@ -68,7 +68,7 @@ We will learn more about constructs like `start` and `*>` in later pages, but fo
6868
Of course, the easiest way to play with Cats Effect is to try it out in a Scala REPL. We recommend using [Ammonite](https://ammonite.io/#Ammonite-REPL) for this kind of thing. To get started, run the following lines (if not using Ammonite, skip the first line and make sure that Cats Effect and its dependencies are correctly configured on the classpath):
6969

7070
```scala
71-
import $ivy.`org.typelevel::cats-effect:3.5.5`
71+
import $ivy.`org.typelevel::cats-effect:3.5.6`
7272

7373
import cats.effect.unsafe.implicits._
7474
import cats.effect.IO

docs/migration-guide.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,9 @@ Cats Effect 3 splits the code dependency into multiple modules. If you were prev
8181
The current non-test modules are:
8282

8383
```scala
84-
"org.typelevel" %% "cats-effect-kernel" % "3.5.5",
85-
"org.typelevel" %% "cats-effect-std" % "3.5.5",
86-
"org.typelevel" %% "cats-effect" % "3.5.5",
84+
"org.typelevel" %% "cats-effect-kernel" % "3.5.6",
85+
"org.typelevel" %% "cats-effect-std" % "3.5.6",
86+
"org.typelevel" %% "cats-effect" % "3.5.6",
8787
```
8888

8989
- `kernel` - type class definitions, simple concurrency primitives
@@ -96,7 +96,7 @@ The current non-test modules are:
9696
libraryDependencies ++= Seq(
9797
//...
9898
- "org.typelevel" %% "cats-effect" % "2.4.0",
99-
+ "org.typelevel" %% "cats-effect" % "3.5.5",
99+
+ "org.typelevel" %% "cats-effect" % "3.5.6",
100100
//...
101101
)
102102
```

docs/std/mapref.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ as long as their keys belong to different shards.
3232
This is probably one of the most common uses of this datatype.
3333

3434
```scala mdoc:reset:silent
35-
//> using lib "org.typelevel::cats-effect::3.5.5"
35+
//> using lib "org.typelevel::cats-effect::3.5.6"
3636

3737
import cats.effect.IO
3838
import cats.effect.std.MapRef

docs/std/ref.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ This is probably one of the most common uses of this concurrency primitive.
3333
In this example, the workers will concurrently run and update the value of the `Ref`.
3434

3535
```scala mdoc:reset:silent
36-
//> using lib "org.typelevel::cats-effect::3.5.5"
36+
//> using lib "org.typelevel::cats-effect::3.5.6"
3737

3838
import cats.effect.{IO, IOApp, Sync}
3939
import cats.effect.kernel.Ref

docs/tutorial.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,11 @@ running the code snippets in this tutorial, it is recommended to use the same
4242
```scala
4343
name := "cats-effect-tutorial"
4444

45-
version := "3.5.5"
45+
version := "3.5.6"
4646

4747
scalaVersion := "2.13.13"
4848

49-
libraryDependencies += "org.typelevel" %% "cats-effect" % "3.5.5" withSources() withJavadoc()
49+
libraryDependencies += "org.typelevel" %% "cats-effect" % "3.5.6" withSources() withJavadoc()
5050

5151
scalacOptions ++= Seq(
5252
"-feature",

docs/typeclasses/spawn.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ In English, the semantics of this are as follows:
231231

232232
- If the child fiber completed successfully, produce its result
233233
- If it errored, re-raise the error within the current fiber
234-
- If it canceled, attempt to self-cancel, and if the self-cancelation fails, **deadlock**
234+
- If it canceled, the caller is indefinitely suspended without termination (a.k.a. **deadlock**)
235235

236236
Sometimes this is an appropriate semantic, and the cautiously-verbose `joinWithNever` function implements it for you. It is worth noting that this semantic was the *default* in Cats Effect 2 (and in fact, no other semantic was possible).
237237

kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala

Lines changed: 48 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -72,17 +72,23 @@ trait GenTemporal[F[_], E] extends GenConcurrent[F, E] with Clock[F] {
7272
productL(fa)(sleep(time))
7373

7474
/**
75-
* Returns an effect that either completes with the result of the source within the specified
76-
* time `duration` or otherwise evaluates the `fallback`.
75+
* Returns an effect that either completes with the result of the source or otherwise
76+
* evaluates the `fallback`.
7777
*
78-
* The source is canceled in the event that it takes longer than the specified time duration
79-
* to complete. Once the source has been successfully canceled (and has completed its
80-
* finalizers), the fallback will be sequenced. If the source is uncancelable, the resulting
81-
* effect will wait for it to complete before evaluating the fallback.
78+
* The source is raised against the timeout `duration`, and its cancelation is triggered if
79+
* the source doesn't complete within the specified time. The resulting effect will always
80+
* wait for the source effect to complete (and to complete its finalizers), and will return
81+
* the source's outcome over sequencing the `fallback`.
82+
*
83+
* In case source and timeout complete simultaneously, the result of the source will be
84+
* returned over sequencing the `fallback`.
85+
*
86+
* If the source in uncancelable, `fallback` will never be evaluated.
8287
*
8388
* @param duration
84-
* The time span for which we wait for the source to complete; in the event that the
85-
* specified time has passed without the source completing, the `fallback` gets evaluated
89+
* The time span for which we wait for the source to complete before triggering its
90+
* cancelation; in the event that the specified time has passed without the source
91+
* completing, the `fallback` gets evaluated
8692
*
8793
* @param fallback
8894
* The task evaluated after the duration has passed and the source canceled
@@ -91,33 +97,53 @@ trait GenTemporal[F[_], E] extends GenConcurrent[F, E] with Clock[F] {
9197
handleDuration(duration, fa)(timeoutTo(fa, _, fallback))
9298

9399
protected def timeoutTo[A](fa: F[A], duration: FiniteDuration, fallback: F[A]): F[A] =
94-
flatMap(race(fa, sleep(duration))) {
95-
case Left(a) => pure(a)
96-
case Right(_) => fallback
100+
uncancelable { poll =>
101+
implicit val F: GenTemporal[F, E] = this
102+
103+
poll(racePair(fa, sleep(duration))) flatMap {
104+
case Left((oc, f)) => f.cancel *> oc.embed(poll(F.canceled) *> F.never)
105+
case Right((f, _)) => f.cancel *> f.join.flatMap { oc => oc.embed(fallback) }
106+
}
97107
}
98108

99109
/**
100-
* Returns an effect that either completes with the result of the source within the specified
101-
* time `duration` or otherwise raises a `TimeoutException`.
110+
* Returns an effect that either completes with the result of the source or raises a
111+
* `TimeoutException`.
102112
*
103-
* The source is canceled in the event that it takes longer than the specified time duration
104-
* to complete. Once the source has been successfully canceled (and has completed its
105-
* finalizers), the `TimeoutException` will be raised. If the source is uncancelable, the
106-
* resulting effect will wait for it to complete before raising the exception.
113+
* The source is raced against the timeout `duration`, and its cancelation is triggered if the
114+
* source doesn't complete within the specified time. The resulting effect will always wait
115+
* for the source effect to complete (and to complete its finalizers), and will return the
116+
* source's outcome over raising a `TimeoutException`.
117+
*
118+
* In case source and timeout complete simultaneously, the result of the source will be
119+
* returned over raising a `TimeoutException`.
120+
*
121+
* If the source effect is uncancelable, a `TimeoutException` will never be raised.
107122
*
108123
* @param duration
109-
* The time span for which we wait for the source to complete; in the event that the
110-
* specified time has passed without the source completing, a `TimeoutException` is raised
124+
* The time span for which we wait for the source to complete before triggering its
125+
* cancelation; in the event that the specified time has passed without the source
126+
* completing, a `TimeoutException` is raised
127+
* @see
128+
* [[timeoutAndForget[A](fa:F[A],duration:scala\.concurrent\.duration\.Duration)* timeoutAndForget]]
129+
* for a variant which does not wait for cancelation of the source effect to complete.
111130
*/
112131
def timeout[A](fa: F[A], duration: Duration)(implicit ev: TimeoutException <:< E): F[A] = {
113132
handleDuration(duration, fa)(timeout(fa, _))
114133
}
115134

116135
protected def timeout[A](fa: F[A], duration: FiniteDuration)(
117136
implicit ev: TimeoutException <:< E): F[A] = {
118-
flatMap(race(fa, sleep(duration))) {
119-
case Left(a) => pure(a)
120-
case Right(_) => raiseError[A](ev(new TimeoutException(duration.toString())))
137+
uncancelable { poll =>
138+
implicit val F: GenTemporal[F, E] = this
139+
140+
poll(racePair(fa, sleep(duration))) flatMap {
141+
case Left((oc, f)) => f.cancel *> oc.embed(poll(F.canceled) *> F.never)
142+
case Right((f, _)) =>
143+
f.cancel *> f.join.flatMap { oc =>
144+
oc.embed(raiseError[A](ev(new TimeoutException(duration.toString()))))
145+
}
146+
}
121147
}
122148
}
123149

0 commit comments

Comments
 (0)