daybreak 0.1.1 → 0.1.2
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/.travis.yml +9 -0
- data/README +2 -2
- data/daybreak.gemspec +1 -2
- data/lib/daybreak.rb +7 -13
- data/lib/daybreak/db.rb +23 -38
- data/lib/daybreak/reader.rb +7 -18
- data/lib/daybreak/record.rb +30 -50
- data/lib/daybreak/version.rb +1 -1
- data/lib/daybreak/writer.rb +6 -8
- data/test/test.rb +8 -4
- data/test/test_helper.rb +2 -1
- metadata +65 -67
data/.travis.yml
ADDED
data/README
CHANGED
@@ -5,10 +5,10 @@
|
|
5
5
|
|
6
6
|
Daybreak is a simple key value store for ruby. It has user defined persistence,
|
7
7
|
and all data is stored in a table in memory so ruby niceties are available.
|
8
|
-
Daybreak is faster than other
|
8
|
+
Daybreak is faster than other ruby options like pstore and dbm.
|
9
9
|
|
10
10
|
$ gem install daybreak
|
11
11
|
|
12
12
|
Docs: http://propublica.github.com/daybreak/
|
13
13
|
Issue Tracker: http://propublica.github.com/daybreak/issues
|
14
|
-
Benchmarks: https://gist.github.com/4146590
|
14
|
+
Benchmarks: https://gist.github.com/4146590
|
data/daybreak.gemspec
CHANGED
@@ -14,7 +14,6 @@ Gem::Specification.new do |gem|
|
|
14
14
|
gem.require_paths = ["lib"]
|
15
15
|
gem.licenses = ["MIT"]
|
16
16
|
gem.version = Daybreak::VERSION
|
17
|
+
gem.add_development_dependency 'rake'
|
17
18
|
gem.add_development_dependency 'minitest'
|
18
|
-
gem.add_development_dependency 'simplecov'
|
19
|
-
gem.add_development_dependency 'ruby-prof'
|
20
19
|
end
|
data/lib/daybreak.rb
CHANGED
@@ -1,17 +1,11 @@
|
|
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
1
|
require 'tempfile'
|
8
2
|
require 'thread'
|
9
|
-
require 'zlib'
|
10
3
|
require 'fcntl'
|
4
|
+
require 'zlib'
|
11
5
|
|
12
|
-
require
|
13
|
-
require
|
14
|
-
require
|
15
|
-
require
|
16
|
-
require
|
17
|
-
require
|
6
|
+
require 'daybreak/version'
|
7
|
+
require 'daybreak/locking'
|
8
|
+
require 'daybreak/record'
|
9
|
+
require 'daybreak/writer'
|
10
|
+
require 'daybreak/reader'
|
11
|
+
require 'daybreak/db'
|
data/lib/daybreak/db.rb
CHANGED
@@ -14,10 +14,11 @@ module Daybreak
|
|
14
14
|
# @yield [key] a block that will return the default value to store.
|
15
15
|
# @yieldparam [String] key the key to be stored.
|
16
16
|
def initialize(file, default=nil, &blk)
|
17
|
+
@table = {}
|
17
18
|
@file_name = file
|
18
|
-
|
19
|
-
@
|
20
|
-
@default = blk
|
19
|
+
@reader = Reader.new(@file_name)
|
20
|
+
@writer = Writer.new(@file_name)
|
21
|
+
@default = block_given? ? blk : default
|
21
22
|
read!
|
22
23
|
end
|
23
24
|
|
@@ -64,12 +65,7 @@ module Daybreak
|
|
64
65
|
if @table.has_key? key
|
65
66
|
@table[key]
|
66
67
|
elsif default?
|
67
|
-
|
68
|
-
value = @default.call(key)
|
69
|
-
else
|
70
|
-
value = @default
|
71
|
-
end
|
72
|
-
set key, value
|
68
|
+
set key, Proc === @default ? @default.call(key) : @default
|
73
69
|
end
|
74
70
|
end
|
75
71
|
alias_method :get, :"[]"
|
@@ -78,8 +74,8 @@ module Daybreak
|
|
78
74
|
# @yield [key, value] blk the iterator for each key value pair.
|
79
75
|
# @yieldparam [String] key the key.
|
80
76
|
# @yieldparam value the value from the database.
|
81
|
-
def each
|
82
|
-
keys.each { |k|
|
77
|
+
def each
|
78
|
+
keys.each { |k| yield(k, get(k)) }
|
83
79
|
end
|
84
80
|
|
85
81
|
# Does this db have a default value.
|
@@ -104,6 +100,7 @@ module Daybreak
|
|
104
100
|
def length
|
105
101
|
@table.keys.length
|
106
102
|
end
|
103
|
+
alias_method :size, :length
|
107
104
|
|
108
105
|
# Serialize the data for writing to disk, if you don't want to use <tt>Marshal</tt>
|
109
106
|
# overwrite this method.
|
@@ -121,11 +118,10 @@ module Daybreak
|
|
121
118
|
Marshal.load(value)
|
122
119
|
end
|
123
120
|
|
124
|
-
#
|
121
|
+
# Empty the database file.
|
125
122
|
def empty!
|
126
123
|
@writer.truncate!
|
127
|
-
|
128
|
-
reset!
|
124
|
+
@table.clear
|
129
125
|
read!
|
130
126
|
end
|
131
127
|
alias_method :clear, :empty!
|
@@ -135,53 +131,42 @@ module Daybreak
|
|
135
131
|
@writer.flush!
|
136
132
|
end
|
137
133
|
|
138
|
-
# Reset the state of the database, you should call <tt>read!</tt> after calling this.
|
139
|
-
def reset!
|
140
|
-
@table = {}
|
141
|
-
@writer = Daybreak::Writer.new(@file_name)
|
142
|
-
@reader = Daybreak::Reader.new(@file_name)
|
143
|
-
end
|
144
|
-
|
145
134
|
# Close the database for reading and writing.
|
146
135
|
def close!
|
147
136
|
@writer.close!
|
148
|
-
@reader.close!
|
149
137
|
end
|
150
138
|
|
151
139
|
# Compact the database to remove stale commits and reduce the file size.
|
152
140
|
def compact!
|
153
|
-
# Create a new temporary
|
154
|
-
tmp_file =
|
155
|
-
copy_db = self.class.new tmp_file
|
141
|
+
# Create a new temporary database
|
142
|
+
tmp_file = @file_name + "-#{$$}-#{Thread.current.object_id}"
|
143
|
+
copy_db = self.class.new tmp_file
|
156
144
|
|
157
145
|
# Copy the database key by key into the temporary table
|
158
|
-
each do |key|
|
146
|
+
each do |key, value|
|
159
147
|
copy_db.set(key, get(key))
|
160
148
|
end
|
161
149
|
copy_db.close!
|
162
150
|
|
163
|
-
# Empty this database
|
164
|
-
empty!
|
165
151
|
close!
|
166
152
|
|
167
153
|
# Move the copy into place
|
168
|
-
tmp_file
|
169
|
-
FileUtils.mv tmp_file.path, @file_name
|
170
|
-
tmp_file.unlink
|
154
|
+
File.rename tmp_file, @file_name
|
171
155
|
|
172
|
-
#
|
173
|
-
|
156
|
+
# Reopen this database
|
157
|
+
@writer = Writer.new(@file_name)
|
158
|
+
@table.clear
|
174
159
|
read!
|
175
160
|
end
|
176
161
|
|
177
162
|
# Read all values from the log file. If you want to check for changed data
|
178
163
|
# call this again.
|
179
164
|
def read!
|
180
|
-
@reader.read do |
|
181
|
-
if
|
182
|
-
@table.delete
|
165
|
+
@reader.read do |(key, data, deleted)|
|
166
|
+
if deleted
|
167
|
+
@table.delete key
|
183
168
|
else
|
184
|
-
@table[
|
169
|
+
@table[key] = parse(data)
|
185
170
|
end
|
186
171
|
end
|
187
172
|
end
|
@@ -189,7 +174,7 @@ module Daybreak
|
|
189
174
|
private
|
190
175
|
|
191
176
|
def write(key, value, sync = false, delete = false)
|
192
|
-
@writer.write(
|
177
|
+
@writer.write([key, serialize(value), delete])
|
193
178
|
flush! if sync
|
194
179
|
end
|
195
180
|
end
|
data/lib/daybreak/reader.rb
CHANGED
@@ -8,30 +8,19 @@ module Daybreak
|
|
8
8
|
@file_name = file
|
9
9
|
end
|
10
10
|
|
11
|
-
# Close the Reader's file descriptor.
|
12
|
-
def close!
|
13
|
-
@fd.close unless @fd.nil? || !@fd.closed?
|
14
|
-
end
|
15
|
-
|
16
11
|
# Read all values from the aof file.
|
17
12
|
#
|
18
13
|
# Right now this is really expensive, every call to read will
|
19
14
|
# close and reread the whole db file, but since cross process
|
20
15
|
# consistency is handled by the user, this should be fair warning.
|
21
|
-
def read
|
22
|
-
open
|
23
|
-
|
24
|
-
|
16
|
+
def read
|
17
|
+
File.open(@file_name, 'r') do |fd|
|
18
|
+
fd.binmode
|
19
|
+
fd.advise(:sequential) if fd.respond_to? :advise
|
20
|
+
while !fd.eof?
|
21
|
+
yield Record.read(fd)
|
22
|
+
end
|
25
23
|
end
|
26
|
-
close!
|
27
|
-
end
|
28
|
-
|
29
|
-
private
|
30
|
-
|
31
|
-
def open!
|
32
|
-
@fd = File.open @file_name, 'r'
|
33
|
-
@fd.binmode
|
34
|
-
@fd.advise(:sequential) if @fd.respond_to? :advise
|
35
24
|
end
|
36
25
|
end
|
37
26
|
end
|
data/lib/daybreak/record.rb
CHANGED
@@ -1,87 +1,67 @@
|
|
1
1
|
module Daybreak
|
2
2
|
# Records define how data is serialized and read from disk.
|
3
|
-
|
3
|
+
module Record
|
4
4
|
# Thrown when either key or data is missing
|
5
5
|
class UnnacceptableDataError < Exception; end
|
6
6
|
|
7
7
|
# Thrown when there is a CRC mismatch between the data from the disk
|
8
8
|
# and what was written to disk previously.
|
9
9
|
class CorruptDataError < Exception; end
|
10
|
-
include Locking
|
11
10
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
attr_accessor :key, :data
|
11
|
+
extend self
|
12
|
+
extend Locking
|
16
13
|
|
17
|
-
|
18
|
-
|
19
|
-
@data = data
|
20
|
-
if deleted
|
21
|
-
@deleted = DELETION_MASK
|
22
|
-
else
|
23
|
-
@deleted = 0
|
24
|
-
end
|
25
|
-
end
|
14
|
+
# The mask a record uses to check for deletion.
|
15
|
+
DELETION_MASK = 1 << 31
|
26
16
|
|
27
17
|
# Read a record from an open io source, check the CRC, and set <tt>@key</tt>
|
28
18
|
# and <tt>@data</tt>.
|
29
19
|
# @param [#read] io an IO instance to read from
|
30
|
-
def read(io)
|
31
|
-
lock io do
|
32
|
-
@key = read_key(io)
|
33
|
-
@data = read_data(io)
|
34
|
-
crc = io.read(4)
|
35
|
-
raise CorruptDataError, "CRC mismatch #{crc} should be #{crc_string}" unless crc == crc_string
|
36
|
-
end
|
37
|
-
self
|
38
|
-
end
|
39
20
|
|
40
21
|
# The serialized representation of the key value pair plus the CRC.
|
41
22
|
# @return [String]
|
42
|
-
def
|
43
|
-
raise UnnacceptableDataError,
|
44
|
-
|
23
|
+
def serialize(record)
|
24
|
+
raise UnnacceptableDataError, 'key and data must be defined' unless record[0] && record[1]
|
25
|
+
s = key_data_string(record)
|
26
|
+
s << crc_string(s)
|
45
27
|
end
|
46
28
|
|
47
29
|
# Create a new record to read from IO.
|
48
30
|
# @param [#read] io an IO instance to read from
|
49
|
-
def
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
31
|
+
def read(io)
|
32
|
+
lock io do
|
33
|
+
record = []
|
34
|
+
masked = read32(io)
|
35
|
+
# Read the record's key bytes
|
36
|
+
record << io.read(masked & (DELETION_MASK - 1)) <<
|
37
|
+
# Read the record's value bytes
|
38
|
+
io.read(read32(io)) <<
|
39
|
+
# Set the deletion flag
|
40
|
+
((masked & DELETION_MASK) != 0)
|
41
|
+
crc = io.read(4)
|
42
|
+
raise CorruptDataError, 'CRC mismatch' unless crc == crc_string(key_data_string(record))
|
43
|
+
record
|
44
|
+
end
|
55
45
|
end
|
56
46
|
|
57
47
|
private
|
58
48
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
def crc_string
|
64
|
-
[Zlib.crc32(byte_string, 0)].pack('N')
|
49
|
+
# Return the deletion flag plus two length prefixed cells
|
50
|
+
def key_data_string(record)
|
51
|
+
part(record[0], record[0].bytesize + (record[2] ? DELETION_MASK : 0)) << part(record[1], record[1].bytesize)
|
65
52
|
end
|
66
53
|
|
67
|
-
def
|
68
|
-
|
54
|
+
def crc_string(s)
|
55
|
+
[Zlib.crc32(s, 0)].pack('N')
|
69
56
|
end
|
70
57
|
|
71
|
-
def
|
72
|
-
|
73
|
-
@deleted = masked & DELETION_MASK
|
74
|
-
length = masked & (DELETION_MASK - 1)
|
75
|
-
io.read length
|
58
|
+
def part(data, length)
|
59
|
+
[length].pack('N') << data
|
76
60
|
end
|
77
61
|
|
78
62
|
def read32(io)
|
79
63
|
raw = io.read(4)
|
80
64
|
raw.unpack('N')[0]
|
81
65
|
end
|
82
|
-
|
83
|
-
def part(data, length)
|
84
|
-
[length].pack('N') + data
|
85
|
-
end
|
86
66
|
end
|
87
67
|
end
|
data/lib/daybreak/version.rb
CHANGED
data/lib/daybreak/writer.rb
CHANGED
@@ -5,9 +5,7 @@ module Daybreak
|
|
5
5
|
# Open up the file, ready it for binary and nonblocking writing.
|
6
6
|
def initialize(file)
|
7
7
|
@file = file
|
8
|
-
|
9
8
|
open!
|
10
|
-
|
11
9
|
@worker = Worker.new(@fd)
|
12
10
|
end
|
13
11
|
|
@@ -65,20 +63,20 @@ module Daybreak
|
|
65
63
|
|
66
64
|
# Queue up a write to be committed later.
|
67
65
|
def enqueue(record)
|
68
|
-
@queue << record
|
66
|
+
@queue << record
|
69
67
|
end
|
70
68
|
|
71
69
|
# Loop and block if we don't have work to do or if
|
72
70
|
# the file isn't ready for another write just yet.
|
73
71
|
def work
|
74
|
-
buf =
|
72
|
+
buf = ''
|
75
73
|
loop do
|
76
|
-
|
77
|
-
|
74
|
+
record = @queue.pop
|
75
|
+
unless record
|
78
76
|
@fd.flush
|
79
77
|
break
|
80
78
|
end
|
81
|
-
buf <<
|
79
|
+
buf << Record.serialize(record)
|
82
80
|
read, write = IO.select [], [@fd]
|
83
81
|
if write and fd = write.first
|
84
82
|
lock(fd, File::LOCK_EX) { buf = try_write fd, buf }
|
@@ -98,7 +96,7 @@ module Daybreak
|
|
98
96
|
if s < buf.length
|
99
97
|
buf = buf[s..-1] # didn't finish
|
100
98
|
else
|
101
|
-
buf =
|
99
|
+
buf = ''
|
102
100
|
end
|
103
101
|
rescue Errno::EAGAIN
|
104
102
|
buf = buf # try this again
|
data/test/test.rb
CHANGED
@@ -1,8 +1,12 @@
|
|
1
|
-
require 'rubygems'
|
2
|
-
require 'simplecov'
|
3
1
|
require 'set'
|
4
|
-
|
5
|
-
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'simplecov'
|
5
|
+
SimpleCov.start
|
6
|
+
SimpleCov.command_name "Unit tests"
|
7
|
+
rescue Exception => ex
|
8
|
+
puts "No coverage report generated: #{ex.message}"
|
9
|
+
end
|
6
10
|
|
7
11
|
require File.expand_path(File.dirname(__FILE__)) + '/test_helper.rb'
|
8
12
|
|
data/test/test_helper.rb
CHANGED
metadata
CHANGED
@@ -1,72 +1,63 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: daybreak
|
3
|
-
version: !ruby/object:Gem::Version
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 31
|
4
5
|
prerelease:
|
5
|
-
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 2
|
10
|
+
version: 0.1.2
|
6
11
|
platform: ruby
|
7
|
-
authors:
|
12
|
+
authors:
|
8
13
|
- Jeff Larson
|
9
14
|
autorequire:
|
10
15
|
bindir: bin
|
11
16
|
cert_chain: []
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '0'
|
20
|
-
none: false
|
21
|
-
name: minitest
|
22
|
-
type: :development
|
17
|
+
|
18
|
+
date: 2013-01-07 00:00:00 -05:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: rake
|
23
23
|
prerelease: false
|
24
|
-
requirement: !ruby/object:Gem::Requirement
|
25
|
-
requirements:
|
26
|
-
- - ! '>='
|
27
|
-
- !ruby/object:Gem::Version
|
28
|
-
version: '0'
|
29
|
-
none: false
|
30
|
-
- !ruby/object:Gem::Dependency
|
31
|
-
version_requirements: !ruby/object:Gem::Requirement
|
32
|
-
requirements:
|
33
|
-
- - ! '>='
|
34
|
-
- !ruby/object:Gem::Version
|
35
|
-
version: '0'
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
36
25
|
none: false
|
37
|
-
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 3
|
30
|
+
segments:
|
31
|
+
- 0
|
32
|
+
version: "0"
|
38
33
|
type: :development
|
34
|
+
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: minitest
|
39
37
|
prerelease: false
|
40
|
-
requirement: !ruby/object:Gem::Requirement
|
41
|
-
requirements:
|
42
|
-
- - ! '>='
|
43
|
-
- !ruby/object:Gem::Version
|
44
|
-
version: '0'
|
45
|
-
none: false
|
46
|
-
- !ruby/object:Gem::Dependency
|
47
|
-
version_requirements: !ruby/object:Gem::Requirement
|
48
|
-
requirements:
|
49
|
-
- - ! '>='
|
50
|
-
- !ruby/object:Gem::Version
|
51
|
-
version: '0'
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
52
39
|
none: false
|
53
|
-
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
hash: 3
|
44
|
+
segments:
|
45
|
+
- 0
|
46
|
+
version: "0"
|
54
47
|
type: :development
|
55
|
-
|
56
|
-
requirement: !ruby/object:Gem::Requirement
|
57
|
-
requirements:
|
58
|
-
- - ! '>='
|
59
|
-
- !ruby/object:Gem::Version
|
60
|
-
version: '0'
|
61
|
-
none: false
|
48
|
+
version_requirements: *id002
|
62
49
|
description: A simple dimple key-value store for ruby.
|
63
|
-
email:
|
50
|
+
email:
|
64
51
|
- thejefflarson@gmail.com
|
65
52
|
executables: []
|
53
|
+
|
66
54
|
extensions: []
|
55
|
+
|
67
56
|
extra_rdoc_files: []
|
68
|
-
|
57
|
+
|
58
|
+
files:
|
69
59
|
- .gitignore
|
60
|
+
- .travis.yml
|
70
61
|
- .yardopts
|
71
62
|
- EPIGRAPH
|
72
63
|
- Gemfile
|
@@ -86,36 +77,43 @@ files:
|
|
86
77
|
- test/prof.rb
|
87
78
|
- test/test.rb
|
88
79
|
- test/test_helper.rb
|
80
|
+
has_rdoc: true
|
89
81
|
homepage: http://propublica.github.com/daybreak/
|
90
|
-
licenses:
|
82
|
+
licenses:
|
91
83
|
- MIT
|
92
84
|
post_install_message:
|
93
85
|
rdoc_options: []
|
94
|
-
|
86
|
+
|
87
|
+
require_paths:
|
95
88
|
- lib
|
96
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
97
|
-
requirements:
|
98
|
-
- - ! '>='
|
99
|
-
- !ruby/object:Gem::Version
|
100
|
-
version: '0'
|
89
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
101
90
|
none: false
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
91
|
+
requirements:
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
hash: 3
|
95
|
+
segments:
|
96
|
+
- 0
|
97
|
+
version: "0"
|
98
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
107
99
|
none: false
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
hash: 3
|
104
|
+
segments:
|
105
|
+
- 0
|
106
|
+
version: "0"
|
108
107
|
requirements: []
|
108
|
+
|
109
109
|
rubyforge_project:
|
110
|
-
rubygems_version: 1.
|
110
|
+
rubygems_version: 1.6.2
|
111
111
|
signing_key:
|
112
112
|
specification_version: 3
|
113
|
-
summary: Daybreak provides an in memory key-value store that is easily enumerable
|
114
|
-
|
115
|
-
test_files:
|
113
|
+
summary: Daybreak provides an in memory key-value store that is easily enumerable in ruby.
|
114
|
+
test_files:
|
116
115
|
- test/bench.rb
|
117
116
|
- test/compare.rb
|
118
117
|
- test/prof.rb
|
119
118
|
- test/test.rb
|
120
119
|
- test/test_helper.rb
|
121
|
-
has_rdoc:
|