perobs 3.0.1 → 4.3.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.
Files changed (75) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +19 -18
  3. data/lib/perobs.rb +2 -0
  4. data/lib/perobs/Array.rb +68 -21
  5. data/lib/perobs/BTree.rb +110 -54
  6. data/lib/perobs/BTreeBlob.rb +14 -13
  7. data/lib/perobs/BTreeDB.rb +11 -10
  8. data/lib/perobs/BTreeNode.rb +551 -197
  9. data/lib/perobs/BTreeNodeCache.rb +10 -8
  10. data/lib/perobs/BTreeNodeLink.rb +11 -1
  11. data/lib/perobs/BigArray.rb +285 -0
  12. data/lib/perobs/BigArrayNode.rb +1002 -0
  13. data/lib/perobs/BigHash.rb +246 -0
  14. data/lib/perobs/BigTree.rb +197 -0
  15. data/lib/perobs/BigTreeNode.rb +873 -0
  16. data/lib/perobs/Cache.rb +47 -22
  17. data/lib/perobs/ClassMap.rb +2 -2
  18. data/lib/perobs/ConsoleProgressMeter.rb +61 -0
  19. data/lib/perobs/DataBase.rb +4 -3
  20. data/lib/perobs/DynamoDB.rb +62 -20
  21. data/lib/perobs/EquiBlobsFile.rb +174 -59
  22. data/lib/perobs/FNV_Hash_1a_64.rb +54 -0
  23. data/lib/perobs/FlatFile.rb +536 -242
  24. data/lib/perobs/FlatFileBlobHeader.rb +120 -84
  25. data/lib/perobs/FlatFileDB.rb +58 -27
  26. data/lib/perobs/FuzzyStringMatcher.rb +175 -0
  27. data/lib/perobs/Hash.rb +129 -35
  28. data/lib/perobs/IDList.rb +144 -0
  29. data/lib/perobs/IDListPage.rb +107 -0
  30. data/lib/perobs/IDListPageFile.rb +180 -0
  31. data/lib/perobs/IDListPageRecord.rb +142 -0
  32. data/lib/perobs/LockFile.rb +3 -0
  33. data/lib/perobs/Object.rb +28 -20
  34. data/lib/perobs/ObjectBase.rb +53 -10
  35. data/lib/perobs/PersistentObjectCache.rb +142 -0
  36. data/lib/perobs/PersistentObjectCacheLine.rb +99 -0
  37. data/lib/perobs/ProgressMeter.rb +97 -0
  38. data/lib/perobs/SpaceManager.rb +273 -0
  39. data/lib/perobs/SpaceTree.rb +63 -47
  40. data/lib/perobs/SpaceTreeNode.rb +134 -115
  41. data/lib/perobs/SpaceTreeNodeLink.rb +1 -1
  42. data/lib/perobs/StackFile.rb +1 -1
  43. data/lib/perobs/Store.rb +180 -70
  44. data/lib/perobs/version.rb +1 -1
  45. data/perobs.gemspec +4 -4
  46. data/test/Array_spec.rb +48 -39
  47. data/test/BTreeDB_spec.rb +2 -2
  48. data/test/BTree_spec.rb +50 -1
  49. data/test/BigArray_spec.rb +261 -0
  50. data/test/BigHash_spec.rb +152 -0
  51. data/test/BigTreeNode_spec.rb +153 -0
  52. data/test/BigTree_spec.rb +259 -0
  53. data/test/EquiBlobsFile_spec.rb +105 -5
  54. data/test/FNV_Hash_1a_64_spec.rb +59 -0
  55. data/test/FlatFileDB_spec.rb +199 -15
  56. data/test/FuzzyStringMatcher_spec.rb +261 -0
  57. data/test/Hash_spec.rb +27 -16
  58. data/test/IDList_spec.rb +77 -0
  59. data/test/LegacyDBs/LegacyDB.rb +155 -0
  60. data/test/LegacyDBs/version_3/class_map.json +1 -0
  61. data/test/LegacyDBs/version_3/config.json +1 -0
  62. data/test/LegacyDBs/version_3/database.blobs +0 -0
  63. data/test/LegacyDBs/version_3/database_spaces.blobs +0 -0
  64. data/test/LegacyDBs/version_3/index.blobs +0 -0
  65. data/test/LegacyDBs/version_3/version +1 -0
  66. data/test/LockFile_spec.rb +9 -6
  67. data/test/Object_spec.rb +5 -5
  68. data/test/SpaceManager_spec.rb +176 -0
  69. data/test/SpaceTree_spec.rb +27 -9
  70. data/test/Store_spec.rb +353 -206
  71. data/test/perobs_spec.rb +7 -3
  72. data/test/spec_helper.rb +9 -4
  73. metadata +59 -16
  74. data/lib/perobs/SpaceTreeNodeCache.rb +0 -76
  75. data/lib/perobs/TreeDB.rb +0 -277
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
  #
@@ -49,9 +49,11 @@ module PEROBS
49
49
  attr_reader :attributes
50
50
 
51
51
  # This method can be used to define instance variable for
52
- # PEROBS::Object derived classes.
52
+ # PEROBS::Object derived classes. Persistent attributes always have
53
+ # getter and setter methods defined. So it's essentially equivalent to
54
+ # attr_accessor but additionally declares an attribute as persistent.
53
55
  # @param attributes [Symbol] Name of the instance variable
54
- def po_attr(*attributes)
56
+ def attr_persist(*attributes)
55
57
  attributes.each do |attr_name|
56
58
  unless attr_name.is_a?(Symbol)
57
59
  PEROBS.log.fatal "name must be a symbol but is a " +
@@ -73,6 +75,9 @@ module PEROBS
73
75
  end
74
76
  end
75
77
 
78
+ # This is the deprecated name for the attr_persist method
79
+ alias po_attr attr_persist
80
+
76
81
  end
77
82
 
78
83
  attr_reader :attributes
@@ -108,7 +113,7 @@ module PEROBS
108
113
  # object was saved with an earlier version of the program that did not yet
109
114
  # have the instance variable. If you want to assign another PEROBS object
110
115
  # to the variable you should use the block variant to avoid unnecessary
111
- # creation of PEROBS object that later need to be collected again.
116
+ # creation of PEROBS objects that later need to be collected again.
112
117
  def attr_init(attr, val = nil, &block)
113
118
  if _all_attributes.include?(attr)
114
119
  unless instance_variable_defined?('@' + attr.to_s)
@@ -140,7 +145,7 @@ module PEROBS
140
145
 
141
146
  # Return a list of all object IDs that the attributes of this instance are
142
147
  # referencing.
143
- # @return [Array of Fixnum or Bignum] IDs of referenced objects
148
+ # @return [Array of Integer] IDs of referenced objects
144
149
  def _referenced_object_ids
145
150
  ids = []
146
151
  _all_attributes.each do |attr|
@@ -152,7 +157,7 @@ module PEROBS
152
157
 
153
158
  # This method should only be used during store repair operations. It will
154
159
  # delete all references to the given object ID.
155
- # @param id [Fixnum/Bignum] targeted object ID
160
+ # @param id [Integer] targeted object ID
156
161
  def _delete_reference_to_id(id)
157
162
  _all_attributes.each do |attr|
158
163
  ivar = ('@' + attr.to_s).to_sym
@@ -207,17 +212,7 @@ module PEROBS
207
212
  end
208
213
 
209
214
  def _set(attr, val)
210
- if val.respond_to?(:is_poxreference?)
211
- # References to other PEROBS::Objects must be handled somewhat
212
- # special.
213
- if @store != val.store
214
- PEROBS.log.fatal 'The referenced object is not part of this store'
215
- end
216
- elsif val.is_a?(ObjectBase)
217
- PEROBS.log.fatal 'A PEROBS::ObjectBase object escaped! ' +
218
- 'Have you used self() instead of myself() to get the reference ' +
219
- 'of the PEROBS object that you are trying to assign here?'
220
- end
215
+ _check_assignment_value(val)
221
216
  instance_variable_set(('@' + attr.to_s).to_sym, val)
222
217
  # Let the store know that we have a modified object. If we restored the
223
218
  # object from the DB, we don't mark it as modified.
@@ -231,13 +226,26 @@ module PEROBS
231
226
  end
232
227
 
233
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
+
234
240
  # PEROBS objects that don't have persistent attributes declared don't
235
241
  # really make sense.
236
- unless self.class.attributes
242
+ if attributes.empty?
237
243
  PEROBS.log.fatal "No persistent attributes have been declared for " +
238
- "class #{self.class}. Use 'po_attr' to declare them."
244
+ "class #{self.class} or any parent class. Use 'attr_persist' " +
245
+ "to declare them."
239
246
  end
240
- self.class.attributes
247
+
248
+ attributes
241
249
  end
242
250
 
243
251
  end
@@ -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().
@@ -133,7 +158,8 @@ module PEROBS
133
158
  @store = p.store
134
159
  @_id = p.id
135
160
  @store._register_in_memory(self, @_id)
136
- ObjectSpace.define_finalizer(self, ObjectBase._finalize(@store, @_id))
161
+ ObjectSpace.define_finalizer(
162
+ self, ObjectBase._finalize(@store, @_id, object_id))
137
163
  @_stash_map = nil
138
164
  # Allocate a proxy object for this object. User code should only operate
139
165
  # on this proxy, never on self.
@@ -144,8 +170,8 @@ module PEROBS
144
170
  # is done this way to prevent the Proc object hanging on to a reference to
145
171
  # self which would prevent the object from being collected. This internal
146
172
  # method is not intended for users to call.
147
- def ObjectBase._finalize(store, id)
148
- proc { store._collect(id) }
173
+ def ObjectBase._finalize(store, id, ruby_object_id)
174
+ proc { store._collect(id, ruby_object_id) }
149
175
  end
150
176
 
151
177
  # Library internal method to transfer the Object to a new store.
@@ -158,7 +184,8 @@ module PEROBS
158
184
  # Register the object as in-memory object with the new store.
159
185
  @store._register_in_memory(self, @_id)
160
186
  # Register the finalizer for the new store.
161
- ObjectSpace.define_finalizer(self, ObjectBase._finalize(@store, @_id))
187
+ ObjectSpace.define_finalizer(
188
+ self, ObjectBase._finalize(@store, @_id, object_id))
162
189
  @myself = POXReference.new(@store, @_id)
163
190
  end
164
191
 
@@ -190,6 +217,25 @@ module PEROBS
190
217
  @store.db.put_object(db_obj, @_id)
191
218
  end
192
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
+
193
239
  # Read an raw object with the specified ID from the backing store and
194
240
  # instantiate a new object of the specific type.
195
241
  def ObjectBase.read(store, id)
@@ -207,19 +253,16 @@ module PEROBS
207
253
  end
208
254
 
209
255
  # Restore the object state from the storage back-end.
210
- # @param level [Fixnum] the transaction nesting level
256
+ # @param level [Integer] the transaction nesting level
211
257
  def _restore(level)
212
258
  # Find the most recently stored state of this object. This could be on
213
259
  # any previous stash level or in the regular object DB. If the object
214
- # was created during the transaction, there is not previous state to
260
+ # was created during the transaction, there is no previous state to
215
261
  # restore to.
216
262
  data = nil
217
263
  if @_stash_map
218
264
  (level - 1).downto(0) do |lvl|
219
- if @_stash_map[lvl]
220
- data = @_stash_map[lvl]
221
- break
222
- end
265
+ break if (data = @_stash_map[lvl])
223
266
  end
224
267
  end
225
268
  if data
@@ -0,0 +1,142 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # = PersistentObjectCache.rb -- Persistent Ruby Object Store
4
+ #
5
+ # Copyright (c) 2016, 2017 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/PersistentObjectCacheLine'
29
+
30
+ module PEROBS
31
+
32
+ class PersistentObjectCache
33
+
34
+ # This cache class manages the presence of objects that primarily live in
35
+ # a backing store but temporarily exist in memory as well. To work with
36
+ # these objects, direct references must be only very short lived. Indirect
37
+ # references can be done via a unique ID that the object must provide. Due
38
+ # to the indirect references the Ruby garbage collector can collect these
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.
49
+ # @param klass [Class] The class of the objects to be cached. Objects must
50
+ # provide a uid() method that returns a unique ID for every object.
51
+ # @param collection [] The object collection the objects belong to. It
52
+ # must provide a ::load method.
53
+ def initialize(size, flush_delay, klass, collection)
54
+ @size = size
55
+ @klass = klass
56
+ @collection = collection
57
+ @flush_delay = @flush_counter = flush_delay
58
+ @flush_times = 0
59
+
60
+ clear
61
+ end
62
+
63
+ # Insert an object into the cache.
64
+ # @param object [Object] Object to cache
65
+ # @param modified [Boolean] True if the object was modified, false otherwise
66
+ def insert(object, modified = true)
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
77
+
78
+ nil
79
+ end
80
+
81
+ # Retrieve a object reference from the cache.
82
+ # @param uid [Integer] uid of the object to retrieve.
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
88
+ end
89
+
90
+ # Then check the unmodified object list.
91
+ if (object = @unmodified_entries[uid % @size]) && object.uid == uid
92
+ return object
93
+ end
94
+
95
+ # If we don't have it in memory we need to load it.
96
+ @klass::load(@collection, uid, ref)
97
+ end
98
+
99
+ # Remove a object from the cache.
100
+ # @param uid [Integer] unique ID of object to remove.
101
+ def delete(uid)
102
+ @modified_entries.delete(uid)
103
+
104
+ index = uid % @size
105
+ if (object = @unmodified_entries[index]) && object.uid == uid
106
+ @unmodified_entries[index] = nil
107
+ end
108
+ end
109
+
110
+ # Write all excess modified objects into the backing store. If now is true
111
+ # all modified objects will be written.
112
+ # @param now [Boolean]
113
+ def flush(now = false)
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
123
+ end
124
+ @flush_times += 1
125
+ end
126
+
127
+ # Remove all entries from the cache.
128
+ def clear
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
137
+ end
138
+
139
+ end
140
+
141
+ end
142
+
@@ -0,0 +1,99 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # = PersistentObjectCacheLine.rb -- Persistent Ruby Object Store
4
+ #
5
+ # Copyright (c) 2016, 2017 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
+ class PersistentObjectCacheLine
31
+
32
+ # Utility class to store persistent objects and their
33
+ # modified/not-modified state.
34
+ class Entry < Struct.new(:obj, :modified)
35
+ end
36
+
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
42
+
43
+ def initialize
44
+ @entries = []
45
+ end
46
+
47
+ def insert(object, modified)
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)
57
+ end
58
+
59
+ # Insert the entry at the beginning of the line.
60
+ @entries.unshift(entry)
61
+ end
62
+
63
+ def get(uid)
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
72
+ end
73
+ end
74
+
75
+ # Delete the entry that matches the given UID
76
+ # @param uid [Integer]
77
+ def delete(uid)
78
+ @entries.delete_if { |e| e.obj.uid == uid }
79
+ end
80
+
81
+ # Save all modified entries and delete all but the most recently added.
82
+ def flush(now)
83
+ if now || @entries.length > WATERMARK
84
+ @entries.each do |e|
85
+ if e.modified
86
+ e.obj.save
87
+ e.modified = false
88
+ end
89
+ end
90
+
91
+ # Delete all but the first WATERMARK entry.
92
+ @entries = @entries[0..WATERMARK - 1] if @entries.length > WATERMARK
93
+ end
94
+ end
95
+
96
+ end
97
+
98
+ end
99
+