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