TokyoCabinet pseudo-environment
[rbot] / lib / rbot / registry / tc.rb
1 #-- vim:sw=2:et
2 #++
3 #
4 # :title: DB interface
5
6 begin
7   require 'bdb'
8   if BDB::VERSION_MAJOR < 4
9     fatal "Your bdb (Berkeley DB) version #{BDB::VERSION} is too old!"
10     fatal "rbot will only run with bdb version 4 or higher, please upgrade."
11     fatal "For maximum reliability, upgrade to version 4.2 or higher."
12     raise BDB::Fatal, BDB::VERSION + " is too old"
13   end
14
15   if BDB::VERSION_MAJOR == 4 and BDB::VERSION_MINOR < 2
16     warning "Your bdb (Berkeley DB) version #{BDB::VERSION} may not be reliable."
17     warning "If possible, try upgrade version 4.2 or later."
18   end
19 rescue LoadError
20   warning "rbot couldn't load the bdb module. Old registries won't be upgraded"
21 rescue Exception => e
22   warning "rbot couldn't load the bdb module: #{e.pretty_inspect}"
23 end
24
25
26
27
28 require 'tokyocabinet'
29
30 module Irc
31
32   class DBFatal < Exception ; end
33
34   if defined? BDB
35   # DBHash is for tying a hash to disk (using bdb).
36   # Call it with an identifier, for example "mydata". It'll look for
37   # mydata.db, if it exists, it will load and reference that db.
38   # Otherwise it'll create and empty db called mydata.db
39   class DBHash
40
41     # absfilename:: use +key+ as an actual filename, don't prepend the bot's
42     #               config path and don't append ".db"
43     def initialize(bot, key, absfilename=false)
44       @bot = bot
45       @key = key
46       relfilename = @bot.path key
47       relfilename << '.db'
48       if absfilename && File.exist?(key)
49         # db already exists, use it
50         @db = DBHash.open_db(key)
51       elsif absfilename
52         # create empty db
53         @db = DBHash.create_db(key)
54       elsif File.exist? relfilename
55         # db already exists, use it
56         @db = DBHash.open_db relfilename
57       else
58         # create empty db
59         @db = DBHash.create_db relfilename
60       end
61     end
62
63     def method_missing(method, *args, &block)
64       return @db.send(method, *args, &block)
65     end
66
67     def DBHash.create_db(name)
68       debug "DBHash: creating empty db #{name}"
69       return BDB::Hash.open(name, nil,
70       BDB::CREATE | BDB::EXCL, 0600)
71     end
72
73     def DBHash.open_db(name)
74       debug "DBHash: opening existing db #{name}"
75       return BDB::Hash.open(name, nil, "r+", 0600)
76     end
77
78   end
79   # make BTree lookups case insensitive
80   module ::BDB
81     class CIBtree < Btree
82       def bdb_bt_compare(a, b)
83         if a == nil || b == nil
84           warning "CIBTree: comparing #{a.inspect} (#{self[a].inspect}) with #{b.inspect} (#{self[b].inspect})"
85         end
86         (a||'').downcase <=> (b||'').downcase
87       end
88     end
89   end
90   end
91
92   module ::TokyoCabinet
93     class CIBDB < TokyoCabinet::BDB
94       # Since TokyoCabinet does not have the concept of an environment, we have to do the
95       # database management ourselves. In particular, we have to keep a list of open
96       # registries to be sure we to close all of them on exit
97       @@bot_registries=[]
98       def self.close_bot_registries
99         @@bot_registries.each { |reg| reg.close }
100       end
101
102       def open(path, omode)
103         res = super
104         if res
105           self.setcmpfunc(Proc.new do |a, b|
106             a.downcase <=> b.downcase
107           end)
108           @@bot_registries << res
109         end
110         res
111       end
112     end
113   end
114
115   # DBTree is a BTree equivalent of DBHash, with case insensitive lookups.
116   class DBTree
117     # absfilename:: use +key+ as an actual filename, don't prepend the bot's
118     #               config path and don't append ".db"
119     def initialize(bot, key, absfilename=false)
120       @bot = bot
121       @key = key
122
123       relfilename = @bot.path key
124       relfilename << '.tdb'
125
126       if absfilename && File.exist?(key)
127         # db already exists, use it
128         @db = DBTree.open_db(key)
129       elsif absfilename
130         # create empty db
131         @db = DBTree.create_db(key)
132       elsif File.exist? relfilename
133         # db already exists, use it
134         @db = DBTree.open_db relfilename
135       else
136         # create empty db
137         @db = DBTree.create_db relfilename
138       end
139       oldbasename = (absfilename ? key : relfilename).gsub(/\.tdb$/, ".db")
140       if File.exists? oldbasename and defined? BDB
141         # upgrading
142         warning "Upgrading old database #{oldbasename}..."
143         oldb = ::BDB::CIBtree.open(oldbasename, nil, "r", 0600)
144         oldb.each_key do |k|
145           @db.outlist k
146           @db.putlist k, (oldb.duplicates(k, false))
147         end
148         oldb.close
149         File.rename oldbasename, oldbasename+".bak"
150       end
151       @db
152     end
153
154     def method_missing(method, *args, &block)
155       return @db.send(method, *args, &block)
156     end
157
158     def DBTree.create_db(name)
159       debug "DBTree: creating empty db #{name}"
160       db = TokyoCabinet::CIBDB.new
161       res = db.open(name, TokyoCabinet::CIBDB::OREADER | TokyoCabinet::CIBDB::OCREAT | TokyoCabinet::CIBDB::OWRITER)
162        warning "DBTree: creating empty db #{name}: #{db.errmsg(db.ecode) unless res}"
163       return db
164     end
165
166     def DBTree.open_db(name)
167       debug "DBTree: opening existing db #{name}"
168       db = TokyoCabinet::CIBDB.new
169       res = db.open(name, TokyoCabinet::CIBDB::OREADER | TokyoCabinet::CIBDB::OWRITER)
170        warning "DBTree:opening db #{name}: #{db.errmsg(db.ecode) unless res}"
171       return db
172     end
173
174     def DBTree.cleanup_logs()
175       # no-op
176     end
177
178     def DBTree.stats()
179       # no-op
180     end
181
182     def DBTree.cleanup_env()
183       CIBDB.close_bot_registries
184     end
185
186   end
187
188 end
189
190 module Irc
191 class Bot
192
193   # This class is now used purely for upgrading from prior versions of rbot
194   # the new registry is split into multiple DBHash objects, one per plugin
195   class Registry
196     def initialize(bot)
197       @bot = bot
198       upgrade_data
199       upgrade_data2
200     end
201
202     # check for older versions of rbot with data formats that require updating
203     # NB this function is called _early_ in init(), pretty much all you have to
204     # work with is @bot.botclass.
205     def upgrade_data
206       oldreg = @bot.path 'registry.db'
207       if defined? DBHash
208         newreg = @bot.path 'plugin_registry.db'
209         if File.exist?(oldreg)
210           log _("upgrading old-style (rbot 0.9.5 or earlier) plugin registry to new format")
211           old = ::BDB::Hash.open(oldreg, nil, "r+", 0600)
212           new = ::BDB::CIBtree.open(newreg, nil, ::BDB::CREATE | ::BDB::EXCL, 0600)
213           old.each {|k,v|
214             new[k] = v
215           }
216           old.close
217           new.close
218           File.rename(oldreg, oldreg + ".old")
219         end
220       else
221         warning "Won't upgrade data: BDB not installed" if File.exist? oldreg
222       end
223     end
224
225     def upgrade_data2
226       oldreg = @bot.path 'plugin_registry.db'
227       if not defined? BDB
228         warning "Won't upgrade data: BDB not installed" if File.exist? oldreg
229         return
230       end
231       newdir = @bot.path 'registry'
232       if File.exist?(oldreg)
233         Dir.mkdir(newdir) unless File.exist?(newdir)
234         env = BDB::Env.open(@bot.botclass, BDB::INIT_TRANSACTION | BDB::CREATE | BDB::RECOVER)# | BDB::TXN_NOSYNC)
235         dbs = Hash.new
236         log _("upgrading previous (rbot 0.9.9 or earlier) plugin registry to new split format")
237         old = BDB::CIBtree.open(oldreg, nil, "r+", 0600, "env" => env)
238         old.each {|k,v|
239           prefix,key = k.split("/", 2)
240           prefix.downcase!
241           # subregistries were split with a +, now they are in separate folders
242           if prefix.gsub!(/\+/, "/")
243             # Ok, this code needs to be put in the db opening routines
244             dirs = File.dirname("#{@bot.botclass}/registry/#{prefix}.db").split("/")
245             dirs.length.times { |i|
246               dir = dirs[0,i+1].join("/")+"/"
247               unless File.exist?(dir)
248                 log _("creating subregistry directory #{dir}")
249                 Dir.mkdir(dir)
250               end
251             }
252           end
253           unless dbs.has_key?(prefix)
254             log _("creating db #{@bot.botclass}/registry/#{prefix}.tdb")
255             dbs[prefix] = TokyoCabinet::CIBDB.open("#{@bot.botclass}/registry/#{prefix}.tdb",
256              TokyoCabinet::CIBDB::OREADER | TokyoCabinet::CIBDB::OCREAT | TokyoCabinet::CIBDB::OWRITER)
257           end
258           dbs[prefix][key] = v
259         }
260         old.close
261         File.rename(oldreg, oldreg + ".old")
262         dbs.each {|k,v|
263           log _("closing db #{k}")
264           v.close
265         }
266         env.close
267       end
268     end
269
270   # This class provides persistent storage for plugins via a hash interface.
271   # The default mode is an object store, so you can store ruby objects and
272   # reference them with hash keys. This is because the default store/restore
273   # methods of the plugins' RegistryAccessor are calls to Marshal.dump and
274   # Marshal.restore,
275   # for example:
276   #   blah = Hash.new
277   #   blah[:foo] = "fum"
278   #   @registry[:blah] = blah
279   # then, even after the bot is shut down and disconnected, on the next run you
280   # can access the blah object as it was, with:
281   #   blah = @registry[:blah]
282   # The registry can of course be used to store simple strings, fixnums, etc as
283   # well, and should be useful to store or cache plugin data or dynamic plugin
284   # configuration.
285   #
286   # WARNING:
287   # in object store mode, don't make the mistake of treating it like a live
288   # object, e.g. (using the example above)
289   #   @registry[:blah][:foo] = "flump"
290   # will NOT modify the object in the registry - remember that Registry#[]
291   # returns a Marshal.restore'd object, the object you just modified in place
292   # will disappear. You would need to:
293   #   blah = @registry[:blah]
294   #   blah[:foo] = "flump"
295   #   @registry[:blah] = blah
296   #
297   # If you don't need to store objects, and strictly want a persistant hash of
298   # strings, you can override the store/restore methods to suit your needs, for
299   # example (in your plugin):
300   #   def initialize
301   #     class << @registry
302   #       def store(val)
303   #         val
304   #       end
305   #       def restore(val)
306   #         val
307   #       end
308   #     end
309   #   end
310   # Your plugins section of the registry is private, it has its own namespace
311   # (derived from the plugin's class name, so change it and lose your data).
312   # Calls to registry.each etc, will only iterate over your namespace.
313   class Accessor
314
315     attr_accessor :recovery
316
317     # plugins don't call this - a Registry::Accessor is created for them and
318     # is accessible via @registry.
319     def initialize(bot, name)
320       @bot = bot
321       @name = name.downcase
322       @filename = @bot.path 'registry', @name
323       dirs = File.dirname(@filename).split("/")
324       dirs.length.times { |i|
325         dir = dirs[0,i+1].join("/")+"/"
326         unless File.exist?(dir)
327           debug "creating subregistry directory #{dir}"
328           Dir.mkdir(dir)
329         end
330       }
331       @filename << ".tdb"
332       @registry = nil
333       @default = nil
334       @recovery = nil
335       # debug "initializing registry accessor with name #{@name}"
336     end
337
338     def registry
339         @registry ||= DBTree.new @bot, "registry/#{@name}"
340     end
341
342     def flush
343       # debug "fushing registry #{registry}"
344       return if !@registry
345       registry.sync
346     end
347
348     def close
349       # debug "closing registry #{registry}"
350       return if !@registry
351       registry.close
352     end
353
354     # convert value to string form for storing in the registry
355     # defaults to Marshal.dump(val) but you can override this in your module's
356     # registry object to use any method you like.
357     # For example, if you always just handle strings use:
358     #   def store(val)
359     #     val
360     #   end
361     def store(val)
362       Marshal.dump(val)
363     end
364
365     # restores object from string form, restore(store(val)) must return val.
366     # If you override store, you should override restore to reverse the
367     # action.
368     # For example, if you always just handle strings use:
369     #   def restore(val)
370     #     val
371     #   end
372     def restore(val)
373       begin
374         Marshal.restore(val)
375       rescue Exception => e
376         error _("failed to restore marshal data for #{val.inspect}, attempting recovery or fallback to default")
377         debug e
378         if defined? @recovery and @recovery
379           begin
380             return @recovery.call(val)
381           rescue Exception => ee
382             error _("marshal recovery failed, trying default")
383             debug ee
384           end
385         end
386         return default
387       end
388     end
389
390     # lookup a key in the registry
391     def [](key)
392       if File.exist?(@filename) and registry.has_key?(key.to_s)
393         return restore(registry[key.to_s])
394       else
395         return default
396       end
397     end
398
399     # set a key in the registry
400     def []=(key,value)
401       registry[key.to_s] = store(value)
402     end
403
404     # set the default value for registry lookups, if the key sought is not
405     # found, the default will be returned. The default default (har) is nil.
406     def set_default (default)
407       @default = default
408     end
409
410     def default
411       @default && (@default.dup rescue @default)
412     end
413
414     # just like Hash#each
415     def each(set=nil, bulk=0, &block)
416       return nil unless File.exist?(@filename)
417       registry.fwmkeys(set.to_s).each {|key|
418         block.call(key, restore(registry[key]))
419       }
420     end
421
422     # just like Hash#each_key
423     def each_key(set=nil, bulk=0, &block)
424       return nil unless File.exist?(@filename)
425       registry.fwmkeys(set.to_s).each do |key|
426         block.call(key)
427       end
428     end
429
430     # just like Hash#each_value
431     def each_value(set=nil, bulk=0, &block)
432       return nil unless File.exist?(@filename)
433       registry.fwmkeys(set.to_s).each do |key|
434         block.call(restore(registry[key]))
435       end
436     end
437
438     # just like Hash#has_key?
439     def has_key?(key)
440       return false unless File.exist?(@filename)
441       return registry.has_key?(key.to_s)
442     end
443
444     alias include? has_key?
445     alias member? has_key?
446     alias key? has_key?
447
448     # just like Hash#has_both?
449     def has_both?(key, value)
450       return false unless File.exist?(@filename)
451       registry.has_key?(key.to_s) and registry.has_value?(store(value))
452     end
453
454     # just like Hash#has_value?
455     def has_value?(value)
456       return false unless File.exist?(@filename)
457       return registry.has_value?(store(value))
458     end
459
460     # just like Hash#index?
461     def index(value)
462       self.each do |k,v|
463         return k if v == value
464       end
465       return nil
466     end
467
468     # delete a key from the registry
469     def delete(key)
470       return default unless File.exist?(@filename)
471       return registry.delete(key.to_s)
472     end
473
474     # returns a list of your keys
475     def keys
476       return [] unless File.exist?(@filename)
477       return registry.keys
478     end
479
480     # Return an array of all associations [key, value] in your namespace
481     def to_a
482       return [] unless File.exist?(@filename)
483       ret = Array.new
484       registry.each {|key, value|
485         ret << [key, restore(value)]
486       }
487       return ret
488     end
489
490     # Return an hash of all associations {key => value} in your namespace
491     def to_hash
492       return {} unless File.exist?(@filename)
493       ret = Hash.new
494       registry.each {|key, value|
495         ret[key] = restore(value)
496       }
497       return ret
498     end
499
500     # empties the registry (restricted to your namespace)
501     def clear
502       return true unless File.exist?(@filename)
503       registry.vanish
504     end
505     alias truncate clear
506
507     # returns an array of the values in your namespace of the registry
508     def values
509       return [] unless File.exist?(@filename)
510       ret = Array.new
511       self.each {|k,v|
512         ret << restore(v)
513       }
514       return ret
515     end
516
517     def sub_registry(prefix)
518       return Accessor.new(@bot, @name + "/" + prefix.to_s)
519     end
520
521     # returns the number of keys in your registry namespace
522     def length
523       return 0 unless File.exist?(@filename)
524       registry.length
525     end
526     alias size length
527
528     # That is btree!
529     def putdup(key, value)
530       registry.putdup(key.to_s, store(value))
531     end
532
533     def putlist(key, values)
534       registry.putlist(key.to_s, value.map {|v| store(v)})
535     end
536
537     def getlist(key)
538       return [] unless File.exist?(@filename)
539       (registry.getlist(key.to_s) || []).map {|v| restore(v)}
540     end
541   end
542
543   end
544 end
545 end