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