perobs 2.5.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/perobs/Array.rb +1 -1
- data/lib/perobs/BTree.rb +233 -0
- data/lib/perobs/BTreeDB.rb +2 -0
- data/lib/perobs/BTreeNode.rb +706 -0
- data/lib/perobs/BTreeNodeCache.rb +107 -0
- data/lib/perobs/BTreeNodeLink.rb +141 -0
- data/lib/perobs/EquiBlobsFile.rb +570 -0
- data/lib/perobs/FlatFile.rb +179 -78
- data/lib/perobs/FlatFileBlobHeader.rb +92 -17
- data/lib/perobs/FlatFileDB.rb +16 -7
- data/lib/perobs/LockFile.rb +181 -0
- data/lib/perobs/Object.rb +2 -1
- data/lib/perobs/SpaceTree.rb +181 -0
- data/lib/perobs/SpaceTreeNode.rb +672 -0
- data/lib/perobs/SpaceTreeNodeCache.rb +76 -0
- data/lib/perobs/SpaceTreeNodeLink.rb +103 -0
- data/lib/perobs/Store.rb +27 -13
- data/lib/perobs/version.rb +1 -1
- data/test/BTree_spec.rb +128 -0
- data/test/EquiBlobsFile_spec.rb +199 -0
- data/test/FlatFileDB_spec.rb +63 -9
- data/test/LockFile_spec.rb +133 -0
- data/test/SpaceTree_spec.rb +245 -0
- data/test/Store_spec.rb +3 -0
- data/test/spec_helper.rb +13 -0
- metadata +21 -13
- data/lib/perobs/FixedSizeBlobFile.rb +0 -193
- data/lib/perobs/FreeSpaceManager.rb +0 -204
- data/lib/perobs/IndexTree.rb +0 -145
- data/lib/perobs/IndexTreeNode.rb +0 -316
- data/test/FixedSizeBlobFile_spec.rb +0 -91
- data/test/FreeSpaceManager_spec.rb +0 -91
- data/test/IndexTree_spec.rb +0 -118
@@ -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
|
-
|
data/lib/perobs/IndexTree.rb
DELETED
@@ -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
|
-
|
data/lib/perobs/IndexTreeNode.rb
DELETED
@@ -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
|