cloudtasker 0.2.0 → 0.3.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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -0
  3. data/.rubocop.yml +5 -0
  4. data/.travis.yml +10 -1
  5. data/Appraisals +25 -0
  6. data/Gemfile.lock +10 -4
  7. data/README.md +550 -4
  8. data/app/controllers/cloudtasker/application_controller.rb +2 -0
  9. data/app/controllers/cloudtasker/worker_controller.rb +22 -2
  10. data/cloudtasker.gemspec +4 -3
  11. data/docs/BATCH_JOBS.md +66 -0
  12. data/docs/CRON_JOBS.md +63 -0
  13. data/docs/UNIQUE_JOBS.md +127 -0
  14. data/exe/cloudtasker +15 -0
  15. data/gemfiles/.bundle/config +2 -0
  16. data/gemfiles/google_cloud_tasks_1.0.gemfile +9 -0
  17. data/gemfiles/google_cloud_tasks_1.0.gemfile.lock +263 -0
  18. data/gemfiles/google_cloud_tasks_1.1.gemfile +9 -0
  19. data/gemfiles/google_cloud_tasks_1.1.gemfile.lock +263 -0
  20. data/gemfiles/google_cloud_tasks_1.2.gemfile +9 -0
  21. data/gemfiles/google_cloud_tasks_1.2.gemfile.lock +263 -0
  22. data/gemfiles/google_cloud_tasks_1.3.gemfile +9 -0
  23. data/gemfiles/google_cloud_tasks_1.3.gemfile.lock +264 -0
  24. data/gemfiles/rails_4.0.gemfile +10 -0
  25. data/gemfiles/rails_4.1.gemfile +9 -0
  26. data/gemfiles/rails_4.2.gemfile +9 -0
  27. data/gemfiles/rails_5.0.gemfile +9 -0
  28. data/gemfiles/rails_5.1.gemfile +9 -0
  29. data/gemfiles/rails_5.2.gemfile +9 -0
  30. data/gemfiles/rails_5.2.gemfile.lock +247 -0
  31. data/gemfiles/rails_6.0.gemfile +9 -0
  32. data/gemfiles/rails_6.0.gemfile.lock +263 -0
  33. data/lib/cloudtasker.rb +19 -1
  34. data/lib/cloudtasker/backend/google_cloud_task.rb +139 -0
  35. data/lib/cloudtasker/backend/memory_task.rb +190 -0
  36. data/lib/cloudtasker/backend/redis_task.rb +248 -0
  37. data/lib/cloudtasker/batch/batch_progress.rb +19 -1
  38. data/lib/cloudtasker/batch/job.rb +81 -20
  39. data/lib/cloudtasker/cli.rb +194 -0
  40. data/lib/cloudtasker/cloud_task.rb +91 -0
  41. data/lib/cloudtasker/config.rb +64 -2
  42. data/lib/cloudtasker/cron/job.rb +2 -2
  43. data/lib/cloudtasker/cron/schedule.rb +15 -5
  44. data/lib/cloudtasker/dead_worker_error.rb +6 -0
  45. data/lib/cloudtasker/local_server.rb +74 -0
  46. data/lib/cloudtasker/railtie.rb +10 -0
  47. data/lib/cloudtasker/testing.rb +133 -0
  48. data/lib/cloudtasker/unique_job/job.rb +1 -1
  49. data/lib/cloudtasker/unique_job/lock/base_lock.rb +1 -1
  50. data/lib/cloudtasker/unique_job/lock/until_executed.rb +3 -1
  51. data/lib/cloudtasker/unique_job/lock/while_executing.rb +3 -1
  52. data/lib/cloudtasker/version.rb +1 -1
  53. data/lib/cloudtasker/worker.rb +59 -16
  54. data/lib/cloudtasker/{task.rb → worker_handler.rb} +10 -77
  55. data/lib/cloudtasker/worker_logger.rb +155 -0
  56. data/lib/tasks/setup_queue.rake +10 -0
  57. 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 parent worker
32
- parent_payload = redis.fetch(key(worker_id))
33
- parent_worker = Cloudtasker::Worker.from_hash(parent_payload)
34
- return nil unless parent_worker
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(parent_worker)
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 == 'completed' }
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, :completed)
271
+ update_state(child_batch.batch_id, status)
238
272
 
239
273
  # Notify the worker that a direct batch child worker has completed
240
- worker.try(:on_child_complete, child_batch.worker)
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
- parent_batch&.on_child_complete(self) if complete?
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
- worker.try(:on_batch_node_complete, child_batch.worker)
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<Google::Cloud::Tasks::V2beta3::Task>] The Google Task responses
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