Permalink
Browse files

* 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
  • Loading branch information...
mkristian committed Sep 9, 2009
1 parent e0cee5c commit 6fbf4b366aba7dcabc334728f560e15902bc8bbe
View
@@ -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
View
@@ -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)
View
@@ -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
@@ -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
@@ -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
@@ -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))
@@ -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
@@ -1,5 +1,5 @@
module Rack
module DataMapper
- VERSION = '0.0.0'.freeze
+ VERSION = '0.2.0'.freeze
end
end
View
Binary file not shown.
@@ -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
@@ -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.