cloudtasker 0.6.0 → 0.9.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 (41) 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 +38 -0
  6. data/README.md +142 -26
  7. data/_config.yml +1 -0
  8. data/app/controllers/cloudtasker/worker_controller.rb +21 -5
  9. data/cloudtasker.gemspec +2 -2
  10. data/docs/BATCH_JOBS.md +29 -4
  11. data/docs/CRON_JOBS.md +18 -14
  12. data/exe/cloudtasker +13 -1
  13. data/gemfiles/google_cloud_tasks_1.0.gemfile.lock +26 -9
  14. data/gemfiles/google_cloud_tasks_1.1.gemfile.lock +26 -9
  15. data/gemfiles/google_cloud_tasks_1.2.gemfile.lock +27 -10
  16. data/gemfiles/google_cloud_tasks_1.3.gemfile.lock +26 -9
  17. data/gemfiles/rails_5.2.gemfile.lock +26 -9
  18. data/gemfiles/rails_6.0.gemfile.lock +27 -10
  19. data/lib/cloudtasker.rb +0 -1
  20. data/lib/cloudtasker/backend/google_cloud_task.rb +65 -12
  21. data/lib/cloudtasker/backend/memory_task.rb +5 -3
  22. data/lib/cloudtasker/backend/redis_task.rb +24 -13
  23. data/lib/cloudtasker/batch/batch_progress.rb +11 -2
  24. data/lib/cloudtasker/batch/job.rb +18 -4
  25. data/lib/cloudtasker/cli.rb +6 -5
  26. data/lib/cloudtasker/cloud_task.rb +4 -2
  27. data/lib/cloudtasker/config.rb +30 -9
  28. data/lib/cloudtasker/cron/job.rb +2 -2
  29. data/lib/cloudtasker/cron/schedule.rb +26 -14
  30. data/lib/cloudtasker/local_server.rb +44 -22
  31. data/lib/cloudtasker/redis_client.rb +10 -7
  32. data/lib/cloudtasker/unique_job/job.rb +2 -2
  33. data/lib/cloudtasker/version.rb +1 -1
  34. data/lib/cloudtasker/worker.rb +46 -10
  35. data/lib/cloudtasker/worker_handler.rb +7 -5
  36. data/lib/cloudtasker/worker_logger.rb +1 -1
  37. data/lib/cloudtasker/worker_wrapper.rb +52 -0
  38. data/lib/tasks/setup_queue.rake +12 -2
  39. metadata +6 -6
  40. data/Gemfile.lock +0 -280
  41. data/lib/cloudtasker/railtie.rb +0 -10
@@ -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
  #
@@ -16,10 +16,10 @@ module Cloudtasker
16
16
  #
17
17
  # Return the cloudtasker redis client
18
18
  #
19
- # @return [Class] The redis client.
19
+ # @return [Cloudtasker::RedisClient] The cloudtasker redis client..
20
20
  #
21
21
  def self.redis
22
- RedisClient
22
+ @redis ||= RedisClient.new
23
23
  end
24
24
 
25
25
  #
@@ -87,7 +87,7 @@ module Cloudtasker
87
87
  #
88
88
  # Return the cloudtasker redis client
89
89
  #
90
- # @return [Class] The redis client.
90
+ # @return [Cloudtasker::RedisClient] The cloudtasker redis client..
91
91
  #
92
92
  def redis
93
93
  self.class.redis
@@ -180,9 +180,23 @@ module Cloudtasker
180
180
  # @return [Array<Cloudtasker::Worker>] The updated list of jobs.
181
181
  #
182
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)
183
196
  jobs << worker_klass.new(
184
197
  job_args: args,
185
- job_meta: { key(:parent_id) => batch_id }
198
+ job_meta: { key(:parent_id) => batch_id },
199
+ job_queue: queue
186
200
  )
187
201
  end
188
202
 
@@ -70,7 +70,7 @@ module Cloudtasker
70
70
  #
71
71
  # Run the cloudtasker development server.
72
72
  #
73
- def run
73
+ def run(opts = {})
74
74
  boot_system
75
75
 
76
76
  # Print banner
@@ -90,16 +90,17 @@ module Cloudtasker
90
90
  logger.info "[Cloudtasker/Server] Running in #{RUBY_DESCRIPTION}"
91
91
 
92
92
  # Wait for signals
93
- wait_for_signal(self_read)
93
+ run_server(self_read, opts)
94
94
  end
95
95
 
96
96
  #
97
- # Wait for signals and handle them.
97
+ # Run server and wait for signals.
98
98
  #
99
99
  # @param [IO] read_pipe Where to read signals.
100
+ # @param [Hash] opts Server options.
100
101
  #
101
- def wait_for_signal(read_pipe)
102
- local_server.start
102
+ def run_server(read_pipe, opts = {})
103
+ local_server.start(opts)
103
104
 
104
105
  while (readable_io = IO.select([read_pipe]))
105
106
  signal = readable_io.first[0].gets.strip
@@ -3,7 +3,7 @@
3
3
  module Cloudtasker
4
4
  # An interface class to manage tasks on the backend (Cloud Task or Redis)
5
5
  class CloudTask
6
- attr_accessor :id, :http_request, :schedule_time, :retries
6
+ attr_accessor :id, :http_request, :schedule_time, :retries, :queue
7
7
 
8
8
  #
9
9
  # The backend to use for cloud tasks.
@@ -69,12 +69,14 @@ module Cloudtasker
69
69
  # @param [Hash] http_request The content of the http request.
70
70
  # @param [Integer] schedule_time When to run the job (Unix timestamp)
71
71
  # @param [Integer] retries The number of times the job failed.
72
+ # @param [String] queue The queue the task is in.
72
73
  #
73
- def initialize(id:, http_request:, schedule_time: nil, retries: 0)
74
+ def initialize(id:, http_request:, schedule_time: nil, retries: 0, queue: nil)
74
75
  @id = id
75
76
  @http_request = http_request
76
77
  @schedule_time = schedule_time
77
78
  @retries = retries || 0
79
+ @queue = queue
78
80
  end
79
81
 
80
82
  #
@@ -7,15 +7,29 @@ module Cloudtasker
7
7
  class Config
8
8
  attr_accessor :redis
9
9
  attr_writer :secret, :gcp_location_id, :gcp_project_id,
10
- :gcp_queue_id, :processor_path, :logger, :mode, :max_retries
10
+ :gcp_queue_prefix, :processor_path, :logger, :mode, :max_retries
11
11
 
12
12
  # Retry header in Cloud Task responses
13
13
  RETRY_HEADER = 'X-CloudTasks-TaskExecutionCount'
14
14
 
15
+ # Content-Transfer-Encoding header in Cloud Task responses
16
+ ENCODING_HEADER = 'Content-Transfer-Encoding'
17
+
18
+ # Content Type
19
+ CONTENT_TYPE_HEADER = 'Content-Type'
20
+
21
+ # Authorization header
22
+ AUTHORIZATION_HEADER = 'Authorization'
23
+
15
24
  # Default values
16
25
  DEFAULT_LOCATION_ID = 'us-east1'
17
26
  DEFAULT_PROCESSOR_PATH = '/cloudtasker/run'
18
27
 
28
+ # Default queue values
29
+ DEFAULT_JOB_QUEUE = 'default'
30
+ DEFAULT_QUEUE_CONCURRENCY = 10
31
+ DEFAULT_QUEUE_RETRIES = -1 # unlimited
32
+
19
33
  # The number of times jobs will be attempted before declaring them dead
20
34
  DEFAULT_MAX_RETRY_ATTEMPTS = 25
21
35
 
@@ -23,9 +37,10 @@ module Cloudtasker
23
37
  Missing host for processing.
24
38
  Please specify a processor hostname in form of `https://some-public-dns.example.com`'
25
39
  DOC
26
- QUEUE_ID_MISSING_ERROR = <<~DOC
27
- Missing GCP queue ID.
28
- Please specify a queue ID in the form of `my-queue-id`. You can create a queue using the Google SDK via `gcloud tasks queues create my-queue-id`
40
+ QUEUE_PREFIX_MISSING_ERROR = <<~DOC
41
+ Missing GCP queue prefix.
42
+ Please specify a queue prefix in the form of `my-app`.
43
+ You can create a default queue using the Google SDK via `gcloud tasks queues create my-app-default`
29
44
  DOC
30
45
  PROJECT_ID_MISSING_ERROR = <<~DOC
31
46
  Missing GCP project ID.
@@ -95,8 +110,14 @@ module Cloudtasker
95
110
  def processor_host=(val)
96
111
  @processor_host = val
97
112
 
113
+ # Check if Rails supports host filtering
114
+ return unless val &&
115
+ defined?(Rails) &&
116
+ Rails.application.config.respond_to?(:hosts) &&
117
+ Rails.application.config.hosts&.any?
118
+
98
119
  # Add processor host to the list of authorized hosts
99
- Rails.application.config.hosts << val.gsub(%r{https?://}, '') if val && defined?(Rails)
120
+ Rails.application.config.hosts << val.gsub(%r{https?://}, '')
100
121
  end
101
122
 
102
123
  #
@@ -121,12 +142,12 @@ module Cloudtasker
121
142
  end
122
143
 
123
144
  #
124
- # Return the ID of GCP queue where tasks will be added.
145
+ # Return the prefix used for queues.
125
146
  #
126
- # @return [String] The ID of the processing queue.
147
+ # @return [String] The prefix of the processing queues.
127
148
  #
128
- def gcp_queue_id
129
- @gcp_queue_id || raise(StandardError, QUEUE_ID_MISSING_ERROR)
149
+ def gcp_queue_prefix
150
+ @gcp_queue_prefix || raise(StandardError, QUEUE_PREFIX_MISSING_ERROR)
130
151
  end
131
152
 
132
153
  #
@@ -105,10 +105,10 @@ module Cloudtasker
105
105
  #
106
106
  # Return the cloudtasker redis client
107
107
  #
108
- # @return [Class] The redis client.
108
+ # @return [Cloudtasker::RedisClient] The cloudtasker redis client..
109
109
  #
110
110
  def redis
111
- RedisClient
111
+ @redis ||= RedisClient.new
112
112
  end
113
113
 
114
114
  #
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'fugit'
4
+ require 'cloudtasker/worker_wrapper'
4
5
 
5
6
  module Cloudtasker
6
7
  module Cron
7
8
  # Manage cron schedules
8
9
  class Schedule
9
- attr_accessor :id, :cron, :worker, :task_id, :job_id
10
+ attr_accessor :id, :cron, :worker, :task_id, :job_id, :queue, :args
10
11
 
11
12
  # Key Namespace used for object saved under this class
12
13
  SUB_NAMESPACE = 'schedule'
@@ -14,10 +15,10 @@ module Cloudtasker
14
15
  #
15
16
  # Return the redis client.
16
17
  #
17
- # @return [Class] The redis client
18
+ # @return [Cloudtasker::RedisClient] The cloudtasker redis client.
18
19
  #
19
20
  def self.redis
20
- RedisClient
21
+ @redis ||= RedisClient.new
21
22
  end
22
23
 
23
24
  #
@@ -113,21 +114,25 @@ module Cloudtasker
113
114
  # @param [String] id The schedule id.
114
115
  # @param [String] cron The cron expression.
115
116
  # @param [Class] worker The worker class to run.
117
+ # @param [Array<any>] args The worker arguments.
118
+ # @param [String] queue The queue to use for the cron job.
116
119
  # @param [String] task_id The ID of the actual backend task.
117
120
  # @param [String] job_id The ID of the Cloudtasker worker.
118
121
  #
119
- def initialize(id:, cron:, worker:, task_id: nil, job_id: nil)
122
+ def initialize(id:, cron:, worker:, **opts)
120
123
  @id = id
121
124
  @cron = cron
122
125
  @worker = worker
123
- @task_id = task_id
124
- @job_id = job_id
126
+ @args = opts[:args]
127
+ @queue = opts[:queue]
128
+ @task_id = opts[:task_id]
129
+ @job_id = opts[:job_id]
125
130
  end
126
131
 
127
132
  #
128
133
  # Return the redis client.
129
134
  #
130
- # @return [Class] The redis client
135
+ # @return [Cloudtasker::RedisClient] The cloudtasker redis client.
131
136
  #
132
137
  def redis
133
138
  self.class.redis
@@ -191,7 +196,9 @@ module Cloudtasker
191
196
  {
192
197
  id: id,
193
198
  cron: cron,
194
- worker: worker
199
+ worker: worker,
200
+ args: args,
201
+ queue: queue
195
202
  }
196
203
  end
197
204
 
@@ -201,13 +208,10 @@ module Cloudtasker
201
208
  # @return [Hash] The attributes hash.
202
209
  #
203
210
  def to_h
204
- {
205
- id: id,
206
- cron: cron,
207
- worker: worker,
211
+ to_config.merge(
208
212
  task_id: task_id,
209
213
  job_id: job_id
210
- }
214
+ )
211
215
  end
212
216
 
213
217
  #
@@ -219,6 +223,15 @@ module Cloudtasker
219
223
  @cron_schedule ||= Fugit::Cron.parse(cron)
220
224
  end
221
225
 
226
+ #
227
+ # Return an instance of the underlying worker.
228
+ #
229
+ # @return [Cloudtasker::WorkerWrapper] The worker instance
230
+ #
231
+ def worker_instance
232
+ WorkerWrapper.new(worker_name: worker, job_args: args, job_queue: queue)
233
+ end
234
+
222
235
  #
223
236
  # Return the next time a job should run.
224
237
  #
@@ -279,7 +292,6 @@ module Cloudtasker
279
292
  CloudTask.delete(task_id) if task_id
280
293
 
281
294
  # Schedule worker
282
- worker_instance = Object.const_get(worker).new
283
295
  Job.new(worker_instance).set(schedule_id: id).schedule!
284
296
  end
285
297
  end