innodb_ruby 0.8.8 → 0.9.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.
data/lib/innodb/index.rb CHANGED
@@ -3,12 +3,9 @@
3
3
  # An InnoDB index B-tree, given an Innodb::Space and a root page number.
4
4
  class Innodb::Index
5
5
  attr_reader :root
6
- attr_reader :stats
7
- attr_accessor :debug
8
6
  attr_accessor :record_describer
9
7
 
10
8
  def initialize(space, root_page_number, record_describer=nil)
11
- @debug = false
12
9
  @space = space
13
10
  @record_describer = record_describer || space.record_describer
14
11
 
@@ -27,8 +24,6 @@ class Innodb::Index
27
24
  unless @root.prev.nil? && @root.next.nil?
28
25
  raise "Page #{root_page_number} is a node page, but not appear to be the root; it has previous page and next page pointers"
29
26
  end
30
-
31
- reset_stats
32
27
  end
33
28
 
34
29
  def page(page_number)
@@ -37,10 +32,6 @@ class Innodb::Index
37
32
  page
38
33
  end
39
34
 
40
- def reset_stats
41
- @stats = Hash.new(0)
42
- end
43
-
44
35
  # A helper function to access the index ID in the page header.
45
36
  def id
46
37
  @root.page_header[:index_id]
@@ -65,7 +56,7 @@ class Innodb::Index
65
56
 
66
57
  parent_page.each_child_page do |child_page_number, child_min_key|
67
58
  child_page = page(child_page_number)
68
- child_page.record_describer = @space.record_describer
59
+ child_page.record_describer = record_describer
69
60
  if child_page.type == :INDEX
70
61
  if link_proc
71
62
  link_proc.call(parent_page, child_page, child_min_key, depth+1)
@@ -83,16 +74,42 @@ class Innodb::Index
83
74
 
84
75
  # Return the first leaf page in the index by walking down the left side
85
76
  # of the B-tree until a page at the given level is encountered.
86
- def first_page_at_level(level)
77
+ def min_page_at_level(level)
78
+ page = @root
79
+ record = @root.min_record
80
+ while record && page.level > level
81
+ page = page(record.child_page_number)
82
+ record = page.min_record
83
+ end
84
+ page if page.level == level
85
+ end
86
+
87
+ # Return the minimum record in the index.
88
+ def min_record
89
+ if min_page = min_page_at_level(0)
90
+ min_page.min_record
91
+ end
92
+ end
93
+
94
+ # Return the last leaf page in the index by walking down the right side
95
+ # of the B-tree until a page at the given level is encountered.
96
+ def max_page_at_level(level)
87
97
  page = @root
88
- record = @root.first_record
98
+ record = @root.max_record
89
99
  while record && page.level > level
90
100
  page = page(record.child_page_number)
91
- record = page.first_record
101
+ record = page.max_record
92
102
  end
93
103
  page if page.level == level
94
104
  end
95
105
 
106
+ # Return the maximum record in the index.
107
+ def max_record
108
+ if max_page = max_page_at_level(0)
109
+ max_page.max_record
110
+ end
111
+ end
112
+
96
113
  # Return the file segment with the given name from the fseg header.
97
114
  def fseg(name)
98
115
  @root.fseg_header[name]
@@ -155,7 +172,7 @@ class Innodb::Index
155
172
  return enum_for(:each_page_at_level, level)
156
173
  end
157
174
 
158
- each_page_from(first_page_at_level(level)) { |page| yield page }
175
+ each_page_from(min_page_at_level(level)) { |page| yield page }
159
176
  end
160
177
 
161
178
  # Iterate through all records on all leaf pages in ascending order.
@@ -171,173 +188,17 @@ class Innodb::Index
171
188
  end
172
189
  end
173
190
 
174
- # Compare two arrays of fields to determine if they are equal. This follows
175
- # the same comparison rules as strcmp and others:
176
- # 0 = a is equal to b
177
- # -1 = a is less than b
178
- # +1 = a is greater than b
179
- def compare_key(a, b)
180
- @stats[:compare_key] += 1
181
-
182
- return 0 if a.nil? && b.nil?
183
- return -1 if a.nil? || (!b.nil? && a.size < b.size)
184
- return +1 if b.nil? || (!a.nil? && a.size > b.size)
185
-
186
- a.each_index do |i|
187
- @stats[:compare_key_field_comparison] += 1
188
- return -1 if a[i] < b[i][:value]
189
- return +1 if a[i] > b[i][:value]
190
- end
191
-
192
- return 0
193
- end
194
-
195
- # Search for a record within a single page, and return either a perfect
196
- # match for the key, or the last record closest to they key but not greater
197
- # than the key. (If an exact match is desired, compare_key must be used to
198
- # check if the returned record matches. This makes the function useful for
199
- # search in both leaf and non-leaf pages.)
200
- def linear_search_from_cursor(page, cursor, key)
201
- @stats[:linear_search_from_cursor] += 1
202
-
203
- this_rec = cursor.record
204
-
205
- if @debug
206
- puts "linear_search_from_cursor: page=%i, level=%i, start=(%s)" % [
207
- page.offset,
208
- page.level,
209
- this_rec && this_rec.key_string,
210
- ]
211
- end
212
-
213
- # Iterate through all records until finding either a matching record or
214
- # one whose key is greater than the desired key.
215
- while this_rec && next_rec = cursor.record
216
- @stats[:linear_search_from_cursor_record_scans] += 1
217
-
218
- if @debug
219
- puts "linear_search_from_cursor: page=%i, level=%i, current=(%s)" % [
220
- page.offset,
221
- page.level,
222
- this_rec && this_rec.key_string,
223
- ]
224
- end
225
-
226
- # If we reach supremum, return the last non-system record we got.
227
- return this_rec if next_rec.header[:type] == :supremum
228
-
229
- if compare_key(key, this_rec.key) < 0
230
- return this_rec
231
- end
232
-
233
- if (compare_key(key, this_rec.key) >= 0) &&
234
- (compare_key(key, next_rec.key) < 0)
235
- # The desired key is either an exact match for this_rec or is greater
236
- # than it but less than next_rec. If this is a non-leaf page, that
237
- # will mean that the record will fall on the leaf page this node
238
- # pointer record points to, if it exists at all.
239
- return this_rec
240
- end
241
-
242
- this_rec = next_rec
243
- end
244
-
245
- this_rec
246
- end
247
-
248
- # Search or a record within a single page using the page directory to limit
249
- # the number of record comparisons required. Once the last page directory
250
- # entry closest to but not greater than the key is found, fall back to
251
- # linear search using linear_search_from_cursor to find the closest record
252
- # whose key is not greater than the desired key. (If an exact match is
253
- # desired, the returned record must be checked in the same way as the above
254
- # linear_search_from_cursor function.)
255
- def binary_search_by_directory(page, dir, key)
256
- @stats[:binary_search_by_directory] += 1
257
-
258
- return nil if dir.empty?
259
-
260
- # Split the directory at the mid-point (using integer math, so the division
261
- # is rounding down). Retrieve the record that sits at the mid-point.
262
- mid = ((dir.size-1) / 2)
263
- rec = page.record(dir[mid])
264
-
265
- if @debug
266
- puts "binary_search_by_directory: page=%i, level=%i, dir.size=%i, dir[%i]=(%s)" % [
267
- page.offset,
268
- page.level,
269
- dir.size,
270
- mid,
271
- rec.key_string,
272
- ]
273
- end
274
-
275
- # The mid-point record was the infimum record, which is not comparable with
276
- # compare_key, so we need to just linear scan from here. If the mid-point
277
- # is the beginning of the page there can't be many records left to check
278
- # anyway.
279
- if rec.header[:type] == :infimum
280
- return linear_search_from_cursor(page, page.record_cursor(rec.next), key)
281
- end
282
-
283
- # Compare the desired key to the mid-point record's key.
284
- case compare_key(key, rec.key)
285
- when 0
286
- # An exact match for the key was found. Return the record.
287
- @stats[:binary_search_by_directory_exact_match] += 1
288
- rec
289
- when +1
290
- # The mid-point record's key is less than the desired key.
291
- if dir.size > 2
292
- # There are more entries remaining from the directory, recurse again
293
- # using binary search on the right half of the directory, which
294
- # represents values greater than or equal to the mid-point record's
295
- # key.
296
- @stats[:binary_search_by_directory_recurse_right] += 1
297
- binary_search_by_directory(page, dir[mid...dir.size], key)
298
- else
299
- next_rec = page.record(dir[mid+1])
300
- next_key = next_rec && compare_key(key, next_rec.key)
301
- if dir.size == 1 || next_key == -1 || next_key == 0
302
- # This is the last entry remaining from the directory, or our key is
303
- # greater than rec and less than rec+1's key. Use linear search to
304
- # find the record starting at rec.
305
- @stats[:binary_search_by_directory_linear_search] += 1
306
- linear_search_from_cursor(page, page.record_cursor(rec.offset), key)
307
- elsif next_key == +1
308
- @stats[:binary_search_by_directory_linear_search] += 1
309
- linear_search_from_cursor(page, page.record_cursor(next_rec.offset), key)
310
- else
311
- nil
312
- end
313
- end
314
- when -1
315
- # The mid-point record's key is greater than the desired key.
316
- if dir.size == 1
317
- # If this is the last entry remaining from the directory, we didn't
318
- # find anything workable.
319
- @stats[:binary_search_by_directory_empty_result] += 1
320
- nil
321
- else
322
- # Recurse on the left half of the directory, which represents values
323
- # less than the mid-point record's key.
324
- @stats[:binary_search_by_directory_recurse_left] += 1
325
- binary_search_by_directory(page, dir[0...mid], key)
326
- end
327
- end
328
- end
329
-
330
191
  # Search for a record within the entire index, walking down the non-leaf
331
192
  # pages until a leaf page is found, and then verifying that the record
332
193
  # returned on the leaf page is an exact match for the key. If a matching
333
194
  # record is not found, nil is returned (either because linear_search_in_page
334
195
  # returns nil breaking the loop, or because compare_key returns non-zero).
335
196
  def linear_search(key)
336
- @stats[:linear_search] += 1
197
+ Innodb::Stats.increment :linear_search
337
198
 
338
199
  page = @root
339
200
 
340
- if @debug
201
+ if Innodb.debug?
341
202
  puts "linear_search: root=%i, level=%i, key=(%s)" % [
342
203
  page.offset,
343
204
  page.level,
@@ -346,7 +207,7 @@ class Innodb::Index
346
207
  end
347
208
 
348
209
  while rec =
349
- linear_search_from_cursor(page, page.record_cursor(page.infimum.next), key)
210
+ page.linear_search_from_cursor(page.record_cursor(page.infimum.next), key)
350
211
  if page.level > 0
351
212
  # If we haven't reached a leaf page yet, move down the tree and search
352
213
  # again using linear search.
@@ -355,7 +216,7 @@ class Innodb::Index
355
216
  # We're on a leaf page, so return the page and record if there is a
356
217
  # match. If there is no match, break the loop and cause nil to be
357
218
  # returned.
358
- return page, rec if compare_key(key, rec.key) == 0
219
+ return rec if rec.compare_key(key) == 0
359
220
  break
360
221
  end
361
222
  end
@@ -365,11 +226,11 @@ class Innodb::Index
365
226
  # the page directory to search while making as few record comparisons as
366
227
  # possible. If a matching record is not found, nil is returned.
367
228
  def binary_search(key)
368
- @stats[:binary_search] += 1
229
+ Innodb::Stats.increment :binary_search
369
230
 
370
231
  page = @root
371
232
 
372
- if @debug
233
+ if Innodb.debug?
373
234
  puts "binary_search: root=%i, level=%i, key=(%s)" % [
374
235
  page.offset,
375
236
  page.level,
@@ -379,7 +240,7 @@ class Innodb::Index
379
240
 
380
241
  # Remove supremum from the page directory, since nothing can be scanned
381
242
  # linearly from there anyway.
382
- while rec = binary_search_by_directory(page, page.directory[0...-1], key)
243
+ while rec = page.binary_search_by_directory(page.directory[0...-1], key)
383
244
  if page.level > 0
384
245
  # If we haven't reached a leaf page yet, move down the tree and search
385
246
  # again using binary search.
@@ -388,10 +249,120 @@ class Innodb::Index
388
249
  # We're on a leaf page, so return the page and record if there is a
389
250
  # match. If there is no match, break the loop and cause nil to be
390
251
  # returned.
391
- return page, rec if compare_key(key, rec.key) == 0
252
+ return rec if rec.compare_key(key) == 0
392
253
  break
393
254
  end
394
255
  end
395
256
  end
396
257
 
258
+ # A cursor to walk the index (cursor) forwards or backward starting with
259
+ # a given record, or the minimum (:min) or maximum (:max) record in the
260
+ # index.
261
+ class IndexCursor
262
+ def initialize(index, record, direction)
263
+ Innodb::Stats.increment :index_cursor_create
264
+ @initial = true
265
+ @index = index
266
+ @direction = direction
267
+ case record
268
+ when :min
269
+ # Start at the minimum record on the minimum page in the index.
270
+ @page = index.min_page_at_level(0)
271
+ @page_cursor = @page.record_cursor(:min, direction)
272
+ when :max
273
+ # Start at the maximum record on the maximum page in the index.
274
+ @page = index.max_page_at_level(0)
275
+ @page_cursor = @page.record_cursor(:max, direction)
276
+ else
277
+ # Start at the record provided.
278
+ @page = record.page
279
+ @page_cursor = @page.record_cursor(record.offset, direction)
280
+ end
281
+ @record = @page_cursor.record
282
+ end
283
+
284
+ # Return the current record, mostly as a helper.
285
+ def current_record
286
+ @record
287
+ end
288
+
289
+ # Move to the next record in the forward direction and return it.
290
+ def next_record
291
+ Innodb::Stats.increment :index_cursor_next_record
292
+
293
+ while true
294
+ if rec = @page_cursor.record
295
+ return rec
296
+ end
297
+
298
+ unless next_page = @page.next
299
+ return nil
300
+ end
301
+
302
+ unless @page = @index.page(next_page)
303
+ raise "Failed to load next page"
304
+ end
305
+
306
+ unless @page_cursor = @page.record_cursor(:min, @direction)
307
+ raise "Failed to position cursor"
308
+ end
309
+ end
310
+ end
311
+
312
+ # Move to the previous record in the backward direction and return it.
313
+ def prev_record
314
+ Innodb::Stats.increment :index_cursor_prev_record
315
+
316
+ while true
317
+ if rec = @page_cursor.record
318
+ return rec
319
+ end
320
+
321
+ unless prev_page = @page.prev
322
+ return nil
323
+ end
324
+
325
+ unless @page = @index.page(prev_page)
326
+ raise "Failed to load prev page"
327
+ end
328
+
329
+ unless @page_cursor = @page.record_cursor(:max, @direction)
330
+ raise "Failed to position cursor"
331
+ end
332
+ end
333
+ raise "Not implemented"
334
+ end
335
+
336
+ # Return the next record in the order defined when the cursor was created.
337
+ def record
338
+ if @initial
339
+ @initial = false
340
+ return current_record
341
+ end
342
+
343
+ case @direction
344
+ when :forward
345
+ next_record
346
+ when :backward
347
+ prev_record
348
+ end
349
+ end
350
+
351
+ # Iterate through all records in the cursor.
352
+ def each_record
353
+ unless block_given?
354
+ return enum_for(:each_record)
355
+ end
356
+
357
+ while rec = record
358
+ yield rec
359
+ end
360
+ end
361
+ end
362
+
363
+ # Return an IndexCursor starting at the given record (an Innodb::Record,
364
+ # :min, or :max) and cursor in the direction given (:forward or :backward).
365
+ def cursor(record=:min, direction=:forward)
366
+ IndexCursor.new(self, record, direction)
367
+ end
397
368
  end
data/lib/innodb/log.rb CHANGED
@@ -3,50 +3,113 @@
3
3
  # An InnoDB transaction log file.
4
4
 
5
5
  class Innodb::Log
6
- HEADER_SIZE = 4 * Innodb::LogBlock::BLOCK_SIZE
7
- HEADER_START = 0
8
- DATA_START = HEADER_START + HEADER_SIZE
9
-
10
- #define LOG_GROUP_ID 0 /* log group number */
11
- #define LOG_FILE_START_LSN 4 /* lsn of the start of data in this
12
- #define LOG_FILE_NO 12 /* 4-byte archived log file number;
13
- #define LOG_FILE_WAS_CREATED_BY_HOT_BACKUP 16
14
- #define LOG_FILE_ARCH_COMPLETED OS_FILE_LOG_BLOCK_SIZE
15
- #define LOG_FILE_END_LSN (OS_FILE_LOG_BLOCK_SIZE + 4)
16
- #define LOG_CHECKPOINT_1 OS_FILE_LOG_BLOCK_SIZE
17
- #define LOG_CHECKPOINT_2 (3 * OS_FILE_LOG_BLOCK_SIZE)
18
- #define LOG_FILE_HDR_SIZE (4 * OS_FILE_LOG_BLOCK_SIZE)
6
+ # A map of the name and position of the blocks that form the log header.
7
+ LOG_HEADER_BLOCK_MAP = {
8
+ :LOG_FILE_HEADER => 0,
9
+ :LOG_CHECKPOINT_1 => 1,
10
+ :EMPTY => 2,
11
+ :LOG_CHECKPOINT_2 => 3,
12
+ }
13
+
14
+ # Number of blocks in the log file header.
15
+ LOG_HEADER_BLOCKS = LOG_HEADER_BLOCK_MAP.size
16
+
17
+ # Maximum number of log group checkpoints.
18
+ LOG_CHECKPOINT_GROUPS = 32
19
19
 
20
20
  # Open a log file.
21
21
  def initialize(file)
22
- @name = file
23
- File.open(@name) do |log|
24
- @size = log.stat.size
25
- @blocks = ((@size - DATA_START) / Innodb::LogBlock::BLOCK_SIZE)
26
- end
22
+ @file = File.open(file)
23
+ @size = @file.stat.size
24
+ @blocks = (@size / Innodb::LogBlock::BLOCK_SIZE) - LOG_HEADER_BLOCKS
27
25
  end
28
-
26
+
27
+ # The size (in bytes) of the log.
28
+ attr_reader :size
29
+
30
+ # The number of blocks in the the log.
29
31
  attr_reader :blocks
30
32
 
31
- # Return a log block with a given block number as an InnoDB::LogBlock object.
32
- # Blocks are numbered after the log file header, starting from 0.
33
- def block(block_number)
34
- offset = DATA_START + (block_number.to_i * Innodb::LogBlock::BLOCK_SIZE)
35
- return nil unless offset < @size
36
- return nil unless (offset + Innodb::LogBlock::BLOCK_SIZE) <= @size
37
- File.open(@name) do |log|
38
- log.seek(offset)
39
- block_data = log.read(Innodb::LogBlock::BLOCK_SIZE)
40
- Innodb::LogBlock.new(block_data)
33
+ # Get the raw byte buffer for a specific block by block offset.
34
+ def block_data(offset)
35
+ raise "Invalid block offset" unless (offset % Innodb::LogBlock::BLOCK_SIZE).zero?
36
+ @file.seek(offset)
37
+ @file.read(Innodb::LogBlock::BLOCK_SIZE)
38
+ end
39
+
40
+ # Get a cursor to a block in a given offset of the log.
41
+ def block_cursor(offset)
42
+ Innodb::Cursor.new(block_data(offset), 0)
43
+ end
44
+
45
+ # Return the log header.
46
+ def header
47
+ offset = LOG_HEADER_BLOCK_MAP[:LOG_FILE_HEADER] * Innodb::LogBlock::BLOCK_SIZE
48
+ @header ||= block_cursor(offset).name("header") do |c|
49
+ {
50
+ :group_id => c.name("group_id") { c.get_uint32 },
51
+ :start_lsn => c.name("start_lsn") { c.get_uint64 },
52
+ :created_by => c.name("created_by") { c.seek(16).get_bytes(4) }
53
+ }
41
54
  end
42
55
  end
43
56
 
44
- # Iterate through all log blocks, returning the block number and an
57
+ # Read a log checkpoint from the given cursor.
58
+ def read_checkpoint(c)
59
+ # Log archive related fields (e.g. group_array) are not currently in
60
+ # use or even read by InnoDB. However, for the sake of completeness,
61
+ # they are included.
62
+ {
63
+ :number => c.name("number") { c.get_uint64 },
64
+ :lsn => c.name("lsn") { c.get_uint64 },
65
+ :offset => c.name("offset") { c.get_uint32 },
66
+ :buffer_size => c.name("buffer_size") { c.get_uint32 },
67
+ :archived_lsn => c.name("archived_lsn") { c.get_uint64 },
68
+ :group_array =>
69
+ (0 .. LOG_CHECKPOINT_GROUPS - 1).map do |n|
70
+ c.name("group_array[#{n}]") do
71
+ {
72
+ :archived_file_no => c.name("archived_file_no") { c.get_uint32 },
73
+ :archived_offset => c.name("archived_offset") { c.get_uint32 },
74
+ }
75
+ end
76
+ end,
77
+ :checksum_1 => c.name("checksum_1") { c.get_uint32 },
78
+ :checksum_2 => c.name("checksum_2") { c.get_uint32 },
79
+ :fsp_free_limit => c.name("fsp_free_limit") { c.get_uint32 },
80
+ :fsp_magic => c.name("fsp_magic") { c.get_uint32 },
81
+ }
82
+ end
83
+
84
+ # Return the log checkpoints.
85
+ def checkpoint
86
+ offset1 = LOG_HEADER_BLOCK_MAP[:LOG_CHECKPOINT_1] * Innodb::LogBlock::BLOCK_SIZE
87
+ offset2 = LOG_HEADER_BLOCK_MAP[:LOG_CHECKPOINT_2] * Innodb::LogBlock::BLOCK_SIZE
88
+ @checkpoint ||=
89
+ {
90
+ :checkpoint_1 => block_cursor(offset1).name("checkpoint_1") do |cursor|
91
+ read_checkpoint(cursor)
92
+ end,
93
+ :checkpoint_2 => block_cursor(offset2).name("checkpoint_2") do |cursor|
94
+ read_checkpoint(cursor)
95
+ end
96
+ }
97
+ end
98
+
99
+ # Return a log block with a given block index as an InnoDB::LogBlock object.
100
+ # Blocks are indexed after the log file header, starting from 0.
101
+ def block(block_index)
102
+ return nil unless block_index.between?(0, @blocks - 1)
103
+ offset = (LOG_HEADER_BLOCKS + block_index.to_i) * Innodb::LogBlock::BLOCK_SIZE
104
+ Innodb::LogBlock.new(block_data(offset))
105
+ end
106
+
107
+ # Iterate through all log blocks, returning the block index and an
45
108
  # InnoDB::LogBlock object for each block.
46
109
  def each_block
47
- (0...@blocks).each do |block_number|
48
- current_block = block(block_number)
49
- yield block_number, current_block if current_block
110
+ (0...@blocks).each do |block_index|
111
+ current_block = block(block_index)
112
+ yield block_index, current_block if current_block
50
113
  end
51
114
  end
52
115
  end