Skip to content

Commit

Permalink
* added more specs for the session store
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
mkristian committed Sep 9, 2009
1 parent e0cee5c commit 6fbf4b3
Show file tree
Hide file tree
Showing 8 changed files with 283 additions and 18 deletions.
12 changes: 11 additions & 1 deletion History.txt
@@ -1,4 +1,14 @@
=== 1.0.0 / 2009-02-09
=== 0.2.0 / 2009-09-09

* 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

* made it a rubyforge.org project and deployed a gem for it

=== 0.1.0 / 2009-02-09

* 1 major enhancement

Expand Down
33 changes: 33 additions & 0 deletions README.txt
Expand Up @@ -2,10 +2,43 @@

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

* http://rack-datamapper.rubyforge.org

== DESCRIPTION:

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.

=== DataMapper::Session::Abstract::Store

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

* http://rack.rubyforge.org/doc/Rack/Session/Pool.html

* http://rack.rubyforge.org/doc/Rack/Session/Abstract/ID.html

there are two more options

* :session_class - (optional) must be a DataMapper::Resource with session_id, data properties.

* :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.

== Rack Middleware

all these middleware take the name of the datamapper repository (which you configure via DataMapper.setup(:name, ....) as second constructor argument (default is :default)

=== DataMapper::RestfulTransactions

wrappers the request inside an transaction for POST,PUT,DELETE methods

=== DataMapper::TransactionBoundaries

wrappers the all request inside an transaction

=== DataMapper::IdentityMaps

wrappers the all request inside an identity scope


== LICENSE:

(The MIT License)
Expand Down
3 changes: 0 additions & 3 deletions Rakefile
Expand Up @@ -10,7 +10,6 @@ require 'pathname'
require 'yard'

Hoe.new('rack_datamapper', Rack::DataMapper::VERSION) do |p|
# p.rubyforge_name = 'dm-utf8x' # if different than lowercase project name
p.developer('mkristian', 'm.kristian@web.de')
end

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

require 'yard'

YARD::Rake::YardocTask.new

# vim: syntax=Ruby
43 changes: 30 additions & 13 deletions lib/rack_datamapper/session/abstract/store.rb
Expand Up @@ -8,7 +8,23 @@ class Store
def initialize(app, options, id_generator)
@mutex = Mutex.new
if options.delete(:cache)
@@cache = {}
@@cache = if RUBY_PLATFORM =~ /java/
begin
# to avoid memory leaks use a hashmap which clears
# itself on severe memory shortage
require 'softhashmap'
m = Java.SoftHashMap.new
def m.delete(key)
remove(key)
end
m
rescue
# fallback to non java Hash
{}
end
else
{}
end
@@semaphore = Mutex.new
else
@@cache = nil unless self.class.class_variable_defined? :@@cache
Expand All @@ -31,7 +47,8 @@ def get_session(env, sid)
unless sid and session
env['rack.errors'].puts("Session '#{sid.inspect}' not found, initializing...") if $VERBOSE and not sid.nil?
sid = @id_generator.call
session = @@session_class.create(:session_id => sid, :updated_at => Time.now)
session = @@session_class.create(:session_id => sid)
@@cache[sid] = session if @@cache
end
#session.instance_variable_set('@old', {}.merge(session))

Expand All @@ -48,17 +65,17 @@ def set_session(env, sid, session_data, options)
else
@@session_class.get(sid)
end
return false if session.nil?
if options[:renew] or options[:drop]
@@cache.delete(sid) if @@cache
session.destroy
return false if options[:drop]
sid = @id_generator.call
session = @@session_class.create(:session_id => sid, :updated_at => Time.now)
@@cache[sid] = session if @@cache
end
# old_session = new_session.instance_variable_get('@old') || {}
# session = merge_sessions session_id, old_session, new_session, session
return false if session.nil?
if options[:renew] or options[:drop]
@@cache.delete(sid) if @@cache
session.destroy
return false if options[:drop]
sid = @id_generator.call
session = @@session_class.create(:session_id => sid)
@@cache[sid] = session if @@cache
end
# old_session = new_session.instance_variable_get('@old') || {}
# session = merge_sessions session_id, old_session, new_session, session
session.data = session_data
if session.save
session.session_id
Expand Down
2 changes: 1 addition & 1 deletion lib/rack_datamapper/version.rb
@@ -1,5 +1,5 @@
module Rack
module DataMapper
VERSION = '0.0.0'.freeze
VERSION = '0.2.0'.freeze
end
end
Binary file added lib/softhashmap.jar
Binary file not shown.
107 changes: 107 additions & 0 deletions spec/datamapper_store_spec.rb
@@ -0,0 +1,107 @@
$LOAD_PATH << File.dirname(__FILE__)
require 'spec_helper'

describe DataMapper::Session::Abstract::Store do

describe 'without cache' do

def mock_session(stubs={})
@mock_session ||= mock(DataMapper::Session::Abstract::Session, stubs)
end

before :each do
@store = DataMapper::Session::Abstract::Store.new(nil,
{},
Proc.new do
1
end
)
end

it 'should get the session data' do
DataMapper::Session::Abstract::Session.stub!(:get).and_return(mock_session)
mock_session.should_receive(:data).and_return({:id => "id"})
@store.get_session({}, "sid").should == ["sid",{:id => "id"}]
end

it 'should create a new session' do
DataMapper::Session::Abstract::Session.should_receive(:create).and_return(mock_session)
mock_session.should_receive(:data).and_return({})
result = @store.get_session({}, nil)
result[0].should_not be_nil
result[1].should == {}
end

it 'should set the session data' do
DataMapper::Session::Abstract::Session.should_receive(:create).and_return(mock_session)
DataMapper::Session::Abstract::Session.should_receive(:get).twice.and_return(mock_session)
mock_session.should_receive(:data).and_return({})
mock_session.should_receive(:data=).with({:id => 432})
mock_session.should_receive(:save).and_return(true)
mock_session.should_receive(:data).and_return({:id => 123})

session_id = @store.get_session({}, nil)[0]
mock_session.should_receive(:session_id).and_return(session_id);
@store.set_session({}, session_id, {:id => 432}, {}).should == session_id
result = @store.get_session({}, session_id)

result[0].should_not be_nil
result[1].should == {:id => 123}
end
end

describe 'with cache' do

def mock_session(stubs={})
@mock_session ||= mock(DataMapper::Session::Abstract::Session, stubs)
end

before :each do
@store = DataMapper::Session::Abstract::Store.new(nil,
{:cache => true},
Proc.new do
1
end)
end

it 'should create a new session' do
DataMapper::Session::Abstract::Session.should_receive(:create).and_return(mock_session)
mock_session.should_receive(:data).and_return({})
result = @store.get_session({}, nil)
result[0].should_not be_nil
result[1].should == {}
end

it 'should get the session data from storage' do
DataMapper::Session::Abstract::Session.stub!(:get).and_return(mock_session)
mock_session.should_receive(:data).and_return({:id => "id"})
@store.get_session({}, "sid").should == ["sid",{:id => "id"}]
end

it 'should get the session data from cache' do
DataMapper::Session::Abstract::Session.should_receive(:create).and_return(mock_session)
mock_session.should_receive(:data).twice.and_return({})
session_id = @store.get_session({}, nil)[0]

result = @store.get_session({}, session_id)
result[0].should_not be_nil
result[1].should == {}
end

it 'should set the session data' do
DataMapper::Session::Abstract::Session.should_receive(:create).and_return(mock_session)
mock_session.should_receive(:data).and_return({})
mock_session.should_receive(:data=).with({:id => 432})
mock_session.should_receive(:save).and_return(true)
mock_session.should_receive(:data).and_return({:id => 123})

session_id = @store.get_session({}, nil)[0]
mock_session.should_receive(:session_id).and_return(session_id);
@store.set_session({}, session_id, {:id => 432},{}).should == session_id
result = @store.get_session({}, session_id)

result[0].should_not be_nil
result[1].should == {:id => 123}
end
end
end
101 changes: 101 additions & 0 deletions src/main/java/SoftHashMap.java
@@ -0,0 +1,101 @@
/**
* this class is taken with friendly permission to use it
* from <a href="http://javaspecialists.co.za/archive/Issue098.html">javaspecialists.co.za/archive/Issue098.html</a> (section 'New SoftHashMap')
*/
import java.lang.ref.*;
import java.util.*;
import java.io.Serializable;

public class SoftHashMap <K, V> extends AbstractMap<K, V>
implements Serializable {
/** The internal HashMap that will hold the SoftReference. */
private final Map<K, SoftReference<V>> hash =
new HashMap<K, SoftReference<V>>();

private final Map<SoftReference<V>, K> reverseLookup =
new HashMap<SoftReference<V>, K>();

/** Reference queue for cleared SoftReference objects. */
private final ReferenceQueue<V> queue = new ReferenceQueue<V>();

public V get(Object key) {
expungeStaleEntries();
V result = null;
// We get the SoftReference represented by that key
SoftReference<V> soft_ref = hash.get(key);
if (soft_ref != null) {
// From the SoftReference we get the value, which can be
// null if it has been garbage collected
result = soft_ref.get();
if (result == null) {
// If the value has been garbage collected, remove the
// entry from the HashMap.
hash.remove(key);
reverseLookup.remove(soft_ref);
}
}
return result;
}

private void expungeStaleEntries() {
Reference<? extends V> sv;
while ((sv = queue.poll()) != null) {
hash.remove(reverseLookup.remove(sv));
}
}

public V put(K key, V value) {
expungeStaleEntries();
SoftReference<V> soft_ref = new SoftReference<V>(value, queue);
reverseLookup.put(soft_ref, key);
SoftReference<V> result = hash.put(key, soft_ref);
if (result == null) return null;
reverseLookup.remove(result);
return result.get();
}

public V remove(Object key) {
expungeStaleEntries();
SoftReference<V> result = hash.remove(key);
if (result == null) return null;
return result.get();
}

public void clear() {
hash.clear();
reverseLookup.clear();
}

public int size() {
expungeStaleEntries();
return hash.size();
}

/**
* Returns a copy of the key/values in the map at the point of
* calling. However, setValue still sets the value in the
* actual SoftHashMap.
*/
public Set<Entry<K,V>> entrySet() {
expungeStaleEntries();
Set<Entry<K,V>> result = new LinkedHashSet<Entry<K, V>>();
for (final Entry<K, SoftReference<V>> entry : hash.entrySet()) {
final V value = entry.getValue().get();
if (value != null) {
result.add(new Entry<K, V>() {
public K getKey() {
return entry.getKey();
}
public V getValue() {
return value;
}
public V setValue(V v) {
entry.setValue(new SoftReference<V>(v, queue));
return value;
}
});
}
}
return result;
}
}

0 comments on commit 6fbf4b3

Please sign in to comment.