daybreak 0.1.3 → 0.2.0
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 +1 -0
- data/.travis.yml +5 -0
- data/LICENSE +2 -2
- data/README +1 -3
- data/Rakefile +6 -9
- data/daybreak.gemspec +4 -4
- data/lib/daybreak.rb +3 -5
- data/lib/daybreak/db.rb +308 -114
- data/lib/daybreak/format.rb +52 -0
- data/lib/daybreak/queue.rb +107 -0
- data/lib/daybreak/serializer.rb +39 -0
- data/lib/daybreak/version.rb +3 -2
- data/script/bench +95 -0
- data/script/converter +390 -0
- data/test/test.rb +251 -57
- data/test/test_helper.rb +0 -3
- metadata +12 -11
- data/lib/daybreak/record.rb +0 -62
- data/lib/daybreak/writer.rb +0 -127
- data/test/bench.rb +0 -28
- data/test/compare.rb +0 -47
data/test/test.rb
CHANGED
@@ -1,21 +1,16 @@
|
|
1
|
-
require '
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'minitest/benchmark'
|
2
3
|
|
3
|
-
|
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
|
4
|
+
require 'set'
|
10
5
|
|
11
6
|
require File.expand_path(File.dirname(__FILE__)) + '/test_helper.rb'
|
12
7
|
|
13
|
-
describe
|
8
|
+
describe Daybreak::DB do
|
14
9
|
before do
|
15
10
|
@db = Daybreak::DB.new DB_PATH
|
16
11
|
end
|
17
12
|
|
18
|
-
it
|
13
|
+
it 'should insert' do
|
19
14
|
@db[1] = 1
|
20
15
|
assert_equal @db[1], 1
|
21
16
|
assert @db.has_key?(1)
|
@@ -24,98 +19,141 @@ describe "database functions" do
|
|
24
19
|
assert_equal @db.length, 1
|
25
20
|
end
|
26
21
|
|
27
|
-
it
|
28
|
-
@db
|
29
|
-
@db
|
22
|
+
it 'should persist values' do
|
23
|
+
@db['1'] = '4'
|
24
|
+
@db['4'] = '1'
|
25
|
+
@db.sync
|
30
26
|
|
31
27
|
assert_equal @db['1'], '4'
|
32
28
|
db2 = Daybreak::DB.new DB_PATH
|
33
29
|
assert_equal db2['1'], '4'
|
34
30
|
assert_equal db2['4'], '1'
|
35
|
-
db2.close
|
31
|
+
db2.close
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should persist after clear' do
|
35
|
+
@db['1'] = 'xy'
|
36
|
+
@db.clear
|
37
|
+
@db['1'] = '4'
|
38
|
+
@db['4'] = '1'
|
39
|
+
@db.close
|
40
|
+
|
41
|
+
@db = Daybreak::DB.new DB_PATH
|
42
|
+
assert_equal @db['1'], '4'
|
43
|
+
assert_equal @db['4'], '1'
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'should persist after compact' do
|
47
|
+
@db['1'] = 'xy'
|
48
|
+
@db['1'] = 'z'
|
49
|
+
@db.compact
|
50
|
+
@db['1'] = '4'
|
51
|
+
@db['4'] = '1'
|
52
|
+
@db.close
|
53
|
+
|
54
|
+
@db = Daybreak::DB.new DB_PATH
|
55
|
+
assert_equal @db['1'], '4'
|
56
|
+
assert_equal @db['4'], '1'
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'should reload database file in sync after compact' do
|
60
|
+
db = Daybreak::DB.new DB_PATH
|
61
|
+
|
62
|
+
@db['1'] = 'xy'
|
63
|
+
@db['1'] = 'z'
|
64
|
+
@db.compact
|
65
|
+
@db['1'] = '4'
|
66
|
+
@db['4'] = '1'
|
67
|
+
@db.flush
|
68
|
+
|
69
|
+
db.sync
|
70
|
+
assert_equal db['1'], '4'
|
71
|
+
assert_equal db['4'], '1'
|
72
|
+
db.close
|
36
73
|
end
|
37
74
|
|
38
|
-
it
|
75
|
+
it 'should reload database file in sync after clear' do
|
76
|
+
db = Daybreak::DB.new DB_PATH
|
77
|
+
|
78
|
+
@db['1'] = 'xy'
|
79
|
+
@db['1'] = 'z'
|
80
|
+
@db.clear
|
81
|
+
@db['1'] = '4'
|
82
|
+
@db['4'] = '1'
|
83
|
+
@db.flush
|
84
|
+
|
85
|
+
db.sync
|
86
|
+
assert_equal db['1'], '4'
|
87
|
+
assert_equal db['4'], '1'
|
88
|
+
db.close
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'should compact cleanly' do
|
39
92
|
@db[1] = 1
|
40
93
|
@db[1] = 1
|
41
|
-
@db.
|
94
|
+
@db.sync
|
95
|
+
|
42
96
|
size = File.stat(DB_PATH).size
|
43
|
-
@db.compact
|
97
|
+
@db.compact
|
44
98
|
assert_equal @db[1], 1
|
45
99
|
assert size > File.stat(DB_PATH).size
|
46
100
|
end
|
47
101
|
|
48
|
-
it
|
49
|
-
default_db = Daybreak::DB.new(DB_PATH, 0)
|
102
|
+
it 'should allow for default values' do
|
103
|
+
default_db = Daybreak::DB.new(DB_PATH, :default => 0)
|
50
104
|
assert_equal default_db[1], 0
|
51
105
|
default_db[1] = 1
|
52
106
|
assert_equal default_db[1], 1
|
107
|
+
default_db.close
|
53
108
|
end
|
54
109
|
|
55
|
-
it
|
110
|
+
it 'should handle default values that are procs' do
|
56
111
|
db = Daybreak::DB.new(DB_PATH) {|key| Set.new }
|
57
112
|
assert db['foo'].is_a? Set
|
113
|
+
db.close
|
58
114
|
end
|
59
115
|
|
60
|
-
it
|
116
|
+
it 'should be able to sync competing writes' do
|
61
117
|
@db.set! '1', 4
|
62
118
|
db2 = Daybreak::DB.new DB_PATH
|
63
119
|
db2.set! '1', 5
|
64
|
-
@db.
|
120
|
+
@db.sync
|
65
121
|
assert_equal @db['1'], 5
|
122
|
+
db2.close
|
66
123
|
end
|
67
124
|
|
68
|
-
it
|
69
|
-
20.times {|i| @db
|
125
|
+
it 'should be able to handle another process\'s call to compact' do
|
126
|
+
@db.lock { 20.times {|i| @db[i] = i } }
|
70
127
|
db2 = Daybreak::DB.new DB_PATH
|
71
|
-
20.times {|i| @db
|
72
|
-
@db.compact
|
73
|
-
db2.
|
74
|
-
assert_equal
|
128
|
+
@db.lock { 20.times {|i| @db[i] = i } }
|
129
|
+
@db.compact
|
130
|
+
db2.sync
|
131
|
+
assert_equal 19, db2['19']
|
132
|
+
db2.close
|
75
133
|
end
|
76
134
|
|
77
|
-
it
|
135
|
+
it 'can empty the database' do
|
78
136
|
20.times {|i| @db[i] = i }
|
79
|
-
@db.
|
137
|
+
@db.clear
|
80
138
|
db2 = Daybreak::DB.new DB_PATH
|
81
139
|
assert_equal nil, db2['19']
|
140
|
+
db2.close
|
82
141
|
end
|
83
142
|
|
84
|
-
it
|
85
|
-
class StringDB < Daybreak::DB
|
86
|
-
def serialize(it)
|
87
|
-
it.to_s
|
88
|
-
end
|
89
|
-
|
90
|
-
def parse(it)
|
91
|
-
it
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
db = StringDB.new 'string.db'
|
96
|
-
db[1] = 'one'
|
97
|
-
db[2] = 'two'
|
98
|
-
db.delete 2
|
99
|
-
db.compact!
|
100
|
-
assert_equal db[1], 'one'
|
101
|
-
assert_equal db[2], nil
|
102
|
-
db.empty!
|
103
|
-
db.close!
|
104
|
-
end
|
105
|
-
|
106
|
-
it "should handle deletions" do
|
143
|
+
it 'should handle deletions' do
|
107
144
|
@db[1] = 'one'
|
108
145
|
@db[2] = 'two'
|
109
|
-
@db.delete 'two'
|
146
|
+
@db.delete! 'two'
|
110
147
|
assert !@db.has_key?('two')
|
111
148
|
assert_equal @db['two'], nil
|
112
149
|
|
113
150
|
db2 = Daybreak::DB.new DB_PATH
|
114
151
|
assert !db2.has_key?('two')
|
115
152
|
assert_equal db2['two'], nil
|
153
|
+
db2.close
|
116
154
|
end
|
117
155
|
|
118
|
-
it
|
156
|
+
it 'should close and reopen the file when clearing the database' do
|
119
157
|
begin
|
120
158
|
1000.times {@db.clear}
|
121
159
|
rescue
|
@@ -123,8 +161,164 @@ describe "database functions" do
|
|
123
161
|
end
|
124
162
|
end
|
125
163
|
|
164
|
+
it 'should have threadsafe lock' do
|
165
|
+
@db[1] = 0
|
166
|
+
inc = proc { 1000.times { @db.lock { @db[1] += 1 } } }
|
167
|
+
a = Thread.new &inc
|
168
|
+
b = Thread.new &inc
|
169
|
+
a.join
|
170
|
+
b.join
|
171
|
+
assert_equal @db[1], 2000
|
172
|
+
end
|
173
|
+
|
174
|
+
it 'should synchronize across processes' do
|
175
|
+
@db[1] = 0
|
176
|
+
@db.flush
|
177
|
+
@db.close
|
178
|
+
begin
|
179
|
+
a = fork do
|
180
|
+
db = Daybreak::DB.new DB_PATH
|
181
|
+
1000.times do |i|
|
182
|
+
db.lock { db[1] += 1 }
|
183
|
+
db["a#{i}"] = i
|
184
|
+
sleep 0.01 if i % 100 == 0
|
185
|
+
end
|
186
|
+
db.close
|
187
|
+
end
|
188
|
+
b = fork do
|
189
|
+
db = Daybreak::DB.new DB_PATH
|
190
|
+
1000.times do |i|
|
191
|
+
db.lock { db[1] += 1 }
|
192
|
+
db["b#{i}"] = i
|
193
|
+
sleep 0.01 if i % 100 == 0
|
194
|
+
end
|
195
|
+
db.close
|
196
|
+
end
|
197
|
+
Process.wait a
|
198
|
+
Process.wait b
|
199
|
+
@db = Daybreak::DB.new DB_PATH
|
200
|
+
1000.times do |i|
|
201
|
+
assert_equal @db["a#{i}"], i
|
202
|
+
assert_equal @db["b#{i}"], i
|
203
|
+
end
|
204
|
+
assert_equal @db[1], 2000
|
205
|
+
rescue NotImplementedError
|
206
|
+
warn 'fork is not available: skipping multiprocess test'
|
207
|
+
@db = Daybreak::DB.new DB_PATH
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
it 'should synchronize across threads' do
|
212
|
+
@db[1] = 0
|
213
|
+
@db.flush
|
214
|
+
@db.close
|
215
|
+
a = Thread.new do
|
216
|
+
db = Daybreak::DB.new DB_PATH
|
217
|
+
1000.times do |i|
|
218
|
+
db.lock { db[1] += 1 }
|
219
|
+
db["a#{i}"] = i
|
220
|
+
sleep 0.01 if i % 100 == 0
|
221
|
+
end
|
222
|
+
db.close
|
223
|
+
end
|
224
|
+
b = Thread.new do
|
225
|
+
db = Daybreak::DB.new DB_PATH
|
226
|
+
1000.times do |i|
|
227
|
+
db.lock { db[1] += 1 }
|
228
|
+
db["b#{i}"] = i
|
229
|
+
sleep 0.01 if i % 100 == 0
|
230
|
+
end
|
231
|
+
db.close
|
232
|
+
end
|
233
|
+
a.join
|
234
|
+
b.join
|
235
|
+
@db = Daybreak::DB.new DB_PATH
|
236
|
+
1000.times do |i|
|
237
|
+
assert_equal @db["a#{i}"], i
|
238
|
+
assert_equal @db["b#{i}"], i
|
239
|
+
end
|
240
|
+
assert_equal @db[1], 2000
|
241
|
+
end
|
242
|
+
|
243
|
+
it 'should support background compaction' do
|
244
|
+
@db[1] = 0
|
245
|
+
@db.flush
|
246
|
+
@db.close
|
247
|
+
stop = false
|
248
|
+
a = Thread.new do
|
249
|
+
db = Daybreak::DB.new DB_PATH
|
250
|
+
1000.times do |i|
|
251
|
+
db.lock { db[1] += 1 }
|
252
|
+
db["a#{i}"] = i
|
253
|
+
sleep 0.01 if i % 100 == 0
|
254
|
+
end
|
255
|
+
db.close
|
256
|
+
end
|
257
|
+
b = Thread.new do
|
258
|
+
db = Daybreak::DB.new DB_PATH
|
259
|
+
1000.times do |i|
|
260
|
+
db.lock { db[1] += 1 }
|
261
|
+
db["b#{i}"] = i
|
262
|
+
sleep 0.01 if i % 100 == 0
|
263
|
+
end
|
264
|
+
db.close
|
265
|
+
end
|
266
|
+
c = Thread.new do
|
267
|
+
db = Daybreak::DB.new DB_PATH
|
268
|
+
db.compact until stop
|
269
|
+
db.close
|
270
|
+
end
|
271
|
+
d = Thread.new do
|
272
|
+
db = Daybreak::DB.new DB_PATH
|
273
|
+
db.compact until stop
|
274
|
+
db.close
|
275
|
+
end
|
276
|
+
stop = true
|
277
|
+
a.join
|
278
|
+
b.join
|
279
|
+
c.join
|
280
|
+
d.join
|
281
|
+
@db = Daybreak::DB.new DB_PATH
|
282
|
+
1000.times do |i|
|
283
|
+
assert_equal @db["a#{i}"], i
|
284
|
+
assert_equal @db["b#{i}"], i
|
285
|
+
end
|
286
|
+
assert_equal @db[1], 2000
|
287
|
+
end
|
288
|
+
|
289
|
+
it 'should support compact in lock' do
|
290
|
+
@db[1] = 2
|
291
|
+
@db.lock do
|
292
|
+
@db[1] = 2
|
293
|
+
@db.compact
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
it 'should support clear in lock' do
|
298
|
+
@db[1] = 2
|
299
|
+
@db.lock do
|
300
|
+
@db[1] = 2
|
301
|
+
@db.clear
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
it 'should support flush in lock' do
|
306
|
+
@db[1] = 2
|
307
|
+
@db.lock do
|
308
|
+
@db[1] = 2
|
309
|
+
@db.flush
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
it 'should support set! in lock' do
|
314
|
+
@db[1] = 2
|
315
|
+
@db.lock do
|
316
|
+
@db.set!(1, 2)
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
126
320
|
after do
|
127
|
-
@db.
|
128
|
-
@db.close
|
321
|
+
@db.clear
|
322
|
+
@db.close
|
129
323
|
end
|
130
324
|
end
|
data/test/test_helper.rb
CHANGED
metadata
CHANGED
@@ -1,15 +1,16 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: daybreak
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Jeff Larson
|
9
|
+
- Daniel Mendler
|
9
10
|
autorequire:
|
10
11
|
bindir: bin
|
11
12
|
cert_chain: []
|
12
|
-
date: 2013-01-
|
13
|
+
date: 2013-01-14 00:00:00.000000000 Z
|
13
14
|
dependencies:
|
14
15
|
- !ruby/object:Gem::Dependency
|
15
16
|
name: rake
|
@@ -43,9 +44,10 @@ dependencies:
|
|
43
44
|
- - ! '>='
|
44
45
|
- !ruby/object:Gem::Version
|
45
46
|
version: '0'
|
46
|
-
description:
|
47
|
+
description: Incredibly fast pure-ruby key-value store
|
47
48
|
email:
|
48
49
|
- thejefflarson@gmail.com
|
50
|
+
- mail@daniel-mendler.de
|
49
51
|
executables: []
|
50
52
|
extensions: []
|
51
53
|
extra_rdoc_files: []
|
@@ -61,11 +63,12 @@ files:
|
|
61
63
|
- daybreak.gemspec
|
62
64
|
- lib/daybreak.rb
|
63
65
|
- lib/daybreak/db.rb
|
64
|
-
- lib/daybreak/
|
66
|
+
- lib/daybreak/format.rb
|
67
|
+
- lib/daybreak/queue.rb
|
68
|
+
- lib/daybreak/serializer.rb
|
65
69
|
- lib/daybreak/version.rb
|
66
|
-
-
|
67
|
-
-
|
68
|
-
- test/compare.rb
|
70
|
+
- script/bench
|
71
|
+
- script/converter
|
69
72
|
- test/prof.rb
|
70
73
|
- test/test.rb
|
71
74
|
- test/test_helper.rb
|
@@ -93,11 +96,9 @@ rubyforge_project:
|
|
93
96
|
rubygems_version: 1.8.24
|
94
97
|
signing_key:
|
95
98
|
specification_version: 3
|
96
|
-
summary: Daybreak provides an in memory key-value store
|
97
|
-
|
99
|
+
summary: Daybreak provides an incredibly fast pure-ruby in memory key-value store,
|
100
|
+
which is multi-process safe and uses a journal log to store the data.
|
98
101
|
test_files:
|
99
|
-
- test/bench.rb
|
100
|
-
- test/compare.rb
|
101
102
|
- test/prof.rb
|
102
103
|
- test/test.rb
|
103
104
|
- test/test_helper.rb
|
data/lib/daybreak/record.rb
DELETED
@@ -1,62 +0,0 @@
|
|
1
|
-
module Daybreak
|
2
|
-
# Records define how data is serialized and read from disk.
|
3
|
-
module 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
|
-
|
11
|
-
extend self
|
12
|
-
|
13
|
-
# The mask a record uses to check for deletion.
|
14
|
-
DELETION_MASK = 1 << 31
|
15
|
-
|
16
|
-
# Read a record from an open io source, check the CRC, and set <tt>@key</tt>
|
17
|
-
# and <tt>@data</tt>.
|
18
|
-
# @param [#read] io an IO instance to read from
|
19
|
-
|
20
|
-
# The serialized representation of the key value pair plus the CRC.
|
21
|
-
# @return [String]
|
22
|
-
def serialize(record)
|
23
|
-
raise UnnacceptableDataError, 'key and data must be defined' unless record[0] && record[1]
|
24
|
-
s = key_data_string(record)
|
25
|
-
s << crc_string(s)
|
26
|
-
end
|
27
|
-
|
28
|
-
# Create a new record to read from IO.
|
29
|
-
# @param [#read] io an IO instance to read from
|
30
|
-
def deserialize(buf)
|
31
|
-
record = []
|
32
|
-
masked = read32(buf)
|
33
|
-
# Read the record's key bytes
|
34
|
-
record << buf.slice!(0, masked & (DELETION_MASK - 1)) <<
|
35
|
-
# Read the record's value bytes
|
36
|
-
buf.slice!(0, read32(buf)) <<
|
37
|
-
# Set the deletion flag
|
38
|
-
((masked & DELETION_MASK) != 0)
|
39
|
-
raise CorruptDataError, 'CRC mismatch' unless buf.slice!(0, 4) == crc_string(key_data_string(record))
|
40
|
-
record
|
41
|
-
end
|
42
|
-
|
43
|
-
private
|
44
|
-
|
45
|
-
# Return the deletion flag plus two length prefixed cells
|
46
|
-
def key_data_string(record)
|
47
|
-
part(record[0], record[0].bytesize + (record[2] ? DELETION_MASK : 0)) << part(record[1], record[1].bytesize)
|
48
|
-
end
|
49
|
-
|
50
|
-
def crc_string(s)
|
51
|
-
[Zlib.crc32(s, 0)].pack('N')
|
52
|
-
end
|
53
|
-
|
54
|
-
def part(data, length)
|
55
|
-
[length].pack('N') << data
|
56
|
-
end
|
57
|
-
|
58
|
-
def read32(buf)
|
59
|
-
buf.slice!(0, 4).unpack('N')[0]
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|