innodb_ruby 0.8.8 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -8,30 +8,14 @@ class Innodb::LogBlock
8
8
  # Log blocks are fixed-length at 512 bytes in InnoDB.
9
9
  BLOCK_SIZE = 512
10
10
 
11
- HEADER_SIZE = 12
12
- HEADER_START = 0
11
+ # Offset of the header within the log block.
12
+ HEADER_OFFSET = 0
13
13
 
14
- TRAILER_SIZE = 4
15
- TRAILER_START = BLOCK_SIZE - TRAILER_SIZE
14
+ # Offset of the trailer within ths log block.
15
+ TRAILER_OFFSET = BLOCK_SIZE - 4
16
16
 
17
- RECORD_START = HEADER_START + HEADER_SIZE
18
-
19
- # Header:
20
- #define LOG_BLOCK_HDR_NO 0 /* block number which must be > 0 and
21
- #define LOG_BLOCK_HDR_DATA_LEN 4 /* number of bytes of log written to
22
- #define LOG_BLOCK_FIRST_REC_GROUP 6 /* offset of the first start of an
23
- #define LOG_BLOCK_CHECKPOINT_NO 8 /* 4 lower bytes of the value of
24
-
25
- # Trailer:
26
- #define LOG_BLOCK_CHECKSUM 0 /* 4 byte checksum of the log block
27
-
28
- #/* Offsets for a checkpoint field */
29
- #define LOG_CHECKPOINT_NO 0
30
- #define LOG_CHECKPOINT_LSN 8
31
- #define LOG_CHECKPOINT_OFFSET 16
32
- #define LOG_CHECKPOINT_LOG_BUF_SIZE 20
33
- #define LOG_CHECKPOINT_ARCHIVED_LSN 24
34
- #define LOG_CHECKPOINT_GROUP_ARRAY 32
17
+ # Mask used to get the flush bit in the header.
18
+ HEADER_FLUSH_BIT_MASK = 0x80000000
35
19
 
36
20
  # Initialize a log block by passing in a 512-byte buffer containing the raw
37
21
  # log block contents.
@@ -43,36 +27,33 @@ class Innodb::LogBlock
43
27
  @buffer = buffer
44
28
  end
45
29
 
46
- # A helper function to return bytes from the log block buffer based on offset
47
- # and length, both in bytes.
48
- def data(offset, length)
49
- @buffer[offset...(offset + length)]
50
- end
51
-
52
30
  # Return an Innodb::Cursor object positioned at a specific offset.
53
31
  def cursor(offset)
54
- Innodb::Cursor.new(self, offset)
32
+ Innodb::Cursor.new(@buffer, offset)
55
33
  end
56
34
 
57
35
  # Return the log block header.
58
36
  def header
59
- @header ||= begin
60
- c = cursor(HEADER_START)
37
+ @header ||= cursor(HEADER_OFFSET).name("header") do |c|
61
38
  {
62
- :block => c.get_uint32 & (2**31-1),
63
- :data_length => c.get_uint16,
64
- :first_rec_group => c.get_uint16,
65
- :checkpoint_no => c.get_uint32,
39
+ :flush => c.name("flush") {
40
+ c.peek { (c.get_uint32 & HEADER_FLUSH_BIT_MASK) > 0 }
41
+ },
42
+ :block_number => c.name("block_number") {
43
+ c.get_uint32 & ~HEADER_FLUSH_BIT_MASK
44
+ },
45
+ :data_length => c.name("data_length") { c.get_uint16 },
46
+ :first_rec_group => c.name("first_rec_group") { c.get_uint16 },
47
+ :checkpoint_no => c.name("checkpoint_no") { c.get_uint32 },
66
48
  }
67
49
  end
68
50
  end
69
51
 
70
52
  # Return the log block trailer.
71
53
  def trailer
72
- @trailer ||= begin
73
- c = cursor(TRAILER_START)
54
+ @trailer ||= cursor(TRAILER_OFFSET).name("trailer") do |c|
74
55
  {
75
- :checksum => c.get_uint32,
56
+ :checksum => c.name("checksum") { c.get_uint32 },
76
57
  }
77
58
  end
78
59
  end
@@ -127,41 +108,26 @@ class Innodb::LogBlock
127
108
  51 => :ZIP_PAGE_COMPRESS,
128
109
  }
129
110
 
130
- # Return the log record contents. (This is mostly unimplemented.)
131
- def record_content(record_type, offset)
132
- c = cursor(offset)
133
- case record_type
134
- when :MLOG_1BYTE
135
- c.get_uint8
136
- when :MLOG_2BYTE
137
- c.get_uint16
138
- when :MLOG_4BYTE
139
- c.get_uint32
140
- when :MLOG_8BYTE
141
- c.get_uint64
142
- when :UNDO_INSERT
143
- when :COMP_REC_INSERT
144
- end
145
- end
146
-
147
111
  SINGLE_RECORD_MASK = 0x80
148
112
  RECORD_TYPE_MASK = 0x7f
149
113
 
150
- # Return the log record. (This is mostly unimplemented.)
151
- def record
152
- @record ||= begin
153
- if header[:first_rec_group] != 0
154
- c = cursor(header[:first_rec_group])
155
- type_and_flag = c.get_uint8
156
- type = type_and_flag & RECORD_TYPE_MASK
157
- type = RECORD_TYPES[type] || type
158
- single_record = (type_and_flag & SINGLE_RECORD_MASK) > 0
114
+ # Return a preamble of the first record in this block.
115
+ def first_record_preamble
116
+ return nil unless header[:first_rec_group] > 0
117
+ cursor(header[:first_rec_group]).name("header") do |c|
118
+ type_and_flag = c.name("type") { c.get_uint8 }
119
+ type = type_and_flag & RECORD_TYPE_MASK
120
+ type = RECORD_TYPES[type] || type
121
+ single_record = (type_and_flag & SINGLE_RECORD_MASK) > 0
122
+ case type
123
+ when :MULTI_REC_END, :DUMMY_RECORD
124
+ { :type => type }
125
+ else
159
126
  {
160
127
  :type => type,
161
128
  :single_record => single_record,
162
- :content => record_content(type, c.position),
163
- :space => c.get_ic_uint32,
164
- :page_number => c.get_ic_uint32,
129
+ :space => c.name("space") { c.get_ic_uint32 },
130
+ :page_number => c.name("page_number") { c.get_ic_uint32 },
165
131
  }
166
132
  end
167
133
  end
@@ -179,6 +145,6 @@ class Innodb::LogBlock
179
145
 
180
146
  puts
181
147
  puts "record:"
182
- pp record
148
+ pp first_record_preamble
183
149
  end
184
150
  end
data/lib/innodb/page.rb CHANGED
@@ -61,18 +61,12 @@ class Innodb::Page
61
61
  @size ||= @buffer.size
62
62
  end
63
63
 
64
- # A helper function to return bytes from the page buffer based on offset
65
- # and length, both in bytes.
66
- def data(offset, length)
67
- @buffer[offset...(offset + length)]
68
- end
69
-
70
64
  # If no block is passed, return an Innodb::Cursor object positioned at a
71
65
  # specific offset. If a block is passed, create a cursor at the provided
72
66
  # offset and yield it to the provided block one time, and then return the
73
67
  # return value of the block.
74
68
  def cursor(offset)
75
- new_cursor = Innodb::Cursor.new(self, offset)
69
+ new_cursor = Innodb::Cursor.new(@buffer, offset)
76
70
 
77
71
  if block_given?
78
72
  # Call the block once and return its return value.
@@ -11,8 +11,6 @@ require "innodb/fseg_entry"
11
11
  # (the actual data) which grow ascending by offset, free space, the page
12
12
  # directory which grows descending by offset, and the FIL trailer.
13
13
  class Innodb::Page::Index < Innodb::Page
14
- attr_accessor :record_describer
15
-
16
14
  # The size (in bytes) of the "next" pointer in each record header.
17
15
  RECORD_NEXT_SIZE = 2
18
16
 
@@ -228,6 +226,11 @@ class Innodb::Page::Index < Innodb::Page
228
226
  end
229
227
  end
230
228
 
229
+ # A helper function to return the index id.
230
+ def index_id
231
+ page_header && page_header[:index_id]
232
+ end
233
+
231
234
  # A helper function to return the page level from the "page" header, for
232
235
  # easier access.
233
236
  def level
@@ -429,7 +432,7 @@ class Innodb::Page::Index < Innodb::Page
429
432
  def system_record(offset)
430
433
  cursor(offset).name("record[#{offset}]") do |c|
431
434
  header = c.peek { record_header(c) }
432
- Innodb::Record.new({
435
+ Innodb::Record.new(self, {
433
436
  :offset => offset,
434
437
  :header => header,
435
438
  :next => header[:next],
@@ -448,6 +451,23 @@ class Innodb::Page::Index < Innodb::Page
448
451
  @supremum ||= system_record(pos_supremum)
449
452
  end
450
453
 
454
+ def record_describer=(o)
455
+ @record_describer = o
456
+ end
457
+
458
+ def record_describer
459
+ return @record_describer if @record_describer
460
+
461
+ if space and space.innodb_system and index_id
462
+ @record_describer =
463
+ space.innodb_system.data_dictionary.record_describer_by_index_id(index_id)
464
+ elsif space
465
+ @record_describer = space.record_describer
466
+ end
467
+
468
+ @record_describer
469
+ end
470
+
451
471
  # Return a set of field objects that describe the record.
452
472
  def make_record_description
453
473
  description = record_describer.description
@@ -559,48 +579,304 @@ class Innodb::Page::Index < Innodb::Page
559
579
  end
560
580
  end
561
581
 
562
- Innodb::Record.new(this_record)
582
+ Innodb::Record.new(self, this_record)
583
+ end
584
+ end
585
+
586
+ # Return an array of row offsets for all entries in the page directory.
587
+ def directory
588
+ return @directory if @directory
589
+
590
+ @directory = []
591
+ cursor(pos_directory).backward.name("page_directory") do |c|
592
+ directory_slots.times do |n|
593
+ @directory.push c.name("slot[#{n}]") { c.get_uint16 }
594
+ end
595
+ end
596
+
597
+ @directory
598
+ end
599
+
600
+ # Return the slot number of the provided offset in the page directory, or nil
601
+ # if the offset is not present in the page directory.
602
+ def offset_is_directory_slot?(offset)
603
+ directory.index(offset)
604
+ end
605
+
606
+ # Return the slot number of the provided record in the page directory, or nil
607
+ # if the record is not present in the page directory.
608
+ def record_is_directory_slot?(this_record)
609
+ offset_is_directory_slot?(this_record.offset)
610
+ end
611
+
612
+ # Return the slot number for the page directory entry which "owns" the
613
+ # provided record. This will be either the record itself, or the nearest
614
+ # record with an entry in the directory and a value greater than the record.
615
+ def directory_slot_for_record(this_record)
616
+ if slot = record_is_directory_slot?(this_record)
617
+ return slot
563
618
  end
619
+
620
+ unless search_cursor = record_cursor(this_record.next)
621
+ raise "Couldn't position cursor"
622
+ end
623
+
624
+ while rec = search_cursor.record
625
+ if slot = record_is_directory_slot?(rec)
626
+ return slot
627
+ end
628
+ end
629
+
630
+ return record_is_directory_slot?(supremum)
564
631
  end
565
632
 
566
633
  # A class for cursoring through records starting from an arbitrary point.
567
634
  class RecordCursor
568
- def initialize(page, offset)
569
- @page = page
570
- @offset = offset
635
+ def initialize(page, offset, direction)
636
+ Innodb::Stats.increment :page_record_cursor_create
637
+
638
+ @initial = true
639
+ @page = page
640
+ @direction = direction
641
+ case offset
642
+ when :min
643
+ @record = @page.min_record
644
+ when :max
645
+ @record = @page.max_record
646
+ else
647
+ # Offset is a byte offset of a record (hopefully).
648
+ @record = @page.record(offset)
649
+ end
650
+ end
651
+
652
+ # Return the current record, mostly as a helper.
653
+ def current_record
654
+ @record
571
655
  end
572
656
 
573
657
  # Return the next record, and advance the cursor. Return nil when the
574
- # end of records is reached.
575
- def record
576
- return nil unless @offset
658
+ # end of records (supremum) is reached.
659
+ def next_record
660
+ Innodb::Stats.increment :page_record_cursor_next_record
577
661
 
578
- record = @page.record(@offset)
662
+ @record = @page.record(@record.next)
579
663
 
580
- if record == @page.supremum
664
+ if @record == @page.supremum
581
665
  # We've reached the end of the linked list at supremum.
582
- @offset = nil
583
- elsif record.next == @offset
584
- # The record links to itself; go ahead and return it (once), but set
585
- # the next offset to nil to end the loop.
586
- @offset = nil
587
- record
666
+ nil
588
667
  else
589
- @offset = record.next
590
- record
668
+ @record
669
+ end
670
+ end
671
+
672
+ # Return the previous record, and advance the cursor. Return nil when the
673
+ # end of records (infimum) is reached.
674
+ def prev_record
675
+ Innodb::Stats.increment :page_record_cursor_prev_record
676
+
677
+ unless slot = @page.directory_slot_for_record(@record)
678
+ raise "Couldn't find slot for record"
679
+ end
680
+
681
+ unless search_cursor = @page.record_cursor(@page.directory[slot-1])
682
+ raise "Couldn't position search cursor"
683
+ end
684
+
685
+ while rec = search_cursor.record and rec.offset != @record.offset
686
+ if rec.next == @record.offset
687
+ if rec == @page.infimum
688
+ return nil
689
+ end
690
+ return @record = rec
691
+ end
692
+ end
693
+ end
694
+
695
+ # Return the next record in the order defined when the cursor was created.
696
+ def record
697
+ if @initial
698
+ @initial = false
699
+ return current_record
700
+ end
701
+
702
+ case @direction
703
+ when :forward
704
+ next_record
705
+ when :backward
706
+ prev_record
707
+ end
708
+ end
709
+
710
+ # Iterate through all records in the cursor.
711
+ def each_record
712
+ unless block_given?
713
+ return enum_for(:each_record)
714
+ end
715
+
716
+ while rec = record
717
+ yield rec
591
718
  end
592
719
  end
593
720
  end
594
721
 
595
722
  # Return a RecordCursor starting at offset.
596
- def record_cursor(offset)
597
- RecordCursor.new(self, offset)
723
+ def record_cursor(offset=:min, direction=:forward)
724
+ RecordCursor.new(self, offset, direction)
598
725
  end
599
726
 
600
- # Return the first record on this page.
601
- def first_record
602
- first = record(infimum.next)
603
- first if first != supremum
727
+ # Return the minimum record on this page.
728
+ def min_record
729
+ min = record(infimum.next)
730
+ min if min != supremum
731
+ end
732
+
733
+ # Return the maximum record on this page.
734
+ def max_record
735
+ # Since the records are only singly-linked in the forward direction, in
736
+ # order to do find the last record, we must create a cursor and walk
737
+ # backwards one step.
738
+ unless max_cursor = record_cursor(supremum.offset, :backward)
739
+ raise "Couldn't position cursor"
740
+ end
741
+ # Note the deliberate use of prev_record rather than record; we want
742
+ # to skip over supremum itself.
743
+ max = max_cursor.prev_record
744
+ max if max != infimum
745
+ end
746
+
747
+ # Search for a record within a single page, and return either a perfect
748
+ # match for the key, or the last record closest to they key but not greater
749
+ # than the key. (If an exact match is desired, compare_key must be used to
750
+ # check if the returned record matches. This makes the function useful for
751
+ # search in both leaf and non-leaf pages.)
752
+ def linear_search_from_cursor(search_cursor, key)
753
+ Innodb::Stats.increment :linear_search_from_cursor
754
+
755
+ this_rec = search_cursor.record
756
+
757
+ if Innodb.debug?
758
+ puts "linear_search_from_cursor: page=%i, level=%i, start=(%s)" % [
759
+ offset,
760
+ level,
761
+ this_rec && this_rec.key_string,
762
+ ]
763
+ end
764
+
765
+ # Iterate through all records until finding either a matching record or
766
+ # one whose key is greater than the desired key.
767
+ while this_rec && next_rec = search_cursor.record
768
+ Innodb::Stats.increment :linear_search_from_cursor_record_scans
769
+
770
+ if Innodb.debug?
771
+ puts "linear_search_from_cursor: page=%i, level=%i, current=(%s)" % [
772
+ offset,
773
+ level,
774
+ this_rec && this_rec.key_string,
775
+ ]
776
+ end
777
+
778
+ # If we reach supremum, return the last non-system record we got.
779
+ return this_rec if next_rec.header[:type] == :supremum
780
+
781
+ if this_rec.compare_key(key) < 0
782
+ return this_rec
783
+ end
784
+
785
+ if (this_rec.compare_key(key) >= 0) &&
786
+ (next_rec.compare_key(key) < 0)
787
+ # The desired key is either an exact match for this_rec or is greater
788
+ # than it but less than next_rec. If this is a non-leaf page, that
789
+ # will mean that the record will fall on the leaf page this node
790
+ # pointer record points to, if it exists at all.
791
+ return this_rec
792
+ end
793
+
794
+ this_rec = next_rec
795
+ end
796
+
797
+ this_rec
798
+ end
799
+
800
+ # Search or a record within a single page using the page directory to limit
801
+ # the number of record comparisons required. Once the last page directory
802
+ # entry closest to but not greater than the key is found, fall back to
803
+ # linear search using linear_search_from_cursor to find the closest record
804
+ # whose key is not greater than the desired key. (If an exact match is
805
+ # desired, the returned record must be checked in the same way as the above
806
+ # linear_search_from_cursor function.)
807
+ def binary_search_by_directory(dir, key)
808
+ Innodb::Stats.increment :binary_search_by_directory
809
+
810
+ return nil if dir.empty?
811
+
812
+ # Split the directory at the mid-point (using integer math, so the division
813
+ # is rounding down). Retrieve the record that sits at the mid-point.
814
+ mid = ((dir.size-1) / 2)
815
+ rec = record(dir[mid])
816
+
817
+ if Innodb.debug?
818
+ puts "binary_search_by_directory: page=%i, level=%i, dir.size=%i, dir[%i]=(%s)" % [
819
+ offset,
820
+ level,
821
+ dir.size,
822
+ mid,
823
+ rec.key_string,
824
+ ]
825
+ end
826
+
827
+ # The mid-point record was the infimum record, which is not comparable with
828
+ # compare_key, so we need to just linear scan from here. If the mid-point
829
+ # is the beginning of the page there can't be many records left to check
830
+ # anyway.
831
+ if rec.header[:type] == :infimum
832
+ return linear_search_from_cursor(record_cursor(rec.next), key)
833
+ end
834
+
835
+ # Compare the desired key to the mid-point record's key.
836
+ case rec.compare_key(key)
837
+ when 0
838
+ # An exact match for the key was found. Return the record.
839
+ Innodb::Stats.increment :binary_search_by_directory_exact_match
840
+ rec
841
+ when +1
842
+ # The mid-point record's key is less than the desired key.
843
+ if dir.size > 2
844
+ # There are more entries remaining from the directory, recurse again
845
+ # using binary search on the right half of the directory, which
846
+ # represents values greater than or equal to the mid-point record's
847
+ # key.
848
+ Innodb::Stats.increment :binary_search_by_directory_recurse_right
849
+ binary_search_by_directory(dir[mid...dir.size], key)
850
+ else
851
+ next_rec = record(dir[mid+1])
852
+ next_key = next_rec && next_rec.compare_key(key)
853
+ if dir.size == 1 || next_key == -1 || next_key == 0
854
+ # This is the last entry remaining from the directory, or our key is
855
+ # greater than rec and less than rec+1's key. Use linear search to
856
+ # find the record starting at rec.
857
+ Innodb::Stats.increment :binary_search_by_directory_linear_search
858
+ linear_search_from_cursor(record_cursor(rec.offset), key)
859
+ elsif next_key == +1
860
+ Innodb::Stats.increment :binary_search_by_directory_linear_search
861
+ linear_search_from_cursor(record_cursor(next_rec.offset), key)
862
+ else
863
+ nil
864
+ end
865
+ end
866
+ when -1
867
+ # The mid-point record's key is greater than the desired key.
868
+ if dir.size == 1
869
+ # If this is the last entry remaining from the directory, we didn't
870
+ # find anything workable.
871
+ Innodb::Stats.increment :binary_search_by_directory_empty_result
872
+ nil
873
+ else
874
+ # Recurse on the left half of the directory, which represents values
875
+ # less than the mid-point record's key.
876
+ Innodb::Stats.increment :binary_search_by_directory_recurse_left
877
+ binary_search_by_directory(dir[0...mid], key)
878
+ end
879
+ end
604
880
  end
605
881
 
606
882
  # Iterate through all records.
@@ -618,6 +894,7 @@ class Innodb::Page::Index < Innodb::Page
618
894
  nil
619
895
  end
620
896
 
897
+ # Iterate through all records in the garbage list.
621
898
  def each_garbage_record
622
899
  unless block_given?
623
900
  return enum_for(:each_garbage_record)
@@ -653,19 +930,6 @@ class Innodb::Page::Index < Innodb::Page
653
930
  nil
654
931
  end
655
932
 
656
- # Return an array of row offsets for all entries in the page directory.
657
- def directory
658
- return @directory if @directory
659
-
660
- @directory = []
661
- cursor(pos_directory).backward.name("page_directory") do |c|
662
- directory_slots.times do |n|
663
- @directory.push c.name("slot[#{n}]") { c.get_uint16 }
664
- end
665
- end
666
-
667
- @directory
668
- end
669
933
 
670
934
  # Dump the contents of a page for debugging purposes.
671
935
  def dump