module Daybreak # Daybreak::DB contains the public api for Daybreak. It includes # Enumerable for functional goodies like map, each, reduce and friends. # @api public class DB include Enumerable # Set default value, can be a callable attr_writer :default # Create a new Daybreak::DB. The second argument is the default value # to store when accessing a previously unset key, this follows the # Hash standard. # @param [String] file the path to the db file # @param [Hash] options a hash that contains the options for creating a new database # @option options [Class] :serializer Serializer class # @option options [Class] :format Format class # @option options [Object] :default Default value # @yield [key] a block that will return the default value to store. # @yieldparam [String] key the key to be stored. def initialize(file, options = {}, &block) @serializer = (options[:serializer] || Serializer::Default).new @table = Hash.new &method(:hash_default) @journal = Journal.new(file, (options[:format] || Format).new, @serializer) do |record| if !record @table.clear elsif record.size == 1 @table.delete(record.first) else @table[record.first] = @serializer.load(record.last) end end @default = block ? block : options[:default] @mutex = Mutex.new # Mutex used by #synchronize and #lock @@databases_mutex.synchronize { @@databases << self } end # Database file name # @return [String] database file name def file @journal.file end # Return default value belonging to key # @param [Object] key the default value to retrieve. # @return [Object] value the default value def default(key = nil) @table.default(@serializer.key_for(key)) end # Retrieve a value at key from the database. If the default value was specified # when this database was created, that value will be set and returned. Aliased # as get. # @param [Object] key the value to retrieve from the database. # @return [Object] the value def [](key) @table[@serializer.key_for(key)] end alias_method :get, '[]' # Set a key in the database to be written at some future date. If the data # needs to be persisted immediately, call db.set(key, value, true). # @param [Object] key the key of the storage slot in the database # @param [Object] value the value to store # @return [Object] the value def []=(key, value) key = @serializer.key_for(key) @journal << [key, value] @table[key] = value end alias_method :set, '[]=' # set! flushes data immediately to disk. # @param [Object] key the key of the storage slot in the database # @param [Object] value the value to store # @return [Object] the value def set!(key, value) set(key, value) flush value end # Delete a key from the database # @param [Object] key the key of the storage slot in the database # @return [Object] the value def delete(key) key = @serializer.key_for(key) @journal << [key] @table.delete(key) end # Immediately delete the key on disk. # @param [Object] key the key of the storage slot in the database # @return [Object] the value def delete!(key) value = delete(key) flush value end # Update database with hash (Fast batch update) # @param [Hash] hash the key/value hash # @return [DB] self def update(hash) shash = {} hash.each do |key, value| shash[@serializer.key_for(key)] = value end @journal << shash @table.update(shash) self end # Updata database and flush data to disk. # @param [Hash] hash the key/value hash # @return [DB] self def update!(hash) update(hash) @journal.flush end # Does this db have this key? # @param [Object] key the key to check if the DB has it # @return [Boolean] def has_key?(key) @table.has_key?(@serializer.key_for(key)) end alias_method :key?, :has_key? alias_method :include?, :has_key? alias_method :member?, :has_key? # Does this db have this value? # @param [Object] value the value to check if the DB has it # @return [Boolean] def has_value?(value) @table.has_value?(value) end alias_method :value?, :has_value? # Return the number of stored items. # @return [Fixnum] def size @table.size end alias_method :length, :size # Utility method that will return the size of the database in bytes, # useful for determining when to compact # @return [Fixnum] def bytesize @journal.bytesize end # Counter of how many records are in the journal # @return [Fixnum] def logsize @journal.size end # Return true if database is empty. # @return [Boolean] def empty? @table.empty? end # Iterate over the key, value pairs in the database. # @yield [key, value] blk the iterator for each key value pair. # @yieldparam key the key. # @yieldparam value the value from the database. def each(&block) @table.each(&block) end # Return the keys in the db. # @return [Array] def keys @table.keys end # Flush all changes to disk. # @return [DB] self def flush @journal.flush self end # Sync the database with what is on disk, by first flushing changes, and # then loading the new records if necessary. # @return [DB] self def load @journal.load self end alias_method :sunrise, :load # Lock the database for an exclusive commit across processes and threads # @note This method performs an expensive locking over process boundaries. # If you want to synchronize only between threads, use #synchronize. # @see #synchronize # @yield a block where every change to the database is synced # @yieldparam [DB] db # @return result of the block def lock @mutex.synchronize { @journal.lock { yield self } } end # Synchronize access to the database from multiple threads # @note Daybreak is not thread safe, if you want to access it from # multiple threads, all accesses have to be in the #synchronize block. # @see #lock # @yield a block where every change to the database is synced # @yieldparam [DB] db # @return result of the block def synchronize @mutex.synchronize { yield self } end # Remove all keys and values from the database. # @return [DB] self def clear @table.clear @journal.clear self end # Compact the database to remove stale commits and reduce the file size. # @return [DB] self def compact @journal.compact { @table } self end # Close the database for reading and writing. # @return nil def close @journal.close @@databases_mutex.synchronize { @@databases.delete(self) } nil end # Check to see if we've already closed the database. # @return [Boolean] def closed? @journal.closed? end private # @private @@databases = [] # @private @@databases_mutex = Mutex.new # A handler that will ensure that databases are closed and synced when the # current process exits. # @private def self.exit_handler loop do db = @@databases_mutex.synchronize { @@databases.first } break unless db warn "Daybreak database #{db.file} was not closed, state might be inconsistent" begin db.close rescue Exception => ex warn "Failed to close daybreak database: #{ex.message}" end end end at_exit { Daybreak::DB.exit_handler } # The block used in @table for new records def hash_default(_, key) if @default != nil value = @default.respond_to?(:call) ? @default.call(key) : @default @journal << [key, value] @table[key] = value end end end end