rsched 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/ChangeLog ADDED
@@ -0,0 +1,18 @@
1
+
2
+ == 2011-07-19 version 0.3.0
3
+
4
+ * Changed not to release a lock when a task is failed
5
+ * Added -E, --release-on-fail option
6
+ * Added respectable timeout implementation
7
+
8
+
9
+ == 2011-07-11 version 0.2.0
10
+
11
+ * Splits single bin file into multiple files
12
+ * Added test codes
13
+
14
+
15
+ == 2011-07-10 version 0.1.0
16
+
17
+ * Initial release
18
+
@@ -9,13 +9,15 @@ confout = nil
9
9
  schedule = []
10
10
 
11
11
  defaults = {
12
- :timeout => 600,
12
+ :timeout => 30,
13
13
  :resume => 3600,
14
14
  :delete => 2592000,
15
15
  :delay => 0,
16
16
  :interval => 10,
17
17
  :type => 'mysql',
18
18
  :name => "#{Process.pid}.#{`hostname`.strip}",
19
+ :kill_retry => 60,
20
+ :release_on_fail => false,
19
21
  }
20
22
 
21
23
  conf = { }
@@ -36,7 +38,7 @@ op.on('-a', '--add EXPR', 'Add an execution schedule') {|s|
36
38
  schedule << s
37
39
  }
38
40
 
39
- op.on('-t', '--timeout SEC', 'Retry timeout (default: 600)', Integer) {|i|
41
+ op.on('-t', '--timeout SEC', 'Retry timeout (default: 30)', Integer) {|i|
40
42
  conf[:timeout] = i
41
43
  }
42
44
 
@@ -64,14 +66,26 @@ op.on('-F', '--from YYYY-mm-dd_OR_now', 'Time to start scheduling') {|s|
64
66
  end
65
67
  }
66
68
 
67
- #op.on('-x', '--kill-timeout SEC', 'Threashold time before killing process (default: timeout * 5)', Integer) {|i|
68
- # conf[:kill_timeout] = i
69
- #}
69
+ op.on('-e', '--extend-timeout SEC', 'Threashold time before extending visibility timeout (default: timeout * 3/4)', Integer) {|i|
70
+ conf[:extend_timeout] = i
71
+ }
72
+
73
+ op.on('-x', '--kill-timeout SEC', 'Threashold time before killing process (default: timeout * 10)', Integer) {|i|
74
+ conf[:kill_timeout] = i
75
+ }
76
+
77
+ op.on('-X', '--kill-retry SEC', 'Threashold time before retrying killing process (default: 60)', Integer) {|i|
78
+ conf[:kill_retry] = i
79
+ }
70
80
 
71
81
  op.on('-i', '--interval SEC', 'Scheduling interval (default: 10)', Integer) {|i|
72
82
  conf[:interval] = i
73
83
  }
74
84
 
85
+ op.on('-E', '--release-on-fail', 'Releases lock if task failed so that other node can retry immediately', TrueClass) {|b|
86
+ conf[:release_on_fail] = b
87
+ }
88
+
75
89
  op.on('-T', '--type TYPE', 'Lock database type (default: mysql)') {|s|
76
90
  conf[:type] = s
77
91
  }
@@ -177,6 +191,14 @@ begin
177
191
  raise "Unknown lock server type '#{conf[:type]}'"
178
192
  end
179
193
 
194
+ unless conf[:extend_timeout]
195
+ conf[:extend_timeout] = conf[:timeout] / 4 * 3
196
+ end
197
+
198
+ unless conf[:kill_timeout]
199
+ conf[:kill_timeout] = conf[:timeout] * 10
200
+ end
201
+
180
202
  rescue
181
203
  usage $!.to_s
182
204
  end
@@ -248,9 +270,16 @@ end
248
270
  if type == :run
249
271
  load File.expand_path(conf[:run])
250
272
  run_proc = method(:run)
273
+ if defined? terminate
274
+ kill_proc = method(:terminate)
275
+ else
276
+ kill_proc = Proc.new { }
277
+ end
251
278
  else
252
279
  run_proc = RSched::ExecRunner.new(conf[:exec])
280
+ kill_proc = run_proc.method(:terminate)
253
281
  end
254
282
 
255
- worker.run(run_proc)
283
+ worker.init_proc(run_proc, kill_proc)
284
+ worker.run
256
285
 
data/lib/rsched/dblock.rb CHANGED
@@ -4,9 +4,8 @@ module RSched
4
4
 
5
5
  class DBLock < Lock
6
6
  def initialize(hostname, timeout, uri, user, pass)
7
+ super(hostname, timeout)
7
8
  require 'dbi'
8
- @hostname = hostname
9
- @timeout = timeout
10
9
  @db = DBI.connect(uri, user, pass)
11
10
  init_db
12
11
  end
@@ -47,6 +46,13 @@ class DBLock < Lock
47
46
  return n > 0
48
47
  end
49
48
 
49
+ def extend_timeout(token, timeout=Time.now.to_i+@timeout)
50
+ ident, time = *token
51
+ n = @db.do('UPDATE rsched SET timeout=? WHERE ident = ? AND time = ? AND host = ?;',
52
+ timeout, ident, time, @hostname)
53
+ return n > 0
54
+ end
55
+
50
56
  def delete_before(ident, time)
51
57
  @db.do('DELETE FROM rsched WHERE ident = ? AND time < ? AND finish IS NOT NULL;', ident, time)
52
58
  end
@@ -61,8 +67,8 @@ class DBLock < Lock
61
67
  end
62
68
 
63
69
  def try_update(ident, time, now)
64
- n = @db.do('UPDATE rsched SET host=?, timeout=? WHERE ident = ? AND time = ? AND finish IS NULL AND (timeout < ? OR host = ?);',
65
- @hostname, now+@timeout, ident, time, now, @hostname)
70
+ n = @db.do('UPDATE rsched SET host=?, timeout=? WHERE ident = ? AND time = ? AND finish IS NULL AND timeout < ?;',
71
+ @hostname, now+@timeout, ident, time, now)
66
72
  return n > 0
67
73
  end
68
74
 
data/lib/rsched/engine.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'thread'
2
+ require 'monitor'
2
3
  require 'time'
3
4
  require 'rsched/lock'
4
5
 
@@ -50,10 +51,16 @@ class Engine
50
51
  @delay = conf[:delay]
51
52
  @interval = conf[:interval]
52
53
  @delete = conf[:delete]
54
+ @extend_timeout = conf[:extend_timeout]
55
+ @kill_timeout = conf[:kill_timeout]
56
+ @kill_retry = conf[:kill_retry]
53
57
  @sched_start = conf[:from] || 0
58
+ @release_on_fail = conf[:release_on_fail]
54
59
  @finished = false
55
60
  @ss = {}
56
61
 
62
+ @extender = TimerThread.new(@lock, @extend_timeout, @kill_timeout, @kill_retry)
63
+
57
64
  @mutex = Mutex.new
58
65
  @cond = ConditionVariable.new
59
66
  end
@@ -64,7 +71,13 @@ class Engine
64
71
  @ss[ident] = Sched.new(cron, action, @sched_start, now-@resume, now-@delay)
65
72
  end
66
73
 
67
- def run(run_proc)
74
+ def init_proc(run_proc, kill_proc)
75
+ @run_proc = run_proc
76
+ @extender.init_proc(kill_proc)
77
+ end
78
+
79
+ def run
80
+ @extender.start
68
81
  until @finished
69
82
  one = false
70
83
 
@@ -75,8 +88,8 @@ class Engine
75
88
  s.queue.delete_if {|time|
76
89
  next if @finished
77
90
 
78
- x = @lock.acquire(ident, time)
79
- case x
91
+ token = @lock.acquire(ident, time)
92
+ case token
80
93
  when nil
81
94
  # already finished
82
95
  true
@@ -87,16 +100,7 @@ class Engine
87
100
 
88
101
  else
89
102
  one = true
90
- if process(ident, time, s.action, run_proc)
91
- # success
92
- @lock.finish(x)
93
- try_delete(ident)
94
- true
95
- else
96
- # fail
97
- @lock.release(x)
98
- false
99
- end
103
+ process(token, ident, time, s.action)
100
104
  end
101
105
  }
102
106
 
@@ -114,6 +118,7 @@ class Engine
114
118
 
115
119
  def shutdown
116
120
  @finished = true
121
+ @extender.shutdown
117
122
  @mutex.synchronize {
118
123
  @cond.broadcast
119
124
  }
@@ -138,22 +143,120 @@ class Engine
138
143
  end
139
144
  end
140
145
 
141
- def process(ident, time, action, run_proc)
146
+ def process(token, ident, time, action)
147
+ puts "started token=#{token.inspect} time=#{time}"
148
+
149
+ @extender.set_token(token)
150
+
151
+ success = false
142
152
  begin
143
- run_proc.call(ident, time, action)
144
- return true
153
+ @run_proc.call(ident, time, action)
154
+ puts "finished token=#{token.inspect}"
155
+ success = true
145
156
  rescue
146
- puts "failed ident=#{ident} time=#{time}: #{$!}"
157
+ puts "failed token=#{token.inspect} time=#{time}: #{$!}"
147
158
  $!.backtrace.each {|bt|
148
159
  puts " #{bt}"
149
160
  }
150
- return false
161
+ end
162
+
163
+ @extender.reset_token
164
+
165
+ if success
166
+ @lock.finish(token)
167
+ cleanup_old_entries(ident)
168
+ true
169
+ else
170
+ if @release_on_fail
171
+ @lock.release(token)
172
+ end
173
+ false
151
174
  end
152
175
  end
153
176
 
154
- def try_delete(ident)
177
+ def cleanup_old_entries(ident)
155
178
  @lock.delete_before(ident, Time.now.to_i-@delete)
156
179
  end
180
+
181
+ class TimerThread
182
+ include MonitorMixin
183
+
184
+ def initialize(lock, extend_timeout, kill_timeout, kill_retry)
185
+ super()
186
+ @lock = lock
187
+ @extend_timeout = extend_timeout
188
+ @kill_timeout = kill_timeout
189
+ @kill_retry = kill_retry
190
+ @kill_time = nil
191
+ @kill_proc = nil
192
+ @extend_time = nil
193
+ @token = nil
194
+ @finished = false
195
+ end
196
+
197
+ def init_proc(kill_proc)
198
+ @kill_proc = kill_proc
199
+ end
200
+
201
+ def start
202
+ @thread = Thread.new(&method(:run))
203
+ end
204
+
205
+ def join
206
+ @thread.join
207
+ end
208
+
209
+ def set_token(token)
210
+ synchronize do
211
+ now = Time.now.to_i
212
+ @extend_time = now + @extend_timeout
213
+ @kill_time = now + @kill_timeout
214
+ @token = token
215
+ end
216
+ end
217
+
218
+ def reset_token
219
+ synchronize do
220
+ @token = nil
221
+ end
222
+ end
223
+
224
+ def shutdown
225
+ @finished = true
226
+ end
227
+
228
+ private
229
+ def run
230
+ until @finished
231
+ sleep 1
232
+ synchronize do
233
+ if @token
234
+ now = Time.now.to_i
235
+ try_kill(now, @token)
236
+ try_extend(now, @token)
237
+ end
238
+ end
239
+ end
240
+ end
241
+
242
+ def try_extend(now, token)
243
+ if now > @extend_time
244
+ puts "extending token=#{token.inspect}"
245
+ @lock.extend_timeout(token, now)
246
+ @extend_time = now + @extend_timeout
247
+ end
248
+ end
249
+
250
+ def try_kill(now, token)
251
+ if now > @kill_time
252
+ if @kill_proc
253
+ puts "killing #{token.inspect}..."
254
+ @kill_proc.call rescue nil
255
+ end
256
+ @kill_time = now + @kill_retry
257
+ end
258
+ end
259
+ end
157
260
  end
158
261
 
159
262
 
@@ -161,11 +264,14 @@ class ExecRunner
161
264
  def initialize(cmd)
162
265
  @cmd = cmd + ' ' + ARGV.map {|a| Shellwords.escape(a) }.join(' ')
163
266
  @iobuf = ''
267
+ @pid = nil
268
+ @next_kill = :TERM
164
269
  end
165
270
 
166
271
  def call(ident, time, action)
167
272
  message = [ident, time, action].join("\t")
168
273
  IO.popen(@cmd, "r+") {|io|
274
+ @pid = io.pid
169
275
  io.write(message) rescue nil
170
276
  io.close_write
171
277
  begin
@@ -180,6 +286,11 @@ class ExecRunner
180
286
  raise "Command failed"
181
287
  end
182
288
  end
289
+
290
+ def terminate
291
+ Process.kill(@next_kill, @pid)
292
+ @next_kill = :KILL
293
+ end
183
294
  end
184
295
 
185
296
 
data/lib/rsched/lock.rb CHANGED
@@ -4,16 +4,23 @@ module RSched
4
4
 
5
5
  class Lock
6
6
  def initialize(hostname, timeout)
7
+ @hostname = hostname
8
+ @timeout = timeout
7
9
  end
8
10
 
11
+ attr_reader :hostname, :timeout
12
+
9
13
  # acquired=token, locked=false, finished=nil
10
- def acquire(ident, time)
14
+ def acquire(ident, time, now=Time.now.to_i)
15
+ end
16
+
17
+ def release(token, next_timeout=Time.now.to_i)
11
18
  end
12
19
 
13
- def release(token)
20
+ def finish(token, now=Time.now.to_i)
14
21
  end
15
22
 
16
- def finish(token)
23
+ def extend_timeout(token, timeout=Time.now.to_i+@timeout)
17
24
  end
18
25
 
19
26
  def delete_before(ident, time)
@@ -1,5 +1,5 @@
1
1
  module RSched
2
2
 
3
- VERSION = '0.2.0'
3
+ VERSION = '0.3.0'
4
4
 
5
5
  end
data/test/dblock_test.rb CHANGED
@@ -34,10 +34,11 @@ class DBLockTest < Test::Unit::TestCase
34
34
  token = db2.acquire('ident1', time, now)
35
35
  assert_equal false, token
36
36
 
37
- # same host can relock
38
- token = db1.acquire('ident1', time, now)
39
- assert_not_equal nil, token
40
- assert_not_equal false, token
37
+ # v0.4.0: same host can't relock
38
+ ## same host can relock
39
+ #token = db1.acquire('ident1', time, now)
40
+ #assert_not_equal nil, token
41
+ #assert_not_equal false, token
41
42
 
42
43
  # different identifier
43
44
  token = db2.acquire('ident2', time, now)
@@ -135,17 +136,19 @@ class DBLockTest < Test::Unit::TestCase
135
136
  assert_not_equal nil, token
136
137
  assert_not_equal false, token
137
138
 
138
- # same host can relock
139
- # + extend timeout
140
- token = db1.acquire('ident1', time, now+TIMEOUT)
141
- assert_not_equal nil, token
142
- assert_not_equal false, token
139
+ # different host can't extend (even if same token)
140
+ ok = db2.extend_timeout(token, now+TIMEOUT*2)
141
+ assert_equal false, ok
142
+
143
+ # same host can extend timeout
144
+ ok = db1.extend_timeout(token, now+TIMEOUT*2)
145
+ assert_equal true, ok
143
146
 
144
- # timeout is extended
147
+ # timeout is extended; different host can't lock
145
148
  token_ = db2.acquire('ident1', time, now+TIMEOUT+1)
146
149
  assert_equal false, token_
147
150
 
148
- # extended timeout is expired
151
+ # extended timeout is expired; different host can lock
149
152
  token = db2.acquire('ident1', time, now+TIMEOUT*2+1)
150
153
  assert_not_equal nil, token
151
154
  assert_not_equal false, token
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rsched
3
3
  version: !ruby/object:Gem::Version
4
- hash: 23
4
+ hash: 19
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
- - 2
8
+ - 3
9
9
  - 0
10
- version: 0.2.0
10
+ version: 0.3.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Sadayuki Furuhashi
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-07-12 00:00:00 +09:00
18
+ date: 2011-07-18 00:00:00 +09:00
19
19
  default_executable: rsched
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -57,6 +57,7 @@ executables:
57
57
  extensions: []
58
58
 
59
59
  extra_rdoc_files:
60
+ - ChangeLog
60
61
  - README.rdoc
61
62
  files:
62
63
  - bin/rsched
@@ -69,6 +70,7 @@ files:
69
70
  - test/exec_test.rb
70
71
  - test/sched_test.rb
71
72
  - test/test_helper.rb
73
+ - ChangeLog
72
74
  - README.rdoc
73
75
  - test/cat.sh
74
76
  - test/fail.sh