cloudtasker 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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
  #