-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
13 changed files
with
308 additions
and
53 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
39 changes: 39 additions & 0 deletions
39
generated-doc/out/adr/0004-channels-safe-unsafe-operations.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
# 4. Channels: safe/unsafe Operations | ||
|
||
Date: 2024-02-28 | ||
|
||
## Context | ||
|
||
Channel operations such as `send`, `receive`, `select`, `close` etc. might fail because a channel is closed. How should | ||
this be signalled to the user? | ||
|
||
## Decision | ||
|
||
We decided to have two variants of the methods: | ||
|
||
* default: `send`, `receive` etc., which throw an exception, when the channel is closed | ||
* safe: `sendSafe`, `receiveSafe` etc., which return a `ChannelClosed` value, when the channel is closed | ||
|
||
The "safe" variants are more performant: no stack trace is created, when the channel is closed. They are used by all | ||
channel combinators (such as `map`, `filter` etc.), to detect and propagate the errors downstream. | ||
|
||
### Why not `Either` or `Try`? | ||
|
||
To avoid allocations on each operation (e.g. receive). Channels might be on the "hot path" and they might be important | ||
for performance. Union types provide a nice alternative here. | ||
|
||
Even with `Either`, though, if e.g. `send` had a signature `Either[ChannelClosed, Unit]`, discarding the result would | ||
at most be a warning (not in all cases), so potentially an error might go unnoticed. | ||
|
||
### Why is the default to throw? | ||
|
||
Let's consider `send`. If the default would be `send(t: T): ChannelClosed | Unit`, with an additional exception-throwing | ||
variant `sendUnsafe(t: T): Unit`, then the API would be quite surprising. | ||
|
||
Coming to the library as a new user, they could just call send / receive. The compiler might warn them in some cases | ||
that they discard the non-unit result of `send`, but (a) would they pay attention to those warnings, and (b) would they | ||
get them in the first place (this type of compiler warning isn't detected in 100% o fcases). | ||
|
||
In other words - it would be quite easy to mistakenly discard the results of `send`, so a default which guards against | ||
that (by throwing exceptions) is better, and the "safe" can always be used intentionally version if that's what's | ||
needed. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
# 5. Application errors | ||
|
||
Date: 2024-03-05 | ||
|
||
## Context | ||
|
||
In some cases, it's useful to treat some return values as errors, which should cause the enclosing scope to end. | ||
|
||
## Decision | ||
|
||
For computation combinators, which include `par`, `race` and `supervised`, we decided to introduce the concept of | ||
application errors. These are values of a shape defined by an `ErrorMode`, which are specially treated by ox - if | ||
such a value represents an error, the enclosing scope ends. | ||
|
||
Some design limitations include: | ||
|
||
* we want normal scopes to remain unchanged | ||
* methods requiring a concurrency scope (that is, `using Ox`) should be callable from the new scope | ||
* all forks that might report application errors, must be constrained to return the same type of application errors | ||
* computation combinators, such as `par`, should have a single implmentation both when using application errors and | ||
exceptions only | ||
|
||
Taking this into account, we separate the `Ox` capability, which allows starting forks, and `OxError`, which | ||
additionally allows reporting application errors. An inheritance hierarchy, `OxError <: Ox` ensures that we can call | ||
methods requiring the `Ox` capability if `OxError` is available, but not the other way round. | ||
|
||
Finally, introducing a special `forkError` method allows us to require that it is run within a `supervisedError` scope | ||
and that it must return a value of the correct shape. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
# Error handling in scopes | ||
|
||
How errors are handled depends on the type of concurrency scope that is used. | ||
|
||
## Supervised scope | ||
|
||
The "default" and recommended scope is created using `supervised`. When this scope is used, any fork created using | ||
`fork` or `forkUser` that fails with an exception, will cause the enclosing scope to end: | ||
|
||
```scala | ||
import ox.{forkUser, supervised} | ||
|
||
supervised { | ||
forkUser { | ||
Thread.sleep(100) | ||
throw new RuntimeException("boom!") | ||
} | ||
forkUser { | ||
// other forks will be interrupted | ||
} | ||
} | ||
// will re-throw the "boom!' exception | ||
``` | ||
|
||
If an unsupervised fork fails (created using `forkUnsupervised` / `forkCancellable`), that exception will be thrown | ||
when invoking `Fork.join`. | ||
|
||
## Supervised scope with application errors | ||
|
||
Additionally, supervised scopes can be created with an error mode, which allows ending the scope when a fork returns | ||
a value that is an [application error](error-handling.md). This can be done by using `supervisedError` and `forkError`, | ||
for example: | ||
|
||
```scala | ||
import ox.{EitherMode, forkUserError, supervisedError} | ||
|
||
supervisedError(EitherMode[Int]) { | ||
forkUserError { Left(10) } | ||
Right(()) | ||
} | ||
// returns Left(10) | ||
``` | ||
|
||
Even though the body of the scope returns success (a `Right`), the scope ends with an application error (a `Left`), | ||
which is reported by a user fork. Note that if we used a daemon fork, the scope might have ended before the error | ||
was reported. | ||
|
||
Only forks created with `forkError` and `forkUserError` can report application errors, and they **must** return a value | ||
of the shape as described by the error mode (in the example above, all `forkError`, `forkUserError` and the scope body | ||
must return an `Either[Int, T]` for arbitrary `T`s). | ||
|
||
The behavior of `fork` and `forkUser` in `supervisedError` scopes is unchanged, that is, their return values are not | ||
inspected. | ||
|
||
## Unsupervised scopes | ||
|
||
In an unsupervised scope (created using `scoped`), failures of the forks won't be reported in any way, unless they | ||
are explicitly joined. Hence, if there's no `Fork.join`, the exception might go unnoticed. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
# General approach to error handling | ||
|
||
The primary error signalling mechanism in ox are exceptions. They are appropriately handled by computation combinators, | ||
such as [`par`](par.md), [`race`](race.md), as well as by [scopes](fork-join.md) and [channels](channels/index.md). | ||
|
||
The general rule for computation combinators is that using them should throw exactly the same exceptions, as if the | ||
provided code was executed directly. That is, no additional exceptions might be thrown, and no exceptions are swallowed. | ||
The only difference is that some exceptions might be added as suppressed (e.g. interrupted exceptions). | ||
|
||
Some examples of exception handling in ox include: | ||
|
||
* short-circuiting in `par` and `race` when one of the computations fails | ||
* retrying computations in `retry` when they fail | ||
* ending a `supervised` concurrency scope when a supervised fork fails | ||
|
||
## Application errors | ||
|
||
Some of the functionalities provided by ox also support application-level errors. Such errors are represented as values, | ||
e.g. the left side of an `Either[MyError, MyResult]`. They are not thrown, but returned from the computations which | ||
are orchestrated by ox. | ||
|
||
Ox must be made aware of how such application errors are represented. This is done through an `ErrorMode`. Provided | ||
implementations include `EitherMode[E]` (where left sides of `Either`s are used to represent errors), and | ||
`UnionMode[E]`, where a union type of `E` and a successful value is used. Arbitrary user-provided implementations | ||
are possible as well. | ||
|
||
Error modes can be used in [`supervisedError`](error-handling-scopes.md) scopes, as well as in variants of the `par` | ||
and `race` methods. | ||
|
||
```eval_rst | ||
.. note:: | ||
Using application errors allows specifying the possible errors in the type signatures of the methods, and is hence | ||
more type-safe. If used consistently, exceptions might be avoided altogether, except for signalling bugs in the code. | ||
However, representing errors as values might incur a syntax overhead, and might be less convenient in some cases. | ||
Moreover, all I/O libraries typically throw exceptions - to use them with errors-as-values, one would need to provide | ||
a wrapper which would convert such exceptions to values. Hence, while application errors provide a lot of benefits, | ||
they are not a universal solution to error handling. | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
# Extension methods | ||
|
||
Extension-method syntax can be imported using `import ox.syntax.*`. This allows calling methods such as | ||
`.fork`, `.raceSuccessWith`, `.parWith`, `.forever`, `.useInScope` directly on code blocks / values. | ||
`.fork`, `.raceWith`, `.parWith`, `.forever`, `.useInScope` directly on code blocks / values. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.