diff --git a/content/code/thinking-about-rust-actors/index.md b/content/code/thinking-about-rust-actors/index.md index 9e78f95..c4c8d51 100644 --- a/content/code/thinking-about-rust-actors/index.md +++ b/content/code/thinking-about-rust-actors/index.md @@ -30,17 +30,18 @@ Actors: ![](actors.png "A silly representation of the actor model") On my implementation, the actor is actually a module with a `run()` function; -this function exposes the `Sender` part of a channel which acts as the Inbox of -it and the task PID, so the can `.await` it to avoid the main application from -finishing with the actor still running. +this function exposes the `Sender` part of a MPSC +(Multiple-Producer-Single-Consumer) channel which acts as the Inbox of it and +the task PID, so the can `.await` the actor processing loop to avoid the main +application from finishing with the actor still running. {% note() %} For now, I'm ignoring Tokio and async for next examples. {% end %} -And because there is no "Post Office" kind of solver in Rust, we can actually -short circuit the actors by giving the `Sender` channel of an actor as -parameter to a second, so it knows where to send its messages. Something like: +And because there is no "Post Office" kind of solver in Rust, I short-circuited +the actors by giving the `Sender` channel of an actor as parameter to a second, +so it knows where to send its messages. Something like: ```rust let channel3 = actor3::run(...); @@ -48,9 +49,10 @@ let channel2 = actor2::run(channel3); actor1::run(channel2); ``` -In this short sample, whatever "actor1" produces, it sends directly to -"actor2"; "actor2", on its part, produces something that is received by -"actor3". And, with more actors, things just keep chaining. +In this short sample, whatever "actor1" produces, it sends directly to "actor2" +though the channel the latter created; "actor2", on its part, produces +something that is received by "actor3". And, with more actors, things just keep +chaining. {% note() %} I am intentionally ignoring the internals of each actor and their `run()` @@ -120,5 +122,10 @@ let (actor2_pid, actor2_channel) = actor2::run(); I have some ideas to make this part more fluent, but I need to do some more exploration about the topic (specially since I think we can leverage the type -system to now allow actors with different outputs to connect). Once I get those -hammered down, I'll get a follow up post. +system to not allow connecting actors whose input type is not the same as the +output type of the previous actor). Once I get those hammered down, I'll get a +follow up post. + + diff --git a/content/code/thinking-about-rust-actors/index.pt.md b/content/code/thinking-about-rust-actors/index.pt.md new file mode 100644 index 0000000..94a641e --- /dev/null +++ b/content/code/thinking-about-rust-actors/index.pt.md @@ -0,0 +1,133 @@ ++++ +title = "Pensamentos Sobre Atores em Rust" +date = 2023-08-17 + +[taxonomies] +tags = ["rust", "actor model"] ++++ + +Recentemente eu escrevi uma aplicação para o trabalho (desculpa, não posso +mostrar o código) que, por ser fortemente baseada em I/O, eu decidi escrever +usando [Tokio](https://tokio.rs/) e a ideia de usar [Actor Model com +isso](https://ryhl.io/blog/actors-with-tokio/). + +... o que me levou a pensar um pouco mais sobre isso. + + + +Antes de mais nada, Actors em Rust são bem diferentes de atores em linguagens +com um Actor Model de verdade. Em resumo, você tem os seus atores, que rodam de +forma independente, cada ator tem uma caixa de entrada (inbox) para coisas a +serem processadas e uma "caixa de saída" -- com aspas, porque não é exatamente +isso. Um ator recebe uma mensagem, processa mesma e pode ter terminado aí ou +pode produzir algo para ser processado por outro ator -- que seria a caixa de +saída, o que normalmente difere da caixa de entrada porque a caixa de entrada +tem uma fila, mas a caixa de saída não (e é por isso que eu estava usando +"caixa de saída" com aspas antes). + +Todas as mensagens são entregues por um "correio" (ou "post office" no inglês), +que conecta todos os atores: + +![](actors.png "Uma representação simplificada do Actor Model") + +Na minha implementação, o ator era um módulo com uma função chamada `run()`; +essa função expõe a parte de `Sender` de um canal MPSC +(Multiple-Producer-Single-Consumer, ou "Vários Produtos, Um Consumidor") que +haje como a caixa de entrada do ator, e o PID da tarefa, de forma que é +possível fazer um `.await` no loop de processamento para evitar que a aplicação +principal termine enquanto o ator ainda está ativo. + +{% note() %} +Nos exemplos abaixo, eu vou completamente ignorar a parte do Tokio e async. +{% end %} + +Como não há alguma coisa que funcione como um "Correio" em Rust, eu fiz uma +ligação direta entre os atores, entregando o canal `Sender` de um ator como +parâmetro para o segundo, de forma que o segundo saiba para onde enviar as suas +mensagens. Algo do tipo: + +```rust +let canal3 = ator3::run(...); +let canal2 = ator2::run(canal3); +ator1::run(canal2); +``` + +Nesse exemplo, seja lá o que `ator1` produza, ele envia diretamente para o +"ator2" através do canal que o segundo criou; "ator2", por sua vez, produz +alguma coisa que é recebida pelo "ator3". E, com mais atores, só é preciso +ficar fazendo as conexões. + +{% note() %} +Eu estou intencionalmente ignorando os internos de cada ator e as suas funções +`run()`, mas elas seriam variações de: + +```rust +fn run(..) -> (task::JoinHandle<()>, mpsc::Sender) { + let (tx, mut rx) = mpsc::channel::(UM_TAMANHO); + let task = tokio::spawn(async move { + while let Some(dado) = rx.recv().await { + let conversao = processamento_do_ator(dado); + // Talvez envie o "conversao" para o próximo ator? + } + }); + (task, tx) +} +``` +{% end %} + +Mas... como os atores parecem ter uma interface muito parecida, isso se parece +com uma trait! + +Então, como deveria ser a trait de Atores? + +Inicialmente, a função `run()` ou similar devem expor o PID do ator e o canal +de entrada. Algo como: + +```rust +pub trait Actor { + fn run() -> (task::JoinHandle<()>, Sender); +} +``` + +Por que `TipoDeDadosQueOAtorRecebe`? É por que cada ator pode ter um tipo de +mensagem diferente de entrada. Usando o pequeno exemplo acima, "ator2" poderia +estar recebendo `usize` e enviando `String`s para o "ator3". + +Como o tipo muda de ator para ator, nós precisamos de um tipo associado: + +```rust +pub trait Actor { + type Input; + + fn run() -> (task::JoinHandle<()>, Sender); +} +``` + +A ideia básica é que, uma vez que a trait seja implementada por uma struct, +nós possamos fazer algo como: + +```rust +let ator3 = Ator3::new(...); +let (ator3_pid, canal_ator3) = ator3::run(); +``` + +Mas peraí, e como faríamos a ligação entre atores? Isso poderia ser feito com +algo simples como: + +```rust +let ator3 = Ator3::new(); +let (ator3_pid, ator3_canal) = ator3::run(); +let ator2 = Ator2::new(ator3_canal); +let (ator2_pid, ator2_canal) = ator2::run(); +``` + +O que fica meio verboso, mas funciona. + +Eu tenho algumas ideias de como fazer a parte de ligação mais fluente, mas eu +preciso fazer algumas explorações no tópico (principalmente porque eu acho que +dá pra usar o sistema de tipos de Rust para não permitir que sejam conectados +atores cujo tipo de entrada é diferente do tipo de saída do anterior). Quando +eu conseguir pensar em algo, eu faço um post explicando. +