cloudtasker 0.1.0 → 0.6.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/.gitignore +4 -0
- data/.rubocop.yml +5 -0
- data/.travis.yml +10 -1
- data/Appraisals +25 -0
- data/CHANGELOG.md +25 -0
- data/Gemfile.lock +37 -4
- data/README.md +573 -6
- data/Rakefile +6 -0
- data/app/controllers/cloudtasker/application_controller.rb +2 -0
- data/app/controllers/cloudtasker/worker_controller.rb +24 -2
- data/cloudtasker.gemspec +7 -3
- data/docs/BATCH_JOBS.md +66 -0
- data/docs/CRON_JOBS.md +63 -0
- data/docs/UNIQUE_JOBS.md +127 -0
- data/exe/cloudtasker +15 -0
- data/gemfiles/.bundle/config +2 -0
- data/gemfiles/google_cloud_tasks_1.0.gemfile +9 -0
- data/gemfiles/google_cloud_tasks_1.0.gemfile.lock +263 -0
- data/gemfiles/google_cloud_tasks_1.1.gemfile +9 -0
- data/gemfiles/google_cloud_tasks_1.1.gemfile.lock +263 -0
- data/gemfiles/google_cloud_tasks_1.2.gemfile +9 -0
- data/gemfiles/google_cloud_tasks_1.2.gemfile.lock +263 -0
- data/gemfiles/google_cloud_tasks_1.3.gemfile +9 -0
- data/gemfiles/google_cloud_tasks_1.3.gemfile.lock +264 -0
- data/gemfiles/rails_4.0.gemfile +10 -0
- data/gemfiles/rails_4.1.gemfile +9 -0
- data/gemfiles/rails_4.2.gemfile +9 -0
- data/gemfiles/rails_5.0.gemfile +9 -0
- data/gemfiles/rails_5.1.gemfile +9 -0
- data/gemfiles/rails_5.2.gemfile +9 -0
- data/gemfiles/rails_5.2.gemfile.lock +247 -0
- data/gemfiles/rails_6.0.gemfile +9 -0
- data/gemfiles/rails_6.0.gemfile.lock +263 -0
- data/lib/cloudtasker.rb +21 -1
- data/lib/cloudtasker/backend/google_cloud_task.rb +139 -0
- data/lib/cloudtasker/backend/memory_task.rb +190 -0
- data/lib/cloudtasker/backend/redis_task.rb +249 -0
- data/lib/cloudtasker/batch/batch_progress.rb +19 -1
- data/lib/cloudtasker/batch/job.rb +88 -23
- data/lib/cloudtasker/batch/middleware.rb +0 -1
- data/lib/cloudtasker/cli.rb +194 -0
- data/lib/cloudtasker/cloud_task.rb +91 -0
- data/lib/cloudtasker/config.rb +64 -2
- data/lib/cloudtasker/cron/job.rb +6 -3
- data/lib/cloudtasker/cron/middleware.rb +0 -1
- data/lib/cloudtasker/cron/schedule.rb +73 -13
- data/lib/cloudtasker/dead_worker_error.rb +6 -0
- data/lib/cloudtasker/local_server.rb +74 -0
- data/lib/cloudtasker/railtie.rb +10 -0
- data/lib/cloudtasker/redis_client.rb +24 -2
- data/lib/cloudtasker/testing.rb +133 -0
- data/lib/cloudtasker/unique_job/job.rb +5 -2
- data/lib/cloudtasker/unique_job/lock/base_lock.rb +1 -1
- data/lib/cloudtasker/unique_job/lock/until_executed.rb +3 -1
- data/lib/cloudtasker/unique_job/lock/while_executing.rb +3 -1
- data/lib/cloudtasker/unique_job/middleware.rb +0 -1
- data/lib/cloudtasker/version.rb +1 -1
- data/lib/cloudtasker/worker.rb +59 -16
- data/lib/cloudtasker/{task.rb → worker_handler.rb} +10 -77
- data/lib/cloudtasker/worker_logger.rb +155 -0
- data/lib/tasks/setup_queue.rake +10 -0
- metadata +98 -9
- data/lib/cloudtasker/batch/config.rb +0 -11
- data/lib/cloudtasker/cron/config.rb +0 -11
- data/lib/cloudtasker/unique_job/config.rb +0 -10
@@ -6,6 +6,13 @@ module Cloudtasker
|
|
6
6
|
class Job
|
7
7
|
attr_reader :worker
|
8
8
|
|
9
|
+
# Key Namespace used for object saved under this class
|
10
|
+
JOBS_NAMESPACE = 'jobs'
|
11
|
+
STATES_NAMESPACE = 'states'
|
12
|
+
|
13
|
+
# List of statuses triggering a completion callback
|
14
|
+
COMPLETION_STATUSES = %w[completed dead].freeze
|
15
|
+
|
9
16
|
#
|
10
17
|
# Return the cloudtasker redis client
|
11
18
|
#
|
@@ -25,13 +32,13 @@ module Cloudtasker
|
|
25
32
|
def self.find(worker_id)
|
26
33
|
return nil unless worker_id
|
27
34
|
|
28
|
-
# Retrieve
|
29
|
-
|
30
|
-
|
31
|
-
return nil unless
|
35
|
+
# Retrieve related worker
|
36
|
+
payload = redis.fetch(key("#{JOBS_NAMESPACE}/#{worker_id}"))
|
37
|
+
worker = Cloudtasker::Worker.from_hash(payload)
|
38
|
+
return nil unless worker
|
32
39
|
|
33
40
|
# Build batch job
|
34
|
-
self.for(
|
41
|
+
self.for(worker)
|
35
42
|
end
|
36
43
|
|
37
44
|
#
|
@@ -44,7 +51,7 @@ module Cloudtasker
|
|
44
51
|
def self.key(val)
|
45
52
|
return nil if val.nil?
|
46
53
|
|
47
|
-
[
|
54
|
+
[to_s.underscore, val.to_s].join('/')
|
48
55
|
end
|
49
56
|
|
50
57
|
#
|
@@ -134,7 +141,7 @@ module Cloudtasker
|
|
134
141
|
# @return [String] The worker namespaced id.
|
135
142
|
#
|
136
143
|
def batch_gid
|
137
|
-
key(batch_id)
|
144
|
+
key("#{JOBS_NAMESPACE}/#{batch_id}")
|
138
145
|
end
|
139
146
|
|
140
147
|
#
|
@@ -143,7 +150,7 @@ module Cloudtasker
|
|
143
150
|
# @return [String] The batch state namespaced id.
|
144
151
|
#
|
145
152
|
def batch_state_gid
|
146
|
-
|
153
|
+
key("#{STATES_NAMESPACE}/#{batch_id}")
|
147
154
|
end
|
148
155
|
|
149
156
|
#
|
@@ -220,24 +227,62 @@ module Cloudtasker
|
|
220
227
|
return true unless state
|
221
228
|
|
222
229
|
# Check that all children are complete
|
223
|
-
state.values.all? { |e| e
|
230
|
+
state.values.all? { |e| COMPLETION_STATUSES.include?(e) }
|
224
231
|
end
|
225
232
|
end
|
226
233
|
|
234
|
+
#
|
235
|
+
# Run worker callback in a controlled environment to
|
236
|
+
# avoid interruption of the callback flow.
|
237
|
+
#
|
238
|
+
# @param [String, Symbol] callback The callback to run.
|
239
|
+
# @param [Array<any>] *args The callback arguments.
|
240
|
+
#
|
241
|
+
# @return [any] The callback return value
|
242
|
+
#
|
243
|
+
def run_worker_callback(callback, *args)
|
244
|
+
worker.try(callback, *args)
|
245
|
+
rescue StandardError => e
|
246
|
+
Cloudtasker.logger.error("Error running callback #{callback}: #{e}")
|
247
|
+
Cloudtasker.logger.error(e.backtrace.join("\n"))
|
248
|
+
nil
|
249
|
+
end
|
250
|
+
|
251
|
+
#
|
252
|
+
# Callback invoked when the batch is complete
|
253
|
+
#
|
254
|
+
def on_complete(status = :completed)
|
255
|
+
# Invoke worker callback
|
256
|
+
run_worker_callback(:on_batch_complete) if status == :completed
|
257
|
+
|
258
|
+
# Propagate event
|
259
|
+
parent_batch&.on_child_complete(self, status)
|
260
|
+
ensure
|
261
|
+
# The batch tree is complete. Cleanup the tree.
|
262
|
+
cleanup unless parent_batch
|
263
|
+
end
|
264
|
+
|
227
265
|
#
|
228
266
|
# Callback invoked when a direct child batch is complete.
|
229
267
|
#
|
230
268
|
# @param [Cloudtasker::Batch::Job] child_batch The completed child batch.
|
231
269
|
#
|
232
|
-
def on_child_complete(child_batch)
|
270
|
+
def on_child_complete(child_batch, status = :completed)
|
233
271
|
# Update batch state
|
234
|
-
update_state(child_batch.batch_id,
|
272
|
+
update_state(child_batch.batch_id, status)
|
235
273
|
|
236
274
|
# Notify the worker that a direct batch child worker has completed
|
237
|
-
|
275
|
+
case status
|
276
|
+
when :completed
|
277
|
+
run_worker_callback(:on_child_complete, child_batch.worker)
|
278
|
+
when :errored
|
279
|
+
run_worker_callback(:on_child_error, child_batch.worker)
|
280
|
+
when :dead
|
281
|
+
run_worker_callback(:on_child_dead, child_batch.worker)
|
282
|
+
end
|
238
283
|
|
239
284
|
# Notify the parent batch that we are done with this batch
|
240
|
-
|
285
|
+
on_complete if status != :errored && complete?
|
241
286
|
end
|
242
287
|
|
243
288
|
#
|
@@ -245,14 +290,31 @@ module Cloudtasker
|
|
245
290
|
#
|
246
291
|
# @param [Cloudtasker::Batch::Job] child_batch The completed child batch.
|
247
292
|
#
|
248
|
-
def on_batch_node_complete(child_batch)
|
293
|
+
def on_batch_node_complete(child_batch, status = :completed)
|
294
|
+
return false unless status == :completed
|
295
|
+
|
249
296
|
# Notify the worker that a batch node worker has completed
|
250
|
-
|
297
|
+
run_worker_callback(:on_batch_node_complete, child_batch.worker)
|
251
298
|
|
252
299
|
# Notify the parent batch that a node is complete
|
253
300
|
parent_batch&.on_batch_node_complete(child_batch)
|
254
301
|
end
|
255
302
|
|
303
|
+
#
|
304
|
+
# Remove all batch and sub-batch keys from Redis.
|
305
|
+
#
|
306
|
+
def cleanup
|
307
|
+
# Capture batch state
|
308
|
+
state = batch_state
|
309
|
+
|
310
|
+
# Delete child batches recursively
|
311
|
+
state.to_h.keys.each { |id| self.class.find(id)&.cleanup }
|
312
|
+
|
313
|
+
# Delete batch redis entries
|
314
|
+
redis.del(batch_gid)
|
315
|
+
redis.del(batch_state_gid)
|
316
|
+
end
|
317
|
+
|
256
318
|
#
|
257
319
|
# Calculate the progress of the batch.
|
258
320
|
#
|
@@ -271,7 +333,7 @@ module Cloudtasker
|
|
271
333
|
#
|
272
334
|
# Save the batch and enqueue all child workers attached to it.
|
273
335
|
#
|
274
|
-
# @return [Array<
|
336
|
+
# @return [Array<Cloudtasker::CloudTask>] The Google Task responses
|
275
337
|
#
|
276
338
|
def setup
|
277
339
|
return true if jobs.empty?
|
@@ -286,17 +348,14 @@ module Cloudtasker
|
|
286
348
|
#
|
287
349
|
# Post-perform logic. The parent batch is notified if the job is complete.
|
288
350
|
#
|
289
|
-
def complete
|
351
|
+
def complete(status = :completed)
|
290
352
|
return true if reenqueued? || jobs.any?
|
291
353
|
|
292
354
|
# Notify the parent batch that a child is complete
|
293
|
-
if complete?
|
294
|
-
worker.try(:on_batch_complete)
|
295
|
-
parent_batch&.on_child_complete(self)
|
296
|
-
end
|
355
|
+
on_complete(status) if complete?
|
297
356
|
|
298
357
|
# Notify the parent that a batch node has completed
|
299
|
-
parent_batch&.on_batch_node_complete(self)
|
358
|
+
parent_batch&.on_batch_node_complete(self, status)
|
300
359
|
end
|
301
360
|
|
302
361
|
#
|
@@ -313,7 +372,13 @@ module Cloudtasker
|
|
313
372
|
setup
|
314
373
|
|
315
374
|
# Complete batch
|
316
|
-
complete
|
375
|
+
complete(:completed)
|
376
|
+
rescue DeadWorkerError => e
|
377
|
+
complete(:dead)
|
378
|
+
raise(e)
|
379
|
+
rescue StandardError => e
|
380
|
+
complete(:errored)
|
381
|
+
raise(e)
|
317
382
|
end
|
318
383
|
end
|
319
384
|
end
|
@@ -0,0 +1,194 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cloudtasker'
|
4
|
+
require 'cloudtasker/local_server'
|
5
|
+
|
6
|
+
module Cloudtasker
|
7
|
+
# Cloudtasker executable logic
|
8
|
+
module CLI
|
9
|
+
module_function
|
10
|
+
|
11
|
+
#
|
12
|
+
# Return the current environment.
|
13
|
+
#
|
14
|
+
# @return [String] The environment name.
|
15
|
+
#
|
16
|
+
def environment
|
17
|
+
Cloudtasker.config.environment
|
18
|
+
end
|
19
|
+
|
20
|
+
#
|
21
|
+
# Return true if we are running in Rails.
|
22
|
+
#
|
23
|
+
# @return [Boolean] True if rails is loaded.
|
24
|
+
#
|
25
|
+
def rails_app?
|
26
|
+
defined?(::Rails)
|
27
|
+
end
|
28
|
+
|
29
|
+
#
|
30
|
+
# Return true if we are running in JRuby.
|
31
|
+
#
|
32
|
+
# @return [Boolean] True if JRuby is loaded.
|
33
|
+
#
|
34
|
+
def jruby?
|
35
|
+
defined?(::JRUBY_VERSION)
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# Return the Cloudtasker logger
|
40
|
+
#
|
41
|
+
# @return [Logger] The Cloudtasker logger.
|
42
|
+
#
|
43
|
+
def logger
|
44
|
+
Cloudtasker.logger
|
45
|
+
end
|
46
|
+
|
47
|
+
#
|
48
|
+
# Return the local Cloudtasker server.
|
49
|
+
#
|
50
|
+
# @return [Cloudtasker::LocalServer] The local Cloudtasker server.
|
51
|
+
#
|
52
|
+
def local_server
|
53
|
+
@local_server ||= LocalServer.new
|
54
|
+
end
|
55
|
+
|
56
|
+
#
|
57
|
+
# Load Rails if defined
|
58
|
+
#
|
59
|
+
def boot_system
|
60
|
+
# Sync logs
|
61
|
+
STDOUT.sync = true
|
62
|
+
|
63
|
+
# Check for Rails
|
64
|
+
return false unless File.exist?('./config/environment.rb')
|
65
|
+
|
66
|
+
require 'rails'
|
67
|
+
require File.expand_path('./config/environment.rb')
|
68
|
+
end
|
69
|
+
|
70
|
+
#
|
71
|
+
# Run the cloudtasker development server.
|
72
|
+
#
|
73
|
+
def run
|
74
|
+
boot_system
|
75
|
+
|
76
|
+
# Print banner
|
77
|
+
environment == 'development' ? print_banner : print_non_dev_warning
|
78
|
+
|
79
|
+
# Print rails info
|
80
|
+
if rails_app?
|
81
|
+
logger.info "[Cloudtasker/Server] Booted Rails #{::Rails.version} application in #{environment} environment"
|
82
|
+
end
|
83
|
+
|
84
|
+
# Get internal read/write pip
|
85
|
+
self_read, self_write = IO.pipe
|
86
|
+
|
87
|
+
# Setup signals to trap
|
88
|
+
setup_signals(self_write)
|
89
|
+
|
90
|
+
logger.info "[Cloudtasker/Server] Running in #{RUBY_DESCRIPTION}"
|
91
|
+
|
92
|
+
# Wait for signals
|
93
|
+
wait_for_signal(self_read)
|
94
|
+
end
|
95
|
+
|
96
|
+
#
|
97
|
+
# Wait for signals and handle them.
|
98
|
+
#
|
99
|
+
# @param [IO] read_pipe Where to read signals.
|
100
|
+
#
|
101
|
+
def wait_for_signal(read_pipe)
|
102
|
+
local_server.start
|
103
|
+
|
104
|
+
while (readable_io = IO.select([read_pipe]))
|
105
|
+
signal = readable_io.first[0].gets.strip
|
106
|
+
handle_signal(signal)
|
107
|
+
end
|
108
|
+
rescue Interrupt
|
109
|
+
logger.info 'Shutting down'
|
110
|
+
local_server.stop
|
111
|
+
logger.info 'Stopped'
|
112
|
+
end
|
113
|
+
|
114
|
+
#
|
115
|
+
# Define which signals to trap
|
116
|
+
#
|
117
|
+
# @param [IO] write_pipe Where to write signals.
|
118
|
+
#
|
119
|
+
def setup_signals(write_pipe)
|
120
|
+
# Display signals on log output
|
121
|
+
sigs = %w[INT TERM TTIN TSTP]
|
122
|
+
# USR1 and USR2 don't work on the JVM
|
123
|
+
sigs << 'USR2' unless jruby?
|
124
|
+
sigs.each do |sig|
|
125
|
+
begin
|
126
|
+
trap(sig) { write_pipe.puts(sig) }
|
127
|
+
rescue ArgumentError
|
128
|
+
puts "Signal #{sig} not supported"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
#
|
134
|
+
# Handle process signals
|
135
|
+
#
|
136
|
+
# @param [String] sig The signal.
|
137
|
+
#
|
138
|
+
def handle_signal(sig)
|
139
|
+
raise(Interrupt) if %w[INT TERM].include?(sig)
|
140
|
+
end
|
141
|
+
|
142
|
+
#
|
143
|
+
# Return the server banner
|
144
|
+
#
|
145
|
+
# @return [String] The server banner
|
146
|
+
#
|
147
|
+
def banner
|
148
|
+
<<~'TEXT'
|
149
|
+
___ _ _ _ _
|
150
|
+
/ __\ | ___ _ _ __| | |_ __ _ ___| | _____ _ __
|
151
|
+
/ / | |/ _ \| | | |/ _` | __/ _` / __| |/ / _ \ '__|
|
152
|
+
/ /___| | (_) | |_| | (_| | || (_| \__ \ < __/ |
|
153
|
+
\____/|_|\___/ \__,_|\__,_|\__\__,_|___/_|\_\___|_|
|
154
|
+
|
155
|
+
TEXT
|
156
|
+
end
|
157
|
+
|
158
|
+
#
|
159
|
+
# Display a warning message when run in non-dev env.
|
160
|
+
#
|
161
|
+
# @return [<Type>] <description>
|
162
|
+
#
|
163
|
+
def print_non_dev_warning
|
164
|
+
puts "\e[31m"
|
165
|
+
puts non_dev_warning_message
|
166
|
+
puts "\e[0m"
|
167
|
+
end
|
168
|
+
|
169
|
+
#
|
170
|
+
# Return the message to display when users attempt to run
|
171
|
+
# the local development server in non-dev environments.
|
172
|
+
#
|
173
|
+
# @return [String] The warning message.
|
174
|
+
#
|
175
|
+
def non_dev_warning_message
|
176
|
+
<<~'TEXT'
|
177
|
+
============================================ /!\ ====================================================
|
178
|
+
Your are running the Cloudtasker local development server in a NON-DEVELOPMENT environment.
|
179
|
+
This is not recommended as the the development server is not designed for production-like load.
|
180
|
+
If you need a job processing server to run yourself please use Sidekiq instead (https://sidekiq.org)
|
181
|
+
============================================ /!\ ====================================================
|
182
|
+
TEXT
|
183
|
+
end
|
184
|
+
|
185
|
+
#
|
186
|
+
# Print the server banner
|
187
|
+
#
|
188
|
+
def print_banner
|
189
|
+
puts "\e[96m"
|
190
|
+
puts banner
|
191
|
+
puts "\e[0m"
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cloudtasker
|
4
|
+
# An interface class to manage tasks on the backend (Cloud Task or Redis)
|
5
|
+
class CloudTask
|
6
|
+
attr_accessor :id, :http_request, :schedule_time, :retries
|
7
|
+
|
8
|
+
#
|
9
|
+
# The backend to use for cloud tasks.
|
10
|
+
#
|
11
|
+
# @return [Cloudtasker::Backend::GoogleCloudTask, Cloudtasker::Backend::RedisTask] The cloud task backend.
|
12
|
+
#
|
13
|
+
def self.backend
|
14
|
+
# Re-evaluate backend every time if testing mode enabled
|
15
|
+
@backend = nil if defined?(Cloudtasker::Testing)
|
16
|
+
|
17
|
+
@backend ||= begin
|
18
|
+
if defined?(Cloudtasker::Testing) && Cloudtasker::Testing.in_memory?
|
19
|
+
require 'cloudtasker/backend/memory_task'
|
20
|
+
Backend::MemoryTask
|
21
|
+
elsif Cloudtasker.config.mode.to_sym == :development
|
22
|
+
require 'cloudtasker/backend/redis_task'
|
23
|
+
Backend::RedisTask
|
24
|
+
else
|
25
|
+
require 'cloudtasker/backend/google_cloud_task'
|
26
|
+
Backend::GoogleCloudTask
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
# Find a cloud task by id.
|
33
|
+
#
|
34
|
+
# @param [String] id The id of the task.
|
35
|
+
#
|
36
|
+
# @return [Cloudtasker::Cloudtask] The task.
|
37
|
+
#
|
38
|
+
def self.find(id)
|
39
|
+
payload = backend.find(id)&.to_h
|
40
|
+
payload ? new(payload) : nil
|
41
|
+
end
|
42
|
+
|
43
|
+
#
|
44
|
+
# Create a new cloud task.
|
45
|
+
#
|
46
|
+
# @param [Hash] payload Thee task payload
|
47
|
+
#
|
48
|
+
# @return [Cloudtasker::CloudTask] The created task.
|
49
|
+
#
|
50
|
+
def self.create(payload)
|
51
|
+
resp = backend.create(payload)&.to_h
|
52
|
+
resp ? new(resp) : nil
|
53
|
+
end
|
54
|
+
|
55
|
+
#
|
56
|
+
# Delete a cloud task by id.
|
57
|
+
#
|
58
|
+
# @param [String] id The task id.
|
59
|
+
#
|
60
|
+
def self.delete(id)
|
61
|
+
backend.delete(id)
|
62
|
+
end
|
63
|
+
|
64
|
+
#
|
65
|
+
# Build a new instance of the class using a backend response
|
66
|
+
# payload.
|
67
|
+
#
|
68
|
+
# @param [String] id The task id.
|
69
|
+
# @param [Hash] http_request The content of the http request.
|
70
|
+
# @param [Integer] schedule_time When to run the job (Unix timestamp)
|
71
|
+
# @param [Integer] retries The number of times the job failed.
|
72
|
+
#
|
73
|
+
def initialize(id:, http_request:, schedule_time: nil, retries: 0)
|
74
|
+
@id = id
|
75
|
+
@http_request = http_request
|
76
|
+
@schedule_time = schedule_time
|
77
|
+
@retries = retries || 0
|
78
|
+
end
|
79
|
+
|
80
|
+
#
|
81
|
+
# Equality operator.
|
82
|
+
#
|
83
|
+
# @param [Any] other The object to compare.
|
84
|
+
#
|
85
|
+
# @return [Boolean] True if the object is equal.
|
86
|
+
#
|
87
|
+
def ==(other)
|
88
|
+
other.is_a?(self.class) && other.id == id
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|