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.
- checksums.yaml +7 -0
- data/.github/workflows/lint_rubocop.yml +15 -0
- data/.github/workflows/test_ruby_3.x.yml +40 -0
- data/.gitignore +23 -0
- data/.rspec +3 -0
- data/.rubocop.yml +96 -0
- data/Appraisals +76 -0
- data/CHANGELOG.md +248 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +18 -0
- data/LICENSE.txt +21 -0
- data/README.md +1311 -0
- data/Rakefile +8 -0
- data/_config.yml +1 -0
- data/app/controllers/cloudtasker/worker_controller.rb +107 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/cloudtasker.gemspec +42 -0
- data/config/routes.rb +5 -0
- data/docs/BATCH_JOBS.md +144 -0
- data/docs/CRON_JOBS.md +129 -0
- data/docs/STORABLE_JOBS.md +68 -0
- data/docs/UNIQUE_JOBS.md +190 -0
- data/exe/cloudtasker +30 -0
- data/gemfiles/.bundle/config +2 -0
- data/gemfiles/google_cloud_tasks_1.0.gemfile +17 -0
- data/gemfiles/google_cloud_tasks_1.1.gemfile +17 -0
- data/gemfiles/google_cloud_tasks_1.2.gemfile +17 -0
- data/gemfiles/google_cloud_tasks_1.3.gemfile +17 -0
- data/gemfiles/google_cloud_tasks_1.4.gemfile +17 -0
- data/gemfiles/google_cloud_tasks_1.5.gemfile +17 -0
- data/gemfiles/google_cloud_tasks_2.0.gemfile +17 -0
- data/gemfiles/google_cloud_tasks_2.1.gemfile +17 -0
- data/gemfiles/rails_6.1.gemfile +20 -0
- data/gemfiles/rails_7.0.gemfile +18 -0
- data/gemfiles/rails_7.1.gemfile +18 -0
- data/gemfiles/rails_8.0.gemfile +18 -0
- data/gemfiles/rails_8.1.gemfile +18 -0
- data/gemfiles/semantic_logger_3.4.gemfile +16 -0
- data/gemfiles/semantic_logger_4.6.gemfile +16 -0
- data/gemfiles/semantic_logger_4.7.0.gemfile +16 -0
- data/gemfiles/semantic_logger_4.7.2.gemfile +16 -0
- data/lib/active_job/queue_adapters/cloudtasker_adapter.rb +89 -0
- data/lib/cloudtasker/authentication_error.rb +6 -0
- data/lib/cloudtasker/authenticator.rb +90 -0
- data/lib/cloudtasker/backend/google_cloud_task_v1.rb +228 -0
- data/lib/cloudtasker/backend/google_cloud_task_v2.rb +231 -0
- data/lib/cloudtasker/backend/memory_task.rb +202 -0
- data/lib/cloudtasker/backend/redis_task.rb +291 -0
- data/lib/cloudtasker/batch/batch_progress.rb +142 -0
- data/lib/cloudtasker/batch/extension/worker.rb +13 -0
- data/lib/cloudtasker/batch/job.rb +558 -0
- data/lib/cloudtasker/batch/middleware/server.rb +14 -0
- data/lib/cloudtasker/batch/middleware.rb +25 -0
- data/lib/cloudtasker/batch.rb +5 -0
- data/lib/cloudtasker/cli.rb +194 -0
- data/lib/cloudtasker/cloud_task.rb +130 -0
- data/lib/cloudtasker/config.rb +319 -0
- data/lib/cloudtasker/cron/job.rb +205 -0
- data/lib/cloudtasker/cron/middleware/server.rb +14 -0
- data/lib/cloudtasker/cron/middleware.rb +20 -0
- data/lib/cloudtasker/cron/schedule.rb +308 -0
- data/lib/cloudtasker/cron.rb +5 -0
- data/lib/cloudtasker/dead_worker_error.rb +6 -0
- data/lib/cloudtasker/engine.rb +24 -0
- data/lib/cloudtasker/invalid_worker_error.rb +6 -0
- data/lib/cloudtasker/local_server.rb +99 -0
- data/lib/cloudtasker/max_task_size_exceeded_error.rb +14 -0
- data/lib/cloudtasker/meta_store.rb +86 -0
- data/lib/cloudtasker/middleware/chain.rb +250 -0
- data/lib/cloudtasker/missing_worker_arguments_error.rb +6 -0
- data/lib/cloudtasker/redis_client.rb +166 -0
- data/lib/cloudtasker/retry_worker_error.rb +6 -0
- data/lib/cloudtasker/storable/worker.rb +78 -0
- data/lib/cloudtasker/storable.rb +3 -0
- data/lib/cloudtasker/testing.rb +184 -0
- data/lib/cloudtasker/unique_job/conflict_strategy/base_strategy.rb +39 -0
- data/lib/cloudtasker/unique_job/conflict_strategy/raise.rb +28 -0
- data/lib/cloudtasker/unique_job/conflict_strategy/reject.rb +11 -0
- data/lib/cloudtasker/unique_job/conflict_strategy/reschedule.rb +30 -0
- data/lib/cloudtasker/unique_job/job.rb +168 -0
- data/lib/cloudtasker/unique_job/lock/base_lock.rb +70 -0
- data/lib/cloudtasker/unique_job/lock/no_op.rb +11 -0
- data/lib/cloudtasker/unique_job/lock/until_completed.rb +40 -0
- data/lib/cloudtasker/unique_job/lock/until_executed.rb +36 -0
- data/lib/cloudtasker/unique_job/lock/until_executing.rb +30 -0
- data/lib/cloudtasker/unique_job/lock/while_executing.rb +25 -0
- data/lib/cloudtasker/unique_job/lock_error.rb +8 -0
- data/lib/cloudtasker/unique_job/middleware/client.rb +15 -0
- data/lib/cloudtasker/unique_job/middleware/server.rb +14 -0
- data/lib/cloudtasker/unique_job/middleware.rb +36 -0
- data/lib/cloudtasker/unique_job.rb +32 -0
- data/lib/cloudtasker/version.rb +5 -0
- data/lib/cloudtasker/worker.rb +487 -0
- data/lib/cloudtasker/worker_handler.rb +250 -0
- data/lib/cloudtasker/worker_logger.rb +231 -0
- data/lib/cloudtasker/worker_wrapper.rb +52 -0
- data/lib/cloudtasker.rb +57 -0
- data/lib/tasks/setup_queue.rake +20 -0
- 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,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,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
|