cloudtasker 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +4 -0
- data/.rubocop.yml +5 -0
- data/.travis.yml +10 -1
- data/Appraisals +25 -0
- data/Gemfile.lock +10 -4
- data/README.md +550 -4
- data/app/controllers/cloudtasker/application_controller.rb +2 -0
- data/app/controllers/cloudtasker/worker_controller.rb +22 -2
- data/cloudtasker.gemspec +4 -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 +19 -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 +248 -0
- data/lib/cloudtasker/batch/batch_progress.rb +19 -1
- data/lib/cloudtasker/batch/job.rb +81 -20
- 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 +2 -2
- data/lib/cloudtasker/cron/schedule.rb +15 -5
- 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/testing.rb +133 -0
- data/lib/cloudtasker/unique_job/job.rb +1 -1
- 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/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 +55 -6
@@ -9,6 +9,9 @@ module Cloudtasker
|
|
9
9
|
# Key Namespace used for object saved under this class
|
10
10
|
SUB_NAMESPACE = 'job'
|
11
11
|
|
12
|
+
# List of statuses triggering a completion callback
|
13
|
+
COMPLETION_STATUSES = %w[completed dead].freeze
|
14
|
+
|
12
15
|
#
|
13
16
|
# Return the cloudtasker redis client
|
14
17
|
#
|
@@ -28,13 +31,13 @@ module Cloudtasker
|
|
28
31
|
def self.find(worker_id)
|
29
32
|
return nil unless worker_id
|
30
33
|
|
31
|
-
# Retrieve
|
32
|
-
|
33
|
-
|
34
|
-
return nil unless
|
34
|
+
# Retrieve related worker
|
35
|
+
payload = redis.fetch(key(worker_id))
|
36
|
+
worker = Cloudtasker::Worker.from_hash(payload)
|
37
|
+
return nil unless worker
|
35
38
|
|
36
39
|
# Build batch job
|
37
|
-
self.for(
|
40
|
+
self.for(worker)
|
38
41
|
end
|
39
42
|
|
40
43
|
#
|
@@ -223,24 +226,62 @@ module Cloudtasker
|
|
223
226
|
return true unless state
|
224
227
|
|
225
228
|
# Check that all children are complete
|
226
|
-
state.values.all? { |e| e
|
229
|
+
state.values.all? { |e| COMPLETION_STATUSES.include?(e) }
|
227
230
|
end
|
228
231
|
end
|
229
232
|
|
233
|
+
#
|
234
|
+
# Run worker callback in a controlled environment to
|
235
|
+
# avoid interruption of the callback flow.
|
236
|
+
#
|
237
|
+
# @param [String, Symbol] callback The callback to run.
|
238
|
+
# @param [Array<any>] *args The callback arguments.
|
239
|
+
#
|
240
|
+
# @return [any] The callback return value
|
241
|
+
#
|
242
|
+
def run_worker_callback(callback, *args)
|
243
|
+
worker.try(callback, *args)
|
244
|
+
rescue StandardError => e
|
245
|
+
Cloudtasker.logger.error("Error running callback #{callback}: #{e}")
|
246
|
+
Cloudtasker.logger.error(e.backtrace.join("\n"))
|
247
|
+
nil
|
248
|
+
end
|
249
|
+
|
250
|
+
#
|
251
|
+
# Callback invoked when the batch is complete
|
252
|
+
#
|
253
|
+
def on_complete(status = :completed)
|
254
|
+
# Invoke worker callback
|
255
|
+
run_worker_callback(:on_batch_complete) if status == :completed
|
256
|
+
|
257
|
+
# Propagate event
|
258
|
+
parent_batch&.on_child_complete(self, status)
|
259
|
+
ensure
|
260
|
+
# The batch tree is complete. Cleanup the tree.
|
261
|
+
cleanup unless parent_batch
|
262
|
+
end
|
263
|
+
|
230
264
|
#
|
231
265
|
# Callback invoked when a direct child batch is complete.
|
232
266
|
#
|
233
267
|
# @param [Cloudtasker::Batch::Job] child_batch The completed child batch.
|
234
268
|
#
|
235
|
-
def on_child_complete(child_batch)
|
269
|
+
def on_child_complete(child_batch, status = :completed)
|
236
270
|
# Update batch state
|
237
|
-
update_state(child_batch.batch_id,
|
271
|
+
update_state(child_batch.batch_id, status)
|
238
272
|
|
239
273
|
# Notify the worker that a direct batch child worker has completed
|
240
|
-
|
274
|
+
case status
|
275
|
+
when :completed
|
276
|
+
run_worker_callback(:on_child_complete, child_batch.worker)
|
277
|
+
when :errored
|
278
|
+
run_worker_callback(:on_child_error, child_batch.worker)
|
279
|
+
when :dead
|
280
|
+
run_worker_callback(:on_child_dead, child_batch.worker)
|
281
|
+
end
|
241
282
|
|
242
283
|
# Notify the parent batch that we are done with this batch
|
243
|
-
|
284
|
+
on_complete if status != :errored && complete?
|
244
285
|
end
|
245
286
|
|
246
287
|
#
|
@@ -248,14 +289,31 @@ module Cloudtasker
|
|
248
289
|
#
|
249
290
|
# @param [Cloudtasker::Batch::Job] child_batch The completed child batch.
|
250
291
|
#
|
251
|
-
def on_batch_node_complete(child_batch)
|
292
|
+
def on_batch_node_complete(child_batch, status = :completed)
|
293
|
+
return false unless status == :completed
|
294
|
+
|
252
295
|
# Notify the worker that a batch node worker has completed
|
253
|
-
|
296
|
+
run_worker_callback(:on_batch_node_complete, child_batch.worker)
|
254
297
|
|
255
298
|
# Notify the parent batch that a node is complete
|
256
299
|
parent_batch&.on_batch_node_complete(child_batch)
|
257
300
|
end
|
258
301
|
|
302
|
+
#
|
303
|
+
# Remove all batch and sub-batch keys from Redis.
|
304
|
+
#
|
305
|
+
def cleanup
|
306
|
+
# Capture batch state
|
307
|
+
state = batch_state
|
308
|
+
|
309
|
+
# Delete child batches recursively
|
310
|
+
state.to_h.keys.each { |id| self.class.find(id)&.cleanup }
|
311
|
+
|
312
|
+
# Delete batch redis entries
|
313
|
+
redis.del(batch_gid)
|
314
|
+
redis.del(batch_state_gid)
|
315
|
+
end
|
316
|
+
|
259
317
|
#
|
260
318
|
# Calculate the progress of the batch.
|
261
319
|
#
|
@@ -274,7 +332,7 @@ module Cloudtasker
|
|
274
332
|
#
|
275
333
|
# Save the batch and enqueue all child workers attached to it.
|
276
334
|
#
|
277
|
-
# @return [Array<
|
335
|
+
# @return [Array<Cloudtasker::CloudTask>] The Google Task responses
|
278
336
|
#
|
279
337
|
def setup
|
280
338
|
return true if jobs.empty?
|
@@ -289,17 +347,14 @@ module Cloudtasker
|
|
289
347
|
#
|
290
348
|
# Post-perform logic. The parent batch is notified if the job is complete.
|
291
349
|
#
|
292
|
-
def complete
|
350
|
+
def complete(status = :completed)
|
293
351
|
return true if reenqueued? || jobs.any?
|
294
352
|
|
295
353
|
# Notify the parent batch that a child is complete
|
296
|
-
if complete?
|
297
|
-
worker.try(:on_batch_complete)
|
298
|
-
parent_batch&.on_child_complete(self)
|
299
|
-
end
|
354
|
+
on_complete(status) if complete?
|
300
355
|
|
301
356
|
# Notify the parent that a batch node has completed
|
302
|
-
parent_batch&.on_batch_node_complete(self)
|
357
|
+
parent_batch&.on_batch_node_complete(self, status)
|
303
358
|
end
|
304
359
|
|
305
360
|
#
|
@@ -316,7 +371,13 @@ module Cloudtasker
|
|
316
371
|
setup
|
317
372
|
|
318
373
|
# Complete batch
|
319
|
-
complete
|
374
|
+
complete(:success)
|
375
|
+
rescue DeadWorkerError => e
|
376
|
+
complete(:dead)
|
377
|
+
raise(e)
|
378
|
+
rescue StandardError => e
|
379
|
+
complete(:errored)
|
380
|
+
raise(e)
|
320
381
|
end
|
321
382
|
end
|
322
383
|
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
|