perobs 2.3.1 → 2.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.
@@ -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