rack-session-leveldb 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +5 -0
- data/.travis.yml +6 -0
- data/CHANGES.md +6 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +14 -0
- data/README.md +41 -0
- data/Rakefile +1 -0
- data/lib/rack-session-leveldb.rb +1 -0
- data/lib/rack/session/leveldb.rb +108 -0
- data/rack-session-leveldb.gemspec +25 -0
- data/spec/spec_session_leveldb.rb +215 -0
- metadata +90 -0
data/.travis.yml
ADDED
data/CHANGES.md
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
data/README.md
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
rack-session-leveldb
|
2
|
+
====================
|
3
|
+
|
4
|
+
Rack session store for LevelDB
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
gem install rack-session-leveldb
|
9
|
+
|
10
|
+
## Usage
|
11
|
+
|
12
|
+
Simple (db\_path: ENV['TMP']/rack.session)
|
13
|
+
|
14
|
+
use Rack::Session::LevelDB
|
15
|
+
|
16
|
+
Specify DB path
|
17
|
+
|
18
|
+
use Rack::Session::LevelDB, File.dirname(__FILE__) + '/tmp/rack.session'
|
19
|
+
|
20
|
+
Set LevelDB instance
|
21
|
+
|
22
|
+
leveldb = LevelDB::DB.new(File.dirname(__FILE__) + '/db')
|
23
|
+
use Rack::Session::LevelDB, leveldb
|
24
|
+
|
25
|
+
Specify DB path with some config
|
26
|
+
|
27
|
+
use Rack::Session::LevelDB, {
|
28
|
+
:db_path => File.dirname(__FILE__) + '/tmp/rack.session',
|
29
|
+
:expire_after => 600
|
30
|
+
}
|
31
|
+
|
32
|
+
Set LevelDB instance with some config
|
33
|
+
|
34
|
+
leveldb = LevelDB::DB.new(File.dirname(__FILE__) + '/db')
|
35
|
+
use Rack::Session::LevelDB, {
|
36
|
+
:cache => leveldb,
|
37
|
+
:cleanup => false,
|
38
|
+
:expire_after => 600
|
39
|
+
}
|
40
|
+
|
41
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1 @@
|
|
1
|
+
require "rack/session/leveldb"
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'rack/session/abstract/id'
|
2
|
+
require 'leveldb'
|
3
|
+
module Rack
|
4
|
+
module Session
|
5
|
+
class LevelDB < Abstract::ID
|
6
|
+
VERSION = '0.0.1'
|
7
|
+
|
8
|
+
attr_reader :mutex, :pool
|
9
|
+
|
10
|
+
DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.merge \
|
11
|
+
:namespace => 'rack.session:',
|
12
|
+
:db_path => "#{ENV['TMP'] || '/tmp'}/rack.session",
|
13
|
+
:cleanup => true
|
14
|
+
|
15
|
+
def initialize(app, options={})
|
16
|
+
options = {:db_path => options } if options.is_a? String
|
17
|
+
options = {:cache => options } if options.is_a? ::LevelDB::DB
|
18
|
+
super
|
19
|
+
if options[:cache]
|
20
|
+
if options[:cache].kind_of? ::LevelDB::DB
|
21
|
+
@pool = options[:cache]
|
22
|
+
else
|
23
|
+
raise ":cache is not LevelDB"
|
24
|
+
end
|
25
|
+
else
|
26
|
+
@pool = ::LevelDB::DB.new @default_options[:db_path]
|
27
|
+
end
|
28
|
+
|
29
|
+
cleanup_expired if @default_options[:cleanup]
|
30
|
+
@mutex = Mutex.new
|
31
|
+
end
|
32
|
+
|
33
|
+
def generate_sid
|
34
|
+
loop do
|
35
|
+
sid = super
|
36
|
+
break sid unless _exists? sid
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def get_session(env, sid)
|
41
|
+
with_lock(env, [nil, {}]) do
|
42
|
+
unless sid and session = _get(sid)
|
43
|
+
sid, session = generate_sid, {}
|
44
|
+
_put sid, session
|
45
|
+
end
|
46
|
+
[sid, session]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def set_session(env, session_id, new_session, options)
|
51
|
+
with_lock(env, false) do
|
52
|
+
_put session_id, new_session
|
53
|
+
session_id
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def destroy_session(env, session_id, options)
|
58
|
+
with_lock(env) do
|
59
|
+
_delete(session_id)
|
60
|
+
generate_sid unless options[:drop]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def with_lock(env, default=nil)
|
65
|
+
@mutex.lock if env['rack.multithread']
|
66
|
+
yield
|
67
|
+
rescue
|
68
|
+
warn "#{self} is unable to open #{@default_options[:db_path]}."
|
69
|
+
warn $!.inspect
|
70
|
+
default
|
71
|
+
ensure
|
72
|
+
@mutex.unlock if @mutex.locked?
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
def cleanup_expired(time = Time.now)
|
77
|
+
return unless @default_options[:expire_after]
|
78
|
+
@pool.each do |k, v|
|
79
|
+
if k.start_with?(@default_options[:namespace]) and
|
80
|
+
time - Marshal.load(v)[:datetime] > @default_options[:expire_after]
|
81
|
+
@pool.delete(k)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
def _put(sid, session)
|
88
|
+
@pool.put(_key(sid), Marshal.dump({:data => session, :datetime => Time.now}))
|
89
|
+
end
|
90
|
+
|
91
|
+
def _get(sid)
|
92
|
+
Marshal.load(@pool.get(_key(sid)))[:data] rescue nil
|
93
|
+
end
|
94
|
+
|
95
|
+
def _delete(sid)
|
96
|
+
@pool.delete(_key(sid))
|
97
|
+
end
|
98
|
+
|
99
|
+
def _exists?(sid)
|
100
|
+
@pool.exists?(_key(sid))
|
101
|
+
end
|
102
|
+
|
103
|
+
def _key(sid)
|
104
|
+
"#{@default_options[:namespace]}#{sid}"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "rack-session-leveldb"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "rack-session-leveldb"
|
7
|
+
s.version = Rack::Session::LevelDB::VERSION
|
8
|
+
s.authors = ["Masato Igarashi"]
|
9
|
+
s.email = ["m@igrs.jp"]
|
10
|
+
s.homepage = "http://github.com/migrs/rack-session-leveldb"
|
11
|
+
s.summary = %q{Rack session store for LevelDB}
|
12
|
+
s.description = %q{Rack session store for LevelDB}
|
13
|
+
|
14
|
+
#s.rubyforge_project = "rack-session-leveldb"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
# specify any dependencies here; for example:
|
22
|
+
s.add_development_dependency "bacon"
|
23
|
+
s.add_runtime_dependency "rack"
|
24
|
+
s.add_runtime_dependency "leveldb-ruby"
|
25
|
+
end
|
@@ -0,0 +1,215 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'rack/mock'
|
3
|
+
require 'rack/session/leveldb'
|
4
|
+
require 'fileutils'
|
5
|
+
|
6
|
+
describe Rack::Session::LevelDB do
|
7
|
+
session_key = Rack::Session::LevelDB::DEFAULT_OPTIONS[:key]
|
8
|
+
session_match = /#{session_key}=[0-9a-fA-F]+;/
|
9
|
+
|
10
|
+
incrementor = lambda do |env|
|
11
|
+
env["rack.session"]["counter"] ||= 0
|
12
|
+
env["rack.session"]["counter"] += 1
|
13
|
+
Rack::Response.new(env["rack.session"].inspect).to_a
|
14
|
+
end
|
15
|
+
|
16
|
+
session_id = lambda do |env|
|
17
|
+
Rack::Response.new(env["rack.session"].inspect).to_a
|
18
|
+
end
|
19
|
+
|
20
|
+
nothing = lambda do |env|
|
21
|
+
Rack::Response.new("Nothing").to_a
|
22
|
+
end
|
23
|
+
|
24
|
+
drop_session = lambda do |env|
|
25
|
+
env['rack.session.options'][:drop] = true
|
26
|
+
incrementor.call(env)
|
27
|
+
end
|
28
|
+
|
29
|
+
renew_session = lambda do |env|
|
30
|
+
env['rack.session.options'][:renew] = true
|
31
|
+
incrementor.call(env)
|
32
|
+
end
|
33
|
+
|
34
|
+
defer_session = lambda do |env|
|
35
|
+
env['rack.session.options'][:defer] = true
|
36
|
+
incrementor.call(env)
|
37
|
+
end
|
38
|
+
|
39
|
+
before do
|
40
|
+
Rack::Session::LevelDB::DEFAULT_OPTIONS[:db_path] = "#{ENV['TMP'] || '/tmp'}/rack.session-#{rand}"
|
41
|
+
end
|
42
|
+
|
43
|
+
after do
|
44
|
+
FileUtils.rm_r(Rack::Session::LevelDB::DEFAULT_OPTIONS[:db_path])
|
45
|
+
end
|
46
|
+
|
47
|
+
it "creates a new cookie" do
|
48
|
+
pool = Rack::Session::LevelDB.new(incrementor)
|
49
|
+
res = Rack::MockRequest.new(pool).get("/")
|
50
|
+
res["Set-Cookie"].should.match session_match
|
51
|
+
res.body.should.equal '{"counter"=>1}'
|
52
|
+
end
|
53
|
+
|
54
|
+
it "determines session from a cookie" do
|
55
|
+
pool = Rack::Session::LevelDB.new(incrementor)
|
56
|
+
req = Rack::MockRequest.new(pool)
|
57
|
+
cookie = req.get("/")["Set-Cookie"]
|
58
|
+
req.get("/", "HTTP_COOKIE" => cookie).
|
59
|
+
body.should.equal '{"counter"=>2}'
|
60
|
+
req.get("/", "HTTP_COOKIE" => cookie).
|
61
|
+
body.should.equal '{"counter"=>3}'
|
62
|
+
end
|
63
|
+
|
64
|
+
it "survives nonexistant cookies" do
|
65
|
+
pool = Rack::Session::LevelDB.new(incrementor)
|
66
|
+
res = Rack::MockRequest.new(pool).
|
67
|
+
get("/", "HTTP_COOKIE" => "#{session_key}=blarghfasel")
|
68
|
+
res.body.should.equal '{"counter"=>1}'
|
69
|
+
end
|
70
|
+
|
71
|
+
it "does not send the same session id if it did not change" do
|
72
|
+
pool = Rack::Session::LevelDB.new(incrementor)
|
73
|
+
req = Rack::MockRequest.new(pool)
|
74
|
+
|
75
|
+
res0 = req.get("/")
|
76
|
+
cookie = res0["Set-Cookie"][session_match]
|
77
|
+
res0.body.should.equal '{"counter"=>1}'
|
78
|
+
pool.pool.size.should.equal 1
|
79
|
+
|
80
|
+
res1 = req.get("/", "HTTP_COOKIE" => cookie)
|
81
|
+
res1["Set-Cookie"].should.be.nil
|
82
|
+
res1.body.should.equal '{"counter"=>2}'
|
83
|
+
pool.pool.size.should.equal 1
|
84
|
+
|
85
|
+
res2 = req.get("/", "HTTP_COOKIE" => cookie)
|
86
|
+
res2["Set-Cookie"].should.be.nil
|
87
|
+
res2.body.should.equal '{"counter"=>3}'
|
88
|
+
pool.pool.size.should.equal 1
|
89
|
+
end
|
90
|
+
|
91
|
+
it "deletes cookies with :drop option" do
|
92
|
+
pool = Rack::Session::LevelDB.new(incrementor)
|
93
|
+
req = Rack::MockRequest.new(pool)
|
94
|
+
drop = Rack::Utils::Context.new(pool, drop_session)
|
95
|
+
dreq = Rack::MockRequest.new(drop)
|
96
|
+
|
97
|
+
res1 = req.get("/")
|
98
|
+
session = (cookie = res1["Set-Cookie"])[session_match]
|
99
|
+
res1.body.should.equal '{"counter"=>1}'
|
100
|
+
pool.pool.size.should.equal 1
|
101
|
+
|
102
|
+
res2 = dreq.get("/", "HTTP_COOKIE" => cookie)
|
103
|
+
res2["Set-Cookie"].should.be.nil
|
104
|
+
res2.body.should.equal '{"counter"=>2}'
|
105
|
+
pool.pool.size.should.equal 0
|
106
|
+
|
107
|
+
res3 = req.get("/", "HTTP_COOKIE" => cookie)
|
108
|
+
res3["Set-Cookie"][session_match].should.not.equal session
|
109
|
+
res3.body.should.equal '{"counter"=>1}'
|
110
|
+
pool.pool.size.should.equal 1
|
111
|
+
end
|
112
|
+
|
113
|
+
it "provides new session id with :renew option" do
|
114
|
+
pool = Rack::Session::LevelDB.new(incrementor)
|
115
|
+
req = Rack::MockRequest.new(pool)
|
116
|
+
renew = Rack::Utils::Context.new(pool, renew_session)
|
117
|
+
rreq = Rack::MockRequest.new(renew)
|
118
|
+
|
119
|
+
res1 = req.get("/")
|
120
|
+
session = (cookie = res1["Set-Cookie"])[session_match]
|
121
|
+
res1.body.should.equal '{"counter"=>1}'
|
122
|
+
pool.pool.size.should.equal 1
|
123
|
+
|
124
|
+
res2 = rreq.get("/", "HTTP_COOKIE" => cookie)
|
125
|
+
new_cookie = res2["Set-Cookie"]
|
126
|
+
new_session = new_cookie[session_match]
|
127
|
+
new_session.should.not.equal session
|
128
|
+
res2.body.should.equal '{"counter"=>2}'
|
129
|
+
pool.pool.size.should.equal 1
|
130
|
+
|
131
|
+
res3 = req.get("/", "HTTP_COOKIE" => new_cookie)
|
132
|
+
res3.body.should.equal '{"counter"=>3}'
|
133
|
+
pool.pool.size.should.equal 1
|
134
|
+
|
135
|
+
res4 = req.get("/", "HTTP_COOKIE" => cookie)
|
136
|
+
res4.body.should.equal '{"counter"=>1}'
|
137
|
+
pool.pool.size.should.equal 2
|
138
|
+
end
|
139
|
+
|
140
|
+
it "omits cookie with :defer option" do
|
141
|
+
pool = Rack::Session::LevelDB.new(incrementor)
|
142
|
+
defer = Rack::Utils::Context.new(pool, defer_session)
|
143
|
+
dreq = Rack::MockRequest.new(defer)
|
144
|
+
|
145
|
+
res1 = dreq.get("/")
|
146
|
+
res1["Set-Cookie"].should.equal nil
|
147
|
+
res1.body.should.equal '{"counter"=>1}'
|
148
|
+
pool.pool.size.should.equal 1
|
149
|
+
end
|
150
|
+
|
151
|
+
# anyone know how to do this better?
|
152
|
+
it "should merge sessions when multithreaded" do
|
153
|
+
unless $DEBUG
|
154
|
+
1.should.equal 1
|
155
|
+
next
|
156
|
+
end
|
157
|
+
|
158
|
+
warn 'Running multithread tests for Session::LevelDB'
|
159
|
+
pool = Rack::Session::LevelDB.new(incrementor)
|
160
|
+
req = Rack::MockRequest.new(pool)
|
161
|
+
|
162
|
+
res = req.get('/')
|
163
|
+
res.body.should.equal '{"counter"=>1}'
|
164
|
+
cookie = res["Set-Cookie"]
|
165
|
+
sess_id = cookie[/#{pool.key}=([^,;]+)/,1]
|
166
|
+
|
167
|
+
delta_incrementor = lambda do |env|
|
168
|
+
# emulate disconjoinment of threading
|
169
|
+
env['rack.session'] = env['rack.session'].dup
|
170
|
+
Thread.stop
|
171
|
+
env['rack.session'][(Time.now.usec*rand).to_i] = true
|
172
|
+
incrementor.call(env)
|
173
|
+
end
|
174
|
+
tses = Rack::Utils::Context.new pool, delta_incrementor
|
175
|
+
treq = Rack::MockRequest.new(tses)
|
176
|
+
tnum = rand(7).to_i+5
|
177
|
+
r = Array.new(tnum) do
|
178
|
+
Thread.new(treq) do |run|
|
179
|
+
run.get('/', "HTTP_COOKIE" => cookie, 'rack.multithread' => true)
|
180
|
+
end
|
181
|
+
end.reverse.map{|t| t.run.join.value }
|
182
|
+
r.each do |resp|
|
183
|
+
resp['Set-Cookie'].should.equal cookie
|
184
|
+
resp.body.should.include '"counter"=>2'
|
185
|
+
end
|
186
|
+
|
187
|
+
session = pool.pool[sess_id]
|
188
|
+
session.size.should.equal tnum+1 # counter
|
189
|
+
session['counter'].should.equal 2 # meeeh
|
190
|
+
end
|
191
|
+
|
192
|
+
it "does not return a cookie if cookie was not read/written" do
|
193
|
+
app = Rack::Session::LevelDB.new(nothing)
|
194
|
+
res = Rack::MockRequest.new(app).get("/")
|
195
|
+
res["Set-Cookie"].should.be.nil
|
196
|
+
end
|
197
|
+
|
198
|
+
it "does not return a cookie if cookie was not written (only read)" do
|
199
|
+
app = Rack::Session::LevelDB.new(session_id)
|
200
|
+
res = Rack::MockRequest.new(app).get("/")
|
201
|
+
res["Set-Cookie"].should.be.nil
|
202
|
+
end
|
203
|
+
|
204
|
+
it "returns even if not read/written if :expire_after is set" do
|
205
|
+
app = Rack::Session::LevelDB.new(nothing, :expire_after => 3600)
|
206
|
+
res = Rack::MockRequest.new(app).get("/", 'rack.session' => {'not' => 'empty'})
|
207
|
+
res["Set-Cookie"].should.not.be.nil
|
208
|
+
end
|
209
|
+
|
210
|
+
it "returns no cookie if no data was written and no session was created previously, even if :expire_after is set" do
|
211
|
+
app = Rack::Session::LevelDB.new(nothing, :expire_after => 3600)
|
212
|
+
res = Rack::MockRequest.new(app).get("/")
|
213
|
+
res["Set-Cookie"].should.be.nil
|
214
|
+
end
|
215
|
+
end
|
metadata
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rack-session-leveldb
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Masato Igarashi
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-01-21 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bacon
|
16
|
+
requirement: &70238763246740 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70238763246740
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rack
|
27
|
+
requirement: &70238763245760 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70238763245760
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: leveldb-ruby
|
38
|
+
requirement: &70238763244980 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70238763244980
|
47
|
+
description: Rack session store for LevelDB
|
48
|
+
email:
|
49
|
+
- m@igrs.jp
|
50
|
+
executables: []
|
51
|
+
extensions: []
|
52
|
+
extra_rdoc_files: []
|
53
|
+
files:
|
54
|
+
- .gitignore
|
55
|
+
- .travis.yml
|
56
|
+
- CHANGES.md
|
57
|
+
- Gemfile
|
58
|
+
- Gemfile.lock
|
59
|
+
- README.md
|
60
|
+
- Rakefile
|
61
|
+
- lib/rack-session-leveldb.rb
|
62
|
+
- lib/rack/session/leveldb.rb
|
63
|
+
- rack-session-leveldb.gemspec
|
64
|
+
- spec/spec_session_leveldb.rb
|
65
|
+
homepage: http://github.com/migrs/rack-session-leveldb
|
66
|
+
licenses: []
|
67
|
+
post_install_message:
|
68
|
+
rdoc_options: []
|
69
|
+
require_paths:
|
70
|
+
- lib
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ! '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
78
|
+
none: false
|
79
|
+
requirements:
|
80
|
+
- - ! '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
requirements: []
|
84
|
+
rubyforge_project:
|
85
|
+
rubygems_version: 1.8.10
|
86
|
+
signing_key:
|
87
|
+
specification_version: 3
|
88
|
+
summary: Rack session store for LevelDB
|
89
|
+
test_files:
|
90
|
+
- spec/spec_session_leveldb.rb
|