cloudtasker 0.2.0 → 0.7.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 +29 -0
- data/Gemfile.lock +27 -4
- data/README.md +571 -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 +5 -3
- data/docs/BATCH_JOBS.md +66 -0
- data/docs/CRON_JOBS.md +65 -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 +249 -0
- data/lib/cloudtasker/batch/batch_progress.rb +19 -1
- data/lib/cloudtasker/batch/job.rb +85 -23
- 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 +25 -11
- 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 +2 -2
- 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 +61 -17
- 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 +70 -6
@@ -7,7 +7,11 @@ 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'
|
12
|
+
|
13
|
+
# List of statuses triggering a completion callback
|
14
|
+
COMPLETION_STATUSES = %w[completed dead].freeze
|
11
15
|
|
12
16
|
#
|
13
17
|
# Return the cloudtasker redis client
|
@@ -28,13 +32,13 @@ module Cloudtasker
|
|
28
32
|
def self.find(worker_id)
|
29
33
|
return nil unless worker_id
|
30
34
|
|
31
|
-
# Retrieve
|
32
|
-
|
33
|
-
|
34
|
-
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
|
35
39
|
|
36
40
|
# Build batch job
|
37
|
-
self.for(
|
41
|
+
self.for(worker)
|
38
42
|
end
|
39
43
|
|
40
44
|
#
|
@@ -137,7 +141,7 @@ module Cloudtasker
|
|
137
141
|
# @return [String] The worker namespaced id.
|
138
142
|
#
|
139
143
|
def batch_gid
|
140
|
-
key(batch_id)
|
144
|
+
key("#{JOBS_NAMESPACE}/#{batch_id}")
|
141
145
|
end
|
142
146
|
|
143
147
|
#
|
@@ -146,7 +150,7 @@ module Cloudtasker
|
|
146
150
|
# @return [String] The batch state namespaced id.
|
147
151
|
#
|
148
152
|
def batch_state_gid
|
149
|
-
|
153
|
+
key("#{STATES_NAMESPACE}/#{batch_id}")
|
150
154
|
end
|
151
155
|
|
152
156
|
#
|
@@ -223,24 +227,62 @@ module Cloudtasker
|
|
223
227
|
return true unless state
|
224
228
|
|
225
229
|
# Check that all children are complete
|
226
|
-
state.values.all? { |e| e
|
230
|
+
state.values.all? { |e| COMPLETION_STATUSES.include?(e) }
|
227
231
|
end
|
228
232
|
end
|
229
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
|
+
|
230
265
|
#
|
231
266
|
# Callback invoked when a direct child batch is complete.
|
232
267
|
#
|
233
268
|
# @param [Cloudtasker::Batch::Job] child_batch The completed child batch.
|
234
269
|
#
|
235
|
-
def on_child_complete(child_batch)
|
270
|
+
def on_child_complete(child_batch, status = :completed)
|
236
271
|
# Update batch state
|
237
|
-
update_state(child_batch.batch_id,
|
272
|
+
update_state(child_batch.batch_id, status)
|
238
273
|
|
239
274
|
# Notify the worker that a direct batch child worker has completed
|
240
|
-
|
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
|
241
283
|
|
242
284
|
# Notify the parent batch that we are done with this batch
|
243
|
-
|
285
|
+
on_complete if status != :errored && complete?
|
244
286
|
end
|
245
287
|
|
246
288
|
#
|
@@ -248,14 +290,31 @@ module Cloudtasker
|
|
248
290
|
#
|
249
291
|
# @param [Cloudtasker::Batch::Job] child_batch The completed child batch.
|
250
292
|
#
|
251
|
-
def on_batch_node_complete(child_batch)
|
293
|
+
def on_batch_node_complete(child_batch, status = :completed)
|
294
|
+
return false unless status == :completed
|
295
|
+
|
252
296
|
# Notify the worker that a batch node worker has completed
|
253
|
-
|
297
|
+
run_worker_callback(:on_batch_node_complete, child_batch.worker)
|
254
298
|
|
255
299
|
# Notify the parent batch that a node is complete
|
256
300
|
parent_batch&.on_batch_node_complete(child_batch)
|
257
301
|
end
|
258
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
|
+
|
259
318
|
#
|
260
319
|
# Calculate the progress of the batch.
|
261
320
|
#
|
@@ -274,7 +333,7 @@ module Cloudtasker
|
|
274
333
|
#
|
275
334
|
# Save the batch and enqueue all child workers attached to it.
|
276
335
|
#
|
277
|
-
# @return [Array<
|
336
|
+
# @return [Array<Cloudtasker::CloudTask>] The Google Task responses
|
278
337
|
#
|
279
338
|
def setup
|
280
339
|
return true if jobs.empty?
|
@@ -289,17 +348,14 @@ module Cloudtasker
|
|
289
348
|
#
|
290
349
|
# Post-perform logic. The parent batch is notified if the job is complete.
|
291
350
|
#
|
292
|
-
def complete
|
351
|
+
def complete(status = :completed)
|
293
352
|
return true if reenqueued? || jobs.any?
|
294
353
|
|
295
354
|
# 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
|
355
|
+
on_complete(status) if complete?
|
300
356
|
|
301
357
|
# Notify the parent that a batch node has completed
|
302
|
-
parent_batch&.on_batch_node_complete(self)
|
358
|
+
parent_batch&.on_batch_node_complete(self, status)
|
303
359
|
end
|
304
360
|
|
305
361
|
#
|
@@ -316,7 +372,13 @@ module Cloudtasker
|
|
316
372
|
setup
|
317
373
|
|
318
374
|
# Complete batch
|
319
|
-
complete
|
375
|
+
complete(:completed)
|
376
|
+
rescue DeadWorkerError => e
|
377
|
+
complete(:dead)
|
378
|
+
raise(e)
|
379
|
+
rescue StandardError => e
|
380
|
+
complete(:errored)
|
381
|
+
raise(e)
|
320
382
|
end
|
321
383
|
end
|
322
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
|