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.
@@ -29,8 +29,8 @@ require 'zlib'
29
29
 
30
30
  require 'perobs/Log'
31
31
  require 'perobs/FlatFileBlobHeader'
32
- require 'perobs/IndexTree'
33
- require 'perobs/FreeSpaceManager'
32
+ require 'perobs/BTree'
33
+ require 'perobs/SpaceTree'
34
34
 
35
35
  module PEROBS
36
36
 
@@ -39,34 +39,59 @@ module PEROBS
39
39
  # blob data bytes.
40
40
  class FlatFile
41
41
 
42
+ # The number of entries in a single BTree node of the index file.
43
+ INDEX_BTREE_ORDER = 65
44
+
42
45
  # Create a new FlatFile object for a database in the given path.
43
46
  # @param dir [String] Directory path for the data base file
44
47
  def initialize(dir)
45
48
  @db_dir = dir
46
49
  @f = nil
47
- @index = IndexTree.new(dir)
48
- @space_list = FreeSpaceManager.new(dir)
50
+ @index = BTree.new(@db_dir, 'index', INDEX_BTREE_ORDER)
51
+ @space_list = SpaceTree.new(@db_dir)
49
52
  end
50
53
 
51
54
  # Open the flat file for reading and writing.
52
55
  def open
53
56
  file_name = File.join(@db_dir, 'database.blobs')
57
+ new_db_created = false
54
58
  begin
55
59
  if File.exist?(file_name)
56
60
  @f = File.open(file_name, 'rb+')
57
61
  else
58
- PEROBS.log.info 'New database.blobs file created'
62
+ PEROBS.log.info "New FlatFile database '#{file_name}' created"
59
63
  @f = File.open(file_name, 'wb+')
64
+ new_db_created = true
60
65
  end
61
66
  rescue IOError => e
62
- PEROBS.log.fatal "Cannot open flat file database #{file_name}: " +
67
+ PEROBS.log.fatal "Cannot open FlatFile database #{file_name}: " +
63
68
  e.message
64
69
  end
65
70
  unless @f.flock(File::LOCK_NB | File::LOCK_EX)
66
- PEROBS.log.fatal 'Database is locked by another process'
71
+ PEROBS.log.fatal "FlatFile database '#{file_name}' is locked by " +
72
+ "another process"
73
+ end
74
+
75
+ begin
76
+ @index.open(!new_db_created)
77
+ @space_list.open
78
+ rescue FatalError
79
+ # Ensure that the index is really closed.
80
+ @index.close
81
+ # Erase it completely
82
+ @index.erase
83
+ # Then create it again.
84
+ @index.open
85
+
86
+ # Ensure that the spaces list is really closed.
87
+ @space_list.close
88
+ # Erase it completely
89
+ @space_list.erase
90
+ # Then create it again
91
+ @space_list.open
92
+
93
+ regenerate_index_and_spaces
67
94
  end
68
- @index.open
69
- @space_list.open
70
95
  end
71
96
 
72
97
  # Close the flat file. This method must be called to ensure that all data
@@ -74,10 +99,13 @@ module PEROBS
74
99
  def close
75
100
  @space_list.close
76
101
  @index.close
77
- @f.flush
78
- @f.flock(File::LOCK_UN)
79
- @f.close
80
- @f = nil
102
+
103
+ if @f
104
+ @f.flush
105
+ @f.flock(File::LOCK_UN)
106
+ @f.close
107
+ @f = nil
108
+ end
81
109
  end
82
110
 
83
111
  # Force outstanding data to be written to the filesystem.
@@ -87,6 +115,7 @@ module PEROBS
87
115
  rescue IOError => e
88
116
  PEROBS.log.fatal "Cannot sync flat file database: #{e.message}"
89
117
  end
118
+ @index.sync
90
119
  end
91
120
 
92
121
  # Delete the blob for the specified ID.
@@ -105,16 +134,10 @@ module PEROBS
105
134
  # @param addr [Integer] Address of the blob to delete
106
135
  # @param id [Integer] ID of the blob to delete
107
136
  def delete_obj_by_address(addr, id)
108
- @index.delete_value(id)
137
+ @index.remove(id)
109
138
  header = FlatFileBlobHeader.read_at(@f, addr, id)
110
- begin
111
- @f.seek(addr)
112
- @f.write([ 0 ].pack('C'))
113
- @f.flush
114
- @space_list.add_space(addr, header.length)
115
- rescue IOError => e
116
- PEROBS.log.fatal "Cannot erase blob for ID #{header.id}: #{e.message}"
117
- end
139
+ header.clear_flags
140
+ @space_list.add_space(addr, header.length)
118
141
  end
119
142
 
120
143
  # Delete all unmarked objects.
@@ -136,12 +159,23 @@ module PEROBS
136
159
  deleted_ids
137
160
  end
138
161
 
139
- # Write the given object into the file. This method assumes that no other
140
- # entry with the given ID exists already in the file.
162
+ # Write the given object into the file. This method never uses in-place
163
+ # updates for existing objects. A new copy is inserted first and only when
164
+ # the insert was successful, the old copy is deleted and the index
165
+ # updated.
141
166
  # @param id [Integer] ID of the object
142
167
  # @param raw_obj [String] Raw object as String
143
168
  # @return [Integer] position of the written blob in the blob file
144
169
  def write_obj_by_id(id, raw_obj)
170
+ # Check if we have already an object with the given ID. We'll mark it as
171
+ # outdated and save the header for later deletion. In case this
172
+ # operation is aborted or interrupted we ensure that we either have the
173
+ # old or the new version available.
174
+ if (old_addr = find_obj_addr_by_id(id))
175
+ old_header = FlatFileBlobHeader.read_at(@f, old_addr)
176
+ old_header.set_outdated_flag
177
+ end
178
+
145
179
  crc = checksum(raw_obj)
146
180
 
147
181
  # If the raw_obj is larger then 256 characters we will compress it to
@@ -168,12 +202,12 @@ module PEROBS
168
202
  "blob space (#{header.length})."
169
203
  end
170
204
  if header.is_valid?
171
- PEROBS.log.fatal "Entry (mark: #{header.mark}) is already used."
205
+ PEROBS.log.fatal "Entry (flags: #{header.flags}) is already used."
172
206
  end
173
207
  end
174
- @f.seek(addr)
175
- FlatFileBlobHeader.new(compressed ? (1 << 2) | 1 : 1, raw_obj.length,
176
- id, crc).write(@f)
208
+ flags = 1 << FlatFileBlobHeader::VALID_FLAG_BIT
209
+ flags |= (1 << FlatFileBlobHeader::COMPRESSED_FLAG_BIT) if compressed
210
+ FlatFileBlobHeader.new(@f, addr, flags, raw_obj.length, id, crc).write
177
211
  @f.write(raw_obj)
178
212
  if length != -1 && raw_obj.length < length
179
213
  # The new object was not appended and it did not completely fill the
@@ -186,12 +220,20 @@ module PEROBS
186
220
  end
187
221
  space_address = @f.pos
188
222
  space_length = length - FlatFileBlobHeader::LENGTH - raw_obj.length
189
- FlatFileBlobHeader.new(0, space_length, 0, 0).write(@f)
223
+ FlatFileBlobHeader.new(@f, space_address, 0, space_length,
224
+ 0, 0).write
190
225
  # Register the new space with the space list.
191
226
  @space_list.add_space(space_address, space_length) if space_length > 0
192
227
  end
193
- @f.flush
194
- @index.put_value(id, addr)
228
+ if old_addr
229
+ # If we had an existing object stored for the ID we have to mark
230
+ # this entry as deleted now.
231
+ old_header.clear_flags
232
+ else
233
+ @f.flush
234
+ end
235
+ # Once the blob has been written we can update the index as well.
236
+ @index.insert(id, addr)
195
237
  rescue IOError => e
196
238
  PEROBS.log.fatal "Cannot write blob for ID #{id} to FlatFileDB: " +
197
239
  e.message
@@ -204,7 +246,7 @@ module PEROBS
204
246
  # @param id [Integer] ID of the object
205
247
  # @return [Integer] Offset in the flat file or nil if not found
206
248
  def find_obj_addr_by_id(id)
207
- @index.get_value(id)
249
+ @index.get(id)
208
250
  end
209
251
 
210
252
  # Read the object with the given ID.
@@ -238,9 +280,14 @@ module PEROBS
238
280
  PEROBS.log.fatal "Cannot read blob for ID #{id}: #{e.message}"
239
281
  end
240
282
 
241
- # Uncompress the data if the compression bit is set in the mark byte.
283
+ # Uncompress the data if the compression bit is set in the flags byte.
242
284
  if header.is_compressed?
243
- buf = Zlib.inflate(buf)
285
+ begin
286
+ buf = Zlib.inflate(buf)
287
+ rescue Zlib::BufError, Zlib::DataError
288
+ PEROBS.log.fatal "Corrupted compressed block with ID " +
289
+ "#{header.id} found."
290
+ end
244
291
  end
245
292
 
246
293
  if checksum(buf) != header.crc
@@ -262,15 +309,7 @@ module PEROBS
262
309
  # @param addr [Integer] Offset in the file
263
310
  # @param id [Integer] ID of the object
264
311
  def mark_obj_by_address(addr, id)
265
- header = FlatFileBlobHeader.read_at(@f, addr, id)
266
- begin
267
- @f.seek(addr)
268
- @f.write([ header.mark | (1 << 1) ].pack('C'))
269
- @f.flush
270
- rescue IOError => e
271
- PEROBS.log.fatal "Marking of FlatFile blob with ID #{id} " +
272
- "failed: #{e.message}"
273
- end
312
+ FlatFileBlobHeader.read_at(@f, addr, id).set_mark_flag
274
313
  end
275
314
 
276
315
  # Return true if the object with the given ID is marked, false otherwise.
@@ -297,14 +336,7 @@ module PEROBS
297
336
  if header.is_valid? && header.is_marked?
298
337
  # Clear all valid and marked blocks.
299
338
  marked_blob_count += 1
300
- begin
301
- @f.seek(pos)
302
- @f.write([ header.mark & 0b11111101 ].pack('C'))
303
- @f.flush
304
- rescue IOError => e
305
- PEROBS.log.fatal "Unmarking of FlatFile blob with ID #{blob_id} " +
306
- "failed: #{e.message}"
307
- end
339
+ header.clear_mark_flag
308
340
  end
309
341
  end
310
342
  PEROBS.log.info "#{marked_blob_count} marks in #{total_blob_count} " +
@@ -315,6 +347,7 @@ module PEROBS
315
347
  # implementation. No additional space will be needed on the file system.
316
348
  def defragmentize
317
349
  distance = 0
350
+ new_file_size = 0
318
351
  deleted_blobs = 0
319
352
  valid_blobs = 0
320
353
  t = Time.now
@@ -335,17 +368,19 @@ module PEROBS
335
368
  @f.seek(pos - distance)
336
369
  @f.write(buf)
337
370
  # Update the index with the new position
338
- @index.put_value(header.id, pos - distance)
371
+ @index.insert(header.id, pos - distance)
339
372
  # Mark the space between the relocated current entry and the
340
373
  # next valid entry as deleted space.
341
- FlatFileBlobHeader.new(0, distance - FlatFileBlobHeader::LENGTH,
342
- 0, 0).write(@f)
374
+ FlatFileBlobHeader.new(@f, @f.pos, 0,
375
+ distance - FlatFileBlobHeader::LENGTH,
376
+ 0, 0).write
343
377
  @f.flush
344
378
  rescue IOError => e
345
379
  PEROBS.log.fatal "Error while moving blob for ID #{header.id}: " +
346
380
  e.message
347
381
  end
348
382
  end
383
+ new_file_size = pos + FlatFileBlobHeader::LENGTH + header.length
349
384
  else
350
385
  deleted_blobs += 1
351
386
  distance += entry_bytes
@@ -357,7 +392,7 @@ module PEROBS
357
392
  "#{'%.1f' % (distance.to_f / @f.size * 100.0)}% reclaimed"
358
393
 
359
394
  @f.flush
360
- @f.truncate(@f.size - distance)
395
+ @f.truncate(new_file_size)
361
396
  @f.flush
362
397
  @space_list.clear
363
398
 
@@ -393,56 +428,116 @@ module PEROBS
393
428
  defragmentize
394
429
  end
395
430
 
431
+ # Check (and repair) the FlatFile.
432
+ # @param repair [Boolean] True if errors should be fixed.
433
+ # @return [Integer] Number of errors found
396
434
  def check(repair = false)
397
- return unless @f
435
+ errors = 0
436
+ return errors unless @f
398
437
 
399
438
  t = Time.now
400
439
  PEROBS.log.info "Checking FlatFile database" +
401
440
  "#{repair ? ' in repair mode' : ''}..."
402
441
 
403
442
  # First check the database blob file. Each entry should be readable and
404
- # correct.
443
+ # correct and all IDs must be unique. We use a shadow index to keep
444
+ # track of the already found IDs.
445
+ new_index = BTree.new(@db_dir, 'new-index', INDEX_BTREE_ORDER)
446
+ new_index.erase
447
+ new_index.open
448
+
405
449
  each_blob_header do |pos, header|
406
450
  if header.is_valid?
407
451
  # We have a non-deleted entry.
408
452
  begin
409
453
  @f.seek(pos + FlatFileBlobHeader::LENGTH)
410
454
  buf = @f.read(header.length)
455
+ if buf.length != header.length
456
+ PEROBS.log.error "Premature end of file in blob with ID " +
457
+ "#{header.id}."
458
+ discard_damaged_blob(header) if repair
459
+ errors += 1
460
+ next
461
+ end
462
+
411
463
  # Uncompress the data if the compression bit is set in the mark
412
464
  # byte.
413
- buf = Zlib.inflate(buf) if header.is_compressed?
465
+ if header.is_compressed?
466
+ begin
467
+ buf = Zlib.inflate(buf)
468
+ rescue Zlib::BufError, Zlib::DataError
469
+ PEROBS.log.error "Corrupted compressed block with ID " +
470
+ "#{header.id} found."
471
+ discard_damaged_blob(header) if repair
472
+ errors += 1
473
+ next
474
+ end
475
+ end
414
476
 
415
477
  if header.crc && checksum(buf) != header.crc
416
- if repair
417
- PEROBS.log.error "Checksum failure while checking blob " +
418
- "with ID #{header.id}. Deleting object."
419
- delete_obj_by_address(pos, header.id)
420
- else
421
- PEROBS.log.fatal "Checksum failure while checking blob " +
422
- "with ID #{header.id}"
423
- end
478
+ PEROBS.log.error "Checksum failure while checking blob " +
479
+ "with ID #{header.id}"
480
+ discard_damaged_blob(header) if repair
481
+ errors += 1
482
+ next
424
483
  end
425
484
  rescue IOError => e
426
485
  PEROBS.log.fatal "Check of blob with ID #{header.id} failed: " +
427
486
  e.message
428
487
  end
488
+
489
+ # Check if the ID has already been found in the file.
490
+ if (previous_address = new_index.get(header.id))
491
+ PEROBS.log.error "Multiple blobs for ID #{header.id} found. " +
492
+ "Addresses: #{previous_address}, #{pos}"
493
+ previous_header = FlatFileBlobHeader.read_at(@f, previous_address,
494
+ header.id)
495
+ if repair
496
+ # We have two blobs with the same ID and we must discard one of
497
+ # them.
498
+ if header.is_outdated?
499
+ discard_damaged_blob(header)
500
+ elsif previous_header.is_outdated?
501
+ discard_damaged_blob(previous_header)
502
+ else
503
+ PEROBS.log.error "None of the blobs with same ID have " +
504
+ "the outdated flag set. Deleting the smaller one."
505
+ discard_damaged_blob(header.length < previous_header.length ?
506
+ header : previous_header)
507
+ end
508
+ next
509
+ end
510
+ else
511
+ # ID is unique so far. Add it to the shadow index.
512
+ new_index.insert(header.id, pos)
513
+ end
514
+
429
515
  end
430
516
  end
517
+ # We no longer need the new index.
518
+ new_index.close
519
+ new_index.erase
431
520
 
432
521
  # Now we check the index data. It must be correct and the entries must
433
522
  # match the blob file. All entries in the index must be in the blob file
434
523
  # and vise versa.
435
524
  begin
436
- unless @index.check(self) && @space_list.check(self) &&
437
- cross_check_entries
525
+ index_ok = @index.check do |id, address|
526
+ has_id_at?(id, address)
527
+ end
528
+ unless index_ok && @space_list.check(self) && cross_check_entries
438
529
  regenerate_index_and_spaces if repair
439
530
  end
440
531
  rescue PEROBS::FatalError
532
+ errors += 1
441
533
  regenerate_index_and_spaces if repair
442
534
  end
443
535
 
444
536
  sync if repair
445
- PEROBS.log.info "check_db completed in #{Time.now - t} seconds"
537
+ PEROBS.log.info "check_db completed in #{Time.now - t} seconds. " +
538
+ "#{errors} errors found."
539
+
540
+ errors
446
541
  end
447
542
 
448
543
  # This method clears the index tree and the free space list and
@@ -454,7 +549,7 @@ module PEROBS
454
549
 
455
550
  each_blob_header do |pos, header|
456
551
  if header.is_valid?
457
- @index.put_value(header.id, pos)
552
+ @index.insert(header.id, pos)
458
553
  else
459
554
  @space_list.add_space(pos, header.length) if header.length > 0
460
555
  end
@@ -474,7 +569,7 @@ module PEROBS
474
569
  def inspect
475
570
  s = '['
476
571
  each_blob_header do |pos, header|
477
- s << "{ :pos => #{pos}, :mark => #{header.mark}, " +
572
+ s << "{ :pos => #{pos}, :flags => #{header.flags}, " +
478
573
  ":length => #{header.length}, :id => #{header.id}, " +
479
574
  ":crc => #{header.crc}"
480
575
  if header.is_valid?
@@ -485,8 +580,6 @@ module PEROBS
485
580
  s + ']'
486
581
  end
487
582
 
488
-
489
-
490
583
  private
491
584
 
492
585
  def each_blob_header(&block)
@@ -529,6 +622,8 @@ module PEROBS
529
622
  end
530
623
 
531
624
  def cross_check_entries
625
+ errors = 0
626
+
532
627
  each_blob_header do |pos, header|
533
628
  if !header.is_valid?
534
629
  if header.length > 0
@@ -536,19 +631,25 @@ module PEROBS
536
631
  PEROBS.log.error "FlatFile has free space " +
537
632
  "(addr: #{pos}, len: #{header.length}) that is not in " +
538
633
  "FreeSpaceManager"
539
- return false
634
+ errors += 1
540
635
  end
541
636
  end
542
637
  else
543
- unless @index.get_value(header.id) == pos
638
+ unless @index.get(header.id) == pos
544
639
  PEROBS.log.error "FlatFile blob at address #{pos} is listed " +
545
- "in index with address #{@index.get_value(header.id)}"
546
- return false
640
+ "in index with address #{@index.get(header.id)}"
641
+ errors += 1
547
642
  end
548
643
  end
549
644
  end
550
645
 
551
- true
646
+ errors == 0
647
+ end
648
+
649
+ def discard_damaged_blob(header)
650
+ PEROBS.log.error "Discarding corrupted data blob for ID #{header.id} " +
651
+ "at offset #{header.addr}"
652
+ header.clear_flags
552
653
  end
553
654
 
554
655
  end
@@ -31,16 +31,17 @@ module PEROBS
31
31
 
32
32
  # The FlatFile blob header has the following structure:
33
33
  #
34
- # 1 Byte: Mark byte.
34
+ # 1 Byte: Flags byte.
35
35
  # Bit 0: 0 deleted entry, 1 valid entry
36
36
  # Bit 1: 0 unmarked, 1 marked
37
37
  # Bit 2: 0 uncompressed data, 1 compressed data
38
- # Bit 3 - 7: reserved, must be 0
38
+ # Bit 3: 0 current entry, 1 outdated entry
39
+ # Bit 4 - 7: reserved, must be 0
39
40
  # 8 bytes: Length of the data blob in bytes
40
41
  # 8 bytes: ID of the value in the data blob
41
42
  # 4 bytes: CRC32 checksum of the data blob
42
43
  #
43
- # If the bit 0 of the mark byte is 0, only the length is valid. The blob is
44
+ # If the bit 0 of the flags byte is 0, only the length is valid. The blob is
44
45
  # empty. Only of bit 0 is set then entry is valid.
45
46
  class FlatFileBlobHeader
46
47
 
@@ -48,16 +49,24 @@ module PEROBS
48
49
  FORMAT = 'CQQL'
49
50
  # The length of the header in bytes.
50
51
  LENGTH = 21
52
+ VALID_FLAG_BIT = 0
53
+ MARK_FLAG_BIT = 1
54
+ COMPRESSED_FLAG_BIT = 2
55
+ OUTDATED_FLAG_BIT = 3
51
56
 
52
- attr_reader :mark, :length, :id, :crc
57
+ attr_reader :addr, :flags, :length, :id, :crc
53
58
 
54
- # Create a new FlatFileBlobHeader with the given mark, length, id and crc.
55
- # @param mark [Fixnum] 8 bit number, see above
59
+ # Create a new FlatFileBlobHeader with the given flags, length, id and crc.
60
+ # @param file [File] the FlatFile that contains the header
61
+ # @param addr [Integer] the offset address of the header in the file
62
+ # @param flags [Fixnum] 8 bit number, see above
56
63
  # @param length [Fixnum] length of the header in bytes
57
64
  # @param id [Integer] ID of the blob entry
58
65
  # @param crc [Fixnum] CRC32 checksum of the blob entry
59
- def initialize(mark, length, id, crc)
60
- @mark = mark
66
+ def initialize(file, addr, flags, length, id, crc)
67
+ @file = file
68
+ @addr = addr
69
+ @flags = flags
61
70
  @length = length
62
71
  @id = id
63
72
  @crc = crc
@@ -68,14 +77,22 @@ module PEROBS
68
77
  # @return FlatFileBlobHeader
69
78
  def FlatFileBlobHeader::read(file)
70
79
  begin
80
+ addr = file.pos
71
81
  buf = file.read(LENGTH)
72
82
  rescue IOError => e
73
- PEROBS.log.fatal "Cannot read blob header in flat file DB: #{e.message}"
83
+ PEROBS.log.error "Cannot read blob header in flat file DB: #{e.message}"
84
+ return nil
74
85
  end
75
86
 
76
87
  return nil unless buf
77
88
 
78
- FlatFileBlobHeader.new(*buf.unpack(FORMAT))
89
+ if buf.length != LENGTH
90
+ PEROBS.log.error "Incomplete FlatFileBlobHeader: Only #{buf.length} " +
91
+ "bytes of #{LENGTH} could be read"
92
+ return nil
93
+ end
94
+
95
+ FlatFileBlobHeader.new(file, addr, *buf.unpack(FORMAT))
79
96
  end
80
97
 
81
98
  # Read the header from the given File.
@@ -96,7 +113,7 @@ module PEROBS
96
113
  "#{id ? "for ID #{id} " : ''}at address " +
97
114
  "#{addr}"
98
115
  end
99
- header = FlatFileBlobHeader.new(*buf.unpack(FORMAT))
116
+ header = FlatFileBlobHeader.new(file, addr, *buf.unpack(FORMAT))
100
117
  if id && header.id != id
101
118
  PEROBS.log.fatal "Mismatch between FlatFile index and blob file " +
102
119
  "found for entry with ID #{id}/#{header.id}"
@@ -107,35 +124,93 @@ module PEROBS
107
124
 
108
125
  # Write the header to a given File.
109
126
  # @param file [File]
110
- def write(file)
127
+ def write
111
128
  begin
112
- file.write([ @mark, @length, @id, @crc].pack(FORMAT))
129
+ @file.seek(@addr)
130
+ @file.write([ @flags, @length, @id, @crc].pack(FORMAT))
113
131
  rescue IOError => e
114
132
  PEROBS.log.fatal "Cannot write blob header into flat file DB: " +
115
133
  e.message
116
134
  end
117
135
  end
118
136
 
137
+ # Reset all the flags bit to 0. This marks the blob as invalid.
138
+ # @param file [File] The file handle of the blob file.
139
+ # @param addr [Integer] The address of the header
140
+ def clear_flags
141
+ begin
142
+ @file.seek(@addr)
143
+ @file.write([ 0 ].pack('C'))
144
+ @file.flush
145
+ rescue IOError => e
146
+ PEROBS.log.fatal "Clearing flags of FlatFileBlobHeader with ID " +
147
+ "#{@id} failed: #{e.message}"
148
+ end
149
+ end
150
+
119
151
  # Return true if the header is for a non-empty blob.
120
152
  def is_valid?
121
- bit_set?(0)
153
+ bit_set?(VALID_FLAG_BIT)
122
154
  end
123
155
 
124
156
  # Return true if the blob has been marked.
125
157
  def is_marked?
126
- bit_set?(1)
158
+ bit_set?(MARK_FLAG_BIT)
159
+ end
160
+
161
+ # Set the mark bit.
162
+ def set_mark_flag
163
+ set_flag(MARK_FLAG_BIT)
164
+ write_flags
165
+ end
166
+
167
+ # Clear the mark bit.
168
+ def clear_mark_flag
169
+ clear_flag(MARK_FLAG_BIT)
170
+ write_flags
127
171
  end
128
172
 
129
173
  # Return true if the blob contains compressed data.
130
174
  def is_compressed?
131
- bit_set?(2)
175
+ bit_set?(COMPRESSED_FLAG_BIT)
176
+ end
177
+
178
+ # Set the outdated bit. The entry will be invalid as soon as the current
179
+ # transaction has been completed.
180
+ def set_outdated_flag
181
+ set_flag(OUTDATED_FLAG_BIT)
182
+ write_flags
183
+ end
184
+
185
+ # Return true if the blob contains outdated data.
186
+ def is_outdated?
187
+ bit_set?(OUTDATED_FLAG_BIT)
132
188
  end
133
189
 
134
190
  private
135
191
 
192
+ def write_flags
193
+ begin
194
+ @file.seek(@addr)
195
+ @file.write([ @flags ].pack('C'))
196
+ @file.flush
197
+ rescue IOError => e
198
+ PEROBS.log.fatal "Writing flags of FlatFileBlobHeader with ID #{@id} " +
199
+ "failed: #{e.message}"
200
+ end
201
+ end
202
+
136
203
  def bit_set?(n)
137
204
  mask = 1 << n
138
- @mark & mask == mask
205
+ @flags & mask == mask
206
+ end
207
+
208
+ def set_flag(n)
209
+ @flags |= (1 << n)
210
+ end
211
+
212
+ def clear_flag(n)
213
+ @flags &= ~(1 << n) & 0xFF
139
214
  end
140
215
 
141
216
  end