perobs 2.1.1 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 231bcc1e34113cad62eefa5bdac4b84d18937638
4
- data.tar.gz: dfe932b0e210297a4a24a5a2c076d52db04c5ff6
3
+ metadata.gz: 862c67d14741c0fe0145af7c5eb1c1c147ebe189
4
+ data.tar.gz: c88f1c19c16db2c6b3f2fd046cbb4d41e9fce2c3
5
5
  SHA512:
6
- metadata.gz: 433b39b2568538a634e53adc0bf6ae2ac8f4f8d6a24178b406d5b6de92981d141f6a29ce6a344388eedc5b1c0632e02d4bef6c2996c18650b7805501de29718a
7
- data.tar.gz: 78904802a71bb2b958f79eebb70c945d5506267bf83c1e921de8ec4478a4e88e5fcfe9bf5409beecda8dd7fb4ea6358a366e3640dbd0965365a387f9b324d4e0
6
+ metadata.gz: 11fd013bb0da3088a4ed88dfac390aac5b8cef823fd2bdeb06ab8c7ad4fedbde997365e0f135bae63b72acf5ee9ad3ac10f5640ac6b703785876514be3a480f1
7
+ data.tar.gz: d13b9dd12084975bc5d6e84e6fcc0f91b3f455e56d4532c7a37983c53dd1054db211e8244c8317da5f0cb85e32ce4ce01564e9a3684b442c01bfdf889735a393
data/README.md CHANGED
@@ -32,8 +32,17 @@ When you derive your own class from PEROBS::Object you need to
32
32
  specify which instance variables should be persistent. By using
33
33
  po_attr you can provide a list of symbols that describe the instance
34
34
  variables to persist. This will also create getter and setter methods
35
- for these instance varables. You can use attr_init in the constructor
36
- to define default values for your persistent objects.
35
+ for these instance varables. You can set default values in the
36
+ constructor . The constructor of PEROBS::ObjectBase derived objects
37
+ must have at least one argument. The first argument is a PEROBS
38
+ internal object that must be passed to super() as first thing in
39
+ initialize(). You can have other arguments if needed. Be aware that
40
+ initialize() is not called when objects are restored from the
41
+ database! You can define a restore() method to deal with object
42
+ initialization or modification after restore from database. restore()
43
+ is also the proper place to initialize non-persistent instance
44
+ variables. New objects are created via Store.new() so you cannot call
45
+ the constructor directly in your code.
37
46
 
38
47
  To start off you must create at least one PEROBS::Store object that
39
48
  owns your persistent objects. The store provides the persistent
@@ -52,10 +61,10 @@ almost every Ruby data type. YAML is much slower than JSON and Marshal
52
61
  is not guaranteed to be compatible between Ruby versions.
53
62
 
54
63
  Once you have created a store you can assign objects to it. All
55
- persistent objects must be created with @store.new(). This is
64
+ persistent objects must be created with Store.new(). This is
56
65
  necessary as you will only deal with proxy objects in your code.
57
66
  Except for the member methods, you will never deal with the objects
58
- directly. Instead @store.new() returns a POXReference object that acts
67
+ directly. Instead Store.new() returns a POXReference object that acts
59
68
  as a transparent proxy. This proxy is needed as your code never knows
60
69
  if the actual object is really loaded into the memory or not. PEROBS
61
70
  will handle this transparently for you.
@@ -85,13 +94,19 @@ class Person < PEROBS::Object
85
94
 
86
95
  po_attr :name, :mother, :father, :kids, :spouse, :status
87
96
 
88
- def initialize(store, name)
89
- super
97
+ def initialize(p, name)
98
+ super(p)
90
99
  attr_init(:name, name)
91
100
  attr_init(:kids, store.new(PEROBS::Array))
92
101
  attr_init(:status, :single)
93
102
  end
94
103
 
104
+ def restore
105
+ # Use block version of attr_init() to avoid creating unneded
106
+ # objects. The block is only called when @father doesn't exist yet.
107
+ attr_init(:father) do { store.new(Person, 'Dad') }
108
+ end
109
+
95
110
  def merry(spouse)
96
111
  self.spouse = spouse
97
112
  self.status = :married
@@ -148,6 +163,12 @@ when it detects such objects. Just remember to use myself() instead of
148
163
  self() if you want to pass a reference to the current persistent
149
164
  object to another object.
150
165
 
166
+ ### Caveats and known issues
167
+
168
+ PEROBS is currently not thread-safe. You cannot simultaneously access
169
+ the database from multiple application. You must provide your own
170
+ locking mechanism to prevent this from happening.
171
+
151
172
  ## Installation
152
173
 
153
174
  Add this line to your application's Gemfile:
data/lib/perobs/Array.rb CHANGED
@@ -79,13 +79,16 @@ module PEROBS
79
79
  # New PEROBS objects must always be created by calling # Store.new().
80
80
  # PEROBS users should never call this method or equivalents of derived
81
81
  # methods directly.
82
- # @param store [Store] The Store this hash is stored in
82
+ # @param p [PEROBS::Handle] PEROBS handle
83
83
  # @param size [Fixnum] The requested size of the Array
84
84
  # @param default [Any] The default value that is returned when no value is
85
85
  # stored for a specific key.
86
- def initialize(store, size = 0, default = nil)
87
- super(store)
86
+ def initialize(p, size = 0, default = nil)
87
+ super(p)
88
88
  @data = ::Array.new(size, default)
89
+
90
+ # Ensure that the newly created object will be pushed into the database.
91
+ @store.cache.cache_write(self)
89
92
  end
90
93
 
91
94
  # Return a list of all object IDs of all persistend objects that this Array
@@ -118,7 +118,7 @@ module PEROBS
118
118
 
119
119
  unless found
120
120
  raise ArgumentError,
121
- "Cannot find an entry for ID #{'%016X' % id} to mark"
121
+ "Cannot find an entry for ID #{'%016X' % id} #{id} to mark"
122
122
  end
123
123
 
124
124
  write_index
@@ -126,12 +126,15 @@ module PEROBS
126
126
 
127
127
  # Check if the entry for a given ID is marked.
128
128
  # @param id [Fixnum or Bignum] ID of the entry
129
+ # @param ignore_errors [Boolean] If set to true no errors will be raised
130
+ # for non-existing objects.
129
131
  # @return [TrueClass or FalseClass] true if marked, false otherwise
130
- def is_marked?(id)
132
+ def is_marked?(id, ignore_errors = false)
131
133
  @entries.each do |entry|
132
134
  return entry[MARKED] != 0 if entry[ID] == id
133
135
  end
134
136
 
137
+ return false if ignore_errors
135
138
  raise ArgumentError,
136
139
  "Cannot find an entry for ID #{'%016X' % id} to check"
137
140
  end
@@ -266,7 +269,10 @@ module PEROBS
266
269
 
267
270
  # Create a new entry and insert it. The order must match the above
268
271
  # defined constants!
269
- entry = [ id, bytes, best_fit_start || end_of_last_entry, 0 ]
272
+ # Object reads can trigger creation of new objects. As the marking
273
+ # process triggers reads as well, all newly created objects are always
274
+ # marked to prevent them from being collected right after creation.
275
+ entry = [ id, bytes, best_fit_start || end_of_last_entry, 1 ]
270
276
  @entries.insert(best_fit_index, entry)
271
277
  @entries_by_id[id] = entry
272
278
 
@@ -173,8 +173,10 @@ module PEROBS
173
173
 
174
174
  # Check if the object is marked.
175
175
  # @param id [Fixnum or Bignum] ID of the object to check
176
- def is_marked?(id)
177
- (blob = find_blob(id)) && blob.is_marked?(id)
176
+ # @param ignore_errors [Boolean] If set to true no errors will be raised
177
+ # for non-existing objects.
178
+ def is_marked?(id, ignore_errors = false)
179
+ (blob = find_blob(id)) && blob.is_marked?(id, ignore_errors)
178
180
  end
179
181
 
180
182
  # Basic consistency check.
data/lib/perobs/Cache.rb CHANGED
@@ -69,7 +69,9 @@ module PEROBS
69
69
  # If this condition triggers, we have a bug in the library.
70
70
  raise RuntimeError, "POXReference objects should never be cached"
71
71
  end
72
+
72
73
  if @transaction_stack.empty?
74
+ # We are not in transaction mode.
73
75
  idx = index(obj)
74
76
  if (old_obj = @writes[idx]) && old_obj._id != obj._id
75
77
  # There is another old object using this cache slot. Before we can
@@ -83,45 +85,29 @@ module PEROBS
83
85
  cache_read(obj)
84
86
  # Push the reference of the modified object into the write buffer for
85
87
  # this transaction level.
86
- unless @transaction_stack.last.include?(obj)
87
- @transaction_stack.last << obj
88
+ unless @transaction_stack.last.include?(obj._id)
89
+ @transaction_stack.last << obj._id
90
+ @transaction_objects[obj._id] = obj
88
91
  end
89
92
  end
90
93
  end
91
94
 
92
- # Remove an object from the write cache. This will prevent a modified
93
- # object from being written to the back-end store.
94
- def unwrite(obj)
95
- if @transaction_stack.empty?
96
- idx = index(obj)
97
- if (old_obj = @writes[idx]).nil? || old_obj._id != obj._id
98
- raise RuntimeError, "Object to unwrite is not in cache"
99
- end
100
- @writes[idx] = nil
101
- else
102
- unless @transaction_stack.last.include?(obj)
103
- raise RuntimeError, 'unwrite failed'
104
- end
105
- @transaction_stack.last.delete(obj)
106
- end
107
- end
108
-
109
95
  # Return the PEROBS::Object with the specified ID or nil if not found.
110
96
  # @param id [Fixnum or Bignum] ID of the cached PEROBS::ObjectBase
111
- def object_by_id(id)
112
- idx = id & @mask
113
- # The index is just a hash. We still need to check if the object IDs are
114
- # actually the same before we can return the object.
115
- if (obj = @writes[idx]) && obj._id == id
116
- # The object was in the write cache.
117
- return obj
118
- elsif (obj = @reads[idx]) && obj._id == id
119
- # The object was in the read cache.
120
- return obj
121
- end
122
-
123
- nil
124
- end
97
+ #def object_by_id(id)
98
+ # idx = id & @mask
99
+ # # The index is just a hash. We still need to check if the object IDs are
100
+ # # actually the same before we can return the object.
101
+ # if (obj = @writes[idx]) && obj._id == id
102
+ # # The object was in the write cache.
103
+ # return obj
104
+ # elsif (obj = @reads[idx]) && obj._id == id
105
+ # # The object was in the read cache.
106
+ # return obj
107
+ # end
108
+
109
+ # nil
110
+ #end
125
111
 
126
112
  # Flush all pending writes to the persistant storage back-end.
127
113
  def flush
@@ -140,12 +126,14 @@ module PEROBS
140
126
  # active, the write cached is flushed before the transaction is started.
141
127
  def begin_transaction
142
128
  if @transaction_stack.empty?
143
- # This is the top-level transaction. Flush the write buffer to save
144
- # the current state of all objects.
129
+ # The new transaction is the top-level transaction. Flush the write
130
+ # buffer to save the current state of all objects.
145
131
  flush
146
132
  else
147
- @transaction_stack.last.each do |o|
148
- o._stash(@transaction_stack.length - 1)
133
+ # Save a copy of all objects that were modified during the enclosing
134
+ # transaction.
135
+ @transaction_stack.last.each do |id|
136
+ @transaction_objects[id]._stash(@transaction_stack.length - 1)
149
137
  end
150
138
  end
151
139
  # Push a transaction buffer onto the transaction stack. This buffer will
@@ -163,7 +151,8 @@ module PEROBS
163
151
  when 1
164
152
  # All transactions completed successfully. Write all modified objects
165
153
  # into the backend storage.
166
- @transaction_stack.pop.each { |o| o._sync }
154
+ @transaction_stack.pop.each { |id| @transaction_objects[id]._sync }
155
+ @transaction_objects = ::Hash.new
167
156
  else
168
157
  # A nested transaction completed successfully. We add the list of
169
158
  # modified objects to the list of the enclosing transaction.
@@ -182,7 +171,9 @@ module PEROBS
182
171
  if @transaction_stack.empty?
183
172
  raise RuntimeError, 'No ongoing transaction to abort'
184
173
  end
185
- @transaction_stack.pop.each { |o| o._restore(@transaction_stack.length) }
174
+ @transaction_stack.pop.each do |id|
175
+ @transaction_objects[id]._restore(@transaction_stack.length)
176
+ end
186
177
  end
187
178
 
188
179
  # Clear all cached entries. You must call flush before calling this
@@ -193,7 +184,8 @@ module PEROBS
193
184
  # the read or write cache Arrays.
194
185
  @reads = ::Array.new(2 ** @bits)
195
186
  @writes = ::Array.new(2 ** @bits)
196
- @transaction_stack = []
187
+ @transaction_stack = ::Array.new
188
+ @transaction_objects = ::Hash.new
197
189
  end
198
190
 
199
191
  # Don't include the cache buffers in output of other objects that
@@ -0,0 +1,44 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # = Handle.rb -- Persistent Ruby Object Store
4
+ #
5
+ # Copyright (c) 2015, 2016 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 Handle is the first paramter of the initialize() method of every
31
+ # PEROBS object. By convention this parameter should be called 'p'.
32
+ class Handle
33
+
34
+ attr_reader :store, :id
35
+
36
+ def initialize(store, id)
37
+ @store = store
38
+ @id = id
39
+ end
40
+
41
+ end
42
+
43
+ end
44
+
data/lib/perobs/Hash.rb CHANGED
@@ -77,13 +77,16 @@ module PEROBS
77
77
  # New PEROBS objects must always be created by calling # Store.new().
78
78
  # PEROBS users should never call this method or equivalents of derived
79
79
  # methods directly.
80
- # @param store [Store] The Store this hash is stored in
80
+ # @param p [PEROBS::Handle] PEROBS handle
81
81
  # @param default [Any] The default value that is returned when no value is
82
82
  # stored for a specific key.
83
- def initialize(store, default = nil)
84
- super(store)
83
+ def initialize(p, default = nil)
84
+ super(p)
85
85
  @default = nil
86
86
  @data = {}
87
+
88
+ # Ensure that the newly created object will be pushed into the database.
89
+ @store.cache.cache_write(self)
87
90
  end
88
91
 
89
92
  # Return a list of all object IDs of all persistend objects that this Hash
data/lib/perobs/Object.rb CHANGED
@@ -79,14 +79,17 @@ module PEROBS
79
79
  # New PEROBS objects must always be created by calling # Store.new().
80
80
  # PEROBS users should never call this method or equivalents of derived
81
81
  # methods directly.
82
- def initialize(store)
83
- super
82
+ # @param p [PEROBS::Handle] PEROBS handle
83
+ def initialize(p)
84
+ super(p)
85
+
86
+ # Ensure that the newly created object will be pushed into the database.
87
+ @store.cache.cache_write(self)
84
88
  end
85
89
 
86
- # Initialize the specified attribute _attr_ with the value _val_ unless
87
- # the attribute has been initialized already. Use this method in the class
88
- # constructor to avoid overwriting values that have been set when the
89
- # object was reconstructed from the store.
90
+ # This method is deprecated. It will be removed in future versions. Please
91
+ # use attr_init() instead.
92
+ # the database.
90
93
  # @param attr [Symbol] Name of the attribute
91
94
  # @param val [Any] Value to be set
92
95
  # @return [true|false] True if the value was initialized, otherwise false.
@@ -99,6 +102,26 @@ module PEROBS
99
102
  false
100
103
  end
101
104
 
105
+ # Use this method to initialize persistent attributes in the restore()
106
+ # method that have not yet been initialized. This is the case when the
107
+ # object was saved with an earlier version of the program that did not yet
108
+ # have the instance variable. If you want to assign another PEROBS object
109
+ # to the variable you should use the block variant to avoid unnecessary
110
+ # creation of PEROBS object that later need to be collected again.
111
+ def attr_init(attr, val = nil, &block)
112
+ if _all_attributes.include?(attr)
113
+ unless instance_variable_defined?('@' + attr.to_s)
114
+ _set(attr, block_given? ? yield : val)
115
+ end
116
+ return true
117
+ else
118
+ raise ArgumentError, "'#{attr}' is not a defined persistent " +
119
+ "attribute of class #{self.class}"
120
+ end
121
+
122
+ false
123
+ end
124
+
102
125
  # Call this method to manually mark the object as modified. This is
103
126
  # necessary if you are using the '@' notation to access instance variables
104
127
  # during assignment operations (=, +=, -=, etc.). To avoid having to call
@@ -154,13 +177,13 @@ module PEROBS
154
177
  # Textual dump for debugging purposes
155
178
  # @return [String]
156
179
  def inspect
157
- "{\n" +
180
+ "#{to_s}:#{@_id}\n{\n" +
158
181
  _all_attributes.map do |attr|
159
182
  ivar = ('@' + attr.to_s).to_sym
160
- if (value = instance_variable_get(ivar)).respond_to?('is_poxreference?')
161
- " #{attr}=>#{value.class}:#{value._id}"
183
+ if (value = instance_variable_get(ivar)).respond_to?(:is_poxreference?)
184
+ " #{attr} => <PEROBS::ObjectBase:#{value._id}>"
162
185
  else
163
- " #{attr}=>#{value}"
186
+ " #{attr} => #{value.inspect}"
164
187
  end
165
188
  end.join(",\n") +
166
189
  "\n}\n"
@@ -195,7 +218,8 @@ module PEROBS
195
218
  'you are trying to assign here?'
196
219
  end
197
220
  instance_variable_set(('@' + attr.to_s).to_sym, val)
198
- # Let the store know that we have a modified object.
221
+ # Let the store know that we have a modified object. If we restored the
222
+ # object from the DB, we don't mark it as modified.
199
223
  mark_as_modified
200
224
 
201
225
  val
@@ -120,23 +120,25 @@ module PEROBS
120
120
  # New PEROBS objects must always be created by calling # Store.new().
121
121
  # PEROBS users should never call this method or equivalents of derived
122
122
  # methods directly.
123
- def initialize(store)
124
- @store = store
125
- unless @store.object_creation_in_progress
126
- ::Kernel.raise ::RuntimeError,
127
- "All PEROBS objects must exclusively be created by calling " +
128
- "Store.new(). Never call the object constructor directly."
129
- end
130
- @_id = @store._new_id
123
+ # @param p [PEROBS::Handle] PEROBS handle
124
+ def initialize(p)
125
+ _initialize(p)
126
+ end
127
+
128
+ # This is the real code for initialize. It is called from initialize() but
129
+ # also when we restore objects from the database. In the later case, we
130
+ # don't call the regular constructors. But this code must be exercised on
131
+ # object creation with new() and on restore from DB.
132
+ # param p [PEROBS::Handle] PEROBS handle
133
+ def _initialize(p)
134
+ @store = p.store
135
+ @_id = p.id
131
136
  @store._register_in_memory(self, @_id)
132
137
  ObjectSpace.define_finalizer(self, ObjectBase._finalize(@store, @_id))
133
138
  @_stash_map = nil
134
139
  # Allocate a proxy object for this object. User code should only operate
135
140
  # on this proxy, never on self.
136
141
  @myself = POXReference.new(@store, @_id)
137
-
138
- # Let the store know that we have a modified object.
139
- @store.cache.cache_write(self)
140
142
  end
141
143
 
142
144
  # This method generates the destructor for the objects of this class. It
@@ -150,8 +152,10 @@ module PEROBS
150
152
  # This method can be overloaded by derived classes to do some massaging on
151
153
  # the data after it has been restored from the database. This could either
152
154
  # be some sanity check or code to migrate the object from one version to
153
- # another.
154
- def post_restore
155
+ # another. It is also the right place to initialize non-persistent
156
+ # instance variables as initialize() will only be called when objects are
157
+ # created for the first time.
158
+ def restore
155
159
  end
156
160
 
157
161
  # Two objects are considered equal if their object IDs are the same.
@@ -181,9 +185,10 @@ module PEROBS
181
185
 
182
186
  klass = store.class_map.id_to_class(db_obj['class_id'])
183
187
  # Call the constructor of the specified class.
184
- obj = store._construct_po(Object.const_get(klass), id)
188
+ obj = Object.const_get(klass).allocate
189
+ obj._initialize(Handle.new(store, id))
185
190
  obj._deserialize(db_obj['data'])
186
- obj.post_restore
191
+ obj.restore
187
192
 
188
193
  obj
189
194
  end
@@ -195,22 +200,21 @@ module PEROBS
195
200
  # any previous stash level or in the regular object DB. If the object
196
201
  # was created during the transaction, there is not previous state to
197
202
  # restore to.
198
- id = nil
203
+ data = nil
199
204
  if @_stash_map
200
205
  (level - 1).downto(0) do |lvl|
201
206
  if @_stash_map[lvl]
202
- id = @_stash_map[lvl]
207
+ data = @_stash_map[lvl]
203
208
  break
204
209
  end
205
210
  end
206
211
  end
207
- unless id
208
- if @store.db.include?(@_id)
209
- id = @_id
210
- end
211
- end
212
- if id
213
- db_obj = store.db.get_object(id)
212
+ if data
213
+ # We have a stashed version that we can restore from.
214
+ _deserialize(data)
215
+ elsif @store.db.include?(@_id)
216
+ # We have no stashed version but can restore from the database.
217
+ db_obj = store.db.get_object(@_id)
214
218
  _deserialize(db_obj['data'])
215
219
  end
216
220
  end
@@ -219,25 +223,9 @@ module PEROBS
219
223
  # back-end. The object gets a new ID that is stored in @_stash_map to map
220
224
  # the stash ID back to the original data.
221
225
  def _stash(level)
222
- db_obj = {
223
- 'class' => self.class.to_s,
224
- 'data' => _serialize
225
- }
226
- @_stash_map = [] unless @_stash_map
226
+ @_stash_map ||= ::Array.new
227
227
  # Get a new ID to store this version of the object.
228
- @_stash_map[level] = stash_id = @store._new_id
229
- @store.db.put_object(db_obj, stash_id)
230
- end
231
-
232
- # Library internal method. Do not use outside of this library.
233
- # @private
234
- def _change_id(id)
235
- # Unregister the object with the old ID from the write cache to prevent
236
- # cache corruption. The objects are index by ID in the cache.
237
- @store.cache.unwrite(self)
238
- @store._collect(@_id)
239
- @store._register_in_memory(self, id)
240
- @_id = id
228
+ @_stash_map[level] = _serialize
241
229
  end
242
230
 
243
231
  end
data/lib/perobs/Store.rb CHANGED
@@ -28,6 +28,7 @@
28
28
  require 'set'
29
29
  require 'weakref'
30
30
 
31
+ require 'perobs/Handle'
31
32
  require 'perobs/Cache'
32
33
  require 'perobs/ClassMap'
33
34
  require 'perobs/BTreeDB'
@@ -38,6 +39,9 @@ require 'perobs/Array'
38
39
  # PErsistent Ruby OBject Store
39
40
  module PEROBS
40
41
 
42
+ Statistics = Struct.new(:in_memory_objects, :root_objects,
43
+ :marked_objects, :swept_objects)
44
+
41
45
  # PEROBS::Store is a persistent storage system for Ruby objects. Regular
42
46
  # Ruby objects are transparently stored in a back-end storage and retrieved
43
47
  # when needed. It features a garbage collector that removes all objects that
@@ -64,8 +68,8 @@ module PEROBS
64
68
  #
65
69
  # po_attr :name, :mother, :father, :kids
66
70
  #
67
- # def initialize(store, name)
68
- # super
71
+ # def initialize(cf, name)
72
+ # super(cf)
69
73
  # attr_init(:name, name)
70
74
  # attr_init(:kids, @store.new(PEROBS::Array))
71
75
  # end
@@ -89,7 +93,7 @@ module PEROBS
89
93
  #
90
94
  class Store
91
95
 
92
- attr_reader :db, :cache, :class_map, :object_creation_in_progress
96
+ attr_reader :db, :cache, :class_map
93
97
 
94
98
  # Create a new Store.
95
99
  # @param data_base [String] the name of the database
@@ -125,14 +129,14 @@ module PEROBS
125
129
  # Create a map that can translate classes to numerical IDs and vice
126
130
  # versa.
127
131
  @class_map = ClassMap.new(@db)
128
- # This flag is used to check that PEROBS objects are only created via
129
- # the Store.new() call by PEROBS users.
130
- @object_creation_in_progress = false
131
132
 
132
133
  # List of PEROBS objects that are currently available as Ruby objects
133
134
  # hashed by their ID.
134
135
  @in_memory_objects = {}
135
136
 
137
+ # This objects keeps some counters of interest.
138
+ @stats = Statistics.new
139
+
136
140
  # The Cache reduces read and write latencies by keeping a subset of the
137
141
  # objects in memory.
138
142
  @cache = Cache.new(options[:cache_bits] || 16)
@@ -141,8 +145,7 @@ module PEROBS
141
145
  unless (@root_objects = object_by_id(0))
142
146
  # The root object hash always has the object ID 0.
143
147
  @root_objects = _construct_po(Hash, 0)
144
- # The ID change removes it from the write cache. We need to add it
145
- # again.
148
+ # Mark the root_objects object as modified.
146
149
  @cache.cache_write(@root_objects)
147
150
  end
148
151
  end
@@ -151,29 +154,29 @@ module PEROBS
151
154
  # this Store.
152
155
  # @param klass [Class] The class of the object you want to create. This
153
156
  # must be a derivative of ObjectBase.
154
- # @param *args Optional list of other arguments that are passed to the
157
+ # @param args Optional list of other arguments that are passed to the
155
158
  # constructor of the specified class.
156
159
  # @return [POXReference] A reference to the newly created object.
157
160
  def new(klass, *args)
158
- _construct_po(klass, nil, *args).myself
161
+ unless klass.is_a?(BasicObject)
162
+ raise ArgumentError, "#{klass} is not a BasicObject derivative"
163
+ end
164
+
165
+ obj = _construct_po(klass, _new_id, *args)
166
+ # Mark the new object as modified so it gets pushed into the database.
167
+ @cache.cache_write(obj)
168
+ # Return a POXReference proxy for the newly created object.
169
+ obj.myself
159
170
  end
160
171
 
161
172
  # For library internal use only!
162
173
  # This method will create a new PEROBS object.
163
174
  # @param klass [BasicObject] Class of the object to create
164
- # @param id [Fixnum, Bignum or nil] Requested object ID or nil
165
- # @param *args [Array] Arguments to pass to the object constructor.
175
+ # @param id [Fixnum, Bignum] Requested object ID
176
+ # @param args [Array] Arguments to pass to the object constructor.
166
177
  # @return [BasicObject] Newly constructed PEROBS object
167
178
  def _construct_po(klass, id, *args)
168
- unless klass.is_a?(BasicObject)
169
- raise ArgumentError, "#{klass} is not a BasicObject derivative"
170
- end
171
- @object_creation_in_progress = true
172
- obj = klass.new(self, *args)
173
- @object_creation_in_progress = false
174
- # If a specific object ID was requested we need to set it now.
175
- obj._change_id(id) if id
176
- obj
179
+ klass.new(Handle.new(self, id), *args)
177
180
  end
178
181
 
179
182
  # Delete the entire store. The store is no longer usable after this
@@ -230,7 +233,7 @@ module PEROBS
230
233
  # needed.
231
234
  def sync
232
235
  if @cache.in_transaction?
233
- raise RuntimeError, 'You cannot call sync during a transaction'
236
+ raise RuntimeError, 'You cannot call sync() during a transaction'
234
237
  end
235
238
  @cache.flush
236
239
  end
@@ -241,6 +244,9 @@ module PEROBS
241
244
  # method periodically.
242
245
  # @return [Fixnum] The number of collected objects
243
246
  def gc
247
+ if @cache.in_transaction?
248
+ raise RuntimeError, 'You cannot call gc() during a transaction'
249
+ end
244
250
  sync
245
251
  mark
246
252
  sweep
@@ -282,7 +288,7 @@ module PEROBS
282
288
  # @param repair [TrueClass/FalseClass] true if a repair attempt should be
283
289
  # made.
284
290
  # @return [Fixnum] The number of references to bad objects found.
285
- def check(repair = true)
291
+ def check(repair = false)
286
292
  # All objects must have in-db version.
287
293
  sync
288
294
  # Run basic consistency checks first.
@@ -325,16 +331,18 @@ module PEROBS
325
331
  # Start with the object 0 and the indexes of the root objects. Push them
326
332
  # onto the work stack.
327
333
  stack = [ 0 ] + @root_objects.values
328
- stack.each { |id| @db.mark(id) }
329
334
  while !stack.empty?
330
335
  # Get an object index from the stack.
331
- obj = object_by_id(id = stack.pop)
332
- yield(POXReference.new(self, id)) if block_given?
333
- obj._referenced_object_ids.each do |id|
334
- unless @db.is_marked?(id)
335
- @db.mark(id)
336
- stack << id
337
- end
336
+ unless (obj = object_by_id(id = stack.pop))
337
+ raise RuntimeError, "Database is corrupted. Object with ID #{id} " +
338
+ "not found."
339
+ end
340
+ # Mark the object so it will never be pushed to the stack again.
341
+ @db.mark(id)
342
+ yield(obj.myself) if block_given?
343
+ # Push the IDs of all unmarked referenced objects onto the stack
344
+ obj._referenced_object_ids.each do |r_id|
345
+ stack << r_id unless @db.is_marked?(r_id)
338
346
  end
339
347
  end
340
348
  end
@@ -382,10 +390,10 @@ module PEROBS
382
390
 
383
391
  # This method returns a Hash with some statistics about this store.
384
392
  def statistics
385
- {
386
- :in_memory_objects => @in_memory_objects.length,
387
- :root_objects => 0 #@root_objects.length
388
- }
393
+ @stats.in_memory_objects = @in_memory_objects.length
394
+ @stats.root_objects = @root_objects.length
395
+
396
+ @stats
389
397
  end
390
398
 
391
399
  private
@@ -394,16 +402,21 @@ module PEROBS
394
402
  # objects that are reachable from the root objects.
395
403
  def mark
396
404
  classes = Set.new
397
- each { |obj| classes.add(obj.class) }
405
+ marked_objects = 0
406
+ each { |obj| classes.add(obj.class); marked_objects += 1 }
398
407
  @class_map.keep(classes.map { |c| c.to_s })
408
+
409
+ # The root_objects object is included in the count, but we only want to
410
+ # count user objects here.
411
+ @stats.marked_objects = marked_objects - 1
399
412
  end
400
413
 
401
414
  # Sweep phase of a mark-and-sweep garbage collector. It will remove all
402
415
  # unmarked objects from the store.
403
416
  def sweep
404
- cntr = @db.delete_unmarked_objects.length
417
+ @stats.swept_objects = @db.delete_unmarked_objects.length
405
418
  @cache.reset
406
- cntr
419
+ @stats.swept_objects
407
420
  end
408
421
 
409
422
  # Check the object with the given start_id and all other objects that are
@@ -431,11 +444,19 @@ module PEROBS
431
444
  obj._referenced_object_ids.each do |refd_id|
432
445
  # Push them onto the todo list unless they have been marked
433
446
  # already.
434
- todo_list << [ obj, refd_id ] unless @db.is_marked?(refd_id)
447
+ todo_list << [ obj, refd_id ] unless @db.is_marked?(refd_id, true)
435
448
  end
436
449
  else
437
450
  # Remove references to bad objects.
438
- ref_obj._delete_reference_to_id(id) if ref_obj && repair
451
+ if ref_obj && repair
452
+ $stderr.puts "Fixing broken reference to #{id} in\n" +
453
+ ref_obj.inspect
454
+ ref_obj._delete_reference_to_id(id)
455
+ else
456
+ raise RuntimeError,
457
+ "The following object references a non-existing object #{id}:\n" +
458
+ ref_obj.inspect
459
+ end
439
460
  errors += 1
440
461
  end
441
462
  end
@@ -1,4 +1,4 @@
1
1
  module PEROBS
2
2
  # The version number
3
- VERSION = "2.1.1"
3
+ VERSION = "2.2.0"
4
4
  end
data/test/Array_spec.rb CHANGED
@@ -60,6 +60,21 @@ describe PEROBS::Array do
60
60
  @store.delete_store
61
61
  end
62
62
 
63
+ it 'should store an empty Array persistently' do
64
+ @store['a'] = @store.new(PEROBS::Array)
65
+ @store.transaction do
66
+ @store['b'] = @store.new(PEROBS::Array)
67
+ end
68
+ @store.sync
69
+ @store = nil
70
+ GC.start
71
+
72
+ @store = PEROBS::Store.new(@db_name)
73
+ a = @store['a']
74
+ expect(a.length).to eq(0)
75
+ expect(@store['b'].length).to eq(0)
76
+ end
77
+
63
78
  it 'should store simple objects persistently' do
64
79
  @store['a'] = a = @store.new(PEROBS::Array)
65
80
  a[0] = 'A'
data/test/Object_spec.rb CHANGED
@@ -42,9 +42,9 @@ class O2 < PEROBS::Object
42
42
 
43
43
  def initialize(store)
44
44
  super
45
- init_attr(:a1, 'a1')
46
- init_attr(:a2, nil)
47
- init_attr(:a4, 42)
45
+ attr_init(:a1, 'a1')
46
+ attr_init(:a2, nil)
47
+ attr_init(:a4, 42)
48
48
  end
49
49
 
50
50
  def a3_deref
@@ -110,15 +110,23 @@ describe PEROBS::Store do
110
110
  o2.a3 = o1
111
111
  o2.a4 = @store.new(PEROBS::Array)
112
112
  o2.a4 += [ 0, 1, 2 ]
113
+ @store.transaction do
114
+ @store['o3'] = o3 = @store.new(O1)
115
+ o3.a1 = @store.new(PEROBS::Array)
116
+ end
113
117
  @store.sync
118
+ @store = nil
119
+ GC.start
114
120
 
115
121
  @store = PEROBS::Store.new(@db_name)
116
122
  o1 = @store['o1']
117
123
  o2 = @store['o2']
124
+ o3 = @store['o3']
118
125
  expect(o1.a1).to eq('a1')
119
126
  expect(o2.a1).to be_nil
120
127
  expect(o2.a3).to eq(o1)
121
128
  expect(o2.a4).to eq([ 0, 1, 2 ])
129
+ expect(o3.a1).to eq([])
122
130
  end
123
131
 
124
132
  it 'should transparently access a referenced object' do
data/test/Store_spec.rb CHANGED
@@ -35,9 +35,9 @@ class Person < PEROBS::Object
35
35
 
36
36
  def initialize(store)
37
37
  super
38
- init_attr(:name, '')
39
- init_attr(:bmi, 22.2)
40
- init_attr(:married, false)
38
+ attr_init(:name, '')
39
+ attr_init(:bmi, 22.2)
40
+ attr_init(:married, false)
41
41
  end
42
42
 
43
43
  end
@@ -48,30 +48,30 @@ class PersonN < PEROBS::Object
48
48
 
49
49
  def initialize(store)
50
50
  super
51
- init_attr(:name, '')
52
- init_attr(:bmi, 22.2)
53
- init_attr(:married, false)
51
+ attr_init(:name, '')
52
+ attr_init(:bmi, 22.2)
53
+ attr_init(:married, false)
54
54
  end
55
55
 
56
56
  end
57
57
 
58
58
  class O0 < PEROBS::Object
59
59
 
60
- po_attr :r
60
+ po_attr :child
61
61
 
62
62
  def initialize(store)
63
63
  super
64
- r = @store.new(O1, myself)
64
+ self.child = @store.new(O1, myself)
65
65
  end
66
66
 
67
67
  end
68
68
  class O1 < PEROBS::Object
69
69
 
70
- po_attr :p
70
+ po_attr :parent
71
71
 
72
72
  def initialize(store, p = nil)
73
73
  super(store)
74
- parent = p
74
+ self.parent = p
75
75
  end
76
76
 
77
77
  end
@@ -145,11 +145,6 @@ describe PEROBS::Store do
145
145
  end
146
146
  end
147
147
 
148
- it 'should not allow calls to BasicObject.new()' do
149
- @store = PEROBS::Store.new(@db_file)
150
- expect { Person.new(@store) }.to raise_error RuntimeError
151
- end
152
-
153
148
  it 'should flush cached objects when necessary' do
154
149
  @store = PEROBS::Store.new(@db_file, :cache_bits => 3)
155
150
  last_obj = nil
@@ -431,11 +426,13 @@ describe PEROBS::Store do
431
426
 
432
427
  it 'should handle nested constructors' do
433
428
  @store = PEROBS::Store.new(@db_file)
434
- @store['r'] = @store.new(O0)
429
+ @store['root'] = @store.new(O0)
435
430
  @store.sync
436
431
  expect(@store.check).to eq(0)
432
+
437
433
  @store = PEROBS::Store.new(@db_file)
438
434
  expect(@store.check).to eq(0)
435
+ expect(@store['root'].child.parent).to eq(@store['root'])
439
436
  end
440
437
 
441
438
  it 'should survive a real world usage test' do
@@ -443,58 +440,70 @@ describe PEROBS::Store do
443
440
  @store = PEROBS::Store.new(@db_file, options)
444
441
  ref = {}
445
442
 
446
- 0.upto(2000) do |i|
443
+ deletions_since_last_gc = 0
444
+ 0.upto(5000) do |i|
447
445
  key = "o#{i}"
448
- case i % 8
446
+ case rand(8)
449
447
  when 0
448
+ # Add 'A' person
450
449
  value = 'A' * rand(512)
451
450
  @store[key] = p = @store.new(Person)
452
451
  p.name = value
453
452
  ref[key] = value
454
- @store.sync
455
453
  when 1
454
+ # Add 'B' person
456
455
  value = 'B' * rand(128)
457
456
  @store[key] = p = @store.new(Person)
458
457
  p.name = value
459
458
  ref[key] = value
460
459
  when 2
461
- index = i - rand(20)
462
- if index >= 0
463
- key = "o#{i - rand(20)}"
460
+ # Delete a root entry
461
+ if ref.keys.length > 11
462
+ key = ref.keys[(ref.keys.length / 11).to_i]
463
+ expect(@store[key]).not_to be_nil
464
464
  @store[key] = nil
465
465
  ref.delete(key)
466
+ deletions_since_last_gc += 1
466
467
  end
467
468
  when 3
468
- @store.gc if rand(30) == 0
469
+ # Call garbage collector
470
+ if rand(30) == 0
471
+ @store.gc
472
+ stats = @store.statistics
473
+ expect(stats.marked_objects).to eq(ref.length)
474
+ expect(stats.swept_objects).to eq(deletions_since_last_gc)
475
+ deletions_since_last_gc = 0
476
+ expect(@store.gc).to eq(deletions_since_last_gc)
477
+ end
469
478
  when 4
479
+ # Sync store and reload
470
480
  if rand(15) == 0
471
481
  @store.sync
472
482
  @store = PEROBS::Store.new(@db_file, options)
473
483
  end
474
484
  when 5
475
- index = i - rand(10)
476
- if rand(3) == 0 && index >= 0
477
- key = "o#{i - rand(10)}"
485
+ # Replace an entry with 'C' person
486
+ if ref.keys.length > 13
487
+ key = ref.keys[(ref.keys.length / 13).to_i]
478
488
  value = 'C' * rand(1024)
479
489
  @store[key] = p = @store.new(Person)
480
490
  p.name = value
481
491
  ref[key] = value
492
+ deletions_since_last_gc += 1
482
493
  end
483
494
  when 6
495
+ # Sync and check store
484
496
  if rand(50) == 0
485
497
  @store.sync
486
498
  expect(@store.check(false)).to eq(0)
487
499
  end
488
500
  when 7
489
- index = rand(i)
490
- if ref[key]
501
+ # Compare a random entry with reference entry
502
+ if ref.keys.length > 0
503
+ key = ref.keys[rand(ref.keys.length - 1)]
491
504
  expect(@store[key].name).to eq(ref[key])
492
505
  end
493
506
  end
494
-
495
- if ref[key]
496
- expect(@store[key].name).to eq(ref[key])
497
- end
498
507
  end
499
508
 
500
509
  ref.each do |k, v|
data/test/perobs_spec.rb CHANGED
@@ -32,9 +32,9 @@ class Person < PEROBS::Object
32
32
 
33
33
  def initialize(store)
34
34
  super
35
- init_attr(:name, '')
36
- init_attr(:bmi, 22.2)
37
- init_attr(:married, false)
35
+ attr_init(:name, '')
36
+ attr_init(:bmi, 22.2)
37
+ attr_init(:married, false)
38
38
  end
39
39
 
40
40
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: perobs
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.1
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Schlaeger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-01-10 00:00:00.000000000 Z
11
+ date: 2016-01-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -71,8 +71,8 @@ files:
71
71
  - lib/perobs/Cache.rb
72
72
  - lib/perobs/ClassMap.rb
73
73
  - lib/perobs/DataBase.rb
74
- - lib/perobs/Delegator.rb
75
74
  - lib/perobs/DynamoDB.rb
75
+ - lib/perobs/Handle.rb
76
76
  - lib/perobs/Hash.rb
77
77
  - lib/perobs/Object.rb
78
78
  - lib/perobs/ObjectBase.rb
@@ -1,78 +0,0 @@
1
- # encoding: UTF-8
2
- #
3
- # = Delegator.rb -- Persistent Ruby Object Store
4
- #
5
- # Copyright (c) 2015 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 'perobs/ClassMap'
29
-
30
- module PEROBS
31
-
32
- # This Delegator module provides the methods to turn the PEROBS::Array and
33
- # PEROBS::Hash into proxies for Array and Hash.
34
- module Delegator
35
-
36
- # Proxy all calls to unknown methods to the data object.
37
- def method_missing(method_sym, *args, &block)
38
- if self.class::READERS.include?(method_sym) ||
39
- Enumerable.instance_methods.include?(method_sym)
40
- # If any element of this class is read, we register this object as
41
- # being read with the cache.
42
- @store.cache.cache_read(self)
43
- @data.send(method_sym, *args, &block)
44
- elsif self.class::REWRITERS.include?(method_sym)
45
- # Re-writers don't introduce any new elements. We just mark the object
46
- # as written in the cache and call the class' method.
47
- @store.cache.cache_write(self)
48
- @data.send(method_sym, *args, &block)
49
- elsif (alias_sym = self.class::ALIASES[method_sym])
50
- @store.cache.cache_write(self)
51
- send(alias_sym, *args, &block)
52
- else
53
- # Any method we don't know about must cause an error. A new class
54
- # method needs to be added to the right bucket first.
55
- raise NoMethodError.new("undefined method '#{method_sym}' for " +
56
- "#{self.class}")
57
- end
58
- end
59
-
60
- def respond_to?(method_sym, include_private = false)
61
- self.class::READERS.include?(method_sym) ||
62
- Enumerable.instance_methods.include?(method_sym) ||
63
- self.class::REWRITERS.include?(method_sym) ||
64
- super
65
- end
66
-
67
- # Equivalent to Class::==
68
- # This method is just a reader but also part of BasicObject. Hence
69
- # BasicObject::== would be called instead of method_missing.
70
- def ==(obj)
71
- @store.cache.cache_read(self)
72
- @data == obj
73
- end
74
-
75
- end
76
-
77
- end
78
-