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