cloudtasker 0.1.0 → 0.6.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 (66) 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/CHANGELOG.md +25 -0
  7. data/Gemfile.lock +37 -4
  8. data/README.md +573 -6
  9. data/Rakefile +6 -0
  10. data/app/controllers/cloudtasker/application_controller.rb +2 -0
  11. data/app/controllers/cloudtasker/worker_controller.rb +24 -2
  12. data/cloudtasker.gemspec +7 -3
  13. data/docs/BATCH_JOBS.md +66 -0
  14. data/docs/CRON_JOBS.md +63 -0
  15. data/docs/UNIQUE_JOBS.md +127 -0
  16. data/exe/cloudtasker +15 -0
  17. data/gemfiles/.bundle/config +2 -0
  18. data/gemfiles/google_cloud_tasks_1.0.gemfile +9 -0
  19. data/gemfiles/google_cloud_tasks_1.0.gemfile.lock +263 -0
  20. data/gemfiles/google_cloud_tasks_1.1.gemfile +9 -0
  21. data/gemfiles/google_cloud_tasks_1.1.gemfile.lock +263 -0
  22. data/gemfiles/google_cloud_tasks_1.2.gemfile +9 -0
  23. data/gemfiles/google_cloud_tasks_1.2.gemfile.lock +263 -0
  24. data/gemfiles/google_cloud_tasks_1.3.gemfile +9 -0
  25. data/gemfiles/google_cloud_tasks_1.3.gemfile.lock +264 -0
  26. data/gemfiles/rails_4.0.gemfile +10 -0
  27. data/gemfiles/rails_4.1.gemfile +9 -0
  28. data/gemfiles/rails_4.2.gemfile +9 -0
  29. data/gemfiles/rails_5.0.gemfile +9 -0
  30. data/gemfiles/rails_5.1.gemfile +9 -0
  31. data/gemfiles/rails_5.2.gemfile +9 -0
  32. data/gemfiles/rails_5.2.gemfile.lock +247 -0
  33. data/gemfiles/rails_6.0.gemfile +9 -0
  34. data/gemfiles/rails_6.0.gemfile.lock +263 -0
  35. data/lib/cloudtasker.rb +21 -1
  36. data/lib/cloudtasker/backend/google_cloud_task.rb +139 -0
  37. data/lib/cloudtasker/backend/memory_task.rb +190 -0
  38. data/lib/cloudtasker/backend/redis_task.rb +249 -0
  39. data/lib/cloudtasker/batch/batch_progress.rb +19 -1
  40. data/lib/cloudtasker/batch/job.rb +88 -23
  41. data/lib/cloudtasker/batch/middleware.rb +0 -1
  42. data/lib/cloudtasker/cli.rb +194 -0
  43. data/lib/cloudtasker/cloud_task.rb +91 -0
  44. data/lib/cloudtasker/config.rb +64 -2
  45. data/lib/cloudtasker/cron/job.rb +6 -3
  46. data/lib/cloudtasker/cron/middleware.rb +0 -1
  47. data/lib/cloudtasker/cron/schedule.rb +73 -13
  48. data/lib/cloudtasker/dead_worker_error.rb +6 -0
  49. data/lib/cloudtasker/local_server.rb +74 -0
  50. data/lib/cloudtasker/railtie.rb +10 -0
  51. data/lib/cloudtasker/redis_client.rb +24 -2
  52. data/lib/cloudtasker/testing.rb +133 -0
  53. data/lib/cloudtasker/unique_job/job.rb +5 -2
  54. data/lib/cloudtasker/unique_job/lock/base_lock.rb +1 -1
  55. data/lib/cloudtasker/unique_job/lock/until_executed.rb +3 -1
  56. data/lib/cloudtasker/unique_job/lock/while_executing.rb +3 -1
  57. data/lib/cloudtasker/unique_job/middleware.rb +0 -1
  58. data/lib/cloudtasker/version.rb +1 -1
  59. data/lib/cloudtasker/worker.rb +59 -16
  60. data/lib/cloudtasker/{task.rb → worker_handler.rb} +10 -77
  61. data/lib/cloudtasker/worker_logger.rb +155 -0
  62. data/lib/tasks/setup_queue.rake +10 -0
  63. metadata +98 -9
  64. data/lib/cloudtasker/batch/config.rb +0 -11
  65. data/lib/cloudtasker/cron/config.rb +0 -11
  66. data/lib/cloudtasker/unique_job/config.rb +0 -10
@@ -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,249 @@
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
11
+
12
+ RETRY_INTERVAL = 20 # seconds
13
+
14
+ #
15
+ # Return the cloudtasker redis client
16
+ #
17
+ # @return [Class] The redis client.
18
+ #
19
+ def self.redis
20
+ RedisClient
21
+ end
22
+
23
+ #
24
+ # Return a namespaced key.
25
+ #
26
+ # @param [String, Symbol] val The key to namespace
27
+ #
28
+ # @return [String] The namespaced key.
29
+ #
30
+ def self.key(val)
31
+ return nil if val.nil?
32
+
33
+ [to_s.underscore, val.to_s].join('/')
34
+ end
35
+
36
+ #
37
+ # Return all tasks stored in Redis.
38
+ #
39
+ # @return [Array<Cloudtasker::Backend::RedisTask>] All the tasks.
40
+ #
41
+ def self.all
42
+ redis.search(key('*')).map do |gid|
43
+ payload = redis.fetch(gid)
44
+ new(payload.merge(id: gid.sub(key(''), '')))
45
+ end
46
+ end
47
+
48
+ #
49
+ # Reeturn all tasks ready to process.
50
+ #
51
+ # @return [Array<Cloudtasker::Backend::RedisTask>] All the tasks ready to process.
52
+ #
53
+ def self.ready_to_process
54
+ all.select { |e| e.schedule_time <= Time.now }
55
+ end
56
+
57
+ #
58
+ # Retrieve and remove a task from the queue.
59
+ #
60
+ # @return [Cloudtasker::Backend::RedisTask] A task ready to process.
61
+ #
62
+ def self.pop
63
+ redis.with_lock('cloudtasker/server') do
64
+ ready_to_process.first&.tap(&:destroy)
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 = SecureRandom.uuid
75
+ payload = payload.merge(schedule_time: payload[:schedule_time].to_i)
76
+
77
+ # Save job
78
+ redis.write(key(id), payload)
79
+ new(payload.merge(id: id))
80
+ end
81
+
82
+ #
83
+ # Get a task by id.
84
+ #
85
+ # @param [String] id The id of the task.
86
+ #
87
+ # @return [Cloudtasker::Backend::RedisTask, nil] The task.
88
+ #
89
+ def self.find(id)
90
+ gid = key(id)
91
+ return nil unless (payload = redis.fetch(gid))
92
+
93
+ new(payload.merge(id: id))
94
+ end
95
+
96
+ #
97
+ # Delete a task by id.
98
+ #
99
+ # @param [String] id The task id.
100
+ #
101
+ def self.delete(id)
102
+ redis.del(key(id))
103
+ end
104
+
105
+ #
106
+ # Build a new instance of the class.
107
+ #
108
+ # @param [String] id The ID of the task.
109
+ # @param [Hash] http_request The HTTP request content.
110
+ # @param [Integer] schedule_time When to run the task (Unix timestamp)
111
+ # @param [Integer] retries The number of times the job failed.
112
+ #
113
+ def initialize(id:, http_request:, schedule_time: nil, retries: 0)
114
+ @id = id
115
+ @http_request = http_request
116
+ @schedule_time = Time.at(schedule_time || 0)
117
+ @retries = retries || 0
118
+ end
119
+
120
+ #
121
+ # Return the redis client.
122
+ #
123
+ # @return [Class] The RedisClient.
124
+ #
125
+ def redis
126
+ self.class.redis
127
+ end
128
+
129
+ #
130
+ # Return a hash description of the task.
131
+ #
132
+ # @return [Hash] A hash description of the task.
133
+ #
134
+ def to_h
135
+ {
136
+ id: id,
137
+ http_request: http_request,
138
+ schedule_time: schedule_time.to_i,
139
+ retries: retries
140
+ }
141
+ end
142
+
143
+ #
144
+ # Return the namespaced task id
145
+ #
146
+ # @return [<Type>] The namespaced task id
147
+ #
148
+ def gid
149
+ self.class.key(id)
150
+ end
151
+
152
+ #
153
+ # Retry the task later.
154
+ #
155
+ # @param [Integer] interval The delay in seconds before retrying the task
156
+ #
157
+ def retry_later(interval, is_error: true)
158
+ redis.write(gid,
159
+ retries: is_error ? retries + 1 : retries,
160
+ http_request: http_request,
161
+ schedule_time: (Time.now + interval).to_i)
162
+ end
163
+
164
+ #
165
+ # Remove the task from the queue.
166
+ #
167
+ def destroy
168
+ redis.del(gid)
169
+ end
170
+
171
+ #
172
+ # Deliver the task to the processing endpoint.
173
+ #
174
+ def deliver
175
+ Cloudtasker.logger.info(format_log_message('Processing task...'))
176
+
177
+ # Send request
178
+ resp = http_client.request(request_content)
179
+
180
+ # Delete task if successful
181
+ if resp.code.to_s =~ /20\d/
182
+ destroy
183
+ Cloudtasker.logger.info(format_log_message('Task handled successfully'))
184
+ else
185
+ retry_later(RETRY_INTERVAL)
186
+ Cloudtasker.logger.info(format_log_message("Task failure - Retry in #{RETRY_INTERVAL} seconds..."))
187
+ end
188
+
189
+ resp
190
+ end
191
+
192
+ #
193
+ # Equality operator.
194
+ #
195
+ # @param [Any] other The object to compare.
196
+ #
197
+ # @return [Boolean] True if the object is equal.
198
+ #
199
+ def ==(other)
200
+ other.is_a?(self.class) && other.id == id
201
+ end
202
+
203
+ private
204
+
205
+ #
206
+ # Format a log message
207
+ #
208
+ # @param [String] msg The message to log.
209
+ #
210
+ # @return [String] The formatted message
211
+ #
212
+ def format_log_message(msg)
213
+ "[Cloudtasker/Server][#{id}] #{msg}"
214
+ end
215
+
216
+ #
217
+ # Return the HTTP client.
218
+ #
219
+ # @return [Net::HTTP] The http_client.
220
+ #
221
+ def http_client
222
+ @http_client ||=
223
+ begin
224
+ uri = URI(http_request[:url])
225
+ Net::HTTP.new(uri.host, uri.port).tap { |e| e.read_timeout = 60 * 10 }
226
+ end
227
+ end
228
+
229
+ #
230
+ # Return the HTTP request to send
231
+ #
232
+ # @return [Net::HTTP::Post] The http request
233
+ #
234
+ def request_content
235
+ @request_content ||= begin
236
+ uri = URI(http_request[:url])
237
+ req = Net::HTTP::Post.new(uri.path, http_request[:headers])
238
+
239
+ # Add retries header
240
+ req['X-CloudTasks-TaskExecutionCount'] = retries
241
+
242
+ # Set job payload
243
+ req.body = http_request[:body]
244
+ req
245
+ end
246
+ end
247
+ end
248
+ end
249
+ 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
  #