module Daybreak
# Daybreak::DB contains the public api for Daybreak, you may extend it like
# any other Ruby class (i.e. to overwrite serialize and parse). It includes
# Enumerable for functional goodies like map, each, reduce and friends.
class DB
include Enumerable
# 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 default the default value to store and return when a key is
# not yet in the database.
# @yield [key] a block that will return the default value to store.
# @yieldparam [String] key the key to be stored.
def initialize(file, default=nil, &blk)
@table = {}
@file_name = file
@writer = Writer.new(@file_name)
@default = block_given? ? blk : default
read!
end
# 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 [#to_s] key the key of the storage slot in the database
# @param value the value to store
# @param [Boolean] sync if true, sync this value immediately
def []=(key, value, sync = false)
key = key.to_s
write key, value, sync
@table[key] = value
end
alias_method :set, :"[]="
# set! flushes data immediately to disk.
# @param [#to_s] key the key of the storage slot in the database
# @param value the value to store
def set!(key, value)
set key, value, true
end
# Delete a key from the database
# @param [#to_s] key the key of the storage slot in the database
# @param [Boolean] sync if true, sync this deletion immediately
def delete(key, sync = false)
key = key.to_s
write key, '', sync, true
@table.delete key
end
# delete! immediately deletes the key on disk.
# @param [#to_s] key the key of the storage slot in the database
def delete!(key)
delete key, true
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 [#to_s] key the value to retrieve from the database.
def [](key)
key = key.to_s
if @table.has_key? key
@table[key]
elsif default?
set key, Proc === @default ? @default.call(key) : @default
end
end
alias_method :get, :"[]"
# Iterate over the key, value pairs in the database.
# @yield [key, value] blk the iterator for each key value pair.
# @yieldparam [String] key the key.
# @yieldparam value the value from the database.
def each
keys.each { |k| yield(k, get(k)) }
end
# Does this db have a default value.
def default?
!@default.nil?
end
# Does this db have a value for this key?
# @param [key#to_s] key the key to check if the DB has a key.
def has_key?(key)
@table.has_key? key.to_s
end
# Return the keys in the db.
# @return [Array]
def keys
@table.keys
end
# Return the number of stored items.
# @return [Integer]
def length
@table.keys.length
end
alias_method :size, :length
# Serialize the data for writing to disk, if you don't want to use Marshal
# overwrite this method.
# @param value the value to be serialized
# @return [String]
def serialize(value)
Marshal.dump(value)
end
# Parse the serialized value from disk, like serialize if you want to use a
# different serialization method overwrite this method.
# @param value the value to be parsed
# @return [String]
def parse(value)
Marshal.load(value)
end
# Empty the database file.
def empty!
@writer.truncate!
@table.clear
read!
end
alias_method :clear, :empty!
# Force all queued commits to be written to disk.
def flush!
@writer.flush!
end
# Close the database for reading and writing.
def close!
@writer.close!
end
# Compact the database to remove stale commits and reduce the file size.
def compact!
# Create a new temporary database
tmp_file = @file_name + "-#{$$}-#{Thread.current.object_id}"
copy_db = self.class.new tmp_file
# Copy the database key by key into the temporary table
each do |key, value|
copy_db.set(key, get(key))
end
copy_db.close!
close!
# Move the copy into place
File.rename tmp_file, @file_name
# Reopen this database
@writer = Writer.new(@file_name)
@table.clear
read!
end
# Read all values from the log file. If you want to check for changed data
# call this again.
def read!
buf = nil
File.open(@file_name, 'rb') do |fd|
fd.flock(File::LOCK_SH)
buf = fd.read
end
until buf.empty?
key, data, deleted = Record.deserialize(buf)
if deleted
@table.delete key
else
@table[key] = parse(data)
end
end
end
private
def write(key, value, sync = false, delete = false)
@writer.write([key, serialize(value), delete])
flush! if sync
end
end
end