cloudtasker-tonix 0.1.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 (100) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/lint_rubocop.yml +15 -0
  3. data/.github/workflows/test_ruby_3.x.yml +40 -0
  4. data/.gitignore +23 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +96 -0
  7. data/Appraisals +76 -0
  8. data/CHANGELOG.md +248 -0
  9. data/CODE_OF_CONDUCT.md +74 -0
  10. data/Gemfile +18 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +1311 -0
  13. data/Rakefile +8 -0
  14. data/_config.yml +1 -0
  15. data/app/controllers/cloudtasker/worker_controller.rb +107 -0
  16. data/bin/console +15 -0
  17. data/bin/setup +8 -0
  18. data/cloudtasker.gemspec +42 -0
  19. data/config/routes.rb +5 -0
  20. data/docs/BATCH_JOBS.md +144 -0
  21. data/docs/CRON_JOBS.md +129 -0
  22. data/docs/STORABLE_JOBS.md +68 -0
  23. data/docs/UNIQUE_JOBS.md +190 -0
  24. data/exe/cloudtasker +30 -0
  25. data/gemfiles/.bundle/config +2 -0
  26. data/gemfiles/google_cloud_tasks_1.0.gemfile +17 -0
  27. data/gemfiles/google_cloud_tasks_1.1.gemfile +17 -0
  28. data/gemfiles/google_cloud_tasks_1.2.gemfile +17 -0
  29. data/gemfiles/google_cloud_tasks_1.3.gemfile +17 -0
  30. data/gemfiles/google_cloud_tasks_1.4.gemfile +17 -0
  31. data/gemfiles/google_cloud_tasks_1.5.gemfile +17 -0
  32. data/gemfiles/google_cloud_tasks_2.0.gemfile +17 -0
  33. data/gemfiles/google_cloud_tasks_2.1.gemfile +17 -0
  34. data/gemfiles/rails_6.1.gemfile +20 -0
  35. data/gemfiles/rails_7.0.gemfile +18 -0
  36. data/gemfiles/rails_7.1.gemfile +18 -0
  37. data/gemfiles/rails_8.0.gemfile +18 -0
  38. data/gemfiles/rails_8.1.gemfile +18 -0
  39. data/gemfiles/semantic_logger_3.4.gemfile +16 -0
  40. data/gemfiles/semantic_logger_4.6.gemfile +16 -0
  41. data/gemfiles/semantic_logger_4.7.0.gemfile +16 -0
  42. data/gemfiles/semantic_logger_4.7.2.gemfile +16 -0
  43. data/lib/active_job/queue_adapters/cloudtasker_adapter.rb +89 -0
  44. data/lib/cloudtasker/authentication_error.rb +6 -0
  45. data/lib/cloudtasker/authenticator.rb +90 -0
  46. data/lib/cloudtasker/backend/google_cloud_task_v1.rb +228 -0
  47. data/lib/cloudtasker/backend/google_cloud_task_v2.rb +231 -0
  48. data/lib/cloudtasker/backend/memory_task.rb +202 -0
  49. data/lib/cloudtasker/backend/redis_task.rb +291 -0
  50. data/lib/cloudtasker/batch/batch_progress.rb +142 -0
  51. data/lib/cloudtasker/batch/extension/worker.rb +13 -0
  52. data/lib/cloudtasker/batch/job.rb +558 -0
  53. data/lib/cloudtasker/batch/middleware/server.rb +14 -0
  54. data/lib/cloudtasker/batch/middleware.rb +25 -0
  55. data/lib/cloudtasker/batch.rb +5 -0
  56. data/lib/cloudtasker/cli.rb +194 -0
  57. data/lib/cloudtasker/cloud_task.rb +130 -0
  58. data/lib/cloudtasker/config.rb +319 -0
  59. data/lib/cloudtasker/cron/job.rb +205 -0
  60. data/lib/cloudtasker/cron/middleware/server.rb +14 -0
  61. data/lib/cloudtasker/cron/middleware.rb +20 -0
  62. data/lib/cloudtasker/cron/schedule.rb +308 -0
  63. data/lib/cloudtasker/cron.rb +5 -0
  64. data/lib/cloudtasker/dead_worker_error.rb +6 -0
  65. data/lib/cloudtasker/engine.rb +24 -0
  66. data/lib/cloudtasker/invalid_worker_error.rb +6 -0
  67. data/lib/cloudtasker/local_server.rb +99 -0
  68. data/lib/cloudtasker/max_task_size_exceeded_error.rb +14 -0
  69. data/lib/cloudtasker/meta_store.rb +86 -0
  70. data/lib/cloudtasker/middleware/chain.rb +250 -0
  71. data/lib/cloudtasker/missing_worker_arguments_error.rb +6 -0
  72. data/lib/cloudtasker/redis_client.rb +166 -0
  73. data/lib/cloudtasker/retry_worker_error.rb +6 -0
  74. data/lib/cloudtasker/storable/worker.rb +78 -0
  75. data/lib/cloudtasker/storable.rb +3 -0
  76. data/lib/cloudtasker/testing.rb +184 -0
  77. data/lib/cloudtasker/unique_job/conflict_strategy/base_strategy.rb +39 -0
  78. data/lib/cloudtasker/unique_job/conflict_strategy/raise.rb +28 -0
  79. data/lib/cloudtasker/unique_job/conflict_strategy/reject.rb +11 -0
  80. data/lib/cloudtasker/unique_job/conflict_strategy/reschedule.rb +30 -0
  81. data/lib/cloudtasker/unique_job/job.rb +168 -0
  82. data/lib/cloudtasker/unique_job/lock/base_lock.rb +70 -0
  83. data/lib/cloudtasker/unique_job/lock/no_op.rb +11 -0
  84. data/lib/cloudtasker/unique_job/lock/until_completed.rb +40 -0
  85. data/lib/cloudtasker/unique_job/lock/until_executed.rb +36 -0
  86. data/lib/cloudtasker/unique_job/lock/until_executing.rb +30 -0
  87. data/lib/cloudtasker/unique_job/lock/while_executing.rb +25 -0
  88. data/lib/cloudtasker/unique_job/lock_error.rb +8 -0
  89. data/lib/cloudtasker/unique_job/middleware/client.rb +15 -0
  90. data/lib/cloudtasker/unique_job/middleware/server.rb +14 -0
  91. data/lib/cloudtasker/unique_job/middleware.rb +36 -0
  92. data/lib/cloudtasker/unique_job.rb +32 -0
  93. data/lib/cloudtasker/version.rb +5 -0
  94. data/lib/cloudtasker/worker.rb +487 -0
  95. data/lib/cloudtasker/worker_handler.rb +250 -0
  96. data/lib/cloudtasker/worker_logger.rb +231 -0
  97. data/lib/cloudtasker/worker_wrapper.rb +52 -0
  98. data/lib/cloudtasker.rb +57 -0
  99. data/lib/tasks/setup_queue.rake +20 -0
  100. metadata +241 -0
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fugit'
4
+
5
+ module Cloudtasker
6
+ module Cron
7
+ # Manage cron jobs
8
+ class Job
9
+ attr_reader :worker
10
+
11
+ #
12
+ # Build a new instance of the class
13
+ #
14
+ # @param [Cloudtasker::Worker] worker The cloudtasker worker
15
+ #
16
+ def initialize(worker)
17
+ @worker = worker
18
+ end
19
+
20
+ #
21
+ # Return a namespaced key
22
+ #
23
+ # @param [String, Symbol] val The key to namespace
24
+ #
25
+ # @return [String] The namespaced key.
26
+ #
27
+ def key(val)
28
+ return nil if val.nil?
29
+
30
+ [self.class.to_s.underscore, val.to_s].join('/')
31
+ end
32
+
33
+ #
34
+ # Add cron metadata to the worker.
35
+ #
36
+ # @param [String, Symbol] name The name of the cron task.
37
+ # @param [String] cron The cron expression.
38
+ #
39
+ # @return [Cloudtasker::Cron::Job] self.
40
+ #
41
+ def set(schedule_id:)
42
+ worker.job_meta.set(key(:schedule_id), schedule_id.to_s)
43
+ self
44
+ end
45
+
46
+ #
47
+ # Return the worker id.
48
+ #
49
+ # @return [String] The worker id.
50
+ #
51
+ def job_id
52
+ worker.job_id
53
+ end
54
+
55
+ #
56
+ # Return the namespaced worker id.
57
+ #
58
+ # @return [String] The worker namespaced id.
59
+ #
60
+ def job_gid
61
+ key(job_id)
62
+ end
63
+
64
+ #
65
+ # Return the cron schedule id.
66
+ #
67
+ # @return [String] The schedule id.
68
+ #
69
+ def schedule_id
70
+ @schedule_id ||= worker.job_meta.get(key(:schedule_id))
71
+ end
72
+
73
+ #
74
+ # Return true if the worker is tagged as a cron job.
75
+ #
76
+ # @return [Boolean] True if the worker relates to a cron schedule.
77
+ #
78
+ def cron_job?
79
+ cron_schedule
80
+ end
81
+
82
+ #
83
+ # Return true if the worker is currently processing (includes retries).
84
+ #
85
+ # @return [Boolean] True f the worker is processing.
86
+ #
87
+ def retry_instance?
88
+ cron_job? && state
89
+ end
90
+
91
+ #
92
+ # Return the job processing state.
93
+ #
94
+ # @return [String, nil] The processing state.
95
+ #
96
+ def state
97
+ redis.get(job_gid)&.to_sym
98
+ end
99
+
100
+ #
101
+ # Return the cloudtasker redis client
102
+ #
103
+ # @return [Cloudtasker::RedisClient] The cloudtasker redis client..
104
+ #
105
+ def redis
106
+ @redis ||= RedisClient.new
107
+ end
108
+
109
+ #
110
+ # Return the cron schedule to use for the job.
111
+ #
112
+ # @return [Fugit::Cron] The cron schedule.
113
+ #
114
+ def cron_schedule
115
+ return nil unless schedule_id
116
+
117
+ @cron_schedule ||= Cron::Schedule.find(schedule_id)
118
+ end
119
+
120
+ #
121
+ # Return the time this cron instance is expected to run at.
122
+ #
123
+ # @return [Time] The current cron instance time.
124
+ #
125
+ def current_time
126
+ @current_time ||=
127
+ begin
128
+ Time.parse(worker.job_meta.get(key(:time_at)).to_s)
129
+ rescue ArgumentError
130
+ Time.try(:current) || Time.now
131
+ end
132
+ end
133
+
134
+ #
135
+ # Return the Time when the job should run next.
136
+ #
137
+ # @return [EtOrbi::EoTime] The time the job should run next.
138
+ #
139
+ def next_time
140
+ @next_time ||= cron_schedule&.next_time(current_time)
141
+ end
142
+
143
+ #
144
+ # Return true if the cron job is the one we are expecting. This method
145
+ # is used to ensure that jobs related to outdated cron schedules do not
146
+ # get processed.
147
+ #
148
+ # @return [Boolean] True if the cron job is expected.
149
+ #
150
+ def expected_instance?
151
+ retry_instance? || cron_schedule.job_id == job_id
152
+ end
153
+
154
+ #
155
+ # Store the cron job instance state.
156
+ #
157
+ # @param [String, Symbol] state The worker state.
158
+ #
159
+ def flag(state)
160
+ state.to_sym == :done ? redis.del(job_gid) : redis.set(job_gid, state.to_s)
161
+ end
162
+
163
+ #
164
+ # Schedule the next cron instance.
165
+ #
166
+ # The task only gets scheduled the first time a worker runs for a
167
+ # given cron instance (Typically a cron worker failing and retrying will
168
+ # not lead to a new task getting scheduled).
169
+ #
170
+ def schedule!
171
+ return false unless cron_schedule
172
+
173
+ # Configure next cron worker
174
+ next_worker = worker.new_instance.tap { |e| e.job_meta.set(key(:time_at), next_time.iso8601) }
175
+
176
+ # Schedule next worker
177
+ task = next_worker.schedule(time_at: next_time)
178
+ cron_schedule.update(task_id: task.id, job_id: next_worker.job_id)
179
+ end
180
+
181
+ #
182
+ # Execute the (cron) job. This method is invoked by the cron middleware.
183
+ #
184
+ def execute
185
+ # Execute the job immediately if this worker is not flagged as a cron job.
186
+ return yield unless cron_job?
187
+
188
+ # Abort and reject job if this cron instance is not expected.
189
+ return true unless expected_instance?
190
+
191
+ # Schedule the next instance of the job
192
+ schedule! unless retry_instance?
193
+
194
+ # Flag the cron instance as processing.
195
+ flag(:processing)
196
+
197
+ # Execute the cron instance
198
+ yield
199
+
200
+ # Flag the cron instance as done
201
+ flag(:done)
202
+ end
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudtasker
4
+ module Cron
5
+ module Middleware
6
+ # Server middleware, invoked when jobs are executed
7
+ class Server
8
+ def call(worker, **_kwargs, &block)
9
+ Job.new(worker).execute(&block)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cloudtasker/redis_client'
4
+
5
+ require_relative 'schedule'
6
+ require_relative 'job'
7
+ require_relative 'middleware/server'
8
+
9
+ module Cloudtasker
10
+ module Cron
11
+ # Registration module
12
+ module Middleware
13
+ def self.configure
14
+ Cloudtasker.configure do |config|
15
+ config.server_middleware { |c| c.add(Middleware::Server) }
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,308 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fugit'
4
+ require 'cloudtasker/worker_wrapper'
5
+
6
+ module Cloudtasker
7
+ module Cron
8
+ # Manage cron schedules
9
+ class Schedule
10
+ attr_accessor :id, :cron, :worker, :task_id, :job_id, :queue, :args
11
+
12
+ #
13
+ # Return the redis client.
14
+ #
15
+ # @return [Cloudtasker::RedisClient] The cloudtasker redis client.
16
+ #
17
+ def self.redis
18
+ @redis ||= RedisClient.new
19
+ end
20
+
21
+ #
22
+ # Return a namespaced key.
23
+ #
24
+ # @param [String, Symbol, nil] val The key to namespace
25
+ #
26
+ # @return [String] The namespaced key.
27
+ #
28
+ def self.key(val = nil)
29
+ [to_s.underscore, val].compact.map(&:to_s).join('/')
30
+ end
31
+
32
+ #
33
+ # Return all schedules
34
+ #
35
+ # @return [Array<Cloudtasker::Batch::Schedule>] The list of stored schedules.
36
+ #
37
+ def self.all
38
+ if redis.exists?(key)
39
+ # Use Schedule Set if available
40
+ redis.smembers(key).map { |id| find(id) }
41
+ else
42
+ # Fallback to redis key matching and migrate schedules
43
+ # to use Schedule Set instead.
44
+ redis.search(key('*')).map do |gid|
45
+ schedule_id = gid.sub(key(''), '')
46
+ redis.sadd(key, [schedule_id])
47
+ find(schedule_id)
48
+ end
49
+ end
50
+ end
51
+
52
+ #
53
+ # Synchronize list of cron schedules from a Hash. Schedules
54
+ # not listed in this hash will be removed.
55
+ #
56
+ # @example
57
+ # Cloudtasker::Cron::Schedule.load_from_hash!(
58
+ # my_job: { cron: '0 0 * * *', worker: 'MyWorker' }
59
+ # my_other_job: { cron: '0 10 * * *', worker: 'MyOtherWorker' }
60
+ # )
61
+ #
62
+ def self.load_from_hash!(hash)
63
+ schedules = hash.map do |id, config|
64
+ schedule_config = JSON.parse(config.to_json, symbolize_names: true).merge(id: id.to_s)
65
+ create(**schedule_config)
66
+ end
67
+
68
+ # Remove existing schedules which are not part of the list
69
+ all.reject { |e| schedules.include?(e) }.each { |e| delete(e.id) }
70
+ end
71
+
72
+ #
73
+ # Create a new cron schedule (or update an existing one).
74
+ #
75
+ # @param [Hash] **opts Init opts. See initialize
76
+ #
77
+ # @return [Cloudtasker::Cron::Schedule] The schedule instance.
78
+ #
79
+ def self.create(**opts)
80
+ redis.with_lock(key(opts[:id])) do
81
+ config = find(opts[:id]).to_h.merge(opts)
82
+ new(**config).tap(&:save)
83
+ end
84
+ end
85
+
86
+ #
87
+ # Return a saved cron schedule.
88
+ #
89
+ # @param [String] id The schedule id.
90
+ #
91
+ # @return [Cloudtasker::Cron::Schedule] The schedule instance.
92
+ #
93
+ def self.find(id)
94
+ return nil unless (schedule_config = redis.fetch(key(id)))
95
+
96
+ new(**schedule_config)
97
+ end
98
+
99
+ #
100
+ # Delete a schedule by id.
101
+ #
102
+ # @param [String] id The schedule id.
103
+ #
104
+ def self.delete(id)
105
+ redis.with_lock(key(id)) do
106
+ schedule = find(id)
107
+ return false unless schedule
108
+
109
+ # Delete task and stored schedule
110
+ CloudTask.delete(schedule.task_id) if schedule.task_id
111
+ redis.srem(key, [schedule.id])
112
+ redis.del(schedule.gid)
113
+ end
114
+ end
115
+
116
+ #
117
+ # Build a new instance of the class.
118
+ #
119
+ # @param [String] id The schedule id.
120
+ # @param [String] cron The cron expression.
121
+ # @param [Class] worker The worker class to run.
122
+ # @param [Array<any>] args The worker arguments.
123
+ # @param [String] queue The queue to use for the cron job.
124
+ # @param [String] task_id The ID of the actual backend task.
125
+ # @param [String] job_id The ID of the Cloudtasker worker.
126
+ #
127
+ def initialize(id:, cron:, worker:, **opts)
128
+ @id = id
129
+ @cron = cron
130
+ @worker = worker
131
+ @args = opts[:args]
132
+ @queue = opts[:queue]
133
+ @task_id = opts[:task_id]
134
+ @job_id = opts[:job_id]
135
+ end
136
+
137
+ #
138
+ # Return the redis client.
139
+ #
140
+ # @return [Cloudtasker::RedisClient] The cloudtasker redis client.
141
+ #
142
+ def redis
143
+ self.class.redis
144
+ end
145
+
146
+ #
147
+ # Return the namespaced schedule id.
148
+ #
149
+ # @return [String] The namespaced schedule id.
150
+ #
151
+ def gid
152
+ self.class.key(id)
153
+ end
154
+
155
+ #
156
+ # Return true if the schedule is valid.
157
+ #
158
+ # @return [Boolean] True if the schedule is valid.
159
+ #
160
+ def valid?
161
+ id && cron_schedule && worker
162
+ rescue ArgumentError
163
+ # Rescue invalid cron expressions
164
+ false
165
+ end
166
+
167
+ #
168
+ # Equality operator.
169
+ #
170
+ # @param [Any] other The object to compare.
171
+ #
172
+ # @return [Boolean] True if the object is equal.
173
+ #
174
+ def ==(other)
175
+ other.is_a?(self.class) && other.id == id
176
+ end
177
+
178
+ #
179
+ # Return true if the configuration of the schedule was
180
+ # changed (cron expression or worker).
181
+ #
182
+ # @return [Boolean] True if the schedule config was changed.
183
+ #
184
+ def config_changed?
185
+ self.class.find(id)&.to_config != to_config
186
+ end
187
+
188
+ #
189
+ # RReturn true if the instance attributes were changed compared
190
+ # to the schedule saved in Redis.
191
+ #
192
+ # @return [Boolean] True if the schedule was modified.
193
+ #
194
+ def changed?
195
+ to_h != self.class.find(id).to_h
196
+ end
197
+
198
+ #
199
+ # Return a hash describing the configuration of this schedule.
200
+ #
201
+ # @return [Hash] The config description hash.
202
+ #
203
+ def to_config
204
+ {
205
+ id: id,
206
+ cron: cron,
207
+ worker: worker,
208
+ args: args,
209
+ queue: queue
210
+ }
211
+ end
212
+
213
+ #
214
+ # Return a hash with all the schedule attributes.
215
+ #
216
+ # @return [Hash] The attributes hash.
217
+ #
218
+ def to_h
219
+ to_config.merge(
220
+ task_id: task_id,
221
+ job_id: job_id
222
+ )
223
+ end
224
+
225
+ #
226
+ # Return the cron schedule to use for the job.
227
+ #
228
+ # @return [Fugit::Cron] The cron schedule.
229
+ #
230
+ def cron_schedule
231
+ @cron_schedule ||= Fugit::Cron.do_parse(cron)
232
+ end
233
+
234
+ #
235
+ # Return an instance of the underlying worker.
236
+ #
237
+ # @return [Cloudtasker::WorkerWrapper] The worker instance
238
+ #
239
+ def worker_instance
240
+ WorkerWrapper.new(worker_name: worker, job_args: args, job_queue: queue)
241
+ end
242
+
243
+ #
244
+ # Return the next time a job should run.
245
+ #
246
+ # @param [Time] time An optional reference in time (instead of Time.now)
247
+ #
248
+ # @return [EtOrbi::EoTime] The time the schedule job should run next.
249
+ #
250
+ def next_time(*args)
251
+ cron_schedule.next_time(*args)
252
+ end
253
+
254
+ #
255
+ # Buld edit the object attributes.
256
+ #
257
+ # @param [Hash] opts The attributes to edit.
258
+ #
259
+ def assign_attributes(opts)
260
+ opts
261
+ .select { |k, _| instance_variables.include?(:"@#{k}") }
262
+ .each { |k, v| instance_variable_set("@#{k}", v) }
263
+ end
264
+
265
+ #
266
+ # Edit the object attributes and save the object in Redis.
267
+ #
268
+ # @param [Hash] opts The attributes to edit.
269
+ #
270
+ def update(opts)
271
+ assign_attributes(opts)
272
+ save
273
+ end
274
+
275
+ #
276
+ # Save the object in Redis. If the configuration was changed
277
+ # then any existing cloud task is removed and a task is recreated.
278
+ #
279
+ def save(update_task: true)
280
+ return false unless valid?
281
+
282
+ # Save schedule
283
+ config_was_changed = config_changed?
284
+ redis.sadd(self.class.key, [id])
285
+ redis.write(gid, to_h)
286
+
287
+ # Stop there if backend does not need update
288
+ return true unless update_task && (config_was_changed || !task_id || !CloudTask.find(task_id))
289
+
290
+ # Update backend
291
+ persist_cloud_task
292
+ end
293
+
294
+ private
295
+
296
+ #
297
+ # Update the task in backend.
298
+ #
299
+ def persist_cloud_task
300
+ # Delete previous instance
301
+ CloudTask.delete(task_id) if task_id
302
+
303
+ # Schedule worker
304
+ Job.new(worker_instance).set(schedule_id: id).schedule!
305
+ end
306
+ end
307
+ end
308
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'cron/middleware'
4
+
5
+ Cloudtasker::Cron::Middleware.configure
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudtasker
4
+ class DeadWorkerError < StandardError
5
+ end
6
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudtasker
4
+ # Cloudtasker Rails engine
5
+ class Engine < ::Rails::Engine
6
+ isolate_namespace Cloudtasker
7
+
8
+ config.before_initialize do
9
+ # Mount cloudtasker processing endpoint
10
+ Rails.application.routes.append do
11
+ mount Cloudtasker::Engine, at: '/cloudtasker'
12
+ end
13
+
14
+ # Add ActiveJob adapter
15
+ require 'active_job/queue_adapters/cloudtasker_adapter' if defined?(::ActiveJob::Railtie)
16
+ end
17
+
18
+ config.generators do |g|
19
+ g.test_framework :rspec, fixture: false
20
+ g.assets false
21
+ g.helper false
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudtasker
4
+ class InvalidWorkerError < StandardError
5
+ end
6
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cloudtasker/backend/redis_task'
4
+
5
+ module Cloudtasker
6
+ # Process jobs stored in Redis.
7
+ # Only to be used in development.
8
+ class LocalServer
9
+ # Max number of task requests sent to the processing server
10
+ CONCURRENCY = (ENV['CLOUDTASKER_CONCURRENCY'] || 5).to_i
11
+
12
+ # Default number of threads to allocate to process a specific queue
13
+ QUEUE_CONCURRENCY = 1
14
+
15
+ # Job Polling. How frequently to poll jobs in redis.
16
+ JOB_POLLING_FREQUENCY = 0.5 # seconds
17
+
18
+ #
19
+ # Stop the local server.
20
+ #
21
+ def stop
22
+ @done = true
23
+
24
+ # Terminate threads and repush tasks
25
+ @threads&.values&.flatten&.each do |t|
26
+ t.terminate
27
+ t['task']&.retry_later(0, is_error: false)
28
+ end
29
+
30
+ # Wait for main server to be done
31
+ sleep 1 while @start&.alive?
32
+ end
33
+
34
+ #
35
+ # Start the local server
36
+ #
37
+ # @param [Hash] opts Server options.
38
+ #
39
+ #
40
+ def start(opts = {})
41
+ # Extract queues to process
42
+ queues = opts[:queues].to_a.any? ? opts[:queues] : [[nil, CONCURRENCY]]
43
+
44
+ # Display start banner
45
+ queue_labels = queues.map { |n, c| "#{n || 'all'}=#{c || QUEUE_CONCURRENCY}" }.join(' ')
46
+ Cloudtasker.logger.info("[Cloudtasker/Server] Processing queues: #{queue_labels}")
47
+
48
+ # Start processing queues
49
+ @start ||= Thread.new do
50
+ until @done
51
+ queues.each { |(n, c)| process_jobs(n, c) }
52
+ sleep JOB_POLLING_FREQUENCY
53
+ end
54
+ Cloudtasker.logger.info('[Cloudtasker/Server] Local server exiting...')
55
+ end
56
+ end
57
+
58
+ #
59
+ # Process enqueued workers.
60
+ #
61
+ #
62
+ def process_jobs(queue = nil, concurrency = nil)
63
+ @threads ||= {}
64
+ @threads[queue] ||= []
65
+ max_threads = (concurrency || QUEUE_CONCURRENCY).to_i
66
+
67
+ # Remove any done thread
68
+ @threads[queue].select!(&:alive?)
69
+
70
+ # Process tasks
71
+ while @threads[queue].count < max_threads && (task = Cloudtasker::Backend::RedisTask.pop(queue))
72
+ @threads[queue] << Thread.new(task) { |t| process_task(t) }
73
+ end
74
+ end
75
+
76
+ #
77
+ # Process a given task
78
+ #
79
+ # @param [Cloudtasker::CloudTask] task The task to process
80
+ #
81
+ def process_task(task)
82
+ Thread.current['task'] = task
83
+ Thread.current['attempts'] = 0
84
+
85
+ # Deliver task
86
+ begin
87
+ Thread.current['task']&.deliver
88
+ rescue Errno::EBADF, Errno::ECONNREFUSED => e
89
+ raise(e) unless Thread.current['attempts'] < 3
90
+
91
+ # Retry on connection error, in case the web server is not
92
+ # started yet.
93
+ Thread.current['attempts'] += 1
94
+ sleep(3)
95
+ retry
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudtasker
4
+ # Handle Cloud Task size quota
5
+ # See: https://cloud.google.com/appengine/quotas#Task_Queue
6
+ #
7
+ class MaxTaskSizeExceededError < StandardError
8
+ MSG = 'The size of Cloud Tasks must not exceed 100KB'
9
+
10
+ def initialize(msg = MSG)
11
+ super
12
+ end
13
+ end
14
+ end