restioson / xtra Goto Github PK
View Code? Open in Web Editor NEW🎭 A tiny actor framework
License: Mozilla Public License 2.0
🎭 A tiny actor framework
License: Mozilla Public License 2.0
It is hard to know whether Xtra uses bounded or unbounded channels and how to set them up. Searching documentation for "bounded" does not lead to any hints.
It is hard to get to xtra::Actor::create
when expecting this things to be in xtra::address
or xtra::message_channel
. No other mention about bounded vs unbounded seems to be in the documentation.
README example has None
for creating actor, yet it is not clear that it means unbounded.
I suggest:
xtra::Actor::create
doccomment. There is "unboudned", but not "bounded", so searching may fail to find it.create
method.None
meaning not "default settings" (like users may expact), but "unbound mailbox".xtra::Address::send
. do_send
seems to already carry the warning, but based on do_send
s doccomment alone, it may occur that xtra supports only bound channels. It is hard to get from do_send
's "If the actor’s mailbox is full, it will block." to Actor::create
's message_cap
.xtra::message_channel
, mention that the word "channel" is used specially in xtra and hint that user may want to search for the word "mailbox" instead.xtra::Actor::create
. Like like "... given the cap for the actor’s mailbox (i.e. channel)".The creation of weak addresses will allow for notifications to be implemented (else, a strong address held in the actor's context would prevent it from ever being dropped). It will also allow for more flexibility in the API. This is probably a breaking change, the methods on Address may become part of a trait, perhaps called AddressExt. However, this could be mitigated by implementing them on the types directly as well.
xtra is by design small and thus, I think a big contestor for people making a decision on what to use might be tokio's or futures' mpsc channels, spawned as a task with an endless loop.
It would be interesting to benchmark xtra against such a naive actor implementation.
Have you tried any performance tests? Even if it's not the goal of this lib it'd be nice to know what we're looking at 😉.
The last release (0.5.0-beta.5) is broken since smol 1.1, but master works, so would you consider releasing a new one ?
|
28 | Handle(&'a smol::Executor)
| ^^^^^^^^ expected named lifetime parameter
|
help: consider using the `'a` lifetime
|
28 | Handle(&'a smol::Executor<'a>)
| ^^^^^^^^^^^^
xtra
wants to be a tiny, fast, and safe actor framework.
After working with it for a while, I think this is mostly true but I also think we can do better, mostly on the tiny and safe part. I'd assume there are also performance gains to be made but I know where little about that so I am going to leave that for people with more experience.
At the moment, xtra
's API is already fairly small which I'd consider to be a good thing. However, there are some parts that I'd consider to be inconsistent and where I think we could do better in terms of composability. Additionally, there are some feature where I am not sure whether they need to necessarily be in the core xtra
crate.
do_send
, do_send_async
, send
.We have 3 APIs for sending messages on an Address
. In our project, we ban the do_send
ones. We consider them to be foot-guns because they silently drop return values which could be a Result
and thus, things might be failing without one noticing. I'd like to propose to remove those from the public API. Asynchronous sending can be achieved by spawning the SendFuture
into an executor. The fact that our underlying channels have a dedicated _async` way of sending messages should be hidden IMO.
Address::attach_stream
A nice convenience method that we used a fair bit initially but quickly realized, that it is not as useful as it initially seems, primarily because it does not give you a way of dealing with the return value of the Handler
. I think this function can be removed or moved into some kind of xtra-ext
crate. It builds on top of the Address::send
API and could thus easily live in another crate.
Context::notify_interval
We have found this function to be problematic in two ways:
a. It sends messages asynchronously to the actor, thus potentially piling up a number of messages if the actor cannot process them quickly enough.
b. It does not allow for an error handling policy. We have many handlers that are invoked on an interval that can fail (background sync jobs) but their failure should not abort the loop but instead log a message.
Given that the function really only composes send
, I think we should try and move it out of the core xtra
crate and provide ways for users to easily create their own "send on an interval" loop. Perhaps this can be combined with Address::attach_stream
? An stream of intervals that maps to messages would achieve the same outcome I think.
Context::notify_after
Similar to notify_interval
this function only composes send
and thus, I think we could lift it into a separate crate, minimizing the API surface of Context
.
Context::{notify,notify_all}
We haven't found a usecase for these but it is one of those feature that if not provided by the core crate, are impossible to implement on top. I might be worth debating and clearly documenting, when and what it should be used for. Given that we already have a way of sending messages to an actor, it is not a completely orthogonal feature either.
AddressSink
and friendsWhilst I think interoperability with common traits in the async
ecosystem is important, I've been wondering whether it is worth maintaining a fairly big chunk of code for this feature. Similar to Context::notify
, there are some orthogonality concerns with Address::send
.
cc @zesterer Curious to hear if any of this overlaps with your thoughts that you mentioned in #68 (comment).
I've been implementing the observer pattern over MessageChannel
s. Minimal example of what I'm trying to do:
use xtra::{Message, prelude::MessageChannel};
struct Subject<M: Message> {
observers: Vec<Box<dyn MessageChannel<M>>>
}
impl<M: Message> Subject<M> {
pub fn register_observer(&mut self, observer: Box<dyn MessageChannel<M>>) {
// Add observer if not already present
if !self.observers.contains(&observer) {
self.observers.push(observer);
}
}
}
The problem is that self.observers.contains(&observer)
does not compile because MessageChannel
does not implement PartialEq
.
While not strictly necessary it would be nice to be able to deduplicate the observers
vector.
I have tried determining if this request i.e. implementing PartialEq
for MessageChannel
and in turn for Address
is unreasonable but I don't have a good enough understanding of the inner workings of Address
. Feel free to close this issue if it is nonsense.
Thank you very much for your effort and time :)
As part of doing #122, I started to form the following vision:
Chan
should be the core channel implementation of the library, #122 gets us most of the way thereSender
and Receiver
only really add an opinionated interfaces on top of it, plus reference countingSender
nor Receiver
are publicly exposed, meaning I think they don't actually have to existSender
can be completely inlined into Address
, removing an entire layer of indirectionReceiver
can be promoted to a type called Mailbox
that could be publicly exposedContext
would be something that is constructed only temporarily for the invocation of a handler (I think we discussed this idea at some point)Putting all of this together, we could realise an API like this:
let (mailbox, address) = Mailbox::bounded(5);
tokio::spawn(xtra::run(mailbox, MyActor::new()));
With run
being implemented more or less like what we have today.
Sometimes it is useful for messages to implement fmt::Debug
but if they contain Address
es, that is not possible without a manual impl. Could Address
implement Debug
?
Not sure if we can print something useful but the counter value could be interesting for example!
In #119 and other places, we have discussions that ultimately come down to optimising for different goals.
I am opening this issue to propose and discuss a manifest that states goals and non-goals and rates properties amongst each other.
From the current issue description, xtra wants to be safe, small and performant. Taking inspiration from the agile manifest, we can thus perhaps say that we value:
Example: We would rather not use unsafe code to achieve a more performant implementation.
Example: We would rather not add a "special" function to the public API that makes certain use cases more efficient if the use case can already be achieved with a different API.
Example: We would rather not include a convenience feature that can already be expressed with existing public APIs.
Example: async-await style APIs are convenient to use but it is hard to provide ordering guarantees once a task is spawned into an executor. Ordering guarantees more or less imply poll
style APIs down to user-handlers but those are less convenient to use.
Example: We would rather not add a feature to xtra if it introduces APIs that are not orthogonal to an existing API. In other words, all APIs should be as orthogonal and modular as possible.
Same as with the Agile manifesto, this list doesn't mean that we don't optimise for the items on the right but when in conflict with the left, we will favor the left.
I am using this issue to create two proposals:
dep:
syntax for features to hide the implicit features created by optional crates. I think this part is non-controversial.with-xyz-1_0
convention is unnecessarily complex.Together with this issue, the README should be updated too.
Which channel type is used for sending messages to actors: bounded or unbounded?
If the former, how deadlocks are handled? If the latter, how to do backpressure or load shedding?
If the actor’s mailbox is full, it will block.
So it looks like bounded channels. What size is the buffer? Can it be switched to unbounded? Shall there be a method like xtra::address::Address::inflight_messages
to help load shedding and prioritisation?
The removal of with-executor is technically a breaking change. More info on the doc stuff: https://doc.rust-lang.org/rustdoc/unstable-features.html
This leads to a smaller public API.
Should come with the addition of an example how to start multiple actors behind the same address, i.e. demonstrating the cloning of the context.
Extracted out of #71.
This should be introduced via an extension crate to not grow the core API of xtra unnecessarily.
It is important that this must work only on Handler
s that return ()
. That is also the main reason for providing this functionality to users and not telling them to write it themselves. It is a potential footgun to drop that return value.
Currently, defining a Handler
requires the usage of async_trait
and filling in various elements into a trait impl.
In theory however, a macro can learn all required pieces of information from the following code block:
impl Actor {
pub async fn handle_message(&mut self, message: Message) -> i32 {
todo!()
}
}
This needs to be expanded to:
#[async_trait::async_trait]
impl Handler<Message> for Actor {
type Return = i32;
fn handle(&mut self, message: Message, _ctx: &mut Context<Self>) -> i32 {
todo!()
}
}
This is what https://github.com/comit-network/xtra-productivity does although it is not yet updated to the latest version of xtra
where f.e. the Message
type is already removed. @klochowicz and myself authored that crate at my last gig. I think it would be nice to pull this into this repository as a separate crate, re-export it through xtra
and expose it as xtra::handler
so it can be used like:
#[xtra::handler]
impl Actor {
// function definitions here
}
The main features of the macro are:
impl Handler
per fn in the impl
blockContext
if not usedhttps://github.com/comit-network/xtra-productivity would need a lot of polish before it can be officially released, esp. in regards to error messages. The implementation isn't too complex so we can also start from scratch if necessary.
Hey there! First of all, wanted to say thanks for this great library, tried many actor frameworks so far, but sticking with xtra
due to its simplicity, speed, and easy handling of async/await :-)
I have a question about notify_all
method, which is not clear to me. It says to notify the message to all of the actors running with the same address, but I wasn't able to figure out how to register two actors at the same address.
My goal is the following: I have some actor, which accepts messages, but also broadcasts (via notify_all) messages (like events). The only way I found to do this is to call clone_channel
and pass it to the actor itself so the actor can call it later (vector of MessageChannel
).
So there are two questions:
notify_all
?xtra
such it will notify all the actors that have handlers for the Message for example?Thank you!
if (iID == AC_VEHICLE && !IS_INTERNAL_BUILD)
return false;
this was the output
Currently, we have two ways of constructing a SendFuture
: One that retains the actor type and one that boxes the inner future up. The latter is required for MessageChannel
to work.
As a consequence, we need to have another SendFuture
exposed by the channel implementation that we can nest in the upper-most SendFuture
. This indirection is a bit annoying and makes things harder to understand. It also makes things like #120 harder to implement. I think that this particular instrumentation feature would be easier to build if there were less indirection.
Would it be worth it to always pay the cost of an additional allocation and inline the inner SendFuture
in the outer one in favor of less indirection and features like #120 being easier to implement? I also have a feeling that most of the other instrumentation would be easier if we had less indirection here.
When sending async messages like A -[send]-> B -[send]-> A
, xtra will be stuck in a deadlock. After researching a bit it seems that this is expected behavior. There was notify
mentioned to mitigate that, but that doesn't seem to work in my case. If it's possible a pointer/example would be helpful.
It'd also nice if it'd be somehow possible to print a warning or something when this happens, because keeping track of who sends messages to whom in an actor system can get quite hard. Going by that reddit thread I can see that it's not an easy problem to solve.
Kudos again for xtra as it is!
Without having a better name for this idea, here is what this is about:
With #85, the eventloop in Context
is becoming very simple. It would amazing if you could polish up the APIs around Sender
, ActorMessage
etc in such a way that we can expose all these types publicly and provide them as fundamental building blocks for an actor system, together with a "basic" event loop that just reads and dispatches messages.
Anything on top like instrumentation, logging or things like #41 could then be solved outside of this library.
With #118, Context::stop_all
is moved to Address::stop_all
.
Whilst doing this change and updating the docs, I was wondering why we need to provide this functionality within xtra? If the behaviour is the exact same as sending a Stop
message to each actor, writing yourself a custom Address::stop_all
function is pretty trivial:
struct Actor;
struct Stop;
#[async_trait]
impl Handler<Stop> for Actor {
type Return = ();
fn handle(&mut self, _: Stop, ctx: &mut Context<Self>) {
ctx.stop();
}
}
// To stop all actors:
address.broadcast(Stop).await;
If we still consider this too much boilerplate, we could provide a xtra-stop-handler
crate that offers:
Stop
message type#[derive(StopHandler)]
Is there a reason why the implementation of stop_all
needs to live xtra
? There is a fair bit of code involved in providing it, like a custom envelope etc. We could save a good few lines of code by deleting it if it can really be expressed with existing building blocks.
IMHO this generally reads better. Actor framework Acteur
is using this notion btw.
Especially in the where
clauses, T: Handles<Msg>
just sounds better IMHO.
Personal taste, so please close if you disagree :).
Hello,
I have run into an issue in xtra
(via spaad
).
It may be my fault, but it's not clear to me.
Agent
actor, and make a public lazy_static!
of itConfig
actor, and make a public lazy_static!
of itThis way anywhere in the application I can just send a message to the actors, it's great.
This may be the reason for my issue, idk.
So for example, in the new()
of Agent
:
#[spaad::entangled]
pub struct Agent {
my_state: HashMap<String, ConfigValue>
}
#[spaad::entangled]
impl Actor for Agent {}
#[spaad::entangled]
impl Agent {
#[spaad::spawn]
pub fn new() -> Self {
let c = futures::executor::block_on(CONFIG.get_config_value());
log::debug!("Got config: {:#?}", c);
Self {
// use config values to make my_state
my_state: { } // ...
}
}
// ...
}
where get_config_value
in this example is a handler.
We never reach Got config: ...
, shockingly we also never reach the inside of get_config_value
.
This is a contrived example, I had the same issue occur before where I was sending messages to the AGENT
lazy_static!
from a tokio task created with spawn
. I spent like a day trying to fix it, and then it began working, but I could not figure out exactly what made it work. I'm pretty sure I'm missing some big piece of information.
AGENT
can handle messages sent to it, however CONFIG
never can. They seem to be identical in implementation and instantiation. CONFIG
succeeds in being created, so it's not like its waiting for the actor to come alive.
Any help would be great, thank you.
With #85, we completely revamp the internals of xtra.
This started a discussion on whether to even should support unbounded channels. The argument would be that backpressure is an important topic and users should care about it. If they don't they can set a pathologically high bound but at least it would be explicit.
Related: #43.
This value could be propagated up from Context::run
and would make it a lot easier to use xtra's actor for tasks that are meant to complete after a while. The result of the task would be the return value from stopped
.
Thoughts?
Actix has support for Recipient
, an alternative to Address
where the actor type is hidden, leaving only the message itself. This is extremely useful for building large APIs across multiple crates where being able to name actors causes dependency cycles.
How feasible would it be for xtra
to support this?
Although it doesn't require much boilerplate at the moment, I think it would be a nice addition to add a derive(Actor)
macro that generates a default Actor
implementation with Stop = ()
.
I'd love to see something like spawn_default
which would use a sensible default Spawner depending on the configured cargo feature
. For async-std, this would default to spawn(&mut AsyncStd)
.
Having a default spawner makes extensions easier. See my xtra-addons
crate, which implements a Registry
. The registry needs to be able to spawn a new actor, so that you can just type MyActor::from_registry
(xactor has the same feature ;-). But spawning a new actor depends on the runtime, so that I have to provide an implementation myself (that's why I only support async-std atm).
If you like to have this feature, I am willing to implement.
This is probably more something to be added in an extension crate, but it would be cool to be able to define actors from closures:
let initial_state = 0u64;
xtra::actor::unbounded(initial_state)
.handle(|name: String, ctx: &mut Context| async move {
format!("Hello {name}")
});
Not sure how nameable the actor type would be (some nested AndHandler<HandlerFn<String, String>, AndHandler<HandlerFn<u64, u64>>>
stuff probably) so perhaps one would only be able to access such an actor through MessageChannel
s?
With #85, we are introducing 3 mailboxes: ordered, prioritized and broadcast.
Ordered exists because BinaryHeaps
don't preserve insertion order and thus, messages with the same priority (like 0 as the default) could be processed in a different order, assuming they are used with split_receiver
and the processing thus happens async.
I am claiming that this is way too much of a detail to expose to the user (which has to eventually learn that there are 3 mailboxes) so my suggestion would be to:
a) Use a data structure that preserves insertion order
b) Accept the re-ordering of messages
I am not data structure expert but couldn't we for example also use an atomic counter to tag each incoming message and sort the heap based on priority and message counter? That should preserve the sending order even among messages with the same priority.
I am trying to integrate xtra
with libp2p
.
libp2p
's main design is a big state machine that is called a Swarm
. I has a cancellation-safe, async next
method but also a manual poll_next_unpin
function that advances the internal state.
Calling this function is necessary to have the network layer (represented by the Swarm
) make progress. Additionally, in order to interact with the Swarm
, one needs a mutable reference.
It would be nice if xtra
could expose a way of embedding another async component like Swarm
that could be polled as part of its event loop. Polling the Swarm
yields events which I'd like to handle within the actor, probably as notifications to itself?
attach_stream
is not an option because I still need a &mut reference to the Swarm
whenever I want to interact with the network layer.
The only way of integrating xtra
with libp2p
at the moment is to Arc<Mutex>
the Swarm
. This works but it would be nice if there were a more elegant solution!
Thoughts?
A Context
is usually not available other than from within an actor's Handler
and thus, this feature cannot be used from all places.
I'd claim that most likely, a piece of code shutting down all instances of a set of actors will be outside those actors and thus, would likely want to do this from the Address
. An Address
can still be acquired from a Context
so this can still be used from within a Handler
.
Additionally, to make progress on #91, we need to remove as many APIs from Context
as possible and this one is using the sender which is already embedded in an Address
so it is easy to port.
This was a mistake on my part. For anyone who need to use 0.5.0-beta.5 before I can release 6, you can work around this by doing:
pub struct WasmBindgen;
impl Spawner for WasmBindgen {
fn spawn<F: Future<Output = ()> + Send + 'static>(&mut self, fut: F) {
wasm_bindgen_futures::spawn_local(fut)
}
}
Add keywords to the cargo.toml
In the current beta 0.5.0-beta.5
, an actor is not automatically dropped when its last Address
is dropped.
This behavior can be caused by the following code:
use xtra::prelude::*;
use xtra::spawn::AsyncStd;
struct Printer {
times: usize,
}
impl Printer {
fn new() -> Self {
Printer { times: 0 }
}
}
#[async_trait::async_trait]
impl Actor for Printer {
async fn stopped(&mut self) {
println!("Printer is stopped");
}
}
struct Print(String);
impl Message for Print {
type Result = ();
}
#[async_trait::async_trait]
impl Handler<Print> for Printer {
async fn handle(&mut self, print: Print, _ctx: &mut Context<Self>) {
self.times += 1;
println!("Printing {}. Printed {} times so far.", print.0, self.times);
}
}
#[async_std::main]
async fn main() {
{
let addr = Printer::new().create(None).spawn(&mut AsyncStd);
addr.send(Print("hello".to_string()))
.await
.expect("Printer should not be dropped");
}
println!("Printer should be dropped here");
}
With this code, Printer is actually never dropped, even until the application stops.
This is probably because of refcount.rs line 74, where the strong refcount is checked against ==2
.
It's only 1
in the example, though.
In actix, an actor can be spawn in another thread by start_in_arbiter method. I wonder if it is supported in xtra too.
Currently, I fail to find a equivalent method in xtra
. Maybe I can achieve this feature by creating an actor in another thread and spawn it, and use a OneShot
channel to return the address to main thread?
With this feature, CPU intensive actors can be spawned in a new OS thread to boost performance.
Code examples should be included in the documentation to allow the library to easily be picked up. Luckily, the API surface is quite small, so this shouldn't be so much of a challenge.
It would be nice if xtra
could detect deadlocks where two actors use .send
to invoke each other from their handlers. Perhaps we can implement some form of "delayed" log message that triggers after a handler has been processing for longer than, say, 5 seconds? If the message itself is also logged, that would help a lot in finding these kind of bugs!
First a with-tokio-0_3
feature will be added along with the deprecation of with-tokio-0_2
, and then when 0.6.0 releases with-tokio-0_2
will probably be removed unless there is significant desire for it.
This would probably require a LocalAddress struct in the outward facing api, as well as LocalAddressWeak.
If you feel that this is useful to xtra, feel free to import the Registry
and Broker
functionality from xtra-addons
crate into xtra
. The work is based on what currently exists in xactor and brings xtra feature-wise en par with xactor. Xtra's feature set is even greater as it allows for actors sharing the same message queu (work stealing) which xactor currently lacks, and also xactor does not have the notification system that xtra has (but is generally faster).
Based on the docs and how it is used in Context::select
I think I understand what this API is for. However, putting myself into the shoes of an outside user, I think this API is too nuanced to be exposed / exist and when encountering it, it is hard to "do the right thing" (i.e. choose between Drop
and cancel
) without understanding the internals of xtra
.
If I understand correctly, this goes back to #94 and the fact that binary heaps are not insertion order stable. If we had only a fixed set of priorities (as suggested in #92), we could just re-insert the message at the front without having messages perceived as out of order, correct?
I think we should work towards removing the ReceiveFuture::cancel
API.
Thoughts?
IMO, they both basically represent the same thing.
When doing this, it would also be good to capture the actors name using std::any::type_name
as a &'static str
in the error for the Display
impl.
It would be great if MessageChannel
and WeakMessageChannel
would implement Clone
like Address
. I have a patch for this based on v0.4.2
of this crate since v0.5
still seems to be under development.
Create aliases to format and check formatting.
Use command-line options to configure grouping of imports.
Whilst working on #51, I came to the conclusion that I'd like to propose to remove the stopping
function and with it the concept of KeepRunning
.
The reasons why I consider it problematic are:
KeepRunning::StopAll
, we may terminate actors without them being able to prevent that. It seems weird that a single actor can intervene shutdown (when initiated via Context#stop
) but other actors cannot.Context#stop
. It seems much cleaner to use a supervisor pattern (see https://github.com/itchysats/itchysats/blob/master/xtras/src/supervisor.rs for example) to restart an actor when it was shut down. With the recent addition of Actor#Stop
, this is actually super easy and clean to implement.The current features of stopping
and KeepRunning
can still be achieved through other means:
Context
can be done with a custom message and the notify_all
functionRemoving stopping
and KeepRunning
would IMO also simplify a lot of the state management in Context
and make #51 easier to implement / finish. What do you think?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.