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