mongo_rack 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/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