David Beck

Thoughts

Follow on GitHub

Rust Actor Library: First Assorted Thoughts

04 Nov 2016 by David Beck on [LinkedIn] / [Feed]

I spent most of my spare time in the past few months on an actor library for Rust.

This is not a re-implementation of Erlang/Elixir/OTP. It has a lot narrower and different feature set. (I originally wanted a similar thing, but I changed course a few times, narrowed the scope and arrived to something different.)

first

[dependencies]
acto-rs = "0.5.2"

This actor library runs tasks with the help of fixed number of threads. If the scheduler is started without parameters (start()) then it starts only one thread. If more threads are desired then use start_with_threads():

extern crate acto_rs;

fn first() {
  use acto_rs::scheduler;
  let mut sched = scheduler::new();
  sched.start_with_threads(4);
  sched.stop();
}

For this to make any sense we will need to add tasks. Tasks must implement the Task trait:

pub trait Task {
  fn execute(&mut self, stop: &mut bool);
  fn name(&self) -> &String;
  fn input_count(&self) -> usize;
  fn output_count(&self) -> usize;
  fn input_id(&self, ch_id: ReceiverChannelId) -> Option<(ChannelId, SenderName)>;
  fn input_channel_pos(&self, ch_id: ReceiverChannelId) -> ChannelPosition;
  fn output_channel_pos(&self, ch_id: SenderChannelId) -> ChannelPosition;
}

The execution of the tasks is controlled by a SchedulingRule.

Tasks

Tasks can have typed input and output channels. However it is possible to schedule a tasks without any channels. This doesn’t make much sense but shows the minimum:

// a very simple task with a counter only
struct NakedTask {
  // to help the Task trait impl
  name : String,
  // state
  count : usize,
}

Here is the implementation of the Task trait:

impl Task for NakedTask {
  // execute() runs 3 times and after it sets the stop flag
  // which tells the scheduler, not to execute this task anymore
  fn execute(&mut self, stop: &mut bool) {
    self.count += 1;
    println!("- {} #{}", self.name, self.count);
    if self.count == 3 {
      // three is enough
      *stop = true;
    }
  }

  fn name(&self) -> &String { &self.name }

  // zero / None values, since NakedTask has
  // no input or output channels
  fn input_count(&self) -> usize { 0 }
  fn output_count(&self) -> usize { 0 }
  fn input_id(&self, _ch_id: ReceiverChannelId)
    -> Option<(ChannelId, SenderName)> { None }
  fn input_channel_pos(&self, _ch_id: ReceiverChannelId)
    -> ChannelPosition { ChannelPosition(0) }
  fn output_channel_pos(&self, _ch_id: SenderChannelId)
    -> ChannelPosition { ChannelPosition(0) }
}

Finally we need to pass instance(s) of NakedTask to a scheduler:

pub fn run_naked() {
  // - create a scheduler
  // - add a recurring task
  // - stop the scheduler after 4 seconds
  let mut sched = scheduler::new();
  sched.start();
  sched.add_task(
    Box::new(NakedTask{name:String::from("RunningNaked"), count:0}),
    SchedulingRule::Periodic(PeriodLengthInUsec(1_000_000))).unwrap();
  thread::sleep(time::Duration::from_secs(4));
  sched.stop();
}

(Github repository for playing around with acto-rs.)

Periodic, Loop and OnExternalEvent

NakedTask has little practical use however it can demonstrate 2 other SchedulingRules too. The Periodic is shown above, run 3 times and wait a second in between. The Loop rule is even more useless, its main feature is to increase electricity bill:

pub fn increase_my_bill() {
  let mut sched = scheduler::new();
  sched.start();
  sched.add_task(
    Box::new(NakedTask{name:String::from("IncreaseBill"), count:0}),
    SchedulingRule::Loop).unwrap();
  thread::sleep(time::Duration::from_secs(1));
  sched.stop();
}

In general Loop can be used for Tasks need to run continuously in a round-robin fashion.

The OnExternalEvent is slightly more usable because it allows running a Tasks triggered by an external event. This is a good candidate for MIO integration:

pub fn trigger_me() {
  let mut sched = scheduler::new();
  sched.start();
  let task_id = sched.add_task(
    Box::new(NakedTask{name:String::from("TriggerMe"), count:0}),
    SchedulingRule::OnExternalEvent).unwrap();
  // notify(..) wakes up the task identified by task_id
  sched.notify(&task_id).unwrap();
  sched.stop();
}

With channels

If a Task happen to have channels and able to talk to other Tasks then it becomes more interesting. To create such tasks I added a few helpers. Depending on the number and type of input/output channels, different helpers are available.

A simple Source task for example has one output channel. This channel can pass Messages which can be a value or an error or an acknowledgement.

For the Source trait I added a convenience new function that creates an object that conforms the Task trait and holds the channel handles. This simplifies creating a source element.

Similarly I created helpers for:

  • a Sink element that has a single input channel
  • a Filter element that has a single input and a single output channel of possibly different types, which allows translating between different message types
  • a YSplit element that has an input channels and two possibly different output channels, which allows emitting different messages
  • a YMerge element that may receive messages on two input channels of possibly different type and emits messages on an output channel
  • a Scatter element that has one input channel and possibly many output channels of the same type
  • a Gather element that can have many input channels of the same type and one output channel

Closing thoughts

This post became a lot longer than I wanted. In a next post I will give examples for using the helpers to create elements that are actually passing messages to each other.

Rust

This library only needs stable Rust and probably works with nightly too.

rustc --version
rustc 1.12.1 (d4f39402a 2016-10-19)

Update

  1. I have written a follow up post based on the feedbacks.
  2. I have written a new post with another example.