class Innodb::Page

Constants

Address
FilHeader
FilTrailer
PAGE_TYPE

InnoDB Page Type constants from include/fil0fil.h.

PAGE_TYPE_BY_VALUE
Region
UNDEFINED_PAGE_NUMBER

A page number representing “undefined” values, (4294967295).

Attributes

specialized_classes[R]
space[R]

Public Class Methods

handle(_page, space, buffer, page_number = nil) click to toggle source

Allow the specialized class to do something that isn’t ‘new’ with this page.

# File lib/innodb/page.rb, line 97
def self.handle(_page, space, buffer, page_number = nil)
  new(space, buffer, page_number)
end
maybe_undefined(page_number) click to toggle source

A helper to convert “undefined” values stored in previous and next pointers in the page header to nil.

# File lib/innodb/page.rb, line 344
def self.maybe_undefined(page_number)
  page_number unless undefined?(page_number)
end
new(space, buffer, page_number = nil) click to toggle source

Initialize a page by passing in a buffer containing the raw page contents. The buffer size should match the space’s page size.

# File lib/innodb/page.rb, line 103
def initialize(space, buffer, page_number = nil)
  raise "Page can't be initialized from nil space or buffer (space: #{space}, buffer: #{buffer})" unless buffer
  raise "Buffer size #{buffer.size} is different than space page size" if space && space.page_size != buffer.size

  @space  = space
  @buffer = buffer
  @page_number = page_number
end
page_type_by_value(value) click to toggle source
# File lib/innodb/page.rb, line 348
def self.page_type_by_value(value)
  PAGE_TYPE_BY_VALUE[value] || value
end
parse(space, buffer, page_number = nil) click to toggle source

Load a page as a generic page in order to make the “fil” header accessible, and then attempt to hand off the page to a specialized class to be re-parsed if possible. If there is no specialized class for this type of page, return the generic object.

This could be optimized to reach into the page buffer and efficiently extract the page type in order to avoid throwing away a generic Innodb::Page object when parsing every specialized page, but this is a bit cleaner, and we’re not particularly performance sensitive.

# File lib/innodb/page.rb, line 83
def self.parse(space, buffer, page_number = nil)
  # Create a page object as a generic page.
  page = Innodb::Page.new(space, buffer, page_number)

  # If there is a specialized class available for this page type, re-create
  # the page object using that specialized class.
  if (specialized_class = specialized_classes[page.type])
    page = specialized_class.handle(page, space, buffer, page_number)
  end

  page
end
register_specialization(page_type, specialized_class) click to toggle source
# File lib/innodb/page.rb, line 60
def self.register_specialization(page_type, specialized_class)
  @specialized_classes[page_type] = specialized_class
end
specialization_for(page_type) click to toggle source
# File lib/innodb/page.rb, line 64
def self.specialization_for(page_type)
  # This needs to intentionally use Innodb::Page because we need to register
  # in the class instance variable in *that* class.
  Innodb::Page.register_specialization(page_type, self)
end
specialization_for?(page_type) click to toggle source
# File lib/innodb/page.rb, line 70
def self.specialization_for?(page_type)
  Innodb::Page.specialized_classes.include?(page_type)
end
undefined?(page_number) click to toggle source

A helper to check if a page number is the undefined page number.

# File lib/innodb/page.rb, line 338
def self.undefined?(page_number)
  page_number == UNDEFINED_PAGE_NUMBER
end

Public Instance Methods

checksum_crc32() click to toggle source

Calculate the checksum of the page using the CRC32c algorithm.

# File lib/innodb/page.rb, line 422
def checksum_crc32
  raise "Checksum calculation is only supported for 16 KiB pages" unless default_page_size?

  @checksum_crc32 ||= begin
    # Calculate the CRC32c of the page header.
    crc_partial_header = Digest::CRC32c.new
    each_page_header_byte_as_uint8 do |byte|
      crc_partial_header << byte.chr
    end

    # Calculate the CRC32c of the page body.
    crc_page_body = Digest::CRC32c.new
    each_page_body_byte_as_uint8 do |byte|
      crc_page_body << byte.chr
    end

    # Bitwise XOR the two checksums together.
    crc_partial_header.checksum ^ crc_page_body.checksum
  end
end
checksum_crc32?() click to toggle source
# File lib/innodb/page.rb, line 443
def checksum_crc32?
  checksum == checksum_crc32
end
checksum_innodb() click to toggle source

Calculate the checksum of the page using InnoDB’s algorithm.

# File lib/innodb/page.rb, line 402
def checksum_innodb
  raise "Checksum calculation is only supported for 16 KiB pages" unless default_page_size?

  @checksum_innodb ||= begin
    # Calculate the InnoDB checksum of the page header.
    c_partial_header = Innodb::Checksum.fold_enumerator(each_page_header_byte_as_uint8)

    # Calculate the InnoDB checksum of the page body.
    c_page_body = Innodb::Checksum.fold_enumerator(each_page_body_byte_as_uint8)

    # Add the two checksums together, and mask the result back to 32 bits.
    (c_partial_header + c_page_body) & Innodb::Checksum::MAX
  end
end
checksum_innodb?() click to toggle source
# File lib/innodb/page.rb, line 417
def checksum_innodb?
  checksum == checksum_innodb
end
checksum_invalid?() click to toggle source

Is the page checksum incorrect?

# File lib/innodb/page.rb, line 453
def checksum_invalid?
  !checksum_valid?
end
checksum_type() click to toggle source
# File lib/innodb/page.rb, line 457
def checksum_type
  return :crc32 if checksum_crc32?
  return :innodb if checksum_innodb?

  nil
end
checksum_valid?() click to toggle source

Is the page checksum correct?

# File lib/innodb/page.rb, line 448
def checksum_valid?
  checksum_crc32? || checksum_innodb?
end
corrupt?() click to toggle source

Is the page corrupt, either due to data corruption, tearing, or in the wrong place?

# File lib/innodb/page.rb, line 494
def corrupt?
  checksum_invalid? || torn? || misplaced?
end
cursor(buffer_offset) { |new_cursor| ... } click to toggle source

If no block is passed, return an BufferCursor object positioned at a specific offset. If a block is passed, create a cursor at the provided offset and yield it to the provided block one time, and then return the return value of the block.

# File lib/innodb/page.rb, line 139
def cursor(buffer_offset)
  new_cursor = BufferCursor.new(@buffer, buffer_offset)
  new_cursor.push_name("space[#{space&.name || 'unknown'}]")
  new_cursor.push_name("page[#{name}]")

  if block_given?
    # Call the block once and return its return value.
    yield new_cursor
  else
    # Return the cursor itself.
    new_cursor
  end
end
default_page_size?() click to toggle source
# File lib/innodb/page.rb, line 119
def default_page_size?
  size == Innodb::Space::DEFAULT_PAGE_SIZE
end
dump() click to toggle source

Dump the contents of a page for debugging purposes.

# File lib/innodb/page.rb, line 547
def dump
  puts "#{self}:"
  puts

  puts "fil header:"
  pp fil_header
  puts

  puts "fil trailer:"
  pp fil_trailer
  puts
end
each_page_body_byte_as_uint8(&block) click to toggle source

Iterate each byte of the page body, except for the FIL header and the FIL trailer.

# File lib/innodb/page.rb, line 395
def each_page_body_byte_as_uint8(&block)
  return enum_for(:each_page_body_byte_as_uint8) unless block_given?

  cursor(pos_page_body).each_byte_as_uint8(size_page_body, &block)
end
each_page_header_byte_as_uint8(&block) click to toggle source

Iterate each byte of the FIL header.

# File lib/innodb/page.rb, line 387
def each_page_header_byte_as_uint8(&block)
  return enum_for(:each_page_header_byte_as_uint8) unless block_given?

  cursor(pos_partial_page_header).each_byte_as_uint8(size_partial_page_header, &block)
end
each_region() { |region( offset: pos_fil_header, length: size_fil_header, name: :fil_header, info: "FIL Header"| ... } click to toggle source
# File lib/innodb/page.rb, line 503
def each_region
  return enum_for(:each_region) unless block_given?

  yield Region.new(
    offset: pos_fil_header,
    length: size_fil_header,
    name: :fil_header,
    info: "FIL Header"
  )

  yield Region.new(
    offset: pos_fil_trailer,
    length: size_fil_trailer,
    name: :fil_trailer,
    info: "FIL Trailer"
  )

  nil
end
extent_descriptor?() click to toggle source

Is this an extent descriptor page (either FSP_HDR or XDES)?

# File lib/innodb/page.rb, line 499
def extent_descriptor?
  type == :FSP_HDR || type == :XDES
end
fil_header() click to toggle source

Return the “fil” header from the page, which is common for all page types.

# File lib/innodb/page.rb, line 353
def fil_header
  @fil_header ||= cursor(pos_fil_header).name("fil_header") do |c|
    FilHeader.new(
      checksum: c.name("checksum") { c.read_uint32 },
      offset: c.name("offset") { c.read_uint32 },
      prev: c.name("prev") { Innodb::Page.maybe_undefined(c.read_uint32) },
      next: c.name("next") { Innodb::Page.maybe_undefined(c.read_uint32) },
      lsn: c.name("lsn") { c.read_uint64 },
      type: c.name("type") { Innodb::Page.page_type_by_value(c.read_uint16) },
      flush_lsn: c.name("flush_lsn") { c.read_uint64 },
      space_id: c.name("space_id") { c.read_uint32 }
    )
  end
end
fil_trailer() click to toggle source

Return the “fil” trailer from the page, which is common for all page types.

# File lib/innodb/page.rb, line 369
def fil_trailer
  @fil_trailer ||= cursor(pos_fil_trailer).name("fil_trailer") do |c|
    FilTrailer.new(
      checksum: c.name("checksum") { c.read_uint32 },
      lsn_low32: c.name("lsn_low32") { c.read_uint32 }
    )
  end
end
in_doublewrite_buffer?() click to toggle source

Is the page in the doublewrite buffer?

# File lib/innodb/page.rb, line 471
def in_doublewrite_buffer?
  space&.system_space? && space.doublewrite_page?(offset)
end
inspect() click to toggle source

Implement a custom inspect method to avoid irb printing the contents of the page buffer, since it’s very large and mostly not interesting.

# File lib/innodb/page.rb, line 542
def inspect
  "#<#{self.class} #{inspect_header_fields || '(page header unavailable)'}>"
end
inspect_header_fields() click to toggle source
# File lib/innodb/page.rb, line 523
def inspect_header_fields
  return nil unless fil_header

  %i[
    size
    space_id
    offset
    type
    prev
    next
    checksum_valid?
    checksum_type
    torn?
    misplaced?
  ].map { |m| "#{m}=#{send(m).inspect}" }.join(", ")
end
misplaced?() click to toggle source

Is the page misplaced in the wrong file or by offset in the file?

# File lib/innodb/page.rb, line 488
def misplaced?
  !in_doublewrite_buffer? && (misplaced_space? || misplaced_offset?)
end
misplaced_offset?() click to toggle source

Is the page number stored in the header different from the page number which was supposed to be read?

# File lib/innodb/page.rb, line 483
def misplaced_offset?
  offset != @page_number
end
misplaced_space?() click to toggle source

Is the space ID stored in the header different from that of the space provided when initializing this page?

# File lib/innodb/page.rb, line 477
def misplaced_space?
  space && (space_id != space.space_id)
end
name() click to toggle source

Return a simple string to uniquely identify this page within the space. Be careful not to call anything which would instantiate a BufferCursor so that we can use this method in cursor initialization.

# File lib/innodb/page.rb, line 126
def name
  page_offset = BinData::Uint32be.read(@buffer.slice(4, 4))
  page_type = BinData::Uint16be.read(@buffer.slice(24, 2))
  "%i,%s" % [
    page_offset,
    PAGE_TYPE_BY_VALUE[page_type],
  ]
end
pos_fil_header() click to toggle source

Return the byte offset of the start of the “fil” header, which is at the beginning of the page. Included here primarily for completeness.

# File lib/innodb/page.rb, line 155
def pos_fil_header
  0
end
pos_fil_trailer() click to toggle source

Return the byte offset of the start of the “fil” trailer, which is at the end of the page.

# File lib/innodb/page.rb, line 180
def pos_fil_trailer
  size - size_fil_trailer
end
pos_page_body() click to toggle source

Return the position of the “body” of the page, which starts after the FIL header.

# File lib/innodb/page.rb, line 191
def pos_page_body
  pos_fil_header + size_fil_header
end
pos_partial_page_header() click to toggle source

The start of the checksummed portion of the file header.

# File lib/innodb/page.rb, line 165
def pos_partial_page_header
  pos_fil_header + 4
end
size() click to toggle source

Return the page size, to eventually be able to deal with non-16kB pages.

# File lib/innodb/page.rb, line 115
def size
  @size ||= @buffer.size
end
size_fil_header() click to toggle source

Return the size of the “fil” header, in bytes.

# File lib/innodb/page.rb, line 160
def size_fil_header
  4 + 4 + 4 + 4 + 8 + 2 + 8 + 4
end
size_fil_trailer() click to toggle source

Return the size of the “fil” trailer, in bytes.

# File lib/innodb/page.rb, line 185
def size_fil_trailer
  4 + 4
end
size_page_body() click to toggle source

Return the size of the page body, excluding the header and trailer.

# File lib/innodb/page.rb, line 196
def size_page_body
  size - size_fil_trailer - size_fil_header
end
size_partial_page_header() click to toggle source

The size of the portion of the fil header that is included in the checksum. Exclude the following:

:checksum   (offset 4, size 4)
:flush_lsn  (offset 26, size 8)
:space_id   (offset 34, size 4)
# File lib/innodb/page.rb, line 174
def size_partial_page_header
  size_fil_header - 4 - 8 - 4
end
torn?() click to toggle source

Is the LSN stored in the header different from the one stored in the trailer?

# File lib/innodb/page.rb, line 466
def torn?
  fil_header.lsn_low32 != fil_trailer.lsn_low32
end