Skip to content

Commit 6fbf4b3

Browse files
committed
* added more specs for the session store
* added java SoftHashMap to avoid memory leaks of the session cache (when used with jruby and java >=1.5) * some bug fixes with the cache * some more docu
1 parent e0cee5c commit 6fbf4b3

File tree

8 files changed

+283
-18
lines changed

8 files changed

+283
-18
lines changed

History.txt

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
1-
=== 1.0.0 / 2009-02-09
1+
=== 0.2.0 / 2009-09-09
2+
3+
* added more specs for the session store
4+
5+
* added java SoftHashMap to avoid memory leaks of the session cache (when used with jruby and java >=1.5)
6+
7+
* some bug fixes with the cache
8+
9+
* made it a rubyforge.org project and deployed a gem for it
10+
11+
=== 0.1.0 / 2009-02-09
212

313
* 1 major enhancement
414

README.txt

+33
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,43 @@
22

33
* http://github.com/mkristian/rack_datamapper
44

5+
* http://rack-datamapper.rubyforge.org
6+
57
== DESCRIPTION:
68

79
this collection of plugins helps to add datamapper functionality to Rack. there is a IdentityMaps plugin which wrappes the request and with it all database actions are using that identity map. the transaction related plugin TransactionBoundaries and RestfulTransactions wrappes the request into a transaction. for using datamapper to store session data there is the DatamapperStore.
810

11+
=== DataMapper::Session::Abstract::Store
12+
13+
this is actual store class which can be wrapped to be used in a specific environement, i.e. Rack::Session::Datamapper. this store can the same options as the session store from rack, see
14+
15+
* http://rack.rubyforge.org/doc/Rack/Session/Pool.html
16+
17+
* http://rack.rubyforge.org/doc/Rack/Session/Abstract/ID.html
18+
19+
there are two more options
20+
21+
* :session_class - (optional) must be a DataMapper::Resource with session_id, data properties.
22+
23+
* :cache - Boolean (default: false) if set to true the store will first try to retrieve the session from a memory cache otherwise fallback to the session_class resource. in case the platform is java (jruby) the cache uses SoftReferences which clears the cache on severe memory shortage, but it needs java 1.5 or higher for this.
24+
25+
== Rack Middleware
26+
27+
all these middleware take the name of the datamapper repository (which you configure via DataMapper.setup(:name, ....) as second constructor argument (default is :default)
28+
29+
=== DataMapper::RestfulTransactions
30+
31+
wrappers the request inside an transaction for POST,PUT,DELETE methods
32+
33+
=== DataMapper::TransactionBoundaries
34+
35+
wrappers the all request inside an transaction
36+
37+
=== DataMapper::IdentityMaps
38+
39+
wrappers the all request inside an identity scope
40+
41+
942
== LICENSE:
1043

1144
(The MIT License)

Rakefile

-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ require 'pathname'
1010
require 'yard'
1111

1212
Hoe.new('rack_datamapper', Rack::DataMapper::VERSION) do |p|
13-
# p.rubyforge_name = 'dm-utf8x' # if different than lowercase project name
1413
p.developer('mkristian', '[email protected]')
1514
end
1615

@@ -28,8 +27,6 @@ Spec::Rake::SpecTask.new(:spec) do |t|
2827
t.spec_files = Pathname.glob('./spec/**/*_spec.rb')
2928
end
3029

31-
require 'yard'
32-
3330
YARD::Rake::YardocTask.new
3431

3532
# vim: syntax=Ruby

lib/rack_datamapper/session/abstract/store.rb

+30-13
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,23 @@ class Store
88
def initialize(app, options, id_generator)
99
@mutex = Mutex.new
1010
if options.delete(:cache)
11-
@@cache = {}
11+
@@cache = if RUBY_PLATFORM =~ /java/
12+
begin
13+
# to avoid memory leaks use a hashmap which clears
14+
# itself on severe memory shortage
15+
require 'softhashmap'
16+
m = Java.SoftHashMap.new
17+
def m.delete(key)
18+
remove(key)
19+
end
20+
m
21+
rescue
22+
# fallback to non java Hash
23+
{}
24+
end
25+
else
26+
{}
27+
end
1228
@@semaphore = Mutex.new
1329
else
1430
@@cache = nil unless self.class.class_variable_defined? :@@cache
@@ -31,7 +47,8 @@ def get_session(env, sid)
3147
unless sid and session
3248
env['rack.errors'].puts("Session '#{sid.inspect}' not found, initializing...") if $VERBOSE and not sid.nil?
3349
sid = @id_generator.call
34-
session = @@session_class.create(:session_id => sid, :updated_at => Time.now)
50+
session = @@session_class.create(:session_id => sid)
51+
@@cache[sid] = session if @@cache
3552
end
3653
#session.instance_variable_set('@old', {}.merge(session))
3754

@@ -48,17 +65,17 @@ def set_session(env, sid, session_data, options)
4865
else
4966
@@session_class.get(sid)
5067
end
51-
return false if session.nil?
52-
if options[:renew] or options[:drop]
53-
@@cache.delete(sid) if @@cache
54-
session.destroy
55-
return false if options[:drop]
56-
sid = @id_generator.call
57-
session = @@session_class.create(:session_id => sid, :updated_at => Time.now)
58-
@@cache[sid] = session if @@cache
59-
end
60-
# old_session = new_session.instance_variable_get('@old') || {}
61-
# session = merge_sessions session_id, old_session, new_session, session
68+
return false if session.nil?
69+
if options[:renew] or options[:drop]
70+
@@cache.delete(sid) if @@cache
71+
session.destroy
72+
return false if options[:drop]
73+
sid = @id_generator.call
74+
session = @@session_class.create(:session_id => sid)
75+
@@cache[sid] = session if @@cache
76+
end
77+
# old_session = new_session.instance_variable_get('@old') || {}
78+
# session = merge_sessions session_id, old_session, new_session, session
6279
session.data = session_data
6380
if session.save
6481
session.session_id

lib/rack_datamapper/version.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
module Rack
22
module DataMapper
3-
VERSION = '0.0.0'.freeze
3+
VERSION = '0.2.0'.freeze
44
end
55
end

lib/softhashmap.jar

3.56 KB
Binary file not shown.

spec/datamapper_store_spec.rb

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
$LOAD_PATH << File.dirname(__FILE__)
2+
require 'spec_helper'
3+
4+
describe DataMapper::Session::Abstract::Store do
5+
6+
describe 'without cache' do
7+
8+
def mock_session(stubs={})
9+
@mock_session ||= mock(DataMapper::Session::Abstract::Session, stubs)
10+
end
11+
12+
before :each do
13+
@store = DataMapper::Session::Abstract::Store.new(nil,
14+
{},
15+
Proc.new do
16+
1
17+
end
18+
)
19+
end
20+
21+
it 'should get the session data' do
22+
DataMapper::Session::Abstract::Session.stub!(:get).and_return(mock_session)
23+
mock_session.should_receive(:data).and_return({:id => "id"})
24+
@store.get_session({}, "sid").should == ["sid",{:id => "id"}]
25+
end
26+
27+
it 'should create a new session' do
28+
DataMapper::Session::Abstract::Session.should_receive(:create).and_return(mock_session)
29+
mock_session.should_receive(:data).and_return({})
30+
result = @store.get_session({}, nil)
31+
result[0].should_not be_nil
32+
result[1].should == {}
33+
end
34+
35+
it 'should set the session data' do
36+
DataMapper::Session::Abstract::Session.should_receive(:create).and_return(mock_session)
37+
DataMapper::Session::Abstract::Session.should_receive(:get).twice.and_return(mock_session)
38+
mock_session.should_receive(:data).and_return({})
39+
mock_session.should_receive(:data=).with({:id => 432})
40+
mock_session.should_receive(:save).and_return(true)
41+
mock_session.should_receive(:data).and_return({:id => 123})
42+
43+
session_id = @store.get_session({}, nil)[0]
44+
mock_session.should_receive(:session_id).and_return(session_id);
45+
@store.set_session({}, session_id, {:id => 432}, {}).should == session_id
46+
result = @store.get_session({}, session_id)
47+
48+
result[0].should_not be_nil
49+
result[1].should == {:id => 123}
50+
end
51+
end
52+
53+
describe 'with cache' do
54+
55+
def mock_session(stubs={})
56+
@mock_session ||= mock(DataMapper::Session::Abstract::Session, stubs)
57+
end
58+
59+
before :each do
60+
@store = DataMapper::Session::Abstract::Store.new(nil,
61+
{:cache => true},
62+
Proc.new do
63+
1
64+
end)
65+
end
66+
67+
it 'should create a new session' do
68+
DataMapper::Session::Abstract::Session.should_receive(:create).and_return(mock_session)
69+
mock_session.should_receive(:data).and_return({})
70+
result = @store.get_session({}, nil)
71+
result[0].should_not be_nil
72+
result[1].should == {}
73+
end
74+
75+
it 'should get the session data from storage' do
76+
DataMapper::Session::Abstract::Session.stub!(:get).and_return(mock_session)
77+
mock_session.should_receive(:data).and_return({:id => "id"})
78+
@store.get_session({}, "sid").should == ["sid",{:id => "id"}]
79+
end
80+
81+
it 'should get the session data from cache' do
82+
DataMapper::Session::Abstract::Session.should_receive(:create).and_return(mock_session)
83+
mock_session.should_receive(:data).twice.and_return({})
84+
session_id = @store.get_session({}, nil)[0]
85+
86+
result = @store.get_session({}, session_id)
87+
result[0].should_not be_nil
88+
result[1].should == {}
89+
end
90+
91+
it 'should set the session data' do
92+
DataMapper::Session::Abstract::Session.should_receive(:create).and_return(mock_session)
93+
mock_session.should_receive(:data).and_return({})
94+
mock_session.should_receive(:data=).with({:id => 432})
95+
mock_session.should_receive(:save).and_return(true)
96+
mock_session.should_receive(:data).and_return({:id => 123})
97+
98+
session_id = @store.get_session({}, nil)[0]
99+
mock_session.should_receive(:session_id).and_return(session_id);
100+
@store.set_session({}, session_id, {:id => 432},{}).should == session_id
101+
result = @store.get_session({}, session_id)
102+
103+
result[0].should_not be_nil
104+
result[1].should == {:id => 123}
105+
end
106+
end
107+
end

src/main/java/SoftHashMap.java

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/**
2+
* this class is taken with friendly permission to use it
3+
* from <a href="http://javaspecialists.co.za/archive/Issue098.html">javaspecialists.co.za/archive/Issue098.html</a> (section 'New SoftHashMap')
4+
*/
5+
import java.lang.ref.*;
6+
import java.util.*;
7+
import java.io.Serializable;
8+
9+
public class SoftHashMap <K, V> extends AbstractMap<K, V>
10+
implements Serializable {
11+
/** The internal HashMap that will hold the SoftReference. */
12+
private final Map<K, SoftReference<V>> hash =
13+
new HashMap<K, SoftReference<V>>();
14+
15+
private final Map<SoftReference<V>, K> reverseLookup =
16+
new HashMap<SoftReference<V>, K>();
17+
18+
/** Reference queue for cleared SoftReference objects. */
19+
private final ReferenceQueue<V> queue = new ReferenceQueue<V>();
20+
21+
public V get(Object key) {
22+
expungeStaleEntries();
23+
V result = null;
24+
// We get the SoftReference represented by that key
25+
SoftReference<V> soft_ref = hash.get(key);
26+
if (soft_ref != null) {
27+
// From the SoftReference we get the value, which can be
28+
// null if it has been garbage collected
29+
result = soft_ref.get();
30+
if (result == null) {
31+
// If the value has been garbage collected, remove the
32+
// entry from the HashMap.
33+
hash.remove(key);
34+
reverseLookup.remove(soft_ref);
35+
}
36+
}
37+
return result;
38+
}
39+
40+
private void expungeStaleEntries() {
41+
Reference<? extends V> sv;
42+
while ((sv = queue.poll()) != null) {
43+
hash.remove(reverseLookup.remove(sv));
44+
}
45+
}
46+
47+
public V put(K key, V value) {
48+
expungeStaleEntries();
49+
SoftReference<V> soft_ref = new SoftReference<V>(value, queue);
50+
reverseLookup.put(soft_ref, key);
51+
SoftReference<V> result = hash.put(key, soft_ref);
52+
if (result == null) return null;
53+
reverseLookup.remove(result);
54+
return result.get();
55+
}
56+
57+
public V remove(Object key) {
58+
expungeStaleEntries();
59+
SoftReference<V> result = hash.remove(key);
60+
if (result == null) return null;
61+
return result.get();
62+
}
63+
64+
public void clear() {
65+
hash.clear();
66+
reverseLookup.clear();
67+
}
68+
69+
public int size() {
70+
expungeStaleEntries();
71+
return hash.size();
72+
}
73+
74+
/**
75+
* Returns a copy of the key/values in the map at the point of
76+
* calling. However, setValue still sets the value in the
77+
* actual SoftHashMap.
78+
*/
79+
public Set<Entry<K,V>> entrySet() {
80+
expungeStaleEntries();
81+
Set<Entry<K,V>> result = new LinkedHashSet<Entry<K, V>>();
82+
for (final Entry<K, SoftReference<V>> entry : hash.entrySet()) {
83+
final V value = entry.getValue().get();
84+
if (value != null) {
85+
result.add(new Entry<K, V>() {
86+
public K getKey() {
87+
return entry.getKey();
88+
}
89+
public V getValue() {
90+
return value;
91+
}
92+
public V setValue(V v) {
93+
entry.setValue(new SoftReference<V>(v, queue));
94+
return value;
95+
}
96+
});
97+
}
98+
}
99+
return result;
100+
}
101+
}

0 commit comments

Comments
 (0)