Skip to content
Open
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
96 changes: 80 additions & 16 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,75 @@ connect-mysql-session

A MySQL session store for the [connectjs][] [session middleware][] for [node.js][].

Currently, this code appears to function correctly but it has not been optimized for performance. The store is implemented using [sequelize][] ORM, simply dumping the JSON-serialized session into a MySQL TEXT column.
An highly optimized dependency-reduced version of original work by "CarnegieLearning".

Installation
------------
Optimizations/Deltas
--------------------
* Now written (and maintained) in Coffeescript
* Removed unnecessary dependence on Sequelize
* Forward support for mySql's in-memory database engine

Why MySQL for Sessions?
------------------------

### Less Maintenance

* If you already use MySQL for your primary data store, eliminating the use of Mongo or Redis reduces the number of vendors, number of critical failure points, and probability of failure in your system as a whole.

* Reduced polyglot results from eliminating yet another domain specific language (Redis/Mongo) from your development stack.

* You don't have to build/configure additional monitoring and management for your session store. Your primary data store automatically covers it.

* Upgrades to your primary datastore automatically effect the session store. You don't need to perform two separate upgrades.

### Lower Operating Costs

* It is less expensive to scale existing technology (provision a larger database server), than to provision multiple smaller database servers

* Fewer servers makes it less expensive to run staging and development copies of your infrastructure.

* Fewer languages means less development time and fewer management and monitoring tools to buy. You are already monitoring your primary data store, why not just reuse that investment.


### Better performance?

Sessions are the simplest case of table storage using no relations and single primary key btree or hash indexes. This largely mitigates the disadvantages of relational database overhead (conversely mitigating most of the advantages of dictionary stores that are essentially the same thing as flat tables with single indexes).

Using [npm][]:
By default this library uses the InnoDB persistent storage engine in MySQL to allow for up to 16MB of data to be stored in each user session and to do so with dynamic memory allocation. InnoDB is only about 2%-8% slower than a similarly provisioned Redis instance.

npm install -g connect-mysql-session
If greater performance is desired, you can switch to the MySQL Memory engine with a one word change to the code (will eventually be a direct config option in this library). MySQL in-memory table stores are about as efficient as data storage can get, primary due to its lack of features. The entire table is statically allocated with data allocated in small blocks within it and indexed with a hash or binary tree.

By cloning the repo:
As [this study](http://bit.ly/17ZzafB) revealed,

git clone git://github.com/CarnegieLearning/connect-mysql-session.git
cd connect-mysql-session
npm link
MySQL's Memory Engine performed sustained writes at 92% the speed of Redis, yet performed reads at almost 25X (times!!!) the speed. Given that session stores show a heavy read bias, the end result is a large performance gain.

(Note: in both cases you may need to use `sudo` when performing the `npm` step.)
Limitations
-----------

### General

These limitations apply regardless of the database engine chosen:

* MySQL version >= 5.0.3 with Memory Engine is required
* Node.js version >= 0.8
* Session data must be JSON serializable (no binary objects)

### Memory Engine

In general, if you follow best-practices for session storage you won't have problems, but MySQL's memory engine gains performance through limiting what and how you can store data.

* Maximum serialized session size is 20k bytes (MySQL Memory Engine restriction resulting from row-size limit)
* Memory allocated to the engine is not available to cache primary tables and can hurt performance if too large.

### InnoDB Engine

If you use the InnoDB engine (default):

* Maximum serialized session size is 16MB bytes


Installation
------------

Usage
-----
Expand All @@ -31,8 +84,12 @@ The following example uses [expressjs][], but this should work fine using [conne
var app = express.createServer();
app.use(express.cookieParser());
app.use(express.session({
store: new MySQLSessionStore("dbname", "user", "password", {
// options...
store: new MySQLSessionStore({
host: 127.0.0.1, //database host name
user: "root", //database username
password: "", //database user's password
checkExpirationInterval: 12*60*60, //how frequently to check for dead sessions (seconds)
defaultExpiration: 7*24*60*60 //how long to keep session alive (seconds)
}),
secret: "keyboard cat"
}));
Expand All @@ -41,21 +98,28 @@ The following example uses [expressjs][], but this should work fine using [conne
Options
-------

### forceSync ###
### host, user, password ###

Default: `false`. If set to true, the Sessions table will be dropped before being reinitialized, effectively clearing all session data.
Database credentials. Defaults to localhost defaults.

### checkExpirationInterval ###

Default: `1000*60*10` (10 minutes). How frequently the session store checks for and clears expired sessions.
Default: `12*60*60` (Twice a day). Specified in seconds. How frequently the session store checks for and clears expired sessions.

### defaultExpiration ###

Default: `1000*60*60*24` (1 day). How long session data is stored for "user session" cookies -- i.e. sessions that only last as long as the user keeps their browser open, which are created by doing `req.session.maxAge = null`.
Default: `7*24*60*60` (1 week). Specified in seconds. How long session data is stored for "user session" cookies -- i.e. sessions that only last as long as the user keeps their browser open, which are created by doing `req.session.maxAge = null`.

Changes
-------

### 0.2.6 (2013-09-14)

* Switch to Coffeescript
* Removed Sequelize
* Built on InnoDB engine (MUCH more space performant)
* Built on memory engine (MUCH more time performant)

### 0.1.1 and 0.1.2 (2011-08-03) ###

* Lazy initialization to ensure model is ready before accessing.
Expand Down
117 changes: 117 additions & 0 deletions lib/connect-mysql-session.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
mysql = require "mysql"

module.exports = (connect) ->

###
options =
host: name of the database's host
user: login username
password: login password
checkExpirationInterval: (in seconds)
defaultExpiration: (in seconds)
client: (optional) fully instantiated client to use, instead of creating one internally
###

class MySqlStore extends connect.session.Store
constructor: (@options) ->
# -- Context
@initialized = false
# -- Default values
@options = @options or {}
@options.host ?= "127.0.0.1"
@options.user ?= "root"
@options.password ?= ""
@options.checkExpirationInterval ?= 12*60*60 #check twice a day
@options.defaultExpiration ?= 7*24*60*60 #expire after one week
# -- Link middleware
connect.session.Store.call this, @options
# -- Create client
@client = options.client or mysql.createConnection @options
@client.on "error", =>
@emit "disconnect"
@client.on "connect", =>
@emit "connect"

initialize: (fn) =>
return fn() if @initialized #run only once
@client.connect()
sql = """
CREATE DATABASE IF NOT EXISTS `sessions`
"""
@client.query sql, (err, rows, fields) =>
if err?
console.log "Failed to initialize MySQL session store. Couldn't create sessions database.", err
return fn err
sql = """
CREATE TABLE IF NOT EXISTS `sessions`.`session` (
`sid` varchar(40) NOT NULL DEFAULT '',
`ttl` int(11) DEFAULT NULL,
`json` mediumtext DEFAULT NULL,
`createdAt` datetime DEFAULT NULL,
`updatedAt` datetime DEFAULT NULL,
PRIMARY KEY (`sid`)
)
ENGINE=INNODB
DEFAULT CHARSET=utf8
"""
@client.query sql, (err, rows, fields) =>
if err?
console.log "Failed to initialize MySQL session store. Couldn't create session table.", err
return fn err
console.log "MySQL session store initialized."
@initialized = true
@_watchdog() #expire expired sessions
fn()

_watchdog: () =>
sql = """
DELETE FROM `sessions`.`session` WHERE TIME_TO_SEC(UTC_TIMESTAMP()) - TIME_TO_SEC(`updatedAt`) > `ttl`
"""
@client.query sql, [], (err, meta) =>
console.log "Could not cleanup expired sessions:", err if err?
console.log "Removed #{meta.affectedRows} expired user sessions." if meta.affectedRows > 0
setTimeout @_watchdog, @options.checkExpirationInterval * 1000


get: (sid, fn) =>
@initialize (error) =>
return fn error if error?
@client.query "SELECT * FROM `sessions`.`session` WHERE `sid`=?", [sid], (err, rows, fields) =>
return fn err if err?
result = undefined
try
result = JSON.parse rows[0].json if rows?[0]?
catch err
return fn err
fn undefined, result


set: (sid, session, fn) =>
maxAge = session.cookie.maxAge
ttl = @options.ttl
json = JSON.stringify(session)
ttl = ttl or ((if "number" is typeof maxAge then maxAge / 1000 | 0 else @options.defaultExpiration))
@initialize (error) =>
return fn error if error?
# -- Update session if exists; Create otherwise
sql = """
INSERT INTO `sessions`.`session` (`sid`, `ttl`, `json`, `createdAt`, `updatedAt`)
VALUES (?, ?, ?, UTC_TIMESTAMP(), UTC_TIMESTAMP())
ON DUPLICATE KEY
UPDATE `ttl`=?, `json`=?, `updatedAt`=UTC_TIMESTAMP()
"""
@client.query sql, [sid, ttl, json, ttl, json], (err) =>
return fn err if err?
fn.apply(this, arguments)


destroy: (sid, fn) =>
@initialize (error) =>
return fn error if error?
@client.query "DELETE FROM `sessions`.`session` WHERE `sid`=?",[sid], (err, rows, fields) ->
if err?
console.log "Session " + sid + " could not be destroyed."
return fn err, undefined
fn()

return MySqlStore
Loading