higgs 0.1.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.
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: