Created
November 5, 2012 22:02
-
-
Save threez/4020680 to your computer and use it in GitHub Desktop.
This is a small journaling multi process aware database for counting choises
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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