rack-session-leveldb 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ pkg/*
4
+ *.swp
5
+ .DS_Store
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ script: "bacon -I./lib spec/spec_session_leveldb.rb"
3
+ rvm:
4
+ - 1.8.7
5
+ - 1.9.2
6
+ - 1.9.3
@@ -0,0 +1,6 @@
1
+ CHANGES
2
+ =======
3
+
4
+ ### [0.0.1](https://github.com/migrs/rack-session-leveldb/tree/v0.0.1) / 2012-01-22
5
+
6
+ * Initial Release
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source :rubygems
2
+
3
+ # Specify your gem's dependencies in rack-session-leveldb.gemspec
4
+ # gemspec
5
+
6
+ gem 'rack'
7
+ gem 'leveldb-ruby'
8
+
9
+ group :test do
10
+ gem 'bacon'
11
+ end
@@ -0,0 +1,14 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ bacon (1.1.0)
5
+ leveldb-ruby (0.10)
6
+ rack (1.4.0)
7
+
8
+ PLATFORMS
9
+ ruby
10
+
11
+ DEPENDENCIES
12
+ bacon
13
+ leveldb-ruby
14
+ rack
@@ -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
+
@@ -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