Skip to content

Commit

Permalink
Release 0.0.24
Browse files Browse the repository at this point in the history
  • Loading branch information
adamw committed Mar 26, 2024
1 parent 1d5ef36 commit 143e4e6
Show file tree
Hide file tree
Showing 9 changed files with 272 additions and 45 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.23"
"com.softwaremill.ox" %% "core" % "0.0.24"
```

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

```scala
//> using dep "com.softwaremill.ox::core:0.0.23"
//> using dep "com.softwaremill.ox::core:0.0.24"
```

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
53 changes: 53 additions & 0 deletions generated-doc/out/adr/0006-actors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# 5. Application errors

Date: 2024-03-26

## Context

Motivated by the Kafka integration, it's often useful to call methods on an object with guaranteed serialisation of
access, just as it happens in actors, which protect their mutable state.

## Decision

The current implementation of actors is very simple, and allows sending any thunk to be executed given the current
actor's state. This forces the internal state to be mutable. Such an approach was chosen because of its simplicity,
and how well it fits the motivating Kafka use-case, but it might need revisiting once more use-cases arise.

An alternative implementation would force each actor invocation to return the updated actor state, in addition to
the value that should be returned to the caller (if any). However, it's not clear then how to combine this with
the type-safe syntax of invoking actors (or "sending messages" to them). For each method `T.m(args ...): U` that is
accessible via `ActorRef[T]`, the actor itself would need to have a `TA.ma(args ...): S => (U, S)` method, where `S` is
the actor's state. The fact that the `T` and `TA` types "match" in this way could be probably verified using a macro,
but would be harder to implement by users and more complex.

While the idea is that the thunks passed to `ActorRef.ask` and `ActorRef.tell` should invoked a single method on the
actor's interface (similar to "sending a message"), this is not actually verified. As an improvement, these methods
could be changed to a macro that would verify the shape of the lambda passed to them:

```scala
def doAsk[T, U: Type](f: Expr[T => U], c: Expr[Sink[MethodInvocation]])(using Quotes): Expr[U] =
import quotes.reflect.*
'{
val cf = new CompletableFuture[U]()
val onResult = (v: Any) => { val _ = cf.complete(v.asInstanceOf[U]) }
val onException = (e: Throwable) => { val _ = cf.completeExceptionally(e) }
$c.send(${
f.asTerm match {
case Inlined(_, _, Block(List(DefDef(_, _, _, Some(Apply(Select(_, method), parameters)))), _)) =>
'{ MethodInvocation(${ Expr(method) }, ${ Expr.ofList(parameters.map(_.asExpr)) }, onResult, onException) }
case _ => report.errorAndAbort(s"Expected a method call in the form _.someMethod(param1, param2), but got: ${f.show}")
}
})
cf.get()
}
```

Another limitation of this implementation is that it's not possible to schedule messages to self, as using the actor's
`ActorRef` from within the actor's implementation can easily lead to a deadlock (always, if the invocation would be an
`ask`, and with some probability if it would be a `tell` - when the actor's channel would become full).

Finally, error handling might be implemented differently - so that each exception thrown by the actor's methods would
be propagated to the actor's enclosing scope, and would close the actor's channel. While this is the only possibility
in case of `.tell`, as otherwise the exception would go unnoticed, in case of `.ask` only fata exceptions are propagated
this way. Non-fatal ones are propagated to the caller, keeping with the original assumption that using an actor should
be as close as possible to calling the method directly (which would simply propagate the exception).
86 changes: 86 additions & 0 deletions generated-doc/out/channels/actors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Actors

Actors in ox enable invoking methods on an object serially, keeping the behavior as close as possible to a direct
invocation. That is, even though invocations may happen from multiple threads, they are guaranteed to happen one after
the other, not concurrently.

Actor invocations are fully type-safe, with minimal overhead. They use [channels](index.md) and
[scopes](../fork-join.md) behind the scenes.

One of the use-cases is integrating with external APIs, which are represented by an object containing mutable state.
Such integrations must be protected and cannot be accessed by multiple threads concurrently.

```eval_rst
.. note::
Note that actors as described below are a very basic implementation, covering only some use cases for local
concurrency. In general, actors are especially useful when working in distributedor clustered systems, or when
implementing patterns such as event sourcing. For these use-cases, see the `Pekko <https://pekko.apache.org>`_
project.
```

An actor can be created given any value (representing the actor's state) using `Actor.create`. This creates a fork in
the current concurrency scope, and a channel (using the `StageCapacity` in scope) for scheduling invocations on the
actor's logic.

The result is an `ActorRef`, using which invocations can be scheduled using either the `ask` or `tell` methods.

## Ask

`ask` sends an invocation to the actor and awaits for a result. For example:

```scala
import ox.supervised
import ox.channels.*

class Stateful:
private var counter: Int = 0
def increment(delta: Int): Int =
counter += delta
counter

supervised {
val ref = Actor.create(new Stateful)

ref.ask(_.increment(5)) // blocks until the invocation completes
ref.ask(_.increment(4)) // returns 9
}
```

If a non-fatal exception is thrown by the invocation, it's propagated to the caller, and the actor continues processing
other invocations. Fatal exceptions (e.g. interruptions) are propagated to the enclosing actor's scope, and the actor
closes - trying to create another invocation will throw an exception.

In this approach, actor's internal state usually has to be mutable. For a more functional style, an actor's
implementation can contain a state machine with a single mutable field, containing the current state; each invocation of
an actor's method can then match on the current state, and calculate the next one.

## Tell

It's also possible to schedule an invocation to be processed in the background using `.tell`. This method only blocks
until the invocation can be sent to the actor's channel, but doesn't wait until it's processed.

Note that any exceptions that occur when handling invocations scheduled using `.tell` will be propagated to the actor's
enclosing scope, and will cause the actor to close.

## Close

When creating an actor, it's possible to specify a callback that will be called uninterruptedly before the actor closes.
Such a callback can be used to release any resources held by the actor's logic. It's called when the actor closes, which
includes closing of the enclosing scope:

```scala
import ox.supervised
import ox.channels.*

class Stateful:
def work(howHard: Int): Unit = throw new RuntimeException("boom!")
def close(): Unit = println("Closing")

supervised {
val ref = Actor.create(new Stateful, Some(_.close()))

// fire-and-forget, exception causes the scope to close
ref.tell(_.work(5))
}
```
6 changes: 5 additions & 1 deletion generated-doc/out/control-flow.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# Control flow methods

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

* `forever { ... }` repeatedly evaluates the given code block forever
* `repeatWhile { ... }` repeatedly evaluates the given code block, as long as it returns `true`
* `repeatUntil { ... }` repeatedly evaluates the given code block, until it returns `true`
* `uninterruptible { ... }` evaluates the given code block making sure it can't be interrupted
* `never` blocks the current thread indefinitely, until it is interrupted

All of these are `inline` methods.
25 changes: 25 additions & 0 deletions generated-doc/out/fork-local.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,28 @@ supervised {
```

Scoped values propagate across nested scopes.

```eval_rst
.. note::
Due to the "structured" nature of setting a fork local's value, forks using external (wider) scopes should not be
created, as an attempt to do so will throw a ``java.util.concurrent.StructureViolationException``.
```

## Creating helper functions which set fork locals

If you're writing a helper function which sets a value of a fork local within a passed code block, you have to make
sure that the code block doesn't accidentally capture the outer concurrency scope (leading to an exception on the
first `fork`).

This can be done by capturing the code block as a context function `Ox ?=> T`, so that any nested invocations of `fork`
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)) {
try f
finally span.end()
}
```
19 changes: 17 additions & 2 deletions generated-doc/out/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ to get to know ox's API.

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

## sbt dependency
## Add to your project

```scala
"com.softwaremill.ox" %% "core" % "0.0.23"
// sbt dependency
"com.softwaremill.ox" %% "core" % "0.0.24"

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

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

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

## Other projects

The wider goal of direct-style Scala is enabling teams to deliver working software quickly and with confidence. Our
other projects, including [sttp client](https://sttp.softwaremill.com) and [tapir](https://tapir.softwaremill.com),
also include integrations directly tailored towards direct-style.

## Commercial Support

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!
Expand All @@ -44,6 +54,10 @@ We offer commercial support for ox and related technologies, as well as developm

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

## Inspiration & building blocks

Expand Down Expand Up @@ -89,6 +103,7 @@ We offer commercial support for ox and related technologies, as well as developm
channels/select
channels/errors
channels/backpressure
channels/actors
.. toctree::
:maxdepth: 2
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.23"
"com.softwaremill.ox" %% "kafka" % "0.0.24"
```

`Source`s which read from a Kafka topic, mapping stages and drains which publish to Kafka topics are available through
Expand Down
24 changes: 10 additions & 14 deletions generated-doc/out/resources.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Resources

## In-scope
## Allocate & release

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

```scala
import ox.{supervised, useInScope}
Expand All @@ -25,25 +25,21 @@ supervised {
}
```

## Supervised / scoped
## Release-only

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

```scala
import ox.useSupervised
import ox.{supervised, releaseAfterScope}

case class MyResource(c: Int)

def acquire(c: Int): MyResource =
println(s"acquiring $c ...")
MyResource(c)

def release(resource: MyResource): Unit =
println(s"releasing ${resource.c} ...")

useSupervised(acquire(10))(release) { resource =>
println(s"Using $resource ...")
supervised {
val resource1 = MyResource(10)
releaseAfterScope(release(resource1))
println(s"Using $resource1 ...")
}
```

If the resource extends `AutoCloseable`, the `release` method doesn't need to be provided.
Loading

0 comments on commit 143e4e6

Please sign in to comment.