Skip to content

Commit 143e4e6

Browse files
committed
Release 0.0.24
1 parent 1d5ef36 commit 143e4e6

File tree

9 files changed

+272
-45
lines changed

9 files changed

+272
-45
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@ the project!
2323
To test ox, use the following dependency, using either [sbt](https://www.scala-sbt.org):
2424

2525
```scala
26-
"com.softwaremill.ox" %% "core" % "0.0.23"
26+
"com.softwaremill.ox" %% "core" % "0.0.24"
2727
```
2828

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

3131
```scala
32-
//> using dep "com.softwaremill.ox::core:0.0.23"
32+
//> using dep "com.softwaremill.ox::core:0.0.24"
3333
```
3434

3535
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).

generated-doc/out/adr/0006-actors.md

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# 5. Application errors
2+
3+
Date: 2024-03-26
4+
5+
## Context
6+
7+
Motivated by the Kafka integration, it's often useful to call methods on an object with guaranteed serialisation of
8+
access, just as it happens in actors, which protect their mutable state.
9+
10+
## Decision
11+
12+
The current implementation of actors is very simple, and allows sending any thunk to be executed given the current
13+
actor's state. This forces the internal state to be mutable. Such an approach was chosen because of its simplicity,
14+
and how well it fits the motivating Kafka use-case, but it might need revisiting once more use-cases arise.
15+
16+
An alternative implementation would force each actor invocation to return the updated actor state, in addition to
17+
the value that should be returned to the caller (if any). However, it's not clear then how to combine this with
18+
the type-safe syntax of invoking actors (or "sending messages" to them). For each method `T.m(args ...): U` that is
19+
accessible via `ActorRef[T]`, the actor itself would need to have a `TA.ma(args ...): S => (U, S)` method, where `S` is
20+
the actor's state. The fact that the `T` and `TA` types "match" in this way could be probably verified using a macro,
21+
but would be harder to implement by users and more complex.
22+
23+
While the idea is that the thunks passed to `ActorRef.ask` and `ActorRef.tell` should invoked a single method on the
24+
actor's interface (similar to "sending a message"), this is not actually verified. As an improvement, these methods
25+
could be changed to a macro that would verify the shape of the lambda passed to them:
26+
27+
```scala
28+
def doAsk[T, U: Type](f: Expr[T => U], c: Expr[Sink[MethodInvocation]])(using Quotes): Expr[U] =
29+
import quotes.reflect.*
30+
'{
31+
val cf = new CompletableFuture[U]()
32+
val onResult = (v: Any) => { val _ = cf.complete(v.asInstanceOf[U]) }
33+
val onException = (e: Throwable) => { val _ = cf.completeExceptionally(e) }
34+
$c.send(${
35+
f.asTerm match {
36+
case Inlined(_, _, Block(List(DefDef(_, _, _, Some(Apply(Select(_, method), parameters)))), _)) =>
37+
'{ MethodInvocation(${ Expr(method) }, ${ Expr.ofList(parameters.map(_.asExpr)) }, onResult, onException) }
38+
case _ => report.errorAndAbort(s"Expected a method call in the form _.someMethod(param1, param2), but got: ${f.show}")
39+
}
40+
})
41+
cf.get()
42+
}
43+
```
44+
45+
Another limitation of this implementation is that it's not possible to schedule messages to self, as using the actor's
46+
`ActorRef` from within the actor's implementation can easily lead to a deadlock (always, if the invocation would be an
47+
`ask`, and with some probability if it would be a `tell` - when the actor's channel would become full).
48+
49+
Finally, error handling might be implemented differently - so that each exception thrown by the actor's methods would
50+
be propagated to the actor's enclosing scope, and would close the actor's channel. While this is the only possibility
51+
in case of `.tell`, as otherwise the exception would go unnoticed, in case of `.ask` only fata exceptions are propagated
52+
this way. Non-fatal ones are propagated to the caller, keeping with the original assumption that using an actor should
53+
be as close as possible to calling the method directly (which would simply propagate the exception).

generated-doc/out/channels/actors.md

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Actors
2+
3+
Actors in ox enable invoking methods on an object serially, keeping the behavior as close as possible to a direct
4+
invocation. That is, even though invocations may happen from multiple threads, they are guaranteed to happen one after
5+
the other, not concurrently.
6+
7+
Actor invocations are fully type-safe, with minimal overhead. They use [channels](index.md) and
8+
[scopes](../fork-join.md) behind the scenes.
9+
10+
One of the use-cases is integrating with external APIs, which are represented by an object containing mutable state.
11+
Such integrations must be protected and cannot be accessed by multiple threads concurrently.
12+
13+
```eval_rst
14+
.. note::
15+
16+
Note that actors as described below are a very basic implementation, covering only some use cases for local
17+
concurrency. In general, actors are especially useful when working in distributedor clustered systems, or when
18+
implementing patterns such as event sourcing. For these use-cases, see the `Pekko <https://pekko.apache.org>`_
19+
project.
20+
```
21+
22+
An actor can be created given any value (representing the actor's state) using `Actor.create`. This creates a fork in
23+
the current concurrency scope, and a channel (using the `StageCapacity` in scope) for scheduling invocations on the
24+
actor's logic.
25+
26+
The result is an `ActorRef`, using which invocations can be scheduled using either the `ask` or `tell` methods.
27+
28+
## Ask
29+
30+
`ask` sends an invocation to the actor and awaits for a result. For example:
31+
32+
```scala
33+
import ox.supervised
34+
import ox.channels.*
35+
36+
class Stateful:
37+
private var counter: Int = 0
38+
def increment(delta: Int): Int =
39+
counter += delta
40+
counter
41+
42+
supervised {
43+
val ref = Actor.create(new Stateful)
44+
45+
ref.ask(_.increment(5)) // blocks until the invocation completes
46+
ref.ask(_.increment(4)) // returns 9
47+
}
48+
```
49+
50+
If a non-fatal exception is thrown by the invocation, it's propagated to the caller, and the actor continues processing
51+
other invocations. Fatal exceptions (e.g. interruptions) are propagated to the enclosing actor's scope, and the actor
52+
closes - trying to create another invocation will throw an exception.
53+
54+
In this approach, actor's internal state usually has to be mutable. For a more functional style, an actor's
55+
implementation can contain a state machine with a single mutable field, containing the current state; each invocation of
56+
an actor's method can then match on the current state, and calculate the next one.
57+
58+
## Tell
59+
60+
It's also possible to schedule an invocation to be processed in the background using `.tell`. This method only blocks
61+
until the invocation can be sent to the actor's channel, but doesn't wait until it's processed.
62+
63+
Note that any exceptions that occur when handling invocations scheduled using `.tell` will be propagated to the actor's
64+
enclosing scope, and will cause the actor to close.
65+
66+
## Close
67+
68+
When creating an actor, it's possible to specify a callback that will be called uninterruptedly before the actor closes.
69+
Such a callback can be used to release any resources held by the actor's logic. It's called when the actor closes, which
70+
includes closing of the enclosing scope:
71+
72+
```scala
73+
import ox.supervised
74+
import ox.channels.*
75+
76+
class Stateful:
77+
def work(howHard: Int): Unit = throw new RuntimeException("boom!")
78+
def close(): Unit = println("Closing")
79+
80+
supervised {
81+
val ref = Actor.create(new Stateful, Some(_.close()))
82+
83+
// fire-and-forget, exception causes the scope to close
84+
ref.tell(_.work(5))
85+
}
86+
```

generated-doc/out/control-flow.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
# Control flow methods
22

3-
There are some helper methods which might be useful when writing forked code:
3+
There are some helper methods which might be useful when writing code using ox's concurrency operators:
44

55
* `forever { ... }` repeatedly evaluates the given code block forever
66
* `repeatWhile { ... }` repeatedly evaluates the given code block, as long as it returns `true`
7+
* `repeatUntil { ... }` repeatedly evaluates the given code block, until it returns `true`
78
* `uninterruptible { ... }` evaluates the given code block making sure it can't be interrupted
9+
* `never` blocks the current thread indefinitely, until it is interrupted
10+
11+
All of these are `inline` methods.

generated-doc/out/fork-local.md

+25
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,28 @@ supervised {
2828
```
2929

3030
Scoped values propagate across nested scopes.
31+
32+
```eval_rst
33+
.. note::
34+
35+
Due to the "structured" nature of setting a fork local's value, forks using external (wider) scopes should not be
36+
created, as an attempt to do so will throw a ``java.util.concurrent.StructureViolationException``.
37+
```
38+
39+
## Creating helper functions which set fork locals
40+
41+
If you're writing a helper function which sets a value of a fork local within a passed code block, you have to make
42+
sure that the code block doesn't accidentally capture the outer concurrency scope (leading to an exception on the
43+
first `fork`).
44+
45+
This can be done by capturing the code block as a context function `Ox ?=> T`, so that any nested invocations of `fork`
46+
will use the provided instance, not the outer one. E.g.:
47+
48+
```scala
49+
def withSpan[T](spanName: String)(f: Ox ?=> T): T =
50+
val span = spanBuilder.startSpan(spanName)
51+
currentSpan.scopedWhere(Some(span)) {
52+
try f
53+
finally span.end()
54+
}
55+
```

generated-doc/out/index.md

+17-2
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@ to get to know ox's API.
77

88
In addition to this documentation, ScalaDocs can be browsed at [https://javadoc.io](https://www.javadoc.io/doc/com.softwaremill.ox).
99

10-
## sbt dependency
10+
## Add to your project
1111

1212
```scala
13-
"com.softwaremill.ox" %% "core" % "0.0.23"
13+
// sbt dependency
14+
"com.softwaremill.ox" %% "core" % "0.0.24"
15+
16+
// scala-cli dependency
17+
//> using dep "com.softwaremill.ox::core:0.0.24"
1418
```
1519

1620
## Scope of the project
@@ -36,6 +40,12 @@ Development and maintenance of ox is sponsored by [SoftwareMill](https://softwar
3640

3741
[![](https://files.softwaremill.com/logo/logo.png "SoftwareMill")](https://softwaremill.com)
3842

43+
## Other projects
44+
45+
The wider goal of direct-style Scala is enabling teams to deliver working software quickly and with confidence. Our
46+
other projects, including [sttp client](https://sttp.softwaremill.com) and [tapir](https://tapir.softwaremill.com),
47+
also include integrations directly tailored towards direct-style.
48+
3949
## Commercial Support
4050

4151
We offer commercial support for ox and related technologies, as well as development services. [Contact us](https://softwaremill.com/contact/) to learn more about our offer!
@@ -44,6 +54,10 @@ We offer commercial support for ox and related technologies, as well as developm
4454

4555
* [Prototype Loom-based concurrency API for Scala](https://softwaremill.com/prototype-loom-based-concurrency-api-for-scala/)
4656
* [Go-like channels using project Loom and Scala](https://softwaremill.com/go-like-channels-using-project-loom-and-scala/)
57+
* [Two types of futures](https://softwaremill.com/two-types-of-futures/)
58+
* [Supervision, Kafka and Java 21: what’s new in Ox](https://softwaremill.com/supervision-kafka-and-java-21-whats-new-in-ox/)
59+
* [Designing a (yet another) retry API](https://softwaremill.com/designing-a-yet-another-retry-api/)
60+
* [Handling errors in direct-style Scala](https://softwaremill.com/handling-errors-in-direct-style-scala/)
4761

4862
## Inspiration & building blocks
4963

@@ -89,6 +103,7 @@ We offer commercial support for ox and related technologies, as well as developm
89103
channels/select
90104
channels/errors
91105
channels/backpressure
106+
channels/actors
92107
93108
.. toctree::
94109
:maxdepth: 2

generated-doc/out/kafka.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Dependency:
44

55
```scala
6-
"com.softwaremill.ox" %% "kafka" % "0.0.23"
6+
"com.softwaremill.ox" %% "kafka" % "0.0.24"
77
```
88

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

generated-doc/out/resources.md

+10-14
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# Resources
22

3-
## In-scope
3+
## Allocate & release
44

5-
Resources can be allocated within a scope. They will be released in reverse acquisition order, after the scope completes
6-
(that is, after all forks started within finish). E.g.:
5+
Resources can be allocated within a concurrency scope. They will be released in reverse acquisition order, after all
6+
forks started within the scope finish (but before the scope completes). E.g.:
77

88
```scala
99
import ox.{supervised, useInScope}
@@ -25,25 +25,21 @@ supervised {
2525
}
2626
```
2727

28-
## Supervised / scoped
28+
## Release-only
2929

30-
Resources can also be used in a dedicated scope:
30+
You can also register resources to be released (without acquisition logic), before the scope completes:
3131

3232
```scala
33-
import ox.useSupervised
33+
import ox.{supervised, releaseAfterScope}
3434

3535
case class MyResource(c: Int)
3636

37-
def acquire(c: Int): MyResource =
38-
println(s"acquiring $c ...")
39-
MyResource(c)
40-
4137
def release(resource: MyResource): Unit =
4238
println(s"releasing ${resource.c} ...")
4339

44-
useSupervised(acquire(10))(release) { resource =>
45-
println(s"Using $resource ...")
40+
supervised {
41+
val resource1 = MyResource(10)
42+
releaseAfterScope(release(resource1))
43+
println(s"Using $resource1 ...")
4644
}
4745
```
48-
49-
If the resource extends `AutoCloseable`, the `release` method doesn't need to be provided.

0 commit comments

Comments
 (0)