cloudtasker 0.6.0 → 0.9.0

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