perfectsched 0.8.10 → 0.8.11
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.
- checksums.yaml +4 -4
- data/.travis.yml +11 -4
- data/ChangeLog +6 -0
- data/Gemfile +2 -0
- data/README.md +2 -1
- data/circle.yml +3 -0
- data/lib/perfectsched.rb +4 -23
- data/lib/perfectsched/application/decider.rb +1 -1
- data/lib/perfectsched/backend/rdb_compat.rb +5 -4
- data/lib/perfectsched/schedule_metadata.rb +0 -20
- data/lib/perfectsched/version.rb +1 -1
- data/lib/perfectsched/worker.rb +1 -4
- data/perfectsched.gemspec +6 -4
- data/spec/application/base_spec.rb +81 -0
- data/spec/application/decider_spec.rb +54 -0
- data/spec/client_spec.rb +122 -0
- data/spec/engine_spec.rb +82 -0
- data/spec/model_spec.rb +24 -0
- data/spec/perfect_sched_spec.rb +105 -0
- data/spec/rdb_compat_backend_spec.rb +287 -27
- data/spec/runner_spec.rb +23 -0
- data/spec/schedule_collection_spec.rb +63 -63
- data/spec/schedule_spec.rb +66 -0
- data/spec/spec_helper.rb +7 -2
- data/spec/task_spec.rb +51 -0
- data/spec/worker_spec.rb +182 -39
- metadata +52 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6a1e556c19cb6846d4efa60879d267a272fd9d8c
|
4
|
+
data.tar.gz: a8c46704005fd12df81826d6759ab0a9934e8a8f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2f4ca879b0acb729876fddfaa0712694701c65208039e48e93ee0853d7b141e80d16f59bb2c894e38864fa737bfb86c9cdbfa72737e95b22d7b1e009fa570a3f
|
7
|
+
data.tar.gz: d99b81c74ccd32504d4d334259491eabdfb6fdf9db790ee6c6ac0120c9c9678ff678cd256364a7694c84ec74697195f7b74d4e89225fe209f8cd8e114e7250bc
|
data/.travis.yml
CHANGED
@@ -1,8 +1,15 @@
|
|
1
1
|
rvm:
|
2
|
-
-
|
3
|
-
- 2.
|
4
|
-
- 2.1.6
|
5
|
-
- 2.2.2
|
2
|
+
- 2.2.5
|
3
|
+
- 2.3.1
|
6
4
|
- ruby-head
|
7
5
|
|
8
6
|
script: "bundle exec rake spec"
|
7
|
+
|
8
|
+
sudo: false
|
9
|
+
|
10
|
+
matrix:
|
11
|
+
allow_failures:
|
12
|
+
- rvm: ruby-head
|
13
|
+
|
14
|
+
notifications:
|
15
|
+
webhooks: http://td-beda.herokuapp.com/travisci_callback
|
data/ChangeLog
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# PerfectSched
|
2
2
|
|
3
3
|
[](https://travis-ci.org/treasure-data/perfectsched)
|
4
|
+
[](https://coveralls.io/github/treasure-data/perfectsched?branch=master)
|
4
5
|
|
5
6
|
PerfectSched is a highly available distributed cron built on top of RDBMS.
|
6
7
|
|
@@ -85,7 +86,7 @@ PerfectSched.open(config) {|sc|
|
|
85
86
|
:next_time => Time.parse('2013-01-01 00:00:00 +0900').to_i,
|
86
87
|
:data => data,
|
87
88
|
}
|
88
|
-
sc.
|
89
|
+
sc.add("sched-id", "type1", options)
|
89
90
|
}
|
90
91
|
```
|
91
92
|
|
data/circle.yml
ADDED
data/lib/perfectsched.rb
CHANGED
@@ -52,7 +52,7 @@ module PerfectSched
|
|
52
52
|
require File.expand_path(v, File.dirname(__FILE__))
|
53
53
|
}
|
54
54
|
|
55
|
-
require '
|
55
|
+
require 'chrono'
|
56
56
|
require 'tzinfo'
|
57
57
|
|
58
58
|
def self.open(config, &block)
|
@@ -71,28 +71,9 @@ module PerfectSched
|
|
71
71
|
end
|
72
72
|
|
73
73
|
def self.cron_time(cron, timestamp, timezone)
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
raise ArgumentError, "invalid cron format: #{$!}: #{cron.inspect}"
|
78
|
-
end
|
79
|
-
|
80
|
-
begin
|
81
|
-
tz = TZInfo::Timezone.get(timezone) if timezone
|
82
|
-
rescue
|
83
|
-
raise ArgumentError, "unknown timezone: #{$!}: #{timezone.inspect}"
|
84
|
-
end
|
85
|
-
|
86
|
-
ts = (timestamp + 59) / 60 * 60
|
87
|
-
while true
|
88
|
-
t = Time.at(ts).utc
|
89
|
-
t = tz.utc_to_local(t) if tz
|
90
|
-
if tab.is_specification_in_effect?(t)
|
91
|
-
return ts
|
92
|
-
end
|
93
|
-
ts += 60
|
94
|
-
# FIXME break
|
95
|
-
end
|
74
|
+
ts = timestamp - 1 # compatibility
|
75
|
+
t = Time.find_zone!(timezone || 'UTC'.freeze).at(ts)
|
76
|
+
Chrono::NextTime.new(now: t, source: cron).to_time.to_i
|
96
77
|
end
|
97
78
|
|
98
79
|
def self.next_time(cron, before_timestamp, timezone)
|
@@ -40,7 +40,7 @@ module PerfectSched
|
|
40
40
|
begin
|
41
41
|
m = method(type)
|
42
42
|
rescue NameError
|
43
|
-
raise UndefinedDecisionError, "Undefined decision #{type} options=#{
|
43
|
+
raise UndefinedDecisionError, "Undefined decision #{type} options=#{opts.inspect}"
|
44
44
|
end
|
45
45
|
m.call(opts)
|
46
46
|
end
|
@@ -20,6 +20,7 @@ module PerfectSched
|
|
20
20
|
module Backend
|
21
21
|
class RDBCompatBackend
|
22
22
|
include BackendHelper
|
23
|
+
MAX_RETRY = 10
|
23
24
|
|
24
25
|
class Token < Struct.new(:row_id, :scheduled_time, :cron, :delay, :timezone)
|
25
26
|
end
|
@@ -76,13 +77,13 @@ module PerfectSched
|
|
76
77
|
def init_database(options)
|
77
78
|
sql = %[
|
78
79
|
CREATE TABLE IF NOT EXISTS `#{@table}` (
|
79
|
-
id VARCHAR(
|
80
|
+
id VARCHAR(255) NOT NULL,
|
80
81
|
timeout INT NOT NULL,
|
81
82
|
next_time INT NOT NULL,
|
82
83
|
cron VARCHAR(128) NOT NULL,
|
83
84
|
delay INT NOT NULL,
|
84
|
-
data
|
85
|
-
timezone VARCHAR(
|
85
|
+
data LONGBLOB NOT NULL,
|
86
|
+
timezone VARCHAR(255) NULL,
|
86
87
|
PRIMARY KEY (id)
|
87
88
|
);]
|
88
89
|
connect {
|
@@ -97,7 +98,7 @@ module PerfectSched
|
|
97
98
|
raise NotFoundError, "schedule key=#{key} does not exist"
|
98
99
|
end
|
99
100
|
attributes = create_attributes(row)
|
100
|
-
return
|
101
|
+
return ScheduleWithMetadata.new(@client, key, attributes)
|
101
102
|
}
|
102
103
|
end
|
103
104
|
|
@@ -57,24 +57,4 @@ module PerfectSched
|
|
57
57
|
@attributes[:message]
|
58
58
|
end
|
59
59
|
end
|
60
|
-
|
61
|
-
class ScheduleMetadata
|
62
|
-
include Model
|
63
|
-
|
64
|
-
def initialize(client, key, attributes)
|
65
|
-
super(client)
|
66
|
-
@key = key
|
67
|
-
end
|
68
|
-
|
69
|
-
def schedule
|
70
|
-
Schedule.new(@client, @key)
|
71
|
-
end
|
72
|
-
|
73
|
-
def inspect
|
74
|
-
"#<#{self.class} @key=#{@key.inspect} @attributes=#{@attributes.inspect}>"
|
75
|
-
end
|
76
|
-
|
77
|
-
include ScheduleMetadataAccessors
|
78
|
-
end
|
79
|
-
|
80
60
|
end
|
data/lib/perfectsched/version.rb
CHANGED
data/lib/perfectsched/worker.rb
CHANGED
@@ -88,10 +88,7 @@ module PerfectSched
|
|
88
88
|
begin
|
89
89
|
return if @replaced_pid
|
90
90
|
stop
|
91
|
-
@replaced_pid = Process.
|
92
|
-
exec(*command)
|
93
|
-
exit!(127)
|
94
|
-
end
|
91
|
+
@replaced_pid = Process.spawn(*command)
|
95
92
|
rescue
|
96
93
|
@log.error "failed to replace: #{$!}"
|
97
94
|
$!.backtrace.each {|bt| @log.warn "\t#{bt}" }
|
data/perfectsched.gemspec
CHANGED
@@ -16,12 +16,14 @@ Gem::Specification.new do |gem|
|
|
16
16
|
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
17
17
|
gem.require_paths = ['lib']
|
18
18
|
|
19
|
-
gem.
|
19
|
+
gem.required_ruby_version = '>= 2.1'
|
20
|
+
gem.add_dependency "chrono", "~> 0.3.0"
|
20
21
|
gem.add_dependency "sequel", "~> 3.48.0"
|
21
22
|
gem.add_dependency "tzinfo", "~> 1.1"
|
22
|
-
gem.add_dependency "perfectqueue", "~> 0.8.41"
|
23
|
+
gem.add_dependency "perfectqueue", "~>0.9", ">= 0.8.41"
|
23
24
|
gem.add_development_dependency "rake", "~> 0.9.2"
|
24
|
-
gem.add_development_dependency "rspec", "~>
|
25
|
-
gem.add_development_dependency "simplecov", "~> 0.
|
25
|
+
gem.add_development_dependency "rspec", "~> 3.4.0"
|
26
|
+
gem.add_development_dependency "simplecov", "~> 0.10.0"
|
26
27
|
gem.add_development_dependency "sqlite3", "~> 1.3.3"
|
28
|
+
gem.add_development_dependency "mysql2", "~> 0.3.20"
|
27
29
|
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe PerfectSched::Application::Base do
|
4
|
+
describe '.decider=' do
|
5
|
+
it 'defines .decider which returns the decider' do
|
6
|
+
decider_klass = double('decider_klass')
|
7
|
+
klass = PerfectSched::Application::Base
|
8
|
+
allow(klass).to receive(:decider).and_call_original
|
9
|
+
allow(klass).to receive(:decider=).with(decider_klass).and_call_original
|
10
|
+
expect(klass.decider = decider_klass).to eq(decider_klass)
|
11
|
+
expect(klass.decider).to eq(decider_klass)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe '.decider' do
|
16
|
+
it 'returns DefaultDecider' do
|
17
|
+
expect(PerfectSched::Application::Base.decider).to eq(PerfectSched::Application::DefaultDecider)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe '#new' do
|
22
|
+
let (:task){ double('task') }
|
23
|
+
let (:base) { PerfectSched::Application::Base.new(task) }
|
24
|
+
it 'calls super and set decider'do
|
25
|
+
expect(base).to be_an_instance_of(PerfectSched::Application::Base)
|
26
|
+
expect(base.instance_variable_get(:@task)).to eq(task)
|
27
|
+
expect(base.instance_variable_get(:@decider)).to be_an_instance_of(Application::DefaultDecider)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe '#run' do
|
32
|
+
let (:base) { PerfectSched::Application::Base.new(double('task')) }
|
33
|
+
it 'returns nil if before_perform returns false' do
|
34
|
+
allow(base).to receive(:before_perform).and_return(false)
|
35
|
+
expect(base.run).to be_nil
|
36
|
+
end
|
37
|
+
it 'returns nil' do
|
38
|
+
expect(base).to receive(:before_perform).exactly(:once).and_call_original
|
39
|
+
expect(base).to receive(:perform).exactly(:once).and_return(nil)
|
40
|
+
expect(base).to receive(:after_perform).exactly(:once).and_call_original
|
41
|
+
expect(base.run).to be_nil
|
42
|
+
end
|
43
|
+
it 'calls unexpected_error_raised on error' do
|
44
|
+
allow(base).to receive(:before_perform).exactly(:once).and_call_original
|
45
|
+
allow(base).to receive(:perform).exactly(:once) { raise }
|
46
|
+
allow(base).to receive(:decide!).with(:unexpected_error_raised, error: kind_of(Exception)).exactly(:once)
|
47
|
+
expect(base.run).to be_nil
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe '#before_perform' do
|
52
|
+
let (:base) { PerfectSched::Application::Base.new(double('task')) }
|
53
|
+
it 'returns true' do
|
54
|
+
expect(base.before_perform).to be true
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe '#after_perform' do
|
59
|
+
let (:base) { PerfectSched::Application::Base.new(double('task')) }
|
60
|
+
it 'returns nil' do
|
61
|
+
expect(base.after_perform).to be_nil
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe '#decide!' do
|
66
|
+
let (:base) do
|
67
|
+
decider = double('decider')
|
68
|
+
expect(decider).to receive(:decide!).with(:type, :option).exactly(:once)
|
69
|
+
decider_klass = double('decider_klass')
|
70
|
+
allow(decider_klass).to receive(:new).with(kind_of(PerfectSched::Application::Base)).and_return(decider)
|
71
|
+
klass = PerfectSched::Application::Base
|
72
|
+
allow(klass).to receive(:decider).and_call_original
|
73
|
+
allow(klass).to receive(:decider=).with(decider_klass).and_call_original
|
74
|
+
klass.decider = decider_klass
|
75
|
+
klass.new(double('task'))
|
76
|
+
end
|
77
|
+
it 'calls decider.decide' do
|
78
|
+
expect(base.decide!(:type, :option)).to be_nil
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe PerfectSched::Application::UndefinedDecisionError do
|
4
|
+
it { is_expected.to be_an_instance_of(PerfectSched::Application::UndefinedDecisionError) }
|
5
|
+
it { is_expected.to be_a(Exception) }
|
6
|
+
end
|
7
|
+
|
8
|
+
describe PerfectSched::Application::Decider do
|
9
|
+
let (:task){ double('task') }
|
10
|
+
let (:schedules){ double('schedules') }
|
11
|
+
let (:base){ double('base', schedules: schedules, task: task) }
|
12
|
+
let (:decider) { PerfectSched::Application::Decider.new(base) }
|
13
|
+
describe '#new' do
|
14
|
+
it 'returns a decider' do
|
15
|
+
expect(decider).to be_an_instance_of(PerfectSched::Application::Decider)
|
16
|
+
expect(decider.instance_variable_get(:@base)).to eq(base)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe '#schedules' do
|
21
|
+
it 'returns @base.schedules' do
|
22
|
+
expect(decider.schedules).to eq(schedules)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '#task' do
|
27
|
+
let (:decider) do
|
28
|
+
base = double('base')
|
29
|
+
allow(base).to receive(:task).exactly(:once).and_return(task)
|
30
|
+
PerfectSched::Application::Decider.new(base)
|
31
|
+
end
|
32
|
+
it 'calls @base.task' do
|
33
|
+
expect(decider.task).to eq(task)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe '#decide!' do
|
38
|
+
it 'calls the specified method' do
|
39
|
+
opts = double('opts')
|
40
|
+
ret = double('ret')
|
41
|
+
allow(decider).to receive(:foo).exactly(:once).with(opts).and_return(ret)
|
42
|
+
expect(decider.decide!(:foo, opts)).to eq(ret)
|
43
|
+
end
|
44
|
+
it 'raises UndefinedDecisionError on unknown method' do
|
45
|
+
expect{ decider.decide!(:foo, double) }.to raise_error(PerfectSched::Application::UndefinedDecisionError)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe PerfectSched::Application::DefaultDecider do
|
51
|
+
subject { PerfectSched::Application::DefaultDecider.new(nil) }
|
52
|
+
it { is_expected.to be_a(PerfectSched::Application::Decider) }
|
53
|
+
it { is_expected.to be_an_instance_of(PerfectSched::Application::DefaultDecider) }
|
54
|
+
end
|
data/spec/client_spec.rb
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Client do
|
4
|
+
let (:config){ {} }
|
5
|
+
let (:client){ Client.new(config) }
|
6
|
+
let (:ret){ double('ret') }
|
7
|
+
let (:backend){ double('backend') }
|
8
|
+
let (:options){ double('options') }
|
9
|
+
let (:task_token){ double('task_token') }
|
10
|
+
let (:key){ double('key') }
|
11
|
+
before do
|
12
|
+
allow(Backend).to receive(:new_backend) \
|
13
|
+
.with(kind_of(Client), config).and_return(backend)
|
14
|
+
end
|
15
|
+
describe '.new' do
|
16
|
+
subject { client }
|
17
|
+
it {
|
18
|
+
is_expected.to be_an_instance_of(Client) }
|
19
|
+
end
|
20
|
+
describe '#backend' do
|
21
|
+
subject { client.backend }
|
22
|
+
it { is_expected.to eq backend }
|
23
|
+
end
|
24
|
+
describe '#config' do
|
25
|
+
subject { client.config }
|
26
|
+
it { is_expected.to eq config }
|
27
|
+
end
|
28
|
+
describe '#init_database' do
|
29
|
+
subject { client.init_database(options) }
|
30
|
+
before { expect(backend).to receive(:init_database).with(options).and_return(ret) }
|
31
|
+
it { is_expected.to eq ret }
|
32
|
+
end
|
33
|
+
describe '#get_schedule_metadata' do
|
34
|
+
subject { client.get_schedule_metadata(key, options) }
|
35
|
+
before { expect(backend).to receive(:get_schedule_metadata).with(key, options).and_return(ret) }
|
36
|
+
it { is_expected.to eq ret }
|
37
|
+
end
|
38
|
+
describe '#delete' do
|
39
|
+
subject { client.delete(key, options) }
|
40
|
+
before { expect(backend).to receive(:delete).with(key, options).and_return(ret) }
|
41
|
+
it { is_expected.to eq ret }
|
42
|
+
end
|
43
|
+
describe '#modify' do
|
44
|
+
subject { client.modify(key, options) }
|
45
|
+
before { expect(backend).to receive(:modify).with(key, options).and_return(ret) }
|
46
|
+
it { is_expected.to eq ret }
|
47
|
+
end
|
48
|
+
describe '#list' do
|
49
|
+
let (:pr){ proc{} }
|
50
|
+
subject { client.list(key, &pr) }
|
51
|
+
before { expect(backend).to receive(:list).with(key){|*_, &b| expect(b).to eq pr; ret} }
|
52
|
+
it { is_expected.to eq ret }
|
53
|
+
end
|
54
|
+
describe '#acquire' do
|
55
|
+
let (:alive_time){ double('alive_time') }
|
56
|
+
let (:max_acquire){ double('max_acquire') }
|
57
|
+
subject { client.acquire(options) }
|
58
|
+
before { expect(backend).to receive(:acquire).with(alive_time, max_acquire, options).and_return(ret) }
|
59
|
+
context 'options are given' do
|
60
|
+
let (:options){ {alive_time: alive_time, max_acquire: max_acquire} }
|
61
|
+
it { is_expected.to eq ret }
|
62
|
+
end
|
63
|
+
context 'alive_time options is not given' do
|
64
|
+
let (:max_acquire){ 1 }
|
65
|
+
let (:options){ {} }
|
66
|
+
let (:config){ {alive_time: alive_time} }
|
67
|
+
it { is_expected.to eq ret }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
describe '#release' do
|
71
|
+
let (:alive_time){ double('alive_time') }
|
72
|
+
subject { client.release(task_token, options) }
|
73
|
+
before { expect(backend).to receive(:release).with(task_token, alive_time, options).and_return(ret) }
|
74
|
+
context 'alive_time options is given' do
|
75
|
+
let (:options){ {alive_time: alive_time} }
|
76
|
+
it { is_expected.to eq ret }
|
77
|
+
end
|
78
|
+
context 'alive_time options is not given' do
|
79
|
+
let (:options){ {} }
|
80
|
+
let (:config){ {alive_time: alive_time} }
|
81
|
+
it { is_expected.to eq ret }
|
82
|
+
end
|
83
|
+
end
|
84
|
+
describe '#heartbeat' do
|
85
|
+
let (:alive_time){ double('alive_time') }
|
86
|
+
subject { client.heartbeat(task_token, options) }
|
87
|
+
before { expect(backend).to receive(:heartbeat).with(task_token, alive_time, options).and_return(ret) }
|
88
|
+
context 'alive_time options is given' do
|
89
|
+
let (:options){ {alive_time: alive_time} }
|
90
|
+
it { is_expected.to eq ret }
|
91
|
+
end
|
92
|
+
context 'alive_time options is not given' do
|
93
|
+
let (:options){ {} }
|
94
|
+
let (:config){ {alive_time: alive_time} }
|
95
|
+
it { is_expected.to eq ret }
|
96
|
+
end
|
97
|
+
end
|
98
|
+
describe '#retry' do
|
99
|
+
let (:retry_wait){ double('retry_wait') }
|
100
|
+
subject { client.retry(task_token, options) }
|
101
|
+
before { expect(backend).to receive(:heartbeat).with(task_token, retry_wait, options).and_return(ret) }
|
102
|
+
context 'retry_wait options is given' do
|
103
|
+
let (:options){ {retry_wait: retry_wait} }
|
104
|
+
it { is_expected.to eq ret }
|
105
|
+
end
|
106
|
+
context 'retry_wait options is not given' do
|
107
|
+
let (:options){ {} }
|
108
|
+
let (:config){ {retry_wait: retry_wait} }
|
109
|
+
it { is_expected.to eq ret }
|
110
|
+
end
|
111
|
+
end
|
112
|
+
describe '#finish' do
|
113
|
+
subject { client.finish(task_token, options) }
|
114
|
+
before { expect(backend).to receive(:finish).with(task_token, options).and_return(ret) }
|
115
|
+
it { is_expected.to eq ret }
|
116
|
+
end
|
117
|
+
describe '#close' do
|
118
|
+
subject { client.close }
|
119
|
+
before { expect(backend).to receive(:close).with(no_args).and_return(ret) }
|
120
|
+
it { is_expected.to eq ret }
|
121
|
+
end
|
122
|
+
end
|