perobs 2.3.1 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,296 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # = IndexTreeNode.rb -- Persistent Ruby Object Store
4
+ #
5
+ # Copyright (c) 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
+ require 'perobs/Log'
29
+
30
+ module PEROBS
31
+
32
+ # The IndexTreeNode is the building block of the IndexTree. Each node can
33
+ # hold up to 16 entries. An entry is described by type bits and can be empty
34
+ # (0), an reference into the object ID file (1) or a reference to another
35
+ # IndexTreeNode for the next nibble (2). Each level of the tree is
36
+ # associated with an specific nibble of the ID. The nibble is used to
37
+ # identify the entry within the node. IndexTreeNode objects are in-memory
38
+ # represenations of the nodes in the IndexTree file.
39
+ class IndexTreeNode
40
+
41
+ attr_reader :address
42
+
43
+ ENTRIES = 16
44
+ ENTRY_BYTES = 8
45
+ TYPE_BYTES = 4
46
+ NODE_BYTES = TYPE_BYTES + ENTRIES * ENTRY_BYTES
47
+
48
+ # Create a new IndexTreeNode.
49
+ # @param tree [IndexTree] The tree this node belongs to
50
+ # @param nibble_idx [Fixnum] the level of the node in the tree (root
51
+ # being 0)
52
+ # @param address [Integer] The address of this node in the blob file
53
+ def initialize(tree, nibble_idx, address = nil)
54
+ @tree = tree
55
+ if nibble_idx >= 16
56
+ # We are processing 64 bit numbers, so we have at most 16 nibbles.
57
+ PEROBS.log.fatal 'nibble must be 0 - 15'
58
+ end
59
+ @nibble_idx = nibble_idx
60
+ if (@address = address).nil? || !read_node
61
+ # Create a new node if none with this address exists already.
62
+ @entry_types = 0
63
+ @entries = ::Array.new(ENTRIES, 0)
64
+ @address = @tree.nodes.free_address
65
+ write_node
66
+ end
67
+ end
68
+
69
+ # Store a value for the given ID. Existing values will be overwritten.
70
+ # @param id [Integer] ID (or key)
71
+ # @param value [Integer] value
72
+ def put_value(id, value)
73
+ index = calc_index(id)
74
+ case get_entry_type(index)
75
+ when 0
76
+ # The entry is still empty. Store the id and value and set the entry
77
+ # to holding a value (1).
78
+ set_entry_type(index, 1)
79
+ @entries[index] = address = @tree.ids.free_address
80
+ store_id_and_value(address, id, value)
81
+ write_node
82
+ when 1
83
+ existing_value = @entries[index]
84
+ existing_id, existing_address = get_id_and_address(existing_value)
85
+ if id == existing_id
86
+ if value != existing_address
87
+ # The entry already holds another value.
88
+ store_id_and_value(@entries[index], id, value)
89
+ end
90
+ else
91
+ # The entry already holds a value. We need to create a new node and
92
+ # store the existing value and the new value in it.
93
+ # First get the exiting value of the entry and the corresponding ID.
94
+ # Create a new node.
95
+ node = @tree.get_node(@nibble_idx + 1)
96
+ # The entry of the current node is now a reference to the new node.
97
+ set_entry_type(index, 2)
98
+ @entries[index] = node.address
99
+ # Store the existing value and the new value with their IDs.
100
+ node.set_entry(existing_id, existing_value)
101
+ node.put_value(id, value)
102
+ end
103
+ write_node
104
+ when 2
105
+ # The entry is a reference to another node.
106
+ node = @tree.get_node(@nibble_idx + 1, @entries[index])
107
+ node.put_value(id, value)
108
+ else
109
+ PEROBS.log.fatal "Illegal node type #{get_entry_type(index)}"
110
+ end
111
+ end
112
+
113
+ # Retrieve the value for the given ID.
114
+ # @param id [Integer] ID (or key)
115
+ # @return [Integer] value or nil
116
+ def get_value(id)
117
+ index = calc_index(id)
118
+ case get_entry_type(index)
119
+ when 0
120
+ # There is no entry for this ID.
121
+ return nil
122
+ when 1
123
+ # There is a value stored for the ID part that we have seen so far. We
124
+ # still need to compare the requested ID with the full ID to determine
125
+ # a match.
126
+ stored_id, address = get_id_and_address(@entries[index])
127
+ if id == stored_id
128
+ # We have a match. Return the value.
129
+ return address
130
+ else
131
+ # Just a partial match of the least significant nibbles.
132
+ return nil
133
+ end
134
+ when 2
135
+ # The entry is a reference to another node. Just follow it and look at
136
+ # the next nibble.
137
+ return @tree.get_node(@nibble_idx + 1, @entries[index]).
138
+ get_value(id)
139
+ else
140
+ PEROBS.log.fatal "Illegal node type #{get_entry_type(index)}"
141
+ end
142
+ end
143
+
144
+ # Delete the entry for the given ID.
145
+ # @param id [Integer] ID or key
146
+ # @return [Boolean] True if a key was found and deleted, otherwise false.
147
+ def delete_value(id)
148
+ index = calc_index(id)
149
+ case get_entry_type(index)
150
+ when 0
151
+ # There is no entry for this ID.
152
+ return false
153
+ when 1
154
+ # We have a value. Check that the ID matches and delete the value.
155
+ stored_id, address = get_id_and_address(@entries[index])
156
+ if id == stored_id
157
+ @tree.ids.delete_blob(@entries[index])
158
+ @entries[index] = 0
159
+ set_entry_type(index, 0)
160
+ write_node
161
+ return true
162
+ else
163
+ # Just a partial ID match.
164
+ return false
165
+ end
166
+ when 2
167
+ # The entry is a reference to another node.
168
+ node = @tree.get_node(@nibble_idx + 1, @entries[index])
169
+ result = node.delete_value(id)
170
+ if node.empty?
171
+ # If the sub-node is empty after the delete we delete the whole
172
+ # sub-node.
173
+ @tree.delete_node(@nibble_idx + 1, @entries[index])
174
+ # Eliminate the reference to the sub-node and update this node in
175
+ # the file.
176
+ set_entry_type(index, 0)
177
+ write_node
178
+ end
179
+ return result
180
+ else
181
+ PEROBS.node.fatal "Illegal node type #{get_entry_type(index)}"
182
+ end
183
+ end
184
+
185
+ # Recursively check this node and all sub nodes. Compare the found
186
+ # ID/address pairs with the corresponding entry in the given FlatFile.
187
+ # @param flat_file [FlatFile]
188
+ # @return [Boolean] true if no errors were found, false otherwise
189
+ def check(flat_file)
190
+ ENTRIES.times do |index|
191
+ case get_entry_type(index)
192
+ when 0
193
+ # Empty entry, nothing to do here.
194
+ when 1
195
+ # There is a value stored for the ID part that we have seen so far.
196
+ # We still need to compare the requested ID with the full ID to
197
+ # determine a match.
198
+ id, address = get_id_and_address(@entries[index])
199
+ unless flat_file.has_id_at?(id, address)
200
+ PEROBS.log.error "The entry for ID #{id} in the index was not " +
201
+ "found in the FlatFile at address #{address}"
202
+ return false
203
+ end
204
+ when 2
205
+ # The entry is a reference to another node. Just follow it and look
206
+ # at the next nibble.
207
+ unless @tree.get_node(@nibble_idx + 1, @entries[index]).
208
+ check(flat_file)
209
+ return false
210
+ end
211
+ else
212
+ return false
213
+ end
214
+ end
215
+
216
+ true
217
+ end
218
+
219
+ # Convert the node and all sub-nodes to human readable format.
220
+ def inspect
221
+ str = "{\n"
222
+ 0.upto(15) do |i|
223
+ case get_entry_type(i)
224
+ when 0
225
+ # Don't show empty entries.
226
+ when 1
227
+ id, address = get_id_and_address(@entries[i])
228
+ str += " #{id} => #{address},\n"
229
+ when 2
230
+ str += " " + @tree.get_node(@nibble_idx + 1, @entries[i]).
231
+ inspect.gsub(/\n/, "\n ")
232
+ end
233
+ end
234
+ str + "}\n"
235
+ end
236
+
237
+ # Utility method to set the value of an existing node entry.
238
+ # @param id [Integer] ID or key
239
+ # @param value [Integer] value to set. Note that this value must be an
240
+ # address from the ids list.
241
+ def set_entry(id, value)
242
+ index = calc_index(id)
243
+ set_entry_type(index, 1)
244
+ @entries[index] = value
245
+ end
246
+
247
+ # Check if the node is empty.
248
+ # @return [Boolean] True if all entries are empty.
249
+ def empty?
250
+ @entry_types == 0
251
+ end
252
+
253
+ private
254
+
255
+ def calc_index(id)
256
+ (id >> (4 * @nibble_idx)) & 0xF
257
+ end
258
+
259
+ def read_node
260
+ return false unless (bytes = @tree.nodes.retrieve_blob(@address))
261
+ @entry_types = bytes[0, TYPE_BYTES].unpack('L')[0]
262
+ @entries = bytes[TYPE_BYTES, ENTRIES * ENTRY_BYTES].unpack('Q16')
263
+ true
264
+ end
265
+
266
+ def write_node
267
+ bytes = ([ @entry_types ] + @entries).pack('LQ16')
268
+ @tree.nodes.store_blob(@address, bytes)
269
+ end
270
+
271
+ def set_entry_type(index, type)
272
+ if index < 0 || index > 15
273
+ PEROBS.log.fatal "Index must be between 0 and 15"
274
+ end
275
+ @entry_types = ((@entry_types & ~(0x3 << 2 * index)) |
276
+ ((type & 0x3) << 2 * index)) & 0xFFFFFFFF
277
+ end
278
+
279
+ def get_entry_type(index)
280
+ if index < 0 || index > 15
281
+ PEROBS.log.fatal "Index must be between 0 and 15"
282
+ end
283
+ (@entry_types >> 2 * index) & 0x3
284
+ end
285
+
286
+ def get_id_and_address(id_address)
287
+ @tree.ids.retrieve_blob(id_address).unpack('QQ')
288
+ end
289
+
290
+ def store_id_and_value(address, id, value)
291
+ @tree.ids.store_blob(address, [ id, value ].pack('QQ'))
292
+ end
293
+
294
+ end
295
+
296
+ end
data/lib/perobs/Log.rb ADDED
@@ -0,0 +1,125 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # = Log.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
+ #!/usr/bin/env ruby -w
28
+ # encoding: UTF-8
29
+ #
30
+
31
+ require 'monitor'
32
+ require 'logger'
33
+ require 'singleton'
34
+
35
+ module PEROBS
36
+
37
+ # This is the Exception type that will be thrown for all unrecoverable
38
+ # library internal (program logic) errors.
39
+ class FatalError < StandardError ; end
40
+
41
+ # This is the Exception type that will be thrown for all program errors that
42
+ # are caused by user error rather than program logic errors.
43
+ class UsageError < StandardError ; end
44
+
45
+ # The ILogger class is a singleton that provides a common logging mechanism
46
+ # to all objects. It exposes essentially the same interface as the Logger
47
+ # class, just as a singleton and extends fatal to raise an FatalError
48
+ # exception.
49
+ class ILogger < Monitor
50
+
51
+ include Singleton
52
+
53
+ # Default options to create a logger. Keep 4 log files, each 1MB max.
54
+ @@options = [ 4, 2**20 ]
55
+ @@level = Logger::INFO
56
+ @@formatter = proc do |severity, time, progname, msg|
57
+ "#{time} #{severity} #{msg}\n"
58
+ end
59
+ @@logger = nil
60
+
61
+ # Set log level.
62
+ # @param l [Logger::WARN, Logger:INFO, etc]
63
+ def level=(l)
64
+ @@level = l
65
+ end
66
+
67
+ # Set Logger formatter.
68
+ # @param f [Proc]
69
+ def formatter=(f)
70
+ @@formatter = f
71
+ end
72
+
73
+ # Set Logger options
74
+ # @param o [Array] Optional parameters for Logger.new().
75
+ def options=(o)
76
+ @@options = o
77
+ end
78
+
79
+ # Redirect all log messages to the given IO.
80
+ # @param io [IO] Output file descriptor
81
+ def open(io)
82
+ begin
83
+ @@logger = Logger.new(io, *@@options)
84
+ rescue IOError => e
85
+ @@logger = Logger.new($stderr)
86
+ $stderr.puts "Cannot open log file: #{e.message}"
87
+ end
88
+ @@logger.level = @@level
89
+ @@logger.formatter = @@formatter
90
+ end
91
+
92
+ # Pass all calls to unknown methods to the @@logger object.
93
+ def method_missing(method, *args, &block)
94
+ @@logger.send(method, *args, &block)
95
+ end
96
+
97
+ # Make it properly introspectable.
98
+ def respond_to?(method, include_private = false)
99
+ @@logger.respond_to?(method)
100
+ end
101
+
102
+ # Print an error message via the Logger and raise a Fit4Ruby::Error.
103
+ # This method should be used to abort the program in case of program logic
104
+ # errors.
105
+ def fatal(msg, &block)
106
+ @@logger.fatal(msg, &block)
107
+ raise FatalError, msg
108
+ end
109
+
110
+ end
111
+
112
+ class << self
113
+
114
+ ILogger.instance.open($stderr)
115
+
116
+ # Convenience method to we can use PEROBS::log instead of
117
+ # PEROBS::ILogger.instance.
118
+ def log
119
+ ILogger.instance
120
+ end
121
+
122
+ end
123
+
124
+ end
125
+
data/lib/perobs/Object.rb CHANGED
@@ -27,6 +27,7 @@
27
27
 
28
28
  require 'time'
29
29
 
30
+ require 'perobs/Log'
30
31
  require 'perobs/ObjectBase'
31
32
 
32
33
  module PEROBS
@@ -53,7 +54,7 @@ module PEROBS
53
54
  def po_attr(*attributes)
54
55
  attributes.each do |attr_name|
55
56
  unless attr_name.is_a?(Symbol)
56
- raise ArgumentError, "name must be a symbol but is a " +
57
+ PEROBS.log.fatal "name must be a symbol but is a " +
57
58
  "#{attr_name.class}"
58
59
  end
59
60
 
@@ -115,8 +116,8 @@ module PEROBS
115
116
  end
116
117
  return true
117
118
  else
118
- raise ArgumentError, "'#{attr}' is not a defined persistent " +
119
- "attribute of class #{self.class}"
119
+ PEROBS.log.fatal "'#{attr}' is not a defined persistent " +
120
+ "attribute of class #{self.class}"
120
121
  end
121
122
 
122
123
  false
@@ -177,7 +178,7 @@ module PEROBS
177
178
  # Textual dump for debugging purposes
178
179
  # @return [String]
179
180
  def inspect
180
- "#{to_s}:#{@_id}\n{\n" +
181
+ "<#{self.class}:#{@_id}>\n{\n" +
181
182
  _all_attributes.map do |attr|
182
183
  ivar = ('@' + attr.to_s).to_sym
183
184
  if (value = instance_variable_get(ivar)).respond_to?(:is_poxreference?)
@@ -209,13 +210,12 @@ module PEROBS
209
210
  # References to other PEROBS::Objects must be handled somewhat
210
211
  # special.
211
212
  if @store != val.store
212
- raise ArgumentError, 'The referenced object is not part of this store'
213
+ PEROBS.log.fatal 'The referenced object is not part of this store'
213
214
  end
214
215
  elsif val.is_a?(ObjectBase)
215
- raise ArgumentError, 'A PEROBS::ObjectBase object escaped! ' +
216
- 'Have you used self() instead of myself() to' +
217
- 'get the reference of the PEROBS object that ' +
218
- 'you are trying to assign here?'
216
+ PEROBS.log.fatal 'A PEROBS::ObjectBase object escaped! ' +
217
+ 'Have you used self() instead of myself() to get the reference ' +
218
+ 'of the PEROBS object that you are trying to assign here?'
219
219
  end
220
220
  instance_variable_set(('@' + attr.to_s).to_sym, val)
221
221
  # Let the store know that we have a modified object. If we restored the
@@ -233,8 +233,7 @@ module PEROBS
233
233
  # PEROBS objects that don't have persistent attributes declared don't
234
234
  # really make sense.
235
235
  unless self.class.attributes
236
- raise StandardError
237
- "No persistent attributes have been declared for " +
236
+ PEROBS.log.fatal "No persistent attributes have been declared for " +
238
237
  "class #{self.class}. Use 'po_attr' to declare them."
239
238
  end
240
239
  self.class.attributes
@@ -25,6 +25,7 @@
25
25
  # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26
26
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27
27
 
28
+ require 'perobs/Log'
28
29
  require 'perobs/ClassMap'
29
30
 
30
31
  module PEROBS
@@ -47,13 +48,11 @@ module PEROBS
47
48
  # Proxy all calls to unknown methods to the referenced object.
48
49
  def method_missing(method_sym, *args, &block)
49
50
  unless (obj = _referenced_object)
50
- ::Kernel.raise ::RuntimeError,
51
- "Internal consistency error. No object with ID #{@id} found in " +
52
- 'the store.'
51
+ ::PEROBS.log.fatal "Internal consistency error. No object with ID " +
52
+ "#{@id} found in the store."
53
53
  end
54
54
  if obj.respond_to?(:is_poxreference?)
55
- ::Kernel.raise ::RuntimeError,
56
- "POXReference that references a POXReference found."
55
+ ::PEROBS.log.fatal "POXReference that references a POXReference found."
57
56
  end
58
57
  obj.send(method_sym, *args, &block)
59
58
  end
@@ -149,6 +148,20 @@ module PEROBS
149
148
  proc { store._collect(id) }
150
149
  end
151
150
 
151
+ # Library internal method to transfer the Object to a new store.
152
+ # @param store [Store] New store
153
+ def _transfer(store)
154
+ @store = store
155
+ # Remove the previously defined finalizer as it is attached to the old
156
+ # store.
157
+ ObjectSpace.undefine_finalizer(self)
158
+ # Register the object as in-memory object with the new store.
159
+ @store._register_in_memory(self, @_id)
160
+ # Register the finalizer for the new store.
161
+ ObjectSpace.define_finalizer(self, ObjectBase._finalize(@store, @_id))
162
+ @myself = POXReference.new(@store, @_id)
163
+ end
164
+
152
165
  # This method can be overloaded by derived classes to do some massaging on
153
166
  # the data after it has been restored from the database. This could either
154
167
  # be some sanity check or code to migrate the object from one version to
@@ -0,0 +1,137 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # = StackFile.rb -- Persistent Ruby Object Store
4
+ #
5
+ # Copyright (c) 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
+ require 'perobs/Log'
29
+
30
+ module PEROBS
31
+
32
+ # This class implements a file based stack. All entries must have the same
33
+ # size.
34
+ class StackFile
35
+
36
+ # Create a new stack file in the given directory with the given file name.
37
+ # @param dir [String] Directory
38
+ # @param name [String] File name
39
+ # @param entry_bytes [Fixnum] Number of bytes each entry must have
40
+ def initialize(dir, name, entry_bytes)
41
+ @file_name = File.join(dir, name + '.stack')
42
+ @entry_bytes = entry_bytes
43
+ @f = nil
44
+ end
45
+
46
+ # Open the stack file.
47
+ def open
48
+ begin
49
+ if File.exist?(@file_name)
50
+ @f = File.open(@file_name, 'rb+')
51
+ else
52
+ @f = File.open(@file_name, 'wb+')
53
+ end
54
+ rescue => e
55
+ PEROBS.log.fatal "Cannot open stack file #{@file_name}: #{e.message}"
56
+ end
57
+ end
58
+
59
+ # Close the stack file. This method must be called before the program is
60
+ # terminated to avoid data loss.
61
+ def close
62
+ begin
63
+ @f.flush
64
+ @f.close
65
+ rescue IOError => e
66
+ PEROBS.log.fatal "Cannot close stack file #{@file_name}: #{e.message}"
67
+ end
68
+ end
69
+
70
+ # Flush out unwritten data to file.
71
+ def sync
72
+ begin
73
+ @f.flush
74
+ rescue IOError => e
75
+ PEROBS.log.fatal "Cannot sync stack file #{@file_name}: #{e.message}"
76
+ end
77
+ end
78
+
79
+ # Push the given bytes onto the stack file.
80
+ # @param bytes [String] Bytes to write.
81
+ def push(bytes)
82
+ if bytes.length != @entry_bytes
83
+ PEROBS.log.fatal "All stack entries must be #{@entry_bytes} " +
84
+ "long. This entry is #{bytes.length} bytes long."
85
+ end
86
+ begin
87
+ @f.seek(0, IO::SEEK_END)
88
+ @f.write(bytes)
89
+ rescue => e
90
+ PEROBS.log.fatal "Cannot push to stack file #{@file_name}: #{e.message}"
91
+ end
92
+ end
93
+
94
+ # Pop the last entry from the stack file.
95
+ # @return [String or nil] Popped entry or nil if stack is already empty.
96
+ def pop
97
+ begin
98
+ return nil if @f.size == 0
99
+
100
+ @f.seek(-@entry_bytes, IO::SEEK_END)
101
+ bytes = @f.read(@entry_bytes)
102
+ @f.truncate(@f.size - @entry_bytes)
103
+ @f.flush
104
+ rescue => e
105
+ PEROBS.log.fatal "Cannot pop from stack file #{@file_name}: " +
106
+ e.message
107
+ end
108
+
109
+ bytes
110
+ end
111
+
112
+ # Remove all entries from the stack.
113
+ def clear
114
+ @f.truncate(0)
115
+ @f.flush
116
+ end
117
+
118
+ # Iterate over all entries in the stack and call the given block for the
119
+ # bytes.
120
+ def each
121
+ @f.seek(0)
122
+ while !@f.eof
123
+ yield(@f.read(@entry_bytes))
124
+ end
125
+ end
126
+
127
+ # Return the content of the stack as an Array.
128
+ # @return [Array]
129
+ def to_ary
130
+ a = []
131
+ each { |bytes| a << bytes }
132
+ a
133
+ end
134
+
135
+ end
136
+
137
+ end