riker-rs / riker

Easily build efficient, highly concurrent and resilient applications. An Actor Framework for Rust.
https://riker.rs
MIT License
1.02k stars 69 forks source link

How to use a (diesel) database thru riker? #103

Open igalic opened 4 years ago

igalic commented 4 years ago

in transforming a project to async, we've hit a bit of a wall: https://github.com/Plume-org/Plume/pull/777 that wall is the database, or the way we currently (have to) use it. i was looking into using riker as the actor framework of choice, instead of creating an ad-hoc solution by ourselves

based on some unsuccessful experiments however, I'm not sure riker alone will do.

especially considering this comment: https://github.com/riker-rs/riker/pull/98#issuecomment-637965542 if my Context is a Connection, that can't be read only! but it also doesn't make sense for the Actor holding a Connection Context, to recv a Connection Msg

the correct message would be either String or Statement

i feel like the tools are there in riker to do this, buti don't have the understanding of how to put them together yet

(and if they aren't in riker alone, they should be in https://github.com/riker-rs/riker-cqrs (but that library's documentation is even more incomprehensible to me))

aav commented 4 years ago

If I understand your question - you are trying to find a way of how to create an actor that wraps a mutable database connection. Of course, it's not a good idea to pass the connection from outside.

The actor can be seen as a container of something mutable, that due to some unavoidable errors may get into an invalid state. i.e. due to panic. In this case, the supervision mechanism kicks in and restarts (recreates) the whole actor. After that, the actor's state becomes consistent again and is able to react to further mess messages, that from the inbox.

So, in your case, the actor should be responsible for opening the database connection.

It is often the case when actor creation requires some parameters, i.e. database connection url (or alike). Riker provides such a facility, by having actor_of_args/2 function.

Please take a look at the example:

use std::time::Duration;
use riker::actors::*;

struct MyDatabase;

impl MyDatabase {
    fn connect(_url: String) -> Self {
        MyDatabase{}
    }

    fn query(&mut self) {
    }
}

struct MyDatabaseActor {
    database: MyDatabase
}

#[derive(Debug, Clone)]
enum Query {
    QueryOk,
    QueryPanic,
}

impl Actor for MyDatabaseActor {
    type Msg = Query;

    fn recv(&mut self, _ctx: &Context<Self::Msg>, msg: Self::Msg, _sender: Sender) {
        match msg {
            Query::QueryOk =>
                self.database.query(),

            Query::QueryPanic =>
                // i.e. network connection lost
                panic!("simulate panic"),
        }
    }
}

impl ActorFactoryArgs<String> for MyDatabaseActor {
    fn create_args(database_url: String) -> Self {
        println!("database connection to {:?} established", database_url);
        MyDatabaseActor{database: MyDatabase::connect(database_url)}
    }
}

fn main() {
    let sys = ActorSystem::new().unwrap();
    let database = sys.actor_of_args::<MyDatabaseActor, _>("my-actor", String::from("db://....")).unwrap();

    database.tell(Query::QueryOk, None);
    // force panic and see that actor restarts
    database.tell(Query::QueryPanic, None);

    std::thread::sleep(Duration::from_millis(50000));
}

As you can, we pass the connection URL, and the actor itself is responsible for initializing its internal state. In case, our code panics, the supervisor will restart the actor, and the connection will be re-created.

I hope I was able to answer your question.

filipcro commented 4 years ago

Why not use connection pool? Opening and closing connection for each query can decrease performance in some applications.

aav commented 4 years ago

@filipcro of course, connection pool can be used/created, but this is slightly different topic.

leenozara commented 4 years ago

Just add to this:

Implementing database connectivity depends on what you're using actors to represent, how you're modeling your system. For example, if you have an actor that is intermittently measuring a value, say the price of SP 500 index, and it is acting upon that value by following a series of rules but doesn't depend on previous values, then it isn't responsible for the persistence of those values. It might decide to share the value measured, either directly to an actor or distributed to a channel. Other actors can then choose to do something with that value, including persist to a database. In the case of writing to the database the example provided by @aav where a single actor is responsible for database read/writes makes sense.

If however your actor is making decisions, i.e. changing it's behavior, based on the current value and historical values (by reading from the database), then writing to the database becomes essential - the actor needs to confirm the value has been written to the database before handling the next message. In this case you can use a database connection reference. I personally have not used Diesel, but what counts is that your connection reference be Send and Sync. By being Sync it doesn't matter that Context is read-only. The read-only really refers to the internal state of the actor which is owned and managed by the System. You're free to have values, including database connections, that are shared by multiple actors because the value would be owned by the reference itself. If your Diesel connection isn't already Sync then you can wrap it in Arc, and if it requires mutable then use Mutex or other guard. E.g. Arc<Mutex<Connection>>>.

The first example above is more along the lines of logging, or read-side data persistence (e.g. for viewing on a webpage). The second example is more in line with what you'd expect from a Persistent Actor and based on write-side.

I'm not sure how helpful that is, but you're facing a very common design choice. Future versions of Riker will provide both API code level support (Persistent Actors) and guidance in the form of documentation for working with shared mutable types across actors.

olexiyb commented 4 years ago

BTW take a look into sqlx. It supports multiple db, it's native driver, no need to use C wrapper libpq for postrgres DB, I found the syntax is easier to understand, has macros to verify query during compilation.

igalic commented 4 years ago

the reason we went with Diesel, which is still very sync, was that back in the day when everything was sync, we needed a database framework that does migrations. we might be stuck with it, for the time being, until sqlx has migrations, or Diesel goes async.

kunjee17 commented 3 years ago

@igalic sqlx beta is having migration now. You should check out sqlx cli readme. Hopefully it should be there in complete version soon.

update it is no more in beta. Also extend support to actix runtime as well.