db adaptors: nil internal variable when closing
[rbot] / lib / rbot / registry / bdb.rb
1 #-- vim:sw=2:et
2 #++
3 #
4 # :title: Berkeley DB interface
5
6 begin
7   require 'bdb'
8 rescue LoadError
9   fatal "rbot couldn't load the bdb module, perhaps you need to install it? try http://www.ruby-lang.org/en/raa-list.rhtml?name=bdb or http://github.com/knu/ruby-bdb"
10 rescue Exception => e
11   fatal "rbot couldn't load the bdb module: #{e.pretty_inspect}"
12 end
13
14 if not defined? BDB
15   exit 2
16 end
17
18 module Irc
19   DBFatal = BDB::Fatal
20 end
21
22 if BDB::VERSION_MAJOR < 4
23   fatal "Your bdb (Berkeley DB) version #{BDB::VERSION} is too old!"
24   fatal "rbot will only run with bdb version 4 or higher, please upgrade."
25   fatal "For maximum reliability, upgrade to version 4.2 or higher."
26   raise BDB::Fatal, BDB::VERSION + " is too old"
27 end
28
29 if BDB::VERSION_MAJOR == 4 and BDB::VERSION_MINOR < 2
30   warning "Your bdb (Berkeley DB) version #{BDB::VERSION} may not be reliable."
31   warning "If possible, try upgrade version 4.2 or later."
32 end
33
34 # make BTree lookups case insensitive
35 module BDB
36   class CIBtree < Btree
37     def bdb_bt_compare(a, b)
38       if a == nil || b == nil
39         warning "CIBTree: comparing key #{a.inspect} with #{b.inspect}"
40       end
41       (a||'').downcase <=> (b||'').downcase
42     end
43   end
44 end
45
46 module Irc
47
48   # DBHash is for tying a hash to disk (using bdb).
49   # Call it with an identifier, for example "mydata". It'll look for
50   # mydata.db, if it exists, it will load and reference that db.
51   # Otherwise it'll create and empty db called mydata.db
52   class DBHash
53
54     # absfilename:: use +key+ as an actual filename, don't prepend the bot's
55     #               config path and don't append ".db"
56     def initialize(bot, key, absfilename=false)
57       @bot = bot
58       @key = key
59       relfilename = @bot.path key
60       relfilename << '.db'
61       if absfilename && File.exist?(key)
62         # db already exists, use it
63         @db = DBHash.open_db(key)
64       elsif absfilename
65         # create empty db
66         @db = DBHash.create_db(key)
67       elsif File.exist? relfilename
68         # db already exists, use it
69         @db = DBHash.open_db relfilename
70       else
71         # create empty db
72         @db = DBHash.create_db relfilename
73       end
74     end
75
76     def method_missing(method, *args, &block)
77       return @db.send(method, *args, &block)
78     end
79
80     def DBHash.create_db(name)
81       debug "DBHash: creating empty db #{name}"
82       return BDB::Hash.open(name, nil,
83       BDB::CREATE | BDB::EXCL, 0600)
84     end
85
86     def DBHash.open_db(name)
87       debug "DBHash: opening existing db #{name}"
88       return BDB::Hash.open(name, nil, "r+", 0600)
89     end
90
91   end
92
93
94   # DBTree is a BTree equivalent of DBHash, with case insensitive lookups.
95   class DBTree
96     @@env=nil
97     # TODO: make this customizable
98     # Note that it must be at least four times lg_bsize
99     @@lg_max = 8*1024*1024
100     # absfilename:: use +key+ as an actual filename, don't prepend the bot's
101     #               config path and don't append ".db"
102     def initialize(bot, key, absfilename=false)
103       @bot = bot
104       @key = key
105       if @@env.nil?
106         begin
107           @@env = BDB::Env.open(@bot.botclass, BDB::INIT_TRANSACTION | BDB::CREATE | BDB::RECOVER, "set_lg_max" => @@lg_max)
108           debug "DBTree: environment opened with max log size #{@@env.conf['lg_max']}"
109         rescue => e
110           debug "DBTree: failed to open environment: #{e.pretty_inspect}. Retrying ..."
111           @@env = BDB::Env.open(@bot.botclass, BDB::INIT_TRANSACTION | BDB::CREATE |  BDB::RECOVER)
112         end
113         #@@env = BDB::Env.open(@bot.botclass, BDB::CREATE | BDB::INIT_MPOOL | BDB::RECOVER)
114       end
115
116       relfilename = @bot.path key
117       relfilename << '.db'
118
119       if absfilename && File.exist?(key)
120         # db already exists, use it
121         @db = DBTree.open_db(key)
122       elsif absfilename
123         # create empty db
124         @db = DBTree.create_db(key)
125       elsif File.exist? relfilename
126         # db already exists, use it
127         @db = DBTree.open_db relfilename
128       else
129         # create empty db
130         @db = DBTree.create_db relfilename
131       end
132     end
133
134     def method_missing(method, *args, &block)
135       return @db.send(method, *args, &block)
136     end
137
138     def DBTree.create_db(name)
139       debug "DBTree: creating empty db #{name}"
140       return @@env.open_db(BDB::CIBtree, name, nil, BDB::CREATE | BDB::EXCL, 0600)
141     end
142
143     def DBTree.open_db(name)
144       debug "DBTree: opening existing db #{name}"
145       return @@env.open_db(BDB::CIBtree, name, nil, "r+", 0600)
146     end
147
148     def DBTree.cleanup_logs()
149       if @@env.nil?
150         debug "No BDB environment, skipping checkpoint"
151         return
152       end
153       begin
154         debug "DBTree: checkpointing ..."
155         @@env.checkpoint
156       rescue Exception => e
157         debug "Failed: #{e.pretty_inspect}"
158       end
159       begin
160         debug "DBTree: flushing log ..."
161         @@env.log_flush
162         logs = @@env.log_archive(BDB::ARCH_ABS)
163         debug "DBTree: deleting archivable logs: #{logs.join(', ')}."
164         logs.each { |log|
165           File.delete(log)
166         }
167       rescue Exception => e
168         debug "Failed: #{e.pretty_inspect}"
169       end
170     end
171
172     def DBTree.stats()
173       if @@env.nil?
174         debug "No BDB environment, no stats"
175         return
176       end
177       begin
178         debug "General stats:"
179         debug @@env.stat
180         debug "Lock stats:"
181         debug @@env.lock_stat
182         debug "Log stats:"
183         debug @@env.log_stat
184         debug "Txn stats:"
185         debug @@env.txn_stat
186       rescue
187         debug "Couldn't dump stats"
188       end
189     end
190
191     def DBTree.cleanup_env()
192       if @@env.nil?
193         debug "No BDB environment, skipping cleanup"
194         return
195       end
196       begin
197         debug "DBTree: checking transactions ..."
198         has_active_txn = @@env.txn_stat["st_nactive"] > 0
199         if has_active_txn
200           warning "DBTree: not all transactions completed!"
201         end
202         DBTree.cleanup_logs
203         debug "DBTree: closing environment #{@@env}"
204         path = @@env.home
205         @@env.close
206         @@env = nil
207         if has_active_txn
208           debug "DBTree: keeping file because of incomplete transactions"
209         else
210           debug "DBTree: cleaning up environment in #{path}"
211           BDB::Env.remove("#{path}")
212         end
213       rescue Exception => e
214         error "failed to clean up environment: #{e.pretty_inspect}"
215       end
216     end
217
218   end
219
220 end
221
222
223 module Irc
224 class Bot
225
226   # This class is now used purely for upgrading from prior versions of rbot
227   # the new registry is split into multiple DBHash objects, one per plugin
228   class Registry
229     def initialize(bot)
230       @bot = bot
231       upgrade_data
232       upgrade_data2
233     end
234
235     # check for older versions of rbot with data formats that require updating
236     # NB this function is called _early_ in init(), pretty much all you have to
237     # work with is @bot.botclass.
238     def upgrade_data
239       oldreg = @bot.path 'registry.db'
240       newreg = @bot.path 'plugin_registry.db'
241       if File.exist?(oldreg)
242         log _("upgrading old-style (rbot 0.9.5 or earlier) plugin registry to new format")
243         old = BDB::Hash.open(oldreg, nil, "r+", 0600)
244         new = BDB::CIBtree.open(newreg, nil, BDB::CREATE | BDB::EXCL, 0600)
245         old.each {|k,v|
246           new[k] = v
247         }
248         old.close
249         new.close
250         File.rename(oldreg, oldreg + ".old")
251       end
252     end
253
254     def upgrade_data2
255       oldreg = @bot.path 'plugin_registry.db'
256       newdir = @bot.path 'registry'
257       if File.exist?(oldreg)
258         Dir.mkdir(newdir) unless File.exist?(newdir)
259         env = BDB::Env.open(@bot.botclass, BDB::INIT_TRANSACTION | BDB::CREATE | BDB::RECOVER)# | BDB::TXN_NOSYNC)
260         dbs = Hash.new
261         log _("upgrading previous (rbot 0.9.9 or earlier) plugin registry to new split format")
262         old = BDB::CIBtree.open(oldreg, nil, "r+", 0600, "env" => env)
263         old.each {|k,v|
264           prefix,key = k.split("/", 2)
265           prefix.downcase!
266           # subregistries were split with a +, now they are in separate folders
267           if prefix.gsub!(/\+/, "/")
268             # Ok, this code needs to be put in the db opening routines
269             dirs = File.dirname("#{@bot.botclass}/registry/#{prefix}.db").split("/")
270             dirs.length.times { |i|
271               dir = dirs[0,i+1].join("/")+"/"
272               unless File.exist?(dir)
273                 log _("creating subregistry directory #{dir}")
274                 Dir.mkdir(dir)
275               end
276             }
277           end
278           unless dbs.has_key?(prefix)
279             log _("creating db #{@bot.botclass}/registry/#{prefix}.db")
280             dbs[prefix] = BDB::CIBtree.open("#{@bot.botclass}/registry/#{prefix}.db",
281               nil, BDB::CREATE | BDB::EXCL,
282               0600, "env" => env)
283           end
284           dbs[prefix][key] = v
285         }
286         old.close
287         File.rename(oldreg, oldreg + ".old")
288         dbs.each {|k,v|
289           log _("closing db #{k}")
290           v.close
291         }
292         env.close
293       end
294     end
295
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 << ".db"
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.flush
373       registry.sync
374     end
375
376     def close
377       # debug "closing registry #{registry}"
378       return if !@registry
379       registry.close
380       @registry = nil
381     end
382
383     # convert value to string form for storing in the registry
384     # defaults to Marshal.dump(val) but you can override this in your module's
385     # registry object to use any method you like.
386     # For example, if you always just handle strings use:
387     #   def store(val)
388     #     val
389     #   end
390     def store(val)
391       Marshal.dump(val)
392     end
393
394     # restores object from string form, restore(store(val)) must return val.
395     # If you override store, you should override restore to reverse the
396     # action.
397     # For example, if you always just handle strings use:
398     #   def restore(val)
399     #     val
400     #   end
401     def restore(val)
402       begin
403         Marshal.restore(val)
404       rescue Exception => e
405         error _("failed to restore marshal data for #{val.inspect}, attempting recovery or fallback to default")
406         debug e
407         if defined? @recovery and @recovery
408           begin
409             return @recovery.call(val)
410           rescue Exception => ee
411             error _("marshal recovery failed, trying default")
412             debug ee
413           end
414         end
415         return default
416       end
417     end
418
419     # lookup a key in the registry
420     def [](key)
421       if File.exist?(@filename) && registry.has_key?(key)
422         return restore(registry[key])
423       else
424         return default
425       end
426     end
427
428     # set a key in the registry
429     def []=(key,value)
430       registry[key] = store(value)
431     end
432
433     # set the default value for registry lookups, if the key sought is not
434     # found, the default will be returned. The default default (har) is nil.
435     def set_default (default)
436       @default = default
437     end
438
439     def default
440       @default && (@default.dup rescue @default)
441     end
442
443     # just like Hash#each
444     def each(set=nil, bulk=0, &block)
445       return nil unless File.exist?(@filename)
446       registry.each(set, bulk) {|key,value|
447         block.call(key, restore(value))
448       }
449     end
450
451     # just like Hash#each_key
452     def each_key(set=nil, bulk=0, &block)
453       return nil unless File.exist?(@filename)
454       registry.each_key(set, bulk) {|key|
455         block.call(key)
456       }
457     end
458
459     # just like Hash#each_value
460     def each_value(set=nil, bulk=0, &block)
461       return nil unless File.exist?(@filename)
462       registry.each_value(set, bulk) { |value|
463         block.call(restore(value))
464       }
465     end
466
467     # just like Hash#has_key?
468     def has_key?(key)
469       return false unless File.exist?(@filename)
470       return registry.has_key?(key)
471     end
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       return registry.has_both?(key, 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       return nil unless File.exist?(@filename)
491       ind = registry.index(store(value))
492       if ind
493         return ind
494       else
495         return nil
496       end
497     end
498
499     # delete a key from the registry
500     def delete(key)
501       return default unless File.exist?(@filename)
502       return registry.delete(key)
503     end
504
505     # returns a list of your keys
506     def keys
507       return [] unless File.exist?(@filename)
508       return registry.keys
509     end
510
511     # Return an array of all associations [key, value] in your namespace
512     def to_a
513       return [] unless File.exist?(@filename)
514       ret = Array.new
515       registry.each {|key, value|
516         ret << [key, restore(value)]
517       }
518       return ret
519     end
520
521     # Return an hash of all associations {key => value} in your namespace
522     def to_hash
523       return {} unless File.exist?(@filename)
524       ret = Hash.new
525       registry.each {|key, value|
526         ret[key] = restore(value)
527       }
528       return ret
529     end
530
531     # empties the registry (restricted to your namespace)
532     def clear
533       return true unless File.exist?(@filename)
534       registry.clear
535     end
536     alias truncate clear
537
538     # returns an array of the values in your namespace of the registry
539     def values
540       return [] unless File.exist?(@filename)
541       ret = Array.new
542       self.each {|k,v|
543         ret << restore(v)
544       }
545       return ret
546     end
547
548     def sub_registry(prefix)
549       return Accessor.new(@bot, @name + "/" + prefix.to_s)
550     end
551
552     # returns the number of keys in your registry namespace
553     def length
554       self.keys.length
555     end
556     alias size length
557
558   end
559
560   end
561 end
562 end