Closed5

RustのActorを扱えるライブラリrikerを使ってみる

yukiyuki

とりあえず使ってみた感想

  • 軽く遊べる程度にはいろいろ揃っている。
  • メッセージが型付きでとても Rust らしい。
  • Akka とほとんど API の名前が同じで Akka を読み替えながら実装できそうな感じがする。
yukiyuki

まだ TicketSeller 側は実装中だけど、『Akka実践バイブル』を参考に Scala のコードを Rust に直してみた。

使ってみた感じは、完全に Akka 笑。

実践 Akka のコードは Typed Akka ではないため、正直型付けをどうしたらいいかかなり悩んだ。これも完全なサンプルとは言えないと思う。とくに、パターンマッチングの部分。消費しないケースがあると Rust ではエラーになるが、サンプルコードの方は Any に対する match なのでそのケースが考慮されてない、など。

あとは私の Akka の習熟度の問題があるのだけど、Result で返すところをことごとく unwrap している。recv の返り値は Unit なのだけど、Result を適切にさばくには…というのが研究の余地がありそう。

ticket_seller.rs
use riker::actors::*;

#[derive(Clone, Debug)]
pub struct Ticket {
    id: i32,
}

impl Ticket {
    pub fn new(id: i32) -> Self {
        Ticket { id }
    }
}

#[derive(Clone, Debug)]
pub enum TicketSellerMessage {
    Add(Vec<Ticket>),
    Buy(usize),
    Ticket(i32),
    Tickets { event: String, entries: Vec<Ticket> },
    GetEvent,
    Cancel,
}

#[derive(Default)]
pub struct TicketSellerActor {
    pub event: String,
    pub tickets: Vec<Ticket>,
}

impl Actor for TicketSellerActor {
    type Msg = TicketSellerMessage;

    fn recv(
        &mut self,
        ctx: &Context<TicketSellerMessage>,
        msg: TicketSellerMessage,
        sender: Sender,
    ) {
        match msg {
            TicketSellerMessage::Add(tickets) => {
                let mut tickets = tickets;
                self.tickets.append(&mut tickets)
            }
            TicketSellerMessage::Buy(nr_of_tickets) => {
                let entries: Vec<Ticket> = self
                    .tickets
                    .iter()
                    .take(nr_of_tickets)
                    .map(|s| s.clone())
                    .collect();
                if entries.len() >= nr_of_tickets {
                    unimplemented!()
                } else {
                    unimplemented!()
                }
            }
            TicketSellerMessage::GetEvent => unimplemented!(),
            TicketSellerMessage::Cancel => unimplemented!(),
            _ => (),
        }
    }
}
box_office.rs
use crate::ticket_seller::{Ticket, TicketSellerActor, TicketSellerMessage};
use anyhow::Result;
use riker::actor::*;

#[derive(Clone, Debug)]
pub struct Event {}

#[derive(Clone, Debug)]
pub enum BoxOfficeMessage {
    CreateEvent { name: String, tickets: i32 },
    GetEvent(String),
    GetTickets { event: String, tickets: i32 },
    CancelEvent(String),
    Event { name: String, tickets: i32 },
    Events(Vec<Event>),
}

#[derive(Clone, Debug)]
pub enum EventResponse {
    EventCreated(Event),
    EventExists,
}

#[derive(Default)]
pub struct BoxOfficeActor {}

impl BoxOfficeActor {
    fn create_ticket_seller(
        &self,
        name: &str,
        context: &Context<BoxOfficeMessage>,
    ) -> Result<ActorRef<TicketSellerMessage>> {
        let actor = context.actor_of::<TicketSellerActor>(name)?;
        Ok(actor)
    }
}

impl Actor for BoxOfficeActor {
    type Msg = BoxOfficeMessage;

    fn recv(&mut self, ctx: &Context<BoxOfficeMessage>, msg: BoxOfficeMessage, sender: Sender) {
        match msg {
            BoxOfficeMessage::CreateEvent { name, tickets } => {
                let found = ctx
                    .myself
                    .children()
                    .find(|actor_ref| actor_ref.name() == name);
                match found {
                    Some(base_ref) => {
                        base_ref
                            .try_tell(EventResponse::EventExists, Some(ctx.myself().into()))
                            .unwrap();
                    }
                    None => {
                        let event_tickets = self.create_ticket_seller(&name, ctx).unwrap();
                        let new_tickets: Vec<Ticket> = (1..tickets)
                            .map(|ticket_id| Ticket::new(ticket_id))
                            .collect();
                        event_tickets.send_msg(
                            TicketSellerMessage::Add(new_tickets),
                            Some(ctx.myself().into()),
                        );
                        sender.map(|base_ref| {
                            base_ref
                                .try_tell(
                                    EventResponse::EventCreated(Event {}),
                                    Some(ctx.myself().into()),
                                )
                                .unwrap()
                        });
                    }
                }
            }
            _ => (),
        }
    }
}
yukiyuki

かなり実装は適当できっと動かないけれど、エントリポイントはこんな感じになるのかな。もう Rust 版 Akka にしか見えない。

main.rs
use riker::actors::*;
use riker_sandbox::box_office::{BoxOfficeActor, BoxOfficeMessage};

fn main() {
    let sys = ActorSystem::new().unwrap();
    let box_office_actor = sys.actor_of::<BoxOfficeActor>("box-office").unwrap();
    box_office_actor.tell(
        BoxOfficeMessage::CreateEvent {
            name: "event".to_string(),
            tickets: 200,
        },
        None,
    );
}
yukiyuki

ActorRefBasicActorRef の違いが使っている限りではよくわからなかったけれど、ドキュメントを読んだらだいたいわかった。

まず、それぞれの構造体のシグネチャは下記のようになっている。

actor_ref.rs
#[derive(Clone)]
pub struct ActorRef<Msg: Message> {
    pub cell: ExtendedCell<Msg>,
}
actor_ref.rs
#[derive(Clone)]
pub struct BasicActorRef {
    pub cell: ActorCell,
}

BasicActorRef の方は、メッセージを tell する際、そのメッセージの型はなんでも入れられる(メッセージは型付けされていないといえる)。一方で、ActorRef の方はメッセージを tell する際には、その Actor で定義したメッセージの型しか入れることができない。

BasicActorReftell する関数のシグネチャを見てみると、

    pub fn try_tell<Msg>(
        &self,
        msg: Msg,
        sender: impl Into<Option<BasicActorRef>>,
    ) -> Result<(), ()>
    where
        Msg: Message + Send,
    {
        self.try_tell_any(&mut AnyMessage::new(msg, true), sender)
    }

となっており、関数に使われているメッセージの型を関数の型引数としているだけで、内部では AnyMessage という内容を生成している(内部で std::any::Any を使用していい感じにしておいてくれる)。

std::any::Any というのは動的型付けをエミュレートできるように用意された標準ライブラリの機能。Rust には Scala のような Any 型は組み込みでは存在しないが、この標準ライブラリを使用することで、実質それと似たようなことができる。

https://doc.rust-lang.org/std/any/trait.Any.html

一方で、ActorReftell する関数のシグネチャを見てみると、

impl<T, M> Tell<T> for ActorRef<M>
where
    T: Message + Into<M>,
    M: Message,
{
    fn tell(&self, msg: T, sender: Sender) {
        self.send_msg(msg.into(), sender);
    }

(...)
}

このように T: Message + Into<M> となっており、実質 Actor に紐づくメッセージの型を参照している。

どのように ActorRefActor のメッセージの型が一致させられているかと言うと、Context という構造体で一気に紐付けされる。

pub struct Context<Msg: Message> {
    pub myself: ActorRef<Msg>,
    pub system: ActorSystem,
    pub(crate) kernel: KernelRef,
}

Context は、Actor がもつ recv という関数の引数として登場してくる。type Msg: Messagerecv は具象型のアクターで実装が必要になる。

pub trait Actor: Send + 'static {
    type Msg: Message;

    fn pre_start(&mut self, ctx: &Context<Self::Msg>) {}

    fn post_start(&mut self, ctx: &Context<Self::Msg>) {}

    fn post_stop(&mut self) {}

    fn supervisor_strategy(&self) -> Strategy {
        Strategy::Restart
    }

    fn sys_recv(&mut self, ctx: &Context<Self::Msg>, msg: SystemMsg, sender: Sender) {}

    fn recv(&mut self, ctx: &Context<Self::Msg>, msg: Self::Msg, sender: Sender);
}
このスクラップは2020/12/20にクローズされました