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

Feature: Add Sqlite session backend #298

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

bbogdan95
Copy link

PR Type

Feature

PR Checklist

  • Tests for the changes have been added / updated.
  • Documentation comments have been added / updated.
  • A changelog entry has been made for the appropriate packages.
  • Format code with the nightly rustfmt (cargo +nightly fmt).

Overview

Implemented Sqlite session backend. The database/table will get created automatically if they don't exist.

Default garbage collection of stale sessions happens through a database trigger. (it can be turned off and the trigger will be dropped, and garbage collection can be implemented outside if needed.)

@bbogdan95
Copy link
Author

I fixed the doctest errors in sqlite.rs (SqliteSessionStore), but I still have these errors locally and I'm not sure what am I missing:

running 11 tests
test src/lib.rs - (line 70) - compile ... ok
test src/lib.rs - (line 41) - compile ... FAILED
test src/middleware.rs - middleware::SessionMiddleware (line 48) - compile ... FAILED
test src/middleware.rs - middleware::SessionMiddleware (line 81) - compile ... FAILED
test src/storage/cookie.rs - storage::cookie::CookieSessionStore (line 14) - compile ... ok
test src/storage/sqlite.rs - storage::sqlite::SqliteSessionStore (line 17) - compile ... ok
test src/session.rs - session::Session (line 265) ... ok
test src/config.rs - config::SessionMiddlewareBuilder<Store>::session_lifecycle (line 252) ... ok
test src/config.rs - config::PersistentSession (line 102) ... ok
test src/session.rs - session::Session (line 25) ... ok
test src/storage/session_key.rs - storage::session_key::SessionKey (line 12) ... ok

failures:

---- src/lib.rs - (line 41) stdout ----
error[E0432]: unresolved import `actix_session::storage::RedisActorSessionStore`
 --> src/lib.rs:43:49
  |
4 | use actix_session::{Session, SessionMiddleware, storage::RedisActorSessionStore};
  |                                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ no `RedisActorSessionStore` in `storage`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0432`.
Couldn't compile the test.
---- src/middleware.rs - middleware::SessionMiddleware (line 48) stdout ----
error[E0432]: unresolved import `actix_session::storage::RedisActorSessionStore`
 --> src/middleware.rs:50:49
  |
4 | use actix_session::{Session, SessionMiddleware, storage::RedisActorSessionStore};
  |                                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ no `RedisActorSessionStore` in `storage`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0432`.
Couldn't compile the test.
---- src/middleware.rs - middleware::SessionMiddleware (line 81) stdout ----
error[E0432]: unresolved import `actix_session::storage::RedisActorSessionStore`
 --> src/middleware.rs:83:49
  |
4 | use actix_session::{Session, SessionMiddleware, storage::RedisActorSessionStore};
  |                                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ no `RedisActorSessionStore` in `storage`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0432`.
Couldn't compile the test.

failures:
    src/lib.rs - (line 41)
    src/middleware.rs - middleware::SessionMiddleware (line 48)
    src/middleware.rs - middleware::SessionMiddleware (line 81)

test result: FAILED. 8 passed; 3 failed; 0 ignored; 0 measured; 0 filtered out; finished in 4.53s

Any suggestions please ?

[dev-dependencies]
actix-session = { path = ".", features = ["cookie-session", "redis-actor-session", "redis-rs-session"] }
actix-session = { path = ".", features = ["cookie-session", "sqlite-session"] }
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
actix-session = { path = ".", features = ["cookie-session", "sqlite-session"] }
actix-session = { path = ".", features = ["cookie-session", "redis-actor-session", "sqlite-session"] }

We should keep enabling all features for the test suite, that's where your error is coming from.

Comment on lines -60 to +66
required-features = ["redis-actor-session"]
required-features = ["sqlite-session"]
Copy link
Contributor

Choose a reason for hiding this comment

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

You should add a new example for sqlite instead of modifying the existing one for Redis.

/// executes before every insert on the `sessions` table.
pub fn new(
pool: Pool<SqliteConnectionManager>,
garbage_collect: bool,
Copy link
Contributor

Choose a reason for hiding this comment

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

It's almost never a good idea to use a boolean in a public API - let's add an enum with two variants using self-describing names.

Comment on lines +93 to +125
conn.execute(
"CREATE TABLE IF NOT EXISTS sessions (
id INTEGER NOT NULL,
session_key TEXT NOT NULL,
session_state TEXT NOT NULL,
expiry INTEGER NOT NULL,
PRIMARY KEY(id AUTOINCREMENT)
)",
[],
)
.map_err(Into::into)
.map_err(LoadError::Other)?;

// in order to garbage collect stale sessions, we will use a trigger
if garbage_collect {
conn.execute(
"
CREATE TRIGGER IF NOT EXISTS garbage_collect
BEFORE INSERT
ON sessions
BEGIN
DELETE FROM sessions WHERE expiry < STRFTIME('%s');
END;
",
[],
)
.map_err(Into::into)
.map_err(LoadError::Other)?;
} else {
conn.execute("DROP TRIGGER IF EXISTS garbage_collect", [])
.map_err(Into::into)
.map_err(LoadError::Other)?;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a one-off setup action that should not be executed every single time somebody tries to use Sqlite as a session backend in their applications.
It should live in a separate routine, new should assume that the initialization has already taken place.

async fn load(&self, session_key: &SessionKey) -> Result<Option<SessionState>, LoadError> {
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());

let conn = self.pool.get().unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

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

You don't want to unwrap here.

session_key TEXT NOT NULL,
session_state TEXT NOT NULL,
expiry INTEGER NOT NULL,
PRIMARY KEY(id AUTOINCREMENT)
Copy link
Contributor

Choose a reason for hiding this comment

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

This allows for multiple rows sharing the same session_key - that's not what we want. I'd suggest using the session_key as primary key or, at the very least, add a UNIQUE constraint on it.

.unwrap()
.unix_timestamp();

let conn = self.pool.get().unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

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

We do not want to unwrap here.

Comment on lines +194 to +197
let mut stmt = match statement {
Ok(v) => v,
Err(e) => return Err(SaveError::Other(anyhow::anyhow!(e))),
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
let mut stmt = match statement {
Ok(v) => v,
Err(e) => return Err(SaveError::Other(anyhow::anyhow!(e))),
};
let mut stmt = statement.map_err(Into::into).map_err(SaveError::Other)?;

async fn delete(&self, session_key: &SessionKey) -> Result<(), anyhow::Error> {
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());

let conn = self.pool.get().unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

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

We do not want to unwrap here.

@LukeMathWalker
Copy link
Contributor

Left a bunch of comments - sorry for the long delay!

Another issue that needs to be addressed: rustqlite offers a sync API, while we are running in an async context. We'll need to spawn the queries on the blocking task pool to avoid blocking the runtime.

@jayvdb
Copy link
Contributor

jayvdb commented Feb 14, 2024

@bbogdan95 , will you be able to complete this?

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

3 participants