On Thu, 8 Feb 2007, Eric Hodel wrote:

>
> I've heard a few people talk of it (mainly, TupleSpace with a persistence 
> (database)), but to my knowledge nobody's done it.
>

i have.  if people like it, i'll release next week.


1) it's a hack


2) it's persistent


   jib:~ > cat a.rb
   require 'sqlitespace'

   rss = Rinda::SQLiteSpace.new
   any_pair = [nil, nil]
   rss.read_all(any_pair).sort.each{|t| p t}   # note this prints FIRST!
   rss.write [Time.now, rand]

   jib:~ > ruby a.rb

   jib:~ > ruby a.rb
   [Fri Feb 09 16:53:37 MST 2007, 0.388129945611581]

   jib:~ > ruby a.rb
   [Fri Feb 09 16:53:37 MST 2007, 0.388129945611581]
   [Fri Feb 09 16:53:52 MST 2007, 0.250816953601316]

   jib:~ > ruby a.rb
   [Fri Feb 09 16:53:37 MST 2007, 0.388129945611581]
   [Fri Feb 09 16:53:52 MST 2007, 0.250816953601316]
   [Fri Feb 09 16:53:54 MST 2007, 0.400566845666617]


3) it's pretty fast (and uses little cpu)

   jib:~ > cat b.rb
   require 'sqlitespace'

   rss = Rinda::SQLiteSpace.new

   rss.transaction{ 42424.times{ write [Time.now, rand] } }


   jib:~ > time ruby b.rb

   real    0m19.063s
   user    0m18.110s
   sys     0m0.560s


4) it's a hack


5) obviously it requires sqlite to be installed, but that's it


6) it's a hack



here's the code:



harp:~ > cat sqlitespace.rb

require 'monitor'
require 'thread'
require 'drb/drb'
require 'rinda/rinda'
require 'rinda/tuplespace'
require 'fileutils'
require 'base64'

begin
   require 'rubygems'
rescue LoadError
   42
ensure
   require 'sqlite'
end


module Rinda
   class SQLiteSpace < ::Rinda::TupleSpace
     def initialize(timeout=60, prefix=home_dir)
       super()
     ensure
       setup prefix
     end

     %w[ prefix dir bag read_waiter take_waiter notify_waiter ].each{|a| attr a}

     def setup(prefix=home_dir)
       @prefix = prefix
       FileUtils.mkdir_p @prefix
       @dir = File.join @prefix, 'sqlitespace'
       FileUtils.mkdir_p @dir

       @bag = SQLiteBag.new File.join(@dir, 'bag')
       @read_waiter = SQLiteBag.new File.join(@dir, 'read_waiter')
       @take_waiter = SQLiteBag.new File.join(@dir, 'take_watier')
       @notify_waiter = SQLiteBag.new File.join(@dir, 'notify_watier')
     end

     def transaction *a, &b
       @bag.transaction{
         @read_waiter.transaction{
           @take_waiter.transaction{
             @notify_waiter.transaction{
               instance_eval &b
             }
           }
         }
       }
     end


     def home_dir
       ENV['HOME'] || ENV['USERPROFILE']  # *nix || winblows
     end
   end

   class SQLiteBag
     class DB
       SCHEMA = <<-SQL
         create table bag(
           id integer primary key,
           size integer,
           blobsize integer,
           blob blob
         )
       SQL

       %w[ path db in_transaction ].each{|a| attr a}

       def initialize path
         @path = path
         @db = ::SQLite::Database.new(path) rescue ::SQLite::Database.new(path,0)
         @in_transaction = false
         setup
       end

       def setup
         begin
           @db.execute(SCHEMA){}
         rescue ::SQLite::SQLException => e
           42
         end
       end

     #
     # for older rubys
     #
       begin
         Base64
         def encode64 blob
           Base64.encode64 blob
         end

         def decode64 blob
           Base64.decode64 blob
         end
       rescue NameError
       end

     #
     # handles DRbUndumped feature of Rinda::TupleEntry class
     #
       def encode obj
         data = [obj.class]
         obj.instance_variables.each do |ivar|
           value = obj.instance_variable_get ivar
           data << [ivar, value]
         end
         blob = encode64(Marshal.dump(data))
       end
     #
     # handles DRbUndumped feature of Rinda::TupleEntry class
     #
       def decode blob
         data = Marshal.load(decode64(blob))
         klass = data.shift
         obj = klass.allocate
         data.each{|ivar, value| obj.instance_variable_set ivar, value}
         obj
       end

       def execute *a, &b
         @db.execute *a, &b
       end

       def transaction *a, &b
         raise 'nested transaction' if @in_transaction
         begin
           @db.execute 'begin transaction'
           @in_transaction = true
           yield
         ensure
           @db.execute 'end transaction'
           @in_transaction = false
         end
       end

       def template_entry(ary)
         if ::Rinda::TemplateEntry === ary
           ary
         else
           ::Rinda::TemplateEntry.new ary
         end
       end

       def push(ary)
         size = ary.size
         blob = encode ary
         blobsize = blob.size
         @db.execute "insert into bag values(null, #{ size }, #{ blob.size }, '#{ blob }')"
       end

       def delete(ary)
         size = ary.size
         blob = encode ary
         blobsize = blob.size
         @db.execute "delete from bag where size=#{ size } and blobsize=#{ blobsize } and blob='#{ blob }'"
       end

       def find_all(template, limit=nil, &block)
         template= Rinda::TemplateEntry.new(Array.new(template)) if Numeric === template
         size = template.size
         all = []
         @db.execute "select blob from bag where size=#{ size } #{ limit ? ('limit=%d' % limit) : '' }" do |tuple|
           blob = tuple['blob']
           t = decode blob
           if t.alive? && template.match(t)
             block ? block[t] : (all << t)
           end
         end
         block ? nil : all
       end

       def find(template, limit=1, &block)
         find_all(template, limit).first
       end

       def find_all_template(tuple, limit=nil, &block)
         size = tuple.size
         all = []
         @db.execute "select blob from bag where size=#{ size } #{ limit ? ('limit=%d' % limit) : '' }" do |tuple|
           blob = tuple['blob']
           t = decode blob
           if t.alive? && t.match(tuple)
             block ? block[t] : (all << t)
           end
         end
         block ? nil : all
       end

       def delete_unless_alive
         deleted = []
         @db.transaction do
           find_all do |tuple|
             unless tuple.alive?
               delete tuple
               deleted << tuple
             end
           end
         end
         deleted
       end
     end

     %w[ path db ].each{|a| attr a}

     def initialize path
       @path = path
       @db = DB.new path
       @hash = {}
     end

     def transaction *a, &b
       @db.transaction *a, &b
     end

     %w( push delete find_all find find_all_template delete ).each do |m|
       module_eval <<-code
         def #{ m } *a, &b
           @db.#{ m } *a, &b
         end
       code
     end
   end
end



enjoy.

-a
-- 
we can deny everything, except that we have the possibility of being better.
simply reflect on that.
- the dalai lama

-a
-- 
we can deny everything, except that we have the possibility of being better.
simply reflect on that.
- the dalai lama