cloudtasker-tonix 0.1.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 (100) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/lint_rubocop.yml +15 -0
  3. data/.github/workflows/test_ruby_3.x.yml +40 -0
  4. data/.gitignore +23 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +96 -0
  7. data/Appraisals +76 -0
  8. data/CHANGELOG.md +248 -0
  9. data/CODE_OF_CONDUCT.md +74 -0
  10. data/Gemfile +18 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +1311 -0
  13. data/Rakefile +8 -0
  14. data/_config.yml +1 -0
  15. data/app/controllers/cloudtasker/worker_controller.rb +107 -0
  16. data/bin/console +15 -0
  17. data/bin/setup +8 -0
  18. data/cloudtasker.gemspec +42 -0
  19. data/config/routes.rb +5 -0
  20. data/docs/BATCH_JOBS.md +144 -0
  21. data/docs/CRON_JOBS.md +129 -0
  22. data/docs/STORABLE_JOBS.md +68 -0
  23. data/docs/UNIQUE_JOBS.md +190 -0
  24. data/exe/cloudtasker +30 -0
  25. data/gemfiles/.bundle/config +2 -0
  26. data/gemfiles/google_cloud_tasks_1.0.gemfile +17 -0
  27. data/gemfiles/google_cloud_tasks_1.1.gemfile +17 -0
  28. data/gemfiles/google_cloud_tasks_1.2.gemfile +17 -0
  29. data/gemfiles/google_cloud_tasks_1.3.gemfile +17 -0
  30. data/gemfiles/google_cloud_tasks_1.4.gemfile +17 -0
  31. data/gemfiles/google_cloud_tasks_1.5.gemfile +17 -0
  32. data/gemfiles/google_cloud_tasks_2.0.gemfile +17 -0
  33. data/gemfiles/google_cloud_tasks_2.1.gemfile +17 -0
  34. data/gemfiles/rails_6.1.gemfile +20 -0
  35. data/gemfiles/rails_7.0.gemfile +18 -0
  36. data/gemfiles/rails_7.1.gemfile +18 -0
  37. data/gemfiles/rails_8.0.gemfile +18 -0
  38. data/gemfiles/rails_8.1.gemfile +18 -0
  39. data/gemfiles/semantic_logger_3.4.gemfile +16 -0
  40. data/gemfiles/semantic_logger_4.6.gemfile +16 -0
  41. data/gemfiles/semantic_logger_4.7.0.gemfile +16 -0
  42. data/gemfiles/semantic_logger_4.7.2.gemfile +16 -0
  43. data/lib/active_job/queue_adapters/cloudtasker_adapter.rb +89 -0
  44. data/lib/cloudtasker/authentication_error.rb +6 -0
  45. data/lib/cloudtasker/authenticator.rb +90 -0
  46. data/lib/cloudtasker/backend/google_cloud_task_v1.rb +228 -0
  47. data/lib/cloudtasker/backend/google_cloud_task_v2.rb +231 -0
  48. data/lib/cloudtasker/backend/memory_task.rb +202 -0
  49. data/lib/cloudtasker/backend/redis_task.rb +291 -0
  50. data/lib/cloudtasker/batch/batch_progress.rb +142 -0
  51. data/lib/cloudtasker/batch/extension/worker.rb +13 -0
  52. data/lib/cloudtasker/batch/job.rb +558 -0
  53. data/lib/cloudtasker/batch/middleware/server.rb +14 -0
  54. data/lib/cloudtasker/batch/middleware.rb +25 -0
  55. data/lib/cloudtasker/batch.rb +5 -0
  56. data/lib/cloudtasker/cli.rb +194 -0
  57. data/lib/cloudtasker/cloud_task.rb +130 -0
  58. data/lib/cloudtasker/config.rb +319 -0
  59. data/lib/cloudtasker/cron/job.rb +205 -0
  60. data/lib/cloudtasker/cron/middleware/server.rb +14 -0
  61. data/lib/cloudtasker/cron/middleware.rb +20 -0
  62. data/lib/cloudtasker/cron/schedule.rb +308 -0
  63. data/lib/cloudtasker/cron.rb +5 -0
  64. data/lib/cloudtasker/dead_worker_error.rb +6 -0
  65. data/lib/cloudtasker/engine.rb +24 -0
  66. data/lib/cloudtasker/invalid_worker_error.rb +6 -0
  67. data/lib/cloudtasker/local_server.rb +99 -0
  68. data/lib/cloudtasker/max_task_size_exceeded_error.rb +14 -0
  69. data/lib/cloudtasker/meta_store.rb +86 -0
  70. data/lib/cloudtasker/middleware/chain.rb +250 -0
  71. data/lib/cloudtasker/missing_worker_arguments_error.rb +6 -0
  72. data/lib/cloudtasker/redis_client.rb +166 -0
  73. data/lib/cloudtasker/retry_worker_error.rb +6 -0
  74. data/lib/cloudtasker/storable/worker.rb +78 -0
  75. data/lib/cloudtasker/storable.rb +3 -0
  76. data/lib/cloudtasker/testing.rb +184 -0
  77. data/lib/cloudtasker/unique_job/conflict_strategy/base_strategy.rb +39 -0
  78. data/lib/cloudtasker/unique_job/conflict_strategy/raise.rb +28 -0
  79. data/lib/cloudtasker/unique_job/conflict_strategy/reject.rb +11 -0
  80. data/lib/cloudtasker/unique_job/conflict_strategy/reschedule.rb +30 -0
  81. data/lib/cloudtasker/unique_job/job.rb +168 -0
  82. data/lib/cloudtasker/unique_job/lock/base_lock.rb +70 -0
  83. data/lib/cloudtasker/unique_job/lock/no_op.rb +11 -0
  84. data/lib/cloudtasker/unique_job/lock/until_completed.rb +40 -0
  85. data/lib/cloudtasker/unique_job/lock/until_executed.rb +36 -0
  86. data/lib/cloudtasker/unique_job/lock/until_executing.rb +30 -0
  87. data/lib/cloudtasker/unique_job/lock/while_executing.rb +25 -0
  88. data/lib/cloudtasker/unique_job/lock_error.rb +8 -0
  89. data/lib/cloudtasker/unique_job/middleware/client.rb +15 -0
  90. data/lib/cloudtasker/unique_job/middleware/server.rb +14 -0
  91. data/lib/cloudtasker/unique_job/middleware.rb +36 -0
  92. data/lib/cloudtasker/unique_job.rb +32 -0
  93. data/lib/cloudtasker/version.rb +5 -0
  94. data/lib/cloudtasker/worker.rb +487 -0
  95. data/lib/cloudtasker/worker_handler.rb +250 -0
  96. data/lib/cloudtasker/worker_logger.rb +231 -0
  97. data/lib/cloudtasker/worker_wrapper.rb +52 -0
  98. data/lib/cloudtasker.rb +57 -0
  99. data/lib/tasks/setup_queue.rake +20 -0
  100. metadata +241 -0
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudtasker
4
+ module Backend
5
+ # Manage local tasks pushed to memory.
6
+ # Used for testing.
7
+ class MemoryTask
8
+ attr_accessor :job_retries
9
+ attr_reader :id, :http_request, :schedule_time, :queue
10
+
11
+ #
12
+ # Return true if we are in test inline execution mode.
13
+ #
14
+ # @return [Boolean] True if inline mode enabled.
15
+ #
16
+ def self.inline_mode?
17
+ defined?(Cloudtasker::Testing) && Cloudtasker::Testing.inline?
18
+ end
19
+
20
+ #
21
+ # Return true if errors must be raised immediately
22
+ #
23
+ # @return [Boolean] True if raise error mode is enabled.
24
+ #
25
+ def self.raise_errors?
26
+ defined?(Cloudtasker::Testing) && Cloudtasker::Testing.raise_errors?
27
+ end
28
+
29
+ #
30
+ # Return the task queue. A worker class name
31
+ #
32
+ # @return [Array<Hash>] <description>
33
+ #
34
+ def self.queue
35
+ @queue ||= []
36
+ end
37
+
38
+ #
39
+ # Run all Tasks in the queue. Optionally filter which tasks to run based
40
+ # on the worker class name.
41
+ #
42
+ # @param [String] worker_class_name Run tasks for a specific worker class name.
43
+ #
44
+ # @return [Array<any>] The return values of the workers perform method.
45
+ #
46
+ def self.drain(worker_class_name = nil)
47
+ all(worker_class_name).map(&:execute)
48
+ end
49
+
50
+ #
51
+ # Return all enqueued tasks. A worker class name can be specified
52
+ # to filter the returned results.
53
+ #
54
+ # @param [String] worker_class_name Filter tasks on worker class name.
55
+ #
56
+ # @return [Array<Cloudtasker::Backend::MemoryTask>] All the tasks
57
+ #
58
+ def self.all(worker_class_name = nil)
59
+ # Always return a copy of the queue so that tasks can safely be removed
60
+ # (via #execute) without tampering with the enumerator
61
+ if worker_class_name
62
+ queue.select { |e| e.worker_class_name == worker_class_name }
63
+ else
64
+ queue.dup
65
+ end
66
+ end
67
+
68
+ #
69
+ # Push a job to the queue.
70
+ #
71
+ # @param [Hash] payload The Cloud Task payload.
72
+ #
73
+ def self.create(payload)
74
+ id = payload[:id] || SecureRandom.uuid
75
+ payload = payload.merge(schedule_time: payload[:schedule_time].to_i)
76
+
77
+ # Save task
78
+ task = new(**payload, id: id)
79
+ queue << task
80
+
81
+ # Execute task immediately if in testing and inline mode enabled
82
+ task.execute if inline_mode?
83
+
84
+ task
85
+ end
86
+
87
+ #
88
+ # Get a task by id.
89
+ #
90
+ # @param [String] id The id of the task.
91
+ #
92
+ # @return [Cloudtasker::Backend::MemoryTask, nil] The task.
93
+ #
94
+ def self.find(id)
95
+ queue.find { |e| e.id == id }
96
+ end
97
+
98
+ #
99
+ # Delete a task by id.
100
+ #
101
+ # @param [String] id The task id.
102
+ #
103
+ def self.delete(id)
104
+ queue.reject! { |e| e.id == id }
105
+ end
106
+
107
+ #
108
+ # Clear the queue.
109
+ #
110
+ # @param [String] worker_class_name Filter jobs on worker class name.
111
+ #
112
+ # @return [Array<Cloudtasker::Backend::MemoryTask>] The updated queue
113
+ #
114
+ def self.clear(worker_class_name = nil)
115
+ if worker_class_name
116
+ queue.reject! { |e| e.worker_class_name == worker_class_name }
117
+ else
118
+ queue.clear
119
+ end
120
+ end
121
+
122
+ #
123
+ # Build a new instance of the class.
124
+ #
125
+ # @param [String] id The ID of the task.
126
+ # @param [Hash] http_request The HTTP request content.
127
+ # @param [Integer] schedule_time When to run the task (Unix timestamp)
128
+ #
129
+ def initialize(id:, http_request:, schedule_time: nil, queue: nil, job_retries: 0, **_xargs)
130
+ @id = id
131
+ @http_request = http_request
132
+ @schedule_time = Time.at(schedule_time || 0)
133
+ @queue = queue
134
+ @job_retries = job_retries || 0
135
+ end
136
+
137
+ #
138
+ # Return task payload.
139
+ #
140
+ # @return [Hash] The task payload.
141
+ #
142
+ def payload
143
+ @payload ||= JSON.parse(http_request[:body], symbolize_names: true)
144
+ end
145
+
146
+ #
147
+ # Return the worker class from the task payload.
148
+ #
149
+ # @return [String] The task worker class name.
150
+ #
151
+ def worker_class_name
152
+ payload[:worker]
153
+ end
154
+
155
+ #
156
+ # Return a hash description of the task.
157
+ #
158
+ # @return [Hash] A hash description of the task.
159
+ #
160
+ def to_h
161
+ {
162
+ id: id,
163
+ http_request: http_request,
164
+ schedule_time: schedule_time.to_i,
165
+ queue: queue
166
+ }
167
+ end
168
+
169
+ #
170
+ # Execute the task.
171
+ #
172
+ # @return [Any] The return value of the worker perform method.
173
+ #
174
+ def execute
175
+ # Execute worker
176
+ worker_payload = payload.merge(job_retries: job_retries, task_id: id)
177
+ resp = WorkerHandler.with_worker_handling(worker_payload, &:execute)
178
+
179
+ # Delete task
180
+ self.class.delete(id)
181
+ resp
182
+ rescue DeadWorkerError => e
183
+ self.class.delete(id)
184
+ raise(e) if self.class.raise_errors?
185
+ rescue StandardError => e
186
+ self.job_retries += 1
187
+ raise(e) if self.class.raise_errors?
188
+ end
189
+
190
+ #
191
+ # Equality operator.
192
+ #
193
+ # @param [Any] other The object to compare.
194
+ #
195
+ # @return [Boolean] True if the object is equal.
196
+ #
197
+ def ==(other)
198
+ other.is_a?(self.class) && other.id == id
199
+ end
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,291 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cloudtasker/redis_client'
4
+ require 'net/http'
5
+
6
+ module Cloudtasker
7
+ module Backend
8
+ # Manage local tasks pushed to Redis
9
+ class RedisTask
10
+ attr_reader :id, :http_request, :schedule_time, :retries, :queue, :dispatch_deadline
11
+
12
+ RETRY_INTERVAL = Config::LOCAL_SERVER_RETRY_DELAY
13
+
14
+ #
15
+ # Return the Cloudtasker redis client
16
+ #
17
+ # @return [Cloudtasker::RedisClient] The cloudtasker redis client.
18
+ #
19
+ def self.redis
20
+ @redis ||= RedisClient.new
21
+ end
22
+
23
+ #
24
+ # Return a namespaced key.
25
+ #
26
+ # @param [String, Symbol, nil] val The key to namespace
27
+ #
28
+ # @return [String] The namespaced key.
29
+ #
30
+ def self.key(val = nil)
31
+ [to_s.underscore, val].compact.map(&:to_s).join('/')
32
+ end
33
+
34
+ #
35
+ # Return all tasks stored in Redis.
36
+ #
37
+ # @return [Array<Cloudtasker::Backend::RedisTask>] All the tasks.
38
+ #
39
+ def self.all
40
+ if redis.exists?(key)
41
+ # Use Schedule Set if available
42
+ redis.smembers(key).map { |id| find(id) }.compact
43
+ else
44
+ # Fallback to redis key matching and migrate tasks
45
+ # to use Task Set instead.
46
+ redis.search(key('*')).map do |gid|
47
+ task_id = gid.sub(key(''), '')
48
+ redis.sadd(key, [task_id])
49
+ find(task_id)
50
+ end
51
+ end
52
+ end
53
+
54
+ #
55
+ # Return all tasks ready to process.
56
+ #
57
+ # @param [String] queue The queue to retrieve items from.
58
+ #
59
+ # @return [Array<Cloudtasker::Backend::RedisTask>] All the tasks ready to process.
60
+ #
61
+ def self.ready_to_process(queue = nil)
62
+ list = all.select { |e| e.schedule_time <= Time.now }
63
+ list = list.select { |e| e.queue == queue } if queue
64
+ list
65
+ end
66
+
67
+ #
68
+ # Retrieve and remove a task from the queue.
69
+ #
70
+ # @param [String] queue The queue to retrieve items from.
71
+ #
72
+ # @return [Cloudtasker::Backend::RedisTask] A task ready to process.
73
+ #
74
+ def self.pop(queue = nil)
75
+ redis.with_lock('cloudtasker/server') do
76
+ ready_to_process(queue).first&.tap(&:destroy)
77
+ end
78
+ end
79
+
80
+ #
81
+ # Push a job to the queue.
82
+ #
83
+ # @param [Hash] payload The Cloud Task payload.
84
+ #
85
+ def self.create(payload)
86
+ id = SecureRandom.uuid
87
+ payload = payload.merge(schedule_time: payload[:schedule_time].to_i)
88
+
89
+ # Save job
90
+ redis.write(key(id), payload)
91
+ redis.sadd(key, [id])
92
+ new(**payload, id: id)
93
+ end
94
+
95
+ #
96
+ # Get a task by id.
97
+ #
98
+ # @param [String] id The id of the task.
99
+ #
100
+ # @return [Cloudtasker::Backend::RedisTask, nil] The task.
101
+ #
102
+ def self.find(id)
103
+ gid = key(id)
104
+ return nil unless (payload = redis.fetch(gid))
105
+
106
+ new(**payload, id: id)
107
+ end
108
+
109
+ #
110
+ # Delete a task by id.
111
+ #
112
+ # @param [String] id The task id.
113
+ #
114
+ def self.delete(id)
115
+ redis.srem(key, [id])
116
+ redis.del(key(id))
117
+ end
118
+
119
+ #
120
+ # Build a new instance of the class.
121
+ #
122
+ # @param [String] id The ID of the task.
123
+ # @param [Hash] http_request The HTTP request content.
124
+ # @param [Integer] schedule_time When to run the task (Unix timestamp)
125
+ # @param [Integer] retries The number of times the job failed.
126
+ # @param [Integer] dispatch_deadline The dispatch_deadline in seconds.
127
+ #
128
+ def initialize(id:, http_request:, schedule_time: nil, retries: 0, queue: nil, dispatch_deadline: nil)
129
+ @id = id
130
+ @http_request = http_request
131
+ @schedule_time = Time.at(schedule_time || 0)
132
+ @retries = retries || 0
133
+ @queue = queue || Config::DEFAULT_JOB_QUEUE
134
+ @dispatch_deadline = dispatch_deadline || Config::DEFAULT_DISPATCH_DEADLINE
135
+ end
136
+
137
+ #
138
+ # Return the redis client.
139
+ #
140
+ # @return [Class] The RedisClient.
141
+ #
142
+ def redis
143
+ self.class.redis
144
+ end
145
+
146
+ #
147
+ # Return a hash description of the task.
148
+ #
149
+ # @return [Hash] A hash description of the task.
150
+ #
151
+ def to_h
152
+ {
153
+ id: id,
154
+ http_request: http_request,
155
+ schedule_time: schedule_time.to_i,
156
+ retries: retries,
157
+ queue: queue,
158
+ dispatch_deadline: dispatch_deadline
159
+ }
160
+ end
161
+
162
+ #
163
+ # Return the namespaced task id
164
+ #
165
+ # @return [<Type>] The namespaced task id
166
+ #
167
+ def gid
168
+ self.class.key(id)
169
+ end
170
+
171
+ #
172
+ # Retry the task later.
173
+ #
174
+ # @param [Integer] interval The delay in seconds before retrying the task
175
+ # @param [Hash] opts Additional options
176
+ # @option opts [Boolean] :is_error Increase number of retries. Default to true.
177
+ #
178
+ def retry_later(interval, opts = {})
179
+ is_error = opts.to_h.fetch(:is_error, true)
180
+
181
+ redis.write(
182
+ gid,
183
+ retries: is_error ? retries + 1 : retries,
184
+ http_request: http_request,
185
+ schedule_time: (Time.now + interval).to_i,
186
+ queue: queue,
187
+ dispatch_deadline: dispatch_deadline
188
+ )
189
+ redis.sadd(self.class.key, [id])
190
+ end
191
+
192
+ #
193
+ # Remove the task from the queue.
194
+ #
195
+ def destroy
196
+ self.class.delete(id)
197
+ end
198
+
199
+ #
200
+ # Deliver the task to the processing endpoint.
201
+ #
202
+ def deliver
203
+ Cloudtasker.logger.info(format_log_message('Processing task...'))
204
+
205
+ # Send request
206
+ resp = http_client.request(request_content)
207
+
208
+ # Delete task if successful
209
+ if resp.code.to_s =~ /20\d/
210
+ destroy
211
+ Cloudtasker.logger.info(format_log_message('Task handled successfully'))
212
+ else
213
+ retry_later(RETRY_INTERVAL)
214
+ Cloudtasker.logger.info(format_log_message("Task failure - Retry in #{RETRY_INTERVAL} seconds..."))
215
+ end
216
+
217
+ resp
218
+ rescue Errno::ECONNREFUSED
219
+ retry_later(RETRY_INTERVAL)
220
+ Cloudtasker.logger.info(format_log_message("Processor not ready - Retry in #{RETRY_INTERVAL} seconds..."))
221
+ rescue Net::ReadTimeout
222
+ retry_later(RETRY_INTERVAL)
223
+ Cloudtasker.logger.info(
224
+ format_log_message(
225
+ "Task deadline exceeded (#{dispatch_deadline}s) - Retry in #{RETRY_INTERVAL} seconds..."
226
+ )
227
+ )
228
+ end
229
+
230
+ #
231
+ # Equality operator.
232
+ #
233
+ # @param [Any] other The object to compare.
234
+ #
235
+ # @return [Boolean] True if the object is equal.
236
+ #
237
+ def ==(other)
238
+ other.is_a?(self.class) && other.id == id
239
+ end
240
+
241
+ private
242
+
243
+ #
244
+ # Format a log message
245
+ #
246
+ # @param [String] msg The message to log.
247
+ #
248
+ # @return [String] The formatted message
249
+ #
250
+ def format_log_message(msg)
251
+ "[Cloudtasker/Server][#{id}] #{msg}"
252
+ end
253
+
254
+ #
255
+ # Return the HTTP client.
256
+ #
257
+ # @return [Net::HTTP] The http_client.
258
+ #
259
+ def http_client
260
+ @http_client ||=
261
+ begin
262
+ uri = URI(http_request[:url])
263
+ http = Net::HTTP.new(uri.host, uri.port).tap { |e| e.read_timeout = dispatch_deadline }
264
+ http.use_ssl = true if uri.instance_of?(URI::HTTPS)
265
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE unless Cloudtasker.config.local_server_ssl_verify
266
+ http
267
+ end
268
+ end
269
+
270
+ #
271
+ # Return the HTTP request to send
272
+ #
273
+ # @return [Net::HTTP::Post] The http request
274
+ #
275
+ def request_content
276
+ @request_content ||= begin
277
+ uri = URI(http_request[:url])
278
+ req = Net::HTTP::Post.new(uri.path, http_request[:headers])
279
+
280
+ # Add task headers
281
+ req[Cloudtasker::Config::TASK_ID_HEADER] = id
282
+ req[Cloudtasker::Config::RETRY_HEADER] = retries
283
+
284
+ # Set job payload
285
+ req.body = http_request[:body]
286
+ req
287
+ end
288
+ end
289
+ end
290
+ end
291
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fugit'
4
+
5
+ module Cloudtasker
6
+ module Batch
7
+ # Capture the progress of a batch
8
+ class BatchProgress
9
+ attr_reader :batches
10
+
11
+ #
12
+ # Build a new instance of the class.
13
+ #
14
+ # @param [Array<Cloudtasker::Batch::Job>] batches The batches to consider
15
+ #
16
+ def initialize(batches = [])
17
+ @batches = batches
18
+ end
19
+
20
+ # Count the number of items in a given status
21
+
22
+ #
23
+ # Count the number of items in a given status
24
+ #
25
+ # @param [String] status The status to count
26
+ #
27
+ # @return [Integer] The number of jobs in the status
28
+ #
29
+ def count(status = 'all')
30
+ batches.sum { |e| e.batch_state_count(status) }
31
+ end
32
+
33
+ #
34
+ # Return the total number jobs.
35
+ #
36
+ # @return [Integer] The number number of jobs.
37
+ #
38
+ def total
39
+ count
40
+ end
41
+
42
+ #
43
+ # Return the number of completed jobs.
44
+ #
45
+ # @return [Integer] The number of completed jobs.
46
+ #
47
+ def completed
48
+ @completed ||= count('completed')
49
+ end
50
+
51
+ #
52
+ # Return the number of scheduled jobs.
53
+ #
54
+ # @return [Integer] The number of scheduled jobs.
55
+ #
56
+ def scheduled
57
+ @scheduled ||= count('scheduled')
58
+ end
59
+
60
+ #
61
+ # Return the number of processing jobs.
62
+ #
63
+ # @return [Integer] The number of processing jobs.
64
+ #
65
+ def processing
66
+ @processing ||= count('processing')
67
+ end
68
+
69
+ #
70
+ # Return the number of jobs with errors.
71
+ #
72
+ # @return [Integer] The number of errored jobs.
73
+ #
74
+ def errored
75
+ @errored ||= count('errored')
76
+ end
77
+
78
+ #
79
+ # Return the number of dead jobs.
80
+ #
81
+ # @return [Integer] The number of dead jobs.
82
+ #
83
+ def dead
84
+ @dead ||= count('dead')
85
+ end
86
+
87
+ #
88
+ # Return the number of jobs not completed yet.
89
+ #
90
+ # @return [Integer] The number of jobs pending.
91
+ #
92
+ def pending
93
+ total - done
94
+ end
95
+
96
+ #
97
+ # Return the number of jobs completed or dead.
98
+ #
99
+ # @return [Integer] The number of jobs done.
100
+ #
101
+ def done
102
+ completed + dead
103
+ end
104
+
105
+ #
106
+ # Return the batch progress percentage.
107
+ #
108
+ # A `min_total` can be specified to linearize the calculation, while jobs get added at
109
+ # the start of the batch.
110
+ #
111
+ # Similarly a `smoothing` parameter can be specified to add a constant to the total
112
+ # and linearize the calculation, which becomes: `done / (total + smoothing)`
113
+ #
114
+ # @param [Integer] min_total The minimum for the total number of jobs
115
+ # @param [Integer] smoothing An additive smoothing for the total number of jobs
116
+ #
117
+ # @return [Float] The progress percentage.
118
+ #
119
+ def percent(min_total: 0, smoothing: 0)
120
+ # Get the total value to use
121
+ actual_total = [min_total, total + smoothing].max
122
+
123
+ # Abort if we cannot divide
124
+ return 0 if actual_total.zero?
125
+
126
+ # Calculate progress
127
+ (done.to_f / actual_total) * 100
128
+ end
129
+
130
+ #
131
+ # Add a batch progress to another one.
132
+ #
133
+ # @param [Cloudtasker::Batch::BatchProgress] progress The progress to add.
134
+ #
135
+ # @return [Cloudtasker::Batch::BatchProgress] The sum of the two batch progresses.
136
+ #
137
+ def +(other)
138
+ self.class.new(batches + other.batches)
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudtasker
4
+ module Batch
5
+ module Extension
6
+ # Include batch related methods onto Cloudtasker::Worker
7
+ # See: Cloudtasker::Batch::Middleware#configure
8
+ module Worker
9
+ attr_accessor :batch, :parent_batch
10
+ end
11
+ end
12
+ end
13
+ end