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