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

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

Open
igalic opened this issue Jun 3, 2020 · 7 comments
Open

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

igalic opened this issue Jun 3, 2020 · 7 comments

Comments

@igalic
Copy link
Contributor

igalic commented Jun 3, 2020

in transforming a project to async, we've hit a bit of a wall: Plume-org/Plume#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:
#98 (comment)
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
Copy link

aav commented Jun 4, 2020

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
Copy link

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

@aav
Copy link

aav commented Jun 22, 2020

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

@leenozara
Copy link
Contributor

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
Copy link
Contributor

olexiyb commented Jul 7, 2020

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
Copy link
Contributor Author

igalic commented Jul 7, 2020

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
Copy link

kunjee17 commented Nov 1, 2020

@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.

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

No branches or pull requests

6 participants