perobs 4.1.0 → 4.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -54,7 +54,7 @@ module PEROBS
54
54
 
55
55
  # Benchmark runs showed a cache size of 128 to be a good compromise
56
56
  # between read and write performance trade-offs and memory consumption.
57
- @cache = PersistentObjectCache.new(128, 5000, SpaceTreeNode, self)
57
+ @cache = PersistentObjectCache.new(256, -1, SpaceTreeNode, self)
58
58
  end
59
59
 
60
60
  # Open the SpaceTree file.
@@ -46,8 +46,9 @@ require 'perobs/ConsoleProgressMeter'
46
46
  # PErsistent Ruby OBject Store
47
47
  module PEROBS
48
48
 
49
- Statistics = Struct.new(:in_memory_objects, :root_objects,
50
- :marked_objects, :swept_objects)
49
+ Statistics = Struct.new(:in_memory_objects, :root_objects, :zombie_objects,
50
+ :marked_objects, :swept_objects,
51
+ :created_objects, :collected_objects)
51
52
 
52
53
  # PEROBS::Store is a persistent storage system for Ruby objects. Regular
53
54
  # Ruby objects are transparently stored in a back-end storage and retrieved
@@ -109,6 +110,7 @@ module PEROBS
109
110
  class Store
110
111
 
111
112
  attr_reader :db, :cache, :class_map
113
+ attr_writer :root_objects
112
114
 
113
115
  # Create a new Store.
114
116
  # @param data_base [String] the name of the database
@@ -143,6 +145,9 @@ module PEROBS
143
145
  # It defaults to ProgressMeter which only logs into
144
146
  # the log. Use ConsoleProgressMeter or a derived
145
147
  # class for more fancy progress reporting.
148
+ # :no_root_objects : Create a new store without root objects. This only
149
+ # makes sense if you want to copy the objects of
150
+ # another store into this store.
146
151
  def initialize(data_base, options = {})
147
152
  # Create a backing store handler
148
153
  @progressmeter = (options[:progressmeter] ||= ProgressMeter.new)
@@ -155,25 +160,32 @@ module PEROBS
155
160
  # List of PEROBS objects that are currently available as Ruby objects
156
161
  # hashed by their ID.
157
162
  @in_memory_objects = {}
163
+ # List of objects that were destroyed already but were still found in
164
+ # the in_memory_objects list. _collect has not yet been called for them.
165
+ @zombie_objects = {}
158
166
 
159
167
  # This objects keeps some counters of interest.
160
168
  @stats = Statistics.new
169
+ @stats[:created_objects] = 0
170
+ @stats[:collected_objects] = 0
161
171
 
162
172
  # The Cache reduces read and write latencies by keeping a subset of the
163
173
  # objects in memory.
164
174
  @cache = Cache.new(options[:cache_bits] || 16)
165
175
 
166
176
  # The named (global) objects IDs hashed by their name
167
- unless (@root_objects = object_by_id(0))
168
- PEROBS.log.debug "Initializing the PEROBS store"
169
- # The root object hash always has the object ID 0.
170
- @root_objects = _construct_po(Hash, 0)
171
- # Mark the root_objects object as modified.
172
- @cache.cache_write(@root_objects)
173
- end
174
- unless @root_objects.is_a?(Hash)
175
- PEROBS.log.fatal "Database corrupted: Root objects must be a Hash " +
176
- "but is a #{@root_objects.class}"
177
+ unless options[:no_root_objects]
178
+ unless (@root_objects = object_by_id(0))
179
+ PEROBS.log.debug "Initializing the PEROBS store"
180
+ # The root object hash always has the object ID 0.
181
+ @root_objects = _construct_po(Hash, 0)
182
+ # Mark the root_objects object as modified.
183
+ @cache.cache_write(@root_objects)
184
+ end
185
+ unless @root_objects.is_a?(Hash)
186
+ PEROBS.log.fatal "Database corrupted: Root objects must be a Hash " +
187
+ "but is a #{@root_objects.class}"
188
+ end
177
189
  end
178
190
  end
179
191
 
@@ -185,7 +197,9 @@ module PEROBS
185
197
  sync
186
198
 
187
199
  # Create a new store with the specified directory and options.
188
- new_db = Store.new(dir, options)
200
+ new_options = options.clone
201
+ new_options[:no_root_objects] = true
202
+ new_db = Store.new(dir, new_options)
189
203
  # Clear the cache.
190
204
  new_db.sync
191
205
  # Copy all objects of the existing store to the new store.
@@ -196,6 +210,7 @@ module PEROBS
196
210
  obj._sync
197
211
  i += 1
198
212
  end
213
+ new_db.root_objects = new_db.object_by_id(0)
199
214
  PEROBS.log.debug "Copied #{i} objects into new database at #{dir}"
200
215
  # Flush the new store and close it.
201
216
  new_db.exit
@@ -203,7 +218,6 @@ module PEROBS
203
218
  true
204
219
  end
205
220
 
206
-
207
221
  # Close the store and ensure that all in-memory objects are written out to
208
222
  # the storage backend. The Store object is no longer usable after this
209
223
  # method was called.
@@ -216,10 +230,22 @@ module PEROBS
216
230
  end
217
231
  @cache.flush if @cache
218
232
  @db.close if @db
219
- @db = @class_map = @in_memory_objects = @stats = @cache = @root_objects =
220
- nil
221
- end
222
233
 
234
+ GC.start
235
+ if @stats
236
+ unless @stats[:created_objects] == @stats[:collected_objects] +
237
+ @in_memory_objects.length
238
+ PEROGS.log.fatal "Created objects count " +
239
+ "(#{@stats[:created_objects]})" +
240
+ " is not equal to the collected count " +
241
+ "(#{@stats[:collected_objects]}) + in_memory_objects count " +
242
+ "(#{@in_memory_objects.length})"
243
+ end
244
+ end
245
+
246
+ @db = @class_map = @in_memory_objects = @zombie_objects =
247
+ @stats = @cache = @root_objects = nil
248
+ end
223
249
 
224
250
  # You need to call this method to create new PEROBS objects that belong to
225
251
  # this Store.
@@ -255,8 +281,8 @@ module PEROBS
255
281
  # deletes the entire database.
256
282
  def delete_store
257
283
  @db.delete_database
258
- @db = @class_map = @in_memory_objects = @stats = @cache = @root_objects =
259
- nil
284
+ @db = @class_map = @in_memory_objects = @zombie_objects =
285
+ @stats = @cache = @root_objects = nil
260
286
  end
261
287
 
262
288
  # Store the provided object under the given name. Use this to make the
@@ -358,10 +384,10 @@ module PEROBS
358
384
  rescue RangeError => e
359
385
  # Due to a race condition the object can still be in the
360
386
  # @in_memory_objects list but has been collected already by the Ruby
361
- # GC. In that case we need to load it again. In this case the
362
- # _collect() call will happen much later, potentially after we have
363
- # registered a new object with the same ID.
364
- @in_memory_objects.delete(id)
387
+ # GC. In that case we need to load it again. The _collect() call
388
+ # will happen much later, potentially after we have registered a new
389
+ # object with the same ID.
390
+ @zombie_objects[id] = @in_memory_objects.delete(id)
365
391
  end
366
392
  end
367
393
 
@@ -510,7 +536,16 @@ module PEROBS
510
536
  # @param obj [BasicObject] Object to register
511
537
  # @param id [Integer] object ID
512
538
  def _register_in_memory(obj, id)
539
+ unless obj.is_a?(ObjectBase)
540
+ PEROBS.log.fatal "You can only register ObjectBase objects"
541
+ end
542
+ if @in_memory_objects.include?(id)
543
+ PEROBS.log.fatal "The Store::_in_memory_objects list already " +
544
+ "contains an object for ID #{id}"
545
+ end
546
+
513
547
  @in_memory_objects[id] = obj.object_id
548
+ @stats[:created_objects] += 1
514
549
  end
515
550
 
516
551
  # Remove the object from the in-memory list. This is an internal method
@@ -520,6 +555,10 @@ module PEROBS
520
555
  def _collect(id, ruby_object_id)
521
556
  if @in_memory_objects[id] == ruby_object_id
522
557
  @in_memory_objects.delete(id)
558
+ @stats[:collected_objects] += 1
559
+ elsif @zombie_objects[id] == ruby_object_id
560
+ @zombie_objects.delete(id)
561
+ @stats[:collected_objects] += 1
523
562
  end
524
563
  end
525
564
 
@@ -527,6 +566,7 @@ module PEROBS
527
566
  def statistics
528
567
  @stats.in_memory_objects = @in_memory_objects.length
529
568
  @stats.root_objects = @root_objects.length
569
+ @stats.zombie_objects = @zombie_objects.length
530
570
 
531
571
  @stats
532
572
  end
@@ -556,8 +596,10 @@ module PEROBS
556
596
  # Sweep phase of a mark-and-sweep garbage collector. It will remove all
557
597
  # unmarked objects from the store.
558
598
  def sweep
559
- @stats.swept_objects = @db.delete_unmarked_objects
560
- @cache.reset
599
+ @stats.swept_objects = @db.delete_unmarked_objects do |id|
600
+ @cache.evict(id)
601
+ end
602
+ GC.start
561
603
  PEROBS.log.debug "#{@stats.swept_objects} objects collected"
562
604
  @stats.swept_objects
563
605
  end
@@ -1,4 +1,4 @@
1
1
  module PEROBS
2
2
  # The version number
3
- VERSION = "4.1.0"
3
+ VERSION = "4.2.0"
4
4
  end
@@ -16,9 +16,9 @@ GEM_SPEC = Gem::Specification.new do |spec|
16
16
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
17
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
18
  spec.require_paths = ["lib"]
19
- spec.required_ruby_version = '>=2.3'
19
+ spec.required_ruby_version = '>=2.4'
20
20
 
21
21
  spec.add_development_dependency 'bundler', '~> 2.3'
22
22
  spec.add_development_dependency 'yard', '~>0.9.12'
23
- spec.add_development_dependency 'rake', '~> 10.1'
23
+ spec.add_development_dependency 'rake', '~> 12.3.3'
24
24
  end
@@ -51,6 +51,7 @@ describe PEROBS::BTree do
51
51
  @m.open
52
52
  expect(@m.to_s).to eql("o--- @1\n")
53
53
  #expect(@m.to_a).to eql([])
54
+ expect(@m.check).to be true
54
55
  end
55
56
 
56
57
  it 'should support adding sequential key/value pairs' do
@@ -32,7 +32,7 @@ require 'spec_helper'
32
32
  require 'perobs'
33
33
  require 'perobs/BigArray'
34
34
 
35
- NODE_ENTRIES = 4
35
+ NODE_ENTRIES = 6
36
36
 
37
37
  describe PEROBS::BigArray do
38
38
 
@@ -59,12 +59,16 @@ describe PEROBS::BigArray do
59
59
  expect(@a.length).to eq(0)
60
60
  expect(@a.check).to be true
61
61
  expect(@a[0]).to be nil
62
+ expect(@a.first).to be nil
63
+ expect(@a.last).to be nil
62
64
  end
63
65
 
64
66
  it 'should append the first element' do
65
67
  @a << 0
66
68
  expect(@a.empty?).to be false
67
69
  expect(@a[0]).to eq(0)
70
+ expect(@a.first).to eq(0)
71
+ expect(@a.last).to eq(0)
68
72
  expect(@a.length).to eq(1)
69
73
  expect(@a.check).to be true
70
74
  end
@@ -75,6 +79,8 @@ describe PEROBS::BigArray do
75
79
  expect(@a.check).to be true
76
80
  expect(@a.length).to eq(i + 1)
77
81
  end
82
+ expect(@a.first).to eq(0)
83
+ expect(@a.last).to eq(10 * NODE_ENTRIES - 1)
78
84
  end
79
85
 
80
86
  it 'should insert at 0' do
@@ -111,7 +117,7 @@ describe PEROBS::BigArray do
111
117
 
112
118
  it 'should support the [] operator' do
113
119
  expect(@a[0]).to be nil
114
- expect{@a[-1]}.to raise_error(IndexError)
120
+ expect(@a[-1]).to be nil
115
121
  @a[0] = 0
116
122
  expect(@a[0]).to eq(0)
117
123
  1.upto(3 * NODE_ENTRIES) do |i|
@@ -124,7 +130,7 @@ describe PEROBS::BigArray do
124
130
  0.upto(3 * NODE_ENTRIES) do |i|
125
131
  expect(@a[-3 * NODE_ENTRIES + i - 1]).to eq(i)
126
132
  end
127
- expect{@a[-3 * NODE_ENTRIES - 2]}.to raise_error(IndexError)
133
+ expect(@a[-3 * NODE_ENTRIES - 2]).to be nil
128
134
  (3 * NODE_ENTRIES + 1).upto(4 * NODE_ENTRIES) do |i|
129
135
  expect(@a[i]).to be nil
130
136
  end
@@ -142,7 +148,7 @@ describe PEROBS::BigArray do
142
148
  expect(@a.length).to eq(0)
143
149
  expect(@a.check).to be true
144
150
 
145
- n = 3 * NODE_ENTRIES
151
+ n = 5 * NODE_ENTRIES
146
152
  0.upto(n) { |i| @a.insert(i, i) }
147
153
  0.upto(n) do |i|
148
154
  expect(@a.delete_at(0)).to eql(i)
@@ -155,20 +161,52 @@ describe PEROBS::BigArray do
155
161
  expect(@a.check).to be true
156
162
  end
157
163
 
158
- n = 11 * NODE_ENTRIES
164
+ n = 15 * NODE_ENTRIES
159
165
  0.upto(n - 1) { |i| @a.insert(i, i) }
160
166
  expect(@a.delete_at(n + 2)).to be nil
161
167
  expect(@a.delete_at(-(n + 2))).to be nil
162
168
  expect(@a.size).to eql(n)
163
169
 
164
170
  n.times do |i|
165
- @a.delete_at(rand(@a.size))
171
+ idx = rand(@a.size)
172
+ @a.delete_at(idx)
166
173
  expect(@a.size).to be (n - 1 - i)
167
174
  expect(@a.check).to be true
168
175
  end
169
176
  expect(@a.size).to eql(0)
170
177
  end
171
178
 
179
+ it 'should fill the gaps' do
180
+ 1.upto(4) do |i|
181
+ idx = i * NODE_ENTRIES * NODE_ENTRIES
182
+ @a[idx] = idx
183
+ expect(@a[idx - 1]).to be nil
184
+ expect(@a[idx + 1]).to be nil
185
+ expect(@a.check).to be true
186
+ end
187
+ expect(@a[0]).to be nil
188
+ end
189
+
190
+ it 'should insert after a gap' do
191
+ ref = []
192
+ 10.times do |i|
193
+ idx = 10 + i * 3
194
+ @a[idx] = idx
195
+ ref[idx] = idx
196
+ expect(@a[idx]).to eql(idx)
197
+ expect(@a.check).to be true
198
+ end
199
+ 10.times do |i|
200
+ idx = i * 5
201
+ @a[idx] = idx
202
+ ref[idx] = idx
203
+ expect(@a[idx]).to eql(idx)
204
+ expect(@a.check).to be true
205
+ end
206
+ expect(@a.check).to be true
207
+ expect(@a.to_a).to eql(ref)
208
+ end
209
+
172
210
  it 'should iterate over all values' do
173
211
  n = 3 * NODE_ENTRIES
174
212
  0.upto(n) { |i| @a.insert(i, i) }
@@ -191,6 +229,15 @@ describe PEROBS::BigArray do
191
229
  end
192
230
  end
193
231
 
232
+ it 'should insert at the beginning' do
233
+ (5 * NODE_ENTRIES).downto(0) do |i|
234
+ @a.insert(0, i)
235
+ end
236
+ expect(@a.check).to be true
237
+ a = Array.new(5 * NODE_ENTRIES + 1) { |i| i }
238
+ expect(@a.to_a).to eq(a)
239
+ end
240
+
194
241
  it 'should persist the data' do
195
242
  db_name = generate_db_name(__FILE__ + "_persist")
196
243
  store = PEROBS::Store.new(db_name)
@@ -72,6 +72,13 @@ describe PEROBS::Hash do
72
72
  expect(@h.keys).to eql([ 'foo' ])
73
73
  end
74
74
 
75
+ it 'should store a few objects' do
76
+ 20.times do |i|
77
+ @h["bar#{i}"] = "foo#{i}"
78
+ end
79
+ expect(@h.size).to eql(20)
80
+ end
81
+
75
82
  it 'should return nil for unknown objects' do
76
83
  expect(@h['bar']).to be_nil
77
84
  end
@@ -127,6 +134,7 @@ describe PEROBS::Hash do
127
134
  end
128
135
  expect(h.check).to be true
129
136
  expect(h.length).to eql(n)
137
+ expect(store.check).to eql(0)
130
138
  store.exit
131
139
 
132
140
  store = PEROBS::Store.new(db_name)
@@ -71,7 +71,7 @@ describe PEROBS::FlatFileDB do
71
71
  expect { db2.open }.to raise_error(PEROBS::FatalError)
72
72
  end
73
73
 
74
- it 'should do a version upgrade' do
74
+ it 'should do a version upgrade from version 3' do
75
75
  # Close the store
76
76
  @store.exit
77
77
  src_dir = File.join(File.dirname(__FILE__), 'LegacyDBs', 'version_3')
@@ -82,6 +82,21 @@ describe PEROBS::FlatFileDB do
82
82
  capture_io { expect(db.check).to be true }
83
83
  end
84
84
 
85
+ it 'should do a version upgrade from version 4.1' do
86
+ # Close the store
87
+ @store.exit
88
+ src_dir = File.join(File.dirname(__FILE__), 'LegacyDBs', 'version_4.1')
89
+ FileUtils.cp_r(Dir.glob(src_dir + '/*'), @db_dir)
90
+
91
+ db = LegacyDB.new(@db_dir)
92
+ capture_io { db.open }
93
+ capture_io { expect(db.repair).to eql(0) }
94
+ expect(File.exist?(File.join(@db_dir, 'space_index.blobs'))).to be true
95
+ expect(File.exist?(File.join(@db_dir, 'space_list.blobs'))).to be true
96
+ expect(File.exist?(File.join(@db_dir, 'database_spaces.blobs'))).to be false
97
+ capture_io { expect(db.check).to be true }
98
+ end
99
+
85
100
  it 'should refuse a version downgrade' do
86
101
  # Close the store
87
102
  @store.exit
@@ -119,7 +134,7 @@ describe PEROBS::FlatFileDB do
119
134
  expect(store['o'].b).to eql(42)
120
135
  end
121
136
 
122
- it 'should repair a corrupted database.blobs file' do
137
+ it 'should discard a corrupted blob inside the database.blobs file' do
123
138
  @store.exit
124
139
 
125
140
  db = PEROBS::FlatFileDB.new(@db_dir)
@@ -142,7 +157,7 @@ describe PEROBS::FlatFileDB do
142
157
  f.close
143
158
 
144
159
  db.open
145
- expect(db.check_db).to eql(2)
160
+ expect(db.check_db).to eql(1)
146
161
  expect(db.check_db(true)).to eql(1)
147
162
  db.close
148
163
  db = PEROBS::FlatFileDB.new(@db_dir, { :log => $stderr,
@@ -160,5 +175,95 @@ describe PEROBS::FlatFileDB do
160
175
  db.close
161
176
  end
162
177
 
178
+ it 'should discard a corrupted blob at the end of the database.blobs file' do
179
+ @store.exit
180
+
181
+ db = PEROBS::FlatFileDB.new(@db_dir)
182
+ db_file = File.join(@db_dir, 'database.blobs')
183
+ db.open
184
+ 0.upto(5) do |i|
185
+ db.put_object("#{i + 1}:#{'X' * (i + 1) * 30}$", i + 1)
186
+ end
187
+ db.close
188
+
189
+ f = File.truncate(db_file, File.size(db_file) - 20)
190
+
191
+ db.open
192
+ expect(db.check_db).to eql(2)
193
+ expect(db.check_db(true)).to eql(2)
194
+ db.close
195
+ db = PEROBS::FlatFileDB.new(@db_dir, { :log => $stderr,
196
+ :log_level => Logger::ERROR })
197
+ db.open
198
+ expect(db.check_db).to eql(0)
199
+
200
+ 0.upto(4) do |i|
201
+ expect(db.get_object(i + 1)).to eql("#{i + 1}:#{'X' * (i + 1) * 30}$")
202
+ end
203
+ expect(db.get_object(6)).to be_nil
204
+ db.close
205
+ end
206
+
207
+ it 'should discard a corrupted header at the end of the database.blobs file' do
208
+ @store.exit
209
+
210
+ db = PEROBS::FlatFileDB.new(@db_dir)
211
+ db_file = File.join(@db_dir, 'database.blobs')
212
+ db.open
213
+ 0.upto(5) do |i|
214
+ db.put_object("#{i + 1}:#{'X' * (i + 1) * 30}$", i + 1)
215
+ end
216
+ db.close
217
+
218
+ f = File.truncate(db_file, File.size(db_file) - 200)
219
+
220
+ db.open
221
+ expect(db.check_db).to eql(1)
222
+ expect(db.check_db(true)).to eql(1)
223
+ db.close
224
+ db = PEROBS::FlatFileDB.new(@db_dir, { :log => $stderr,
225
+ :log_level => Logger::ERROR })
226
+ db.open
227
+ expect(db.check_db).to eql(0)
228
+
229
+ 0.upto(4) do |i|
230
+ expect(db.get_object(i + 1)).to eql("#{i + 1}:#{'X' * (i + 1) * 30}$")
231
+ end
232
+ expect(db.get_object(6)).to be_nil
233
+ db.close
234
+ end
235
+
236
+ it 'should handle a lost blob at the end of the database.blobs file' do
237
+ @store.exit
238
+
239
+ db = PEROBS::FlatFileDB.new(@db_dir)
240
+ db_file = File.join(@db_dir, 'database.blobs')
241
+ db.open
242
+ 0.upto(5) do |i|
243
+ db.put_object("#{i + 1}:#{'X' * (i + 1) * 30}$", i + 1)
244
+ end
245
+ db.close
246
+
247
+ # This exactly removes the last blob (#6)
248
+ f = File.truncate(db_file, File.size(db_file) - 210)
249
+
250
+ db.open
251
+ expect(db.check_db).to eql(1)
252
+ # The repair won't find the missing blob since the blob file is without
253
+ # errors.
254
+ expect(db.check_db(true)).to eql(0)
255
+ db.close
256
+ db = PEROBS::FlatFileDB.new(@db_dir, { :log => $stderr,
257
+ :log_level => Logger::ERROR })
258
+ db.open
259
+ expect(db.check_db).to eql(0)
260
+
261
+ 0.upto(4) do |i|
262
+ expect(db.get_object(i + 1)).to eql("#{i + 1}:#{'X' * (i + 1) * 30}$")
263
+ end
264
+ expect(db.get_object(6)).to be_nil
265
+ db.close
266
+ end
267
+
163
268
  end
164
269