higgs 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. data/ChangeLog +208 -0
  2. data/LICENSE +26 -0
  3. data/README +2 -0
  4. data/Rakefile +75 -0
  5. data/bin/higgs_backup +67 -0
  6. data/bin/higgs_dump_index +43 -0
  7. data/bin/higgs_dump_jlog +42 -0
  8. data/bin/higgs_verify +37 -0
  9. data/lib/cgi/session/higgs.rb +72 -0
  10. data/lib/higgs/block.rb +192 -0
  11. data/lib/higgs/cache.rb +117 -0
  12. data/lib/higgs/dbm.rb +55 -0
  13. data/lib/higgs/exceptions.rb +31 -0
  14. data/lib/higgs/flock.rb +77 -0
  15. data/lib/higgs/index.rb +164 -0
  16. data/lib/higgs/jlog.rb +159 -0
  17. data/lib/higgs/lock.rb +189 -0
  18. data/lib/higgs/storage.rb +1086 -0
  19. data/lib/higgs/store.rb +228 -0
  20. data/lib/higgs/tar.rb +390 -0
  21. data/lib/higgs/thread.rb +370 -0
  22. data/lib/higgs/tman.rb +513 -0
  23. data/lib/higgs/utils/bman.rb +285 -0
  24. data/lib/higgs/utils.rb +22 -0
  25. data/lib/higgs/version.rb +21 -0
  26. data/lib/higgs.rb +59 -0
  27. data/misc/cache_bench/cache_bench.rb +43 -0
  28. data/misc/dbm_bench/.strc +8 -0
  29. data/misc/dbm_bench/Rakefile +78 -0
  30. data/misc/dbm_bench/dbm_multi_thread.rb +199 -0
  31. data/misc/dbm_bench/dbm_rnd_delete.rb +43 -0
  32. data/misc/dbm_bench/dbm_rnd_read.rb +44 -0
  33. data/misc/dbm_bench/dbm_rnd_update.rb +44 -0
  34. data/misc/dbm_bench/dbm_seq_read.rb +45 -0
  35. data/misc/dbm_bench/dbm_seq_write.rb +44 -0
  36. data/misc/dbm_bench/st_verify.rb +28 -0
  37. data/misc/io_bench/cksum_bench.rb +48 -0
  38. data/misc/io_bench/jlog_bench.rb +71 -0
  39. data/misc/io_bench/write_bench.rb +128 -0
  40. data/misc/thread_bench/lock_bench.rb +132 -0
  41. data/mkrdoc.rb +8 -0
  42. data/rdoc.yml +13 -0
  43. data/sample/count.rb +60 -0
  44. data/sample/dbmtest.rb +38 -0
  45. data/test/Rakefile +45 -0
  46. data/test/run.rb +32 -0
  47. data/test/test_block.rb +163 -0
  48. data/test/test_cache.rb +214 -0
  49. data/test/test_cgi_session.rb +142 -0
  50. data/test/test_flock.rb +162 -0
  51. data/test/test_index.rb +258 -0
  52. data/test/test_jlog.rb +180 -0
  53. data/test/test_lock.rb +320 -0
  54. data/test/test_online_backup.rb +169 -0
  55. data/test/test_storage.rb +439 -0
  56. data/test/test_storage_conf.rb +202 -0
  57. data/test/test_storage_init_opts.rb +89 -0
  58. data/test/test_store.rb +211 -0
  59. data/test/test_tar.rb +432 -0
  60. data/test/test_thread.rb +541 -0
  61. data/test/test_tman.rb +875 -0
  62. data/test/test_tman_init_opts.rb +56 -0
  63. data/test/test_utils_bman.rb +234 -0
  64. metadata +115 -0
@@ -0,0 +1,1086 @@
1
+ # = transactional storage core
2
+ #
3
+ # Author:: $Author: toki $
4
+ # Date:: $Date: 2007-09-26 00:20:20 +0900 (Wed, 26 Sep 2007) $
5
+ # Revision:: $Revision: 559 $
6
+ #
7
+ # == license
8
+ # :include:LICENSE
9
+ #
10
+
11
+ require 'forwardable'
12
+ require 'higgs/block'
13
+ require 'higgs/cache'
14
+ require 'higgs/flock'
15
+ require 'higgs/index'
16
+ require 'higgs/jlog'
17
+ require 'higgs/tar'
18
+ require 'higgs/thread'
19
+ require 'thread'
20
+ require 'yaml'
21
+
22
+ module Higgs
23
+ # = transactional storage core
24
+ class Storage
25
+ # for ident(1)
26
+ CVS_ID = '$Id: storage.rb 559 2007-09-25 15:20:20Z toki $'
27
+
28
+ extend Forwardable
29
+ include Exceptions
30
+
31
+ class Error < HiggsError
32
+ end
33
+
34
+ class PanicError < Error
35
+ end
36
+
37
+ class NotWritableError < Error
38
+ end
39
+
40
+ class ShutdownException < Exceptions::ShutdownException
41
+ end
42
+
43
+ PROPERTIES_CKSUM_TYPE = 'SUM16'
44
+ PROPERTIES_CKSUM_BITS = 16
45
+
46
+ DATA_HASH = {}
47
+ [ [ :SUM16, proc{|s| s.sum(16).to_s }, nil ],
48
+ [ :MD5, proc{|s| Digest::MD5.hexdigest(s) }, 'digest/md5' ],
49
+ [ :RMD160, proc{|s| Digest::RMD160.hexdigest(s) }, 'digest/rmd160' ],
50
+ [ :SHA1, proc{|s| Digest::SHA1.hexdigest(s) }, 'digest/sha1' ],
51
+ [ :SHA256, proc{|s| Digest::SHA256.hexdigest(s) }, 'digest/sha2' ],
52
+ [ :SHA384, proc{|s| Digest::SHA384.hexdigest(s) }, 'digest/sha2' ],
53
+ [ :SHA512, proc{|s| Digest::SHA512.hexdigest(s) }, 'digest/sha2' ]
54
+ ].each do |hash_symbol, hash_proc, hash_lib|
55
+ if (hash_lib) then
56
+ begin
57
+ require(hash_lib)
58
+ rescue LoadError
59
+ next
60
+ end
61
+ end
62
+ DATA_HASH[hash_symbol] = hash_proc
63
+ end
64
+
65
+ DATA_HASH_BIN = {}
66
+ DATA_HASH.each do |cksum_symbol, cksum_proc|
67
+ DATA_HASH_BIN[cksum_symbol.to_s] = cksum_proc
68
+ end
69
+
70
+ # options for Higgs::Storage
71
+ module InitOptions
72
+ # these options are defined.
73
+ # [<tt>:number_of_read_io</tt>] number of read I/O handle of pool. default is <tt>2</tt>.
74
+ # [<tt>:read_only</tt>] if <tt>true</tt> then storage is read-only. default is <tt>false</tt>.
75
+ # [<tt>:properties_cache</tt>] read-cache for properties. default is a new instance of Higgs::LRUCache.
76
+ # [<tt>:data_hash_type</tt>] hash type (<tt>:SUM16</tt>, <tt>:MD5</tt>, <tt>:RMD160</tt>,
77
+ # <tt>:SHA1</tt>, <tt>:SHA256</tt>, <tt>:SHA384</tt> or <tt>:SHA512</tt>)
78
+ # for data check. default is <tt>:MD5</tt>.
79
+ # [<tt>:jlog_sync</tt>] see Higgs::JournalLogger for detail. default is <tt>false</tt>.
80
+ # [<tt>:jlog_hash_type</tt>] see Higgs::JournalLogger for detail. default is <tt>:MD5</tt>.
81
+ # [<tt>:jlog_rotate_size</tt>] when this size is exceeded, journal log is switched to a new file.
82
+ # default is <tt>1024 * 256</tt>.
83
+ # [<tt>:jlog_rotate_max</tt>] old journal log is preserved in this number.
84
+ # if <tt>:jlog_rotate_max</tt> is <tt>0</tt>, old journal log is
85
+ # not deleted. if online-backup is used, <tt>:jlog_rotate_max</tt>
86
+ # should be <tt>0</tt>. default is <tt>1</tt>.
87
+ # [<tt>:jlog_rotate_service_uri</tt>] URI for DRb remote call to switch journal log to a new file.
88
+ # if online-backup is used, <tt>:jlog_rotate_service_uri</tt>
89
+ # should be defined. default is not defined.
90
+ # [<tt>:logger</tt>] procedure to create a logger. default is a procedure to create a new
91
+ # instance of Logger with logging level <tt>Logger::WARN</tt>.
92
+ def init_options(options)
93
+ @number_of_read_io = options[:number_of_read_io] || 2
94
+
95
+ if (options.key? :read_only) then
96
+ @read_only = options[:read_only]
97
+ else
98
+ @read_only = false
99
+ end
100
+
101
+ if (options.key? :properties_cache) then
102
+ @properties_cache = options[:properties_cache]
103
+ else
104
+ @properties_cache = LRUCache.new
105
+ end
106
+
107
+ @data_hash_type = options[:data_hash_type] || :MD5
108
+ unless (DATA_HASH.key? @data_hash_type) then
109
+ raise "unknown data hash type: #{@data_hash_type}"
110
+ end
111
+
112
+ if (options.key? :jlog_sync) then
113
+ @jlog_sync = options[:jlog_sync]
114
+ else
115
+ @jlog_sync = false
116
+ end
117
+
118
+ @jlog_hash_type = options[:jlog_hash_type] || :MD5
119
+ @jlog_rotate_size = options[:jlog_rotate_size] || 1024 * 256
120
+ @jlog_rotate_max = options[:jlog_rotate_max] || 1
121
+ @jlog_rotate_service_uri = options[:jlog_rotate_service_uri]
122
+
123
+ if (options.key? :logger) then
124
+ @Logger = options[:logger]
125
+ else
126
+ require 'logger'
127
+ @Logger = proc{|path|
128
+ logger = Logger.new(path, 1)
129
+ logger.level = Logger::WARN
130
+ logger
131
+ }
132
+ end
133
+ end
134
+ private :init_options
135
+
136
+ attr_reader :read_only
137
+ attr_reader :number_of_read_io
138
+ attr_reader :data_hash_type
139
+ attr_reader :jlog_sync
140
+ attr_reader :jlog_hash_type
141
+ attr_reader :jlog_rotate_size
142
+ attr_reader :jlog_rotate_max
143
+ attr_reader :jlog_rotate_service_uri
144
+ end
145
+ include InitOptions
146
+
147
+ # export Higgs::Storage methods from <tt>@storage</tt> instance variable.
148
+ #
149
+ # these methods are delegated.
150
+ # * Higgs::Storage#name
151
+ # * Higgs::Storage#read_only
152
+ # * Higgs::Storage#number_of_read_io
153
+ # * Higgs::Storage#data_hash_type
154
+ # * Higgs::Storage#jlog_sync
155
+ # * Higgs::Storage#jlog_hash_type
156
+ # * Higgs::Storage#jlog_rotate_size
157
+ # * Higgs::Storage#jlog_rotate_max
158
+ # * Higgs::Storage#jlog_rotate_service_uri
159
+ # * Higgs::Storage#shutdown
160
+ # * Higgs::Storage#shutdown?
161
+ # * Higgs::Storage#rotate_journal_log
162
+ # * Higgs::Storage#verify
163
+ #
164
+ module Export
165
+ extend Forwardable
166
+
167
+ def_delegator :@storage, :name
168
+ def_delegator :@storage, :read_only
169
+ def_delegator :@storage, :number_of_read_io
170
+ def_delegator :@storage, :data_hash_type
171
+ def_delegator :@storage, :jlog_sync
172
+ def_delegator :@storage, :jlog_hash_type
173
+ def_delegator :@storage, :jlog_rotate_size
174
+ def_delegator :@storage, :jlog_rotate_max
175
+ def_delegator :@storage, :jlog_rotate_service_uri
176
+ def_delegator :@storage, :shutdown
177
+ def_delegator :@storage, :shutdown?
178
+ def_delegator :@storage, :rotate_journal_log
179
+ def_delegator :@storage, :verify
180
+ end
181
+
182
+ def self.load_conf(path)
183
+ conf = YAML.load(IO.read(path))
184
+ options = {}
185
+ for name, value in conf
186
+ case (name)
187
+ when 'data_hash_type', 'jlog_hash_type'
188
+ value = value.to_sym
189
+ when 'properties_cache_limit_size', 'master_cache_limit_size'
190
+ name = name.sub(/_limit_size$/, '')
191
+ value = LRUCache.new(value)
192
+ when 'logging_level'
193
+ require 'logger'
194
+ name = 'logger'
195
+ level = case (value)
196
+ when 'debug', 'info', 'warn', 'error', 'fatal'
197
+ Logger.const_get(value.upcase)
198
+ else
199
+ raise "unknown logging level: #{value}"
200
+ end
201
+ value = proc{|path|
202
+ logger = Logger.new(path, 1)
203
+ logger.level = level
204
+ logger
205
+ }
206
+ end
207
+ options[name.to_sym] = value
208
+ end
209
+ options
210
+ end
211
+
212
+ # <tt>name</tt> is storage name.
213
+ # see Higgs::Storage::InitOptions for <tt>options</tt>.
214
+ #
215
+ # storage is composed of the following 5 files.
216
+ # <tt>name.log</tt>:: event log. default logging level is <tt>WARN</tt>.
217
+ # <tt>name.tar</tt>:: data file compatible with unix TAR format.
218
+ # <tt>name.idx</tt>:: index snapshot. genuine index is Hash in the memory.
219
+ # see Higgs::Index for detail.
220
+ # <tt>name.jlog</tt>:: transaction journal log. see Higgs::JournalLogger for detail.
221
+ # <tt>name.lock</tt>:: lock file for File#flock. see Higgs::FileLock for detail.
222
+ #
223
+ def initialize(name, options={})
224
+ @name = name
225
+ @log_name = "#{@name}.log"
226
+ @tar_name = "#{@name}.tar"
227
+ @idx_name = "#{@name}.idx"
228
+ @jlog_name = "#{@name}.jlog"
229
+ @lock_name = "#{@name}.lock"
230
+
231
+ @commit_lock = Mutex.new
232
+ @state_lock = Mutex.new
233
+ @broken = false
234
+ @shutdown = false
235
+
236
+ init_options(options)
237
+
238
+ init_completed = false
239
+ begin
240
+ @flock = FileLock.new(@lock_name, @read_only)
241
+ if (@read_only) then
242
+ @flock.read_lock
243
+ else
244
+ @flock.write_lock
245
+ end
246
+
247
+ @logger = @Logger.call(@log_name)
248
+ @logger.info("storage open start...")
249
+ if (@read_only) then
250
+ @logger.info("get file lock for read")
251
+ else
252
+ @logger.info("get file lock for write")
253
+ end
254
+
255
+ @logger.info format('block format version: 0x%04X', Block::FMT_VERSION)
256
+ @logger.info("journal log hash type: #{@jlog_hash_type}")
257
+ @logger.info("index format version: #{Index::MAJOR_VERSION}.#{Index::MINOR_VERSION}")
258
+ @logger.info("storage data hash type: #{@data_hash_type}")
259
+ @logger.info("storage properties cksum type: #{PROPERTIES_CKSUM_TYPE}")
260
+
261
+ @logger.info("properties cache type: #{@properties_cache.class}")
262
+ @properties_cache = SharedWorkCache.new(@properties_cache) {|key|
263
+ value = read_record_body(key, :p) and decode_properties(key, value)
264
+ }
265
+
266
+ unless (@read_only) then
267
+ begin
268
+ w_io = File.open(@tar_name, File::WRONLY | File::CREAT | File::EXCL, 0660)
269
+ @logger.info("create and get I/O handle for write: #{@tar_name}")
270
+ rescue Errno::EEXIST
271
+ @logger.info("open I/O handle for write: #{@tar_name}")
272
+ w_io = File.open(@tar_name, File::WRONLY, 0660)
273
+ end
274
+ w_io.binmode
275
+ @w_tar = Tar::ArchiveWriter.new(w_io)
276
+ end
277
+
278
+ @logger.info("build I/O handle pool for read.")
279
+ @r_tar_pool = Pool.new(@number_of_read_io) {
280
+ r_io = File.open(@tar_name, File::RDONLY)
281
+ r_io.binmode
282
+ @logger.info("open I/O handle for read: #{@tar_name}")
283
+ Tar::ArchiveReader.new(Tar::RawIO.new(r_io))
284
+ }
285
+
286
+ @index = Index.new
287
+ if (File.exist? @idx_name) then
288
+ @logger.info("load index: #{@idx_name}")
289
+ @index.load(@idx_name)
290
+ end
291
+ if (JournalLogger.need_for_recovery? @jlog_name) then
292
+ recover
293
+ end
294
+ unless (@read_only) then
295
+ @logger.info("journal log sync mode: #{@jlog_sync}")
296
+ @logger.info("open journal log for write: #{@jlog_name}")
297
+ @jlog = JournalLogger.open(@jlog_name, @jlog_sync, @jlog_hash_type)
298
+ end
299
+
300
+ if (@jlog_rotate_service_uri) then
301
+ @logger.info("start journal log rotation service: #{@jlog_rotate_service_uri}")
302
+ require 'drb'
303
+ @jlog_rotate_service = DRb::DRbServer.new(@jlog_rotate_service_uri,
304
+ method(:rotate_journal_log))
305
+ else
306
+ @jlog_rotate_service = nil
307
+ end
308
+
309
+ init_completed = true
310
+ ensure
311
+ if (init_completed) then
312
+ @logger.info("completed storage open.")
313
+ else
314
+ @broken = true
315
+
316
+ if ($! && @logger) then
317
+ begin
318
+ @logger.error($!)
319
+ rescue
320
+ # ignore error
321
+ end
322
+ end
323
+
324
+ if (@jlog_rotate_service) then
325
+ begin
326
+ @jlog_rotate_service.stop_service
327
+ rescue
328
+ # ignore error
329
+ end
330
+ end
331
+
332
+ unless (@read_only) then
333
+ if (@jlog) then
334
+ begin
335
+ @jlog.close(false)
336
+ rescue
337
+ # ignore error
338
+ end
339
+ end
340
+ end
341
+
342
+ if (@r_tar_pool) then
343
+ @r_tar_pool.shutdown{|r_tar|
344
+ begin
345
+ r_tar.close
346
+ rescue
347
+ # ignore errno
348
+ end
349
+ }
350
+ end
351
+
352
+ unless (@read_only) then
353
+ if (@w_tar) then
354
+ begin
355
+ @w_tar.close(false)
356
+ rescue
357
+ # ignore error
358
+ end
359
+ end
360
+ end
361
+
362
+ if (@logger) then
363
+ begin
364
+ @logger.fatal("abrot storage open.")
365
+ @logger.close
366
+ rescue
367
+ # ignore error
368
+ end
369
+ end
370
+
371
+ if (@flock) then
372
+ begin
373
+ @flock.close
374
+ rescue
375
+ # ignore error
376
+ end
377
+ end
378
+ end
379
+ end
380
+ end
381
+
382
+ def check_panic
383
+ @state_lock.synchronize{
384
+ if (@shutdown) then
385
+ raise ShutdownException, 'storage shutdown'
386
+ end
387
+ if (@broken) then
388
+ raise PanicError, 'broken storage'
389
+ end
390
+ }
391
+ end
392
+ private :check_panic
393
+
394
+ def recover
395
+ @logger.warn('incompleted storage and recover from journal log...')
396
+
397
+ check_panic
398
+ if (@read_only) then
399
+ @logger.warn('read only storage is not recoverable.')
400
+ raise NotWritableError, 'need for recovery'
401
+ end
402
+
403
+ recover_completed = false
404
+ begin
405
+ safe_pos = 0
406
+ @logger.info("open journal log for read: #{@jlog_name}")
407
+ File.open(@jlog_name, File::RDONLY) {|f|
408
+ f.binmode
409
+ begin
410
+ JournalLogger.scan_log(f) {|log|
411
+ change_number = log[0]
412
+ @logger.info("apply journal log: #{change_number}")
413
+ Storage.apply_journal(@w_tar, @index, log)
414
+ }
415
+ rescue Block::BrokenError
416
+ # nothing to do.
417
+ end
418
+ safe_pos = f.tell
419
+ }
420
+ @logger.info("last safe point of journal log: #{safe_pos}")
421
+
422
+ File.open(@jlog_name, File::WRONLY, 0660) {|f|
423
+ f.binmode
424
+ @logger.info("shrink journal log to erase last broken segment.")
425
+ f.truncate(safe_pos)
426
+ f.seek(safe_pos)
427
+ @logger.info("write eof mark to journal log.")
428
+ JournalLogger.eof_mark(f)
429
+ }
430
+ recover_completed = true
431
+ ensure
432
+ unless (recover_completed) then
433
+ @state_lock.synchronize{ @broken = true }
434
+ @logger.error("BROKEN: failed to recover.")
435
+ @logger.error($!) if $!
436
+ end
437
+ end
438
+
439
+ @logger.info('completed recovery from journal log.')
440
+ end
441
+ private :recover
442
+
443
+ attr_reader :name
444
+
445
+ def shutdown
446
+ @commit_lock.synchronize{
447
+ @state_lock.synchronize{
448
+ if (@shutdown) then
449
+ raise ShutdownException, 'storage shutdown'
450
+ end
451
+ @logger.info("shutdown start...")
452
+ @shutdown = true
453
+
454
+ if (@jlog_rotate_service) then
455
+ @logger.info("stop journal log rotation service: #{@jlog_rotate_service}")
456
+ @jlog_rotate_service.stop_service
457
+ end
458
+
459
+ unless (@read_only) then
460
+ if (@broken) then
461
+ @logger.warn("abort journal log: #{@jlog_name}")
462
+ @jlog.close(false)
463
+ else
464
+ @logger.info("close journal log: #{@jlog_name}")
465
+ @jlog.close
466
+ end
467
+ end
468
+
469
+ if (! @broken && ! @read_only) then
470
+ @logger.info("save index: #{@idx_name}")
471
+ @index.save(@idx_name)
472
+ end
473
+
474
+ @r_tar_pool.shutdown{|r_tar|
475
+ @logger.info("close I/O handle for read: #{@tar_name}")
476
+ r_tar.close
477
+ }
478
+ unless (@read_only) then
479
+ @logger.info("sync write data: #{@tar_name}")
480
+ @w_tar.fsync
481
+ @logger.info("close I/O handle for write: #{@tar_name}")
482
+ @w_tar.close(false)
483
+ end
484
+
485
+ @logger.info("unlock: #{@lock_name}")
486
+ @flock.close
487
+
488
+ @logger.info("completed shutdown.")
489
+ @logger.close
490
+ }
491
+ }
492
+ nil
493
+ end
494
+
495
+ def shutdown?
496
+ @state_lock.synchronize{ @shutdown }
497
+ end
498
+
499
+ def self.rotate_entries(name)
500
+ rotate_list = Dir["#{name}.*"].map{|nm|
501
+ n = Integer(nm[(name.length + 1)..-1])
502
+ [ nm, n ]
503
+ }.sort{|a, b|
504
+ a[1] <=> b[1]
505
+ }.map{|nm, n|
506
+ nm
507
+ }
508
+ rotate_list
509
+ end
510
+
511
+ def internal_rotate_journal_log(save_index)
512
+ @logger.info("start journal log rotation...")
513
+
514
+ commit_log = []
515
+ while (File.exist? "#{@jlog_name}.#{@index.change_number}")
516
+ @index.succ!
517
+ @logger.debug("index succ: #{@index.change_number}") if @logger.debug?
518
+ commit_log << { :ope => :succ, :cnum => @index.change_number }
519
+ end
520
+ unless (commit_log.empty?) then
521
+ @logger.debug("write journal log: #{@index.change_number}") if @logger.debug?
522
+ @jlog.write([ @index.change_number, commit_log ])
523
+ end
524
+ rot_jlog_name = "#{@jlog_name}.#{@index.change_number}"
525
+
526
+ if (save_index) then
527
+ case (save_index)
528
+ when String
529
+ @logger.info("save index: #{save_index}")
530
+ @index.save(save_index)
531
+ else
532
+ @logger.info("save index: #{@idx_name}")
533
+ @index.save(@idx_name)
534
+ end
535
+ else
536
+ @logger.info("no save index.")
537
+ end
538
+
539
+ @logger.info("close journal log.")
540
+ @jlog.close
541
+ @logger.info("rename journal log: #{@jlog_name} -> #{rot_jlog_name}")
542
+ File.rename(@jlog_name, rot_jlog_name)
543
+ if (@jlog_rotate_max > 0) then
544
+ rotate_list = Storage.rotate_entries(@jlog_name)
545
+ while (rotate_list.length > @jlog_rotate_max)
546
+ unlink_jlog_name = rotate_list.shift
547
+ @logger.info("unlink old journal log: #{unlink_jlog_name}")
548
+ File.unlink(unlink_jlog_name)
549
+ end
550
+ end
551
+ @logger.info("open journal log: #{@jlog_name}")
552
+ @jlog = JournalLogger.open(@jlog_name, @jlog_sync, @jlog_hash_type)
553
+
554
+ @logger.info("completed journal log rotation.")
555
+ end
556
+ private :internal_rotate_journal_log
557
+
558
+ def rotate_journal_log(save_index=true)
559
+ @commit_lock.synchronize{
560
+ check_panic
561
+ if (@read_only) then
562
+ raise NotWritableError, 'failed to write to read only storage'
563
+ end
564
+
565
+ rotate_completed = false
566
+ begin
567
+ internal_rotate_journal_log(save_index)
568
+ rotate_completed = true
569
+ ensure
570
+ unless (rotate_completed) then
571
+ @state_lock.synchronize{ @broken = true }
572
+ @logger.error("BROKEN: failed to rotate journal log.")
573
+ @logger.error($!) if $!
574
+ end
575
+ end
576
+ }
577
+
578
+ nil
579
+ end
580
+
581
+ def raw_write_and_commit(write_list, commit_time=Time.now)
582
+ @commit_lock.synchronize{
583
+ @logger.debug("start raw_write_and_commit.") if @logger.debug?
584
+
585
+ check_panic
586
+ if (@read_only) then
587
+ raise NotWritableError, 'failed to write to read only storage'
588
+ end
589
+
590
+ commit_log = []
591
+ commit_completed = false
592
+ eoa = @index.eoa
593
+
594
+ begin
595
+ for ope, key, type, name, value in write_list
596
+ case (ope)
597
+ when :write
598
+ @logger.debug("journal log for write: (key,type)=(#{key},#{type})") if @logger.debug?
599
+ unless (value.kind_of? String) then
600
+ raise TypeError, "can't convert #{value.class} (value) to String"
601
+ end
602
+ blocked_size = Tar::Block::BLKSIZ + Tar::Block.blocked_size(value.length)
603
+
604
+ # recycle
605
+ if (pos = @index.free_fetch(blocked_size)) then
606
+ @logger.debug("write type of recycle free region: (pos,size)=(#{pos},#{blocked_size})") if @logger.debug?
607
+ commit_log << {
608
+ :ope => :free_fetch,
609
+ :pos => pos,
610
+ :siz => blocked_size
611
+ }
612
+ commit_log << {
613
+ :ope => :write,
614
+ :key => key,
615
+ :pos => pos,
616
+ :typ => type,
617
+ :mod => commit_time,
618
+ :nam => name,
619
+ :val => value
620
+ }
621
+ if (i = @index[key]) then
622
+ if (j = i[type]) then
623
+ commit_log << {
624
+ :ope => :free_store,
625
+ :pos => j[:pos],
626
+ :siz => j[:siz],
627
+ :mod => commit_time
628
+ }
629
+ @index.free_store(j[:pos], j[:siz])
630
+ j[:pos] = pos
631
+ j[:siz] = blocked_size
632
+ else
633
+ i[type] = { :pos => pos, :siz => blocked_size }
634
+ end
635
+ else
636
+ @index[key] = { type => { :pos => pos, :siz => blocked_size } }
637
+ end
638
+ next
639
+ end
640
+
641
+ # overwrite
642
+ if (i = @index[key]) then
643
+ if (j = i[type]) then
644
+ if (j[:siz] >= blocked_size) then
645
+ @logger.debug("write type of overwrite: (pos,size)=(#{j[:pos]},#{blocked_size})") if @logger.debug?
646
+ commit_log << {
647
+ :ope => :write,
648
+ :key => key,
649
+ :pos => j[:pos],
650
+ :typ => type,
651
+ :mod => commit_time,
652
+ :nam => name,
653
+ :val => value
654
+ }
655
+ if (j[:siz] > blocked_size) then
656
+ commit_log << {
657
+ :ope => :free_store,
658
+ :pos => j[:pos] + blocked_size,
659
+ :siz => j[:siz] - blocked_size,
660
+ :mod => commit_time
661
+ }
662
+ @logger.debug("tail free region: (pos,size)=(#{commit_log[-1][:pos]},#{commit_log[-1][:siz]})") if @logger.debug?
663
+ @index.free_store(commit_log.last[:pos], commit_log.last[:siz])
664
+ j[:siz] = blocked_size
665
+ end
666
+ next
667
+ end
668
+ end
669
+ end
670
+
671
+ # append
672
+ @logger.debug("write type of append: (pos,size)=(#{eoa},#{blocked_size})")
673
+ commit_log << {
674
+ :ope => :write,
675
+ :key => key,
676
+ :pos => eoa,
677
+ :typ => type,
678
+ :mod => commit_time,
679
+ :nam => name,
680
+ :val => value
681
+ }
682
+ if (i = @index[key]) then
683
+ if (j = i[type]) then
684
+ commit_log << {
685
+ :ope => :free_store,
686
+ :pos => j[:pos],
687
+ :siz => j[:siz],
688
+ :mod => commit_time
689
+ }
690
+ @index.free_store(j[:pos], j[:siz])
691
+ j[:pos] = eoa
692
+ j[:siz] = blocked_size
693
+ else
694
+ i[type] = { :pos => eoa, :siz => blocked_size }
695
+ end
696
+ else
697
+ @index[key] = { type => { :pos => eoa, :siz => blocked_size } }
698
+ end
699
+ eoa += blocked_size
700
+ commit_log << {
701
+ :ope => :eoa,
702
+ :pos => eoa
703
+ }
704
+ when :delete
705
+ @logger.debug("journal log for delete: #{key}") if @logger.debug?
706
+ if (i = @index.delete(key)) then
707
+ commit_log << {
708
+ :ope => :delete,
709
+ :key => key
710
+ }
711
+ i.each_value{|j|
712
+ commit_log << {
713
+ :ope => :free_store,
714
+ :pos => j[:pos],
715
+ :siz => j[:siz],
716
+ :mod => commit_time
717
+ }
718
+ @index.free_store(j[:pos], j[:siz])
719
+ }
720
+ end
721
+ else
722
+ raise ArgumentError, "unknown operation: #{cmd[:ope]}"
723
+ end
724
+ end
725
+
726
+ @index.succ!
727
+ @logger.debug("index succ: #{@index.change_number}") if @logger.debug?
728
+ commit_log << { :ope => :succ, :cnum => @index.change_number }
729
+
730
+ @logger.debug("write journal log: #{@index.change_number}") if @logger.debug?
731
+ @jlog.write([ @index.change_number, commit_log ])
732
+
733
+ for cmd in commit_log
734
+ case (cmd[:ope])
735
+ when :write
736
+ name = cmd[:nam][0, Tar::Block::MAX_LEN]
737
+ @logger.debug("write data to storage: (name,pos,size)=(#{name},#{cmd[:pos]},#{cmd[:val].size})") if @logger.debug?
738
+ @w_tar.seek(cmd[:pos])
739
+ @w_tar.add(name, cmd[:val], :mtime => cmd[:mod])
740
+ when :free_store
741
+ @logger.debug("write free region to storage: (pos,size)=(#{cmd[:pos]},#{cmd[:siz]})") if @logger.debug?
742
+ name = format('.free.%x', cmd[:pos] >> 9)
743
+ @w_tar.seek(cmd[:pos])
744
+ @w_tar.write_header(:name => name, :size => cmd[:siz] - Tar::Block::BLKSIZ, :mtime => cmd[:mod])
745
+ when :delete, :eoa, :free_fetch, :succ
746
+ # nothing to do.
747
+ else
748
+ raise "unknown operation: #{cmd[:ope]}"
749
+ end
750
+ end
751
+ if (@index.eoa != eoa) then
752
+ @logger.debug("write EOA to storage: #{eoa}")
753
+ @index.eoa = eoa
754
+ @w_tar.seek(eoa)
755
+ @w_tar.write_EOA
756
+ end
757
+ @logger.debug("flush storage.")
758
+ @w_tar.flush
759
+
760
+ if (@jlog_rotate_size > 0 && @jlog.size >= @jlog_rotate_size) then
761
+ internal_rotate_journal_log(true)
762
+ end
763
+
764
+ commit_completed = true
765
+ ensure
766
+ unless (commit_completed) then
767
+ @state_lock.synchronize{ @broken = true }
768
+ @logger.error("BROKEN: failed to commit.")
769
+ @logger.error($!) if $!
770
+ end
771
+ end
772
+
773
+ @logger.debug("completed raw_write_and_commit.") if @logger.debug?
774
+ }
775
+
776
+ nil
777
+ end
778
+
779
+ def self.apply_journal(w_tar, index, log)
780
+ change_number, commit_log = log
781
+ if (change_number - 1 < index.change_number) then
782
+ # skip old jounal log
783
+ elsif (change_number - 1 > index.change_number) then
784
+ raise PanicError, "lost journal log (cnum: #{index.change_number + 1})"
785
+ else # if (change_number - 1 == index.change_number) then
786
+ for cmd in commit_log
787
+ case (cmd[:ope])
788
+ when :write
789
+ name = "#{cmd[:key]}.#{cmd[:typ]}"[0, Tar::Block::MAX_LEN]
790
+ w_tar.seek(cmd[:pos])
791
+ w_tar.add(cmd[:nam], cmd[:val], :mtime => cmd[:mod])
792
+ blocked_size = Tar::Block::BLKSIZ + Tar::Block.blocked_size(cmd[:val].length)
793
+ if (i = index[cmd[:key]]) then
794
+ if (j = i[cmd[:typ]]) then
795
+ j[:pos] = cmd[:pos]
796
+ j[:siz] = blocked_size
797
+ else
798
+ i[cmd[:typ]] = { :pos => cmd[:pos], :siz => blocked_size }
799
+ end
800
+ else
801
+ index[cmd[:key]] = { cmd[:typ] => { :pos => cmd[:pos], :siz => blocked_size } }
802
+ end
803
+ when :delete
804
+ index.delete(cmd[:key])
805
+ when :free_fetch
806
+ index.free_fetch_at(cmd[:pos], cmd[:siz])
807
+ when :free_store
808
+ index.free_store(cmd[:pos], cmd[:siz])
809
+ name = format('.free.%x', cmd[:pos] >> 9)
810
+ w_tar.seek(cmd[:pos])
811
+ w_tar.write_header(:name => name, :size => cmd[:siz] - Tar::Block::BLKSIZ, :mtime => cmd[:mod])
812
+ when :eoa
813
+ index.eoa = cmd[:pos]
814
+ when :succ
815
+ index.succ!
816
+ if (index.change_number != cmd[:cnum]) then
817
+ raise PanicError, 'lost journal log'
818
+ end
819
+ else
820
+ raise "unknown operation from #{curr_jlog_name}: #{cmd[:ope]}"
821
+ end
822
+ end
823
+ end
824
+ nil
825
+ end
826
+
827
+ def self.recover(name, out=nil, verbose_level=1)
828
+ tar_name = "#{name}.tar"
829
+ idx_name = "#{name}.idx"
830
+ jlog_name = "#{name}.jlog"
831
+ lock_name = "#{name}.lock"
832
+
833
+ flock = FileLock.new(lock_name)
834
+ flock.synchronize{
835
+ begin
836
+ w_io = File.open(tar_name, File::WRONLY | File::CREAT | File::EXCL, 0660)
837
+ rescue Errno::EEXIST
838
+ w_io = File.open(tar_name, File::WRONLY, 0660)
839
+ end
840
+ w_io.binmode
841
+ w_tar = Tar::ArchiveWriter.new(w_io)
842
+
843
+ index = Index.new
844
+ index.load(idx_name) if (File.exist? idx_name)
845
+
846
+
847
+ out << "recovery target: #{name}\n" if (out && verbose_level >= 1)
848
+ for curr_name in rotate_entries(jlog_name)
849
+ JournalLogger.each_log(curr_name) do |log|
850
+ change_number = log[0]
851
+ out << "apply journal log: #{change_number}\n" if (out && verbose_level >= 1)
852
+ apply_journal(w_tar, index, log)
853
+ end
854
+ end
855
+ w_tar.seek(index.eoa)
856
+ w_tar.write_EOA
857
+
858
+ index.save(idx_name)
859
+ w_tar.fsync
860
+ w_tar.close(false)
861
+ }
862
+ flock.close
863
+
864
+ nil
865
+ end
866
+
867
+ def write_and_commit(write_list, commit_time=Time.now)
868
+ check_panic
869
+ if (@read_only) then
870
+ raise NotWritableError, 'failed to write to read only storage'
871
+ end
872
+
873
+ raw_write_list = []
874
+ deleted_entries = {}
875
+ update_properties = {}
876
+
877
+ for ope, key, value in write_list
878
+ case (ope)
879
+ when :write
880
+ unless (value.kind_of? String) then
881
+ raise TypeError, "can't convert #{value.class} (value) to String"
882
+ end
883
+ raw_write_list << [ :write, key, :d, key.to_s, value ]
884
+ deleted_entries[key] = false
885
+ if (properties = update_properties[key]) then
886
+ # nothing to do.
887
+ elsif (properties = internal_fetch_properties(key)) then
888
+ update_properties[key] = properties
889
+ else
890
+ # new properties
891
+ properties = {
892
+ 'system_properties' => {
893
+ 'hash_type' => @data_hash_type.to_s,
894
+ 'hash_value' => nil,
895
+ 'created_time' => commit_time,
896
+ 'changed_time' => commit_time,
897
+ 'modified_time' => nil,
898
+ 'string_only' => false
899
+ },
900
+ 'custom_properties' => {}
901
+ }
902
+ update_properties[key] = properties
903
+ end
904
+ properties['system_properties']['hash_value'] = DATA_HASH[@data_hash_type].call(value)
905
+ properties['system_properties']['modified_time'] = commit_time
906
+ @properties_cache.delete(key)
907
+ when :delete
908
+ raw_write_list << [ :delete, key ]
909
+ deleted_entries[key] = true
910
+ update_properties.delete(key)
911
+ @properties_cache.delete(key)
912
+ when :custom_properties, :system_properties
913
+ if (deleted_entries[key]) then
914
+ raise IndexError, "not exist properties at key: #{key}"
915
+ end
916
+ if (properties = update_properties[key]) then
917
+ # nothing to do.
918
+ elsif (properties = internal_fetch_properties(key)) then
919
+ update_properties[key] = properties
920
+ else
921
+ raise IndexError, "not exist properties at key: #{key}"
922
+ end
923
+ properties['system_properties']['changed_time'] = commit_time
924
+ case (ope)
925
+ when :custom_properties
926
+ properties['custom_properties'] = value
927
+ when :system_properties
928
+ if (value.key? 'string_only') then
929
+ properties['system_properties']['string_only'] = value['string_only'] ? true : false
930
+ end
931
+ else
932
+ raise ArgumentError, "unknown operation: #{ope}"
933
+ end
934
+ @properties_cache.delete(key)
935
+ else
936
+ raise ArgumentError, "unknown operation: #{ope}"
937
+ end
938
+ end
939
+
940
+ for key, properties in update_properties
941
+ raw_write_list << [ :write, key, :p, "#{key}.p", encode_properties(properties) ]
942
+ end
943
+
944
+ raw_write_and_commit(raw_write_list, commit_time)
945
+
946
+ nil
947
+ end
948
+
949
+ def read_record(key, type)
950
+ head_and_body = nil
951
+ if (i = @index[key]) then
952
+ if (j = i[type]) then
953
+ @r_tar_pool.transaction{|r_tar|
954
+ r_tar.seek(j[:pos])
955
+ head_and_body = r_tar.fetch
956
+ }
957
+ unless (head_and_body) then
958
+ @state_lock.synchronize{ @broken = true }
959
+ @logger.error("BROKEN: failed to read record: #{key}")
960
+ raise PanicError, "failed to read record: #{key}"
961
+ end
962
+ end
963
+ end
964
+ head_and_body
965
+ end
966
+ private :read_record
967
+
968
+ def read_record_body(key, type)
969
+ head_and_body = read_record(key, type) or return
970
+ head_and_body[:body]
971
+ end
972
+ private :read_record_body
973
+
974
+ def encode_properties(properties)
975
+ body = properties.to_yaml
976
+ head = "\# #{PROPERTIES_CKSUM_TYPE} #{body.sum(PROPERTIES_CKSUM_BITS)}\n"
977
+ head + body
978
+ end
979
+ private :encode_properties
980
+
981
+ def decode_properties(key, value)
982
+ head, body = value.split(/\n/, 2)
983
+ cksum_type, cksum_value = head.sub(/^#\s+/, '').split(/\s+/, 2)
984
+ if (cksum_type != PROPERTIES_CKSUM_TYPE) then
985
+ @state_lock.synchronize{ @broken = true }
986
+ @logger.error("BROKEN: unknown properties cksum type: #{cksum_type}")
987
+ raise PanicError, "unknown properties cksum type: #{cksum_type}"
988
+ end
989
+ if (body.sum(PROPERTIES_CKSUM_BITS) != Integer(cksum_value)) then
990
+ @state_lock.synchronize{ @broken = true }
991
+ @logger.error("BROKEN: mismatch properties cksum at #{key}")
992
+ raise PanicError, "mismatch properties cksum at #{key}"
993
+ end
994
+ YAML.load(body)
995
+ end
996
+ private :decode_properties
997
+
998
+ def internal_fetch_properties(key)
999
+ @properties_cache[key] # see initialize
1000
+ end
1001
+ private :internal_fetch_properties
1002
+
1003
+ def fetch_properties(key)
1004
+ check_panic
1005
+ internal_fetch_properties(key)
1006
+ end
1007
+
1008
+ def fetch(key)
1009
+ check_panic
1010
+ value = read_record_body(key, :d) or return
1011
+ unless (properties = internal_fetch_properties(key)) then
1012
+ @state_lock.synchronize{ @broken = true }
1013
+ @logger.error("BROKEN: failed to read properties: #{key}")
1014
+ raise PanicError, "failed to read properties: #{key}"
1015
+ end
1016
+ hash_type = properties['system_properties']['hash_type']
1017
+ unless (cksum_proc = DATA_HASH_BIN[hash_type]) then
1018
+ @state_lock.synchronize{ @broken = true }
1019
+ @logger.error("BROKEN: unknown data hash type: #{hash_type}")
1020
+ raise PanicError, "unknown data hash type: #{hash_type}"
1021
+ end
1022
+ hash_value = cksum_proc.call(value)
1023
+ if (hash_value != properties['system_properties']['hash_value']) then
1024
+ @state_lock.synchronize{ @broken = true }
1025
+ @logger.error("BROKEN: mismatch hash value at #{key}")
1026
+ raise PanicError, "mismatch hash value at #{key}"
1027
+ end
1028
+ value
1029
+ end
1030
+
1031
+ def string_only(key)
1032
+ properties = fetch_properties(key) or raise IndexError, "not exist properties at key: #{key}"
1033
+ properties['system_properties']['string_only']
1034
+ end
1035
+
1036
+ def_delegator :@index, :identity
1037
+
1038
+ def key?(key)
1039
+ check_panic
1040
+ @index.key? key
1041
+ end
1042
+
1043
+ def each_key
1044
+ check_panic
1045
+ @index.each_key do |key|
1046
+ yield(key)
1047
+ end
1048
+ self
1049
+ end
1050
+
1051
+ VERIFY_VERBOSE_LIST = [
1052
+ [ 'hash_type', proc{|type| type } ],
1053
+ [ 'hash_value', proc{|value| value } ],
1054
+ [ 'created_time', proc{|t| t.strftime('%Y-%m-%d %H:%M:%S.') + format('%03d', Integer(t.to_f % 1000)) } ],
1055
+ [ 'changed_time', proc{|t| t.strftime('%Y-%m-%d %H:%M:%S.') + format('%03d', Integer(t.to_f % 1000)) } ],
1056
+ [ 'modified_time', proc{|t| t.strftime('%Y-%m-%d %H:%M:%S.') + format('%03d', Integer(t.to_f % 1000)) } ],
1057
+ [ 'string_only', proc{|flag| flag.to_s } ]
1058
+ ]
1059
+
1060
+ def verify(out=nil, verbose_level=1)
1061
+ check_panic
1062
+ @index.each_key do |key|
1063
+ if (out && verbose_level >= 1) then
1064
+ out << "check #{key}\n"
1065
+ end
1066
+
1067
+ data = fetch(key)
1068
+
1069
+ if (out && verbose_level >= 2) then
1070
+ out << " #{data.length} bytes\n"
1071
+ properties = fetch_properties(key) or raise PanicError, "not exist properties at key: #{key}"
1072
+ for key, format in VERIFY_VERBOSE_LIST
1073
+ value = properties['system_properties'][key]
1074
+ out << ' ' << key << ': ' << format.call(value) << "\n"
1075
+ end
1076
+ end
1077
+ end
1078
+ nil
1079
+ end
1080
+ end
1081
+ end
1082
+
1083
+ # Local Variables:
1084
+ # mode: Ruby
1085
+ # indent-tabs-mode: nil
1086
+ # End: