Skip to content

JoinActors: Fair Join Pattern Matching for Actors

License

Notifications You must be signed in to change notification settings

a-y-man/join-actors

Repository files navigation

JoinActors: Fair Join Pattern Matching for Actors

Description

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.

Code base Structure

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.

API Usage

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.

Build and Test

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.

Benchmarks

See the benchmarks README for more information on how to run the benchmarks.