Recently I’ve been trying to extend my actor library, Aktors to deal with type safety. The current version relies heavily on the Any type, which has two serious problems:

  • It means that your code is way slower than it needs to be - Any means dynamic dispatch all over the place
  • You lose static type safety - you can send an actor a message that it can not handle

What we want in an actor framework is the ability to write simple, sequential code and have concurrency ‘just happen’. We don’t want to sacrifice performance, and we don’t want to sacrifice type safety.

(note: If you are interested in working on the project, feel free to reach out to me, I’d be happy to walk anyone through the code and split some work out)

Current State

To provide that experience I created derive_aktor.

This project provides a macro that you can apply to your structure, and generates all of the necessary types and functions such that you can work with an Actor version of your structure fairly seamlessly, without having to think (much) about concurrency.

For example:

pub struct PrintLogger {

}

#[derive_actor]
impl PrintLogger {
    pub fn info<T: Debug + Send + 'static>(&self, data: T) {
        println!("{:?}", data);
    }

    pub fn error<T: Debug + Send + 'static>(&self, data: T) {
        println!("{:?}", data);
    }
}

The above code will generate a new impl for PrintLogger, with the following method:

impl PrintLogger {
    pub fn route_msg<InfoT: Debug + Send + 'static, ErrorT: Debug + Send +
                     'static>(&mut self,
                              msg: PrintLoggerMessage<InfoT, ErrorT>) {
        match msg {
            PrintLoggerMessage::InfoVariant { data: data } => self.info(data),
            PrintLoggerMessage::ErrorVariant { data: data } => self.error(data),
        };
    }
}

as well as a PrintLoggerActor that we will interact with directly.

impl<InfoT: Debug + Send + 'static, ErrorT: Debug + Send + 'static> PrintLoggerActor<InfoT,
                                                                                     ErrorT> {
    pub fn new<H: Send + fibers::Spawn + Clone + 'static>(handle: H,
                                                          actor: PrintLogger)
                                                          -> PrintLoggerActor<InfoT, ErrorT> {
        let mut actor = actor;
        let (sender, receiver) = unbounded();
        let id = "random string".to_owned();
        let recvr = receiver.clone();
        handle.spawn(futures::lazy(move || {
            loop_fn(0, move |_| match recvr.try_recv() {
                Ok(msg) => {
                    actor.route_msg(msg);
                    Ok::<_, _>(futures::future::Loop::Continue(0))
                }
                Err(TryRecvError::Disconnected) => Ok::<_, _>(futures::future::Loop::Break(())),
                Err(TryRecvError::Empty) => Ok::<_, _>(futures::future::Loop::Continue(0)),
            })
        }));
        PrintLoggerActor {
            sender: sender,
            receiver: receiver,
            id: id,
        }
    }
    pub fn info(&self, data: InfoT) {
        let msg = PrintLoggerMessage::InfoVariant { data: data };
        self.sender.send(msg);
    }
    pub fn error(&self, data: ErrorT) {
        let msg = PrintLoggerMessage::ErrorVariant { data: data };
        self.sender.send(msg);
    }
}

The above code is a little complicated. The end result is three methods:

  • A ‘new’ constructor, which takes a tokio executor handle and the PrintLogger actor.
  • Actor versions of the ‘info’ and ‘error’ methods, which pack the arguments into a message and send them off to the underlying PrintLogger’s route_msg function.

The end result is that you can work with an actor that has an identical interface to your underlying structure - same method names, same types - but all functions are non blocking.

Future: What about return values?

Imagine a function with a similar structure as the following:

  pub fn try_log(&self, data: String) -> Result<(), ()> {
    Ok(())
  }

We may want to handle the potential error case in that function. Currently, you have to pass in an actor that can handle the error. One of the next steps in the library is to provide a more general way to handle results of functions.

Specifically, that function would generate an enum variant like this:

  PrintLogger::TryLog {
    data: String,
    ret: Fn(Result<(), ()>)
  }

And the route_msg for that variant would look like this:

  match msg {
    PrintLogger::TryLog {data: data, ret: ret } => ret(self.try_log(data))
  }

The return value of try_log will be handed to the closure. You can then send the value to any actors within the scope:

  let error_handler = ErrorHandlerActor::new(handle, err_handler);
  let print_logger = PrintLoggerActor::new(handle, logger);

  print_logger.try_log("logline".to_owned(), |res| {
    if let Err(e) = res {
        error_handler.handle(e)
    }
  })

The above code is fairly representative of what I’m hoping to generate.

Future: Trait Actors

Another future improvement will be Trait Actors. In the same way that derive_actor provides an Actor interface for a structure, it can provide one for a Trait.

  pub trait Logger {
    fn info(&self, data: String);
    fn error(&self, data: String);
  }

This should generate an ActorLogger that will route messages to an underlying Logger trait object.



blog comments powered by Disqus

Published

07 May 2017

Categories