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.
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