cloudtasker 0.4.0 → 0.8.1

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.rubocop.yml +5 -0
  4. data/.travis.yml +3 -3
  5. data/CHANGELOG.md +37 -0
  6. data/README.md +163 -26
  7. data/Rakefile +6 -0
  8. data/_config.yml +1 -0
  9. data/app/controllers/cloudtasker/worker_controller.rb +6 -6
  10. data/cloudtasker.gemspec +3 -2
  11. data/docs/BATCH_JOBS.md +29 -4
  12. data/docs/CRON_JOBS.md +18 -14
  13. data/exe/cloudtasker +13 -1
  14. data/gemfiles/google_cloud_tasks_1.0.gemfile.lock +22 -5
  15. data/gemfiles/google_cloud_tasks_1.1.gemfile.lock +22 -5
  16. data/gemfiles/google_cloud_tasks_1.2.gemfile.lock +23 -6
  17. data/gemfiles/google_cloud_tasks_1.3.gemfile.lock +22 -5
  18. data/gemfiles/rails_5.2.gemfile.lock +22 -5
  19. data/gemfiles/rails_6.0.gemfile.lock +23 -6
  20. data/lib/cloudtasker.rb +0 -1
  21. data/lib/cloudtasker/backend/google_cloud_task.rb +41 -8
  22. data/lib/cloudtasker/backend/memory_task.rb +5 -3
  23. data/lib/cloudtasker/backend/redis_task.rb +24 -13
  24. data/lib/cloudtasker/batch/batch_progress.rb +11 -2
  25. data/lib/cloudtasker/batch/job.rb +24 -9
  26. data/lib/cloudtasker/cli.rb +6 -5
  27. data/lib/cloudtasker/cloud_task.rb +4 -2
  28. data/lib/cloudtasker/config.rb +18 -9
  29. data/lib/cloudtasker/cron/job.rb +2 -2
  30. data/lib/cloudtasker/cron/schedule.rb +37 -21
  31. data/lib/cloudtasker/local_server.rb +44 -22
  32. data/lib/cloudtasker/redis_client.rb +7 -8
  33. data/lib/cloudtasker/unique_job/job.rb +2 -2
  34. data/lib/cloudtasker/version.rb +1 -1
  35. data/lib/cloudtasker/worker.rb +46 -10
  36. data/lib/cloudtasker/worker_handler.rb +5 -3
  37. data/lib/cloudtasker/worker_logger.rb +1 -1
  38. data/lib/cloudtasker/worker_wrapper.rb +52 -0
  39. data/lib/tasks/setup_queue.rake +12 -2
  40. metadata +21 -6
  41. data/Gemfile.lock +0 -263
  42. data/lib/cloudtasker/railtie.rb +0 -10
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- cloudtasker (0.2.0)
4
+ cloudtasker (0.8.0)
5
5
  activesupport
6
6
  fugit
7
7
  google-cloud-tasks
@@ -71,9 +71,19 @@ GEM
71
71
  tzinfo
72
72
  faraday (0.17.0)
73
73
  multipart-post (>= 1.2, < 3)
74
+ faraday-http-cache (2.0.0)
75
+ faraday (~> 0.8)
74
76
  fugit (1.3.3)
75
77
  et-orbi (~> 1.1, >= 1.1.8)
76
78
  raabro (~> 1.1)
79
+ github_changelog_generator (1.15.0)
80
+ activesupport
81
+ faraday-http-cache
82
+ multi_json
83
+ octokit (~> 4.6)
84
+ rainbow (>= 2.2.1)
85
+ rake (>= 10.0)
86
+ retriable (~> 3.0)
77
87
  globalid (0.4.2)
78
88
  activesupport (>= 4.2.0)
79
89
  google-cloud-tasks (1.3.1)
@@ -87,7 +97,7 @@ GEM
87
97
  googleauth (~> 0.9)
88
98
  grpc (~> 1.24)
89
99
  rly (~> 0.2.3)
90
- google-protobuf (3.10.1)
100
+ google-protobuf (3.11.0)
91
101
  googleapis-common-protos (1.3.9)
92
102
  google-protobuf (~> 3.0)
93
103
  googleapis-common-protos-types (~> 1.0)
@@ -112,7 +122,7 @@ GEM
112
122
  concurrent-ruby (~> 1.0)
113
123
  jaro_winkler (1.5.4)
114
124
  jwt (2.2.1)
115
- loofah (2.3.1)
125
+ loofah (2.4.0)
116
126
  crass (~> 1.0.2)
117
127
  nokogiri (>= 1.5.9)
118
128
  mail (2.7.1)
@@ -130,8 +140,10 @@ GEM
130
140
  nio4r (2.5.2)
131
141
  nokogiri (1.10.5)
132
142
  mini_portile2 (~> 2.4.0)
143
+ octokit (4.14.0)
144
+ sawyer (~> 0.8.0, >= 0.5.3)
133
145
  os (1.0.1)
134
- parallel (1.19.0)
146
+ parallel (1.19.1)
135
147
  parser (2.6.5.0)
136
148
  ast (~> 2.4.0)
137
149
  public_suffix (4.0.1)
@@ -166,6 +178,7 @@ GEM
166
178
  rainbow (3.0.0)
167
179
  rake (10.5.0)
168
180
  redis (4.1.3)
181
+ retriable (3.1.2)
169
182
  rly (0.2.3)
170
183
  rspec (3.9.0)
171
184
  rspec-core (~> 3.9.0)
@@ -195,10 +208,13 @@ GEM
195
208
  rainbow (>= 2.2.2, < 4.0)
196
209
  ruby-progressbar (~> 1.7)
197
210
  unicode-display_width (>= 1.4.0, < 1.7)
198
- rubocop-rspec (1.36.0)
211
+ rubocop-rspec (1.37.0)
199
212
  rubocop (>= 0.68.1)
200
213
  ruby-progressbar (1.10.1)
201
214
  safe_yaml (1.0.5)
215
+ sawyer (0.8.2)
216
+ addressable (>= 2.3.5)
217
+ faraday (> 0.8, < 2.0)
202
218
  signet (0.12.0)
203
219
  addressable (~> 2.3)
204
220
  faraday (~> 0.9)
@@ -233,6 +249,7 @@ DEPENDENCIES
233
249
  appraisal
234
250
  bundler (~> 2.0)
235
251
  cloudtasker!
252
+ github_changelog_generator
236
253
  rails (= 5.2)
237
254
  rake (~> 10.0)
238
255
  rspec (~> 3.0)
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- cloudtasker (0.2.0)
4
+ cloudtasker (0.8.0)
5
5
  activesupport
6
6
  fugit
7
7
  google-cloud-tasks
@@ -84,9 +84,19 @@ GEM
84
84
  tzinfo
85
85
  faraday (0.17.0)
86
86
  multipart-post (>= 1.2, < 3)
87
+ faraday-http-cache (2.0.0)
88
+ faraday (~> 0.8)
87
89
  fugit (1.3.3)
88
90
  et-orbi (~> 1.1, >= 1.1.8)
89
91
  raabro (~> 1.1)
92
+ github_changelog_generator (1.15.0)
93
+ activesupport
94
+ faraday-http-cache
95
+ multi_json
96
+ octokit (~> 4.6)
97
+ rainbow (>= 2.2.1)
98
+ rake (>= 10.0)
99
+ retriable (~> 3.0)
90
100
  globalid (0.4.2)
91
101
  activesupport (>= 4.2.0)
92
102
  google-cloud-tasks (1.3.1)
@@ -100,7 +110,7 @@ GEM
100
110
  googleauth (~> 0.9)
101
111
  grpc (~> 1.24)
102
112
  rly (~> 0.2.3)
103
- google-protobuf (3.10.1-universal-darwin)
113
+ google-protobuf (3.11.0)
104
114
  googleapis-common-protos (1.3.9)
105
115
  google-protobuf (~> 3.0)
106
116
  googleapis-common-protos-types (~> 1.0)
@@ -114,7 +124,7 @@ GEM
114
124
  multi_json (~> 1.11)
115
125
  os (>= 0.9, < 2.0)
116
126
  signet (~> 0.12)
117
- grpc (1.25.0-universal-darwin)
127
+ grpc (1.25.0)
118
128
  google-protobuf (~> 3.8)
119
129
  googleapis-common-protos-types (~> 1.0)
120
130
  grpc-google-iam-v1 (0.6.9)
@@ -125,7 +135,7 @@ GEM
125
135
  concurrent-ruby (~> 1.0)
126
136
  jaro_winkler (1.5.4)
127
137
  jwt (2.2.1)
128
- loofah (2.3.1)
138
+ loofah (2.4.0)
129
139
  crass (~> 1.0.2)
130
140
  nokogiri (>= 1.5.9)
131
141
  mail (2.7.1)
@@ -143,8 +153,10 @@ GEM
143
153
  nio4r (2.5.2)
144
154
  nokogiri (1.10.5)
145
155
  mini_portile2 (~> 2.4.0)
156
+ octokit (4.14.0)
157
+ sawyer (~> 0.8.0, >= 0.5.3)
146
158
  os (1.0.1)
147
- parallel (1.19.0)
159
+ parallel (1.19.1)
148
160
  parser (2.6.5.0)
149
161
  ast (~> 2.4.0)
150
162
  public_suffix (4.0.1)
@@ -181,6 +193,7 @@ GEM
181
193
  rainbow (3.0.0)
182
194
  rake (10.5.0)
183
195
  redis (4.1.3)
196
+ retriable (3.1.2)
184
197
  rly (0.2.3)
185
198
  rspec (3.9.0)
186
199
  rspec-core (~> 3.9.0)
@@ -210,10 +223,13 @@ GEM
210
223
  rainbow (>= 2.2.2, < 4.0)
211
224
  ruby-progressbar (~> 1.7)
212
225
  unicode-display_width (>= 1.4.0, < 1.7)
213
- rubocop-rspec (1.36.0)
226
+ rubocop-rspec (1.37.0)
214
227
  rubocop (>= 0.68.1)
215
228
  ruby-progressbar (1.10.1)
216
229
  safe_yaml (1.0.5)
230
+ sawyer (0.8.2)
231
+ addressable (>= 2.3.5)
232
+ faraday (> 0.8, < 2.0)
217
233
  signet (0.12.0)
218
234
  addressable (~> 2.3)
219
235
  faraday (~> 0.9)
@@ -249,6 +265,7 @@ DEPENDENCIES
249
265
  appraisal
250
266
  bundler (~> 2.0)
251
267
  cloudtasker!
268
+ github_changelog_generator
252
269
  rails (= 6.0)
253
270
  rake (~> 10.0)
254
271
  rspec (~> 3.0)
data/lib/cloudtasker.rb CHANGED
@@ -47,5 +47,4 @@ module Cloudtasker
47
47
  end
48
48
  end
49
49
 
50
- require 'cloudtasker/railtie' if defined?(Rails)
51
50
  require 'cloudtasker/engine' if defined?(::Rails::Engine)
@@ -9,15 +9,28 @@ module Cloudtasker
9
9
  #
10
10
  # Create the queue configured in Cloudtasker if it does not already exist.
11
11
  #
12
+ # @param [String] queue_name The relative name of the queue.
13
+ #
12
14
  # @return [Google::Cloud::Tasks::V2beta3::Queue] The queue
13
15
  #
14
- def self.setup_queue
15
- client.get_queue(queue_path)
16
+ def self.setup_queue(**opts)
17
+ # Build full queue path
18
+ queue_name = opts[:name] || Cloudtasker::Config::DEFAULT_JOB_QUEUE
19
+ full_queue_name = queue_path(queue_name)
20
+
21
+ # Try to get existing queue
22
+ client.get_queue(full_queue_name)
16
23
  rescue Google::Gax::RetryError
24
+ # Extract options
25
+ concurrency = (opts[:concurrency] || Cloudtasker::Config::DEFAULT_QUEUE_CONCURRENCY).to_i
26
+ retries = (opts[:retries] || Cloudtasker::Config::DEFAULT_QUEUE_RETRIES).to_i
27
+
28
+ # Create queue on 'not found' error
17
29
  client.create_queue(
18
30
  client.location_path(config.gcp_project_id, config.gcp_location_id),
19
- name: queue_path,
20
- retry_config: { max_attempts: -1 }
31
+ name: full_queue_name,
32
+ retry_config: { max_attempts: retries },
33
+ rate_limits: { max_concurrent_dispatches: concurrency }
21
34
  )
22
35
  end
23
36
 
@@ -42,13 +55,15 @@ module Cloudtasker
42
55
  #
43
56
  # Return the fully qualified path for the Cloud Task queue.
44
57
  #
58
+ # @param [String] queue_name The relative name of the queue.
59
+ #
45
60
  # @return [String] The queue path.
46
61
  #
47
- def self.queue_path
62
+ def self.queue_path(queue_name)
48
63
  client.queue_path(
49
64
  config.gcp_project_id,
50
65
  config.gcp_location_id,
51
- config.gcp_queue_id
66
+ [config.gcp_queue_prefix, queue_name].join('-')
52
67
  )
53
68
  end
54
69
 
@@ -94,8 +109,11 @@ module Cloudtasker
94
109
  schedule_time: format_schedule_time(payload[:schedule_time])
95
110
  ).compact
96
111
 
112
+ # Extract relative queue name
113
+ relative_queue = payload.delete(:queue)
114
+
97
115
  # Create task
98
- resp = client.create_task(queue_path, payload)
116
+ resp = client.create_task(queue_path(relative_queue), payload)
99
117
  resp ? new(resp) : nil
100
118
  rescue Google::Gax::RetryError
101
119
  nil
@@ -121,6 +139,20 @@ module Cloudtasker
121
139
  @gcp_task = gcp_task
122
140
  end
123
141
 
142
+ #
143
+ # Return the relative queue (queue name minus prefix) the task is in.
144
+ #
145
+ # @return [String] The relative queue name
146
+ #
147
+ def relative_queue
148
+ gcp_task
149
+ .name
150
+ .match(%r{/queues/([^/]+)})
151
+ &.captures
152
+ &.first
153
+ &.sub("#{self.class.config.gcp_queue_prefix}-", '')
154
+ end
155
+
124
156
  #
125
157
  # Return a hash description of the task.
126
158
  #
@@ -131,7 +163,8 @@ module Cloudtasker
131
163
  id: gcp_task.name,
132
164
  http_request: gcp_task.to_h[:http_request],
133
165
  schedule_time: gcp_task.to_h.dig(:schedule_time, :seconds).to_i,
134
- retries: gcp_task.to_h[:response_count]
166
+ retries: gcp_task.to_h[:response_count],
167
+ queue: relative_queue
135
168
  }
136
169
  end
137
170
  end
@@ -7,7 +7,7 @@ module Cloudtasker
7
7
  # Manage local tasks pushed to memory.
8
8
  # Used for testing.
9
9
  class MemoryTask
10
- attr_reader :id, :http_request, :schedule_time
10
+ attr_reader :id, :http_request, :schedule_time, :queue
11
11
 
12
12
  #
13
13
  # Return the task queue. A worker class name
@@ -116,10 +116,11 @@ module Cloudtasker
116
116
  # @param [Hash] http_request The HTTP request content.
117
117
  # @param [Integer] schedule_time When to run the task (Unix timestamp)
118
118
  #
119
- def initialize(id:, http_request:, schedule_time: nil)
119
+ def initialize(id:, http_request:, schedule_time: nil, queue: nil)
120
120
  @id = id
121
121
  @http_request = http_request
122
122
  @schedule_time = Time.at(schedule_time || 0)
123
+ @queue = queue
123
124
  end
124
125
 
125
126
  #
@@ -149,7 +150,8 @@ module Cloudtasker
149
150
  {
150
151
  id: id,
151
152
  http_request: http_request,
152
- schedule_time: schedule_time.to_i
153
+ schedule_time: schedule_time.to_i,
154
+ queue: queue
153
155
  }
154
156
  end
155
157
 
@@ -7,17 +7,17 @@ module Cloudtasker
7
7
  module Backend
8
8
  # Manage local tasks pushed to Redis
9
9
  class RedisTask
10
- attr_reader :id, :http_request, :schedule_time, :retries
10
+ attr_reader :id, :http_request, :schedule_time, :retries, :queue
11
11
 
12
12
  RETRY_INTERVAL = 20 # seconds
13
13
 
14
14
  #
15
15
  # Return the cloudtasker redis client
16
16
  #
17
- # @return [Class] The redis client.
17
+ # @return [Cloudtasker::RedisClient] The cloudtasker redis client..
18
18
  #
19
19
  def self.redis
20
- RedisClient
20
+ @redis ||= RedisClient.new
21
21
  end
22
22
 
23
23
  #
@@ -48,20 +48,26 @@ module Cloudtasker
48
48
  #
49
49
  # Reeturn all tasks ready to process.
50
50
  #
51
+ # @param [String] queue The queue to retrieve items from.
52
+ #
51
53
  # @return [Array<Cloudtasker::Backend::RedisTask>] All the tasks ready to process.
52
54
  #
53
- def self.ready_to_process
54
- all.select { |e| e.schedule_time <= Time.now }
55
+ def self.ready_to_process(queue = nil)
56
+ list = all.select { |e| e.schedule_time <= Time.now }
57
+ list = list.select { |e| e.queue == queue } if queue
58
+ list
55
59
  end
56
60
 
57
61
  #
58
62
  # Retrieve and remove a task from the queue.
59
63
  #
64
+ # @param [String] queue The queue to retrieve items from.
65
+ #
60
66
  # @return [Cloudtasker::Backend::RedisTask] A task ready to process.
61
67
  #
62
- def self.pop
68
+ def self.pop(queue = nil)
63
69
  redis.with_lock('cloudtasker/server') do
64
- ready_to_process.first&.tap(&:destroy)
70
+ ready_to_process(queue).first&.tap(&:destroy)
65
71
  end
66
72
  end
67
73
 
@@ -110,11 +116,12 @@ module Cloudtasker
110
116
  # @param [Integer] schedule_time When to run the task (Unix timestamp)
111
117
  # @param [Integer] retries The number of times the job failed.
112
118
  #
113
- def initialize(id:, http_request:, schedule_time: nil, retries: 0)
119
+ def initialize(id:, http_request:, schedule_time: nil, retries: 0, queue: nil)
114
120
  @id = id
115
121
  @http_request = http_request
116
122
  @schedule_time = Time.at(schedule_time || 0)
117
123
  @retries = retries || 0
124
+ @queue = queue || Cloudtasker::Config::DEFAULT_JOB_QUEUE
118
125
  end
119
126
 
120
127
  #
@@ -136,7 +143,8 @@ module Cloudtasker
136
143
  id: id,
137
144
  http_request: http_request,
138
145
  schedule_time: schedule_time.to_i,
139
- retries: retries
146
+ retries: retries,
147
+ queue: queue
140
148
  }
141
149
  end
142
150
 
@@ -155,10 +163,13 @@ module Cloudtasker
155
163
  # @param [Integer] interval The delay in seconds before retrying the task
156
164
  #
157
165
  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)
166
+ redis.write(
167
+ gid,
168
+ retries: is_error ? retries + 1 : retries,
169
+ http_request: http_request,
170
+ schedule_time: (Time.now + interval).to_i,
171
+ queue: queue
172
+ )
162
173
  end
163
174
 
164
175
  #
@@ -77,7 +77,16 @@ module Cloudtasker
77
77
  # @return [Integer] The number of jobs pending.
78
78
  #
79
79
  def pending
80
- total - completed - dead
80
+ total - done
81
+ end
82
+
83
+ #
84
+ # Return the number of jobs completed or dead.
85
+ #
86
+ # @return [Integer] The number of jobs done.
87
+ #
88
+ def done
89
+ completed + dead
81
90
  end
82
91
 
83
92
  #
@@ -88,7 +97,7 @@ module Cloudtasker
88
97
  def percent
89
98
  return 0 if total.zero?
90
99
 
91
- pending.to_f / total
100
+ (done.to_f / total) * 100
92
101
  end
93
102
 
94
103
  #
@@ -7,7 +7,8 @@ module Cloudtasker
7
7
  attr_reader :worker
8
8
 
9
9
  # Key Namespace used for object saved under this class
10
- SUB_NAMESPACE = 'job'
10
+ JOBS_NAMESPACE = 'jobs'
11
+ STATES_NAMESPACE = 'states'
11
12
 
12
13
  # List of statuses triggering a completion callback
13
14
  COMPLETION_STATUSES = %w[completed dead].freeze
@@ -15,10 +16,10 @@ module Cloudtasker
15
16
  #
16
17
  # Return the cloudtasker redis client
17
18
  #
18
- # @return [Class] The redis client.
19
+ # @return [Cloudtasker::RedisClient] The cloudtasker redis client..
19
20
  #
20
21
  def self.redis
21
- RedisClient
22
+ @redis ||= RedisClient.new
22
23
  end
23
24
 
24
25
  #
@@ -32,7 +33,7 @@ module Cloudtasker
32
33
  return nil unless worker_id
33
34
 
34
35
  # Retrieve related worker
35
- payload = redis.fetch(key(worker_id))
36
+ payload = redis.fetch(key("#{JOBS_NAMESPACE}/#{worker_id}"))
36
37
  worker = Cloudtasker::Worker.from_hash(payload)
37
38
  return nil unless worker
38
39
 
@@ -86,7 +87,7 @@ module Cloudtasker
86
87
  #
87
88
  # Return the cloudtasker redis client
88
89
  #
89
- # @return [Class] The redis client.
90
+ # @return [Cloudtasker::RedisClient] The cloudtasker redis client..
90
91
  #
91
92
  def redis
92
93
  self.class.redis
@@ -140,7 +141,7 @@ module Cloudtasker
140
141
  # @return [String] The worker namespaced id.
141
142
  #
142
143
  def batch_gid
143
- key(batch_id)
144
+ key("#{JOBS_NAMESPACE}/#{batch_id}")
144
145
  end
145
146
 
146
147
  #
@@ -149,7 +150,7 @@ module Cloudtasker
149
150
  # @return [String] The batch state namespaced id.
150
151
  #
151
152
  def batch_state_gid
152
- [batch_gid, 'state'].join('/')
153
+ key("#{STATES_NAMESPACE}/#{batch_id}")
153
154
  end
154
155
 
155
156
  #
@@ -179,9 +180,23 @@ module Cloudtasker
179
180
  # @return [Array<Cloudtasker::Worker>] The updated list of jobs.
180
181
  #
181
182
  def add(worker_klass, *args)
183
+ add_to_queue(worker.job_queue, worker_klass, *args)
184
+ end
185
+
186
+ #
187
+ # Add a worker to the batch using a specific queue.
188
+ #
189
+ # @param [String, Symbol] queue The name of the queue
190
+ # @param [Class] worker_klass The worker class.
191
+ # @param [Array<any>] *args The worker arguments.
192
+ #
193
+ # @return [Array<Cloudtasker::Worker>] The updated list of jobs.
194
+ #
195
+ def add_to_queue(queue, worker_klass, *args)
182
196
  jobs << worker_klass.new(
183
197
  job_args: args,
184
- job_meta: { key(:parent_id) => batch_id }
198
+ job_meta: { key(:parent_id) => batch_id },
199
+ job_queue: queue
185
200
  )
186
201
  end
187
202
 
@@ -371,7 +386,7 @@ module Cloudtasker
371
386
  setup
372
387
 
373
388
  # Complete batch
374
- complete(:success)
389
+ complete(:completed)
375
390
  rescue DeadWorkerError => e
376
391
  complete(:dead)
377
392
  raise(e)