perobs 2.5.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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