perfectsched 0.7.19 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,25 @@
1
+ #
2
+ # PerfectSched
3
+ #
4
+ # Copyright (C) 2012 FURUHASHI Sadayuki
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ module PerfectSched
20
+
21
+ require 'perfectqueue/signal_queue'
22
+ SignalQueue = PerfectQueue::SignalQueue
23
+
24
+ end
25
+
@@ -0,0 +1,46 @@
1
+ #
2
+ # PerfectSched
3
+ #
4
+ # Copyright (C) 2012 FURUHASHI Sadayuki
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ module PerfectSched
20
+
21
+ class Task < ScheduleWithMetadata
22
+ include Model
23
+
24
+ def initialize(client, key, attributes, scheduled_time, task_token)
25
+ super(client, key, attributes)
26
+ @scheduled_time = scheduled_time
27
+ @task_token = task_token
28
+ end
29
+
30
+ attr_reader :scheduled_time
31
+
32
+ def release!(options={})
33
+ @client.release(@task_token, options)
34
+ end
35
+
36
+ def retry!(options={})
37
+ @client.retry(@task_token, options)
38
+ end
39
+
40
+ def finish!(options={})
41
+ @client.finish(@task_token, options)
42
+ end
43
+ end
44
+
45
+ end
46
+
@@ -1,5 +1,3 @@
1
1
  module PerfectSched
2
-
3
- VERSION = '0.7.19'
4
-
2
+ VERSION = "0.8.0"
5
3
  end
@@ -0,0 +1,163 @@
1
+ #
2
+ # PerfectSched
3
+ #
4
+ # Copyright (C) 2012 FURUHASHI Sadayuki
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ module PerfectSched
20
+
21
+ class Worker
22
+ def self.run(runner, config=nil, &block)
23
+ new(runner, config, &block).run
24
+ end
25
+
26
+ def initialize(runner, config=nil, &block)
27
+ # initial logger
28
+ STDERR.sync = true
29
+ @log = DaemonsLogger.new(STDERR)
30
+
31
+ @runner = runner
32
+ block = Proc.new { config } if config
33
+ @config_load_proc = block
34
+ @finished = false
35
+ end
36
+
37
+ def run
38
+ @sig = install_signal_handlers
39
+ begin
40
+ @engine = Engine.new(@runner, load_config)
41
+ begin
42
+ until @finished
43
+ @engine.run
44
+ end
45
+ ensure
46
+ @engine.shutdown
47
+ end
48
+ ensure
49
+ @sig.shutdown
50
+ end
51
+ return nil
52
+ rescue
53
+ @log.error "#{$!.class}: #{$!}"
54
+ $!.backtrace.each {|x| @log.error " #{x}" }
55
+ return nil
56
+ end
57
+
58
+ def stop
59
+ @log.info "stop"
60
+ begin
61
+ @finished = true
62
+ @engine.stop if @engine
63
+ rescue
64
+ @log.error "failed to stop: #{$!}"
65
+ $!.backtrace.each {|bt| @log.warn "\t#{bt}" }
66
+ return false
67
+ end
68
+ return true
69
+ end
70
+
71
+ def restart
72
+ @log.info "Received restart"
73
+ begin
74
+ engine = Engine.new(@runner, load_config)
75
+ current = @engine
76
+ @engine = engine
77
+ current.shutdown
78
+ rescue
79
+ @log.error "failed to restart: #{$!}"
80
+ $!.backtrace.each {|bt| @log.warn "\t#{bt}" }
81
+ return false
82
+ end
83
+ return true
84
+ end
85
+
86
+ def replace(command=[$0]+ARGV)
87
+ @log.info "replace"
88
+ begin
89
+ return if @replaced_pid
90
+ stop
91
+ @replaced_pid = Process.fork do
92
+ exec(*command)
93
+ exit!(127)
94
+ end
95
+ rescue
96
+ @log.error "failed to replace: #{$!}"
97
+ $!.backtrace.each {|bt| @log.warn "\t#{bt}" }
98
+ return false
99
+ end
100
+ self
101
+ end
102
+
103
+ def logrotated
104
+ @log.info "reopen a log file"
105
+ begin
106
+ @log.reopen!
107
+ rescue
108
+ @log.error "failed to restart: #{$!}"
109
+ $!.backtrace.each {|bt| @log.warn "\t#{bt}" }
110
+ return false
111
+ end
112
+ return true
113
+ end
114
+
115
+ private
116
+ def load_config
117
+ raw_config = @config_load_proc.call
118
+ config = {}
119
+ raw_config.each_pair {|k,v| config[k.to_sym] = v }
120
+
121
+ log = DaemonsLogger.new(config[:log] || STDERR)
122
+ if old_log = @log
123
+ old_log.close
124
+ end
125
+ @log = log
126
+
127
+ config[:logger] = log
128
+
129
+ return config
130
+ end
131
+
132
+ def install_signal_handlers(&block)
133
+ SignalQueue.start do |sig|
134
+ sig.trap :TERM do
135
+ stop
136
+ end
137
+ sig.trap :INT do
138
+ stop
139
+ end
140
+
141
+ sig.trap :QUIT do
142
+ stop
143
+ end
144
+
145
+ sig.trap :USR1 do
146
+ restart
147
+ end
148
+
149
+ sig.trap :HUP do
150
+ restart
151
+ end
152
+
153
+ sig.trap :USR2 do
154
+ logrotated
155
+ end
156
+
157
+ trap :CHLD, "SIG_IGN"
158
+ end
159
+ end
160
+ end
161
+
162
+ end
163
+
data/lib/perfectsched.rb CHANGED
@@ -1,4 +1,98 @@
1
- require 'perfectsched/engine'
2
- require 'perfectsched/croncalc'
3
- require 'perfectsched/backend'
4
- require 'perfectsched/version'
1
+ #
2
+ # PerfectSched
3
+ #
4
+ # Copyright (C) 2012 FURUHASHI Sadayuki
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ module PerfectSched
20
+ require 'json'
21
+ require 'thread' # Mutex, CoditionVariable
22
+
23
+ {
24
+ :Application => 'perfectsched/application',
25
+ :Backend => 'perfectsched/backend',
26
+ :BackendHelper => 'perfectsched/backend',
27
+ :BlockingFlag => 'perfectsched/blocking_flag',
28
+ :Client => 'perfectsched/client',
29
+ :DaemonsLogger => 'perfectsched/daemons_logger',
30
+ :Engine => 'perfectsched/engine',
31
+ :Model => 'perfectsched/model',
32
+ :Runner => 'perfectsched/runner',
33
+ :ScheduleCollection => 'perfectsched/schedule_collection',
34
+ :Schedule => 'perfectsched/schedule',
35
+ :ScheduleWithMetadata => 'perfectsched/schedule',
36
+ :ScheduleMetadata => 'perfectsched/schedule_metadata',
37
+ :ScheduleMetadataAccessors => 'perfectsched/schedule_metadata',
38
+ :SignalQueue => 'perfectsched/signal_queue',
39
+ :Task => 'perfectsched/task',
40
+ :Worker => 'perfectsched/worker',
41
+ }.each_pair {|k,v|
42
+ autoload k, File.expand_path(v, File.dirname(__FILE__))
43
+ }
44
+ [
45
+ 'perfectsched/version',
46
+ 'perfectsched/error',
47
+ ].each {|v|
48
+ require File.expand_path(v, File.dirname(__FILE__))
49
+ }
50
+
51
+ require 'cron-spec'
52
+ require 'tzinfo'
53
+
54
+ def self.open(config, &block)
55
+ c = Client.new(config)
56
+ begin
57
+ q = ScheduleCollection.new(c)
58
+ if block
59
+ block.call(q)
60
+ else
61
+ c = nil
62
+ return q
63
+ end
64
+ ensure
65
+ c.close if c
66
+ end
67
+ end
68
+
69
+ def self.cron_time(cron, timestamp, timezone)
70
+ begin
71
+ tab = CronSpec::CronSpecification.new(cron)
72
+ rescue
73
+ raise ArgumentError, "invalid cron format: #{$!}: #{cron.inspect}"
74
+ end
75
+
76
+ begin
77
+ tz = TZInfo::Timezone.get(timezone) if timezone
78
+ rescue
79
+ raise ArgumentError, "unknown timezone: #{$!}: #{timezone.inspect}"
80
+ end
81
+
82
+ ts = (timestamp + 59) / 60 * 60
83
+ while true
84
+ t = Time.at(ts).utc
85
+ t = tz.utc_to_local(t) if tz
86
+ if tab.is_specification_in_effect?(t)
87
+ return ts
88
+ end
89
+ ts += 60
90
+ # FIXME break
91
+ end
92
+ end
93
+
94
+ def self.next_time(cron, before_timestamp, timezone)
95
+ cron_time(cron, before_timestamp+1, timezone)
96
+ end
97
+ end
98
+
@@ -0,0 +1,27 @@
1
+ # encoding: utf-8
2
+ $:.push File.expand_path('../lib', __FILE__)
3
+ require 'perfectsched/version'
4
+
5
+ Gem::Specification.new do |gem|
6
+ gem.name = "perfectsched"
7
+ gem.description = "Highly available distributed cron built on RDBMS"
8
+ gem.homepage = "https://github.com/treasure-data/perfectsched"
9
+ gem.summary = gem.description
10
+ gem.version = PerfectSched::VERSION
11
+ gem.authors = ["Sadayuki Furuhashi"]
12
+ gem.email = "frsyuki@gmail.com"
13
+ gem.has_rdoc = false
14
+ gem.files = `git ls-files`.split("\n")
15
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
+ gem.require_paths = ['lib']
18
+
19
+ gem.add_dependency "cron-spec", [">= 0.1.2", "<= 0.1.2"]
20
+ gem.add_dependency "sequel", "~> 3.26.0"
21
+ gem.add_dependency "tzinfo", "~> 0.3.29"
22
+ gem.add_dependency "perfectqueue", "~> 0.8.0"
23
+ gem.add_development_dependency "rake", "~> 0.9.2"
24
+ gem.add_development_dependency "rspec", "~> 2.10.0"
25
+ gem.add_development_dependency "simplecov", "~> 0.5.4"
26
+ gem.add_development_dependency "sqlite3", "~> 1.3.3"
27
+ end
@@ -0,0 +1,172 @@
1
+
2
+ describe ScheduleCollection do
3
+ before do
4
+ @sc = create_test_sched
5
+ end
6
+
7
+ after do
8
+ @sc.client.close
9
+ end
10
+
11
+ it 'is a ScheduleCollection' do
12
+ @sc.class.should == PerfectSched::ScheduleCollection
13
+ end
14
+
15
+ it 'succeess add' do
16
+ @sc.add('sched01', 't01', {:cron=>'* * * * *', :timezone=>'UTC'})
17
+ end
18
+
19
+ it 'fail duplicated add' do
20
+ @sc.add('sched01', 't01', {:cron=>'* * * * *', :timezone=>'UTC'})
21
+
22
+ lambda {
23
+ @sc.add('sched01', 't01', {:cron=>'* * * * *', :timezone=>'UTC'})
24
+ }.should raise_error AlreadyExistsError
25
+
26
+ @sc['sched01'].delete!
27
+
28
+ @sc.add('sched01', 't01', {:cron=>'* * * * *', :timezone=>'UTC'})
29
+ end
30
+
31
+ it 'acquire' do
32
+ time = 1323820800 # 2011-12-14 00:00:00 UTC
33
+
34
+ s01 = @sc.add('sched01', 't01', :cron=>"* * * * *", :data=>{'k'=>1}, :next_time=>time)
35
+
36
+ t01 = @sc.poll(:alive_time=>120, :now=>time)
37
+ t01.key.should == 'sched01'
38
+ t01.type.should == 't01'
39
+ t01.cron.should == "* * * * *"
40
+ t01.delay.should == 0
41
+ t01.data.should == {'k'=>1}
42
+ t01.scheduled_time.should == Time.at(time).utc
43
+
44
+ t02 = @sc.poll(:alive_time=>120, :now=>time+60)
45
+ t02.should == nil
46
+
47
+ t01.finish!
48
+
49
+ t04 = @sc.poll(:alive_time=>120, :now=>time+60)
50
+ t04.key.should == 'sched01'
51
+ t01.type.should == 't01'
52
+ t04.cron.should == "* * * * *"
53
+ t04.delay.should == 0
54
+ t04.data.should == {'k'=>1}
55
+ t04.scheduled_time.should == Time.at(time+60).utc
56
+ end
57
+
58
+ it 'timezone' do
59
+ time = 1323820800 # 2011-12-14 00:00:00 UTC
60
+
61
+ s01 = @sc.add('sched01', 't01', :cron=>"0 0 * * *", :next_time=>time-60, :timezone=>'UTC')
62
+ #s01.class.should == Schedule
63
+ #s01.key.should == 'sched01'
64
+
65
+ s02 = @sc.add('sched02', 't01', :cron=>"0 0 * * *", :next_time=>time-60, :timezone=>'Asia/Tokyo')
66
+ #s02.class.should == Schedule
67
+ #s02.key.should == 'sched02'
68
+
69
+ t01 = @sc.poll(:alive_time=>86400, :now=>time)
70
+ t01.class.should == Task
71
+ t01.key.should == 'sched01'
72
+ t01.type.should == 't01'
73
+ t01.scheduled_time.should == Time.at(time).utc
74
+
75
+ t02 = @sc.poll(:alive_time=>86400, :now=>time+54000)
76
+ t02.class.should == Task
77
+ t02.key.should == 'sched02'
78
+ t02.type.should == 't01'
79
+ t02.scheduled_time.should == Time.at(time+54000).utc
80
+ end
81
+
82
+ it 'delay' do
83
+ time = 1323820800 # 2011-12-14 00:00:00 UTC
84
+
85
+ s01 = @sc.add('sched01', 't01', :cron=>"0 * * * *", :delay=>30, :next_time=>time, :timezone=>'UTC')
86
+
87
+ t01 = @sc.poll(:alive_time=>86400, :now=>time)
88
+ t01.should == nil
89
+
90
+ t02 = @sc.poll(:alive_time=>86400, :now=>time+30)
91
+ t02.class.should == Task
92
+ t02.key.should == 'sched01'
93
+ t02.type.should == 't01'
94
+ t02.scheduled_time.should == Time.at(time).utc
95
+ t02.delay.should == 30
96
+
97
+ t02.finish!
98
+
99
+ t03 = @sc.poll(:alive_time=>86400, :now=>time+3600)
100
+ t03.should == nil
101
+
102
+ t04 = @sc.poll(:alive_time=>86400, :now=>time+3630)
103
+ t04.class.should == Task
104
+ t04.key.should == 'sched01'
105
+ t04.type.should == 't01'
106
+ t04.scheduled_time.should == Time.at(time+3600).utc
107
+ t04.delay.should == 30
108
+ end
109
+
110
+ it 'invalid cron format' do
111
+ lambda {
112
+ @sc.add('sched01', 't01', :cron=>'???')
113
+ }.should raise_error ArgumentError
114
+
115
+ lambda {
116
+ @sc.add('sched01', 't01', :cron=>'* * * * * *')
117
+ }.should raise_error ArgumentError
118
+ end
119
+
120
+ it 'fail duplicated add' do
121
+ @sc.add('sched01', 't01', :cron=>"0 * * * *")
122
+ lambda {
123
+ @sc.add('sched01', 't01', :cron=>"0 * * * *")
124
+ }.should raise_error AlreadyExistsError
125
+
126
+ @sc['sched01'].delete!
127
+
128
+ @sc.add('sched01', 't01', :cron=>"0 * * * *")
129
+ end
130
+
131
+ it 'list' do
132
+ time = 1323820800 # 2011-12-14 00:00:00 UTC
133
+
134
+ @sc.add('sched01', 't01', :cron=>"0 * * * *", :next_time=>time, :delay=>1)
135
+ @sc.add('sched02', 't02', :cron=>"0 * * * *", :next_time=>time, :delay=>2)
136
+ @sc.add('sched03', 't03', :cron=>"0 * * * *", :next_time=>time, :delay=>3, :next_run_time=>time+3600)
137
+
138
+ a = []
139
+ @sc.list {|s|
140
+ a << s
141
+ }
142
+ a.sort_by! {|s| s.key }
143
+
144
+ s01 = a.shift
145
+ s01.class.should == ScheduleWithMetadata
146
+ s01.key.should == 'sched01'
147
+ s01.type.should == 't01'
148
+ s01.cron.should == '0 * * * *'
149
+ s01.delay.should == 1
150
+ s01.next_time.should == Time.at(time).utc
151
+ s01.next_run_time.should == Time.at(time+1).utc
152
+
153
+ s02 = a.shift
154
+ s02.class.should == ScheduleWithMetadata
155
+ s02.key.should == 'sched02'
156
+ s02.type.should == 't02'
157
+ s02.cron.should == '0 * * * *'
158
+ s02.delay.should == 2
159
+ s02.next_time.should == Time.at(time).utc
160
+ s02.next_run_time.should == Time.at(time+2).utc
161
+
162
+ s03 = a.shift
163
+ s03.class.should == ScheduleWithMetadata
164
+ s03.key.should == 'sched03'
165
+ s03.type.should == 't03'
166
+ s03.cron.should == '0 * * * *'
167
+ s03.delay.should == 3
168
+ s03.next_time.should == Time.at(time).utc
169
+ s03.next_run_time.should == Time.at(time+3600).utc
170
+ end
171
+ end
172
+