Skip to content

Instantly share code, notes, and snippets.

@threez
Created November 5, 2012 22:02
Show Gist options
  • Save threez/4020680 to your computer and use it in GitHub Desktop.
Save threez/4020680 to your computer and use it in GitHub Desktop.
This is a small journaling multi process aware database for counting choises
require "fileutils"
# === PSDB Packed Stats Database
#
# This is a small journaling multi process aware database for counting choises.
#
# === Authors
# * dpree
# * threez
class PSDB
# Location where the packed statistic data is stored.
PACK_FILE = ".pack".freeze
include FileUtils
class <<self
alias open new
end
# Create a new database directory and optionally
# pass a block to work with the database.
# @param [String] dir_path path to an dir (must not exist, beside the parents)
# @yield The block automatically takes care of closing the database.
# @yieldparam [PSDB] db the database instance.
def initialize(dir_path, &block)
@dir_path = dir_path
mkdir_p(@dir_path)
@journals = {}
if block_given?
begin
block.call(self)
ensure
close
end
end
end
# Returns the stats for a given key.
# @param [String] key the name of the key.
# @param [Hash<String, Hash<String, Fixnum>>] hash of all the stats per key.
# Each key points to another hash of choises and there counts.
# @example
# db.stats # => { "sales" => { "A" => 12 , "B" => 76 } }
def stats
pack
packed_stats
end
# Adds a new choise to the database.
# @param [String] key the name of the key
# @param [String] value a one byte char for the choise.
# @raise [ArgumentError] if the is not 1 byte
def push(key, value)
if value.to_s.size != 1
raise ArgumentError, "value #{value.inspect} must be 1 byte!"
end
unless @journals[key]
@journals[key] ||= File.open(path(key), "a+")
@journals[key].sync = true
end
@journals[key].write value
end
# Closes all open journals.
def close
@journals.values.each(&:close)
end
private
# Packs / compresses the journals into the pack file to allow for quick
# access to the statistic data.
# @see PACK_FILE
def pack
journals = Dir[path("*")]
if journals.any?
results = journals.inject(packed_stats) do |h, path|
key = File.basename(path)
h[key] ||= {}
journal_stats(key).each do |choise, value|
h[key][choise] ||= 0
h[key][choise] += value
end
rm path
h
end
File.open(path(PACK_FILE), "w") do |f|
f.write Marshal.dump(results)
end
end
end
# Calculates the stats based of the journal files. If the journal stats
# should be based on some other values an optional base can be passed.
# @param [String] key name of the key
# @param [Hash] base hash of options to start counting from
def journal_stats(key, base = {})
if File.exists? path(key)
results = File.read(path(key)).each_char.inject(base) do |h, k|
h[k] ||= 0
h[k] += 1
h
end
else
base
end
end
# Returns just the packed stats
# @note Doesn't contain the stats that are still in the journals.
# @param [Hash<String, Hash<String, Fixnum>>] hash of all the stats per key.
# Each key points to another hash of choises and there counts.
# @example simple hash (A and B are choises)
# db.packed_stats # => { "sales" => { "A" => 12 , "B" => 76 } }
def packed_stats
if File.exist?(path(PACK_FILE))
data = File.read(path(PACK_FILE))
return Marshal.load(data)
end
{}
end
# Generates paths for db files based on the passed name.
# @param [String] name the name of the file
# @return [String] the path to the file in der database
def path(name)
File.join(@dir_path, name)
end
end
if __FILE__ == $0
def work(after_action = nil)
PSDB.open("./db") do |db|
1000.times do
db.push 'sales', (rand > 0.5) ? 'A' : 'B'
db.push 'products', (rand > 0.5) ? 'C' : 'D'
end
end
end
if fork
work :show
Process.wait
PSDB.open("./db") do |db|
p db.stats['sales']
p db.stats['sales'].values.inject(0) { |sum, i| sum += i }
end
else
work
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment