-
Notifications
You must be signed in to change notification settings - Fork 7
Use expireAt as index #11
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
Changes from all commits
89981b2
db6a76f
374d776
9129246
a5b3fe2
50d5d1c
a94f090
459341b
345f977
92ec601
0735bda
281f623
a5bad73
b49ea3d
6dbf35b
56a644a
bb65d43
af0f846
636f3a7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,7 @@ authors = [ | |
"Yoshua Wuyts <[email protected]>", | ||
"Irina Shestak <[email protected]>", | ||
"Anton Whalley <[email protected]>", | ||
"Javier Viola <[email protected]>" | ||
] | ||
|
||
[features] | ||
|
@@ -24,6 +25,7 @@ async-trait = "0.1.36" | |
serde_json = "1.0.56" | ||
|
||
[dev-dependencies] | ||
async-std = { version = "1.6.2", features = ["attributes"] } | ||
async-std = { version = "1.8.0", features = ["attributes"] } | ||
rand = {version = "0.7.3"} | ||
lazy_static = "1" | ||
tide = "0.15" |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -46,12 +46,66 @@ | |||||
$ cargo add async-mongodb-session | ||||||
``` | ||||||
|
||||||
## Configuration | ||||||
## Overview | ||||||
By default this library utilises the document expiration feature based on [specific clock time](https://docs.mongodb.com/manual/tutorial/expire-data/#expire-documents-at-a-specific-clock-time) supported by mongodb to auto-expire the session. | ||||||
|
||||||
For other option to offloading the session expiration to the mongodb layer check the [Advance options](#advance-options). | ||||||
|
||||||
## Example with tide | ||||||
Create an HTTP server that keep track of user visits in the session. | ||||||
|
||||||
```rust | ||||||
#[async_std::main] | ||||||
async fn main() -> tide::Result<()> { | ||||||
tide::log::start(); | ||||||
let mut app = tide::new(); | ||||||
|
||||||
app.with(tide::sessions::SessionMiddleware::new( | ||||||
MongodbSessionStore::new("mongodb://127.0.0.1:27017", "db_name", "collection").await?, | ||||||
std::env::var("TIDE_SECRET") | ||||||
.expect( | ||||||
"Please provide a TIDE_SECRET value of at \ | ||||||
least 32 bytes in order to run this example", | ||||||
) | ||||||
.as_bytes(), | ||||||
)); | ||||||
|
||||||
app.with(tide::utils::Before( | ||||||
|mut request: tide::Request<()>| async move { | ||||||
let session = request.session_mut(); | ||||||
let visits: usize = session.get("visits").unwrap_or_default(); | ||||||
session.insert("visits", visits + 1).unwrap(); | ||||||
request | ||||||
}, | ||||||
)); | ||||||
|
||||||
app.at("/").get(|req: tide::Request<()>| async move { | ||||||
let visits: usize = req.session().get("visits").unwrap(); | ||||||
Ok(format!("you have visited this website {} times", visits)) | ||||||
}); | ||||||
|
||||||
app.at("/reset") | ||||||
.get(|mut req: tide::Request<()>| async move { | ||||||
req.session_mut().destroy(); | ||||||
Ok(tide::Redirect::new("/")) | ||||||
}); | ||||||
|
||||||
app.listen("127.0.0.1:8080").await?; | ||||||
|
||||||
Ok(()) | ||||||
} | ||||||
``` | ||||||
## Advance options | ||||||
a [specified number of seconds](https://docs.mongodb.com/manual/tutorial/expire-data/#expire-documents-after-a-specified-number-of-seconds) or in a | ||||||
|
||||||
The specified number of seconds approach is designed to enable the session time out to be managed at the mongodb layer. This approach provides a globally consistent session timeout across multiple processes but has the downside that all services using the same session collection must use the same timeout value. | ||||||
|
||||||
This library utilises the document [expiry feature](https://docs.mongodb.com/manual/tutorial/expire-data/#expire-documents-after-a-specified-number-of-seconds) in mongodb to expire the session. | ||||||
The specific clock time clock time approach is where you require more flexibility on your session timeouts such as a different session timeout per running service or you would prefer to manage the session time out at the process level. This is more flexible but might lead to some perceived inconsistency in session timeout depending on your upgrade/rollout strategy. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was thinking something along the lines of The specific clock time clock time approach is where you require more flexibility on your session timeouts such as a different session timeout per running service or you would prefer to manage the session time out at the process level. This is more flexible but might lead to some perceived inconsistency in session timeout depending on your upgrade/rollout strategy." There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi @No9, Neat!! If you want I can add those lines. 👍
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @pepoviola If you're OK with it then lets add it inbetween the content that already there There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi @No9, added 👍 . Thanks for the help! |
||||||
The management of the expiry feature fits into the 12 factor [admin process definintion](https://12factor.net/admin-processes) so it's recommended to use an process outside of your web application to manage the expiry parameters. | ||||||
|
||||||
## Manual configuration | ||||||
|
||||||
A `created` property is available on the root of the session document that so the [expiry feature](https://docs.mongodb.com/manual/tutorial/expire-data/#expire-documents-after-a-specified-number-of-seconds) can be used in the configuration. | ||||||
|
||||||
If your application code to create a session store is something like: | ||||||
|
@@ -62,14 +116,25 @@ let store = MongodbSessionStore::connect("mongodb://127.0.0.1:27017", "db_name", | |||||
Then the script to create the expiry would be: | ||||||
``` | ||||||
use db_name; | ||||||
db.coll_session.createIndex( { created": 1 } , { expireAfterSeconds: 300 } ) | ||||||
db.coll_session.createIndex( { "created": 1 } , { expireAfterSeconds: 300 } ); | ||||||
``` | ||||||
|
||||||
If you wish to redefine the session duration then the index must be dropped first using: | ||||||
``` | ||||||
use db_name; | ||||||
db.coll_session.dropIndex( { "created": 1 }) | ||||||
db.coll_session.createIndex( { created": 1 } , { expireAfterSeconds: 300 } ) | ||||||
db.coll_session.createIndex( { "created": 1 } , { expireAfterSeconds: 300 } ); | ||||||
``` | ||||||
|
||||||
Other way to set create the index is using `index_on_created` passing the amount of seconds to expiry after the session. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
Also, an `expireAt` property is available on the root of the session document IFF the session expire is set. Note that [async-session doesn't set by default](https://github.com/http-rs/async-session/blob/main/src/session.rs#L98). | ||||||
|
||||||
To enable this [expiry feature](https://docs.mongodb.com/manual/tutorial/expire-data/#expire-documents-at-a-specific-clock-time) at `index` for `expireAt` should be created calling `index_on_expiry_at` function or with this script ( following the above example ) | ||||||
|
||||||
``` | ||||||
use db_name; | ||||||
db.coll_session.createIndex( { "expireAt": 1 } , { expireAfterSeconds: 0 } ); | ||||||
``` | ||||||
|
||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should keep both approaches in the README as well as state when you would use one over the other. |
||||||
## Test | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
extern crate async_mongodb_session; | ||
extern crate tide; | ||
|
||
use async_mongodb_session::MongodbSessionStore; | ||
|
||
#[async_std::main] | ||
async fn main() -> tide::Result<()> { | ||
tide::log::start(); | ||
let mut app = tide::new(); | ||
|
||
app.with(tide::sessions::SessionMiddleware::new( | ||
MongodbSessionStore::new("mongodb://127.0.0.1:27017", "db_name", "collection").await?, | ||
std::env::var("TIDE_SECRET") | ||
.expect( | ||
"Please provide a TIDE_SECRET value of at \ | ||
least 32 bytes in order to run this example", | ||
) | ||
.as_bytes(), | ||
)); | ||
|
||
app.with(tide::utils::Before( | ||
|mut request: tide::Request<()>| async move { | ||
let session = request.session_mut(); | ||
let visits: usize = session.get("visits").unwrap_or_default(); | ||
session.insert("visits", visits + 1).unwrap(); | ||
request | ||
}, | ||
)); | ||
|
||
app.at("/").get(|req: tide::Request<()>| async move { | ||
let visits: usize = req.session().get("visits").unwrap(); | ||
Ok(format!("you have visited this website {} times", visits)) | ||
}); | ||
|
||
app.at("/reset") | ||
.get(|mut req: tide::Request<()>| async move { | ||
req.session_mut().destroy(); | ||
Ok(tide::Redirect::new("/")) | ||
}); | ||
|
||
app.listen("127.0.0.1:8080").await?; | ||
|
||
Ok(()) | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -7,20 +7,20 @@ | |||||
//! use async_session::{Session, SessionStore}; | ||||||
//! | ||||||
//! # fn main() -> async_session::Result { async_std::task::block_on(async { | ||||||
//! let store = MongodbSessionStore::connect("mongodb://127.0.0.1:27017", "db_name", "collection"); | ||||||
//! let store = MongodbSessionStore::new("mongodb://127.0.0.1:27017", "db_name", "collection"); | ||||||
//! # Ok(()) }) } | ||||||
//! ``` | ||||||
|
||||||
#![forbid(unsafe_code, future_incompatible, rust_2018_idioms)] | ||||||
#![deny(missing_debug_implementations, nonstandard_style)] | ||||||
#![warn(missing_docs, missing_doc_code_examples, unreachable_pub)] | ||||||
|
||||||
use async_session::chrono::Utc; | ||||||
use async_session::chrono::{Duration, Utc}; | ||||||
use async_session::{Result, Session, SessionStore}; | ||||||
use async_trait::async_trait; | ||||||
use mongodb::bson; | ||||||
use mongodb::bson::doc; | ||||||
use mongodb::options::ReplaceOptions; | ||||||
use mongodb::options::{ReplaceOptions, SelectionCriteria}; | ||||||
use mongodb::Client; | ||||||
|
||||||
/// A MongoDB session store. | ||||||
|
@@ -29,21 +29,24 @@ pub struct MongodbSessionStore { | |||||
client: mongodb::Client, | ||||||
db: String, | ||||||
coll_name: String, | ||||||
ttl: usize, | ||||||
} | ||||||
|
||||||
impl MongodbSessionStore { | ||||||
/// Create a new instance of `MongodbSessionStore`. | ||||||
/// Create a new instance of `MongodbSessionStore` after stablish the connection to monngodb. | ||||||
/// ```rust | ||||||
/// # fn main() -> async_session::Result { async_std::task::block_on(async { | ||||||
/// # use async_mongodb_session::MongodbSessionStore; | ||||||
/// let store = | ||||||
/// MongodbSessionStore::connect("mongodb://127.0.0.1:27017", "db_name", "collection") | ||||||
/// MongodbSessionStore::new("mongodb://127.0.0.1:27017", "db_name", "collection") | ||||||
/// .await?; | ||||||
/// # Ok(()) }) } | ||||||
/// ``` | ||||||
pub async fn connect(uri: &str, db: &str, coll_name: &str) -> mongodb::error::Result<Self> { | ||||||
pub async fn new(uri: &str, db: &str, coll_name: &str) -> mongodb::error::Result<Self> { | ||||||
let client = Client::with_uri_str(uri).await?; | ||||||
Ok(Self::from_client(client, db, coll_name)) | ||||||
let middleware = Self::from_client(client, db, coll_name); | ||||||
middleware.create_expire_index("expireAt", 0).await?; | ||||||
Ok(middleware) | ||||||
} | ||||||
|
||||||
/// Create a new instance of `MongodbSessionStore` from an open client. | ||||||
|
@@ -70,8 +73,120 @@ impl MongodbSessionStore { | |||||
client, | ||||||
db: db.to_string(), | ||||||
coll_name: coll_name.to_string(), | ||||||
ttl: 1200, // 20 mins by default. | ||||||
} | ||||||
} | ||||||
|
||||||
/// Initialize the default expiration mechanism, based on the document expiration | ||||||
/// that mongodb provides https://docs.mongodb.com/manual/tutorial/expire-data/#expire-documents-at-a-specific-clock-time. | ||||||
/// The default ttl applyed to sessions without expiry is 20 minutes. | ||||||
/// If the `expireAt` date field contains a date in the past, mongodb considers the document expired and will be deleted. | ||||||
/// Note: mongodb runs the expiration logic every 60 seconds. | ||||||
/// ```rust | ||||||
/// # fn main() -> async_session::Result { async_std::task::block_on(async { | ||||||
/// # use async_mongodb_session::MongodbSessionStore; | ||||||
/// let store = | ||||||
/// MongodbSessionStore::new("mongodb://127.0.0.1:27017", "db_name", "collection") | ||||||
/// .await?; | ||||||
/// store.initialize().await?; | ||||||
/// # Ok(()) }) } | ||||||
/// ``` | ||||||
pub async fn initialize(&self) -> Result { | ||||||
&self.index_on_expiry_at().await?; | ||||||
Ok(()) | ||||||
} | ||||||
|
||||||
/// Get the default ttl value in seconds. | ||||||
/// ```rust | ||||||
/// # fn main() -> async_session::Result { async_std::task::block_on(async { | ||||||
/// # use async_mongodb_session::MongodbSessionStore; | ||||||
/// let store = | ||||||
/// MongodbSessionStore::new("mongodb://127.0.0.1:27017", "db_name", "collection") | ||||||
/// .await?; | ||||||
/// let ttl = store.ttl(); | ||||||
/// # Ok(()) }) } | ||||||
/// ``` | ||||||
pub fn ttl(&self) -> usize { | ||||||
self.ttl | ||||||
} | ||||||
|
||||||
/// Set the default ttl value in seconds. | ||||||
/// ```rust | ||||||
/// # fn main() -> async_session::Result { async_std::task::block_on(async { | ||||||
/// # use async_mongodb_session::MongodbSessionStore; | ||||||
/// let mut store = | ||||||
/// MongodbSessionStore::new("mongodb://127.0.0.1:27017", "db_name", "collection") | ||||||
/// .await?; | ||||||
/// store.set_ttl(300); | ||||||
/// # Ok(()) }) } | ||||||
/// ``` | ||||||
pub fn set_ttl(&mut self, ttl: usize) { | ||||||
self.ttl = ttl; | ||||||
} | ||||||
|
||||||
/// private associated function | ||||||
/// Create an `expire after seconds` index in the provided field. | ||||||
/// Testing is covered by initialize test. | ||||||
async fn create_expire_index( | ||||||
&self, | ||||||
field_name: &str, | ||||||
expire_after_seconds: u32, | ||||||
) -> mongodb::error::Result<()> { | ||||||
let create_index = doc! { | ||||||
"createIndexes": &self.coll_name, | ||||||
"indexes": [ | ||||||
{ | ||||||
"key" : { field_name: 1 }, | ||||||
"name": format!("session_expire_index_{}", field_name), | ||||||
"expireAfterSeconds": expire_after_seconds, | ||||||
} | ||||||
] | ||||||
}; | ||||||
self.client | ||||||
.database(&self.db) | ||||||
.run_command( | ||||||
create_index, | ||||||
SelectionCriteria::ReadPreference(mongodb::options::ReadPreference::Primary), | ||||||
) | ||||||
.await?; | ||||||
Ok(()) | ||||||
} | ||||||
|
||||||
/// Create a new index for the `created` property and set the expiry ttl (in secods). | ||||||
/// The session will expire when the number of seconds in the expireAfterSeconds field has passed | ||||||
/// since the time specified in its created field. | ||||||
/// https://docs.mongodb.com/manual/tutorial/expire-data/#expire-documents-after-a-specified-number-of-seconds | ||||||
/// ```rust | ||||||
/// # fn main() -> async_session::Result { async_std::task::block_on(async { | ||||||
/// # use async_mongodb_session::MongodbSessionStore; | ||||||
/// let store = | ||||||
/// MongodbSessionStore::new("mongodb://127.0.0.1:27017", "db_name", "collection") | ||||||
/// .await?; | ||||||
/// store.index_on_created(300).await?; | ||||||
/// # Ok(()) }) } | ||||||
/// ``` | ||||||
pub async fn index_on_created(&self, expire_after_seconds: u32) -> Result { | ||||||
self.create_expire_index("created", expire_after_seconds) | ||||||
.await?; | ||||||
Ok(()) | ||||||
} | ||||||
|
||||||
/// Create a new index for the `expireAt` property, allowing to expire sessions at a specific clock time. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
/// If the `expireAt` date field contains a date in the past, mongodb considers the document expired and will be deleted. | ||||||
/// https://docs.mongodb.com/manual/tutorial/expire-data/#expire-documents-at-a-specific-clock-time | ||||||
/// ```rust | ||||||
/// # fn main() -> async_session::Result { async_std::task::block_on(async { | ||||||
/// # use async_mongodb_session::MongodbSessionStore; | ||||||
/// let store = | ||||||
/// MongodbSessionStore::new("mongodb://127.0.0.1:27017", "db_name", "collection") | ||||||
/// .await?; | ||||||
/// store.index_on_expiry_at().await?; | ||||||
/// # Ok(()) }) } | ||||||
/// ``` | ||||||
pub async fn index_on_expiry_at(&self) -> Result { | ||||||
self.create_expire_index("expireAt", 0).await?; | ||||||
Ok(()) | ||||||
} | ||||||
} | ||||||
|
||||||
#[async_trait] | ||||||
|
@@ -82,7 +197,13 @@ impl SessionStore for MongodbSessionStore { | |||||
let value = bson::to_bson(&session)?; | ||||||
let id = session.id(); | ||||||
let query = doc! { "session_id": id }; | ||||||
let replacement = doc! { "session_id": id, "session": value, "created": Utc::now() }; | ||||||
let expire_at = match session.expiry() { | ||||||
None => Utc::now() + Duration::from_std(std::time::Duration::from_secs(1200)).unwrap(), | ||||||
|
||||||
Some(expiry) => *{ expiry }, | ||||||
}; | ||||||
let replacement = doc! { "session_id": id, "session": value, "expireAt": expire_at, "created": Utc::now() }; | ||||||
|
||||||
let opts = ReplaceOptions::builder().upsert(true).build(); | ||||||
coll.replace_one(query, replacement, Some(opts)).await?; | ||||||
|
||||||
|
@@ -114,7 +235,8 @@ impl SessionStore for MongodbSessionStore { | |||||
|
||||||
async fn clear_store(&self) -> Result { | ||||||
let coll = self.client.database(&self.db).collection(&self.coll_name); | ||||||
coll.drop(None).await?; // does this need to be followed by a create? | ||||||
coll.drop(None).await?; | ||||||
self.initialize().await?; | ||||||
Ok(()) | ||||||
} | ||||||
} |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'm curious about the word "perceived" — is it actually consistent?