cloudtasker 0.3.0 → 0.8.0

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