cloudtasker 0.3.0 → 0.8.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.
data/docs/CRON_JOBS.md CHANGED
@@ -9,7 +9,7 @@ The Cloudtasker cron job extension allows you to register workers to run at fixe
9
9
  You can schedule cron jobs by adding the following to your cloudtasker initializer:
10
10
  ```ruby
11
11
  # The cron job extension is optional and must be explicitly required
12
- require 'cloudtasker/cron_job'
12
+ require 'cloudtasker/cron'
13
13
 
14
14
  Cloudtasker.configure do |config|
15
15
  # Specify your redis url.
@@ -18,18 +18,24 @@ Cloudtasker.configure do |config|
18
18
  end
19
19
 
20
20
  # Specify all your cron jobs below. This will synchronize your list of cron jobs (cron jobs previously created and not listed below will be removed).
21
- Cloudtasker::Cron::Schedule.load_from_hash!(
22
- # Run job every minute
23
- some_schedule_name: {
24
- worker: 'SomeCronWorker',
25
- cron: '* * * * *'
26
- },
27
- # Run job every hour on the fifteenth minute
28
- other_cron_schedule: {
29
- worker: 'OtherCronWorker',
30
- cron: '15 * * * *'
31
- }
32
- )
21
+ unless Rails.env.test?
22
+ Cloudtasker::Cron::Schedule.load_from_hash!(
23
+ # Run job every minute
24
+ some_schedule_name: {
25
+ worker: 'SomeCronWorker',
26
+ cron: '* * * * *'
27
+ },
28
+ # Run job every hour on the fifteenth minute
29
+ other_cron_schedule: {
30
+ worker: 'OtherCronWorker',
31
+ cron: '15 * * * *',
32
+ queue: 'critical'
33
+ args:
34
+ - 'foo'
35
+ - 'bar
36
+ }
37
+ )
38
+ end
33
39
  ```
34
40
 
35
41
  ## Using a configuration file
@@ -56,7 +62,7 @@ Then register the jobs inside your Cloudtasker initializer this way:
56
62
  # ... Cloudtasker configuration ...
57
63
 
58
64
  schedule_file = 'config/cloudtasker_cron.yml'
59
- if File.exist?(schedule_file)
65
+ if File.exist?(schedule_file) && !Rails.env.test?
60
66
  Cloudtasker::Cron::Schedule.load_from_hash!(YAML.load_file(schedule_file))
61
67
  end
62
68
  ```
data/exe/cloudtasker CHANGED
@@ -3,9 +3,21 @@
3
3
 
4
4
  require 'bundler/setup'
5
5
  require 'cloudtasker/cli'
6
+ require 'optparse'
7
+
8
+ options = {}
9
+ OptionParser.new do |opts|
10
+ opts.banner = 'Usage: cloudtasker [options]'
11
+
12
+ opts.on('-q QUEUE', '--queue=QUEUE', 'Queue to process and number of threads. ' \
13
+ "Examples: '-q critical' | '-q critical,2' | '-q critical,3 -q defaults,2'") do |o|
14
+ options[:queues] ||= []
15
+ options[:queues] << o.split(',')
16
+ end
17
+ end.parse!
6
18
 
7
19
  begin
8
- Cloudtasker::CLI.run
20
+ Cloudtasker::CLI.run(options)
9
21
  rescue StandardError => e
10
22
  raise e if $DEBUG
11
23
 
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
 
@@ -1,22 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'cloudtasker/redis_client'
4
+ require 'net/http'
4
5
 
5
6
  module Cloudtasker
6
7
  module Backend
7
8
  # Manage local tasks pushed to Redis
8
9
  class RedisTask
9
- attr_reader :id, :http_request, :schedule_time, :retries
10
+ attr_reader :id, :http_request, :schedule_time, :retries, :queue
10
11
 
11
12
  RETRY_INTERVAL = 20 # seconds
12
13
 
13
14
  #
14
15
  # Return the cloudtasker redis client
15
16
  #
16
- # @return [Class] The redis client.
17
+ # @return [Cloudtasker::RedisClient] The cloudtasker redis client..
17
18
  #
18
19
  def self.redis
19
- RedisClient
20
+ @redis ||= RedisClient.new
20
21
  end
21
22
 
22
23
  #
@@ -47,20 +48,26 @@ module Cloudtasker
47
48
  #
48
49
  # Reeturn all tasks ready to process.
49
50
  #
51
+ # @param [String] queue The queue to retrieve items from.
52
+ #
50
53
  # @return [Array<Cloudtasker::Backend::RedisTask>] All the tasks ready to process.
51
54
  #
52
- def self.ready_to_process
53
- 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
54
59
  end
55
60
 
56
61
  #
57
62
  # Retrieve and remove a task from the queue.
58
63
  #
64
+ # @param [String] queue The queue to retrieve items from.
65
+ #
59
66
  # @return [Cloudtasker::Backend::RedisTask] A task ready to process.
60
67
  #
61
- def self.pop
68
+ def self.pop(queue = nil)
62
69
  redis.with_lock('cloudtasker/server') do
63
- ready_to_process.first&.tap(&:destroy)
70
+ ready_to_process(queue).first&.tap(&:destroy)
64
71
  end
65
72
  end
66
73
 
@@ -109,11 +116,12 @@ module Cloudtasker
109
116
  # @param [Integer] schedule_time When to run the task (Unix timestamp)
110
117
  # @param [Integer] retries The number of times the job failed.
111
118
  #
112
- def initialize(id:, http_request:, schedule_time: nil, retries: 0)
119
+ def initialize(id:, http_request:, schedule_time: nil, retries: 0, queue: nil)
113
120
  @id = id
114
121
  @http_request = http_request
115
122
  @schedule_time = Time.at(schedule_time || 0)
116
123
  @retries = retries || 0
124
+ @queue = queue
117
125
  end
118
126
 
119
127
  #
@@ -135,7 +143,8 @@ module Cloudtasker
135
143
  id: id,
136
144
  http_request: http_request,
137
145
  schedule_time: schedule_time.to_i,
138
- retries: retries
146
+ retries: retries,
147
+ queue: queue
139
148
  }
140
149
  end
141
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
  #
@@ -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)
@@ -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
  #