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