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.
- data/README +257 -0
- data/README.tmpl +30 -0
- data/VERSION +1 -0
- data/gemspec.rb +23 -0
- data/gen_readme.rb +26 -0
- data/install.rb +206 -0
- data/lib/sldb-0.1.0.rb +1302 -0
- data/lib/sldb.rb +1302 -0
- data/sample/a.rb +22 -0
- data/sample/b.rb +29 -0
- data/sample/c.rb +35 -0
- data/sample/d.rb +37 -0
- data/sample/e.rb +19 -0
- data/white_box/test.rb +49 -0
- metadata +55 -0
data/lib/sldb.rb
ADDED
@@ -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
|