This library implements join patterns in Scala 3, a coordination mechanism for concurrent message-passing programs, first introduced in the join calculus. Join patterns allow to declaratively specify how to react and synchronize distributed computations.
The library offers a convenient and safe method to define and utilize join patterns in Scala 3. It achieves this by leveraging Scala 3's metaprogramming capabilities, extending the language through the use of macros and the reflection API. We use the Scala 3 pattern matching syntax to define join patterns, and the library converts these into an internal representation using the aforementioned techniques.
Additionally, the library uses the Actor model as a practical example to demonstrate the application of join patterns. At present, we are employing a simple homemade actor model implementation.
The source code for the join patterns library is organized as follows:
-
join-actors/core
: Contains the core implementation of the join patterns library.-
join-actors/core/src/main/scala/
: Has the following sub-packages:-
actors
: Contains the prototype actor implementation. -
join_patterns
: Contains the implementation of the join pattern matching algorithm and code generation macros.Matcher.scala
: Contains the matcher trait that is implemented by the different join pattern matching algorithms.BruteForceMatcher.scala
: Contains the brute-force matcher implementation.StatefulTreeMatcher.scala
: Contains the stateful tree-based matcher implementation.
-
examples
: Contains examples of using the join patterns library.
-
-
-
join-actors/benchmarks
: Contains the benchmarking code for the join patterns library.-
join-actors/benchmarks/src/main/scala/
: Has the following files:-
Benchmarks.scala
: Contains the benchmarking code for the join patterns library. -
SmartHouse.scala
: Contains the Smart House example used for benchmarking. -
Size.scala
: Contains the benchmarks for Pattern size without guards benchmarks. -
SizeWithGuards.scala
: Contains the benchmarks for Pattern size with guards benchmarks.
-
-
join-actors/benchmarks/data
: Contains the data files generated by the benchmarks.- The data files for each experiment are grouped in directories named after
the experiment prefixed with timestamp. Generally, the structure of these
directories is as follows:
{{yyyy_MM_dd_HH_mm_ss}}_{{benchmark_name}}/
: Containing the following:{{benchmark_name}}_BruteForceAlgorithm.csv
: Contains the raw data for the benchmark with the brute-force algorithm.{{benchmark_name}}_StatefulTreeAlgorithm.csv
: Contains the raw data for the benchmark with the stateful tree algorithm.
- The data files for each experiment are grouped in directories named after
the experiment prefixed with timestamp. Generally, the structure of these
directories is as follows:
-
The library provides a simple API to define join patterns. The following example demonstrates how to define a join pattern for a simple factory shop floor monitroring system, as seen in the paper:
- First add the necessary imports:
import join_patterns.receive
import actor.*
import actor.Result.*
- Define the messages that the actors will exchange. We use Scala 3 enums to define the message types with their respective payloads:
enum MachineEvent:
case Fault(faultID: Int, ts: Long)
enum WorkerEvent:
case Fix(faultID: Int, ts: Long)
enum SystemEvent:
case DelayedFault(faultID: Int, ts: Long)
case Shutdown()
type Event = MachineEvent | WorkerEvent | SystemEvent
- Now we define the monitor as an actor with join definitions using the
receive
macro. The join patterns are written as Scala tuples followed by a guard condition.
def monitor(algorithm: MatchingAlgorithm) =
Actor[Event, Unit] {
receive { (self: ActorRef[Event]) =>
{
case (Fault(fid1, ts1), Fix(fid2, ts2)) if fid1 == fid2 => ...
case (Fault(fid1, ts1), Fault(fid2, ts2), Fix(fid3, ts3))
if fid2 == fid3 && ts2 > ts1 + TEN_MIN => ...
case (DelayedFault(fid1, ts1), Fix(fid2, ts2)) if fid1 == fid2 => ...
...
}
}(algorithm)
}
- Finally, we can run the monitor as follows:
def runFactorySimple(algorithm: MatchingAlgorithm) =
// Predefined sequence of events
val events = List(
Fault(1, ONE_MIN),
Fault(2, TEN_MIN),
Fault(3, QUARTER_HR),
Fix(3, THIRTY_MIN)
)
// Start the monitor actor
val (monitorFut, monitorRef) = monitor(algorithm).start()
// Send the events to the monitor actor
events foreach (event => monitorRef ! event)
// Shutdown the monitor actor
monitorRef ! Shutdown()
// Wait for the monitor actor to finish
Await.ready(monitorFut, Duration(15, "m"))
The algorithm can be set to BruteForceMatcher
or StatefulTreeBasedAlgorithm
.
In the example above, some minor details are omitted for brevity. The full
example can be found in the FactorySimpl.scala file.
The library can be compiled by installing a Java Development Kit (version 21 or later) and
sbt (version 1.9 or later) and running sbt compile
. Then, sbt
will download the required
dependencies (including the Scala 3 compiler).
To compile the library, run the following command from the root directory (where
the build.sbt
file is located):
sbt clean compile
To run the tests of the core library, run the following command:
sbt core/test
To run for instance the Factory Simple
example with the predefined configuration
run the following command:
sbt "core/run factory-simple --algorithm stateful"
The --algorithm
flag can be set to brute
or stateful
to run the brute-force or
stateful tree-based algorithm, respectively.
There are other examples available in the examples
package that can be run in a similar way.
See the benchmarks README for more information on how to run the benchmarks.