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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/perobs/Array.rb +17 -4
- data/lib/perobs/BTreeBlob.rb +22 -23
- data/lib/perobs/BTreeDB.rb +11 -12
- data/lib/perobs/Cache.rb +5 -4
- data/lib/perobs/DataBase.rb +19 -7
- data/lib/perobs/DynamoDB.rb +1 -1
- data/lib/perobs/FixedSizeBlobFile.rb +189 -0
- data/lib/perobs/FlatFile.rb +513 -0
- data/lib/perobs/FlatFileDB.rb +249 -0
- data/lib/perobs/FreeSpaceManager.rb +204 -0
- data/lib/perobs/Hash.rb +17 -4
- data/lib/perobs/IndexTree.rb +164 -0
- data/lib/perobs/IndexTreeNode.rb +296 -0
- data/lib/perobs/Log.rb +125 -0
- data/lib/perobs/Object.rb +10 -11
- data/lib/perobs/ObjectBase.rb +18 -5
- data/lib/perobs/StackFile.rb +137 -0
- data/lib/perobs/Store.rb +85 -19
- data/lib/perobs/TreeDB.rb +276 -0
- data/lib/perobs/version.rb +1 -1
- data/test/Array_spec.rb +11 -2
- data/test/FixedSizeBlobFile_spec.rb +91 -0
- data/test/FlatFileDB_spec.rb +56 -0
- data/test/FreeSpaceManager_spec.rb +91 -0
- data/test/Hash_spec.rb +11 -2
- data/test/IndexTree_spec.rb +118 -0
- data/test/Object_spec.rb +29 -17
- data/test/StackFile_spec.rb +113 -0
- data/test/Store_spec.rb +37 -3
- metadata +22 -3
@@ -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
|
-
|
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
|
-
|
119
|
-
|
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
|
-
"
|
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
|
-
|
213
|
+
PEROBS.log.fatal 'The referenced object is not part of this store'
|
213
214
|
end
|
214
215
|
elsif val.is_a?(ObjectBase)
|
215
|
-
|
216
|
-
|
217
|
-
|
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
|
-
|
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
|
data/lib/perobs/ObjectBase.rb
CHANGED
@@ -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
|
-
::
|
51
|
-
"
|
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
|
-
::
|
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
|