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
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cloudtasker/redis_client'
4
+
5
+ module Cloudtasker
6
+ module Backend
7
+ # Manage local tasks pushed to memory.
8
+ # Used for testing.
9
+ class MemoryTask
10
+ attr_reader :id, :http_request, :schedule_time
11
+
12
+ #
13
+ # Return the task queue. A worker class name
14
+ #
15
+ # @return [Array<Hash>] <description>
16
+ #
17
+ def self.queue
18
+ @queue ||= []
19
+ end
20
+
21
+ #
22
+ # Return the workers currently in the queue.
23
+ #
24
+ # @param [String] worker_class_name Filter jobs on worker class name.
25
+ #
26
+ # @return [Array<Cloudtasker::Worker] The list of workers
27
+ #
28
+ def self.jobs(worker_class_name = nil)
29
+ all(worker_class_name).map(&:worker)
30
+ end
31
+
32
+ #
33
+ # Run all Tasks in the queue. Optionally filter which tasks to run based
34
+ # on the worker class name.
35
+ #
36
+ # @param [String] worker_class_name Run tasks for a specific worker class name.
37
+ #
38
+ # @return [Array<any>] The return values of the workers perform method.
39
+ #
40
+ def self.drain(worker_class_name = nil)
41
+ all(worker_class_name).map(&:execute)
42
+ end
43
+
44
+ #
45
+ # Return all enqueued tasks. A worker class name can be specified
46
+ # to filter the returned results.
47
+ #
48
+ # @param [String] worker_class_name Filter tasks on worker class name.
49
+ #
50
+ # @return [Array<Cloudtasker::Backend::MemoryTask>] All the tasks
51
+ #
52
+ def self.all(worker_class_name = nil)
53
+ list = queue
54
+ list = list.select { |e| e.worker_class_name == worker_class_name } if worker_class_name
55
+ list
56
+ end
57
+
58
+ #
59
+ # Push a job to the queue.
60
+ #
61
+ # @param [Hash] payload The Cloud Task payload.
62
+ #
63
+ def self.create(payload)
64
+ id = payload[:id] || SecureRandom.uuid
65
+ payload = payload.merge(schedule_time: payload[:schedule_time].to_i)
66
+
67
+ # Save task
68
+ task = new(payload.merge(id: id))
69
+ queue << task
70
+
71
+ # Execute task immediately if in testing and inline mode enabled
72
+ task.execute if defined?(Cloudtasker::Testing) && Cloudtasker::Testing.inline?
73
+
74
+ task
75
+ end
76
+
77
+ #
78
+ # Get a task by id.
79
+ #
80
+ # @param [String] id The id of the task.
81
+ #
82
+ # @return [Cloudtasker::Backend::MemoryTask, nil] The task.
83
+ #
84
+ def self.find(id)
85
+ queue.find { |e| e.id == id }
86
+ end
87
+
88
+ #
89
+ # Delete a task by id.
90
+ #
91
+ # @param [String] id The task id.
92
+ #
93
+ def self.delete(id)
94
+ queue.reject! { |e| e.id == id }
95
+ end
96
+
97
+ #
98
+ # Clear the queue.
99
+ #
100
+ # @param [String] worker_class_name Filter jobs on worker class name.
101
+ #
102
+ # @return [Array<Cloudtasker::Backend::MemoryTask>] The updated queue
103
+ #
104
+ def self.clear(worker_class_name = nil)
105
+ if worker_class_name
106
+ queue.reject! { |e| e.worker_class_name == worker_class_name }
107
+ else
108
+ queue.clear
109
+ end
110
+ end
111
+
112
+ #
113
+ # Build a new instance of the class.
114
+ #
115
+ # @param [String] id The ID of the task.
116
+ # @param [Hash] http_request The HTTP request content.
117
+ # @param [Integer] schedule_time When to run the task (Unix timestamp)
118
+ #
119
+ def initialize(id:, http_request:, schedule_time: nil)
120
+ @id = id
121
+ @http_request = http_request
122
+ @schedule_time = Time.at(schedule_time || 0)
123
+ end
124
+
125
+ #
126
+ # Return task payload.
127
+ #
128
+ # @return [Hash] The task payload.
129
+ #
130
+ def payload
131
+ @payload ||= JSON.parse(http_request.dig(:body), symbolize_names: true)
132
+ end
133
+
134
+ #
135
+ # Return the worker class from the task payload.
136
+ #
137
+ # @return [String] The task worker class name.
138
+ #
139
+ def worker_class_name
140
+ payload[:worker]
141
+ end
142
+
143
+ #
144
+ # Return a hash description of the task.
145
+ #
146
+ # @return [Hash] A hash description of the task.
147
+ #
148
+ def to_h
149
+ {
150
+ id: id,
151
+ http_request: http_request,
152
+ schedule_time: schedule_time.to_i
153
+ }
154
+ end
155
+
156
+ #
157
+ # Return the worker attached to this task.
158
+ #
159
+ # @return [Cloudtasker::Worker] The task worker.
160
+ #
161
+ def worker
162
+ @worker ||= Worker.from_hash(payload)
163
+ end
164
+
165
+ #
166
+ # Execute the task.
167
+ #
168
+ # @return [Any] The return value of the worker perform method.
169
+ #
170
+ def execute
171
+ resp = worker.execute
172
+ self.class.delete(id)
173
+ resp
174
+ rescue StandardError
175
+ worker.job_retries += 1
176
+ end
177
+
178
+ #
179
+ # Equality operator.
180
+ #
181
+ # @param [Any] other The object to compare.
182
+ #
183
+ # @return [Boolean] True if the object is equal.
184
+ #
185
+ def ==(other)
186
+ other.is_a?(self.class) && other.id == id
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,248 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cloudtasker/redis_client'
4
+
5
+ module Cloudtasker
6
+ module Backend
7
+ # Manage local tasks pushed to Redis
8
+ class RedisTask
9
+ attr_reader :id, :http_request, :schedule_time, :retries
10
+
11
+ RETRY_INTERVAL = 20 # seconds
12
+
13
+ #
14
+ # Return the cloudtasker redis client
15
+ #
16
+ # @return [Class] The redis client.
17
+ #
18
+ def self.redis
19
+ RedisClient
20
+ end
21
+
22
+ #
23
+ # Return a namespaced key.
24
+ #
25
+ # @param [String, Symbol] val The key to namespace
26
+ #
27
+ # @return [String] The namespaced key.
28
+ #
29
+ def self.key(val)
30
+ return nil if val.nil?
31
+
32
+ [to_s.underscore, val.to_s].join('/')
33
+ end
34
+
35
+ #
36
+ # Return all tasks stored in Redis.
37
+ #
38
+ # @return [Array<Cloudtasker::Backend::RedisTask>] All the tasks.
39
+ #
40
+ def self.all
41
+ redis.search(key('*')).map do |gid|
42
+ payload = redis.fetch(gid)
43
+ new(payload.merge(id: gid.sub(key(''), '')))
44
+ end
45
+ end
46
+
47
+ #
48
+ # Reeturn all tasks ready to process.
49
+ #
50
+ # @return [Array<Cloudtasker::Backend::RedisTask>] All the tasks ready to process.
51
+ #
52
+ def self.ready_to_process
53
+ all.select { |e| e.schedule_time <= Time.now }
54
+ end
55
+
56
+ #
57
+ # Retrieve and remove a task from the queue.
58
+ #
59
+ # @return [Cloudtasker::Backend::RedisTask] A task ready to process.
60
+ #
61
+ def self.pop
62
+ redis.with_lock('cloudtasker/server') do
63
+ ready_to_process.first&.tap(&:destroy)
64
+ end
65
+ end
66
+
67
+ #
68
+ # Push a job to the queue.
69
+ #
70
+ # @param [Hash] payload The Cloud Task payload.
71
+ #
72
+ def self.create(payload)
73
+ id = SecureRandom.uuid
74
+ payload = payload.merge(schedule_time: payload[:schedule_time].to_i)
75
+
76
+ # Save job
77
+ redis.write(key(id), payload)
78
+ new(payload.merge(id: id))
79
+ end
80
+
81
+ #
82
+ # Get a task by id.
83
+ #
84
+ # @param [String] id The id of the task.
85
+ #
86
+ # @return [Cloudtasker::Backend::RedisTask, nil] The task.
87
+ #
88
+ def self.find(id)
89
+ gid = key(id)
90
+ return nil unless (payload = redis.fetch(gid))
91
+
92
+ new(payload.merge(id: id))
93
+ end
94
+
95
+ #
96
+ # Delete a task by id.
97
+ #
98
+ # @param [String] id The task id.
99
+ #
100
+ def self.delete(id)
101
+ redis.del(key(id))
102
+ end
103
+
104
+ #
105
+ # Build a new instance of the class.
106
+ #
107
+ # @param [String] id The ID of the task.
108
+ # @param [Hash] http_request The HTTP request content.
109
+ # @param [Integer] schedule_time When to run the task (Unix timestamp)
110
+ # @param [Integer] retries The number of times the job failed.
111
+ #
112
+ def initialize(id:, http_request:, schedule_time: nil, retries: 0)
113
+ @id = id
114
+ @http_request = http_request
115
+ @schedule_time = Time.at(schedule_time || 0)
116
+ @retries = retries || 0
117
+ end
118
+
119
+ #
120
+ # Return the redis client.
121
+ #
122
+ # @return [Class] The RedisClient.
123
+ #
124
+ def redis
125
+ self.class.redis
126
+ end
127
+
128
+ #
129
+ # Return a hash description of the task.
130
+ #
131
+ # @return [Hash] A hash description of the task.
132
+ #
133
+ def to_h
134
+ {
135
+ id: id,
136
+ http_request: http_request,
137
+ schedule_time: schedule_time.to_i,
138
+ retries: retries
139
+ }
140
+ end
141
+
142
+ #
143
+ # Return the namespaced task id
144
+ #
145
+ # @return [<Type>] The namespaced task id
146
+ #
147
+ def gid
148
+ self.class.key(id)
149
+ end
150
+
151
+ #
152
+ # Retry the task later.
153
+ #
154
+ # @param [Integer] interval The delay in seconds before retrying the task
155
+ #
156
+ def retry_later(interval, is_error: true)
157
+ redis.write(gid,
158
+ retries: is_error ? retries + 1 : retries,
159
+ http_request: http_request,
160
+ schedule_time: (Time.now + interval).to_i)
161
+ end
162
+
163
+ #
164
+ # Remove the task from the queue.
165
+ #
166
+ def destroy
167
+ redis.del(gid)
168
+ end
169
+
170
+ #
171
+ # Deliver the task to the processing endpoint.
172
+ #
173
+ def deliver
174
+ Cloudtasker.logger.info(format_log_message('Processing task...'))
175
+
176
+ # Send request
177
+ resp = http_client.request(request_content)
178
+
179
+ # Delete task if successful
180
+ if resp.code.to_s =~ /20\d/
181
+ destroy
182
+ Cloudtasker.logger.info(format_log_message('Task handled successfully'))
183
+ else
184
+ retry_later(RETRY_INTERVAL)
185
+ Cloudtasker.logger.info(format_log_message("Task failure - Retry in #{RETRY_INTERVAL} seconds..."))
186
+ end
187
+
188
+ resp
189
+ end
190
+
191
+ #
192
+ # Equality operator.
193
+ #
194
+ # @param [Any] other The object to compare.
195
+ #
196
+ # @return [Boolean] True if the object is equal.
197
+ #
198
+ def ==(other)
199
+ other.is_a?(self.class) && other.id == id
200
+ end
201
+
202
+ private
203
+
204
+ #
205
+ # Format a log message
206
+ #
207
+ # @param [String] msg The message to log.
208
+ #
209
+ # @return [String] The formatted message
210
+ #
211
+ def format_log_message(msg)
212
+ "[Cloudtasker/Server][#{id}] #{msg}"
213
+ end
214
+
215
+ #
216
+ # Return the HTTP client.
217
+ #
218
+ # @return [Net::HTTP] The http_client.
219
+ #
220
+ def http_client
221
+ @http_client ||=
222
+ begin
223
+ uri = URI(http_request[:url])
224
+ Net::HTTP.new(uri.host, uri.port).tap { |e| e.read_timeout = 60 * 10 }
225
+ end
226
+ end
227
+
228
+ #
229
+ # Return the HTTP request to send
230
+ #
231
+ # @return [Net::HTTP::Post] The http request
232
+ #
233
+ def request_content
234
+ @request_content ||= begin
235
+ uri = URI(http_request[:url])
236
+ req = Net::HTTP::Post.new(uri.path, http_request[:headers])
237
+
238
+ # Add retries header
239
+ req['X-CloudTasks-TaskExecutionCount'] = retries
240
+
241
+ # Set job payload
242
+ req.body = http_request[:body]
243
+ req
244
+ end
245
+ end
246
+ end
247
+ end
248
+ end
@@ -53,13 +53,31 @@ module Cloudtasker
53
53
  @processing ||= count('processing')
54
54
  end
55
55
 
56
+ #
57
+ # Return the number of jobs with errors.
58
+ #
59
+ # @return [Integer] The number of errored jobs.
60
+ #
61
+ def errored
62
+ @errored ||= count('errored')
63
+ end
64
+
65
+ #
66
+ # Return the number of dead jobs.
67
+ #
68
+ # @return [Integer] The number of dead jobs.
69
+ #
70
+ def dead
71
+ @dead ||= count('dead')
72
+ end
73
+
56
74
  #
57
75
  # Return the number of jobs not completed yet.
58
76
  #
59
77
  # @return [Integer] The number of jobs pending.
60
78
  #
61
79
  def pending
62
- total - completed
80
+ total - completed - dead
63
81
  end
64
82
 
65
83
  #