cloudtasker 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
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
 
@@ -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,7 +7,7 @@ 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'
@@ -16,6 +16,11 @@ module Cloudtasker
16
16
  DEFAULT_LOCATION_ID = 'us-east1'
17
17
  DEFAULT_PROCESSOR_PATH = '/cloudtasker/run'
18
18
 
19
+ # Default queue values
20
+ DEFAULT_JOB_QUEUE = 'default'
21
+ DEFAULT_QUEUE_CONCURRENCY = 10
22
+ DEFAULT_QUEUE_RETRIES = -1 # unlimited
23
+
19
24
  # The number of times jobs will be attempted before declaring them dead
20
25
  DEFAULT_MAX_RETRY_ATTEMPTS = 25
21
26
 
@@ -23,9 +28,10 @@ module Cloudtasker
23
28
  Missing host for processing.
24
29
  Please specify a processor hostname in form of `https://some-public-dns.example.com`'
25
30
  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`
31
+ QUEUE_PREFIX_MISSING_ERROR = <<~DOC
32
+ Missing GCP queue prefix.
33
+ Please specify a queue prefix in the form of `my-app`.
34
+ You can create a default queue using the Google SDK via `gcloud tasks queues create my-app-default`
29
35
  DOC
30
36
  PROJECT_ID_MISSING_ERROR = <<~DOC
31
37
  Missing GCP project ID.
@@ -121,12 +127,12 @@ module Cloudtasker
121
127
  end
122
128
 
123
129
  #
124
- # Return the ID of GCP queue where tasks will be added.
130
+ # Return the prefix used for queues.
125
131
  #
126
- # @return [String] The ID of the processing queue.
132
+ # @return [String] The prefix of the processing queues.
127
133
  #
128
- def gcp_queue_id
129
- @gcp_queue_id || raise(StandardError, QUEUE_ID_MISSING_ERROR)
134
+ def gcp_queue_prefix
135
+ @gcp_queue_prefix || raise(StandardError, QUEUE_PREFIX_MISSING_ERROR)
130
136
  end
131
137
 
132
138
  #
@@ -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
@@ -9,6 +9,9 @@ module Cloudtasker
9
9
  # Max number of task requests sent to the processing server
10
10
  CONCURRENCY = (ENV['CLOUDTASKER_CONCURRENCY'] || 5).to_i
11
11
 
12
+ # Default number of threads to allocate to process a specific queue
13
+ QUEUE_CONCURRENCY = 1
14
+
12
15
  #
13
16
  # Stop the local server.
14
17
  #
@@ -16,7 +19,7 @@ module Cloudtasker
16
19
  @done = true
17
20
 
18
21
  # Terminate threads and repush tasks
19
- @threads&.each do |t|
22
+ @threads&.values&.flatten&.each do |t|
20
23
  t.terminate
21
24
  t['task']&.retry_later(0, is_error: false)
22
25
  end
@@ -28,11 +31,21 @@ module Cloudtasker
28
31
  #
29
32
  # Start the local server
30
33
  #
34
+ # @param [Hash] opts Server options.
35
+ #
31
36
  #
32
- def start
37
+ def start(opts = {})
38
+ # Extract queues to process
39
+ queues = opts[:queues].to_a.any? ? opts[:queues] : [[nil, CONCURRENCY]]
40
+
41
+ # Display start banner
42
+ queue_labels = queues.map { |n, c| "#{n || 'all'}=#{c || QUEUE_CONCURRENCY}" }.join(' ')
43
+ Cloudtasker.logger.info("[Cloudtasker/Server] Processing queues: #{queue_labels}")
44
+
45
+ # Start processing queues
33
46
  @start ||= Thread.new do
34
47
  until @done
35
- process_jobs
48
+ queues.each { |(n, c)| process_jobs(n, c) }
36
49
  sleep 1
37
50
  end
38
51
  Cloudtasker.logger.info('[Cloudtasker/Server] Local server exiting...')
@@ -43,31 +56,40 @@ module Cloudtasker
43
56
  # Process enqueued workers.
44
57
  #
45
58
  #
46
- def process_jobs
47
- @threads ||= []
59
+ def process_jobs(queue = nil, concurrency = nil)
60
+ @threads ||= {}
61
+ @threads[queue] ||= []
62
+ max_threads = (concurrency || QUEUE_CONCURRENCY).to_i
48
63
 
49
64
  # Remove any done thread
50
- @threads.select!(&:alive?)
65
+ @threads[queue].select!(&:alive?)
51
66
 
52
67
  # Process tasks
53
- while @threads.count < CONCURRENCY && (task = Cloudtasker::Backend::RedisTask.pop)
54
- @threads << Thread.new do
55
- Thread.current['task'] = task
56
- Thread.current['attempts'] = 0
68
+ while @threads[queue].count < max_threads && (task = Cloudtasker::Backend::RedisTask.pop(queue))
69
+ @threads[queue] << Thread.new { process_task(task) }
70
+ end
71
+ end
57
72
 
58
- # Deliver task
59
- begin
60
- Thread.current['task'].deliver
61
- rescue Errno::ECONNREFUSED => e
62
- raise(e) unless Thread.current['attempts'] < 3
73
+ #
74
+ # Process a given task
75
+ #
76
+ # @param [Cloudtasker::CloudTask] task The task to process
77
+ #
78
+ def process_task(task)
79
+ Thread.current['task'] = task
80
+ Thread.current['attempts'] = 0
63
81
 
64
- # Retry on connection error, in case the web server is not
65
- # started yet.
66
- Thread.current['attempts'] += 1
67
- sleep(3)
68
- retry
69
- end
70
- end
82
+ # Deliver task
83
+ begin
84
+ Thread.current['task'].deliver
85
+ rescue Errno::ECONNREFUSED => e
86
+ raise(e) unless Thread.current['attempts'] < 3
87
+
88
+ # Retry on connection error, in case the web server is not
89
+ # started yet.
90
+ Thread.current['attempts'] += 1
91
+ sleep(3)
92
+ retry
71
93
  end
72
94
  end
73
95
  end