perobs 4.1.0 → 4.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.
@@ -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