perfectsched 0.7.19 → 0.8.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,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
+