Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ authors = [
"Yoshua Wuyts <[email protected]>",
"Irina Shestak <[email protected]>",
"Anton Whalley <[email protected]>",
"Javier Viola <[email protected]>"
]

[features]
Expand All @@ -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"
73 changes: 69 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Member

@jbr jbr Oct 14, 2020

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?

Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
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.
The specific clock time approach is for use cases when 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.


Copy link
Collaborator

@No9 No9 Oct 12, 2020

Choose a reason for hiding this comment

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

I was thinking something along the lines of
"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.

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi @No9, Neat!! If you want I can add those lines. 👍

Copy link
Collaborator

@No9 No9 Oct 12, 2020

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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:
Expand All @@ -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.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Other way to set create the index is using `index_on_created` passing the amount of seconds to expiry after the session.
Another way to set create the index is to use `index_on_created`, passing the amount of seconds to expiry after the session.


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 } );
```

Copy link
Collaborator

Choose a reason for hiding this comment

The 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
Expand Down
44 changes: 44 additions & 0 deletions examples/mongodb_session_store.rs
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(())
}
140 changes: 131 additions & 9 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/// Create a new index for the `expireAt` property, allowing to expire sessions at a specific clock time.
/// Create a new index for the `expireAt` property, in order to expire sessions at a specific clock time.

/// 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]
Expand All @@ -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?;

Expand Down Expand Up @@ -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(())
}
}
Loading