perobs 2.5.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,204 +0,0 @@
1
- # encoding: UTF-8
2
- #
3
- # = FreeSpaceManager.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
- require 'perobs/StackFile'
30
-
31
- module PEROBS
32
-
33
- # The FreeSpaceManager keeps a list of the free spaces in the FlatFile. Each
34
- # space is stored with address and size. The data is persisted in the file
35
- # system. Internally the free spaces are stored in different pools. Each
36
- # pool holds spaces that are at least of a given size and not as big as the
37
- # next pool up. Pool entry minimum sizes increase by a factor of 2 from
38
- # pool to pool.
39
- class FreeSpaceManager
40
-
41
- # Create a new FreeSpaceManager object in the specified directory.
42
- # @param dir [String] directory path
43
- def initialize(dir)
44
- @dir = dir
45
- @pools = []
46
- end
47
-
48
- # Open the pool files.
49
- def open
50
- Dir.glob(File.join(@dir, 'free_list_*.stack')).each do |file|
51
- basename = File.basename(file)
52
- # Cut out the pool index from the file name.
53
- index = basename[10..-7].to_i
54
- @pools[index] = StackFile.new(@dir, basename[0..-7], 2 * 8)
55
- end
56
- end
57
-
58
- # Close all pool files.
59
- def close
60
- @pools = []
61
- end
62
-
63
- # Add a new space with a given address and size.
64
- # @param address [Integer] Starting address of the space
65
- # @param size [Integer] size of the space in bytes
66
- def add_space(address, size)
67
- if size <= 0
68
- PEROBS.log.fatal "Size (#{size}) must be larger than 0."
69
- end
70
- pool_index = msb(size)
71
- new_pool(pool_index) unless @pools[pool_index]
72
- push_pool(pool_index, [ address, size ].pack('QQ'))
73
- end
74
-
75
- # Get a space that has at least the requested size.
76
- # @param size [Integer] Required size in bytes
77
- # @return [Array] Touple with address and actual size of the space.
78
- def get_space(size)
79
- if size <= 0
80
- PEROBS.log.fatal "Size (#{size}) must be larger than 0."
81
- end
82
- # When we search for a free space we need to search the pool that
83
- # corresponds to (size - 1) * 2. It is the pool that has the spaces that
84
- # are at least as big as size.
85
- pool_index = size == 1 ? 0 : msb(size - 1) + 1
86
- unless @pools[pool_index]
87
- return nil
88
- else
89
- return nil unless (entry = pop_pool(pool_index))
90
- sp_address, sp_size = entry.unpack('QQ')
91
- if sp_size < size
92
- PEROBS.log.fatal "Space at address #{sp_address} is too small. " +
93
- "Must be at least #{size} bytes but is only #{sp_size} bytes."
94
- end
95
- [ sp_address, sp_size ]
96
- end
97
- end
98
-
99
- # Clear all pools and forget any registered spaces.
100
- def clear
101
- @pools.each do |pool|
102
- if pool
103
- pool.open
104
- pool.clear
105
- pool.close
106
- end
107
- end
108
- close
109
- end
110
-
111
- # Check if there is a space in the free space lists that matches the
112
- # address and the size.
113
- # @param [Integer] address Address of the space
114
- # @param [Integer] size Length of the space in bytes
115
- # @return [Boolean] True if space is found, false otherwise
116
- def has_space?(address, size)
117
- unless (pool = @pools[msb(size)])
118
- return false
119
- end
120
-
121
- pool.open
122
- pool.each do |entry|
123
- sp_address, sp_size = entry.unpack('QQ')
124
- if address == sp_address
125
- if size != sp_size
126
- PEROBS.log.fatal "FreeSpaceManager has space with different " +
127
- "size"
128
- end
129
- pool.close
130
- return true
131
- end
132
- end
133
-
134
- pool.close
135
- false
136
- end
137
-
138
- def check(flat_file)
139
- @pools.each do |pool|
140
- next unless pool
141
-
142
- pool.open
143
- pool.each do |entry|
144
- address, size = entry.unpack('QQ')
145
- unless flat_file.has_space?(address, size)
146
- PEROBS.log.error "FreeSpaceManager has space that isn't " +
147
- "available in the FlatFile."
148
- return false
149
- end
150
- end
151
- pool.close
152
- end
153
-
154
- true
155
- end
156
-
157
- def inspect
158
- '[' + @pools.map do |p|
159
- if p
160
- p.open
161
- r = p.to_ary.map { |bs| bs.unpack('QQ')}.inspect
162
- p.close
163
- r
164
- else
165
- 'nil'
166
- end
167
- end.join(', ') + ']'
168
- end
169
-
170
- private
171
-
172
- def new_pool(index)
173
- # The file name pattern for the pool files.
174
- filename = "free_list_#{index}"
175
- @pools[index] = sf = StackFile.new(@dir, filename, 2 * 8)
176
- end
177
-
178
- def push_pool(index, value)
179
- pool = @pools[index]
180
- pool.open
181
- pool.push(value)
182
- pool.close
183
- end
184
-
185
- def pop_pool(index)
186
- pool = @pools[index]
187
- pool.open
188
- value = pool.pop
189
- pool.close
190
-
191
- value
192
- end
193
-
194
- def msb(i)
195
- unless i > 0
196
- PEROBS.log.fatal "i must be larger than 0"
197
- end
198
- i.to_s(2).length - 1
199
- end
200
-
201
- end
202
-
203
- end
204
-
@@ -1,145 +0,0 @@
1
- # encoding: UTF-8
2
- #
3
- # = IndexTree.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
- require 'perobs/FixedSizeBlobFile'
30
- require 'perobs/IndexTreeNode'
31
-
32
- module PEROBS
33
-
34
- # The IndexTree maps the object ID to the address in the FlatFile. The
35
- # search in the tree is much faster than the linear search in the FlatFile.
36
- class IndexTree
37
-
38
- attr_reader :nodes, :ids
39
-
40
- def initialize(db_dir)
41
- # Directory path used to store the files.
42
- @db_dir = db_dir
43
-
44
- # This FixedSizeBlobFile contains the nodes of the IndexTree.
45
- @nodes = FixedSizeBlobFile.new(db_dir, 'database_index',
46
- IndexTreeNode::NODE_BYTES)
47
-
48
- # The node sequence usually only reveals a partial match with the
49
- # requested ID. So, the leaves of the tree point to the object_id_index
50
- # file which contains the full object ID and the address of the
51
- # corresponding object in the FlatFile.
52
- @ids = FixedSizeBlobFile.new(db_dir, 'object_id_index', 2 * 8)
53
- end
54
-
55
- # Open the tree files.
56
- def open
57
- @nodes.open
58
- @ids.open
59
- @root = IndexTreeNode.new(self, 0, 0)
60
- end
61
-
62
- # Close the tree files.
63
- def close
64
- @ids.close
65
- @nodes.close
66
- @root = nil
67
- end
68
-
69
- # Flush out all unwritten data
70
- def sync
71
- @ids.sync
72
- @nodes.sync
73
- end
74
-
75
- # Delete all data from the tree.
76
- def clear
77
- @nodes.clear
78
- @ids.clear
79
- @root = IndexTreeNode.new(self, 0, 0)
80
- end
81
-
82
- # Return an IndexTreeNode object that corresponds to the given address.
83
- # @param nibble [Fixnum] Index of the nibble the node should correspond to
84
- # @param address [Integer] Address of the node in @nodes or nil
85
- def get_node(nibble, address = nil)
86
- if nibble >= 16
87
- # We only support 64 bit keys, so nibble cannot be larger than 15.
88
- PEROBS.log.fatal "Nibble must be within 0 - 15 but is #{nibble}"
89
- end
90
- # We don't have a IndexTreeNode object yet for this node. Create it
91
- # with the data from the 'database_index' file.
92
- node = IndexTreeNode.new(self, nibble, address)
93
- return node
94
- end
95
-
96
- # Delete a node from the tree that corresponds to the address.
97
- # @param nibble [Fixnum] The corresponding nibble for the node
98
- # @param address [Integer] The address of the node in @nodes
99
- def delete_node(nibble, address)
100
- if nibble >= 16
101
- # We only support 64 bit keys, so nibble cannot be larger than 15.
102
- PEROBS.log.fatal "Nibble must be within 0 - 15 but is #{nibble}"
103
- end
104
-
105
- # Delete it from the 'database_index' file.
106
- @nodes.delete_blob(address)
107
- end
108
-
109
- # Store a ID/value touple into the tree. The value can later be retrieved
110
- # by the ID again. IDs are always unique in the tree. If the ID already
111
- # exists in the tree, the value will be overwritten.
112
- # @param id [Integer] ID or key
113
- # @param value [Integer] value to store
114
- def put_value(id, value)
115
- @root.put_value(id, value)
116
- end
117
-
118
- # Retrieve the value that was stored with the given ID.
119
- # @param id [Integer] ID of the value to retrieve
120
- # @return [Fixnum] value
121
- def get_value(id)
122
- @root.get_value(id)
123
- end
124
-
125
- # Delete the value with the given ID.
126
- # @param [Integer] id
127
- def delete_value(id)
128
- @root.delete_value(id)
129
- end
130
-
131
- # Check if the index is OK and matches the flat_file data.
132
- def check(flat_file)
133
- @root.check(flat_file, 0)
134
- end
135
-
136
- # Convert the tree into a human readable form.
137
- # @return [String]
138
- def inspect
139
- @root.inspect
140
- end
141
-
142
- end
143
-
144
- end
145
-
@@ -1,316 +0,0 @@
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
- # How many levels of the tree should be kept in memory.
48
- CACHED_LEVELS = 4
49
-
50
- # Create a new IndexTreeNode.
51
- # @param tree [IndexTree] The tree this node belongs to
52
- # @param nibble_idx [Fixnum] the level of the node in the tree (root
53
- # being 0)
54
- # @param address [Integer] The address of this node in the blob file
55
- def initialize(tree, nibble_idx, address = nil)
56
- @tree = tree
57
- if nibble_idx >= 16
58
- # We are processing 64 bit numbers, so we have at most 16 nibbles.
59
- PEROBS.log.fatal 'nibble must be 0 - 15'
60
- end
61
- @nibble_idx = nibble_idx
62
- if (@address = address).nil? || !read_node
63
- # Create a new node if none with this address exists already.
64
- @entry_types = 0
65
- @entries = ::Array.new(ENTRIES, 0)
66
- @address = @tree.nodes.free_address
67
- write_node
68
- end
69
- # These are the pointers that point to the next level of IndexTreeNode
70
- # elements.
71
- @node_ptrs = ::Array.new(ENTRIES, nil)
72
- end
73
-
74
- # Store a value for the given ID. Existing values will be overwritten.
75
- # @param id [Integer] ID (or key)
76
- # @param value [Integer] value
77
- def put_value(id, value)
78
- index = calc_index(id)
79
- case get_entry_type(index)
80
- when 0
81
- # The entry is still empty. Store the id and value and set the entry
82
- # to holding a value (1).
83
- set_entry_type(index, 1)
84
- @entries[index] = address = @tree.ids.free_address
85
- store_id_and_value(address, id, value)
86
- write_node
87
- when 1
88
- existing_value = @entries[index]
89
- existing_id, existing_address = get_id_and_address(existing_value)
90
- if id == existing_id
91
- if value != existing_address
92
- # The entry already holds another value.
93
- store_id_and_value(@entries[index], id, value)
94
- end
95
- else
96
- # The entry already holds a value. We need to create a new node and
97
- # store the existing value and the new value in it.
98
- # First get the exiting value of the entry and the corresponding ID.
99
- # Create a new node.
100
- node = @tree.get_node(@nibble_idx + 1)
101
- # The entry of the current node is now a reference to the new node.
102
- set_entry_type(index, 2)
103
- @entries[index] = node.address
104
- @node_ptrs[index] = node if @nibble_idx < CACHED_LEVELS
105
- # Store the existing value and the new value with their IDs.
106
- node.set_entry(existing_id, existing_value)
107
- node.put_value(id, value)
108
- end
109
- write_node
110
- when 2
111
- # The entry is a reference to another node.
112
- get_node(index).put_value(id, value)
113
- else
114
- PEROBS.log.fatal "Illegal node type #{get_entry_type(index)}"
115
- end
116
- end
117
-
118
- # Retrieve the value for the given ID.
119
- # @param id [Integer] ID (or key)
120
- # @return [Integer] value or nil
121
- def get_value(id)
122
- index = calc_index(id)
123
- case get_entry_type(index)
124
- when 0
125
- # There is no entry for this ID.
126
- return nil
127
- when 1
128
- # There is a value stored for the ID part that we have seen so far. We
129
- # still need to compare the requested ID with the full ID to determine
130
- # a match.
131
- stored_id, address = get_id_and_address(@entries[index])
132
- if id == stored_id
133
- # We have a match. Return the value.
134
- return address
135
- else
136
- # Just a partial match of the least significant nibbles.
137
- return nil
138
- end
139
- when 2
140
- # The entry is a reference to another node. Just follow it and look at
141
- # the next nibble.
142
- return get_node(index).get_value(id)
143
- else
144
- PEROBS.log.fatal "Illegal node type #{get_entry_type(index)}"
145
- end
146
- end
147
-
148
- # Delete the entry for the given ID.
149
- # @param id [Integer] ID or key
150
- # @return [Boolean] True if a key was found and deleted, otherwise false.
151
- def delete_value(id)
152
- index = calc_index(id)
153
- case get_entry_type(index)
154
- when 0
155
- # There is no entry for this ID.
156
- return false
157
- when 1
158
- # We have a value. Check that the ID matches and delete the value.
159
- stored_id, address = get_id_and_address(@entries[index])
160
- if id == stored_id
161
- @tree.ids.delete_blob(@entries[index])
162
- @entries[index] = 0
163
- @node_ptrs[index] = nil
164
- set_entry_type(index, 0)
165
- write_node
166
- return true
167
- else
168
- # Just a partial ID match.
169
- return false
170
- end
171
- when 2
172
- # The entry is a reference to another node.
173
- node = get_node(index)
174
- result = node.delete_value(id)
175
- if node.empty?
176
- # If the sub-node is empty after the delete we delete the whole
177
- # sub-node.
178
- @tree.delete_node(@nibble_idx + 1, @entries[index])
179
- # Eliminate the reference to the sub-node and update this node in
180
- # the file.
181
- set_entry_type(index, 0)
182
- write_node
183
- end
184
- return result
185
- else
186
- PEROBS.node.fatal "Illegal node type #{get_entry_type(index)}"
187
- end
188
- end
189
-
190
- # Recursively check this node and all sub nodes. Compare the found
191
- # ID/address pairs with the corresponding entry in the given FlatFile.
192
- # @param flat_file [FlatFile]
193
- # @param tree_level [Fixnum] Assumed level in the tree. Must correspond
194
- # with @nibble_idx
195
- # @return [Boolean] true if no errors were found, false otherwise
196
- def check(flat_file, tree_level)
197
- if tree_level >= 16
198
- PEROBS.log.error "IndexTreeNode level (#{tree_level}) too large"
199
- return false
200
- end
201
- ENTRIES.times do |index|
202
- case get_entry_type(index)
203
- when 0
204
- # Empty entry, nothing to do here.
205
- when 1
206
- # There is a value stored for the ID part that we have seen so far.
207
- # We still need to compare the requested ID with the full ID to
208
- # determine a match.
209
- id, address = get_id_and_address(@entries[index])
210
- unless flat_file.has_id_at?(id, address)
211
- PEROBS.log.error "The entry for ID #{id} in the index was not " +
212
- "found in the FlatFile at address #{address}"
213
- return false
214
- end
215
- when 2
216
- # The entry is a reference to another node. Just follow it and look
217
- # at the next nibble.
218
- unless get_node(index).check(flat_file, tree_level + 1)
219
- return false
220
- end
221
- else
222
- return false
223
- end
224
- end
225
-
226
- true
227
- end
228
-
229
- # Convert the node and all sub-nodes to human readable format.
230
- def inspect
231
- str = "{\n"
232
- 0.upto(15) do |i|
233
- case get_entry_type(i)
234
- when 0
235
- # Don't show empty entries.
236
- when 1
237
- id, address = get_id_and_address(@entries[i])
238
- str += " #{id} => #{address},\n"
239
- when 2
240
- str += " " + get_node(i).inspect.gsub(/\n/, "\n ")
241
- end
242
- end
243
- str + "}\n"
244
- end
245
-
246
- # Utility method to set the value of an existing node entry.
247
- # @param id [Integer] ID or key
248
- # @param value [Integer] value to set. Note that this value must be an
249
- # address from the ids list.
250
- def set_entry(id, value)
251
- index = calc_index(id)
252
- set_entry_type(index, 1)
253
- @entries[index] = value
254
- end
255
-
256
- # Check if the node is empty.
257
- # @return [Boolean] True if all entries are empty.
258
- def empty?
259
- @entry_types == 0
260
- end
261
-
262
- private
263
-
264
- def calc_index(id)
265
- (id >> (4 * @nibble_idx)) & 0xF
266
- end
267
-
268
- def get_node(index)
269
- unless (node = @node_ptrs[index])
270
- node = @tree.get_node(@nibble_idx + 1, @entries[index])
271
- # We only cache the first levels of the tree to limit the memory
272
- # consumption.
273
- @node_ptrs[index] = node if @nibble_idx < CACHED_LEVELS
274
- end
275
-
276
- node
277
- end
278
-
279
- def read_node
280
- return false unless (bytes = @tree.nodes.retrieve_blob(@address))
281
- @entry_types = bytes[0, TYPE_BYTES].unpack('L')[0]
282
- @entries = bytes[TYPE_BYTES, ENTRIES * ENTRY_BYTES].unpack('Q16')
283
- true
284
- end
285
-
286
- def write_node
287
- bytes = ([ @entry_types ] + @entries).pack('LQ16')
288
- @tree.nodes.store_blob(@address, bytes)
289
- end
290
-
291
- def set_entry_type(index, type)
292
- if index < 0 || index > 15
293
- PEROBS.log.fatal "Index must be between 0 and 15"
294
- end
295
- @entry_types = ((@entry_types & ~(0x3 << 2 * index)) |
296
- ((type & 0x3) << 2 * index)) & 0xFFFFFFFF
297
- end
298
-
299
- def get_entry_type(index)
300
- if index < 0 || index > 15
301
- PEROBS.log.fatal "Index must be between 0 and 15"
302
- end
303
- (@entry_types >> 2 * index) & 0x3
304
- end
305
-
306
- def get_id_and_address(id_address)
307
- @tree.ids.retrieve_blob(id_address).unpack('QQ')
308
- end
309
-
310
- def store_id_and_value(address, id, value)
311
- @tree.ids.store_blob(address, [ id, value ].pack('QQ'))
312
- end
313
-
314
- end
315
-
316
- end