perobs 2.5.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,672 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # = SpaceTreeNode.rb -- Persistent Ruby Object Store
4
+ #
5
+ # Copyright (c) 2016, 2017 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/FlatFileBlobHeader'
30
+ require 'perobs/FlatFile'
31
+ require 'perobs/SpaceTreeNodeLink'
32
+
33
+ module PEROBS
34
+
35
+ # The SpaceTree keeps a complete list of all empty spaces in the FlatFile.
36
+ # Spaces are stored with size and address. The Tree is Tenerary Tree. The
37
+ # nodes can link to other nodes with smaller spaces, same spaces and bigger
38
+ # spaces.
39
+ class SpaceTreeNode
40
+
41
+ attr_accessor :size, :blob_address
42
+ attr_reader :node_address, :parent, :smaller, :equal, :larger
43
+
44
+ # Each node can hold a reference to the parent, a lower, equal or larger
45
+ # size node and the actual value and the address in the FlatFile. Each of
46
+ # these entries is 8 bytes long.
47
+ NODE_BYTES = 6 * 8
48
+ # The pack/unpack format.
49
+ NODE_BYTES_FORMAT = 'Q6'
50
+
51
+ # Create a new SpaceTreeNode object. If node_address is not nil, the data
52
+ # will be read from the SpaceTree file at the given node_address.
53
+ # @param tree [SpaceTree] Tree that the object should belong to
54
+ # @param parent [SpaceTreeNode] Parent node in the tree
55
+ # @param node_address [Integer] Address of the node in the file
56
+ # @param blob_address [Integer] Address of the free space blob
57
+ # @param size [Integer] Size of the free space blob
58
+ def initialize(tree, parent = nil, node_address = nil, blob_address = 0,
59
+ size = 0)
60
+ @tree = tree
61
+ if blob_address < 0
62
+ PEROBS.log.fatal "Node address (#{node_address}) must be larger than 0"
63
+ end
64
+ @blob_address = blob_address
65
+ @size = size
66
+ @smaller = @equal = @larger = nil
67
+ @node_address = node_address
68
+
69
+ unless node_address.nil? || node_address.is_a?(Integer)
70
+ PEROBS.log.fatal "node_address is not Integer: #{node_address.class}"
71
+ end
72
+
73
+ if node_address
74
+ # This must be an existing node. Try to read it and fill the instance
75
+ # variables.
76
+ if size != 0
77
+ PEROBS.log.fatal "If node_address is not nil size must be 0"
78
+ end
79
+ if blob_address != 0
80
+ PEROBS.log.fatal "If node_address is not nil blob_address must be 0"
81
+ end
82
+ unless read_node
83
+ PEROBS.log.fatal "SpaceTree node at address #{node_address} " +
84
+ "does not exist"
85
+ end
86
+ else
87
+ # This is a new node. Make sure the data is written to the file.
88
+ @node_address = @tree.nodes.free_address
89
+ self.parent = parent
90
+ end
91
+ end
92
+
93
+ # Add a new node for the given address and size to the tree.
94
+ # @param address [Integer] address of the free space
95
+ # @param size [Integer] size of the free space
96
+ def add_space(address, size)
97
+ node = self
98
+
99
+ loop do
100
+ if node.size == 0
101
+ # This happens only for the root node if the tree is empty.
102
+ node.set_size_and_address(size, address)
103
+ break
104
+ elsif size < node.size
105
+ # The new size is smaller than this node.
106
+ if node.smaller
107
+ # There is already a smaller node, so pass it on.
108
+ node = node.smaller
109
+ else
110
+ # There is no smaller node yet, so we create a new one as a
111
+ # smaller child of the current node.
112
+ node.set_link('@smaller',
113
+ @tree.new_node(node, address, size))
114
+ break
115
+ end
116
+ elsif size > node.size
117
+ # The new size is larger than this node.
118
+ if node.larger
119
+ # There is already a larger node, so pass it on.
120
+ node = node.larger
121
+ else
122
+ # There is no larger node yet, so we create a new one as a larger
123
+ # child of the current node.
124
+ node.set_link('@larger',
125
+ @tree.new_node(node, address, size))
126
+ break
127
+ end
128
+ else
129
+ # Same size as current node. Insert new node as equal child at top of
130
+ # equal list.
131
+ new_node = @tree.new_node(node, address, size)
132
+ new_node.set_link('@equal', node.equal)
133
+
134
+ node.set_link('@equal', new_node)
135
+
136
+ break
137
+ end
138
+ end
139
+ end
140
+
141
+ # Check if this node or any sub-node has an entry for the given address
142
+ # and size.
143
+ # @param address [Integer] address of the free space
144
+ # @param size [Integer] size of the free space
145
+ # @return [Boolean] True if found, otherwise false
146
+ def has_space?(address, size)
147
+ node = self
148
+ loop do
149
+ if node.blob_address == address
150
+ return true
151
+ elsif size < node.size && node.smaller
152
+ node = node.smaller
153
+ elsif size > node.size && node.larger
154
+ node = node.larger
155
+ elsif size == node.size && node.equal
156
+ node = node.equal
157
+ else
158
+ return false
159
+ end
160
+ end
161
+ end
162
+
163
+ # Return an address/size touple that matches exactly the requested size.
164
+ # Return nil if nothing was found.
165
+ # @param size [Integer] size of the free space
166
+ # @return [Array or nil] address, size touple or nil
167
+ def find_matching_space(size)
168
+ node = self
169
+
170
+ loop do
171
+ if node.size < size
172
+ if node.larger
173
+ # The current space is not yet large enough. If we have a larger sub
174
+ # node check that one next.
175
+ node = node.larger
176
+ else
177
+ break
178
+ end
179
+ elsif node.size == size
180
+ # We've found a space that is an exact match. Remove it from the
181
+ # list and return it.
182
+ address = node.blob_address
183
+ node.delete_node
184
+ return [ address, size ]
185
+ else
186
+ break
187
+ end
188
+ end
189
+
190
+ return nil
191
+ end
192
+
193
+ # Return an address/size touple that matches the requested size or is
194
+ # larger than the requested size plus the overhead for another blob.
195
+ # Return nil if nothing was found.
196
+ # @param size [Integer] size of the free space
197
+ # @return [Array or nil] address, size touple or nil
198
+ def find_equal_or_larger_space(size)
199
+ node = self
200
+
201
+ loop do
202
+ if node.size < size
203
+ if node.larger
204
+ # The current space is not yet large enough. If we have a larger sub
205
+ # node check that one next.
206
+ node = node.larger
207
+ else
208
+ break
209
+ end
210
+ elsif node.size == size ||
211
+ node.size >= size * 2 + FlatFileBlobHeader::LENGTH
212
+ # We've found a space that is either a perfect match or is large
213
+ # enough to hold at least one more record. Remove it from the list and
214
+ # return it.
215
+ actual_size = node.size
216
+ address = node.blob_address
217
+ node.delete_node
218
+ return [ address, actual_size ]
219
+ elsif node.smaller
220
+ # The current space is larger than size but not large enough for an
221
+ # additional record. So check if we have a perfect match in the
222
+ # smaller brach if available.
223
+ node = node.smaller
224
+ else
225
+ break
226
+ end
227
+ end
228
+
229
+ return nil
230
+ end
231
+
232
+ # Remove a smaller/equal/larger link from the current node.
233
+ # @param child_node [SpaceTreeNodeLink] node to remove
234
+ def unlink_node(child_node)
235
+ if @smaller == child_node
236
+ @smaller = nil
237
+ elsif @equal == child_node
238
+ @equal = nil
239
+ elsif @larger == child_node
240
+ @larger = nil
241
+ else
242
+ PEROBS.log.fatal "Cannot unlink unknown child node with address " +
243
+ "#{child_node.node_address} from #{to_s}"
244
+ end
245
+ write_node
246
+ end
247
+
248
+ # Depth-first iterator for all nodes. The iterator yields the given block
249
+ # at 5 points for any found node. The mode variable indicates the point.
250
+ # :on_enter Coming from the parent we've entered the node for the first
251
+ # time
252
+ # :smaller We are about to follow the link to the smaller sub-node
253
+ # :equal We are about to follow the link to the equal sub-node
254
+ # :larger We are about to follow the link to the larger sub-node
255
+ # :on_exit We have completed this node
256
+ def each
257
+ # We use a non-recursive implementation to traverse the tree. This stack
258
+ # keeps track of all the known still to be checked nodes.
259
+ stack = [ [ self, :on_enter ] ]
260
+
261
+ while !stack.empty?
262
+ node, mode = stack.pop
263
+
264
+ # Empty trees only have a dummy node that has no parent, and a size
265
+ # and address of 0.
266
+ break if node.size == 0 && node.blob_address == 0 && node.parent.nil?
267
+
268
+ case mode
269
+ when :on_enter
270
+ yield(node, mode, stack)
271
+ stack.push([ node, :smaller ])
272
+ when :smaller
273
+ yield(node, mode, stack) if node.smaller
274
+ stack.push([ node, :equal ])
275
+ stack.push([ node.smaller, :on_enter]) if node.smaller
276
+ when :equal
277
+ yield(node, mode, stack) if node.equal
278
+ stack.push([ node, :larger ])
279
+ stack.push([ node.equal, :on_enter]) if node.equal
280
+ when :larger
281
+ yield(node, mode, stack) if node.larger
282
+ stack.push([ node, :on_exit])
283
+ stack.push([ node.larger, :on_enter]) if node.larger
284
+ when :on_exit
285
+ yield(node, mode, stack)
286
+ end
287
+ end
288
+ end
289
+
290
+ def delete_node
291
+ if @equal
292
+ # Replace the current node with the next @equal node.
293
+ @equal.set_link('@smaller', @smaller) if @smaller
294
+ @equal.set_link('@larger', @larger) if @larger
295
+ relink_parent(@equal)
296
+ elsif @smaller && @larger.nil?
297
+ # We have no @larger node, so we can just replace the current node
298
+ # with the @smaller node.
299
+ relink_parent(@smaller)
300
+ elsif @larger && @smaller.nil?
301
+ # We have no @smaller node, wo we can just replace the current node
302
+ # with the @larger node.
303
+ relink_parent(@larger)
304
+ elsif @smaller && @larger
305
+ # Find the largest node in the smaller sub-node. This node will
306
+ # replace the current node.
307
+ node = @smaller.find_largest_node
308
+ if node != @smaller
309
+ # If the found node is not the direct @smaller node, attach the
310
+ # smaller sub-node of the found node to the parent of the found
311
+ # node.
312
+ node.relink_parent(node.smaller)
313
+ # The @smaller sub node of the current node is attached to the
314
+ # @smaller link of the found node.
315
+ node.set_link('@smaller', @smaller)
316
+ end
317
+ # Attach the @larger sub-node of the current node to the @larger link
318
+ # of the found node.
319
+ node.set_link('@larger', @larger)
320
+ # Point the link in the parent of the current node to the found node.
321
+ relink_parent(node)
322
+ else
323
+ # The node is a leaf node.
324
+ relink_parent(nil)
325
+ end
326
+ @tree.delete_node(@node_address) if @parent
327
+ end
328
+
329
+ # Replace the link in the parent node of the current node that points to
330
+ # the current node with the given node.
331
+ # @param node [SpaceTreeNode]
332
+ def relink_parent(node)
333
+ if @parent
334
+ if @parent.smaller == self
335
+ @parent.set_link('@smaller', node)
336
+ elsif @parent.equal == self
337
+ @parent.set_link('@equal', node)
338
+ elsif @parent.larger == self
339
+ @parent.set_link('@larger', node)
340
+ else
341
+ PEROBS.log.fatal "Cannot relink unknown child node with address " +
342
+ "#{node.node_address} from #{to_s}"
343
+ end
344
+ else
345
+ if node
346
+ @tree.set_root(node)
347
+ node.parent = nil
348
+ else
349
+ set_size_and_address(0, 0)
350
+ end
351
+ end
352
+ end
353
+
354
+ # Find the node with the smallest size in this sub-tree.
355
+ # @return [SpaceTreeNode]
356
+ def find_smallest_node
357
+ node = self
358
+ loop do
359
+ if node.smaller
360
+ node = node.smaller
361
+ else
362
+ # We've found a 'leaf' node.
363
+ return node
364
+ end
365
+ end
366
+ end
367
+
368
+ # Find the node with the largest size in this sub-tree.
369
+ # @return [SpaceTreeNode]
370
+ def find_largest_node
371
+ node = self
372
+ loop do
373
+ if node.larger
374
+ node = node.larger
375
+ else
376
+ # We've found a 'leaf' node.
377
+ return node
378
+ end
379
+ end
380
+ end
381
+
382
+ def set_size_and_address(size, address)
383
+ @size = size
384
+ @blob_address = address
385
+ write_node
386
+ end
387
+
388
+ def set_link(name, node_or_address)
389
+ if node_or_address
390
+ # Set the link to the given SpaceTreeNode or node address.
391
+ instance_variable_set(name,
392
+ node = node_or_address.is_a?(SpaceTreeNodeLink) ?
393
+ node_or_address :
394
+ SpaceTreeNodeLink.new(@tree, node_or_address))
395
+ # Link the node back to this node via the parent variable.
396
+ node.parent = self
397
+ else
398
+ # Clear the node link.
399
+ instance_variable_set(name, nil)
400
+ end
401
+ write_node
402
+ end
403
+
404
+ def parent=(p)
405
+ @parent = p ? SpaceTreeNodeLink.new(@tree, p) : nil
406
+ write_node
407
+ end
408
+ # Compare this node to another node.
409
+ # @return [Boolean] true if node address is identical, false otherwise
410
+ def ==(node)
411
+ node && @node_address == node.node_address
412
+ end
413
+
414
+ # Collects address and size touples of all nodes in the tree with a
415
+ # depth-first strategy and stores them in an Array.
416
+ # @return [Array] Array with [ address, size ] touples.
417
+ def to_a
418
+ ary = []
419
+
420
+ each do |node, mode, stack|
421
+ if mode == :on_enter
422
+ ary << [ node.blob_address, node.size ]
423
+ end
424
+ end
425
+
426
+ ary
427
+ end
428
+
429
+ # Textual version of the node data. It has the form
430
+ # node_address:[blob_address, size] ^parent_node_address
431
+ # <smaller_node_address >larger_node_address
432
+ # @return [String]
433
+ def to_s
434
+ s = "#{@node_address}:[#{@blob_address}, #{@size}]"
435
+ if @parent
436
+ begin
437
+ s += " ^#{@parent.node_address}"
438
+ rescue
439
+ s += ' ^@'
440
+ end
441
+ end
442
+ if @smaller
443
+ begin
444
+ s += " <#{@smaller.node_address}"
445
+ rescue
446
+ s += ' <@'
447
+ end
448
+ end
449
+ if @equal
450
+ begin
451
+ s += " =#{@equal.node_address}"
452
+ rescue
453
+ s += ' =@'
454
+ end
455
+ end
456
+ if @larger
457
+ begin
458
+ s += " >#{@larger.node_address}"
459
+ rescue
460
+ s += ' >@'
461
+ end
462
+ end
463
+
464
+ s
465
+ end
466
+
467
+ # Check this node and all sub nodes for possible structural or logical
468
+ # errors.
469
+ # @param flat_file [FlatFile] If given, check that the space is also
470
+ # present in the given flat file.
471
+ # @return [false,true] True if OK, false otherwise
472
+ def check(flat_file)
473
+ node_counter = 0
474
+ max_depth = 0
475
+
476
+ each do |node, mode, stack|
477
+ max_depth = stack.size if stack.size > max_depth
478
+
479
+ case mode
480
+ when :smaller
481
+ if node.smaller
482
+ return false unless node.check_node_link('smaller', stack)
483
+ smaller_node = node.smaller
484
+ if smaller_node.size >= node.size
485
+ PEROBS.log.error "Smaller SpaceTreeNode size " +
486
+ "(#{smaller_node}) is not smaller than #{node}"
487
+ return false
488
+ end
489
+ end
490
+ when :equal
491
+ if node.equal
492
+ return false unless node.check_node_link('equal', stack)
493
+ equal_node = node.equal
494
+
495
+ if equal_node.smaller || equal_node.larger
496
+ PEROBS.log.error "Equal node #{equal_node} must not have " +
497
+ "smaller/larger childs"
498
+ return false
499
+ end
500
+
501
+ if node.size != equal_node.size
502
+ PEROBS.log.error "Equal SpaceTreeNode size (#{equal_node}) is " +
503
+ "not equal parent node #{node}"
504
+ return false
505
+ end
506
+ end
507
+ when :larger
508
+ if node.larger
509
+ return false unless node.check_node_link('larger', stack)
510
+ larger_node = node.larger
511
+ if larger_node.size <= node.size
512
+ PEROBS.log.error "Larger SpaceTreeNode size " +
513
+ "(#{larger_node}) is not larger than #{node}"
514
+ return false
515
+ end
516
+ end
517
+ when :on_exit
518
+ if flat_file &&
519
+ !flat_file.has_space?(node.blob_address, node.size)
520
+ PEROBS.log.error "SpaceTreeNode has space at offset " +
521
+ "#{node.blob_address} of size #{node.size} that isn't " +
522
+ "available in the FlatFile."
523
+ return false
524
+ end
525
+
526
+ node_counter += 1
527
+ end
528
+ end
529
+ PEROBS.log.debug "#{node_counter} SpaceTree nodes checked"
530
+ PEROBS.log.debug "Maximum tree depth is #{max_depth}"
531
+
532
+ return true
533
+ end
534
+
535
+ # Check the integrity of the given sub-node link and the parent link
536
+ # pointing back to this node.
537
+ # @param link [String] 'smaller', 'equal' or 'larger'
538
+ # @param stack [Array] List of parent nodes [ address, mode ] touples
539
+ # @return [Boolean] true of OK, false otherwise
540
+ def check_node_link(link, stack)
541
+ if (node = instance_variable_get('@' + link))
542
+ # Node links must only be of class SpaceTreeNodeLink
543
+ unless node.nil? || node.is_a?(SpaceTreeNodeLink)
544
+ PEROBS.log.error "Node link #{link} of node #{to_s} " +
545
+ "is of class #{node.class}"
546
+ return false
547
+ end
548
+
549
+ # Link must not point back to self.
550
+ if node == self
551
+ PEROBS.log.error "#{link} address of node " +
552
+ "#{node.to_s} points to self #{to_s}"
553
+ return false
554
+ end
555
+
556
+ # Link must not point to any of the parent nodes.
557
+ if stack.include?(node)
558
+ PEROBS.log.error "#{link} address of node #{to_s} " +
559
+ "points to parent node #{node}"
560
+
561
+ return false
562
+ end
563
+
564
+ # Parent link of node must point back to self.
565
+ if node.parent != self
566
+ PEROBS.log.error "@#{link} node #{node.to_s} does not have parent " +
567
+ "link pointing " +
568
+ "to parent node #{to_s}. Pointing at " +
569
+ "#{node.parent.nil? ? 'nil' : node.parent.to_s} instead."
570
+
571
+ return false
572
+ end
573
+ end
574
+
575
+ true
576
+ end
577
+
578
+ # Convert the node and all child nodes into a tree like text form.
579
+ # @return [String]
580
+ def to_tree_s
581
+ str = ''
582
+
583
+ each do |node, mode, stack|
584
+ if mode == :on_enter
585
+ begin
586
+ branch_mark = node.parent.nil? ? '' :
587
+ node.parent.smaller == node ? '<' :
588
+ node.parent.equal == node ? '=' :
589
+ node.parent.larger == node ? '>' : '@'
590
+
591
+ str += "#{node.text_tree_prefix}#{branch_mark}-" +
592
+ "#{node.smaller || node.equal || node.larger ? 'v-' : '--'}" +
593
+ "#{node.to_s}\n"
594
+ rescue
595
+ str += "#{node.text_tree_prefix}- @@@@@@@@@@\n"
596
+ end
597
+ end
598
+ end
599
+
600
+ str
601
+ end
602
+
603
+ # The indentation and arch routing for the text tree.
604
+ # @return [String]
605
+ def text_tree_prefix
606
+ if (node = @parent)
607
+ str = '+'
608
+ else
609
+ # Prefix start for root node line
610
+ str = 'o'
611
+ end
612
+
613
+ while node
614
+ last_child = false
615
+ if node.parent
616
+ if node.parent.smaller == node
617
+ last_child = node.parent.equal.nil? && node.parent.larger.nil?
618
+ elsif node.parent.equal == node
619
+ last_child = node.parent.larger.nil?
620
+ elsif node.parent.larger == node
621
+ last_child = true
622
+ end
623
+ else
624
+ # Padding for the root node
625
+ str = ' ' + str
626
+ break
627
+ end
628
+
629
+ str = (last_child ? ' ' : '| ') + str
630
+ node = node.parent
631
+ end
632
+
633
+ str
634
+ end
635
+
636
+ private
637
+
638
+ def write_node
639
+ bytes = [ @blob_address, @size,
640
+ @parent ? @parent.node_address : 0,
641
+ @smaller ? @smaller.node_address : 0,
642
+ @equal ? @equal.node_address : 0,
643
+ @larger ? @larger.node_address : 0].pack(NODE_BYTES_FORMAT)
644
+ @tree.nodes.store_blob(@node_address, bytes)
645
+ end
646
+
647
+ def read_node
648
+ unless @node_address > 0
649
+ PEROBS.log.fatal "@node_address must be larger than 0"
650
+ end
651
+ return false unless (bytes = @tree.nodes.retrieve_blob(@node_address))
652
+
653
+ @blob_address, @size, parent_node_address,
654
+ smaller_node_address, equal_node_address,
655
+ larger_node_address = bytes.unpack(NODE_BYTES_FORMAT)
656
+ # The parent address can also be 0 as the parent can rightly point back
657
+ # to the root node which always has the address 0.
658
+ @parent = parent_node_address != 0 ?
659
+ SpaceTreeNodeLink.new(@tree, parent_node_address) : nil
660
+ @smaller = smaller_node_address != 0 ?
661
+ SpaceTreeNodeLink.new(@tree, smaller_node_address) : nil
662
+ @equal = equal_node_address != 0 ?
663
+ SpaceTreeNodeLink.new(@tree, equal_node_address) : nil
664
+ @larger = larger_node_address != 0 ?
665
+ SpaceTreeNodeLink.new(@tree, larger_node_address) : nil
666
+
667
+ true
668
+ end
669
+
670
+ end
671
+
672
+ end