cloudtasker 0.3.0 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +33 -0
- data/Gemfile.lock +19 -2
- data/README.md +149 -14
- data/Rakefile +6 -0
- data/app/controllers/cloudtasker/worker_controller.rb +8 -6
- data/cloudtasker.gemspec +3 -2
- data/docs/BATCH_JOBS.md +27 -2
- data/docs/CRON_JOBS.md +20 -14
- data/exe/cloudtasker +13 -1
- data/lib/cloudtasker.rb +0 -1
- data/lib/cloudtasker/backend/google_cloud_task.rb +41 -8
- data/lib/cloudtasker/backend/memory_task.rb +5 -3
- data/lib/cloudtasker/backend/redis_task.rb +18 -9
- data/lib/cloudtasker/batch/batch_progress.rb +11 -2
- data/lib/cloudtasker/batch/job.rb +24 -9
- data/lib/cloudtasker/cli.rb +6 -5
- data/lib/cloudtasker/cloud_task.rb +4 -2
- data/lib/cloudtasker/config.rb +14 -8
- data/lib/cloudtasker/cron/job.rb +2 -2
- data/lib/cloudtasker/cron/schedule.rb +37 -21
- data/lib/cloudtasker/local_server.rb +44 -22
- data/lib/cloudtasker/redis_client.rb +7 -8
- data/lib/cloudtasker/unique_job/job.rb +2 -2
- data/lib/cloudtasker/version.rb +1 -1
- data/lib/cloudtasker/worker.rb +46 -10
- data/lib/cloudtasker/worker_handler.rb +5 -3
- data/lib/cloudtasker/worker_logger.rb +1 -1
- data/lib/cloudtasker/worker_wrapper.rb +52 -0
- data/lib/tasks/setup_queue.rake +12 -2
- metadata +20 -5
- data/lib/cloudtasker/railtie.rb +0 -10
data/docs/CRON_JOBS.md
CHANGED
@@ -9,7 +9,7 @@ The Cloudtasker cron job extension allows you to register workers to run at fixe
|
|
9
9
|
You can schedule cron jobs by adding the following to your cloudtasker initializer:
|
10
10
|
```ruby
|
11
11
|
# The cron job extension is optional and must be explicitly required
|
12
|
-
require 'cloudtasker/
|
12
|
+
require 'cloudtasker/cron'
|
13
13
|
|
14
14
|
Cloudtasker.configure do |config|
|
15
15
|
# Specify your redis url.
|
@@ -18,18 +18,24 @@ Cloudtasker.configure do |config|
|
|
18
18
|
end
|
19
19
|
|
20
20
|
# Specify all your cron jobs below. This will synchronize your list of cron jobs (cron jobs previously created and not listed below will be removed).
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
21
|
+
unless Rails.env.test?
|
22
|
+
Cloudtasker::Cron::Schedule.load_from_hash!(
|
23
|
+
# Run job every minute
|
24
|
+
some_schedule_name: {
|
25
|
+
worker: 'SomeCronWorker',
|
26
|
+
cron: '* * * * *'
|
27
|
+
},
|
28
|
+
# Run job every hour on the fifteenth minute
|
29
|
+
other_cron_schedule: {
|
30
|
+
worker: 'OtherCronWorker',
|
31
|
+
cron: '15 * * * *',
|
32
|
+
queue: 'critical'
|
33
|
+
args:
|
34
|
+
- 'foo'
|
35
|
+
- 'bar
|
36
|
+
}
|
37
|
+
)
|
38
|
+
end
|
33
39
|
```
|
34
40
|
|
35
41
|
## Using a configuration file
|
@@ -56,7 +62,7 @@ Then register the jobs inside your Cloudtasker initializer this way:
|
|
56
62
|
# ... Cloudtasker configuration ...
|
57
63
|
|
58
64
|
schedule_file = 'config/cloudtasker_cron.yml'
|
59
|
-
if File.exist?(schedule_file)
|
65
|
+
if File.exist?(schedule_file) && !Rails.env.test?
|
60
66
|
Cloudtasker::Cron::Schedule.load_from_hash!(YAML.load_file(schedule_file))
|
61
67
|
end
|
62
68
|
```
|
data/exe/cloudtasker
CHANGED
@@ -3,9 +3,21 @@
|
|
3
3
|
|
4
4
|
require 'bundler/setup'
|
5
5
|
require 'cloudtasker/cli'
|
6
|
+
require 'optparse'
|
7
|
+
|
8
|
+
options = {}
|
9
|
+
OptionParser.new do |opts|
|
10
|
+
opts.banner = 'Usage: cloudtasker [options]'
|
11
|
+
|
12
|
+
opts.on('-q QUEUE', '--queue=QUEUE', 'Queue to process and number of threads. ' \
|
13
|
+
"Examples: '-q critical' | '-q critical,2' | '-q critical,3 -q defaults,2'") do |o|
|
14
|
+
options[:queues] ||= []
|
15
|
+
options[:queues] << o.split(',')
|
16
|
+
end
|
17
|
+
end.parse!
|
6
18
|
|
7
19
|
begin
|
8
|
-
Cloudtasker::CLI.run
|
20
|
+
Cloudtasker::CLI.run(options)
|
9
21
|
rescue StandardError => e
|
10
22
|
raise e if $DEBUG
|
11
23
|
|
data/lib/cloudtasker.rb
CHANGED
@@ -9,15 +9,28 @@ module Cloudtasker
|
|
9
9
|
#
|
10
10
|
# Create the queue configured in Cloudtasker if it does not already exist.
|
11
11
|
#
|
12
|
+
# @param [String] queue_name The relative name of the queue.
|
13
|
+
#
|
12
14
|
# @return [Google::Cloud::Tasks::V2beta3::Queue] The queue
|
13
15
|
#
|
14
|
-
def self.setup_queue
|
15
|
-
|
16
|
+
def self.setup_queue(**opts)
|
17
|
+
# Build full queue path
|
18
|
+
queue_name = opts[:name] || Cloudtasker::Config::DEFAULT_JOB_QUEUE
|
19
|
+
full_queue_name = queue_path(queue_name)
|
20
|
+
|
21
|
+
# Try to get existing queue
|
22
|
+
client.get_queue(full_queue_name)
|
16
23
|
rescue Google::Gax::RetryError
|
24
|
+
# Extract options
|
25
|
+
concurrency = (opts[:concurrency] || Cloudtasker::Config::DEFAULT_QUEUE_CONCURRENCY).to_i
|
26
|
+
retries = (opts[:retries] || Cloudtasker::Config::DEFAULT_QUEUE_RETRIES).to_i
|
27
|
+
|
28
|
+
# Create queue on 'not found' error
|
17
29
|
client.create_queue(
|
18
30
|
client.location_path(config.gcp_project_id, config.gcp_location_id),
|
19
|
-
name:
|
20
|
-
retry_config: { max_attempts:
|
31
|
+
name: full_queue_name,
|
32
|
+
retry_config: { max_attempts: retries },
|
33
|
+
rate_limits: { max_concurrent_dispatches: concurrency }
|
21
34
|
)
|
22
35
|
end
|
23
36
|
|
@@ -42,13 +55,15 @@ module Cloudtasker
|
|
42
55
|
#
|
43
56
|
# Return the fully qualified path for the Cloud Task queue.
|
44
57
|
#
|
58
|
+
# @param [String] queue_name The relative name of the queue.
|
59
|
+
#
|
45
60
|
# @return [String] The queue path.
|
46
61
|
#
|
47
|
-
def self.queue_path
|
62
|
+
def self.queue_path(queue_name)
|
48
63
|
client.queue_path(
|
49
64
|
config.gcp_project_id,
|
50
65
|
config.gcp_location_id,
|
51
|
-
config.
|
66
|
+
[config.gcp_queue_prefix, queue_name].join('-')
|
52
67
|
)
|
53
68
|
end
|
54
69
|
|
@@ -94,8 +109,11 @@ module Cloudtasker
|
|
94
109
|
schedule_time: format_schedule_time(payload[:schedule_time])
|
95
110
|
).compact
|
96
111
|
|
112
|
+
# Extract relative queue name
|
113
|
+
relative_queue = payload.delete(:queue)
|
114
|
+
|
97
115
|
# Create task
|
98
|
-
resp = client.create_task(queue_path, payload)
|
116
|
+
resp = client.create_task(queue_path(relative_queue), payload)
|
99
117
|
resp ? new(resp) : nil
|
100
118
|
rescue Google::Gax::RetryError
|
101
119
|
nil
|
@@ -121,6 +139,20 @@ module Cloudtasker
|
|
121
139
|
@gcp_task = gcp_task
|
122
140
|
end
|
123
141
|
|
142
|
+
#
|
143
|
+
# Return the relative queue (queue name minus prefix) the task is in.
|
144
|
+
#
|
145
|
+
# @return [String] The relative queue name
|
146
|
+
#
|
147
|
+
def relative_queue
|
148
|
+
gcp_task
|
149
|
+
.name
|
150
|
+
.match(%r{/queues/([^/]+)})
|
151
|
+
&.captures
|
152
|
+
&.first
|
153
|
+
&.sub("#{self.class.config.gcp_queue_prefix}-", '')
|
154
|
+
end
|
155
|
+
|
124
156
|
#
|
125
157
|
# Return a hash description of the task.
|
126
158
|
#
|
@@ -131,7 +163,8 @@ module Cloudtasker
|
|
131
163
|
id: gcp_task.name,
|
132
164
|
http_request: gcp_task.to_h[:http_request],
|
133
165
|
schedule_time: gcp_task.to_h.dig(:schedule_time, :seconds).to_i,
|
134
|
-
retries: gcp_task.to_h[:response_count]
|
166
|
+
retries: gcp_task.to_h[:response_count],
|
167
|
+
queue: relative_queue
|
135
168
|
}
|
136
169
|
end
|
137
170
|
end
|
@@ -7,7 +7,7 @@ module Cloudtasker
|
|
7
7
|
# Manage local tasks pushed to memory.
|
8
8
|
# Used for testing.
|
9
9
|
class MemoryTask
|
10
|
-
attr_reader :id, :http_request, :schedule_time
|
10
|
+
attr_reader :id, :http_request, :schedule_time, :queue
|
11
11
|
|
12
12
|
#
|
13
13
|
# Return the task queue. A worker class name
|
@@ -116,10 +116,11 @@ module Cloudtasker
|
|
116
116
|
# @param [Hash] http_request The HTTP request content.
|
117
117
|
# @param [Integer] schedule_time When to run the task (Unix timestamp)
|
118
118
|
#
|
119
|
-
def initialize(id:, http_request:, schedule_time: nil)
|
119
|
+
def initialize(id:, http_request:, schedule_time: nil, queue: nil)
|
120
120
|
@id = id
|
121
121
|
@http_request = http_request
|
122
122
|
@schedule_time = Time.at(schedule_time || 0)
|
123
|
+
@queue = queue
|
123
124
|
end
|
124
125
|
|
125
126
|
#
|
@@ -149,7 +150,8 @@ module Cloudtasker
|
|
149
150
|
{
|
150
151
|
id: id,
|
151
152
|
http_request: http_request,
|
152
|
-
schedule_time: schedule_time.to_i
|
153
|
+
schedule_time: schedule_time.to_i,
|
154
|
+
queue: queue
|
153
155
|
}
|
154
156
|
end
|
155
157
|
|
@@ -1,22 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'cloudtasker/redis_client'
|
4
|
+
require 'net/http'
|
4
5
|
|
5
6
|
module Cloudtasker
|
6
7
|
module Backend
|
7
8
|
# Manage local tasks pushed to Redis
|
8
9
|
class RedisTask
|
9
|
-
attr_reader :id, :http_request, :schedule_time, :retries
|
10
|
+
attr_reader :id, :http_request, :schedule_time, :retries, :queue
|
10
11
|
|
11
12
|
RETRY_INTERVAL = 20 # seconds
|
12
13
|
|
13
14
|
#
|
14
15
|
# Return the cloudtasker redis client
|
15
16
|
#
|
16
|
-
# @return [
|
17
|
+
# @return [Cloudtasker::RedisClient] The cloudtasker redis client..
|
17
18
|
#
|
18
19
|
def self.redis
|
19
|
-
RedisClient
|
20
|
+
@redis ||= RedisClient.new
|
20
21
|
end
|
21
22
|
|
22
23
|
#
|
@@ -47,20 +48,26 @@ module Cloudtasker
|
|
47
48
|
#
|
48
49
|
# Reeturn all tasks ready to process.
|
49
50
|
#
|
51
|
+
# @param [String] queue The queue to retrieve items from.
|
52
|
+
#
|
50
53
|
# @return [Array<Cloudtasker::Backend::RedisTask>] All the tasks ready to process.
|
51
54
|
#
|
52
|
-
def self.ready_to_process
|
53
|
-
all.select { |e| e.schedule_time <= Time.now }
|
55
|
+
def self.ready_to_process(queue = nil)
|
56
|
+
list = all.select { |e| e.schedule_time <= Time.now }
|
57
|
+
list = list.select { |e| e.queue == queue } if queue
|
58
|
+
list
|
54
59
|
end
|
55
60
|
|
56
61
|
#
|
57
62
|
# Retrieve and remove a task from the queue.
|
58
63
|
#
|
64
|
+
# @param [String] queue The queue to retrieve items from.
|
65
|
+
#
|
59
66
|
# @return [Cloudtasker::Backend::RedisTask] A task ready to process.
|
60
67
|
#
|
61
|
-
def self.pop
|
68
|
+
def self.pop(queue = nil)
|
62
69
|
redis.with_lock('cloudtasker/server') do
|
63
|
-
ready_to_process.first&.tap(&:destroy)
|
70
|
+
ready_to_process(queue).first&.tap(&:destroy)
|
64
71
|
end
|
65
72
|
end
|
66
73
|
|
@@ -109,11 +116,12 @@ module Cloudtasker
|
|
109
116
|
# @param [Integer] schedule_time When to run the task (Unix timestamp)
|
110
117
|
# @param [Integer] retries The number of times the job failed.
|
111
118
|
#
|
112
|
-
def initialize(id:, http_request:, schedule_time: nil, retries: 0)
|
119
|
+
def initialize(id:, http_request:, schedule_time: nil, retries: 0, queue: nil)
|
113
120
|
@id = id
|
114
121
|
@http_request = http_request
|
115
122
|
@schedule_time = Time.at(schedule_time || 0)
|
116
123
|
@retries = retries || 0
|
124
|
+
@queue = queue
|
117
125
|
end
|
118
126
|
|
119
127
|
#
|
@@ -135,7 +143,8 @@ module Cloudtasker
|
|
135
143
|
id: id,
|
136
144
|
http_request: http_request,
|
137
145
|
schedule_time: schedule_time.to_i,
|
138
|
-
retries: retries
|
146
|
+
retries: retries,
|
147
|
+
queue: queue
|
139
148
|
}
|
140
149
|
end
|
141
150
|
|
@@ -77,7 +77,16 @@ module Cloudtasker
|
|
77
77
|
# @return [Integer] The number of jobs pending.
|
78
78
|
#
|
79
79
|
def pending
|
80
|
-
total -
|
80
|
+
total - done
|
81
|
+
end
|
82
|
+
|
83
|
+
#
|
84
|
+
# Return the number of jobs completed or dead.
|
85
|
+
#
|
86
|
+
# @return [Integer] The number of jobs done.
|
87
|
+
#
|
88
|
+
def done
|
89
|
+
completed + dead
|
81
90
|
end
|
82
91
|
|
83
92
|
#
|
@@ -88,7 +97,7 @@ module Cloudtasker
|
|
88
97
|
def percent
|
89
98
|
return 0 if total.zero?
|
90
99
|
|
91
|
-
|
100
|
+
(done.to_f / total) * 100
|
92
101
|
end
|
93
102
|
|
94
103
|
#
|
@@ -7,7 +7,8 @@ module Cloudtasker
|
|
7
7
|
attr_reader :worker
|
8
8
|
|
9
9
|
# Key Namespace used for object saved under this class
|
10
|
-
|
10
|
+
JOBS_NAMESPACE = 'jobs'
|
11
|
+
STATES_NAMESPACE = 'states'
|
11
12
|
|
12
13
|
# List of statuses triggering a completion callback
|
13
14
|
COMPLETION_STATUSES = %w[completed dead].freeze
|
@@ -15,10 +16,10 @@ module Cloudtasker
|
|
15
16
|
#
|
16
17
|
# Return the cloudtasker redis client
|
17
18
|
#
|
18
|
-
# @return [
|
19
|
+
# @return [Cloudtasker::RedisClient] The cloudtasker redis client..
|
19
20
|
#
|
20
21
|
def self.redis
|
21
|
-
RedisClient
|
22
|
+
@redis ||= RedisClient.new
|
22
23
|
end
|
23
24
|
|
24
25
|
#
|
@@ -32,7 +33,7 @@ module Cloudtasker
|
|
32
33
|
return nil unless worker_id
|
33
34
|
|
34
35
|
# Retrieve related worker
|
35
|
-
payload = redis.fetch(key(worker_id))
|
36
|
+
payload = redis.fetch(key("#{JOBS_NAMESPACE}/#{worker_id}"))
|
36
37
|
worker = Cloudtasker::Worker.from_hash(payload)
|
37
38
|
return nil unless worker
|
38
39
|
|
@@ -86,7 +87,7 @@ module Cloudtasker
|
|
86
87
|
#
|
87
88
|
# Return the cloudtasker redis client
|
88
89
|
#
|
89
|
-
# @return [
|
90
|
+
# @return [Cloudtasker::RedisClient] The cloudtasker redis client..
|
90
91
|
#
|
91
92
|
def redis
|
92
93
|
self.class.redis
|
@@ -140,7 +141,7 @@ module Cloudtasker
|
|
140
141
|
# @return [String] The worker namespaced id.
|
141
142
|
#
|
142
143
|
def batch_gid
|
143
|
-
key(batch_id)
|
144
|
+
key("#{JOBS_NAMESPACE}/#{batch_id}")
|
144
145
|
end
|
145
146
|
|
146
147
|
#
|
@@ -149,7 +150,7 @@ module Cloudtasker
|
|
149
150
|
# @return [String] The batch state namespaced id.
|
150
151
|
#
|
151
152
|
def batch_state_gid
|
152
|
-
|
153
|
+
key("#{STATES_NAMESPACE}/#{batch_id}")
|
153
154
|
end
|
154
155
|
|
155
156
|
#
|
@@ -179,9 +180,23 @@ module Cloudtasker
|
|
179
180
|
# @return [Array<Cloudtasker::Worker>] The updated list of jobs.
|
180
181
|
#
|
181
182
|
def add(worker_klass, *args)
|
183
|
+
add_to_queue(worker.job_queue, worker_klass, *args)
|
184
|
+
end
|
185
|
+
|
186
|
+
#
|
187
|
+
# Add a worker to the batch using a specific queue.
|
188
|
+
#
|
189
|
+
# @param [String, Symbol] queue The name of the queue
|
190
|
+
# @param [Class] worker_klass The worker class.
|
191
|
+
# @param [Array<any>] *args The worker arguments.
|
192
|
+
#
|
193
|
+
# @return [Array<Cloudtasker::Worker>] The updated list of jobs.
|
194
|
+
#
|
195
|
+
def add_to_queue(queue, worker_klass, *args)
|
182
196
|
jobs << worker_klass.new(
|
183
197
|
job_args: args,
|
184
|
-
job_meta: { key(:parent_id) => batch_id }
|
198
|
+
job_meta: { key(:parent_id) => batch_id },
|
199
|
+
job_queue: queue
|
185
200
|
)
|
186
201
|
end
|
187
202
|
|
@@ -371,7 +386,7 @@ module Cloudtasker
|
|
371
386
|
setup
|
372
387
|
|
373
388
|
# Complete batch
|
374
|
-
complete(:
|
389
|
+
complete(:completed)
|
375
390
|
rescue DeadWorkerError => e
|
376
391
|
complete(:dead)
|
377
392
|
raise(e)
|
data/lib/cloudtasker/cli.rb
CHANGED
@@ -70,7 +70,7 @@ module Cloudtasker
|
|
70
70
|
#
|
71
71
|
# Run the cloudtasker development server.
|
72
72
|
#
|
73
|
-
def run
|
73
|
+
def run(opts = {})
|
74
74
|
boot_system
|
75
75
|
|
76
76
|
# Print banner
|
@@ -90,16 +90,17 @@ module Cloudtasker
|
|
90
90
|
logger.info "[Cloudtasker/Server] Running in #{RUBY_DESCRIPTION}"
|
91
91
|
|
92
92
|
# Wait for signals
|
93
|
-
|
93
|
+
run_server(self_read, opts)
|
94
94
|
end
|
95
95
|
|
96
96
|
#
|
97
|
-
#
|
97
|
+
# Run server and wait for signals.
|
98
98
|
#
|
99
99
|
# @param [IO] read_pipe Where to read signals.
|
100
|
+
# @param [Hash] opts Server options.
|
100
101
|
#
|
101
|
-
def
|
102
|
-
local_server.start
|
102
|
+
def run_server(read_pipe, opts = {})
|
103
|
+
local_server.start(opts)
|
103
104
|
|
104
105
|
while (readable_io = IO.select([read_pipe]))
|
105
106
|
signal = readable_io.first[0].gets.strip
|
@@ -3,7 +3,7 @@
|
|
3
3
|
module Cloudtasker
|
4
4
|
# An interface class to manage tasks on the backend (Cloud Task or Redis)
|
5
5
|
class CloudTask
|
6
|
-
attr_accessor :id, :http_request, :schedule_time, :retries
|
6
|
+
attr_accessor :id, :http_request, :schedule_time, :retries, :queue
|
7
7
|
|
8
8
|
#
|
9
9
|
# The backend to use for cloud tasks.
|
@@ -69,12 +69,14 @@ module Cloudtasker
|
|
69
69
|
# @param [Hash] http_request The content of the http request.
|
70
70
|
# @param [Integer] schedule_time When to run the job (Unix timestamp)
|
71
71
|
# @param [Integer] retries The number of times the job failed.
|
72
|
+
# @param [String] queue The queue the task is in.
|
72
73
|
#
|
73
|
-
def initialize(id:, http_request:, schedule_time: nil, retries: 0)
|
74
|
+
def initialize(id:, http_request:, schedule_time: nil, retries: 0, queue: nil)
|
74
75
|
@id = id
|
75
76
|
@http_request = http_request
|
76
77
|
@schedule_time = schedule_time
|
77
78
|
@retries = retries || 0
|
79
|
+
@queue = queue
|
78
80
|
end
|
79
81
|
|
80
82
|
#
|