rsched 0.2.0 → 0.3.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/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