Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial implementation of actor runtime #99

Merged
merged 36 commits into from Jan 9, 2024

Conversation

danielgerlag
Copy link
Contributor

@danielgerlag danielgerlag commented Aug 3, 2023

Actor Example

This example demonstrates the Dapr actor framework. To author an actor,

  1. Create a struc decorated with the #[dapr::actor] macro to house your custom actor methods that map to Axum handlers, use Axum extractors to access the incoming request and return an impl IntoResponse.
    Use the DaprJson extractor to deserialize the request from Json coming from a Dapr sidecar.

    #[dapr::actor]
    struct MyActor {
        id: String,
        client: ActorContextClient
    }
    
    #[derive(Serialize, Deserialize)]
    pub struct MyRequest {
        pub name: String,
    }
    
    #[derive(Serialize, Deserialize)]
    pub struct MyResponse {
        pub available: bool,
    }   
    
    impl MyActor {
        fn do_stuff(&self, DaprJson(data): DaprJson<MyRequest>) -> Json<MyResponse> {        
            println!("doing stuff with {}", data.name);        
            Json(MyResponse { 
                available: true 
            })
        }    
    }

    There are many ways to write your actor method signature, using Axum handlers, but you also have access to the actor instance via self. Here is a super simple example:

    pub async fn method_2(&self) -> impl IntoResponse {
        StatusCode::OK
    }
  2. Implement the Actor trait. This trait exposes the following methods:

    • on_activate - Called when an actor is activated on a host
    • on_deactivate - Called when an actor is deactivated on a host
    • on_reminder - Called when a reminder is recieved from the Dapr sidecar
    • on_timer - Called when a timer is recieved from the Dapr sidecar
    #[async_trait]
    impl Actor for MyActor {
        
        async fn on_activate(&self) -> Result<(), ActorError> {
            println!("on_activate {}", self.id);
            Ok(())
        }
    
        async fn on_deactivate(&self) -> Result<(), ActorError> {
            println!("on_deactivate");
            Ok(())
        }
    }
  3. An actor host requires an Http server to recieve callbacks from the Dapr sidecar. The DaprHttpServer object implements this functionality and also encapsulates the actor runtime to service any hosted actors. Use the register_actor method to register an actor type to be serviced, this method takes an ActorTypeRegistration which specifies

    • The actor type name (used by Actor clients), and concrete struct
    • A factory to construct a new instance of that actor type when one is required to be activated by the runtime. The parameters passed to the factory will be the actor type, actor ID, and a Dapr client for managing state, timers and reminders for the actor.
    • The methods that you would like to expose to external clients.
    let mut dapr_server = dapr::server::DaprHttpServer::new();
    
    dapr_server.register_actor(ActorTypeRegistration::new::<MyActor>("MyActor", 
        Box::new(|actor_type, id, client| Arc::new(MyActor{
            actor_type, 
            id, 
            client
        })))
        .register_method("do_stuff", MyActor::do_stuff)
        .register_method("do_other_stuff", MyActor::do_other_stuff));
    
    dapr_server.start(None).await?;

@danielgerlag
Copy link
Contributor Author

@artursouza

@yaron2
Copy link
Member

yaron2 commented Aug 18, 2023

@danielgerlag notice the linter failures

@yaron2
Copy link
Member

yaron2 commented Aug 18, 2023

@NickLarsenNZ can you help review this?

Copy link
Contributor

@NickLarsenNZ NickLarsenNZ left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution @danielgerlag, it is exciting to see actors come to the dapr/rust-sdk.
Also, really appreciate the tests given that the rest of the SDK appears to lack them.

I have left a few comments which I hope are not taken as merge-blockers, but rather as conversation starters which might be helpful both in terms of design but also for future contributors.

With the SDK still being alpha, I'm of the opinion that we should be taking in these sorts of contributions even if they need some adjusting down the track. That will keep contributors interested as well as let people try things out early (it will be trickier once this is out of alpha).


meta: @yaron2, I think it could be a good idea for non-trivial decisions to be recorded as LADRs (Lightweight Architecture Decision Records) for two main reasons:

  1. There is a recorded explanation of why things are they way they are for future contributors.
  2. To help reviewers skip over things that are inconsequential, and to focus on the important parts (remove blockers, and avoid having to prompt for explanations on each PR).


1. Create a struct with your custom actor methods that map to [Axum handlers](https://docs.rs/axum/latest/axum/handler/index.html), use [Axum extractors](https://docs.rs/axum/latest/axum/extract/index.html) to access the incoming request and return an [`impl IntoResponse`](https://docs.rs/axum/latest/axum/response/trait.IntoResponse.html).
Use the `DaprJson` extractor to deserialize the request from Json coming from a Dapr sidecar.
```rust
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: I think the example should also include the struct MyActor {] and any derive macros necessary.

}

#[macro_export]
macro_rules! actor {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it might be preferential to provide a derive or proc macro to tightly couple it with the struct?

eg:

#[dapr::actor]
struct MyActor {}

or

#[derive(dapr::actor)]
struct MyActor {}

The main issue I have with the macro_rules version is how decoupled it is from the struct definition.

  • As a code reader, there's nothing on the struct giving hints as to what it is for (doc comments and struct name aside). I would have to look somewhere around it (if I knew what I was looking for).
  • As an actor implementor it is not so clear where the macro should be put (eg: inside/outside a function). Of course the compiler will likely make this clear when it is in the wrong place, otherwise the implementor might need to dig into the macro source to get a hint where it goes (documentation aside).
  • As an implementor it is easy to forget to mark the trait, whereas when I am defining the struct that is the place where I am already thinking about which macros to put on it.

A lot of this comes from personal taste (as in, not from an authoritative standpoint), so I'm interested in generating some conversation around it to see what others think from a user perspective.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think from a dev experience point of view... this would be better, but I think we need a separate crate to host proc macros. I might need some help with pipelines / publishing to do that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@NickLarsenNZ I have changed this to use the #[dapr::actor] attribute macro, like this:

#[dapr::actor]
struct MyActor {}

Not sure if we need changes for publishing the macro crate.

log = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
axum = {version = "0.6.19", features = ["default", "headers"] }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I understand it, this will only work with HTTP (hyper underneath), and not gRPC (tonic underneath)?

Will the actors work with for Dapr in gRPC mode?

My assumption here is that it only works in HTTP mode (because of axum, with hyper underneath).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do see Tonic being used in the client example, so I assume this is a non-issue. Will leave the comment there anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dapr side car currently does not support invoking actors via grpc as far as I know, so any actor host would have to support http.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dapr side car currently does not support invoking actors via grpc as far as I know, so any actor host would have to support http.

That's correct

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yaron2 Is it supposed to be upgraded soon ? or has it be ?

mdata = m;
}

mdata.insert("Content-Type".to_string(), "application/json".to_string());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the dapr runtime always expect JSON or are other serialization formats supported (eg: protobufs)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dapr runtime doesn't seem to care, but according to this dapr doc page, JSON is the preferred default serialization - https://docs.dapr.io/developing-applications/sdks/sdk-serialization/
The other language SDKs seem to serialize actor payloads to JSON automatically, but we probably also need a way to override it?

Comment on lines +34 to +40
match self {
ActorError::NotRegistered => write!(f, "Actor not registered"),
ActorError::CorruptedState => write!(f, "Actor state corrupted"),
ActorError::MethodNotFound => write!(f, "Method not found"),
ActorError::ActorNotFound => write!(f, "Actor not found"),
ActorError::MethodError(e) => write!(f, "Method error: {}", e),
ActorError::SerializationError() => write!(f, "Serialization error"),
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to suggest using thiserror to keep the error text with the error variants, but it isn't a dependency.
@yaron2, what is preferred going forward in terms of consistency? Or being alpha, are we just looking to get stuff working first?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be a further optimization down the road, yes.

Comment on lines 21 to 85
pub struct ActorTypeRegistration {
name: String,
factory: ActorFactory,
method_registrations: HashMap<String, Box<dyn (FnOnce(Router, Arc<ActorRuntime>) -> Router) + Send + Sync>>,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This applied across the board, but I think there need to be some doc comments to describe the types and methods.

let app = dapr_server.build_test_router().await;
let server = TestServer::new(app.into_make_service()).unwrap();

let invoke_resp = server.put(&format!("/actors/MyActor/{}/method/do_stuff", actor_id)).json(&json!({ "name": "foo" })).await;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: the format string could have the variable inside it, like so:

format!("/actors/MyActor/{actor_id}/method/do_stuff")

Comment on lines 8 to 80
pub struct DaprHttpServer {
actor_runtime: Arc<ActorRuntime>,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is more for my own understanding to reconcile implementing existing building blocks in this SDK, and implementing the actors building block.

Is this intended to be a new pattern for launching the dapr server?

Eg: there's the existing tonic way for the AppCallback. Is this the Hyper (via Axum) counterpart?

On that note, is there something special about actors that makes it different to launch than the existing AppCallback way? I haven't had a good look at the latest protos to see what is defined there (yet). I also asked a variation of this further up, so maybe the answer will be up there.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actors only support callbacks via HTTP today

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, dapr only supports Http for actors... but this could also be a starting point for all other Http callbacks.


1. Start actor host (expose Http server receiver on port 50051):
```bash
dapr run --app-id actor-host --app-protocol http --app-port 50051 cargo run -- --example actor-server
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be worth pointing out that only --app-protocol http is support (if that is indeed the case).
Is it expected that --app-protocol grpc might one day be available?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One day, but not today :)

@NickLarsenNZ NickLarsenNZ mentioned this pull request Aug 18, 2023
14 tasks
Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>
Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>
Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>
Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>
Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>
Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>
@danielgerlag
Copy link
Contributor Author

@danielgerlag notice the linter failures

@yaron2 I do not... where do I see them?

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>
Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>
Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>
Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>
Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>
Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>
Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>
Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>
Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>
Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>
Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>
Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>
Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>
Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>
@yaron2
Copy link
Member

yaron2 commented Oct 2, 2023

@NickLarsenNZ would appreciate another review when possible. thanks!

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>
@danielgerlag
Copy link
Contributor Author

@NickLarsenNZ

@danielgerlag
Copy link
Contributor Author

@yaron2

@yaron2
Copy link
Member

yaron2 commented Nov 14, 2023

@NickLarsenNZ can you review please?

@danielgerlag
Copy link
Contributor Author

@NickLarsenNZ I implemented your suggestion around the macro... could you please re-review?

@yaron2
Copy link
Member

yaron2 commented Jan 3, 2024

@danielgerlag please resolve the conflicts and we can then merge

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>
Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>
Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>
Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>
@danielgerlag
Copy link
Contributor Author

@yaron2 I have resolved the conflicts. I have also updated the CI to publish the macros crate, but am unable to verify that it is correct... if someone could help with that?

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>
Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>
@yaron2 yaron2 merged commit 8db69d8 into dapr:master Jan 9, 2024
5 checks passed
@yaron2
Copy link
Member

yaron2 commented Jan 9, 2024

Merged! Thanks for this great contribution @danielgerlag. Please update our docs in the docs repo to add Rust Actors there where neded.

@cscetbon
Copy link
Contributor

@danielgerlag Can you elaborate on what is missing in that implementation to fully use actors in Rust ?

cscetbon pushed a commit to cscetbon/rust-sdk that referenced this pull request Jan 20, 2024
* actor implementation

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>

* wip

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>

* wip

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>

* wip

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>

* wip

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>

* nits

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>

* tests

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>

* make client cloneable

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>

* logs

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>

* logging

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>

* async methods

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>

* shutdown semantics

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>

* clone actor client context

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>

* actor implementation

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>

* wip

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>

* move tests

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>

* actor factory

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>

* wip

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>

* wip

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>

* readme

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>

* pr feedback Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>

* cargo fmt

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>

* cargo clippy --fix

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>

* proc macro

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>

* dependency shuffle

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>

* Update lib.rs

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>

* docs

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>

* enable decorating type alias

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>

* graceful shutdown

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>

* merge issues

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>

* cargo fmt

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>

* update rust version

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>

* publish macro crate

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>

* dependency issue

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>

* clippy warnings

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>

---------

Signed-off-by: Daniel Gerlag <daniel@gerlag.ca>
@danielgerlag
Copy link
Contributor Author

@cscetbon, I don't think there are any missing features in terms of the Dapr actor model & spec.
I am using it today in another project. The Dapr sidecar itself however, still only supports Http calls for actors, but it is working using that.

@cscetbon
Copy link
Contributor

Thanks @danielgerlag the title of this PR was confusing. I tried to ping you on discord to get more information but without success.

@cscetbon
Copy link
Contributor

cscetbon commented Feb 4, 2024

@cscetbon, I don't think there are any missing features in terms of the Dapr actor model & spec. I am using it today in another project. The Dapr sidecar itself however, still only supports Http calls for actors, but it is working using that.

@danielgerlag Using your code it doesn't seem we're able to send a message to another actor without recreating manually a new connection to dapr. If I'm not mistaking it's because client is private in struct ActorContextClient and it seems to be the only way to access the invoke_actor method.

@danielgerlag
Copy link
Contributor Author

@cscetbon the invoke_actor method would be on the main dapr client. If you want this client available inside your actor, you can pass it into through the factory method with you register the actor, it allows you to define how an actor should be constructed.

@cscetbon
Copy link
Contributor

cscetbon commented Feb 7, 2024

Thanks @danielgerlag, I am able to make it work. However I have to clone the client twice (one to avoid a move and one to make it mutable to call invoke_actor as it requires a mutable instance), any idea how to avoid this ?

...
#[actor]
struct MyActor {
  id: String,
  dapr_client: DaprClient,
}

impl MyActor {

  async fn method_a(&self, DaprJson(req): DaprJson<MyRequest>) -> Result<Json<MyResponse>, ActorError> {
    ...

    let mut client = self.dapr_client.clone(); // CLONE HERE to make it mutable
    let _: Result<MyResponse, dapr::error::Error> = client
     .invoke_actor("MyActor", "a100", "method_b", data, None)
     .await;
     ...
  }
  async fn method_b(&self, DaprJson(req): DaprJson<MyRequest>) -> Result<Json<MyResponse>, ActorError> {
    ...
  }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
  env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
  let mut dapr_server = dapr::server::DaprHttpServer::new().await;

  let port: u16 = std::env::var("DAPR_GRPC_PORT").unwrap().parse().unwrap();
  let address = format!("https://127.0.0.1/:{}", port);

  let client = DaprClient::connect(address).await?;

  dapr_server
    .register_actor(
      ActorTypeRegistration::new::<MyActor>(
        "MyActor",
        Box::new(move |_actor_type, actor_id, _| {
          Arc::new(MyActor {
            id: actor_id.to_string(),
            dapr_client: client.clone(), // CLONE HERE to avoid a move out
          })
        }),
      )
      .register_method("method_a", MyActor::method_a),
      .register_method("method_b", MyActor::method_b),
      )
    )
    .await;
  ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants