perobs 4.0.0 → 4.4.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.
- checksums.yaml +5 -5
- data/README.md +27 -16
- data/lib/perobs/Array.rb +66 -19
- data/lib/perobs/BTree.rb +106 -15
- data/lib/perobs/BTreeBlob.rb +4 -3
- data/lib/perobs/BTreeDB.rb +5 -4
- data/lib/perobs/BTreeNode.rb +482 -156
- data/lib/perobs/BTreeNodeLink.rb +10 -0
- data/lib/perobs/BigArray.rb +285 -0
- data/lib/perobs/BigArrayNode.rb +1002 -0
- data/lib/perobs/BigHash.rb +246 -0
- data/lib/perobs/BigTree.rb +197 -0
- data/lib/perobs/BigTreeNode.rb +873 -0
- data/lib/perobs/Cache.rb +48 -10
- data/lib/perobs/ConsoleProgressMeter.rb +61 -0
- data/lib/perobs/DataBase.rb +4 -3
- data/lib/perobs/DynamoDB.rb +57 -15
- data/lib/perobs/EquiBlobsFile.rb +155 -50
- data/lib/perobs/FNV_Hash_1a_64.rb +54 -0
- data/lib/perobs/FlatFile.rb +519 -227
- data/lib/perobs/FlatFileBlobHeader.rb +113 -54
- data/lib/perobs/FlatFileDB.rb +49 -23
- data/lib/perobs/FuzzyStringMatcher.rb +175 -0
- data/lib/perobs/Hash.rb +127 -33
- data/lib/perobs/IDList.rb +144 -0
- data/lib/perobs/IDListPage.rb +107 -0
- data/lib/perobs/IDListPageFile.rb +180 -0
- data/lib/perobs/IDListPageRecord.rb +142 -0
- data/lib/perobs/Object.rb +18 -15
- data/lib/perobs/ObjectBase.rb +46 -5
- data/lib/perobs/PersistentObjectCache.rb +57 -68
- data/lib/perobs/PersistentObjectCacheLine.rb +24 -12
- data/lib/perobs/ProgressMeter.rb +97 -0
- data/lib/perobs/SpaceManager.rb +273 -0
- data/lib/perobs/SpaceTree.rb +21 -12
- data/lib/perobs/SpaceTreeNode.rb +53 -61
- data/lib/perobs/Store.rb +264 -145
- data/lib/perobs/version.rb +1 -1
- data/lib/perobs.rb +2 -0
- data/perobs.gemspec +4 -4
- data/test/Array_spec.rb +15 -6
- data/test/BTree_spec.rb +6 -2
- data/test/BigArray_spec.rb +261 -0
- data/test/BigHash_spec.rb +152 -0
- data/test/BigTreeNode_spec.rb +153 -0
- data/test/BigTree_spec.rb +259 -0
- data/test/EquiBlobsFile_spec.rb +105 -1
- data/test/FNV_Hash_1a_64_spec.rb +59 -0
- data/test/FlatFileDB_spec.rb +198 -14
- data/test/FuzzyStringMatcher_spec.rb +261 -0
- data/test/Hash_spec.rb +13 -3
- data/test/IDList_spec.rb +77 -0
- data/test/LegacyDBs/LegacyDB.rb +155 -0
- data/test/LegacyDBs/version_3/class_map.json +1 -0
- data/test/LegacyDBs/version_3/config.json +1 -0
- data/test/LegacyDBs/version_3/database.blobs +0 -0
- data/test/LegacyDBs/version_3/database_spaces.blobs +0 -0
- data/test/LegacyDBs/version_3/index.blobs +0 -0
- data/test/LegacyDBs/version_3/version +1 -0
- data/test/LockFile_spec.rb +9 -6
- data/test/SpaceManager_spec.rb +176 -0
- data/test/SpaceTree_spec.rb +4 -1
- data/test/Store_spec.rb +305 -203
- data/test/spec_helper.rb +9 -4
- metadata +57 -16
- data/lib/perobs/BTreeNodeCache.rb +0 -109
- data/lib/perobs/TreeDB.rb +0 -277
@@ -0,0 +1,142 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
#
|
3
|
+
# = IDListPageRecord.rb -- Persistent Ruby Object Store
|
4
|
+
#
|
5
|
+
# Copyright (c) 2018 by Chris Schlaeger <chris@taskjuggler.org>
|
6
|
+
#
|
7
|
+
# MIT License
|
8
|
+
#
|
9
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
10
|
+
# a copy of this software and associated documentation files (the
|
11
|
+
# "Software"), to deal in the Software without restriction, including
|
12
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
13
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
14
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
15
|
+
# the following conditions:
|
16
|
+
#
|
17
|
+
# The above copyright notice and this permission notice shall be
|
18
|
+
# included in all copies or substantial portions of the Software.
|
19
|
+
#
|
20
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
21
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
22
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
23
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
24
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
25
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
26
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
27
|
+
|
28
|
+
module PEROBS
|
29
|
+
|
30
|
+
# The IDListPageRecord class models the elements of the IDList. Each page
|
31
|
+
# holds up to a certain number of IDs that can be cached into a file if
|
32
|
+
# needed. Each page holds IDs within a given interval. The cache is managed
|
33
|
+
# by the IDListPageFile object.
|
34
|
+
class IDListPageRecord
|
35
|
+
|
36
|
+
attr_reader :min_id, :max_id, :page_idx
|
37
|
+
attr_accessor :page_entries
|
38
|
+
|
39
|
+
# Create a new IDListPageRecord object.
|
40
|
+
# @param page_file [IDListPageFile] The page file that manages the cache.
|
41
|
+
# @param min_id [Integer] The smallest ID that can be stored in this page
|
42
|
+
# @param max_id [Integer] the largest ID that can be stored in this page
|
43
|
+
# @param values [Array] An array of IDs to be stored in this page
|
44
|
+
def initialize(page_file, min_id, max_id, values = [])
|
45
|
+
@page_file = page_file
|
46
|
+
@min_id = min_id
|
47
|
+
@max_id = max_id
|
48
|
+
@page_entries = 0
|
49
|
+
@page_idx = @page_file.new_page(self, values)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Check if the given ID is included in this page.
|
53
|
+
# @param id [Integer]
|
54
|
+
# @return [True of False] Return true if found, false otherwise.
|
55
|
+
def include?(id)
|
56
|
+
return false if id < @min_id || @max_id < id
|
57
|
+
|
58
|
+
page.include?(id)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Check if the page is full and can't store any more IDs.
|
62
|
+
# @return [True or False]
|
63
|
+
def is_full?
|
64
|
+
page.is_full?
|
65
|
+
end
|
66
|
+
|
67
|
+
# Insert an ID into the page.
|
68
|
+
# @param id [Integer] The ID to store
|
69
|
+
def insert(id)
|
70
|
+
unless @min_id <= id && id <= @max_id
|
71
|
+
raise ArgumentError, "IDs for this page must be between #{@min_id} " +
|
72
|
+
"and #{@max_id}. #{id} is outside this range."
|
73
|
+
end
|
74
|
+
|
75
|
+
page.insert(id)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Split the current page. This split is done by splitting the ID range in
|
79
|
+
# half. This page will keep the first half, the newly created page will
|
80
|
+
# get the second half. This may not actually yield an empty page as all
|
81
|
+
# values could remain with one of the pages. In this case further splits
|
82
|
+
# need to be issued by the caller.
|
83
|
+
# @return [IDListPageRecord] A new IDListPageRecord object.
|
84
|
+
def split
|
85
|
+
# Determine the new max_id for the old page.
|
86
|
+
max_id = @min_id + (@max_id - @min_id) / 2
|
87
|
+
# Create a new page that stores the upper half of the ID range. Remove
|
88
|
+
# all IDs from this page that now belong into the new page and transfer
|
89
|
+
# them.
|
90
|
+
new_page_record = IDListPageRecord.new(@page_file, max_id + 1, @max_id,
|
91
|
+
page.delete(max_id))
|
92
|
+
# Adjust the max_id of the current page.
|
93
|
+
@max_id = max_id
|
94
|
+
|
95
|
+
new_page_record
|
96
|
+
end
|
97
|
+
|
98
|
+
def values
|
99
|
+
page.values
|
100
|
+
end
|
101
|
+
|
102
|
+
def <=>(pr)
|
103
|
+
@min_id <=> pr.min_id
|
104
|
+
end
|
105
|
+
|
106
|
+
def check
|
107
|
+
unless @min_id < @max_id
|
108
|
+
raise RuntimeError, "min_id must be smaller than max_id"
|
109
|
+
end
|
110
|
+
|
111
|
+
p = page
|
112
|
+
values = p.values
|
113
|
+
unless @page_entries == values.length
|
114
|
+
raise RuntimeError, "Mismatch between node page_entries " +
|
115
|
+
"(#{@page_entries}) and number of values (#{p.values.length})"
|
116
|
+
end
|
117
|
+
|
118
|
+
values.each do |v|
|
119
|
+
if v < @min_id
|
120
|
+
raise RuntimeError, "Page value #{v} is smaller than min_id " +
|
121
|
+
"#{@min_id}"
|
122
|
+
end
|
123
|
+
if v > @max_id
|
124
|
+
raise RuntimeError, "Page value #{v} is larger than max_id #{@max_id}"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
p.check
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
def page
|
134
|
+
# The leaf pages reference the IDListPage objects only by their index.
|
135
|
+
# This method will convert the index into a reference to the actual
|
136
|
+
# object. These references should be very short-lived as a life
|
137
|
+
# reference prevents the page object from being collected.
|
138
|
+
@page_file.page(self)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
data/lib/perobs/Object.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
#
|
3
3
|
# = Object.rb -- Persistent Ruby Object Store
|
4
4
|
#
|
5
|
-
# Copyright (c) 2015, 2016 by Chris Schlaeger <chris@taskjuggler.org>
|
5
|
+
# Copyright (c) 2015, 2016, 2017 by Chris Schlaeger <chris@taskjuggler.org>
|
6
6
|
#
|
7
7
|
# MIT License
|
8
8
|
#
|
@@ -212,17 +212,7 @@ module PEROBS
|
|
212
212
|
end
|
213
213
|
|
214
214
|
def _set(attr, val)
|
215
|
-
|
216
|
-
# References to other PEROBS::Objects must be handled somewhat
|
217
|
-
# special.
|
218
|
-
if @store != val.store
|
219
|
-
PEROBS.log.fatal 'The referenced object is not part of this store'
|
220
|
-
end
|
221
|
-
elsif val.is_a?(ObjectBase)
|
222
|
-
PEROBS.log.fatal 'A PEROBS::ObjectBase object escaped! ' +
|
223
|
-
'Have you used self() instead of myself() to get the reference ' +
|
224
|
-
'of the PEROBS object that you are trying to assign here?'
|
225
|
-
end
|
215
|
+
_check_assignment_value(val)
|
226
216
|
instance_variable_set(('@' + attr.to_s).to_sym, val)
|
227
217
|
# Let the store know that we have a modified object. If we restored the
|
228
218
|
# object from the DB, we don't mark it as modified.
|
@@ -236,13 +226,26 @@ module PEROBS
|
|
236
226
|
end
|
237
227
|
|
238
228
|
def _all_attributes
|
229
|
+
# Collect all persistent attributes from this class and all
|
230
|
+
# super classes into a single Array.
|
231
|
+
attributes = []
|
232
|
+
klass = self.class
|
233
|
+
while klass && klass.respond_to?(:attributes)
|
234
|
+
if (attrs = klass.attributes)
|
235
|
+
attributes += attrs
|
236
|
+
end
|
237
|
+
klass = klass.superclass
|
238
|
+
end
|
239
|
+
|
239
240
|
# PEROBS objects that don't have persistent attributes declared don't
|
240
241
|
# really make sense.
|
241
|
-
|
242
|
+
if attributes.empty?
|
242
243
|
PEROBS.log.fatal "No persistent attributes have been declared for " +
|
243
|
-
"class #{self.class}. Use '
|
244
|
+
"class #{self.class} or any parent class. Use 'attr_persist' " +
|
245
|
+
"to declare them."
|
244
246
|
end
|
245
|
-
|
247
|
+
|
248
|
+
attributes
|
246
249
|
end
|
247
250
|
|
248
251
|
end
|
data/lib/perobs/ObjectBase.rb
CHANGED
@@ -86,6 +86,10 @@ module PEROBS
|
|
86
86
|
_referenced_object == obj
|
87
87
|
end
|
88
88
|
|
89
|
+
def eql?(obj)
|
90
|
+
_referenced_object._id == obj._id
|
91
|
+
end
|
92
|
+
|
89
93
|
# BasicObject provides a equal?() method that prevents method_missing from
|
90
94
|
# being called. So we have to pass the call manually to the referenced
|
91
95
|
# object.
|
@@ -98,6 +102,13 @@ module PEROBS
|
|
98
102
|
end
|
99
103
|
end
|
100
104
|
|
105
|
+
# To allow POXReference objects to be used as Hash keys we need to
|
106
|
+
# implement this function. Conveniently, we can just use the PEROBS object
|
107
|
+
# ID since that is unique.
|
108
|
+
def hash
|
109
|
+
@id
|
110
|
+
end
|
111
|
+
|
101
112
|
# Shortcut to access the _id() method of the referenced object.
|
102
113
|
def _id
|
103
114
|
@id
|
@@ -114,6 +125,20 @@ module PEROBS
|
|
114
125
|
# common to all classes of persistent objects.
|
115
126
|
class ObjectBase
|
116
127
|
|
128
|
+
# This is a list of the native Ruby classes that are supported for
|
129
|
+
# instance variable assignements in addition to other PEROBS objects.
|
130
|
+
if RUBY_VERSION < '2.2'
|
131
|
+
NATIVE_CLASSES = [
|
132
|
+
NilClass, Integer, Bignum, Fixnum, Float, String, Time,
|
133
|
+
TrueClass, FalseClass
|
134
|
+
]
|
135
|
+
else
|
136
|
+
NATIVE_CLASSES = [
|
137
|
+
NilClass, Integer, Float, String, Time,
|
138
|
+
TrueClass, FalseClass
|
139
|
+
]
|
140
|
+
end
|
141
|
+
|
117
142
|
attr_reader :_id, :store, :myself
|
118
143
|
|
119
144
|
# New PEROBS objects must always be created by calling # Store.new().
|
@@ -192,6 +217,25 @@ module PEROBS
|
|
192
217
|
@store.db.put_object(db_obj, @_id)
|
193
218
|
end
|
194
219
|
|
220
|
+
#
|
221
|
+
def _check_assignment_value(val)
|
222
|
+
if val.respond_to?(:is_poxreference?)
|
223
|
+
# References to other PEROBS::Objects must be handled somewhat
|
224
|
+
# special.
|
225
|
+
if @store != val.store
|
226
|
+
PEROBS.log.fatal 'The referenced object is not part of this store'
|
227
|
+
end
|
228
|
+
elsif val.is_a?(ObjectBase)
|
229
|
+
PEROBS.log.fatal 'A PEROBS::ObjectBase object escaped! ' +
|
230
|
+
'Have you used self() instead of myself() to get the reference ' +
|
231
|
+
'of the PEROBS object that you are trying to assign here?'
|
232
|
+
elsif !NATIVE_CLASSES.include?(val.class)
|
233
|
+
PEROBS.log.fatal "Assigning objects of class #{val.class} is not " +
|
234
|
+
"supported. Only PEROBS objects or one of the following classes " +
|
235
|
+
"are supported: #{NATIVE_CLASSES.join(', ')}"
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
195
239
|
# Read an raw object with the specified ID from the backing store and
|
196
240
|
# instantiate a new object of the specific type.
|
197
241
|
def ObjectBase.read(store, id)
|
@@ -213,15 +257,12 @@ module PEROBS
|
|
213
257
|
def _restore(level)
|
214
258
|
# Find the most recently stored state of this object. This could be on
|
215
259
|
# any previous stash level or in the regular object DB. If the object
|
216
|
-
# was created during the transaction, there is
|
260
|
+
# was created during the transaction, there is no previous state to
|
217
261
|
# restore to.
|
218
262
|
data = nil
|
219
263
|
if @_stash_map
|
220
264
|
(level - 1).downto(0) do |lvl|
|
221
|
-
if @_stash_map[lvl]
|
222
|
-
data = @_stash_map[lvl]
|
223
|
-
break
|
224
|
-
end
|
265
|
+
break if (data = @_stash_map[lvl])
|
225
266
|
end
|
226
267
|
end
|
227
268
|
if data
|
@@ -31,29 +31,32 @@ module PEROBS
|
|
31
31
|
|
32
32
|
class PersistentObjectCache
|
33
33
|
|
34
|
-
FLUSH_WATERMARK = 500
|
35
|
-
|
36
34
|
# This cache class manages the presence of objects that primarily live in
|
37
35
|
# a backing store but temporarily exist in memory as well. To work with
|
38
36
|
# these objects, direct references must be only very short lived. Indirect
|
39
37
|
# references can be done via a unique ID that the object must provide. Due
|
40
38
|
# to the indirect references the Ruby garbage collector can collect these
|
41
|
-
# objects
|
42
|
-
#
|
43
|
-
#
|
44
|
-
#
|
45
|
-
#
|
46
|
-
#
|
47
|
-
# @param size [Integer]
|
39
|
+
# objects. To reduce the read and write latencies of the backing store
|
40
|
+
# this class keeps a subset of the objects in memory which prevents them
|
41
|
+
# from being collected. All references to the objects must be resolved via
|
42
|
+
# the get() method to prevent duplicate instances in memory of the same
|
43
|
+
# in-store object. The cache uses a least-recently-used (LRU) scheme to
|
44
|
+
# cache objects.
|
45
|
+
# @param size [Integer] Minimum number of objects to be cached at a time
|
46
|
+
# @param flush_delay [Integer] Determines how often non-forced flushes are
|
47
|
+
# ignored in a row before the flush is really done. If flush_delay
|
48
|
+
# is smaller than 0 non-forced flushed will always be ignored.
|
48
49
|
# @param klass [Class] The class of the objects to be cached. Objects must
|
49
50
|
# provide a uid() method that returns a unique ID for every object.
|
50
51
|
# @param collection [] The object collection the objects belong to. It
|
51
52
|
# must provide a ::load method.
|
52
|
-
def initialize(size, klass, collection)
|
53
|
+
def initialize(size, flush_delay, klass, collection)
|
53
54
|
@size = size
|
54
55
|
@klass = klass
|
55
56
|
@collection = collection
|
56
|
-
@flush_counter =
|
57
|
+
@flush_delay = @flush_counter = flush_delay
|
58
|
+
@flush_times = 0
|
59
|
+
|
57
60
|
clear
|
58
61
|
end
|
59
62
|
|
@@ -61,65 +64,46 @@ module PEROBS
|
|
61
64
|
# @param object [Object] Object to cache
|
62
65
|
# @param modified [Boolean] True if the object was modified, false otherwise
|
63
66
|
def insert(object, modified = true)
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
+
unless object.is_a?(@klass)
|
68
|
+
raise ArgumentError, "You can insert only #{@klass} objects in this " +
|
69
|
+
"cache. You have tried to insert a #{object.class} instead."
|
70
|
+
end
|
71
|
+
|
72
|
+
if modified
|
73
|
+
@modified_entries[object.uid] = object
|
74
|
+
else
|
75
|
+
@unmodified_entries[object.uid % @size] = object
|
76
|
+
end
|
67
77
|
|
68
|
-
|
78
|
+
nil
|
69
79
|
end
|
70
80
|
|
71
81
|
# Retrieve a object reference from the cache.
|
72
82
|
# @param uid [Integer] uid of the object to retrieve.
|
73
|
-
|
74
|
-
|
75
|
-
|
83
|
+
# @param ref [Object] optional reference to be used by the load method
|
84
|
+
def get(uid, ref = nil)
|
85
|
+
# First check if it's a modified object.
|
86
|
+
if (object = @modified_entries[uid])
|
87
|
+
return object
|
76
88
|
end
|
77
89
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
object = ObjectSpace._id2ref(ruby_object_id)
|
82
|
-
# Let's make sure the object is really the object we are looking
|
83
|
-
# for. The GC might have recycled it already and the Ruby object ID
|
84
|
-
# could now be used for another object.
|
85
|
-
if object.is_a?(@klass) && object.uid == uid
|
86
|
-
# Let's put the object in the cache. We might need it soon again.
|
87
|
-
insert(object, false)
|
88
|
-
return object
|
89
|
-
end
|
90
|
-
rescue RangeError
|
91
|
-
# Due to a race condition the object can still be in the
|
92
|
-
# @in_memory_objects list but has been collected already by the Ruby
|
93
|
-
# GC. In that case we need to load it again. In this case the
|
94
|
-
# _collect() call will happen much later, potentially after we have
|
95
|
-
# registered a new object with the same ID.
|
96
|
-
@in_memory_objects.delete(uid)
|
97
|
-
end
|
90
|
+
# Then check the unmodified object list.
|
91
|
+
if (object = @unmodified_entries[uid % @size]) && object.uid == uid
|
92
|
+
return object
|
98
93
|
end
|
99
94
|
|
100
|
-
|
95
|
+
# If we don't have it in memory we need to load it.
|
96
|
+
@klass::load(@collection, uid, ref)
|
101
97
|
end
|
102
98
|
|
103
99
|
# Remove a object from the cache.
|
104
100
|
# @param uid [Integer] unique ID of object to remove.
|
105
101
|
def delete(uid)
|
106
|
-
|
107
|
-
# access it anymore.
|
108
|
-
@in_memory_objects.delete(uid)
|
109
|
-
|
110
|
-
@lines[uid % @size].delete(uid)
|
111
|
-
end
|
102
|
+
@modified_entries.delete(uid)
|
112
103
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
# @param uid [Integer] Object address of the object to remove from
|
117
|
-
# the list
|
118
|
-
# @param ruby_object_id [Integer] The Ruby object ID of the collected
|
119
|
-
# object
|
120
|
-
def _collect(address, ruby_object_id)
|
121
|
-
if @in_memory_objects[id] == ruby_object_id
|
122
|
-
@in_memory_objects.delete(address)
|
104
|
+
index = uid % @size
|
105
|
+
if (object = @unmodified_entries[index]) && object.uid == uid
|
106
|
+
@unmodified_entries[index] = nil
|
123
107
|
end
|
124
108
|
end
|
125
109
|
|
@@ -127,24 +111,29 @@ module PEROBS
|
|
127
111
|
# all modified objects will be written.
|
128
112
|
# @param now [Boolean]
|
129
113
|
def flush(now = false)
|
130
|
-
if now || (@flush_counter -= 1) <= 0
|
131
|
-
@
|
132
|
-
|
114
|
+
if now || (@flush_delay >= 0 && (@flush_counter -= 1) <= 0)
|
115
|
+
@modified_entries.each do |id, object|
|
116
|
+
object.save
|
117
|
+
# Add the object to the unmodified object cache. We might still need
|
118
|
+
# it again soon.
|
119
|
+
@unmodified_entries[object.uid % @size] = object
|
120
|
+
end
|
121
|
+
@modified_entries = ::Hash.new
|
122
|
+
@flush_counter = @flush_delay
|
133
123
|
end
|
124
|
+
@flush_times += 1
|
134
125
|
end
|
135
126
|
|
136
127
|
# Remove all entries from the cache.
|
137
128
|
def clear
|
138
|
-
#
|
139
|
-
#
|
140
|
-
|
141
|
-
|
142
|
-
#
|
143
|
-
#
|
144
|
-
|
145
|
-
|
146
|
-
# also store the modified/not-modified state.
|
147
|
-
@lines = ::Array.new(@size) { |i| PersistentObjectCacheLine.new }
|
129
|
+
# This Array stores all unmodified entries. It has a fixed size and uses
|
130
|
+
# a % operation to compute the index from the object ID.
|
131
|
+
@unmodified_entries = ::Array.new(@size)
|
132
|
+
|
133
|
+
# This Hash stores all modified entries. It can grow and shrink as
|
134
|
+
# needed. A flush operation writes all modified objects into the backing
|
135
|
+
# store.
|
136
|
+
@modified_entries = ::Hash.new
|
148
137
|
end
|
149
138
|
|
150
139
|
end
|
@@ -34,30 +34,42 @@ module PEROBS
|
|
34
34
|
class Entry < Struct.new(:obj, :modified)
|
35
35
|
end
|
36
36
|
|
37
|
-
|
37
|
+
# This defines the minimum size of the cache line. If it is too large, the
|
38
|
+
# time to find an entry will grow too much. If it is too small the number
|
39
|
+
# of cache lines will be too large and create more store overhead. By
|
40
|
+
# running benchmarks it turned out that 8 is a pretty good compromise.
|
41
|
+
WATERMARK = 8
|
38
42
|
|
39
43
|
def initialize
|
40
44
|
@entries = []
|
41
45
|
end
|
42
46
|
|
43
47
|
def insert(object, modified)
|
44
|
-
@entries.
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
48
|
+
if (index = @entries.find_index{ |e| e.obj.uid == object.uid })
|
49
|
+
# We have found and removed an existing entry for this particular
|
50
|
+
# object. If the modified flag is set, ensure that the entry has it
|
51
|
+
# set as well.
|
52
|
+
entry = @entries.delete_at(index)
|
53
|
+
entry.modified = true if modified && !entry.modified
|
54
|
+
else
|
55
|
+
# There is no existing entry for this object. Create a new one.
|
56
|
+
entry = Entry.new(object, modified)
|
49
57
|
end
|
50
58
|
|
51
|
-
# Insert the
|
52
|
-
@entries.unshift(
|
59
|
+
# Insert the entry at the beginning of the line.
|
60
|
+
@entries.unshift(entry)
|
53
61
|
end
|
54
62
|
|
55
63
|
def get(uid)
|
56
|
-
@entries.
|
57
|
-
|
64
|
+
if (index = @entries.find_index{ |e| e.obj.uid == uid })
|
65
|
+
if index > 0
|
66
|
+
# Move the entry to the front.
|
67
|
+
@entries.unshift(@entries.delete_at(index))
|
68
|
+
end
|
69
|
+
@entries.first
|
70
|
+
else
|
71
|
+
nil
|
58
72
|
end
|
59
|
-
|
60
|
-
nil
|
61
73
|
end
|
62
74
|
|
63
75
|
# Delete the entry that matches the given UID
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
#
|
3
|
+
# = ProgressMeter.rb -- Persistent Ruby Object Store
|
4
|
+
#
|
5
|
+
# Copyright (c) 2018 by Chris Schlaeger <chris@taskjuggler.org>
|
6
|
+
#
|
7
|
+
# MIT License
|
8
|
+
#
|
9
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
10
|
+
# a copy of this software and associated documentation files (the
|
11
|
+
# "Software"), to deal in the Software without restriction, including
|
12
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
13
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
14
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
15
|
+
# the following conditions:
|
16
|
+
#
|
17
|
+
# The above copyright notice and this permission notice shall be
|
18
|
+
# included in all copies or substantial portions of the Software.
|
19
|
+
#
|
20
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
21
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
22
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
23
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
24
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
25
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
26
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
27
|
+
|
28
|
+
require 'time'
|
29
|
+
|
30
|
+
module PEROBS
|
31
|
+
|
32
|
+
# This is the base class for all ProgressMeter classes. It only logs into
|
33
|
+
# the PEROBS log. You need to create a derived class that overloads
|
34
|
+
# print_bar() and print_time() to provide more fancy outputs.
|
35
|
+
class ProgressMeter
|
36
|
+
|
37
|
+
def initialize
|
38
|
+
@name = nil
|
39
|
+
@max_value = nil
|
40
|
+
@current_value = nil
|
41
|
+
@start_time = nil
|
42
|
+
@end_time = nil
|
43
|
+
end
|
44
|
+
|
45
|
+
def start(name, max_value)
|
46
|
+
@name = name
|
47
|
+
unless max_value >= 0
|
48
|
+
raise ArgumentError, "Maximum value (#{max_value}) must be larger " +
|
49
|
+
"or equal to 0"
|
50
|
+
end
|
51
|
+
@max_value = max_value
|
52
|
+
@current_value = 0
|
53
|
+
@start_time = Time.now
|
54
|
+
@end_time = nil
|
55
|
+
print_bar
|
56
|
+
|
57
|
+
if block_given?
|
58
|
+
yield(self)
|
59
|
+
done
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def update(value)
|
64
|
+
return unless (value_i = value.to_i) > @current_value
|
65
|
+
|
66
|
+
@current_value = value_i
|
67
|
+
print_bar
|
68
|
+
end
|
69
|
+
|
70
|
+
def done
|
71
|
+
@end_time = Time.now
|
72
|
+
print_time
|
73
|
+
PEROBS.log.info "#{@name} completed in " +
|
74
|
+
secsToHMS(@end_time - @start_time)
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def print_bar
|
80
|
+
end
|
81
|
+
|
82
|
+
def print_time
|
83
|
+
end
|
84
|
+
|
85
|
+
def secsToHMS(secs)
|
86
|
+
secs = secs.to_i
|
87
|
+
s = secs % 60
|
88
|
+
mins = secs / 60
|
89
|
+
m = mins % 60
|
90
|
+
h = mins / 60
|
91
|
+
"#{h}:#{'%02d' % m}:#{'%02d' % s}"
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|