daybreak 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +8 -0
- data/.yardopts +1 -0
- data/EPIGRAPH +6 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README +13 -0
- data/Rakefile +36 -0
- data/daybreak.gemspec +18 -0
- data/lib/daybreak.rb +17 -0
- data/lib/daybreak/db.rb +163 -0
- data/lib/daybreak/locking.rb +14 -0
- data/lib/daybreak/reader.rb +39 -0
- data/lib/daybreak/record.rb +64 -0
- data/lib/daybreak/version.rb +4 -0
- data/lib/daybreak/writer.rb +110 -0
- data/test/bench.rb +28 -0
- data/test/compare.rb +47 -0
- data/test/prof.rb +14 -0
- data/test/test.rb +81 -0
- data/test/test_helper.rb +7 -0
- metadata +104 -0
data/.gitignore
ADDED
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--no-private --protected lib/**/*.rb - README EPIGRAPH LICENSE
|
data/EPIGRAPH
ADDED
@@ -0,0 +1,6 @@
|
|
1
|
+
Above the escutcheon was a declension of the place-names: Falconer Jail 1871,
|
2
|
+
Falconer Reformatory, Falconer Federal Penitentiary, Falconer State Prison,
|
3
|
+
Falconer Correctional Facility, and the last, which had never caught on:
|
4
|
+
Daybreak House.
|
5
|
+
|
6
|
+
John Cheever, Falconer
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Jeff Larson
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
^^ |
|
2
|
+
daybreak ^^ \ _ /
|
3
|
+
-= / \ =-
|
4
|
+
~^~ ^ ^~^~ ~^~ ~ ~^~~^~^-=~=~=-~^~^~^~
|
5
|
+
|
6
|
+
Daybreak is a simple key value store for ruby. It has user defined persistence,
|
7
|
+
and all data is stored in a table in memory so ruby niceties are available.
|
8
|
+
Daybreak's performance is on par with other options like pstore and dbm.
|
9
|
+
|
10
|
+
$ gem install daybreak
|
11
|
+
|
12
|
+
Docs: http://propublica.github.com/daybreak/
|
13
|
+
Issue Tracker: http://propublica.github.com/daybreak/issues
|
data/Rakefile
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
|
4
|
+
task :default do
|
5
|
+
require "./test/test.rb"
|
6
|
+
end
|
7
|
+
|
8
|
+
desc "Run benchmarks"
|
9
|
+
task :bench do
|
10
|
+
require "./test/bench.rb"
|
11
|
+
end
|
12
|
+
|
13
|
+
desc "Run comparisons with other libraries"
|
14
|
+
task :compare do
|
15
|
+
require "./test/compare.rb"
|
16
|
+
end
|
17
|
+
|
18
|
+
desc "Profile a simple run"
|
19
|
+
task :prof do
|
20
|
+
require "./test/prof.rb"
|
21
|
+
end
|
22
|
+
|
23
|
+
require 'erb'
|
24
|
+
|
25
|
+
desc "Write out docs to index.html"
|
26
|
+
task :doc do |t|
|
27
|
+
File.open("index.html", 'w').write ERB.new(File.open("index.erb").read).result(binding)
|
28
|
+
end
|
29
|
+
|
30
|
+
desc "Publish the docs to gh-pages"
|
31
|
+
task :publish do |t|
|
32
|
+
`git checkout gh-pages`
|
33
|
+
`git merge master`
|
34
|
+
`git push`
|
35
|
+
`git checkout master`
|
36
|
+
end
|
data/daybreak.gemspec
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/daybreak/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Jeff Larson"]
|
6
|
+
gem.email = ["thejefflarson@gmail.com"]
|
7
|
+
gem.description = %q{A simple dimple key-value store for ruby.}
|
8
|
+
gem.summary = %q{Daybreak provides an in memory key-value store that is easily enumerable in ruby.}
|
9
|
+
gem.homepage = "http://propublica.github.com/daybreak/"
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\).reject {|f| f =~ /^(index)/}
|
12
|
+
gem.test_files = gem.files.grep(%r{^(test)/})
|
13
|
+
gem.name = "daybreak"
|
14
|
+
gem.require_paths = ["lib"]
|
15
|
+
gem.version = Daybreak::VERSION
|
16
|
+
gem.add_development_dependency 'simplecov'
|
17
|
+
gem.add_development_dependency 'ruby-prof'
|
18
|
+
end
|
data/lib/daybreak.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# Daybreak, a simple dimple key value store for ruby.
|
2
|
+
module Daybreak
|
3
|
+
# The root path for Daybreak
|
4
|
+
ROOT = File.expand_path(File.dirname(__FILE__))
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'tempfile'
|
8
|
+
require 'thread'
|
9
|
+
require 'zlib'
|
10
|
+
require 'fcntl'
|
11
|
+
|
12
|
+
require "#{Daybreak::ROOT}/daybreak/version"
|
13
|
+
require "#{Daybreak::ROOT}/daybreak/locking"
|
14
|
+
require "#{Daybreak::ROOT}/daybreak/record"
|
15
|
+
require "#{Daybreak::ROOT}/daybreak/writer"
|
16
|
+
require "#{Daybreak::ROOT}/daybreak/reader"
|
17
|
+
require "#{Daybreak::ROOT}/daybreak/db"
|
data/lib/daybreak/db.rb
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
module Daybreak
|
2
|
+
# Daybreak::DB contains the public api for Daybreak, you may extend it like
|
3
|
+
# any other Ruby class (i.e. to overwrite serialize and parse). It includes
|
4
|
+
# Enumerable for functional goodies like map, each, reduce and friends.
|
5
|
+
class DB
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
# Create a new Daybreak::DB. The second argument is the default value
|
9
|
+
# to store when accessing a previously unset key, this follows the
|
10
|
+
# Hash standard.
|
11
|
+
# @param [String] file the path to the db file
|
12
|
+
# @param default the default value to store and return when a key is
|
13
|
+
# not yet in the database.
|
14
|
+
# @yield [key] blk a block that will return the default value to store.
|
15
|
+
def initialize(file, default=nil, &blk)
|
16
|
+
@file_name = file
|
17
|
+
reset!
|
18
|
+
@default = default
|
19
|
+
@default = blk if block_given?
|
20
|
+
read!
|
21
|
+
end
|
22
|
+
|
23
|
+
# Set a key in the database to be written at some future date. If the data
|
24
|
+
# needs to be persisted immediately, call <tt>db.set(key, value, true)</tt>.
|
25
|
+
# @param [#to_s] key the key of the storage slot in the database
|
26
|
+
# @param value the value to store
|
27
|
+
# @param [Boolean] sync if true, sync this value immediately
|
28
|
+
def []=(key, value, sync = false)
|
29
|
+
key = key.to_s
|
30
|
+
@writer.write(Record.new(key, serialize(value)))
|
31
|
+
flush! if sync
|
32
|
+
@table[key] = value
|
33
|
+
end
|
34
|
+
alias_method :set, :"[]="
|
35
|
+
|
36
|
+
# set! flushes data immediately to disk.
|
37
|
+
# @param [#to_s] key the key of the storage slot in the database
|
38
|
+
# @param value the value to store
|
39
|
+
def set!(key, value)
|
40
|
+
set key, value, true
|
41
|
+
end
|
42
|
+
|
43
|
+
# Retrieve a value at key from the database. If the default value was specified
|
44
|
+
# when this database was created, that value will be set and returned. Aliased
|
45
|
+
# as <tt>get</tt>.
|
46
|
+
# @param [#to_s] key the value to retrieve from the database.
|
47
|
+
def [](key)
|
48
|
+
key = key.to_s
|
49
|
+
if @table.has_key? key
|
50
|
+
@table[key]
|
51
|
+
elsif default?
|
52
|
+
if @default.is_a? Proc
|
53
|
+
value = @default.call(key)
|
54
|
+
else
|
55
|
+
value = @default
|
56
|
+
end
|
57
|
+
set key, value
|
58
|
+
end
|
59
|
+
end
|
60
|
+
alias_method :get, :"[]"
|
61
|
+
|
62
|
+
# Iterate over the key, value pairs in the database.
|
63
|
+
def each(&blk)
|
64
|
+
keys.each { |k| blk.call(k, get(k)) }
|
65
|
+
end
|
66
|
+
|
67
|
+
# Does this db have a default value.
|
68
|
+
def default?
|
69
|
+
!@default.nil?
|
70
|
+
end
|
71
|
+
|
72
|
+
# Does this db have a value for this key?
|
73
|
+
def has_key?(key)
|
74
|
+
@table.has_key? key.to_s
|
75
|
+
end
|
76
|
+
|
77
|
+
# Return the keys in the db.
|
78
|
+
# @return [Array<String>]
|
79
|
+
def keys
|
80
|
+
@table.keys
|
81
|
+
end
|
82
|
+
|
83
|
+
# Return the number of stored items.
|
84
|
+
# @return [Integer]
|
85
|
+
def length
|
86
|
+
@table.keys.length
|
87
|
+
end
|
88
|
+
|
89
|
+
# Serialize the data for writing to disk, if you don't want to use <tt>Marshal</tt>
|
90
|
+
# overwrite this method.
|
91
|
+
# @param value the value to be serialized
|
92
|
+
# @return [String]
|
93
|
+
def serialize(value)
|
94
|
+
Marshal.dump(value)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Parse the serialized value from disk, like serialize if you want to use a
|
98
|
+
# different serialization method overwrite this method.
|
99
|
+
# @param value the value to be parsed
|
100
|
+
# @return [String]
|
101
|
+
def parse(value)
|
102
|
+
Marshal.load(value)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Reset and empty the database file.
|
106
|
+
def empty!
|
107
|
+
reset!
|
108
|
+
@writer.truncate!
|
109
|
+
end
|
110
|
+
|
111
|
+
# Force all queued commits to be written to disk.
|
112
|
+
def flush!
|
113
|
+
@writer.flush!
|
114
|
+
end
|
115
|
+
|
116
|
+
# Reset the state of the database, you should call <tt>read!</tt> after calling this.
|
117
|
+
def reset!
|
118
|
+
@table = {}
|
119
|
+
@writer = Daybreak::Writer.new(@file_name)
|
120
|
+
@reader = Daybreak::Reader.new(@file_name)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Close the database for reading and writing.
|
124
|
+
def close!
|
125
|
+
@writer.close!
|
126
|
+
@reader.close!
|
127
|
+
end
|
128
|
+
|
129
|
+
# Compact the database to remove stale commits and reduce the file size.
|
130
|
+
def compact!
|
131
|
+
# Create a new temporary file
|
132
|
+
tmp_file = Tempfile.new File.basename(@file_name)
|
133
|
+
copy_db = DB.new tmp_file.path
|
134
|
+
|
135
|
+
# Copy the database key by key into the temporary table
|
136
|
+
each do |key, i|
|
137
|
+
copy_db.set(key, get(key))
|
138
|
+
end
|
139
|
+
copy_db.close!
|
140
|
+
|
141
|
+
# Empty this database
|
142
|
+
empty!
|
143
|
+
|
144
|
+
# Move the copy into place
|
145
|
+
tmp_file.close
|
146
|
+
FileUtils.mv tmp_file.path, @file_name
|
147
|
+
tmp_file.unlink
|
148
|
+
|
149
|
+
# Reset this database
|
150
|
+
close!
|
151
|
+
reset!
|
152
|
+
read!
|
153
|
+
end
|
154
|
+
|
155
|
+
# Read all values from the log file. If you want to check for changed data
|
156
|
+
# call this again.
|
157
|
+
def read!
|
158
|
+
@reader.read do |record|
|
159
|
+
@table[record.key] = parse record.data
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Daybreak
|
2
|
+
# Class for building out the table, you shouldn't need to access this
|
3
|
+
# class directly. Readers are responsible for reading each record in
|
4
|
+
# the file and yeilding the parsed records.
|
5
|
+
class Reader
|
6
|
+
# @param [String] file the file to read from
|
7
|
+
def initialize(file)
|
8
|
+
@file_name = file
|
9
|
+
end
|
10
|
+
|
11
|
+
# Close's the Reader's file descriptor
|
12
|
+
def close!
|
13
|
+
@fd.close unless @fd.nil?
|
14
|
+
end
|
15
|
+
|
16
|
+
# Right now this is really expensive, every call to read will
|
17
|
+
# close and reread the whole db file, but since cross process
|
18
|
+
# consistency is handled by the user, this should be fair warning.
|
19
|
+
def read(&blk)
|
20
|
+
reopen!
|
21
|
+
while !@fd.eof?
|
22
|
+
blk.call(Record.read(@fd))
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def reopen!
|
29
|
+
close!
|
30
|
+
open!
|
31
|
+
end
|
32
|
+
|
33
|
+
def open!
|
34
|
+
@fd = File.open @file_name, 'r'
|
35
|
+
@fd.binmode
|
36
|
+
@fd.advise(:sequential) if @fd.respond_to? :advise
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Daybreak
|
2
|
+
# Records define how data is serialized and read from disk.
|
3
|
+
class Record
|
4
|
+
# Thrown when either key or data is missing
|
5
|
+
class UnnacceptableDataError < Exception; end
|
6
|
+
|
7
|
+
# Thrown when there is a CRC mismatch between the data from the disk
|
8
|
+
# and what was written to disk previously.
|
9
|
+
class CorruptDataError < Exception; end
|
10
|
+
include Locking
|
11
|
+
|
12
|
+
attr_accessor :key, :data
|
13
|
+
|
14
|
+
def initialize(key = nil, data = nil)
|
15
|
+
@key = key
|
16
|
+
@data = data
|
17
|
+
end
|
18
|
+
|
19
|
+
# Read a record from an open io source, check the CRC, and set @key and @data
|
20
|
+
# @param [#read] io an IO instance to read from
|
21
|
+
def read(io)
|
22
|
+
lock io do
|
23
|
+
@key = read_bytes(io)
|
24
|
+
@data = read_bytes(io)
|
25
|
+
crc = io.read(4)
|
26
|
+
raise CorruptDataError, "CRC mismatch #{crc} should be #{crc_string}" unless crc == crc_string
|
27
|
+
end
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
# The serialized representation of the key value pair plus the CRC
|
32
|
+
# @return [String]
|
33
|
+
def representation
|
34
|
+
raise UnnacceptableDataError, "key and data must be defined" if @key.nil? || @data.nil?
|
35
|
+
byte_string + crc_string
|
36
|
+
end
|
37
|
+
|
38
|
+
# Create a new record to read from IO.
|
39
|
+
# @param [#read] io an IO instance to read from
|
40
|
+
def self.read(io)
|
41
|
+
new.read(io)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def byte_string
|
47
|
+
@byte_string ||= part(@key) + part(@data)
|
48
|
+
end
|
49
|
+
|
50
|
+
def crc_string
|
51
|
+
[Zlib.crc32(byte_string, 0)].pack('N')
|
52
|
+
end
|
53
|
+
|
54
|
+
def read_bytes(io)
|
55
|
+
raw = io.read(4)
|
56
|
+
length = raw.unpack('N')[0]
|
57
|
+
io.read(length)
|
58
|
+
end
|
59
|
+
|
60
|
+
def part(data)
|
61
|
+
[data.bytesize].pack('N') + data
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module Daybreak
|
2
|
+
# Writer's handle the actually fiddly task of committing data to disk.
|
3
|
+
# They have a Worker instance that writes in a select loop.
|
4
|
+
class Writer
|
5
|
+
# Open up the file, ready it for binary and nonblocking writing.
|
6
|
+
def initialize(file)
|
7
|
+
@fd = File.open file, 'a'
|
8
|
+
@fd.binmode
|
9
|
+
|
10
|
+
f = @fd.fcntl(Fcntl::F_GETFL, 0)
|
11
|
+
@fd.fcntl(Fcntl::F_SETFL, Fcntl::O_NONBLOCK | f)
|
12
|
+
|
13
|
+
@worker = Worker.new(@fd)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Send a record to the workers queue.
|
17
|
+
def write(record)
|
18
|
+
@worker.enqueue record
|
19
|
+
end
|
20
|
+
|
21
|
+
# Finish writing
|
22
|
+
def finish!
|
23
|
+
@worker.finish!
|
24
|
+
end
|
25
|
+
|
26
|
+
# Flush pending commits, and restart the worker.
|
27
|
+
def flush!
|
28
|
+
@worker.flush!
|
29
|
+
end
|
30
|
+
|
31
|
+
# Finish writing and close the file descriptor
|
32
|
+
def close!
|
33
|
+
finish!
|
34
|
+
@fd.close
|
35
|
+
end
|
36
|
+
|
37
|
+
# Truncate the file.
|
38
|
+
def truncate!
|
39
|
+
@fd.truncate(0)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
# Workers handle the actual fiddly bits of asynchronous io and
|
45
|
+
# and handle background writes.
|
46
|
+
class Worker
|
47
|
+
include Locking
|
48
|
+
|
49
|
+
def initialize(fd)
|
50
|
+
@queue = Queue.new
|
51
|
+
@fd = fd
|
52
|
+
@buffer = ""
|
53
|
+
@thread = Thread.new { work }
|
54
|
+
at_exit { finish! }
|
55
|
+
end
|
56
|
+
|
57
|
+
# Queue up a write to be committed later.
|
58
|
+
def enqueue(record)
|
59
|
+
@queue << record.representation
|
60
|
+
end
|
61
|
+
|
62
|
+
# Loop and block if we don't have work to do or if
|
63
|
+
# the file isn't ready for another write just yet.
|
64
|
+
def work
|
65
|
+
buf = ""
|
66
|
+
loop do
|
67
|
+
str = @queue.pop
|
68
|
+
if str.nil?
|
69
|
+
@fd.flush
|
70
|
+
break
|
71
|
+
end
|
72
|
+
buf << str
|
73
|
+
read, write = IO.select [], [@fd]
|
74
|
+
if write and fd = write.first
|
75
|
+
lock(fd, File::LOCK_EX) { buf = try_write fd, buf }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Try and write the buffer to the file via non blocking file writes.
|
81
|
+
# If the write fails try again.
|
82
|
+
def try_write(fd, buf)
|
83
|
+
begin
|
84
|
+
s = fd.write_nonblock(buf)
|
85
|
+
if s < buf.length
|
86
|
+
buf = buf[s..-1] # didn't finish
|
87
|
+
else
|
88
|
+
buf = ""
|
89
|
+
end
|
90
|
+
rescue Errno::EAGAIN
|
91
|
+
buf = buf # try this again
|
92
|
+
end
|
93
|
+
buf
|
94
|
+
end
|
95
|
+
|
96
|
+
# finish! and start up another worker thread.
|
97
|
+
def flush!
|
98
|
+
finish!
|
99
|
+
@thread = Thread.new { work }
|
100
|
+
true
|
101
|
+
end
|
102
|
+
|
103
|
+
# Push a nil through the queue and block until the write loop is finished.
|
104
|
+
def finish!
|
105
|
+
@queue.push nil
|
106
|
+
@thread.join
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
data/test/bench.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__)) + '/test_helper.rb'
|
2
|
+
|
3
|
+
describe "benchmarks" do
|
4
|
+
before do
|
5
|
+
@db = Daybreak::DB.new DB_PATH
|
6
|
+
1000.times {|i| @db[i] = i }
|
7
|
+
@db.flush!
|
8
|
+
end
|
9
|
+
|
10
|
+
bench_performance_constant "keys with sync" do |n|
|
11
|
+
n.times {|i| @db.set(i, 'i' * i, true) }
|
12
|
+
end
|
13
|
+
|
14
|
+
bench_performance_constant "inserting keys" do |n|
|
15
|
+
n.times {|i| @db[i] = 'i' * i }
|
16
|
+
end
|
17
|
+
|
18
|
+
bench_performance_constant "reading keys" do |n|
|
19
|
+
n.times {|i| assert_equal i % 1000, @db[i % 1000] }
|
20
|
+
end
|
21
|
+
|
22
|
+
after do
|
23
|
+
@db.empty!
|
24
|
+
@db.close!
|
25
|
+
File.unlink(DB_PATH)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
data/test/compare.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__)) + '/test_helper.rb'
|
2
|
+
require 'pstore'
|
3
|
+
|
4
|
+
describe "compare with pstore" do
|
5
|
+
before do
|
6
|
+
@pstore = PStore.new(File.join(HERE, "test.pstore"))
|
7
|
+
end
|
8
|
+
|
9
|
+
bench_performance_constant "pstore bulk performance" do |n|
|
10
|
+
@pstore.transaction do
|
11
|
+
n.times do |i|
|
12
|
+
@pstore[i] = 'i' * i
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
after do
|
18
|
+
File.unlink File.join(HERE, "test.pstore")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
require 'dbm'
|
23
|
+
|
24
|
+
describe "compare with dbm" do
|
25
|
+
before do
|
26
|
+
@dbm = DBM.open(File.join(HERE, "test-dbm"), 666, DBM::WRCREAT)
|
27
|
+
1000.times {|i| @dbm[i.to_s] = i }
|
28
|
+
end
|
29
|
+
|
30
|
+
bench_performance_constant "DBM write performance" do |n|
|
31
|
+
n.times do |i|
|
32
|
+
@dbm[i.to_s] = 'i' * i
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
bench_performance_constant "DBM read performance" do |n|
|
37
|
+
n.times do |i|
|
38
|
+
assert_equal (i % 1000).to_s, @dbm[(i % 1000).to_s]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
after do
|
43
|
+
@dbm.close
|
44
|
+
|
45
|
+
File.unlink File.join(HERE, "test-dbm.db")
|
46
|
+
end
|
47
|
+
end
|
data/test/prof.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__)) + '/test_helper.rb'
|
2
|
+
|
3
|
+
require 'ruby-prof'
|
4
|
+
|
5
|
+
result = RubyProf.profile do
|
6
|
+
db = Daybreak::DB.new './t.db'
|
7
|
+
1000.times {|n| db[n] = n}
|
8
|
+
db.flush!
|
9
|
+
end
|
10
|
+
|
11
|
+
File.unlink './t.db'
|
12
|
+
printer = RubyProf::MultiPrinter.new(result)
|
13
|
+
FileUtils.mkdir('./profile') unless File.exists? './profile'
|
14
|
+
printer.print :path => './profile', :profile => 'profile'
|
data/test/test.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'simplecov'
|
3
|
+
require 'set'
|
4
|
+
SimpleCov.start
|
5
|
+
SimpleCov.command_name "Unit tests"
|
6
|
+
|
7
|
+
require File.expand_path(File.dirname(__FILE__)) + '/test_helper.rb'
|
8
|
+
|
9
|
+
describe "database functions" do
|
10
|
+
before do
|
11
|
+
@db = Daybreak::DB.new DB_PATH
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should insert" do
|
15
|
+
@db[1] = 1
|
16
|
+
assert_equal @db[1], 1
|
17
|
+
assert @db.has_key?(1)
|
18
|
+
@db[1] = '2'
|
19
|
+
assert_equal @db[1], '2'
|
20
|
+
assert_equal @db.length, 1
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should persist values" do
|
24
|
+
@db.set('1', '4', true)
|
25
|
+
@db.set('4', '1', true)
|
26
|
+
|
27
|
+
assert_equal @db['1'], '4'
|
28
|
+
db2 = Daybreak::DB.new DB_PATH
|
29
|
+
assert_equal db2['1'], '4'
|
30
|
+
assert_equal db2['4'], '1'
|
31
|
+
db2.close!
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should compact cleanly" do
|
35
|
+
@db[1] = 1
|
36
|
+
@db[1] = 1
|
37
|
+
@db.flush!
|
38
|
+
size = File.stat(DB_PATH).size
|
39
|
+
@db.compact!
|
40
|
+
assert_equal @db[1], 1
|
41
|
+
assert size > File.stat(DB_PATH).size
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should allow for default values" do
|
45
|
+
default_db = Daybreak::DB.new(DB_PATH, 0)
|
46
|
+
assert_equal default_db[1], 0
|
47
|
+
default_db[1] = 1
|
48
|
+
assert_equal default_db[1], 1
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should handle default values that are procs" do
|
52
|
+
db = Daybreak::DB.new(DB_PATH) {|key| Set.new }
|
53
|
+
assert db['foo'].is_a? Set
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should be able to sync competing writes" do
|
57
|
+
@db.set! '1', 4
|
58
|
+
db2 = Daybreak::DB.new DB_PATH
|
59
|
+
db2.set! '1', 5
|
60
|
+
@db.read!
|
61
|
+
assert_equal @db['1'], 5
|
62
|
+
@db.close!
|
63
|
+
end
|
64
|
+
|
65
|
+
it " should be able to handle another process's call to compact" do
|
66
|
+
20.times {|i| @db.set i, i, true }
|
67
|
+
db2 = Daybreak::DB.new DB_PATH
|
68
|
+
20.times {|i| @db.set i, i + 1, true }
|
69
|
+
@db.compact!
|
70
|
+
db2.read!
|
71
|
+
assert_equal 20, db2['19']
|
72
|
+
end
|
73
|
+
|
74
|
+
after do
|
75
|
+
@db.empty!
|
76
|
+
@db.close!
|
77
|
+
File.unlink(DB_PATH)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: daybreak
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jeff Larson
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-11-19 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: simplecov
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: ruby-prof
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
description: A simple dimple key-value store for ruby.
|
47
|
+
email:
|
48
|
+
- thejefflarson@gmail.com
|
49
|
+
executables: []
|
50
|
+
extensions: []
|
51
|
+
extra_rdoc_files: []
|
52
|
+
files:
|
53
|
+
- .gitignore
|
54
|
+
- .yardopts
|
55
|
+
- EPIGRAPH
|
56
|
+
- Gemfile
|
57
|
+
- LICENSE
|
58
|
+
- README
|
59
|
+
- Rakefile
|
60
|
+
- daybreak.gemspec
|
61
|
+
- lib/daybreak.rb
|
62
|
+
- lib/daybreak/db.rb
|
63
|
+
- lib/daybreak/locking.rb
|
64
|
+
- lib/daybreak/reader.rb
|
65
|
+
- lib/daybreak/record.rb
|
66
|
+
- lib/daybreak/version.rb
|
67
|
+
- lib/daybreak/writer.rb
|
68
|
+
- test/bench.rb
|
69
|
+
- test/compare.rb
|
70
|
+
- test/prof.rb
|
71
|
+
- test/test.rb
|
72
|
+
- test/test_helper.rb
|
73
|
+
homepage: http://propublica.github.com/daybreak/
|
74
|
+
licenses: []
|
75
|
+
post_install_message:
|
76
|
+
rdoc_options: []
|
77
|
+
require_paths:
|
78
|
+
- lib
|
79
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
80
|
+
none: false
|
81
|
+
requirements:
|
82
|
+
- - ! '>='
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
85
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
86
|
+
none: false
|
87
|
+
requirements:
|
88
|
+
- - ! '>='
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
requirements: []
|
92
|
+
rubyforge_project:
|
93
|
+
rubygems_version: 1.8.24
|
94
|
+
signing_key:
|
95
|
+
specification_version: 3
|
96
|
+
summary: Daybreak provides an in memory key-value store that is easily enumerable
|
97
|
+
in ruby.
|
98
|
+
test_files:
|
99
|
+
- test/bench.rb
|
100
|
+
- test/compare.rb
|
101
|
+
- test/prof.rb
|
102
|
+
- test/test.rb
|
103
|
+
- test/test_helper.rb
|
104
|
+
has_rdoc:
|