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.
- data/.gitignore +6 -0
- data/ChangeLog +0 -36
- data/Gemfile +3 -0
- data/README.md +219 -0
- data/Rakefile +19 -0
- data/lib/perfectsched/application.rb +25 -0
- data/lib/perfectsched/backend/rdb_compat.rb +254 -0
- data/lib/perfectsched/backend.rb +44 -82
- data/lib/perfectsched/blocking_flag.rb +25 -0
- data/lib/perfectsched/client.rb +129 -0
- data/lib/perfectsched/command/perfectsched.rb +103 -290
- data/lib/perfectsched/daemons_logger.rb +24 -0
- data/lib/perfectsched/engine.rb +54 -54
- data/lib/perfectsched/error.rb +43 -0
- data/lib/perfectsched/model.rb +37 -0
- data/lib/perfectsched/runner.rb +35 -0
- data/lib/perfectsched/schedule.rb +65 -0
- data/lib/perfectsched/schedule_collection.rb +62 -0
- data/lib/perfectsched/schedule_metadata.rb +76 -0
- data/lib/perfectsched/signal_queue.rb +25 -0
- data/lib/perfectsched/task.rb +46 -0
- data/lib/perfectsched/version.rb +1 -3
- data/lib/perfectsched/worker.rb +163 -0
- data/lib/perfectsched.rb +98 -4
- data/perfectsched.gemspec +27 -0
- data/spec/schedule_collection_spec.rb +172 -0
- data/spec/spec_helper.rb +46 -0
- data/spec/worker_spec.rb +51 -0
- metadata +98 -52
- checksums.yaml +0 -7
- data/README.rdoc +0 -137
- data/lib/perfectsched/backend/null.rb +0 -45
- data/lib/perfectsched/backend/rdb.rb +0 -165
- data/lib/perfectsched/backend/simpledb.rb +0 -174
- data/lib/perfectsched/croncalc.rb +0 -29
- data/test/backend_test.rb +0 -217
- data/test/test_helper.rb +0 -19
data/.gitignore
ADDED
data/ChangeLog
CHANGED
@@ -1,39 +1,3 @@
|
|
1
|
-
== 2016-08-02 version 0.7.19
|
2
|
-
|
3
|
-
* re-package
|
4
|
-
|
5
|
-
== 2016-08-02 version 0.7.17
|
6
|
-
|
7
|
-
* Upgraded perfectqueue v0.8.x
|
8
|
-
* Upgraded sequel v3.48.0
|
9
|
-
* Upgraded tzinfo v1.2.2
|
10
|
-
|
11
|
-
|
12
|
-
== 2014-10-08 version 0.7.16
|
13
|
-
|
14
|
-
* Upgraded perfectqueue v0.7.31
|
15
|
-
* Upgraded sequel v3.48.0
|
16
|
-
|
17
|
-
|
18
|
-
== 2014-06-10 version 0.7.15
|
19
|
-
|
20
|
-
* RDBBackend: fixed modify_checked to update timeout and next_time
|
21
|
-
|
22
|
-
|
23
|
-
== 2014-04-25 version 0.7.14
|
24
|
-
|
25
|
-
* RDBBackend: fixed undefined local variable: 'url' to '@uri'
|
26
|
-
|
27
|
-
|
28
|
-
== 2014-04-25 version 0.7.13
|
29
|
-
|
30
|
-
* RDBBackend: fixed NoMethodError for @url
|
31
|
-
|
32
|
-
|
33
|
-
== 2012-02-21 version 0.7.12
|
34
|
-
|
35
|
-
* RDBBackend: added MySQL's SSL support
|
36
|
-
|
37
1
|
|
38
2
|
== 2012-02-21 version 0.7.10
|
39
3
|
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,219 @@
|
|
1
|
+
# PerfectSched
|
2
|
+
|
3
|
+
PerfectSched is a highly available distributed cron built on top of RDBMS.
|
4
|
+
|
5
|
+
It provides at-least-once semantics; Even if a worker node fails during process a task, the task is retried by another worker.
|
6
|
+
|
7
|
+
PerfectSched also guarantees that only one worker server processes a task if the server is alive.
|
8
|
+
|
9
|
+
All you have to consider is implementing idempotent worker programs. It's recommended to use [PerfectQueue](https://github.com/treasure-data/perfectqueue) with PerfectSched.
|
10
|
+
|
11
|
+
|
12
|
+
## API overview
|
13
|
+
|
14
|
+
```
|
15
|
+
# open a schedule collection
|
16
|
+
PerfectSched.open(config, &block) #=> #<ScheduleCollection>
|
17
|
+
|
18
|
+
# add a schedule
|
19
|
+
ScheduleCollection#add(task_id, type, options)
|
20
|
+
|
21
|
+
# poll a scheduled task
|
22
|
+
# (you don't have to use this method directly. see following sections)
|
23
|
+
ScheduleCollection#poll #=> #<Task>
|
24
|
+
|
25
|
+
# get data associated with a task
|
26
|
+
Task#data #=> #<Hash>
|
27
|
+
|
28
|
+
# finish a task
|
29
|
+
Task#finish!
|
30
|
+
|
31
|
+
# retry a task
|
32
|
+
Task#retry!
|
33
|
+
|
34
|
+
# create a schedule reference
|
35
|
+
ScheduleCollection#[](key) #=> #<Schedule>
|
36
|
+
|
37
|
+
# chack the existance of the schedule
|
38
|
+
Schedule#exists?
|
39
|
+
|
40
|
+
# delete a schedule
|
41
|
+
Schedule#delete!
|
42
|
+
```
|
43
|
+
|
44
|
+
### Error classes
|
45
|
+
|
46
|
+
```
|
47
|
+
ScheduleError < StandardError
|
48
|
+
|
49
|
+
##
|
50
|
+
# Workers may get these errors:
|
51
|
+
#
|
52
|
+
|
53
|
+
AlreadyFinishedError < ScheduleError
|
54
|
+
|
55
|
+
NotFoundError < ScheduleError
|
56
|
+
|
57
|
+
PreemptedError < ScheduleError
|
58
|
+
|
59
|
+
ProcessStopError < RuntimeError
|
60
|
+
|
61
|
+
##
|
62
|
+
# Client or other situation:
|
63
|
+
#
|
64
|
+
|
65
|
+
ConfigError < RuntimeError
|
66
|
+
|
67
|
+
AlreadyExistsError < ScheduleError
|
68
|
+
|
69
|
+
NotSupportedError < ScheduleError
|
70
|
+
```
|
71
|
+
|
72
|
+
|
73
|
+
### Example
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
# submit a task
|
77
|
+
PerfectSched.open(config) {|sc|
|
78
|
+
data = {'key'=>"value"}
|
79
|
+
options = {
|
80
|
+
:cron => '0 * * * *',
|
81
|
+
:delay => 30,
|
82
|
+
:timezone => 'Asia/Tokyo',
|
83
|
+
:next_time => Time.parse('2013-01-01 00:00:00 +0900').to_i,
|
84
|
+
:data => data,
|
85
|
+
}
|
86
|
+
sc.submit("sched-id", "type1", options)
|
87
|
+
}
|
88
|
+
```
|
89
|
+
|
90
|
+
|
91
|
+
## Writing a worker application
|
92
|
+
|
93
|
+
### 1. Implement PerfectSched::Application::Base
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
class TestHandler < PerfectSched::Application::Base
|
97
|
+
# implement run method
|
98
|
+
def run
|
99
|
+
# do something ...
|
100
|
+
puts "acquired task: #{task.inspect}"
|
101
|
+
|
102
|
+
# call task.finish!, task.retry! or task.release!
|
103
|
+
task.finish!
|
104
|
+
end
|
105
|
+
end
|
106
|
+
```
|
107
|
+
|
108
|
+
### 2. Implement PerfectSched::Application::Dispatch
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
class Dispatch < PerfectSched::Application::Dispatch
|
112
|
+
# describe routing
|
113
|
+
route "type1" => TestHandler
|
114
|
+
route /^regexp-.*$/ => :TestHandler # String or Regexp => Class or Symbol
|
115
|
+
end
|
116
|
+
```
|
117
|
+
|
118
|
+
### 3. Run the worker
|
119
|
+
|
120
|
+
In a launcher script or rake file:
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
system('perfectsched run -I. -rapp/schedules/dispatch Dispatch')
|
124
|
+
```
|
125
|
+
|
126
|
+
or:
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
require 'perfectsched'
|
130
|
+
require 'app/schedules/dispatch'
|
131
|
+
|
132
|
+
PerfectSched::Worker.run(Dispatch) {
|
133
|
+
# this method is called when the worker process is restarted
|
134
|
+
raw = File.read('config/perfectsched.yml')
|
135
|
+
yml = YAJL.load(raw)
|
136
|
+
yml[ENV['RAILS_ENV'] || 'development']
|
137
|
+
}
|
138
|
+
```
|
139
|
+
|
140
|
+
### Signal handlers
|
141
|
+
|
142
|
+
- **TERM**,**INT**,**QUIT:** shutdown
|
143
|
+
- **USR1**,**HUP:** restart
|
144
|
+
- **USR2:** reopen log files
|
145
|
+
|
146
|
+
## Configuration
|
147
|
+
|
148
|
+
- **type:** backend type (required; see following sections)
|
149
|
+
- **log:** log file path (default: use stderr)
|
150
|
+
- **poll\_interval:** interval to poll tasks in seconds (default: 1.0 sec)
|
151
|
+
- **timezone:** default timezone (default: 'UTC')
|
152
|
+
- **alive\_time:** duration to continue a heartbeat request (default: 300 sec)
|
153
|
+
- **retry\_wait:** duration to retry a retried task (default: 300 sec)
|
154
|
+
|
155
|
+
## Backend types
|
156
|
+
|
157
|
+
### rdb\_compat
|
158
|
+
|
159
|
+
additional configuration:
|
160
|
+
|
161
|
+
- **url:** URL to the RDBMS (example: 'mysql://user:password@host:port/database')
|
162
|
+
- **table:** name of the table to use
|
163
|
+
|
164
|
+
### rdb
|
165
|
+
|
166
|
+
Not implemented yet.
|
167
|
+
|
168
|
+
|
169
|
+
## Command line management tool
|
170
|
+
|
171
|
+
```
|
172
|
+
Usage: perfectsched [options] <command>
|
173
|
+
|
174
|
+
commands:
|
175
|
+
list Show list of registered schedules
|
176
|
+
add <key> <type> <cron> <data> Register a new schedule
|
177
|
+
delete <key> Delete a registered schedule
|
178
|
+
run <class> Run a worker process
|
179
|
+
init Initialize a backend database
|
180
|
+
|
181
|
+
options:
|
182
|
+
-e, --environment ENV Framework environment (default: development)
|
183
|
+
-c, --config PATH.yml Path to a configuration file (default: config/perfectsched.yml)
|
184
|
+
|
185
|
+
options for add:
|
186
|
+
-d, --delay SEC Delay time before running a schedule (default: 0)
|
187
|
+
-t, --timezone NAME Set timezone (default: UTC)
|
188
|
+
-s, --start UNIXTIME Set the first schedule time (default: now)
|
189
|
+
-a, --at UNIXTIME Set the first run time (default: start+delay)
|
190
|
+
|
191
|
+
options for run:
|
192
|
+
-I, --include PATH Add $LOAD_PATH directory
|
193
|
+
-r, --require PATH Require files before starting
|
194
|
+
```
|
195
|
+
|
196
|
+
### initializing a database
|
197
|
+
|
198
|
+
# assume that the config/perfectsched.yml exists
|
199
|
+
$ perfectsched init
|
200
|
+
|
201
|
+
### submitting a task
|
202
|
+
|
203
|
+
$ perfectsched add s1 user_task '* * * * *' '{}'
|
204
|
+
|
205
|
+
### listing tasks
|
206
|
+
|
207
|
+
$ perfectsched list
|
208
|
+
key type cron delay timezone next_time next_run_time data
|
209
|
+
s1 user_task * * * * * 0 UTC 2012-05-18 22:04:00 UTC 2012-05-18 22:04:00 UTC {}
|
210
|
+
1 entries.
|
211
|
+
|
212
|
+
### delete a schedule
|
213
|
+
|
214
|
+
$ perfectsched delete s1
|
215
|
+
|
216
|
+
### running a worker
|
217
|
+
|
218
|
+
$ perfectsched run -I. -Ilib -rconfig/boot.rb -rapps/schedules/schedule_dispatch.rb ScheduleDispatch
|
219
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler::GemHelper.install_tasks
|
3
|
+
|
4
|
+
require 'rspec/core'
|
5
|
+
require 'rspec/core/rake_task'
|
6
|
+
|
7
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
8
|
+
t.rspec_opts = ["-c", "-f progress", "-r ./spec/spec_helper.rb"]
|
9
|
+
t.pattern = 'spec/**/*_spec.rb'
|
10
|
+
t.verbose = true
|
11
|
+
end
|
12
|
+
|
13
|
+
task :coverage do |t|
|
14
|
+
ENV['SIMPLE_COV'] = '1'
|
15
|
+
Rake::Task["spec"].invoke
|
16
|
+
end
|
17
|
+
|
18
|
+
task :default => :build
|
19
|
+
|
@@ -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/application'
|
22
|
+
Application = PerfectQueue::Application
|
23
|
+
|
24
|
+
end
|
25
|
+
|
@@ -0,0 +1,254 @@
|
|
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
|
+
module Backend
|
21
|
+
class RDBCompatBackend
|
22
|
+
include BackendHelper
|
23
|
+
|
24
|
+
class Token < Struct.new(:row_id, :scheduled_time, :cron, :delay, :timezone)
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(client, config)
|
28
|
+
super
|
29
|
+
|
30
|
+
require 'sequel'
|
31
|
+
url = config[:url]
|
32
|
+
unless url
|
33
|
+
raise ConfigError, "url option is required for the rdb_compat backend"
|
34
|
+
end
|
35
|
+
|
36
|
+
@table = config[:table]
|
37
|
+
unless @table
|
38
|
+
raise ConfigError, "table option is required for the rdb_compat backend"
|
39
|
+
end
|
40
|
+
|
41
|
+
#password = config[:password]
|
42
|
+
#user = config[:user]
|
43
|
+
@db = Sequel.connect(url, :max_connections=>1)
|
44
|
+
@mutex = Mutex.new
|
45
|
+
|
46
|
+
connect {
|
47
|
+
# connection test
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
MAX_SELECT_ROW = 4
|
52
|
+
|
53
|
+
attr_reader :db
|
54
|
+
|
55
|
+
def init_database(options)
|
56
|
+
sql = %[
|
57
|
+
CREATE TABLE IF NOT EXISTS `test_scheds` (
|
58
|
+
id VARCHAR(256) NOT NULL,
|
59
|
+
timeout INT NOT NULL,
|
60
|
+
next_time INT NOT NULL,
|
61
|
+
cron VARCHAR(128) NOT NULL,
|
62
|
+
delay INT NOT NULL,
|
63
|
+
data BLOB NOT NULL,
|
64
|
+
timezone VARCHAR(256) NULL,
|
65
|
+
PRIMARY KEY (id)
|
66
|
+
);]
|
67
|
+
connect {
|
68
|
+
@db.run sql
|
69
|
+
}
|
70
|
+
end
|
71
|
+
|
72
|
+
def get_schedule_metadata(key, options={})
|
73
|
+
connect {
|
74
|
+
row = @db.fetch("SELECT id, timeout, next_time, cron, delay, data, timezone FROM `#{@table}` LIMIT 1").first
|
75
|
+
unless row
|
76
|
+
raise NotFoundError, "schedule key=#{key} does not exist"
|
77
|
+
end
|
78
|
+
attributes = create_attributes(row)
|
79
|
+
return ScheduleMetadata.new(@client, key, attributes)
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
def list(options, &block)
|
84
|
+
connect {
|
85
|
+
@db.fetch("SELECT id, timeout, next_time, cron, delay, data, timezone FROM `#{@table}` ORDER BY timeout ASC") {|row|
|
86
|
+
attributes = create_attributes(row)
|
87
|
+
sched = ScheduleWithMetadata.new(@client, row[:id], attributes)
|
88
|
+
yield sched
|
89
|
+
}
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
def add(key, type, cron, delay, timezone, data, next_time, next_run_time, options)
|
94
|
+
data = data.dup
|
95
|
+
data[:type] = type
|
96
|
+
connect {
|
97
|
+
begin
|
98
|
+
n = @db["INSERT INTO `#{@table}` (id, timeout, next_time, cron, delay, data, timezone) VALUES (?, ?, ?, ?, ?, ?, ?);", key, next_run_time, next_time, cron, delay, data.to_json, timezone].insert
|
99
|
+
return Schedule.new(@client, key)
|
100
|
+
rescue Sequel::DatabaseError
|
101
|
+
raise AlreadyExistsError, "schedule key=#{key} already exists"
|
102
|
+
end
|
103
|
+
}
|
104
|
+
end
|
105
|
+
|
106
|
+
def delete(key, options)
|
107
|
+
connect {
|
108
|
+
n = @db["DELETE FROM `#{@table}` WHERE id=?;", key].delete
|
109
|
+
if n <= 0
|
110
|
+
raise NotFoundError, "schedule key=#{key} does no exist"
|
111
|
+
end
|
112
|
+
}
|
113
|
+
end
|
114
|
+
|
115
|
+
def modify(key, options)
|
116
|
+
ks = []
|
117
|
+
vs = []
|
118
|
+
[:cron, :delay, :timezone].each {|k|
|
119
|
+
# TODO type and data are not supported
|
120
|
+
if v = options[k]
|
121
|
+
ks << k
|
122
|
+
vs << v
|
123
|
+
end
|
124
|
+
}
|
125
|
+
return nil if ks.empty?
|
126
|
+
|
127
|
+
sql = "UPDATE `#{@table}` SET "
|
128
|
+
sql << ks.map {|k| "#{k}=?" }.join(', ')
|
129
|
+
sql << " WHERE id=?"
|
130
|
+
|
131
|
+
args = [sql].concat(vs)
|
132
|
+
args << key
|
133
|
+
|
134
|
+
connect {
|
135
|
+
n = @db[*args].update
|
136
|
+
if n <= 0
|
137
|
+
raise NotFoundError, "schedule key=#{key} does not exist"
|
138
|
+
end
|
139
|
+
}
|
140
|
+
end
|
141
|
+
|
142
|
+
def acquire(alive_time, max_acquire, options)
|
143
|
+
now = (options[:now] || Time.now).to_i
|
144
|
+
next_timeout = now + alive_time
|
145
|
+
|
146
|
+
connect {
|
147
|
+
while true
|
148
|
+
rows = 0
|
149
|
+
@db.fetch("SELECT id, timeout, next_time, cron, delay, data, timezone FROM `#{@table}` WHERE timeout <= ? ORDER BY timeout ASC LIMIT #{MAX_SELECT_ROW};", now) {|row|
|
150
|
+
|
151
|
+
n = @db["UPDATE `#{@table}` SET timeout=? WHERE id=? AND timeout=?;", next_timeout, row[:id], row[:timeout]].update
|
152
|
+
if n > 0
|
153
|
+
scheduled_time = Time.at(row[:next_time]).utc
|
154
|
+
attributes = create_attributes(row)
|
155
|
+
task_token = Token.new(row[:id], row[:next_time], attributes[:cron], attributes[:delay], attributes[:timezone])
|
156
|
+
task = Task.new(@client, row[:id], attributes, scheduled_time, task_token)
|
157
|
+
return [task]
|
158
|
+
end
|
159
|
+
|
160
|
+
rows += 1
|
161
|
+
}
|
162
|
+
if rows < MAX_SELECT_ROW
|
163
|
+
return nil
|
164
|
+
end
|
165
|
+
end
|
166
|
+
}
|
167
|
+
end
|
168
|
+
|
169
|
+
def heartbeat(task_token, alive_time, options)
|
170
|
+
now = (options[:now] || Time.now).to_i
|
171
|
+
row_id = task_token.row_id
|
172
|
+
scheduled_time = task_token.scheduled_time
|
173
|
+
next_run_time = now + alive_time
|
174
|
+
|
175
|
+
connect {
|
176
|
+
n = @db["UPDATE `#{@table}` SET timeout=? WHERE id=? AND next_time=?;", next_run_time, row_id, scheduled_time].update
|
177
|
+
if n < 0
|
178
|
+
raise AlreadyFinishedError, "task time=#{Time.at(scheduled_time).utc} is already finished"
|
179
|
+
end
|
180
|
+
}
|
181
|
+
end
|
182
|
+
|
183
|
+
def finish(task_token, options)
|
184
|
+
row_id = task_token.row_id
|
185
|
+
scheduled_time = task_token.scheduled_time
|
186
|
+
next_time = PerfectSched.next_time(task_token.cron, scheduled_time, task_token.timezone)
|
187
|
+
next_run_time = next_time + task_token.delay
|
188
|
+
|
189
|
+
connect {
|
190
|
+
n = @db["UPDATE `#{@table}` SET timeout=?, next_time=? WHERE id=? AND next_time=?;", next_run_time, next_time, row_id, scheduled_time].update
|
191
|
+
if n < 0
|
192
|
+
raise AlreadyFinishedError, "task time=#{Time.at(scheduled_time).utc} is already finished"
|
193
|
+
end
|
194
|
+
}
|
195
|
+
end
|
196
|
+
|
197
|
+
protected
|
198
|
+
def connect(&block)
|
199
|
+
@mutex.synchronize do
|
200
|
+
retry_count = 0
|
201
|
+
begin
|
202
|
+
block.call
|
203
|
+
rescue
|
204
|
+
# workaround for "Mysql2::Error: Deadlock found when trying to get lock; try restarting transaction" error
|
205
|
+
if $!.to_s.include?('try restarting transaction')
|
206
|
+
err = ([$!] + $!.backtrace.map {|bt| " #{bt}" }).join("\n")
|
207
|
+
retry_count += 1
|
208
|
+
if retry_count < MAX_RETRY
|
209
|
+
STDERR.puts err + "\n retrying."
|
210
|
+
sleep 0.5
|
211
|
+
retry
|
212
|
+
else
|
213
|
+
STDERR.puts err + "\n abort."
|
214
|
+
end
|
215
|
+
end
|
216
|
+
raise
|
217
|
+
ensure
|
218
|
+
@db.disconnect
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def create_attributes(row)
|
224
|
+
timezone = row[:timezone] || 'UTC'
|
225
|
+
delay = row[:delay] || 0
|
226
|
+
cron = row[:cron]
|
227
|
+
next_time = Time.at(row[:next_time]).utc
|
228
|
+
next_run_time = Time.at(row[:timeout]).utc
|
229
|
+
|
230
|
+
begin
|
231
|
+
data = JSON.parse(row[:data] || '{}')
|
232
|
+
rescue
|
233
|
+
data = {}
|
234
|
+
end
|
235
|
+
|
236
|
+
type = data.delete('type') || ''
|
237
|
+
|
238
|
+
attributes = {
|
239
|
+
:timezone => timezone,
|
240
|
+
:delay => delay,
|
241
|
+
:cron => cron,
|
242
|
+
:data => data,
|
243
|
+
:next_time => next_time,
|
244
|
+
:next_run_time => next_run_time,
|
245
|
+
:type => type,
|
246
|
+
:message => nil, # not supported
|
247
|
+
:node => nil, # not supported
|
248
|
+
}
|
249
|
+
end
|
250
|
+
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|