Skip to content

Commit

Permalink
Release 0.0.26
Browse files Browse the repository at this point in the history
  • Loading branch information
adamw committed Apr 17, 2024
1 parent 5398c6d commit d8565b1
Show file tree
Hide file tree
Showing 12 changed files with 81 additions and 34 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ the project!
To test ox, use the following dependency, using either [sbt](https://www.scala-sbt.org):

```scala
"com.softwaremill.ox" %% "core" % "0.0.25"
"com.softwaremill.ox" %% "core" % "0.0.26"
```

Or [scala-cli](https://scala-cli.virtuslab.org):

```scala
//> using dep "com.softwaremill.ox::core:0.0.25"
//> using dep "com.softwaremill.ox::core:0.0.26"
```

Documentation is available at [https://ox.softwaremill.com](https://ox.softwaremill.com), ScalaDocs can be browsed at [https://javadoc.io](https://www.javadoc.io/doc/com.softwaremill.ox).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,12 @@ 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).
get them in the first place (this type of compiler warning isn't detected in 100% o cases).

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.

### Update 17/04/2024

The `...Safe` operations got renamed to `...OrClosed` or `...OrError`, as they can still throw `InterruptedException`s.
2 changes: 1 addition & 1 deletion generated-doc/out/adr/0006-actors.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 5. Application errors
# 6. Actors

Date: 2024-03-26

Expand Down
27 changes: 27 additions & 0 deletions generated-doc/out/adr/0007-supervised-unsupervised-scopes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# 7. Supervised & unsupervised scopes

Date: 2024-04-17

## Context

Originally, ox had only `scoped` which created non-supervised scopes, that is errors were only discovered via explicit
joining. This was later changed by introducing `supervised` and `unsupervised` scopes, where the former would end
immediately when any fork failed, and the latter would not. However, `supervised` scopes have an overhead: they create
an additional fork, in which the scope's main body is run. Is it possible to avoid this extra fork?

## Decision

In short: no.

An alternate design would be to store the thread, that created the scope as part of the supervisor, and when any
exception occurs, interrupt that thread so that it would discover the exception. However, this would be prone to
interruption-races with external interruptions of that main thread. Even if we included an additional flag, specifying
if the interruption happened because the scope ends, it would still be possible for an external interrupt to go
unnoticed (if it happened at the same time, as the internal one). Even though unlikely, such a design would be fragile,
hence we are keeping the current implementation.

## Consequences

To make our design more type-safe, we split the `Ox` capability into `OxPlain` (allowing only unsupervised forks), and
`Ox`. The `plain` nomeclature was chosen to indicate that the scope is unsupervised, however `forkPlain` is much shorter
than e.g. `forkUnsupervised`. Hence introducing a new name seems justified.
2 changes: 1 addition & 1 deletion generated-doc/out/channels/channel-closed.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@ object ChannelClosed:
That is, the result of a `safe` operation might be a value, or information that the channel is closed.

Using extensions methods from `ChannelClosedUnion` it's possible to convert such union types to `Either`s, `Try`s or
exceptions, as well as `map` over such resutls.
exceptions, as well as `map` over such results.
16 changes: 12 additions & 4 deletions generated-doc/out/dictionary.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,30 @@ How we use various terms throughout the codebase and the documentation (or at le

Scopes:
* **concurrency scope**: either `supervised` (default), `supervisedError` (permitting application errors),
or `scoped` ("advanced")
* scope **body**: the code block passed to a concurrency scope (the `supervised` or `scoped` method)
or `unsupervised`
* scope **body**: the code block passed to a concurrency scope (the `supervised`, `supervisedError` or `unsupervised`
method)

Types of forks:
* supervised / plain (unsupervised)
* daemon / user
* optionally, recognizing application errors

Fork lifecycle:
* within scopes, asynchronously running **forks** can be **started**
* after being started a fork is **running**
* then, forks **complete**: either a fork **succeeds** with a value, or a fork **fails** with an exception
* external **cancellation** (`Fork.cancel()`) interrupts the fork and waits until it completes; interruption uses
JVM's mechanism of injecting an `InterruptedException`
* forks are **supervised** if they are run in a `supervised` scope, and not unsupervised (started using `forkPlain` or
`forkCancellable`)

Scope lifecycle:
* a scope **ends**: when unsupervised, the scope's body is entirely evaluated; when supervised, all user (non-daemon) &
supervised forks complete successfully, or at least one user/daemon supervised fork fails, or an application error
is reported. When the scope ends, all forks that are still running are cancelled
* scope **completes**, once all forks complete and finalizers are run; then, the `supervised` or `scoped`
method returns.
* scope **completes**, once all forks complete and finalizers are run; then, the `supervised`, `supervisedErro` or
`unsupervised` method returns.

Errors:
* fork **failure**: when a fork fails with an exception
Expand Down
2 changes: 1 addition & 1 deletion generated-doc/out/error-handling-scopes.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,5 @@ inspected.

## Unsupervised scopes

In an unsupervised scope (created using `scoped`), failures of the forks won't be reported in any way, unless they
In an unsupervised scope (created using `unsupervised`), 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.
4 changes: 0 additions & 4 deletions generated-doc/out/extension.md

This file was deleted.

43 changes: 28 additions & 15 deletions generated-doc/out/fork-join.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ It's safest to use higher-level methods, such as `par` or `race`, however this i
these cases, threads can be started using the structured concurrency APIs described below.

Forks (new threads) can only be started within a **concurrency scope**. Such a scope is defined using the `supervised`,
`supervisedError` or `scoped` methods.
`supervisedError` or `unsupervised` methods.

The lifetime of the forks is defined by the structure of the code, and corresponds to the enclosing `supervised` or
`scoped` block. Once the code block passed to the scope completes, any forks that are still running are interrupted.
The whole block will complete only once all forks have completed (successfully, or with an exception).
The lifetime of the forks is defined by the structure of the code, and corresponds to the enclosing `supervised`,
`supervisedError` or `unsupervised` block. Once the code block passed to the scope completes, any forks that are still
running are interrupted. The whole block will complete only once all forks have completed (successfully, or with an
exception).

Hence, it is guaranteed that all forks started within `supervised` or `scoped` will finish successfully, with an
exception, or due to an interrupt.
Hence, it is guaranteed that all forks started within `supervised`, `supervisedError` or `unsupervised` will finish
successfully, with an exception, or due to an interrupt.

For example, the code below is equivalent to `par`:

Expand All @@ -34,7 +35,7 @@ supervised {
}
```

It is a compile-time error to use `fork`/`forkUser` outside of a `supervised` or `scoped` block. Helper methods might
It is a compile-time error to use any of the `fork` methods outside of a `supervised` block. Helper methods might
require to be run within a scope by requiring the `Ox` capability:

```scala
Expand All @@ -55,10 +56,19 @@ supervised {

Scopes can be arbitrarily nested.

## Types of forks - summary

* `fork`: supervised, daemon fork
* `forkUser`: supervised, user fork (scope will wait for it to complete, if there are no other errors)
* `forkError`: supervised, daemon fork, which is allowed to fail with an application error
* `forkUserError`: supervised, user fork, which is allowed to fail with an application error
* `forkPlain`: unsupervised fork
* `forkCancellable`: unsupervised, cancellable fork

## Supervision

The default scope, created with `supervised`, watches over the forks that are started within. Any forks started with
`fork` and `forkUser` are by default supervised.
`fork`, `forkUser`, `forkError` and `forUserError` are by default supervised.

This means that the scope will end only when either:

Expand Down Expand Up @@ -89,25 +99,28 @@ supervised {

## User, daemon and unsupervised forks

In supervised scopes, forks created using `fork` behave as daemon threads. That is, their failure ends the scope, but
the scope will also end once the body and all user forks succeed, regardless if the (daemon) fork is still running.
Forks created using `fork` behave as daemon threads. That is, their failure ends the scope, but the scope will also end
once the body and all user forks succeed, regardless if the (daemon) fork is still running.

Alternatively, a user fork can be created using `forkUser`. Such a fork is required to complete successfully, in order
for the scope to end successfully. Hence, when the body of the scope completes, the scope will wait until all user
forks have completed as well.

Finally, entirely unsupervised forks can be started using `forkUnsupervised`.
Finally, entirely unsupervised forks can be started using `forkPlain`.

## Unsupervised scopes

An unsupervised scope can be created using `scoped`. Any forks started within are unsupervised. This is considered an
advanced feature, and should be used with caution.
An unsupervised scope can be created using `unsupervised`. Within such scopes, only `forkPlain` and `forkCancellable`
forks can be started.

Such a scope ends, once the code block passed to `scoped` completes. Then, all running forks are cancelled. Still, the
scope completes (that is, the `scoped` block returns) only once all forks have completed.
Such a scope ends, once the code block passed to `unsupervised` completes. Then, all running forks are cancelled. Still,
the scope completes (that is, the `unsupervised` block returns) only once all forks have completed.

Fork failures aren't handled in any special way, and can be inspected using the `Fork.join()` method.

For helper method, the capability that needs to be passed is `OxPlain`, a subtype of `Ox` that only allows starting
unsupervised forks.

## Cancelling forks

By default, forks are not cancellable by the user. Instead, all outstanding forks are cancelled (interrupted) when the
Expand Down
2 changes: 1 addition & 1 deletion generated-doc/out/fork-local.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ will use the provided instance, not the outer one. E.g.:
```scala
def withSpan[T](spanName: String)(f: Ox ?=> T): T =
val span = spanBuilder.startSpan(spanName)
currentSpan.scopedWhere(Some(span)) {
currentSpan.supervisedWhere(Some(span)) {
try f
finally span.end()
}
Expand Down
5 changes: 2 additions & 3 deletions generated-doc/out/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ In addition to this documentation, ScalaDocs can be browsed at [https://javadoc.

```scala
// sbt dependency
"com.softwaremill.ox" %% "core" % "0.0.25"
"com.softwaremill.ox" %% "core" % "0.0.26"

// scala-cli dependency
//> using dep "com.softwaremill.ox::core:0.0.25"
//> using dep "com.softwaremill.ox::core:0.0.26"
```

## Scope of the project
Expand Down Expand Up @@ -87,7 +87,6 @@ We offer commercial support for ox and related technologies, as well as developm
resources
control-flow
utility
extension
dictionary
performance
Expand Down
2 changes: 1 addition & 1 deletion generated-doc/out/kafka.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Dependency:

```scala
"com.softwaremill.ox" %% "kafka" % "0.0.25"
"com.softwaremill.ox" %% "kafka" % "0.0.26"
```

`Source`s which read from a Kafka topic, mapping stages and drains which publish to Kafka topics are available through
Expand Down

0 comments on commit d8565b1

Please sign in to comment.