mongo_rack 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/HISTORY ADDED
@@ -0,0 +1,2 @@
1
+ o 0.0.1 / 2009-12-31
2
+ Initial drop
@@ -0,0 +1,61 @@
1
+ == mongo_rack
2
+
3
+ mongoDB based session management
4
+
5
+ == DESCRIPTION:
6
+
7
+ A mongoDB based rackable session store. Can be used with any rack based web frameworks.
8
+
9
+ == PROJECT INFORMATION
10
+
11
+ * Developer: Fernand Galiana
12
+ * Git: git://github.com/derailed/mongo-rack.git
13
+
14
+ == REQUIREMENTS:
15
+
16
+ * rack 1.0 or later
17
+ * mongo + mongo_ext ruby adapter 0.18 or later
18
+
19
+ == INSTALL:
20
+
21
+ * sudo gem install mongo_rack
22
+
23
+ == USAGE:
24
+
25
+ use Rack::Session::Mongo, { :server => 'localhost:27017/mongo_ses/sessions }
26
+
27
+ where server requires the following format
28
+
29
+ {server_name_or_ip}:{port}/{database_name}/{collection_name}
30
+
31
+ The server description by default will be localhost:2701/mongo_session/sessions
32
+
33
+ Other options includes:
34
+
35
+ pool_size ( default is 1 )
36
+ pool_timeout ( defaults to 1 )
37
+
38
+ == LICENSE:
39
+
40
+ (The MIT License)
41
+
42
+ Copyright (c) 2009
43
+
44
+ Permission is hereby granted, free of charge, to any person obtaining
45
+ a copy of this software and associated documentation files (the
46
+ 'Software'), to deal in the Software without restriction, including
47
+ without limitation the rights to use, copy, modify, merge, publish,
48
+ distribute, sublicense, and/or sell copies of the Software, and to
49
+ permit persons to whom the Software is furnished to do so, subject to
50
+ the following conditions:
51
+
52
+ The above copyright notice and this permission notice shall be
53
+ included in all copies or substantial portions of the Software.
54
+
55
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
56
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
57
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
58
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
59
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
60
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
61
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,31 @@
1
+ begin
2
+ require 'bones'
3
+ Bones.setup
4
+ rescue LoadError
5
+ begin
6
+ load 'tasks/setup.rb'
7
+ rescue LoadError
8
+ raise RuntimeError, '### please install the "bones" gem ###'
9
+ end
10
+ end
11
+
12
+ ensure_in_path 'lib'
13
+ require 'mongo_rack'
14
+
15
+ task :default => 'spec:run'
16
+
17
+ PROJ.name = 'mongo_rack'
18
+ PROJ.authors = 'Fernand Galiana'
19
+ PROJ.email = 'fernand.galiana@gmail.com'
20
+ PROJ.url = 'http://github.com/derailed/mongo_rack'
21
+ PROJ.summary = "Rackable mongoDB based session management"
22
+ PROJ.version = "0.0.1"
23
+ PROJ.ruby_opts = %w[-W0]
24
+ PROJ.readme = 'README.rdoc'
25
+ PROJ.rcov.opts = ["--sort", "coverage", "-T"]
26
+ PROJ.spec.opts << '--color'
27
+
28
+ # Dependencies
29
+ depend_on "rack" , ">= 1.0.0"
30
+ depend_on "mongo" , ">= 0.18.1"
31
+ depend_on "mongo_ext", ">= 0.18.1"
data/fred.rb ADDED
@@ -0,0 +1,26 @@
1
+ # tnum = 10
2
+ # r = Array.new(tnum) do
3
+ # Thread.new do
4
+ # puts "Making request"
5
+ # 10
6
+ # end
7
+ # end.reverse.map{|t| puts t.inspect;t.join; puts t.value }
8
+ #
9
+ # r.each do |res|
10
+ # puts "Checking request #{res}"
11
+ # end
12
+
13
+
14
+ a = Array.new( 10 ) do
15
+ Thread.new( 20 ) do |run|
16
+ puts "Blee + #{run}"
17
+ 20
18
+ end
19
+ end
20
+
21
+ puts a.inspect
22
+
23
+ b = a.reverse.map{ |t| t.join.value }
24
+
25
+ puts "Here"
26
+ puts b.inspect
@@ -0,0 +1,6 @@
1
+ require File.join( File.dirname(__FILE__), %w[.. mongo_rack session_hash] )
2
+
3
+ # Reopen hash to add session access ie indifferent access to keys as symb or str
4
+ class Hash
5
+ include MongoRack::SessionAccess
6
+ end
@@ -0,0 +1,159 @@
1
+ require 'rack/session/abstract/id'
2
+ require 'mongo'
3
+ require File.join( File.dirname(__FILE__), %w[mongo_rack session_hash.rb] )
4
+
5
+ module Rack
6
+ module Session
7
+ class Mongo < Abstract::ID
8
+ attr_reader :mutex, :connection, :db, :sessions #:nodoc:
9
+
10
+ # === Options for mongo_rack
11
+ # :server ::
12
+ # Specifies server, port, db and collection location. Defaults
13
+ # to localhost:27017/mongo_session/sessions. Format must conform to
14
+ # the format {host}:{port}/{database_name}/{collection_name}.
15
+ # :pool_size ::
16
+ # The connection socket pool size - see mongo-ruby-driver docs for settings.
17
+ # Defaults to 1 connection.
18
+ # :pool_timeout ::
19
+ # The connection pool timeout. see mongo-ruby-driver docs for settings.
20
+ # Defaults to 1 sec.
21
+ DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.merge \
22
+ :server => 'localhost:27017/mongo_session/sessions',
23
+ :pool_size => 1,
24
+ :pool_timeout => 1.0
25
+
26
+ # Initializes mongo_rack. Pass in options for default override.
27
+ def initialize(app, options={})
28
+ super
29
+
30
+ host, port, db_name, cltn_name = parse_server_desc( @default_options[:server] )
31
+
32
+ @mutex = Mutex.new
33
+ @connection = ::Mongo::Connection.new( host, port,
34
+ :pool_size => @default_options[:pool_size],
35
+ :timeout => @default_options[:pool_timeout] )
36
+ @db = @connection.db( db_name )
37
+ @sessions = @db[cltn_name]
38
+ end
39
+
40
+ # Fetch session with optional session id. Retrieve session from mongodb if any
41
+ def get_session( env, sid )
42
+ return _get_session( env, sid ) unless env['rack.multithread']
43
+ mutex.synchronize do
44
+ return _get_session( env, sid )
45
+ end
46
+ end
47
+
48
+ # Update session params and sync to mongoDB.
49
+ def set_session( env, sid, new_session, options )
50
+ return _set_session( env, sid, new_session, options ) unless env['rack.multithread']
51
+ mutex.synchronize do
52
+ return _set_session( env, sid, new_session, options )
53
+ end
54
+ end
55
+
56
+ # =======================================================================
57
+ private
58
+
59
+ # Generates unique session id
60
+ def generate_sid
61
+ loop do
62
+ sid = super
63
+ break sid unless sessions.find_one( { :_id => sid } )
64
+ end
65
+ end
66
+
67
+ # Check session expiration date
68
+ def fresh?( ses_obj )
69
+ return true if ses_obj['expire'] == 0
70
+ now = Time.now
71
+ ses_obj['expire'] >= now
72
+ end
73
+
74
+ # Clean out all expired sessions
75
+ def clean_expired!
76
+ sessions.remove( { :expire => { '$lt' => Time.now } } )
77
+ end
78
+
79
+ # parse server description string into host, port, db, cltn
80
+ def parse_server_desc( desc )
81
+ tokens = desc.split( "/" )
82
+ raise "Invalid server description" unless tokens.size == 3
83
+ server_desc = tokens[0].split( ":" )
84
+ raise "Invalid host:port description" unless server_desc.size == 2
85
+ return server_desc.first, server_desc.last.to_i, tokens[1], tokens[2]
86
+ end
87
+
88
+ # fetch session with optional session id
89
+ def _get_session(env, sid)
90
+ if sid
91
+ ses_obj = sessions.find_one( { :_id => sid } )
92
+ session = MongoRack::SessionHash.new( ses_obj['data'] ) if ses_obj and fresh?( ses_obj )
93
+ end
94
+
95
+ unless sid and session
96
+ env['rack.errors'].puts("Session '#{sid.inspect}' not found, initializing...") if $VERBOSE and not sid.nil?
97
+ session = {}
98
+ sid = generate_sid
99
+ ret = sessions.save( { :_id => sid, :data => session } )
100
+ raise "Session collision on '#{sid.inspect}'" unless ret
101
+ end
102
+ session.instance_variable_set( '@old', MongoRack::SessionHash.new.merge(session) )
103
+ return [sid, session]
104
+ rescue => boom
105
+ warn "#{self} is unable to find server."
106
+ warn $!.inspect
107
+ return [ nil, {} ]
108
+ end
109
+
110
+ # update session information with new settings
111
+ def _set_session(env, sid, new_session, options)
112
+ ses_obj = sessions.find_one( { :_id => sid } )
113
+ if ses_obj
114
+ session = MongoRack::SessionHash.new( ses_obj['data'] )
115
+ else
116
+ session = MongoRack::SessionHash.new
117
+ end
118
+
119
+ if options[:renew] or options[:drop]
120
+ sessions.remove( { :_id => sid } )
121
+ return false if options[:drop]
122
+ sid = generate_sid
123
+ sessions.insert( {:_id => sid, :data => {} } )
124
+ end
125
+ old_session = new_session.instance_variable_get('@old') || MongoRack::SessionHash.new
126
+ merged = merge_sessions( sid, old_session, new_session, session )
127
+
128
+ expiry = options[:expire_after]
129
+ expiry = expiry ? Time.now + options[:expire_after] : 0
130
+
131
+ # BOZO ! Use upserts here if minor changes ?
132
+ sessions.save( { :_id => sid, :data => merged, :expire => expiry } )
133
+ return sid
134
+ rescue => boom
135
+ warn "#{self} is unable to find server."
136
+ warn $!.inspect
137
+ return false
138
+ end
139
+
140
+ # merge old, new to current session state
141
+ def merge_sessions( sid, old_s, new_s, cur={} )
142
+ unless Hash === old_s and Hash === new_s
143
+ warn 'Bad old or new sessions provided.'
144
+ return cur
145
+ end
146
+
147
+ delete = old_s.keys - new_s.keys
148
+ warn "//@#{sid}: delete #{delete*','}" if $VERBOSE and not delete.empty?
149
+ delete.each{ |k| cur.delete(k) }
150
+
151
+ update = new_s.keys.select{ |k| new_s[k] != old_s[k] }
152
+ warn "//@#{sid}: update #{update*','}" if $VERBOSE and not update.empty?
153
+ update.each{ |k| cur[k] = new_s[k] }
154
+
155
+ cur
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,144 @@
1
+ # Snagged HashWithIndifferentAccess for A/S
2
+ module MongoRack
3
+ class SessionHash < Hash
4
+
5
+ # Need to enable users to access session using either a symbol or a string as key
6
+ # This call wraps hash to provide this kind of access. No default allowed here. If a key
7
+ # is not found nil will be returned.
8
+ def initialize(constructor = {})
9
+ if constructor.is_a?(Hash)
10
+ super(constructor)
11
+ update(constructor)
12
+ self.default = nil
13
+ else
14
+ super(constructor)
15
+ end
16
+ end
17
+
18
+ # Checks for default value. If key does not exits returns default for hash
19
+ def default(key = nil)
20
+ if key.is_a?(Symbol) && include?(key = key.to_s)
21
+ self[key]
22
+ else
23
+ super
24
+ end
25
+ end
26
+
27
+ alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
28
+ alias_method :regular_update, :update unless method_defined?(:regular_update)
29
+
30
+ # Assigns a new value to the hash:
31
+ #
32
+ # hash = SessionHash.new
33
+ # hash[:key] = "value"
34
+ #
35
+ def []=(key, value)
36
+ regular_writer(convert_key(key), convert_value(value))
37
+ end
38
+
39
+ # Updates the instantized hash with values from the second:
40
+ #
41
+ # hash_1 = SessionHash.new
42
+ # hash_1[:key] = "value"
43
+ #
44
+ # hash_2 = SessionHash.new
45
+ # hash_2[:key] = "New Value!"
46
+ #
47
+ # hash_1.update(hash_2) # => {"key"=>"New Value!"}
48
+ #
49
+ def update(other_hash)
50
+ other_hash.each_pair { |key, value| regular_writer(convert_key(key), convert_value(value)) }
51
+ self
52
+ end
53
+
54
+ alias_method :merge!, :update
55
+
56
+ # Checks the hash for a key matching the argument passed in:
57
+ #
58
+ # hash = SessionHash.new
59
+ # hash["key"] = "value"
60
+ # hash.key? :key # => true
61
+ # hash.key? "key" # => true
62
+ #
63
+ def key?(key)
64
+ super(convert_key(key))
65
+ end
66
+
67
+ alias_method :include?, :key?
68
+ alias_method :has_key?, :key?
69
+ alias_method :member?, :key?
70
+
71
+ # Fetches the value for the specified key, same as doing hash[key]
72
+ def fetch(key, *extras)
73
+ super(convert_key(key), *extras)
74
+ end
75
+
76
+ # Returns an array of the values at the specified indices:
77
+ #
78
+ # hash = SessionHash.new
79
+ # hash[:a] = "x"
80
+ # hash[:b] = "y"
81
+ # hash.values_at("a", "b") # => ["x", "y"]
82
+ #
83
+ def values_at(*indices)
84
+ indices.collect {|key| self[convert_key(key)]}
85
+ end
86
+
87
+ # Returns an exact copy of the hash.
88
+ def dup
89
+ SessionHash.new(self)
90
+ end
91
+
92
+ # Merges the instantized and the specified hashes together, giving precedence to the values from the second hash
93
+ # Does not overwrite the existing hash.
94
+ def merge(hash)
95
+ self.dup.update(hash)
96
+ end
97
+
98
+ # Removes a specified key from the hash.
99
+ def delete(key)
100
+ super(convert_key(key))
101
+ end
102
+
103
+ #:nodoc:
104
+ def stringify_keys!; self end
105
+ #:nodoc:
106
+ def symbolize_keys!; self end
107
+ #:nodoc:
108
+ def to_options!; self end
109
+
110
+ # Convert to a Hash with String keys.
111
+ def to_hash
112
+ Hash.new(default).merge(self)
113
+ end
114
+
115
+ # =========================================================================
116
+ private
117
+
118
+ # converts key to string if symbol
119
+ def convert_key(key)
120
+ key.kind_of?(Symbol) ? key.to_s : key
121
+ end
122
+
123
+ # check value and converts sub obj to session hash if any
124
+ def convert_value(value)
125
+ case value
126
+ when Hash
127
+ value.with_session_access
128
+ when Array
129
+ value.collect { |e| e.is_a?(Hash) ? e.with_session_access : e }
130
+ else
131
+ value
132
+ end
133
+ end
134
+ end
135
+ end
136
+
137
+ module MongoRack
138
+ module SessionAccess
139
+ def with_session_access
140
+ hash = MongoRack::SessionHash.new( self )
141
+ hash
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,207 @@
1
+ require File.join(File.dirname(__FILE__), %w[spec_helper])
2
+
3
+ describe Rack::Session::Mongo do
4
+ before :all do
5
+ @session_key = 'rack.session'
6
+ @session_match = /#{@session_key}=[0-9a-fA-F]+;/
7
+ @db_name = 'mongo_test'
8
+ @cltn_name = 'sessions'
9
+
10
+ @con = Mongo::Connection.new
11
+ @db = @con.db( @db_name )
12
+ @sessions = @db['sessions']
13
+
14
+ @incrementor = lambda do |env|
15
+ env[@session_key]['counter'] ||= 0
16
+ env[@session_key]['counter'] += 1
17
+ Rack::Response.new( env[@session_key].inspect ).to_a
18
+ end
19
+ end
20
+
21
+ it "should connect to a valid server" do
22
+ Rack::Session::Mongo.new( @incrementor, :server => "localhost:27017/#{@db_name}/#{@cltn_name}" )
23
+ end
24
+
25
+ it "should fail if bad server specified" do
26
+ lambda do
27
+ Rack::Session::Mongo.new( @incrementor, :server => "blee:1111/#{@db_name}/#{@cltn_name}" )
28
+ end.should raise_error( Mongo::ConnectionFailure )
29
+ end
30
+
31
+ describe "cookies" do
32
+ before :each do
33
+ @pool = Rack::Session::Mongo.new( @incrementor, :server => "localhost:27017/#{@db_name}/#{@cltn_name}" )
34
+ end
35
+
36
+ it "should create a new cookie correctly" do
37
+ res = Rack::MockRequest.new( @pool ).get( "/", 'rack.multithread' => false )
38
+ res['Set-Cookie'].should match( /^#{@session_key}=/ )
39
+ res.body.should == '{"counter"=>1}'
40
+ session_id = res['Set-Cookie'].match( /^#{@session_key}=(.*?);.*?/ )[1]
41
+ mongo_check( res, :counter, 1 )
42
+ end
43
+
44
+ it "should determine a session from a cookie" do
45
+ req = Rack::MockRequest.new( @pool )
46
+ res = req.get("/", 'rack.multithread' => false )
47
+ cookie = res["Set-Cookie"]
48
+ req.get("/", "HTTP_COOKIE" => cookie, 'rack.multithread' => false ).body.should == '{"counter"=>2}'
49
+ mongo_check( res, :counter, 2 )
50
+ req.get("/", "HTTP_COOKIE" => cookie, 'rack.multithread' => false ).body.should == '{"counter"=>3}'
51
+ mongo_check( res, :counter, 3 )
52
+ end
53
+
54
+ it "survives nonexistant cookies" do
55
+ bad_cookie = "rack.session=bumblebeetuna"
56
+ res = Rack::MockRequest.new( @pool ).get("/", "HTTP_COOKIE" => bad_cookie, 'rack.multithread' => false )
57
+ res.body.should == '{"counter"=>1}'
58
+ cookie = res["Set-Cookie"][@session_match]
59
+ cookie.should_not match( /#{bad_cookie}/ )
60
+ end
61
+
62
+ it "maintains freshness" do
63
+ pool = Rack::Session::Mongo.new( @incrementor, :server => "localhost:27017/#{@db_name}/#{@cltn_name}", :expire_after => 1 )
64
+ res = Rack::MockRequest.new(pool).get('/', 'rack.multithread' => false )
65
+ res.body.should include('"counter"=>1')
66
+ cookie = res["Set-Cookie"]
67
+ res = Rack::MockRequest.new(pool).get('/', "HTTP_COOKIE" => cookie, 'rack.multithread' => false )
68
+ res["Set-Cookie"].should == cookie
69
+ res.body.should include('"counter"=>2')
70
+ puts 'Sleeping to expire session' if $DEBUG
71
+ sleep 2
72
+ res = Rack::MockRequest.new(pool).get('/', "HTTP_COOKIE" => cookie, 'rack.multithread' => false )
73
+ res["Set-Cookie"].should_not == cookie
74
+ res.body.should include( '"counter"=>1' )
75
+ end
76
+
77
+ it "deletes cookies with :drop option" do
78
+ drop_session = lambda do |env|
79
+ env['rack.session.options'][:drop] = true
80
+ @incrementor.call(env)
81
+ end
82
+
83
+ req = Rack::MockRequest.new(@pool)
84
+ drop = Rack::Utils::Context.new(@pool, drop_session)
85
+ dreq = Rack::MockRequest.new(drop)
86
+
87
+ res0 = req.get("/", 'rack.multithread' => false )
88
+ session = (cookie = res0["Set-Cookie"])[@session_match]
89
+ res0.body.should == '{"counter"=>1}'
90
+
91
+ res1 = req.get("/", "HTTP_COOKIE" => cookie, 'rack.multithread' => false )
92
+ res1["Set-Cookie"][@session_match].should == session
93
+ res1.body.should == '{"counter"=>2}'
94
+
95
+ res2 = dreq.get("/", "HTTP_COOKIE" => cookie, 'rack.multithread' => false )
96
+ res2["Set-Cookie"].should == nil
97
+ res2.body.should == '{"counter"=>3}'
98
+
99
+ res3 = req.get("/", "HTTP_COOKIE" => cookie, 'rack.multithread' => false)
100
+ res3["Set-Cookie"][@session_match].should_not == session
101
+ res3.body.should == '{"counter"=>1}'
102
+ end
103
+
104
+ it "provides new session id with :renew option" do
105
+ renew_session = lambda do |env|
106
+ env['rack.session.options'][:renew] = true
107
+ @incrementor.call(env)
108
+ end
109
+
110
+ req = Rack::MockRequest.new(@pool)
111
+ renew = Rack::Utils::Context.new(@pool, renew_session)
112
+ rreq = Rack::MockRequest.new(renew)
113
+
114
+ res0 = req.get("/", 'rack.multithread' => false )
115
+ session = (cookie = res0["Set-Cookie"])[@session_match]
116
+ res0.body.should == '{"counter"=>1}'
117
+
118
+ res1 = req.get("/", "HTTP_COOKIE" => cookie, 'rack.multithread' => false )
119
+ res1["Set-Cookie"][@session_match].should == session
120
+ res1.body.should == '{"counter"=>2}'
121
+
122
+ res2 = rreq.get("/", "HTTP_COOKIE" => cookie, 'rack.multithread' => false )
123
+ new_cookie = res2["Set-Cookie"]
124
+ new_session = new_cookie[@session_match]
125
+ new_session.should_not == session
126
+ res2.body.should == '{"counter"=>3}'
127
+
128
+ res3 = req.get("/", "HTTP_COOKIE" => new_cookie, 'rack.multithread' => false )
129
+ res3["Set-Cookie"][@session_match].should == new_session
130
+ res3.body.should == '{"counter"=>4}'
131
+ end
132
+
133
+ it "omits cookie with :defer option" do
134
+ defer_session = lambda do |env|
135
+ env['rack.session.options'][:defer] = true
136
+ @incrementor.call(env)
137
+ end
138
+
139
+ req = Rack::MockRequest.new(@pool)
140
+ defer = Rack::Utils::Context.new(@pool, defer_session)
141
+ dreq = Rack::MockRequest.new(defer)
142
+
143
+ res0 = req.get("/", 'rack.multithread' => false )
144
+ session = (cookie = res0["Set-Cookie"])[@session_match]
145
+ res0.body.should == '{"counter"=>1}'
146
+
147
+ res1 = req.get("/", "HTTP_COOKIE" => cookie, 'rack.multithread' => false )
148
+ res1["Set-Cookie"][@session_match].should == session
149
+ res1.body.should == '{"counter"=>2}'
150
+
151
+ res2 = dreq.get("/", "HTTP_COOKIE" => cookie, 'rack.multithread' => false )
152
+ res2["Set-Cookie"].should == nil
153
+ res2.body.should == '{"counter"=>3}'
154
+
155
+ res3 = req.get("/", "HTTP_COOKIE" => cookie, 'rack.multithread' => false )
156
+ res3["Set-Cookie"][@session_match].should == session
157
+ res3.body.should == '{"counter"=>4}'
158
+ end
159
+
160
+ it "multithread: should cleanly merge sessions" do
161
+ @pool = Rack::Session::Mongo.new( @incrementor, :server => "localhost:27017/#{@db_name}/#{@cltn_name}", :pool_size => 10 )
162
+
163
+ req = Rack::MockRequest.new( @pool )
164
+
165
+ res = req.get('/')
166
+ res.body.should == '{"counter"=>1}'
167
+ cookie = res["Set-Cookie"]
168
+ sess_id = cookie[/#{@pool.key}=([^,;]+)/,1]
169
+
170
+ r = Array.new( 10 ) do
171
+ Thread.new( req ) do |run|
172
+ req.get( "/", "HTTP_COOKIE" => cookie, 'rack.multithread' => true )
173
+ end
174
+ end.reverse.map{ |t| t.join.value }
175
+
176
+ r.each do |res|
177
+ res['Set-Cookie'].should == cookie
178
+ res.body.should include( '"counter"=>2' )
179
+ end
180
+
181
+ drop_counter = proc do |env|
182
+ env['rack.session'].delete 'counter'
183
+ env['rack.session']['foo'] = 'bar'
184
+ [200, {'Content-Type'=>'text/plain'}, env['rack.session'].inspect]
185
+ end
186
+ tses = Rack::Utils::Context.new @pool, drop_counter
187
+ treq = Rack::MockRequest.new( tses )
188
+
189
+ tnum = 10
190
+ r = Array.new(tnum) do
191
+ Thread.new(treq) do |run|
192
+ run.get('/', "HTTP_COOKIE" => cookie, 'rack.multithread' => true)
193
+ end
194
+ end.reverse.map{|t| t.join.value }
195
+ r.each do |res|
196
+ res['Set-Cookie'].should == cookie
197
+ res.body.should include('"foo"=>"bar"')
198
+ end
199
+
200
+ session = @pool.sessions.find_one( {:_id => sess_id } )
201
+ session['data'].size.should == 1
202
+ session['data']['counter'].should be_nil
203
+ session['data']['foo'].should == 'bar'
204
+ end
205
+
206
+ end
207
+ end