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
@@ -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
@@ -4,19 +4,21 @@ require 'redis'
4
4
 
5
5
  module Cloudtasker
6
6
  # A wrapper with helper methods for redis
7
- module RedisClient
8
- module_function
9
-
7
+ class RedisClient
10
8
  # Suffix added to cache keys when locking them
11
9
  LOCK_KEY_PREFIX = 'cloudtasker/lock'
12
10
 
11
+ def self.client
12
+ @client ||= Redis.new(Cloudtasker.config.redis || {})
13
+ end
14
+
13
15
  #
14
16
  # Return the underlying redis client.
15
17
  #
16
18
  # @return [Redis] The redis client.
17
19
  #
18
20
  def client
19
- @client ||= Redis.new(Cloudtasker.config.redis || {})
21
+ @client ||= self.class.client
20
22
  end
21
23
 
22
24
  #
@@ -50,9 +52,10 @@ module Cloudtasker
50
52
  # Acquire a lock on a cache entry.
51
53
  #
52
54
  # @example
53
- # RedisClient.with_lock('foo')
54
- # content = RedisClient.fetch('foo')
55
- # RedisClient.set(content.merge(bar: 'bar).to_json)
55
+ # redis = RedisClient.new
56
+ # redis.with_lock('foo')
57
+ # content = redis.fetch('foo')
58
+ # redis.set(content.merge(bar: 'bar).to_json)
56
59
  # end
57
60
  #
58
61
  # @param [String] cache_key The cache key to access.
@@ -100,10 +100,10 @@ module Cloudtasker
100
100
  #
101
101
  # Return the Cloudtasker redis client.
102
102
  #
103
- # @return [Class] The Cloudtasker::RedisClient wrapper.
103
+ # @return [Cloudtasker::RedisClient] The cloudtasker redis client.
104
104
  #
105
105
  def redis
106
- Cloudtasker::RedisClient
106
+ @redis ||= Cloudtasker::RedisClient.new
107
107
  end
108
108
 
109
109
  #
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cloudtasker
4
- VERSION = '0.6.0'
4
+ VERSION = '0.9.0'
5
5
  end
@@ -6,6 +6,7 @@ module Cloudtasker
6
6
  # Add class method to including class
7
7
  def self.included(base)
8
8
  base.extend(ClassMethods)
9
+ base.attr_writer :job_queue
9
10
  base.attr_accessor :job_args, :job_id, :job_meta, :job_reenqueued, :job_retries
10
11
  end
11
12
 
@@ -32,8 +33,9 @@ module Cloudtasker
32
33
  # @return [Cloudtasker::Worker, nil] The instantiated worker.
33
34
  #
34
35
  def self.from_hash(hash)
35
- # Symbolize payload keys
36
+ # Symbolize metadata keys and stringify job arguments
36
37
  payload = JSON.parse(hash.to_json, symbolize_names: true)
38
+ payload[:job_args] = JSON.parse(payload[:job_args].to_json)
37
39
 
38
40
  # Extract worker parameters
39
41
  klass_name = payload&.dig(:worker)
@@ -44,7 +46,7 @@ module Cloudtasker
44
46
  return nil unless worker_klass.include?(self)
45
47
 
46
48
  # Return instantiated worker
47
- worker_klass.new(payload.slice(:job_args, :job_id, :job_meta, :job_retries))
49
+ worker_klass.new(payload.slice(:job_queue, :job_args, :job_id, :job_meta, :job_retries))
48
50
  rescue NameError
49
51
  nil
50
52
  end
@@ -80,7 +82,7 @@ module Cloudtasker
80
82
  # @return [Cloudtasker::CloudTask] The Google Task response
81
83
  #
82
84
  def perform_async(*args)
83
- perform_in(nil, *args)
85
+ schedule(args: args)
84
86
  end
85
87
 
86
88
  #
@@ -92,7 +94,7 @@ module Cloudtasker
92
94
  # @return [Cloudtasker::CloudTask] The Google Task response
93
95
  #
94
96
  def perform_in(interval, *args)
95
- new(job_args: args).schedule(interval: interval)
97
+ schedule(args: args, time_in: interval)
96
98
  end
97
99
 
98
100
  #
@@ -104,7 +106,21 @@ module Cloudtasker
104
106
  # @return [Cloudtasker::CloudTask] The Google Task response
105
107
  #
106
108
  def perform_at(time_at, *args)
107
- new(job_args: args).schedule(time_at: time_at)
109
+ schedule(args: args, time_at: time_at)
110
+ end
111
+
112
+ #
113
+ # Enqueue a worker with explicity options.
114
+ #
115
+ # @param [Array<any>] args The job arguments.
116
+ # @param [Time, Integer] time_in The delay in seconds.
117
+ # @param [Time, Integer] time_at The time at which the job should run.
118
+ # @param [String, Symbol] queue The queue on which the worker should run.
119
+ #
120
+ # @return [Cloudtasker::CloudTask] The Google Task response
121
+ #
122
+ def schedule(args: nil, time_in: nil, time_at: nil, queue: nil)
123
+ new(job_args: args, job_queue: queue).schedule({ interval: time_in, time_at: time_at }.compact)
108
124
  end
109
125
 
110
126
  #
@@ -123,11 +139,30 @@ module Cloudtasker
123
139
  # @param [Array<any>] job_args The list of perform args.
124
140
  # @param [String] job_id A unique ID identifying this job.
125
141
  #
126
- def initialize(job_args: [], job_id: nil, job_meta: {}, job_retries: 0)
127
- @job_args = job_args
142
+ def initialize(job_queue: nil, job_args: nil, job_id: nil, job_meta: {}, job_retries: 0)
143
+ @job_args = job_args || []
128
144
  @job_id = job_id || SecureRandom.uuid
129
145
  @job_meta = MetaStore.new(job_meta)
130
146
  @job_retries = job_retries || 0
147
+ @job_queue = job_queue
148
+ end
149
+
150
+ #
151
+ # Return the class name of the worker.
152
+ #
153
+ # @return [String] The class name.
154
+ #
155
+ def job_class_name
156
+ self.class.to_s
157
+ end
158
+
159
+ #
160
+ # Return the queue to use for this worker.
161
+ #
162
+ # @return [String] The name of queue.
163
+ #
164
+ def job_queue
165
+ (@job_queue ||= self.class.cloudtasker_options_hash[:queue] || Config::DEFAULT_JOB_QUEUE).to_s
131
166
  end
132
167
 
133
168
  #
@@ -198,10 +233,10 @@ module Cloudtasker
198
233
  # Return a new instance of the worker with the same args and metadata
199
234
  # but with a different id.
200
235
  #
201
- # @return [<Type>] <description>
236
+ # @return [Cloudtasker::Worker] <description>
202
237
  #
203
238
  def new_instance
204
- self.class.new(job_args: job_args, job_meta: job_meta)
239
+ self.class.new(job_queue: job_queue, job_args: job_args, job_meta: job_meta)
205
240
  end
206
241
 
207
242
  #
@@ -215,7 +250,8 @@ module Cloudtasker
215
250
  job_id: job_id,
216
251
  job_args: job_args,
217
252
  job_meta: job_meta.to_h,
218
- job_retries: job_retries
253
+ job_retries: job_retries,
254
+ job_queue: job_queue
219
255
  }
220
256
  end
221
257
 
@@ -5,7 +5,7 @@ require 'google/cloud/tasks'
5
5
  module Cloudtasker
6
6
  # Build, serialize and schedule tasks on the processing backend.
7
7
  class WorkerHandler
8
- attr_reader :worker, :job_args
8
+ attr_reader :worker
9
9
 
10
10
  # Alrogith used to sign the verification token
11
11
  JWT_ALG = 'HS256'
@@ -42,11 +42,12 @@ module Cloudtasker
42
42
  http_method: 'POST',
43
43
  url: Cloudtasker.config.processor_url,
44
44
  headers: {
45
- 'Content-Type' => 'application/json',
46
- 'Authorization' => "Bearer #{Authenticator.verification_token}"
45
+ Cloudtasker::Config::CONTENT_TYPE_HEADER => 'application/json',
46
+ Cloudtasker::Config::AUTHORIZATION_HEADER => "Bearer #{Authenticator.verification_token}"
47
47
  },
48
48
  body: worker_payload.to_json
49
- }
49
+ },
50
+ queue: worker.job_queue
50
51
  }
51
52
  end
52
53
 
@@ -64,7 +65,8 @@ module Cloudtasker
64
65
  #
65
66
  def worker_payload
66
67
  @worker_payload ||= {
67
- worker: worker.class.to_s,
68
+ worker: worker.job_class_name,
69
+ job_queue: worker.job_queue,
68
70
  job_id: worker.job_id,
69
71
  job_args: worker.job_args,
70
72
  job_meta: worker.job_meta.to_h
@@ -11,7 +11,7 @@ module Cloudtasker
11
11
  end
12
12
 
13
13
  # Only log the job meta information by default (exclude arguments)
14
- DEFAULT_CONTEXT_PROCESSOR = ->(worker) { worker.to_h.slice(:worker, :job_id, :job_meta) }
14
+ DEFAULT_CONTEXT_PROCESSOR = ->(worker) { worker.to_h.slice(:worker, :job_id, :job_meta, :job_queue) }
15
15
 
16
16
  #
17
17
  # Build a new instance of the class.
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cloudtasker/worker'
4
+
5
+ module Cloudtasker
6
+ # A worker class used to schedule jobs without actually
7
+ # instantiating the worker class. This is useful for middlewares
8
+ # needing to enqueue jobs in a Rails initializer. Rails 6 complains
9
+ # about instantiating workers in an iniitializer because of autoloading
10
+ # in zeitwerk mode.
11
+ #
12
+ # Downside of this wrapper: any cloudtasker_options specified on on the
13
+ # worker_class will be ignored.
14
+ #
15
+ # See: https://github.com/rails/rails/issues/36363
16
+ #
17
+ class WorkerWrapper
18
+ include Worker
19
+
20
+ attr_accessor :worker_name
21
+
22
+ #
23
+ # Build a new instance of the class.
24
+ #
25
+ # @param [String] worker_class The name of the worker class.
26
+ # @param [Hash] **opts The worker arguments.
27
+ #
28
+ def initialize(worker_name:, **opts)
29
+ @worker_name = worker_name
30
+ super(opts)
31
+ end
32
+
33
+ #
34
+ # Override parent. Return the underlying worker class name.
35
+ #
36
+ # @return [String] The worker class.
37
+ #
38
+ def job_class_name
39
+ worker_name
40
+ end
41
+
42
+ #
43
+ # Return a new instance of the worker with the same args and metadata
44
+ # but with a different id.
45
+ #
46
+ # @return [Cloudtasker::WorkerWrapper] <description>
47
+ #
48
+ def new_instance
49
+ self.class.new(worker_name: worker_name, job_queue: job_queue, job_args: job_args, job_meta: job_meta)
50
+ end
51
+ end
52
+ end
@@ -1,10 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'cloudtasker/backend/google_cloud_task'
4
+ require 'cloudtasker/config'
5
+
6
+ ENV['GOOGLE_AUTH_SUPPRESS_CREDENTIALS_WARNINGS'] ||= 'true'
4
7
 
5
8
  namespace :cloudtasker do
6
- desc 'Setup the Cloud Task queue'
9
+ desc 'Setup a Cloud Task queue. (default options: ' \
10
+ "name=#{Cloudtasker::Config::DEFAULT_JOB_QUEUE}, " \
11
+ "concurrency=#{Cloudtasker::Config::DEFAULT_QUEUE_CONCURRENCY}, " \
12
+ "retries=#{Cloudtasker::Config::DEFAULT_QUEUE_RETRIES})"
7
13
  task setup_queue: :environment do
8
- Cloudtasker::Backend::GoogleCloudTask.setup_queue
14
+ puts Cloudtasker::Backend::GoogleCloudTask.setup_queue(
15
+ name: ENV['name'],
16
+ concurrency: ENV['concurrency'],
17
+ retries: ENV['retries']
18
+ )
9
19
  end
10
20
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cloudtasker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Arnaud Lachaume
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-11-25 00:00:00.000000000 Z
11
+ date: 2020-01-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -248,7 +248,7 @@ dependencies:
248
248
  - - ">="
249
249
  - !ruby/object:Gem::Version
250
250
  version: '0'
251
- description: Background jobs for Ruby using Google Cloud Tasks (alpha)
251
+ description: Background jobs for Ruby using Google Cloud Tasks (beta)
252
252
  email:
253
253
  - arnaud.lachaume@keypup.io
254
254
  executables:
@@ -264,10 +264,10 @@ files:
264
264
  - CHANGELOG.md
265
265
  - CODE_OF_CONDUCT.md
266
266
  - Gemfile
267
- - Gemfile.lock
268
267
  - LICENSE.txt
269
268
  - README.md
270
269
  - Rakefile
270
+ - _config.yml
271
271
  - app/controllers/cloudtasker/application_controller.rb
272
272
  - app/controllers/cloudtasker/worker_controller.rb
273
273
  - bin/console
@@ -322,7 +322,6 @@ files:
322
322
  - lib/cloudtasker/local_server.rb
323
323
  - lib/cloudtasker/meta_store.rb
324
324
  - lib/cloudtasker/middleware/chain.rb
325
- - lib/cloudtasker/railtie.rb
326
325
  - lib/cloudtasker/redis_client.rb
327
326
  - lib/cloudtasker/testing.rb
328
327
  - lib/cloudtasker/unique_job.rb
@@ -344,6 +343,7 @@ files:
344
343
  - lib/cloudtasker/worker.rb
345
344
  - lib/cloudtasker/worker_handler.rb
346
345
  - lib/cloudtasker/worker_logger.rb
346
+ - lib/cloudtasker/worker_wrapper.rb
347
347
  - lib/tasks/setup_queue.rake
348
348
  homepage: https://github.com/keypup-io/cloudtasker
349
349
  licenses:
@@ -371,5 +371,5 @@ rubyforge_project:
371
371
  rubygems_version: 2.7.9
372
372
  signing_key:
373
373
  specification_version: 4
374
- summary: Background jobs for Ruby using Google Cloud Tasks (alpha)
374
+ summary: Background jobs for Ruby using Google Cloud Tasks (beta)
375
375
  test_files: []