sldb 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1302 @@
1
+ module SLDB
2
+ #--{{{
3
+ require 'logger'
4
+ require 'socket'
5
+ require 'sync'
6
+
7
+ require 'sqlite'
8
+
9
+ require 'posixlock'
10
+ require 'arrayfields'
11
+ require 'lockfile'
12
+ require 'traits'
13
+
14
+ VERSION = '0.1.0'
15
+
16
+ module Util
17
+ #--{{{
18
+ class << self
19
+ def export(*syms)
20
+ #--{{{
21
+ syms.each do |sym|
22
+ sym = "#{ sym }".intern
23
+ module_function sym
24
+ public sym
25
+ end
26
+ #--}}}
27
+ end
28
+ def append_features c
29
+ #--{{{
30
+ super
31
+ c.extend self
32
+ #--}}}
33
+ end
34
+ end
35
+
36
+ def fork(*args, &block)
37
+ #--{{{
38
+ begin
39
+ verbose = $VERBOSE
40
+ $VERBOSE = nil
41
+ ::Process::fork(*args, &block)
42
+ ensure
43
+ $VERBOSE = verbose
44
+ end
45
+ #--}}}
46
+ end
47
+ export 'fork'
48
+
49
+ def getopt opt, hash, default = nil
50
+ #--{{{
51
+ key = opt
52
+ return hash[key] if hash.has_key? key
53
+ key = "#{ key }"
54
+ return hash[key] if hash.has_key? key
55
+ key = key.intern
56
+ return hash[key] if hash.has_key? key
57
+ return default
58
+ #--}}}
59
+ end
60
+ export 'getopt'
61
+
62
+ def optfilter(*list)
63
+ #--{{{
64
+ args, opts = [ list ].flatten.partition{|item| not Hash === item}
65
+ [args, Util::hashify(*opts)]
66
+ #--}}}
67
+ end
68
+ export 'optfilter'
69
+
70
+ def hashify(*hashes)
71
+ #--{{{
72
+ hashes.inject(accum={}){|accum,hash| accum.update hash}
73
+ #--}}}
74
+ end
75
+ export 'hashify'
76
+
77
+ def hostname
78
+ #--{{{
79
+ @__hostname__ ||= Socket::gethostname
80
+ #--}}}
81
+ end
82
+ export 'hostname'
83
+
84
+ def host
85
+ #--{{{
86
+ @__host__ ||= Util::hostname.gsub(%r/\..*$/o,'')
87
+ #--}}}
88
+ end
89
+ export 'host'
90
+
91
+ def uncache file
92
+ #--{{{
93
+ refresh = nil
94
+ begin
95
+ is_a_file = File === file
96
+ path = (is_a_file ? file.path : file.to_s)
97
+ stat = (is_a_file ? file.stat : File::stat(file.to_s))
98
+ refresh = Util::tmpnam(File::dirname(path))
99
+ File::link path, refresh rescue File::symlink path, refresh
100
+ File::chmod stat.mode, path
101
+ File::utime stat.atime, stat.mtime, path
102
+ open(File::dirname(path)){|d| d.fsync rescue nil}
103
+ ensure
104
+ begin
105
+ File::unlink refresh if refresh
106
+ rescue Errno::ENOENT
107
+ end
108
+ end
109
+ #--}}}
110
+ end
111
+ export 'uncache'
112
+
113
+ def tmpnam opts = {}
114
+ #--{{{
115
+ dir = Util::getopt 'dir', opts, Dir::tmpdir
116
+ seed = Util::getopt 'seed', opts, Util::prognam
117
+ path =
118
+ "%s_%s_%s_%s_%d" % [
119
+ Util::hostname,
120
+ seed,
121
+ Process::pid,
122
+ Util::timestamp('nospace' => true),
123
+ rand(101010)
124
+ ]
125
+ dirname, basename = File::dirname(path), File::basename(path)
126
+ tn = File::join(dir, dirname, basename.gsub(%r/[^0-9a-zA-Z]/,'_')).gsub(%r/\s+/, '_')
127
+ File::expand_path tn
128
+ #--}}}
129
+ end
130
+ export 'tmpnam'
131
+
132
+ def timestamp opts = {}
133
+ #--{{{
134
+ time = Util::getopt 'time', opts, Time::now
135
+ local = Util::getopt 'local', opts, false
136
+ nospace = Util::getopt 'nospace', opts, false
137
+ time = time.utc unless local
138
+ usec = "#{ time.usec }"
139
+ usec << ('0' * (6 - usec.size)) if usec.size < 6
140
+ stamp = time.strftime('%Y-%m-%d %H:%M:%S.') << usec
141
+ stamp.gsub!(%r/\s+/,'_') if nospace
142
+ stamp
143
+ #--}}}
144
+ end
145
+ export 'timestamp'
146
+
147
+ def stamptime string, opts = {}
148
+ #--{{{
149
+ local = Util::getopt 'local', opts, false
150
+ string = "#{ string }"
151
+ pat = %r/^\s*(\d\d\d\d)-(\d\d)-(\d\d)[\s_]+(\d\d):(\d\d):(\d\d)(?:.(\d+))?\s*$/o
152
+ match = pat.match string
153
+ raise ArgumentError, "<#{ string.inspect }>" unless match
154
+ yyyy,mm,dd,h,m,s,u = match.to_a[1..-1].map{|m| (m || 0).to_i}
155
+ if local
156
+ Time::local yyyy,mm,dd,h,m,s,u
157
+ else
158
+ Time::gm yyyy,mm,dd,h,m,s,u
159
+ end
160
+ #--}}}
161
+ end
162
+ export 'stamptime'
163
+
164
+ def system(*args, &block)
165
+ #--{{{
166
+ begin
167
+ verbose = $VERBOSE
168
+ $VERBOSE = nil
169
+ ::Kernel::system(*args, &block)
170
+ ensure
171
+ $VERBOSE = verbose
172
+ end
173
+ #--}}}
174
+ end
175
+ export 'system'
176
+
177
+ def env_intlist arg, opts = {}
178
+ #--{{{
179
+ default = Util::getopt 'default', opts
180
+ return default unless arg
181
+ list =
182
+ case arg
183
+ when Array
184
+ arg
185
+ else
186
+ "#{ arg }".split %r/,+/
187
+ end
188
+ list.map{|item| env_int item}
189
+ #--}}}
190
+ end
191
+ export 'env_intlist'
192
+
193
+ def env_floatlist arg, opts = {}
194
+ #--{{{
195
+ default = Util::getopt 'default', opts
196
+ return default unless arg
197
+ list =
198
+ case arg
199
+ when Array
200
+ arg
201
+ else
202
+ "#{ arg }".split %r/,+/
203
+ end
204
+ list.map{|item| env_float item}
205
+ #--}}}
206
+ end
207
+ export 'env_floatlist'
208
+
209
+ def env_boolean arg, opts = {}
210
+ #--{{{
211
+ default = Util::getopt 'default', opts
212
+ return default unless arg
213
+ case arg
214
+ when String
215
+ arg =~ %r/true/io ? true : false
216
+ when Symbol
217
+ arg =~ :true ? true : false
218
+ when Numeric
219
+ arg == 1 ? true : false
220
+ else
221
+ arg ? true : false
222
+ end
223
+ #--}}}
224
+ end
225
+ export 'env_boolean'
226
+
227
+ def env_int arg, opts = {}
228
+ #--{{{
229
+ default = Util::getopt 'default', opts
230
+ return default unless arg
231
+ Integer "#{ arg }"
232
+ #--}}}
233
+ end
234
+ export 'env_int'
235
+
236
+ def env_float arg, opts = {}
237
+ #--{{{
238
+ default = Util::getopt 'default', opts
239
+ return default unless arg
240
+ Float "#{ arg }"
241
+ #--}}}
242
+ end
243
+ export 'env_float'
244
+
245
+ def erreq a, b
246
+ #--{{{
247
+ a.class == b.class and
248
+ a.message == b.message and
249
+ a.backtrace == b.backtrace
250
+ #--}}}
251
+ end
252
+ export 'erreq'
253
+
254
+ def escape! s, char, esc
255
+ #--{{{
256
+ #re = %r/([#{ 0x5c.chr << esc }]*)#{ char }/
257
+ re = %r/([#{ Regexp::quote esc }]*)#{ Regexp::quote char }/
258
+ s.gsub!(re) do
259
+ (($1.size % 2 == 0) ? ($1 << esc) : $1) + char
260
+ end
261
+ #--}}}
262
+ end
263
+ export 'escape!'
264
+
265
+ def escape s, char, esc
266
+ #--{{{
267
+ s = "#{ s }"
268
+ escape! s, char, esc
269
+ s
270
+ #--}}}
271
+ end
272
+ export 'escape'
273
+
274
+ def quote list, qm = "'"
275
+ #--{{{
276
+ [ list ].flatten.map do |f|
277
+ if f
278
+ qm + Util::escape(f,qm,qm) + qm
279
+ else
280
+ 'NULL'
281
+ end
282
+ end
283
+ #--}}}
284
+ end
285
+ export 'quote'
286
+ alias q quote
287
+ export 'q'
288
+ #--}}}
289
+ end # module Util
290
+
291
+ class Logger < ::Logger
292
+ #--{{{
293
+ def initialize(*a, &b)
294
+ #--{{{
295
+ ret = super
296
+ self.level = SLDB::default_logger_level
297
+ ret
298
+ #--}}}
299
+ end
300
+ #--}}}
301
+ end # class Logger
302
+
303
+ class ProcessRefresher
304
+ #--{{{
305
+ SIGNALS = %w( SIGTERM SIGINT SIGKILL )
306
+ attr :path
307
+ attr :pid
308
+ attr :refresh_rate
309
+ def initialize path, refresh_rate = 8
310
+ #--{{{
311
+ @path = path
312
+ File::stat path
313
+ @refresh_rate = Float refresh_rate
314
+ @pipe = IO::pipe
315
+ if((@pid = Util::fork))
316
+ @pipe.last.close
317
+ @pipe = @pipe.first
318
+ @thread = Thread::new{loop{@pipe.gets}}
319
+ Process::detach @pid
320
+ else
321
+ begin
322
+ SIGNALS.each{|sig| trap(sig){ exit! }}
323
+ pid = Process::pid
324
+ ppid = Process::ppid
325
+ $0 = "#{ path }.refresher.#{ pid }"
326
+ @pipe.first.close
327
+ @pipe = @pipe.last
328
+ loop do
329
+ FileUtils::touch @path
330
+ sleep @refresh_rate
331
+ Process::kill 0, ppid
332
+ @pipe.puts pid
333
+ end
334
+ rescue Exception => e
335
+ exit!
336
+ end
337
+ end
338
+ #--}}}
339
+ end
340
+ def kill
341
+ #--{{{
342
+ #Thread::new(Thread::current, @thread, @pipe) do |c, t, p|
343
+ dead = false
344
+ begin
345
+ t.kill rescue nil
346
+ p.close rescue nil
347
+ SIGNALS.each do |sig|
348
+ begin
349
+ Process::kill sig, @pid
350
+ sleep 0.01
351
+ Process::kill sig, @pid
352
+ rescue Errno::ESRCH => e
353
+ dead = true
354
+ break
355
+ rescue => e
356
+ break
357
+ end
358
+ end
359
+ ensure
360
+ unless dead
361
+ n = 42
362
+ begin
363
+ n.times do |i|
364
+ sleep 0.2
365
+ Process::kill 0, @pid
366
+ sleep 1
367
+ end
368
+ rescue Errno::ESRCH
369
+ dead = true
370
+ end
371
+ current.raise "runaway refresher <#{ @pid }> must be killed!" unless dead
372
+ end
373
+ end
374
+ #end
375
+ #--}}}
376
+ end
377
+ #--}}}
378
+ end # class ProcessRefresher
379
+
380
+ class ThreadedRefresher
381
+ #--{{{
382
+ attr :path
383
+ attr :refresh_rate
384
+ def initialize path, refresh_rate = 8
385
+ #--{{{
386
+ @path = path
387
+ @refresh_rate = Float refresh_rate
388
+ @thread =
389
+ Thread::new(@path, @refresh_rate, Thread::current) do |path, refresh_rate, current|
390
+ begin
391
+ loop do
392
+ FileUtils::touch path
393
+ sleep refresh_rate
394
+ end
395
+ rescue Exception => e
396
+ # current.raise e
397
+ exit!
398
+ end
399
+ end
400
+ #--}}}
401
+ end
402
+ def kill
403
+ #--{{{
404
+ @thread.kill
405
+ #--}}}
406
+ end
407
+ #--}}}
408
+ end # class ThreadedRefresher
409
+
410
+ class SleepCycle
411
+ #--{{{
412
+ attr :min
413
+ attr :max
414
+ attr :range
415
+ attr :inc
416
+ def initialize(*args)
417
+ #--{{{
418
+ min, max, inc = args.flatten
419
+ raise ArgumentError, 'no min' unless min
420
+ raise ArgumentError, 'no max' unless max
421
+ raise ArgumentError, 'no inc' unless inc
422
+ @min, @max, @inc = Float(min), Float(max), Float(inc)
423
+ @range = @max - @min
424
+ raise RangeError, "max < min" if @max < @min
425
+ raise RangeError, "inc > range" if @inc > @range
426
+ @list = []
427
+ s = @min
428
+ @list.push(s) and s += @inc while(s <= @max)
429
+ @list[-1] = @max if @list[-1] < @max
430
+ reset
431
+ #--}}}
432
+ end
433
+ def next
434
+ #--{{{
435
+ ret = @list[@idx]
436
+ @idx = ((@idx + 1) % @list.size)
437
+ ret
438
+ #--}}}
439
+ end
440
+ def reset
441
+ #--{{{
442
+ @idx = 0
443
+ #--}}}
444
+ end
445
+ #--}}}
446
+ end # class SleepCycle
447
+
448
+ #
449
+ # module defaults - configurable via ENV
450
+ #
451
+ class << self
452
+ #--{{{
453
+ trait :default_logger_level =>
454
+ (ENV['SLDB_DEBUG'] ? ::Logger::DEBUG : ::Logger::INFO)
455
+
456
+ trait :default_logger =>
457
+ Logger::new(ENV['SLDB_LOG'] || STDERR)
458
+
459
+ trait :default_sql_debug =>
460
+ Util::env_boolean(ENV['SLDB_SQL_DEBUG'], 'default' => false)
461
+
462
+ trait :default_busy_sc =>
463
+ SleepCycle::new(Util::env_floatlist(ENV['SLDB_BUSY_SC'], 'default' => [3, 16, 2]))
464
+
465
+ trait :default_transaction_retries =>
466
+ Util::env_int(ENV['SLDB_TRANSACTION_RETRIES'], 'default' => 4)
467
+
468
+ trait :default_aquire_lock_sc =>
469
+ SleepCycle::new(Util::env_floatlist(ENV['SLDB_AQUIRE_LOCK_SC'], 'default' => [3, 16, 2]))
470
+
471
+ trait :default_transaction_retries_sc =>
472
+ SleepCycle::new(Util::env_floatlist(ENV['SLDB_TRANSACTION_RETRIES_SC'], 'default' => [8, 24, 8]))
473
+
474
+ trait :default_attempt_lockd_recovery =>
475
+ Util::env_boolean(ENV['SLDB_TTEMPT_LOCKD_RECOVERY'], 'default' => true)
476
+
477
+ trait :default_lockd_recover_wait =>
478
+ Util::env_int(ENV['SLDB_LOCKD_RECOVER_WAIT'], 'default' => 1800)
479
+
480
+ trait :default_aquire_lock_lockfile_stale_age =>
481
+ Util::env_int(ENV['SLDB_AQUIRE_LOCK_LOCKFILE_STALE_AGE'], 'default' => 1800)
482
+
483
+ trait :default_refresher =>
484
+ case ENV['SLDB_REFRESHER']
485
+ when %r/thread/i
486
+ ThreadedRefresher
487
+ when %r/process/i
488
+ ProcessRefresher
489
+ else
490
+ ThreadedRefresher
491
+ end
492
+
493
+ trait :default_aquire_lock_refresh_rate =>
494
+ Util::env_int(ENV['SLDB_AQUIRE_LOCK_REFRESH_RATE'], 'default' => 8)
495
+
496
+ trait :default_administrator =>
497
+ ENV['SLDB_ADMINISTRATOR'] || ENV['USER'] || ENV['LOGNAME'] || 'ara.t.howard@noaa.gov'
498
+ #--}}}
499
+ end
500
+
501
+ class AbstractSLDB
502
+ #--{{{
503
+ include Util
504
+ #
505
+ # abstract class traits
506
+ #
507
+ class_trait :fields
508
+ class_trait :pragmas
509
+ class_trait :schema
510
+ class_trait :indexes
511
+ class_trait :path
512
+ #
513
+ # concrete class traits and defaults
514
+ #
515
+ class_trait :logger => SLDB::default_logger
516
+ class_trait :sql_debug => SLDB::default_sql_debug
517
+ class_trait :busy_sc => SLDB::default_busy_sc
518
+ class_trait :transaction_retries => SLDB::default_transaction_retries
519
+ class_trait :aquire_lock_sc => SLDB::default_aquire_lock_sc
520
+ class_trait :transaction_retries_sc => SLDB::default_transaction_retries_sc
521
+ class_trait :attempt_lockd_recovery => SLDB::default_attempt_lockd_recovery
522
+ class_trait :lockd_recover_wait => SLDB::default_lockd_recover_wait
523
+ class_trait :aquire_lock_lockfile_stale_age => SLDB::default_aquire_lock_lockfile_stale_age
524
+ class_trait :refresher => SLDB::default_refresher
525
+ class_trait :aquire_lock_refresh_rate => SLDB::default_aquire_lock_refresh_rate
526
+ class_trait :administrator => SLDB::default_administrator
527
+ #
528
+ # concrete instance traits
529
+ #
530
+ trait :path
531
+ trait :dbdir
532
+ trait :dbpath
533
+ trait :opts
534
+ trait :dirname
535
+ trait :schema
536
+ trait :fields
537
+ trait :mutex
538
+ trait :lockfile
539
+ trait :sql_debug
540
+ trait :busy_sc
541
+ trait :transaction_retries
542
+ trait :aquire_lock_sc
543
+ trait :transaction_retries_sc
544
+ trait :attempt_lockd_recovery
545
+ trait :lockd_recover_wait
546
+ trait :aquire_lock_lockfile_stale_age
547
+ trait :refresher
548
+ trait :aquire_lock_refresh_rate
549
+ trait :administrator
550
+
551
+ def initialize(*args)
552
+ #--{{{
553
+ @args, @opts = Util::optfilter args
554
+ @path = @args.first || __getopt('path')
555
+ raise ArgumentError, 'no path' unless @path
556
+
557
+ @dirname = @path
558
+ @dbpath = File::join @dirname, 'db'
559
+ @schema = File::join @dirname, '.schema'
560
+ @waiting_w = File::join @dirname, ".#{ Util::hostname }.#{ $$ }.waiting.w"
561
+ @waiting_r = File::join @dirname, ".#{ Util::hostname }.#{ $$ }.waiting.r"
562
+ @lock_w = File::join @dirname, ".#{ Util::hostname }.#{ $$ }.lock.w"
563
+ @lock_r = File::join @dirname, ".#{ Util::hostname }.#{ $$ }.lock.r"
564
+ @lockfile = File::join @dirname, '.lock'
565
+ @lockf = Lockfile::new(File::join(@dirname, '.lockf'))
566
+ @in_transaction = false
567
+ @in_ro_transaction = false
568
+ @db = nil
569
+ @synchronizing = false
570
+ @lockd_recover = File::join(File::dirname(@dirname), ".#{ File::basename(@dirname) }.lockd_recover")
571
+ @lockd_recover_lockf = Lockfile::new "#{ @lockd_recover }.lock"
572
+ @lockd_recovered = false
573
+
574
+ @sync = Sync::new
575
+
576
+ @logger = __getopt 'logger', SLDB::default_logger
577
+ @sql_debug = __getopt 'sql_debug'
578
+ @busy_sc = __getopt 'busy_sc'
579
+ @transaction_retries = __getopt 'transaction_retries'
580
+ @aquire_lock_sc = __getopt 'aquire_lock_sc'
581
+ @transaction_retries_sc = __getopt 'transaction_retries_sc'
582
+ @attempt_lockd_recovery = __getopt 'attempt_lockd_recovery'
583
+ @lockd_recover_wait = __getopt 'lockd_recover_wait'
584
+ @aquire_lock_lockfile_stale_age = __getopt 'aquire_lock_lockfile_stale_age'
585
+ @aquire_lock_refresh_rate = __getopt 'aquire_lock_refresh_rate'
586
+ @refresher = __getopt 'refresher'
587
+ @administrator = __getopt 'administrator'
588
+
589
+ __bootstrap
590
+ #--}}}
591
+ end
592
+
593
+ def ro_transaction(opts = {}, &block)
594
+ #--{{{
595
+ ret = nil
596
+ opts['read_only'] = true
597
+ __synchronizing do
598
+ if @in_ro_transaction or @in_transaction
599
+ ret = yield
600
+ else
601
+ __busy_catch do
602
+ __ro_transaction do
603
+ __lockd_recover_wrap(opts) do
604
+ __transaction_wrap(opts) do
605
+ __aquire_lock(opts) do
606
+ __connect(opts) do
607
+ ret = yield
608
+ end
609
+ end
610
+ end
611
+ end
612
+ end
613
+ end
614
+ end
615
+ end
616
+ ret
617
+ #--}}}
618
+ end
619
+ alias read_only_transaction ro_transaction
620
+
621
+ def transaction opts = {}
622
+ #--{{{
623
+ ret = nil
624
+ __synchronizing do
625
+ if @in_transaction
626
+ ret = yield
627
+ else
628
+ __busy_catch do
629
+ __transaction do
630
+ __lockd_recover_wrap(opts) do
631
+ __transaction_wrap(opts) do
632
+ __aquire_lock(opts) do
633
+ __connect(opts) do
634
+ execute 'begin'
635
+ ret = yield
636
+ execute 'commit'
637
+ end
638
+ end
639
+ end
640
+ end
641
+ end
642
+ end
643
+ end
644
+ end
645
+ ret
646
+ #--}}}
647
+ end
648
+
649
+ def execute sql, &block
650
+ #--{{{
651
+ raise 'not in transaction' unless @in_transaction or @in_ro_transaction
652
+ raise 'not connected' unless @connected
653
+ __logger << "sql_debug:\n#{ sql }\n" if @sql_debug
654
+ begin
655
+ @db.execute sql, &block
656
+ rescue SQLite::SQLException => e
657
+ error{ "bad sql!\n#{ sql }" }
658
+ raise
659
+ end
660
+ #--}}}
661
+ end
662
+
663
+ def vacuum
664
+ #--{{{
665
+ ret = false
666
+ __synchronizing do
667
+ if @in_transaction
668
+ ret = yield
669
+ else
670
+ __busy_catch do
671
+ __transaction do
672
+ __lockd_recover_wrap(opts) do
673
+ __transaction_wrap(opts) do
674
+ __aquire_lock(opts) do
675
+ __connect(opts) do
676
+ ret = execute 'vacuum'
677
+ end
678
+ end
679
+ end
680
+ end
681
+ end
682
+ end
683
+ end
684
+ end
685
+ ret
686
+ #--}}}
687
+ end
688
+
689
+ def integrity_check
690
+ #--{{{
691
+ debug{ "running integrity_check on <#{ @dbpath }>" }
692
+ ret = nil
693
+ __synchronizing do
694
+ if @in_transaction
695
+ ret = yield
696
+ else
697
+ __busy_catch do
698
+ __transaction do
699
+ __lockd_recover_wrap(opts) do
700
+ __transaction_wrap(opts) do
701
+ __aquire_lock(opts) do
702
+ __connect(opts) do
703
+ tuple = execute 'PRAGMA integrity_check;'
704
+ ret = (tuple and tuple.first and (tuple.first["integrity_check"] =~ /^\s*ok\s*$/io))
705
+ end
706
+ end
707
+ end
708
+ end
709
+ end
710
+ end
711
+ end
712
+ end
713
+ ret
714
+ #--}}}
715
+ end
716
+
717
+ def lock opts = {}
718
+ #--{{{
719
+ __lockd_recover_wrap(opts){ __aquire_lock(opts){ yield }}
720
+ #--}}}
721
+ end
722
+ alias write_lock lock
723
+ alias wlock write_lock
724
+
725
+ def read_lock(opts = {}, &block)
726
+ #--{{{
727
+ opts['read_only'] = true
728
+ lock opts, &block
729
+ #--}}}
730
+ end
731
+ alias rlock read_lock
732
+
733
+ def table_names
734
+ #--{{{
735
+ ro_transaction do
736
+ tuples = execute "select name from sqlite_master where type = 'table' and name notnull"
737
+ tuples.map{|t| t['name']}
738
+ end
739
+ #--}}}
740
+ end
741
+ alias tablenames table_names
742
+
743
+ def field_names table_name = nil
744
+ #--{{{
745
+ fields = Hash::new{|h,k| h[k] = []}
746
+ ro_transaction do
747
+ tnames = (table_name ? [table_name] : table_names)
748
+ tnames.each do |tname|
749
+ tuples = execute "pragma table_info('#{ tname }')"
750
+ tuples.each{|t| fields[tname] << t['name']}
751
+ end
752
+ end
753
+ table_name ? fields[table_name] : fields
754
+ #--}}}
755
+ end
756
+ alias fieldnames field_names
757
+ alias fields field_names
758
+ alias fields_for field_names
759
+
760
+ def t2h tuple
761
+ #--{{{
762
+ h = {}
763
+ fields = tuple.fields
764
+ fields.each{|f| h[f] = tuple[f]}
765
+ h
766
+ #--}}}
767
+ end
768
+
769
+ def h2t h, table_name = nil
770
+ #--{{{
771
+ table_name = table_names.first
772
+ raise ArgumentError, "no table_name" unless table_name
773
+ fields = field_names table_name
774
+ t = tuple
775
+ fields.each{|f| t[f] = h[f]}
776
+ t
777
+ #--}}}
778
+ end
779
+
780
+ def tuple table_name = nil
781
+ #--{{{
782
+ table_name = table_names.first
783
+ raise ArgumentError, "no table_name" unless table_name
784
+ fields = field_names table_name
785
+ t = Array::new fields.size
786
+ t.fields = fields
787
+ t
788
+ #--}}}
789
+ end
790
+ alias tuple_for tuple
791
+
792
+
793
+ # private
794
+
795
+ def __klass
796
+ #--{{{
797
+ @klass ||= self.class
798
+ #--}}}
799
+ end
800
+ private '__klass'
801
+
802
+ def __dbopen
803
+ #--{{{
804
+ ret = nil
805
+ $db = @db =
806
+ begin
807
+ SQLite::Database::new @dbpath
808
+ rescue
809
+ SQLite::Database::new @dbpath, 0
810
+ end
811
+ @db.use_array = true
812
+ if block_given?
813
+ begin
814
+ ret = yield @db
815
+ ensure
816
+ @db.close rescue nil
817
+ $db = @db = nil
818
+ end
819
+ else
820
+ ret = @db
821
+ end
822
+ ret
823
+ #--}}}
824
+ end
825
+ private '__dbopen'
826
+
827
+ def __bootstrap
828
+ #--{{{
829
+ return if test ?e, @schema
830
+
831
+ __synchronizing do
832
+ FileUtils::mkdir_p @path rescue nil unless test ?d, path
833
+
834
+ raise "<#{ @path }> already exists as a file!" if test ?f, path
835
+
836
+ setup = File::join @dirname, '.setup'
837
+
838
+ open(setup, 'a+') do |f|
839
+ f.posixlock File::LOCK_EX
840
+ unless test ?e, @schema
841
+ tr = @transaction_retries
842
+ begin
843
+ sql = %w( pragmas schema indexes )
844
+ @transaction_retries = 0
845
+ transaction('__ignore_missing_schema' => true) do
846
+ sql.each{|s| execute __klass.send(s) if __klass.send(s)}
847
+ end
848
+ FileUtils::touch @lockfile
849
+ tmp = "#{ @schema }.tmp"
850
+ open(tmp, 'w') do |f|
851
+ sql.each{|s| f.puts __klass.send(s) if __klass.send(s)}
852
+ end
853
+ FileUtils::mv tmp, @schema
854
+ ensure
855
+ @transaction_retries = tr
856
+ end
857
+ FileUtils::rm_f setup
858
+ end
859
+ end
860
+ end
861
+ #--}}}
862
+ end
863
+ private '__bootstrap'
864
+
865
+ def __getopt opt, default = nil
866
+ #--{{{
867
+ class_default = __klass.send opt #rescue nil
868
+ Util::getopt(opt, @opts, default || class_default)
869
+ #--}}}
870
+ end
871
+ private '__getopt'
872
+
873
+ def __synchronizing(*a, &b)
874
+ #--{{{
875
+ begin
876
+ @sync.lock(Sync::EX)
877
+ yield
878
+ ensure
879
+ @sync.lock(Sync::UN)
880
+ end
881
+ #--}}}
882
+ end
883
+ private '__synchronizing'
884
+
885
+ def __ro_transaction
886
+ #--{{{
887
+ ret = nil
888
+ __synchronizing do
889
+ if @in_ro_transaction or @in_transaction
890
+ ret = yield
891
+ else
892
+ state = @in_ro_transaction
893
+ begin
894
+ @in_ro_transaction = true
895
+ ret = yield
896
+ ensure
897
+ @in_ro_transaction = state
898
+ end
899
+ end
900
+ end
901
+ ret
902
+ #--}}}
903
+ end
904
+ private '__ro_transaction'
905
+
906
+ def __transaction
907
+ #--{{{
908
+ __synchronizing do
909
+ raise 'cannot upgrade read-only transaction' if @in_ro_transaction
910
+ ret = nil
911
+ if @in_transaction
912
+ ret = yield
913
+ else
914
+ state = @in_transaction
915
+ begin
916
+ @in_transaction = true
917
+ ret = yield
918
+ ensure
919
+ @in_transaction = state
920
+ end
921
+ end
922
+ ret
923
+ end
924
+ #--}}}
925
+ end
926
+ private '__transaction'
927
+
928
+ def __busy_catch
929
+ #--{{{
930
+ @busy_sc.reset
931
+ begin
932
+ yield
933
+ rescue SQLite::BusyException => e
934
+ warn{ "<#{ @dbpath }> seems to be locked!" }
935
+ warn{ "this should never happen unless - another process is locking the db outside the sldb protocol" }
936
+ timeout = @busy_sc.next
937
+ warn{ "waiting <#{ timeout }>..." }
938
+ sleep timeout
939
+ warn{ "retrying..." }
940
+ retry
941
+ end
942
+ #--}}}
943
+ end
944
+ private '__busy_catch'
945
+
946
+ def __lockd_recover_wrap opts = {}
947
+ #--{{{
948
+ ret = nil
949
+ try_again = false
950
+ begin
951
+ begin
952
+ @lockd_recovered = false
953
+ old_mtime =
954
+ begin
955
+ Util::uncache @lockd_recover rescue nil
956
+ File::stat(@lockd_recover).mtime
957
+ rescue
958
+ Time::now
959
+ end
960
+ ret = yield
961
+ ensure
962
+ new_mtime =
963
+ begin
964
+ Util::uncache @lockd_recover rescue nil
965
+ File::stat(@lockd_recover).mtime
966
+ rescue
967
+ old_mtime
968
+ end
969
+
970
+ if new_mtime and old_mtime and new_mtime > old_mtime and not @lockd_recovered
971
+ try_again = true
972
+ end
973
+ end
974
+ rescue
975
+ if try_again
976
+ warn{ "a remote lockd recovery has invalidated this transaction!" }
977
+ warn{ "retrying..."}
978
+ sleep 120
979
+ retry
980
+ else
981
+ raise
982
+ end
983
+ end
984
+ ret
985
+ #--}}}
986
+ end
987
+ private '__lockd_recover_wrap'
988
+
989
+ #
990
+ # TODO - perhaps should not retry on SQLException?? yet errors seem to map to
991
+ # this exception even when the sql is fine... safest (and most anoying) is to
992
+ # simply retry on any error. arggh.
993
+ #
994
+ def __transaction_wrap opts = {}
995
+ #--{{{
996
+ ro = Util::getopt 'read_only', opts
997
+ ret = nil
998
+ if ro
999
+ ret = yield
1000
+ else
1001
+ errors = []
1002
+ @transaction_retries_sc.reset
1003
+ begin
1004
+ ret = yield
1005
+ rescue => e
1006
+ #rescue SQLite::DatabaseException, SQLite::SQLException, SystemCallError => e
1007
+ if @transaction_retries == 0
1008
+ raise
1009
+ elsif errors.size >= @transaction_retries
1010
+ error{ "MAXIMUM TRANSACTION RETRIES SURPASSED" }
1011
+ raise
1012
+ else
1013
+ warn{ e } if(errors.empty? or not Util::erreq(errors[-1], e))
1014
+ errors << e
1015
+ warn{ "retry <#{ errors.size }>..." }
1016
+ end
1017
+ sleep @transaction_retries_sc.next
1018
+ retry
1019
+ end
1020
+ end
1021
+ ret
1022
+ #--}}}
1023
+ end
1024
+ private '__transaction_wrap'
1025
+
1026
+ def __aquire_lock opts = {}
1027
+ #--{{{
1028
+ ro = Util::getopt 'read_only', opts
1029
+ refresh =
1030
+ if Class === refresher
1031
+ Util::getopt 'refresh', opts, true
1032
+ else
1033
+ false
1034
+ end
1035
+ ret = nil
1036
+
1037
+ @aquire_lock_sc.reset
1038
+
1039
+ waiting, ltype, lfile =
1040
+ if ro
1041
+ [@waiting_r, File::LOCK_SH | File::LOCK_NB, @lock_r]
1042
+ else
1043
+ [@waiting_w, File::LOCK_EX | File::LOCK_NB, @lock_w]
1044
+ end
1045
+
1046
+ ltype_s = (ltype == File::LOCK_EX ? 'write' : 'read')
1047
+ ltype ||= File::LOCK_NB
1048
+
1049
+ aquired = false
1050
+
1051
+ until aquired
1052
+ begin
1053
+ debug{ "aquiring lock" }
1054
+ #@lockf.lock unless ro
1055
+
1056
+ open(@lockfile, 'a+') do |lf|
1057
+ locked = false
1058
+ lock_refresher = nil
1059
+ sc = nil
1060
+
1061
+ begin
1062
+ FileUtils::touch waiting
1063
+ # poll
1064
+ 42.times do |i|
1065
+ debug{ "applying posixlock to <#{ @lockfile }>" }
1066
+ locked = lf.posixlock(ltype | File::LOCK_NB)
1067
+ break if locked
1068
+ sleep rand
1069
+ end
1070
+
1071
+ if locked
1072
+ aquired = true
1073
+ if refresh
1074
+ lock_refresher = refresher::new @lockfile, @aquire_lock_refresh_rate
1075
+ debug{ "refresher pid <#{ refresher.pid }> refresh_rate <#{ @aquire_lock_refresh_rate }>" }
1076
+ end
1077
+ FileUtils::rm_f waiting rescue nil
1078
+ FileUtils::touch lfile rescue nil
1079
+ debug{ "aquired lock" }
1080
+ ret = yield
1081
+ debug{ "released lock" }
1082
+ else
1083
+ aquired = false
1084
+ stat = File::stat @lockfile
1085
+ mtime = stat.mtime
1086
+ stale = mtime < (Time::now - @aquire_lock_lockfile_stale_age)
1087
+ if stale
1088
+ warn{ "detected stale lockfile of mtime <#{ mtime }>" }
1089
+ __lockd_recover if @attempt_ockd_recovery
1090
+ end
1091
+ sc = @aquire_lock_sc.next
1092
+ debug{ "failed to aquire lock - sleep(#{ sc })" }
1093
+ sleep sc
1094
+ end
1095
+
1096
+ ensure
1097
+ if locked
1098
+ unlocked = false
1099
+ begin
1100
+ 42.times do
1101
+ unlocked = lf.posixlock(File::LOCK_UN | File::LOCK_NB)
1102
+ break if unlocked
1103
+ sleep rand
1104
+ end
1105
+ ensure
1106
+ lf.posixlock File::LOCK_UN unless unlocked
1107
+ end
1108
+ end
1109
+ lock_refresher.kill if refresh and lock_refresher
1110
+ FileUtils::rm_f waiting rescue nil
1111
+ FileUtils::rm_f lfile rescue nil
1112
+ end
1113
+ end
1114
+ ensure
1115
+ #@lockf.unlock rescue nil unless read_only
1116
+ end
1117
+ end
1118
+ ret
1119
+ #--}}}
1120
+ end
1121
+ private '__aquire_lock'
1122
+
1123
+ def __connect opts = {}
1124
+ #--{{{
1125
+ yield @db if @connected
1126
+ unless(Util::getopt('__ignore_missing_schema', opts))
1127
+ raise "missing db schema file <#{ @schema }>" unless test ?e, @schema
1128
+ end
1129
+ __dbopen do
1130
+ begin
1131
+ @connected = true
1132
+ yield @db
1133
+ ensure
1134
+ @connected = false
1135
+ end
1136
+ end
1137
+ #--}}}
1138
+ end
1139
+ private '__connect'
1140
+
1141
+ def __lockd_recover
1142
+ #--{{{
1143
+ return nil unless @attempt_lockd_recovery
1144
+ warn{ "attempting lockd recovery" }
1145
+ time = Time::now
1146
+ ret = nil
1147
+
1148
+ @lockd_recover_lockf.lock do
1149
+ Util::uncache @dirname rescue nil
1150
+ Util::uncache @dbpath rescue nil
1151
+ Util::uncache @lockfile rescue nil
1152
+ Util::uncache @lockd_recover rescue nil
1153
+ mtime = File::stat(@lockd_recover).mtime rescue time
1154
+
1155
+ if mtime > time
1156
+ warn{ "skipping lockd recovery (another node has already recovered)" }
1157
+ ret = true
1158
+ else
1159
+ moved = false
1160
+ begin
1161
+ FileUtils::touch @lockd_recover
1162
+ @lockd_recovered = false
1163
+
1164
+ begin
1165
+ report = <<-msg
1166
+ the sldb library has detected a locking failure on your system.
1167
+ this means that fcntl(2) based locks are failing. common causes
1168
+ include bad nfs setup. you are advised to read the nfs faq and
1169
+ consider things such as
1170
+ - having proper nfs daemons running
1171
+ - having firewalls configured properly for bidirectional
1172
+ communication between nfs client and servers
1173
+ - mount options that can prevent locking
1174
+
1175
+ if your system is not on nfs this is most likely the cause of a
1176
+ kernel bug. in either case this library can perform auto-matic
1177
+ recover in 99% of cases and has done so now. you should not see
1178
+ this message again unless you system is very sick.
1179
+
1180
+ the following information may be useful in your debugging.
1181
+
1182
+ hostname : #{ Util::hostname }
1183
+ pid : #{ Process.pid }
1184
+ time : #{ Time::now }
1185
+ db :
1186
+ dir : #{ @dirname }
1187
+ path : #{ @dbpath }
1188
+ stat : #{ File::stat(@dbpath).inspect }
1189
+ lockfile :
1190
+ path : #{ @lockfile }
1191
+ stat : #{ File::stat(@lockfile).inspect }
1192
+ msg
1193
+ info{ "SLDB LOCKD RECOVERY REPORT" }
1194
+ __logger << report
1195
+ cmd = "mail -s SLDB_LOCKD_RECOVERY #{ @administrator } <<eof\n#{ report }\neof"
1196
+ Util::system cmd
1197
+ rescue Exception
1198
+ nil
1199
+ end
1200
+
1201
+ warn{ "sleeping #{ @lockd_recover_wait }s before continuing..." }
1202
+ sleep @lockd_recover_wait
1203
+
1204
+ tmp = "#{ @dirname }.#{ $$ }.#{ Util::hostname }.tmp"
1205
+
1206
+ warn{ "rm_rf <#{ tmp }>" }
1207
+ FileUtils::rm_rf tmp
1208
+
1209
+ warn{ "mv <#{ @dirname }> <#{ tmp }>" }
1210
+ FileUtils::mv @dirname, tmp
1211
+
1212
+ moved = true
1213
+
1214
+ rfiles = [@dbpath, @lockfile].map{|f| File::join(tmp,File::basename(f))}
1215
+ rfiles.each do |f|
1216
+ ftmp = "#{ f }.tmp"
1217
+ warn{ "rm_rf <#{ ftmp }>" }
1218
+ FileUtils::rm_rf ftmp
1219
+ warn{ "cp <#{ f } <#{ ftmp }>" }
1220
+ FileUtils::cp f, ftmp
1221
+ warn{ "rm_f <#{ f }>" }
1222
+ FileUtils::rm f
1223
+ warn{ "mv <#{ ftmp } <#{ f }>" }
1224
+ FileUtils::mv ftmp, f
1225
+ end
1226
+
1227
+ dbtmp = File::join(tmp, File::basename(@dbpath))
1228
+ Util::uncache dbtmp
1229
+
1230
+ if integrity_check(dbtmp)
1231
+ FileUtils::mv tmp, @dirname
1232
+ FileUtils::cp @lockd_recover_lockf.path, @lockd_recover
1233
+ @lockd_recovered = true
1234
+ Util::uncache @dirname rescue nil
1235
+ Util::uncache @dbpath rescue nil
1236
+ Util::uncache @lockfile rescue nil
1237
+ Util::uncache @lockd_recover rescue nil
1238
+ warn{ "lockd recovery complete" }
1239
+ else
1240
+ FileUtils::mv tmp, @dirname
1241
+ @lockd_recovered = false
1242
+ error{ "lockd recovery failed" }
1243
+ end
1244
+
1245
+ ret = @lockd_recovered
1246
+ ensure
1247
+ if moved and not @lockd_recovered and tmp and test(?d, tmp)
1248
+ FileUtils::mv tmp, @dirname rescue nil
1249
+ end
1250
+ end
1251
+ end
1252
+ end
1253
+ ret
1254
+ #--}}}
1255
+ end
1256
+ private '__lockd_recover'
1257
+
1258
+ def __logger
1259
+ #--{{{
1260
+ __synchronizing{ @logger }
1261
+ #--}}}
1262
+ end
1263
+ private '__logger'
1264
+
1265
+ %w( debug info warn error fatal ).each do |meth|
1266
+ module_eval <<-code
1267
+ def #{ meth }(*a, &b)
1268
+ __logger.#{ meth }(*a, &b)
1269
+ end
1270
+ code
1271
+ end
1272
+ #--}}}
1273
+ end # class AbstractSLDB
1274
+
1275
+ #
1276
+ # class factories
1277
+ #
1278
+ class << self
1279
+ #--{{{
1280
+ def new argv = [], &b
1281
+ #--{{{
1282
+ args, opts = Util::optfilter argv
1283
+ path = args.first
1284
+ opts['path'] = path if path
1285
+ klass = Class::new AbstractSLDB
1286
+ klass.instance_eval &b if b
1287
+ opts.each{|k,v| klass.send k, v}
1288
+ # raise ArgumentError, "no schema given" unless klass.schema
1289
+ # raise ArgumentError, "no fields given" unless klass.fields
1290
+ #puts "klass <#{ klass.inspect }>"
1291
+ #puts "klass::new <#{ klass::new.inspect }>"
1292
+ (path ? klass::new : klass)
1293
+ #--}}}
1294
+ end
1295
+ alias klass new
1296
+ alias db_klass new
1297
+ alias class new
1298
+ alias db_class new
1299
+ #--}}}
1300
+ end
1301
+ #--}}}
1302
+ end # module SLDB