perobs 2.3.1 → 2.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|