perobs 2.3.1 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,249 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # = FlatFileDB.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
+
28
+ require 'fileutils'
29
+ require 'zlib'
30
+
31
+ require 'perobs/Log'
32
+ require 'perobs/DataBase'
33
+ require 'perobs/FlatFile'
34
+
35
+ module PEROBS
36
+
37
+ # The FlatFileDB is a storage backend that uses a single flat file to store
38
+ # the value blobs.
39
+ class FlatFileDB < DataBase
40
+
41
+ # This version number increases whenever the on-disk format changes in a
42
+ # way that requires conversion actions after an update.
43
+ VERSION = 1
44
+
45
+ attr_reader :max_blob_size
46
+
47
+ # Create a new FlatFileDB object.
48
+ # @param db_name [String] name of the DB directory
49
+ # @param options [Hash] options to customize the behavior. Currently only
50
+ # the following options are supported:
51
+ # :serializer : Can be :marshal, :json, :yaml
52
+ def initialize(db_name, options = {})
53
+ super(options[:serializer] || :json)
54
+
55
+ @db_dir = db_name
56
+ # Create the database directory if it doesn't exist yet.
57
+ ensure_dir_exists(@db_dir)
58
+ PEROBS.log.open(File.join(@db_dir, 'log'))
59
+ check_version
60
+
61
+ # Read the existing DB config.
62
+ @config = get_hash('config')
63
+ check_option('serializer')
64
+
65
+ put_hash('config', @config)
66
+ end
67
+
68
+ # Open the FlatFileDB for transactions.
69
+ def open
70
+ @flat_file = FlatFile.new(@db_dir)
71
+ @flat_file.open
72
+ PEROBS.log.info "FlatFile opened"
73
+ end
74
+
75
+ # Close the FlatFileDB.
76
+ def close
77
+ @flat_file.close
78
+ @flat_file = nil
79
+ PEROBS.log.info "FlatFile closed"
80
+ end
81
+
82
+ # Delete the entire database. The database is no longer usable after this
83
+ # method was called.
84
+ def delete_database
85
+ FileUtils.rm_rf(@db_dir)
86
+ end
87
+
88
+ def FlatFileDB::delete_db(db_name)
89
+ FileUtils.rm_rf(db_name)
90
+ end
91
+
92
+ # Return true if the object with given ID exists
93
+ # @param id [Fixnum or Bignum]
94
+ def include?(id)
95
+ !@flat_file.find_obj_addr_by_id(id).nil?
96
+ end
97
+
98
+ # Store a simple Hash as a JSON encoded file into the DB directory.
99
+ # @param name [String] Name of the hash. Will be used as file name.
100
+ # @param hash [Hash] A Hash that maps String objects to strings or
101
+ # numbers.
102
+ def put_hash(name, hash)
103
+ file_name = File.join(@db_dir, name + '.json')
104
+ begin
105
+ File.write(file_name, hash.to_json)
106
+ rescue => e
107
+ PEROBS.log.fatal "Cannot write hash file '#{file_name}': #{e.message}"
108
+ end
109
+ end
110
+
111
+ # Load the Hash with the given name.
112
+ # @param name [String] Name of the hash.
113
+ # @return [Hash] A Hash that maps String objects to strings or numbers.
114
+ def get_hash(name)
115
+ file_name = File.join(@db_dir, name + '.json')
116
+ return ::Hash.new unless File.exist?(file_name)
117
+
118
+ begin
119
+ json = File.read(file_name)
120
+ rescue => e
121
+ PEROBS.log.fatal "Cannot read hash file '#{file_name}': #{e.message}"
122
+ end
123
+ JSON.parse(json, :create_additions => true)
124
+ end
125
+
126
+ # Store the given object into the cluster files.
127
+ # @param obj [Hash] Object as defined by PEROBS::ObjectBase
128
+ def put_object(obj, id)
129
+ @flat_file.delete_obj_by_id(id)
130
+ @flat_file.write_obj_by_id(id, serialize(obj))
131
+ end
132
+
133
+ # Load the given object from the filesystem.
134
+ # @param id [Fixnum or Bignum] object ID
135
+ # @return [Hash] Object as defined by PEROBS::ObjectBase or nil if ID does
136
+ # not exist
137
+ def get_object(id)
138
+ if (raw_obj = @flat_file.read_obj_by_id(id))
139
+ return deserialize(raw_obj)
140
+ else
141
+ nil
142
+ end
143
+ end
144
+
145
+ # This method must be called to initiate the marking process.
146
+ def clear_marks
147
+ t = Time.now
148
+ PEROBS.log.info "Clearing all marks"
149
+ @flat_file.clear_all_marks
150
+ PEROBS.log.info "All marks cleared in #{Time.now - t} seconds"
151
+ end
152
+
153
+ # Permanently delete all objects that have not been marked. Those are
154
+ # orphaned and are no longer referenced by any actively used object.
155
+ # @return [Array] List of IDs that have been removed from the DB.
156
+ def delete_unmarked_objects
157
+ t = Time.now
158
+ PEROBS.log.info "Deleting unmarked objects"
159
+ retval = @flat_file.delete_unmarked_objects
160
+ PEROBS.log.info "Unmarked objects deleted in #{Time.now - t} seconds"
161
+ retval
162
+ end
163
+
164
+ # Mark an object.
165
+ # @param id [Fixnum or Bignum] ID of the object to mark
166
+ def mark(id)
167
+ @flat_file.mark_obj_by_id(id)
168
+ end
169
+
170
+ # Check if the object is marked.
171
+ # @param id [Fixnum or Bignum] ID of the object to check
172
+ # @param ignore_errors [Boolean] If set to true no errors will be raised
173
+ # for non-existing objects.
174
+ def is_marked?(id, ignore_errors = false)
175
+ @flat_file.is_marked_by_id?(id)
176
+ end
177
+
178
+ # Basic consistency check.
179
+ # @param repair [TrueClass/FalseClass] True if found errors should be
180
+ # repaired.
181
+ def check_db(repair = false)
182
+ t = Time.now
183
+ PEROBS.log.info "check_db started"
184
+ @flat_file.check(repair)
185
+ PEROBS.log.info "check_db completed in #{Time.now - t} seconds"
186
+ end
187
+
188
+ # Check if the stored object is syntactically correct.
189
+ # @param id [Fixnum/Bignum] Object ID
190
+ # @param repair [TrueClass/FalseClass] True if an repair attempt should be
191
+ # made.
192
+ # @return [TrueClass/FalseClass] True if the object is OK, otherwise
193
+ # false.
194
+ def check(id, repair)
195
+ begin
196
+ get_object(id)
197
+ rescue => e
198
+ PEROBS.log.warn "Cannot read object with ID #{id}: #{e.message}"
199
+ return false
200
+ end
201
+
202
+ true
203
+ end
204
+
205
+ # Store the given serialized object into the cluster files. This method is
206
+ # for internal use only!
207
+ # @param raw [String] Serialized Object as defined by PEROBS::ObjectBase
208
+ # @param id [Fixnum or Bignum] Object ID
209
+ def put_raw_object(raw, id)
210
+ @flat_file.delete_obj_(id)
211
+ @flat_file.write_obj_by_id(id, raw)
212
+ end
213
+
214
+ private
215
+
216
+ def check_version
217
+ version_file = File.join(@db_dir, 'version')
218
+ version = VERSION
219
+
220
+ if File.exist?(version_file)
221
+ begin
222
+ version = File.read(version_file).to_i
223
+ rescue => e
224
+ PEROBS.log.fatal "Cannot read version number file " +
225
+ "'#{version_file}': " + e.message
226
+ end
227
+ else
228
+ write_version_file(version_file)
229
+ end
230
+
231
+ if version > VERSION
232
+ PEROBS.log.fatal "Cannot downgrade the FlatFile database from " +
233
+ "version #{version} to version #{VERSION}"
234
+ end
235
+ end
236
+
237
+ def write_version_file(version_file)
238
+ begin
239
+ File.write(version_file, "#{VERSION}")
240
+ rescue => e
241
+ PEROBS.log.fatal "Cannot write version number file " +
242
+ "'#{version_file}': " + e.message
243
+ end
244
+ end
245
+
246
+ end
247
+
248
+ end
249
+
@@ -0,0 +1,204 @@
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/Hash.rb CHANGED
@@ -2,7 +2,7 @@
2
2
  #
3
3
  # = Hash.rb -- Persistent Ruby Object Store
4
4
  #
5
- # Copyright (c) 2015 by Chris Schlaeger <chris@taskjuggler.org>
5
+ # Copyright (c) 2015, 2016 by Chris Schlaeger <chris@taskjuggler.org>
6
6
  #
7
7
  # MIT License
8
8
  #
@@ -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/ObjectBase'
29
30
 
30
31
  module PEROBS
@@ -48,7 +49,7 @@ module PEROBS
48
49
  :==, :[], :assoc, :compare_by_identity, :compare_by_identity?, :default,
49
50
  :default_proc, :each, :each_key, :each_pair, :each_value, :empty?,
50
51
  :eql?, :fetch, :flatten, :has_key?, :has_value?, :hash, :include?,
51
- :inspect, :invert, :key, :key?, :keys, :length, :member?, :merge,
52
+ :invert, :key, :key?, :keys, :length, :member?, :merge,
52
53
  :pretty_print, :pretty_print_cycle, :rassoc, :reject, :select, :size,
53
54
  :to_a, :to_h, :to_hash, :to_s, :value?, :values, :values_at
54
55
  ] + Enumerable.instance_methods).uniq.each do |method_sym|
@@ -104,6 +105,7 @@ module PEROBS
104
105
  @data.delete_if do |k, v|
105
106
  v && v.respond_to?(:is_poxreference?) && v.id == id
106
107
  end
108
+ @store.cache.cache_write(self)
107
109
  end
108
110
 
109
111
  # Restore the persistent data from a single data structure.
@@ -117,6 +119,17 @@ module PEROBS
117
119
  @data
118
120
  end
119
121
 
122
+ # Textual dump for debugging purposes
123
+ # @return [String]
124
+ def inspect
125
+ "<#{self.class}:#{@_id}>\n{\n" +
126
+ @data.map do |k, v|
127
+ " #{k.inspect} => " + (v.respond_to?(:is_poxreference?) ?
128
+ "<PEROBS::ObjectBase:#{v._id}>" : v.inspect)
129
+ end.join(",\n") +
130
+ "\n}\n"
131
+ end
132
+
120
133
  private
121
134
 
122
135
  def _serialize
@@ -130,9 +143,9 @@ module PEROBS
130
143
  # objects should not be used directly. The library only exposes them
131
144
  # via POXReference proxy objects.
132
145
  if v.is_a?(ObjectBase)
133
- raise RuntimeError, 'A PEROBS::ObjectBase object escaped! ' +
146
+ PEROBS.log.fatal 'A PEROBS::ObjectBase object escaped! ' +
134
147
  "It is stored in a PEROBS::Hash with key #{k.inspect}. " +
135
- 'Have you used self() instead of myself() to' +
148
+ 'Have you used self() instead of myself() to ' +
136
149
  "get the reference of this PEROBS object?\n" +
137
150
  v.inspect
138
151
  end
@@ -0,0 +1,164 @@
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/FixedSizeBlobFile'
29
+ require 'perobs/IndexTreeNode'
30
+
31
+ module PEROBS
32
+
33
+ # The IndexTree maps the object ID to the address in the FlatFile. The
34
+ # search in the tree is much faster than the linear search in the FlatFile.
35
+ class IndexTree
36
+
37
+ # Determines how many levels of the IndexTree will be kept in memory to
38
+ # accerlerate the access. A number of 7 will keep up to 21845 entries in
39
+ # the cache but will accelerate the access to the FlatFile address.
40
+ MAX_CACHED_LEVEL = 7
41
+
42
+ attr_reader :nodes, :ids
43
+
44
+ def initialize(db_dir)
45
+ # Directory path used to store the files.
46
+ @db_dir = db_dir
47
+
48
+ # This FixedSizeBlobFile contains the nodes of the IndexTree.
49
+ @nodes = FixedSizeBlobFile.new(db_dir, 'database_index',
50
+ IndexTreeNode::NODE_BYTES)
51
+
52
+ # The node sequence usually only reveals a partial match with the
53
+ # requested ID. So, the leaves of the tree point to the object_id_index
54
+ # file which contains the full object ID and the address of the
55
+ # corresponding object in the FlatFile.
56
+ @ids = FixedSizeBlobFile.new(db_dir, 'object_id_index', 2 * 8)
57
+
58
+ # The first MAX_CACHED_LEVEL levels of nodes will be cached in memory to
59
+ # improve access times.
60
+ @node_cache = {}
61
+ end
62
+
63
+ # Open the tree files.
64
+ def open
65
+ @nodes.open
66
+ @ids.open
67
+ @root = IndexTreeNode.new(self, 0, 0)
68
+ end
69
+
70
+ # Close the tree files.
71
+ def close
72
+ @ids.close
73
+ @nodes.close
74
+ end
75
+
76
+ # Flush out all unwritten data
77
+ def sync
78
+ @ids.sync
79
+ @nodes.sync
80
+ end
81
+
82
+ # Delete all data from the tree.
83
+ def clear
84
+ @nodes.clear
85
+ @ids.clear
86
+ @node_cache = {}
87
+ @root = IndexTreeNode.new(self, 0, 0)
88
+ end
89
+
90
+ # Return an IndexTreeNode object that corresponds to the given address.
91
+ # @param nibble [Fixnum] Index of the nibble the node should correspond to
92
+ # @param address [Integer] Address of the node in @nodes or nil
93
+ def get_node(nibble, address = nil)
94
+ # Generate a mask for the least significant bits up to and including the
95
+ # nibble.
96
+ mask = (2 ** ((1 + nibble) * 4)) - 1
97
+ if address && (node = @node_cache[address & mask])
98
+ # We have an address and have found the node in the node cache.
99
+ return node
100
+ else
101
+ # We don't have a IndexTreeNode object yet for this node. Create it
102
+ # with the data from the 'database_index' file.
103
+ node = IndexTreeNode.new(self, nibble, address)
104
+ # Add the node to the node cache if it's up to MAX_CACHED_LEVEL levels
105
+ # down from the root.
106
+ @node_cache[address & mask] = node if nibble <= MAX_CACHED_LEVEL
107
+ return node
108
+ end
109
+ end
110
+
111
+ # Delete a node from the tree that corresponds to the address.
112
+ # @param nibble [Fixnum] The corresponding nibble for the node
113
+ # @param address [Integer] The address of the node in @nodes
114
+ def delete_node(nibble, address)
115
+ # First delete the node from the node cache.
116
+ mask = (2 ** ((1 + nibble) * 4)) - 1
117
+ @node_cache.delete(address & mask)
118
+ # Then delete it from the 'database_index' file.
119
+ @nodes.delete_blob(address)
120
+ end
121
+
122
+ # Store a ID/value touple into the tree. The value can later be retrieved
123
+ # by the ID again. IDs are always unique in the tree. If the ID already
124
+ # exists in the tree, the value will be overwritten.
125
+ # @param id [Integer] ID or key
126
+ # @param value [Integer] value to store
127
+ def put_value(id, value)
128
+ #MAX_CACHED_LEVEL.downto(0) do |i|
129
+ # mask = (2 ** ((1 + i) * 4)) - 1
130
+ # if (node = @node_cache[value & mask])
131
+ # return node.put_value(id, value)
132
+ # end
133
+ #end
134
+ @root.put_value(id, value)
135
+ end
136
+
137
+ # Retrieve the value that was stored with the given ID.
138
+ # @param id [Integer] ID of the value to retrieve
139
+ # @return [Fixnum] value
140
+ def get_value(id)
141
+ @root.get_value(id)
142
+ end
143
+
144
+ # Delete the value with the given ID.
145
+ # @param [Integer] id
146
+ def delete_value(id)
147
+ @root.delete_value(id)
148
+ end
149
+
150
+ # Check if the index is OK and matches the flat_file data.
151
+ def check(flat_file)
152
+ @root.check(flat_file)
153
+ end
154
+
155
+ # Convert the tree into a human readable form.
156
+ # @return [String]
157
+ def inspect
158
+ @root.inspect
159
+ end
160
+
161
+ end
162
+
163
+ end
164
+