cloudtasker 0.3.0 → 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.
- 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
|
#
|