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,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cloudtasker/backend/memory_task'
|
|
4
|
+
|
|
5
|
+
module Cloudtasker
|
|
6
|
+
# Enable/Disable test mode for Cloudtasker
|
|
7
|
+
module Testing
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
#
|
|
11
|
+
# Set the test mode, either permanently or
|
|
12
|
+
# temporarily (via block).
|
|
13
|
+
#
|
|
14
|
+
# @param [Symbol] mode The test mode.
|
|
15
|
+
#
|
|
16
|
+
# @return [Symbol] The test mode.
|
|
17
|
+
#
|
|
18
|
+
def switch_test_mode(mode)
|
|
19
|
+
if block_given?
|
|
20
|
+
current_mode = @test_mode
|
|
21
|
+
begin
|
|
22
|
+
@test_mode = mode
|
|
23
|
+
yield
|
|
24
|
+
ensure
|
|
25
|
+
@test_mode = current_mode
|
|
26
|
+
end
|
|
27
|
+
else
|
|
28
|
+
@test_mode = mode
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
#
|
|
33
|
+
# Set the error mode, either permanently or
|
|
34
|
+
# temporarily (via block).
|
|
35
|
+
#
|
|
36
|
+
# @param [Symbol] mode The error mode.
|
|
37
|
+
#
|
|
38
|
+
# @return [Symbol] The error mode.
|
|
39
|
+
#
|
|
40
|
+
def switch_error_mode(mode)
|
|
41
|
+
if block_given?
|
|
42
|
+
current_mode = @error_mode
|
|
43
|
+
begin
|
|
44
|
+
@error_mode = mode
|
|
45
|
+
yield
|
|
46
|
+
ensure
|
|
47
|
+
@error_mode = current_mode
|
|
48
|
+
end
|
|
49
|
+
else
|
|
50
|
+
@error_mode = mode
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
#
|
|
55
|
+
# Set cloudtasker to real mode temporarily
|
|
56
|
+
#
|
|
57
|
+
# @param [Proc] &block The block to run in real mode
|
|
58
|
+
#
|
|
59
|
+
def enable!(&block)
|
|
60
|
+
switch_test_mode(:enabled, &block)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
#
|
|
64
|
+
# Set cloudtasker to fake mode temporarily
|
|
65
|
+
#
|
|
66
|
+
# @param [Proc] &block The block to run in fake mode
|
|
67
|
+
#
|
|
68
|
+
def fake!(&block)
|
|
69
|
+
switch_test_mode(:fake, &block)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
#
|
|
73
|
+
# Set cloudtasker to inline mode temporarily
|
|
74
|
+
#
|
|
75
|
+
# @param [Proc] &block The block to run in inline mode
|
|
76
|
+
#
|
|
77
|
+
def inline!(&block)
|
|
78
|
+
switch_test_mode(:inline, &block)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
#
|
|
82
|
+
# Return true if Cloudtasker is enabled.
|
|
83
|
+
#
|
|
84
|
+
def enabled?
|
|
85
|
+
!@test_mode || @test_mode == :enabled
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
#
|
|
89
|
+
# Return true if Cloudtasker is in fake mode.
|
|
90
|
+
#
|
|
91
|
+
# @return [Boolean] True if jobs must be processed through drain calls.
|
|
92
|
+
#
|
|
93
|
+
def fake?
|
|
94
|
+
@test_mode == :fake
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
#
|
|
98
|
+
# Return true if Cloudtasker is in inline mode.
|
|
99
|
+
#
|
|
100
|
+
# @return [Boolean] True if jobs are run inline.
|
|
101
|
+
#
|
|
102
|
+
def inline?
|
|
103
|
+
@test_mode == :inline
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
#
|
|
107
|
+
# Temporarily raise errors in the same manner
|
|
108
|
+
# inline! does it.
|
|
109
|
+
#
|
|
110
|
+
# This is used when you want to manually drain the jobs
|
|
111
|
+
# but still want to surface errors at runtime, instead of
|
|
112
|
+
# using the retry mechanic.
|
|
113
|
+
#
|
|
114
|
+
def raise_errors!(&block)
|
|
115
|
+
switch_error_mode(:raise, &block)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
#
|
|
119
|
+
# Temporarily silence errors. Job will follow the retry logic.
|
|
120
|
+
#
|
|
121
|
+
def silence_errors!(&block)
|
|
122
|
+
switch_error_mode(:silence, &block)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
#
|
|
126
|
+
# Return true if jobs should raise errors immediately
|
|
127
|
+
# without relying on retries.
|
|
128
|
+
#
|
|
129
|
+
# @return [Boolean] True if jobs are run inline.
|
|
130
|
+
#
|
|
131
|
+
def raise_errors?
|
|
132
|
+
@test_mode == :inline || @error_mode == :raise
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
#
|
|
136
|
+
# Return true if tasks should be managed in memory.
|
|
137
|
+
#
|
|
138
|
+
# @return [Boolean] True if jobs are managed in memory.
|
|
139
|
+
#
|
|
140
|
+
def in_memory?
|
|
141
|
+
!enabled?
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Add extra methods for testing purpose
|
|
146
|
+
module Worker
|
|
147
|
+
#
|
|
148
|
+
# Clear all jobs.
|
|
149
|
+
#
|
|
150
|
+
def self.clear_all
|
|
151
|
+
Backend::MemoryTask.clear
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
#
|
|
155
|
+
# Run all the jobs.
|
|
156
|
+
#
|
|
157
|
+
# @return [Array<any>] The return values of the workers perform method.
|
|
158
|
+
#
|
|
159
|
+
def self.drain_all
|
|
160
|
+
Backend::MemoryTask.drain
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Module class methods
|
|
164
|
+
module ClassMethods
|
|
165
|
+
#
|
|
166
|
+
# Return all jobs related to this worker class.
|
|
167
|
+
#
|
|
168
|
+
# @return [Array<Cloudtasker::Backend::MemoryTask>] The list of tasks
|
|
169
|
+
#
|
|
170
|
+
def jobs
|
|
171
|
+
Backend::MemoryTask.all(to_s)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
#
|
|
175
|
+
# Run all jobs related to this worker class.
|
|
176
|
+
#
|
|
177
|
+
# @return [Array<any>] The return values of the workers perform method.
|
|
178
|
+
#
|
|
179
|
+
def drain
|
|
180
|
+
Backend::MemoryTask.drain(to_s)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cloudtasker
|
|
4
|
+
module UniqueJob
|
|
5
|
+
module ConflictStrategy
|
|
6
|
+
# Base behaviour for conflict strategies
|
|
7
|
+
class BaseStrategy
|
|
8
|
+
attr_reader :job
|
|
9
|
+
|
|
10
|
+
#
|
|
11
|
+
# Build a new instance of the class.
|
|
12
|
+
#
|
|
13
|
+
# @param [Cloudtasker::UniqueJob::Job] job The UniqueJob job
|
|
14
|
+
#
|
|
15
|
+
def initialize(job)
|
|
16
|
+
@job = job
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
#
|
|
20
|
+
# Handling logic to perform when a conflict occurs while
|
|
21
|
+
# scheduling a job.
|
|
22
|
+
#
|
|
23
|
+
# We return nil to flag the job as not scheduled
|
|
24
|
+
#
|
|
25
|
+
def on_schedule
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
#
|
|
30
|
+
# Handling logic to perform when a conflict occurs while
|
|
31
|
+
# executing a job.
|
|
32
|
+
#
|
|
33
|
+
def on_execute
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cloudtasker
|
|
4
|
+
module UniqueJob
|
|
5
|
+
module ConflictStrategy
|
|
6
|
+
# This strategy raises an error on conflict, both on client and server side.
|
|
7
|
+
class Raise < BaseStrategy
|
|
8
|
+
RESCHEDULE_DELAY = 5 # seconds
|
|
9
|
+
|
|
10
|
+
# Raise a Cloudtasker::UniqueJob::LockError
|
|
11
|
+
def on_schedule
|
|
12
|
+
raise_lock_error
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Raise a Cloudtasker::UniqueJob::LockError
|
|
16
|
+
def on_execute
|
|
17
|
+
raise_lock_error
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def raise_lock_error
|
|
23
|
+
raise(UniqueJob::LockError, id: job.id, unique_id: job.unique_id)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cloudtasker
|
|
4
|
+
module UniqueJob
|
|
5
|
+
module ConflictStrategy
|
|
6
|
+
# This strategy reschedules the job on conflict. This strategy can only
|
|
7
|
+
# be used with processing locks (e.g. while_executing).
|
|
8
|
+
class Reschedule < BaseStrategy
|
|
9
|
+
RESCHEDULE_DELAY = 5 # seconds
|
|
10
|
+
|
|
11
|
+
#
|
|
12
|
+
# A conflict on schedule means that this strategy is being used
|
|
13
|
+
# with a lock scheduling strategy (e.g. until_executed) instead of a
|
|
14
|
+
# processing strategy (e.g. while_executing). In this case we let the
|
|
15
|
+
# scheduling happen as it does not make sense to reschedule in this context.
|
|
16
|
+
#
|
|
17
|
+
def on_schedule
|
|
18
|
+
yield
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
#
|
|
22
|
+
# Reschedule the job.
|
|
23
|
+
#
|
|
24
|
+
def on_execute
|
|
25
|
+
job.worker.reenqueue(RESCHEDULE_DELAY)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cloudtasker
|
|
4
|
+
module UniqueJob
|
|
5
|
+
# Wrapper class for Cloudtasker::Worker delegating to lock
|
|
6
|
+
# and conflict strategies
|
|
7
|
+
class Job
|
|
8
|
+
attr_reader :worker, :call_opts
|
|
9
|
+
|
|
10
|
+
# The default lock strategy to use. Defaults to "no lock".
|
|
11
|
+
DEFAULT_LOCK = UniqueJob::Lock::NoOp
|
|
12
|
+
|
|
13
|
+
#
|
|
14
|
+
# Build a new instance of the class.
|
|
15
|
+
#
|
|
16
|
+
# @param [Cloudtasker::Worker] worker The worker at hand
|
|
17
|
+
# @param [Hash] worker The worker options
|
|
18
|
+
#
|
|
19
|
+
def initialize(worker, opts = {})
|
|
20
|
+
@worker = worker
|
|
21
|
+
@call_opts = opts
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
#
|
|
25
|
+
# Return the worker configuration options.
|
|
26
|
+
#
|
|
27
|
+
# @return [Hash] The worker configuration options.
|
|
28
|
+
#
|
|
29
|
+
def options
|
|
30
|
+
worker.class.cloudtasker_options_hash
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
#
|
|
34
|
+
# Return the Time To Live (TTL) that should be set in Redis for
|
|
35
|
+
# the lock key. Having a TTL on lock keys ensures that jobs
|
|
36
|
+
# do not end up stuck due to a dead lock situation.
|
|
37
|
+
#
|
|
38
|
+
# The TTL is calculated using schedule time + expected
|
|
39
|
+
# max job duration.
|
|
40
|
+
#
|
|
41
|
+
# The expected max job duration is set to 10 minutes by default.
|
|
42
|
+
# This value was chosen because it's twice the default request timeout
|
|
43
|
+
# value in Cloud Run. This leaves enough room for queue lag (5 minutes)
|
|
44
|
+
# + job processing (5 minutes).
|
|
45
|
+
#
|
|
46
|
+
# Queue lag is certainly the most unpredictable factor here.
|
|
47
|
+
# Job processing time is less of a factor. Jobs running for more than 5 minutes
|
|
48
|
+
# should be split into sub-jobs to limit invocation time over HTTP. Cloudtasker batch
|
|
49
|
+
# jobs can help achieve that if you need to make one big job split into sub-jobs "atomic".
|
|
50
|
+
#
|
|
51
|
+
# The default lock key expiration of "time_at + 10 minutes" may look aggressive but it
|
|
52
|
+
# is still a better choice than potentially having real-time jobs stuck for X hours.
|
|
53
|
+
#
|
|
54
|
+
# The expected max job duration can be configured via the `lock_ttl`
|
|
55
|
+
# option on the job itself.
|
|
56
|
+
#
|
|
57
|
+
# @return [Integer] The TTL in seconds
|
|
58
|
+
#
|
|
59
|
+
def lock_ttl
|
|
60
|
+
now = Time.now.to_i
|
|
61
|
+
|
|
62
|
+
# Get scheduled at and lock duration
|
|
63
|
+
scheduled_at = [call_opts[:time_at].to_i, now].compact.max
|
|
64
|
+
lock_duration = (options[:lock_ttl] || Cloudtasker::UniqueJob.lock_ttl).to_i
|
|
65
|
+
|
|
66
|
+
# Return TTL
|
|
67
|
+
scheduled_at + lock_duration - now
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
#
|
|
71
|
+
# Return the instantiated lock.
|
|
72
|
+
#
|
|
73
|
+
# @return [Any] The instantiated lock
|
|
74
|
+
#
|
|
75
|
+
def lock_instance
|
|
76
|
+
@lock_instance ||=
|
|
77
|
+
begin
|
|
78
|
+
# Infer lock class and get instance
|
|
79
|
+
lock_name = options[:lock]
|
|
80
|
+
lock_klass = Lock.const_get(lock_name.to_s.split('_').collect(&:capitalize).join)
|
|
81
|
+
lock_klass.new(self)
|
|
82
|
+
rescue NameError
|
|
83
|
+
DEFAULT_LOCK.new(self)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
#
|
|
88
|
+
# Return the list of arguments used for job uniqueness.
|
|
89
|
+
#
|
|
90
|
+
# @return [Array<any>] The list of unique arguments
|
|
91
|
+
#
|
|
92
|
+
def unique_args
|
|
93
|
+
worker.try(:unique_args, worker.job_args) || worker.job_args
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
#
|
|
97
|
+
# Return a unique description of the job in hash format.
|
|
98
|
+
#
|
|
99
|
+
# @return [Hash] Representation of the unique job in hash format.
|
|
100
|
+
#
|
|
101
|
+
def digest_hash
|
|
102
|
+
@digest_hash ||= {
|
|
103
|
+
class: worker.class.to_s,
|
|
104
|
+
unique_args: unique_args
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
#
|
|
109
|
+
# Return the worker job ID.
|
|
110
|
+
#
|
|
111
|
+
# @return [String] The worker job ID.
|
|
112
|
+
#
|
|
113
|
+
def id
|
|
114
|
+
worker.job_id
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
#
|
|
118
|
+
# Return the ID of the unique job.
|
|
119
|
+
#
|
|
120
|
+
# @return [String] The ID of the job.
|
|
121
|
+
#
|
|
122
|
+
def unique_id
|
|
123
|
+
Digest::SHA256.hexdigest(digest_hash.to_json)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
#
|
|
127
|
+
# Return the Global ID of the unique job. The gid
|
|
128
|
+
# includes the UniqueJob namespace.
|
|
129
|
+
#
|
|
130
|
+
# @return [String] The global ID of the job
|
|
131
|
+
#
|
|
132
|
+
def unique_gid
|
|
133
|
+
[self.class.to_s.underscore, unique_id].join('/')
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
#
|
|
137
|
+
# Return the Cloudtasker redis client.
|
|
138
|
+
#
|
|
139
|
+
# @return [Cloudtasker::RedisClient] The cloudtasker redis client.
|
|
140
|
+
#
|
|
141
|
+
def redis
|
|
142
|
+
@redis ||= Cloudtasker::RedisClient.new
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
#
|
|
146
|
+
# Acquire a new unique job lock or check that the lock is
|
|
147
|
+
# currently allocated to this job.
|
|
148
|
+
#
|
|
149
|
+
# Raise a `Cloudtasker::UniqueJob::LockError` if the lock
|
|
150
|
+
# if taken by another job.
|
|
151
|
+
#
|
|
152
|
+
def lock!
|
|
153
|
+
lock_acquired = redis.set(unique_gid, id, nx: true, ex: lock_ttl)
|
|
154
|
+
lock_already_acquired = !lock_acquired && redis.get(unique_gid) == id
|
|
155
|
+
|
|
156
|
+
raise(LockError) unless lock_acquired || lock_already_acquired
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
#
|
|
160
|
+
# Delete the job lock.
|
|
161
|
+
#
|
|
162
|
+
def unlock!
|
|
163
|
+
locked_id = redis.get(unique_gid)
|
|
164
|
+
redis.del(unique_gid) if locked_id == id
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cloudtasker
|
|
4
|
+
module UniqueJob
|
|
5
|
+
module Lock
|
|
6
|
+
# Base behaviour for locks
|
|
7
|
+
class BaseLock
|
|
8
|
+
attr_reader :job
|
|
9
|
+
|
|
10
|
+
#
|
|
11
|
+
# Build a new instance of the class.
|
|
12
|
+
#
|
|
13
|
+
# @param [Cloudtasker::UniqueJob::Job] job The UniqueJob job
|
|
14
|
+
#
|
|
15
|
+
def initialize(job)
|
|
16
|
+
@job = job
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
#
|
|
20
|
+
# Return the worker configuration options.
|
|
21
|
+
#
|
|
22
|
+
# @return [Hash] The worker configuration options.
|
|
23
|
+
#
|
|
24
|
+
def options
|
|
25
|
+
job.options
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
#
|
|
29
|
+
# Return the strategy to use by default. Can be overriden in each lock.
|
|
30
|
+
#
|
|
31
|
+
# @return [Cloudtasker::UniqueJob::ConflictStrategy::BaseStrategy] The strategy to use by default.
|
|
32
|
+
#
|
|
33
|
+
def default_conflict_strategy
|
|
34
|
+
ConflictStrategy::Reject
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
#
|
|
38
|
+
# Return the conflict strategy to use on conflict
|
|
39
|
+
#
|
|
40
|
+
# @return [Cloudtasker::UniqueJob::ConflictStrategy::BaseStrategy] The instantiated strategy.
|
|
41
|
+
#
|
|
42
|
+
def conflict_instance
|
|
43
|
+
@conflict_instance ||=
|
|
44
|
+
begin
|
|
45
|
+
# Infer lock class and get instance
|
|
46
|
+
strategy_name = options[:on_conflict]
|
|
47
|
+
strategy_klass = ConflictStrategy.const_get(strategy_name.to_s.split('_').collect(&:capitalize).join)
|
|
48
|
+
strategy_klass.new(job)
|
|
49
|
+
rescue NameError
|
|
50
|
+
default_conflict_strategy.new(job)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
#
|
|
55
|
+
# Lock logic invoked when a job is scheduled (client middleware).
|
|
56
|
+
#
|
|
57
|
+
def schedule
|
|
58
|
+
yield
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
#
|
|
62
|
+
# Lock logic invoked when a job is executed (server middleware).
|
|
63
|
+
#
|
|
64
|
+
def execute
|
|
65
|
+
yield
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cloudtasker
|
|
4
|
+
module UniqueJob
|
|
5
|
+
module Lock
|
|
6
|
+
# Conflict if any other job with the same args is scheduled or moved to execution
|
|
7
|
+
# while the first job is pending or executing. Unlocks only on successful completion
|
|
8
|
+
# or when a DeadWorkerError is raised.
|
|
9
|
+
class UntilCompleted < BaseLock
|
|
10
|
+
#
|
|
11
|
+
# Acquire a lock for the job and trigger a conflict
|
|
12
|
+
# if the lock could not be acquired.
|
|
13
|
+
#
|
|
14
|
+
def schedule(&block)
|
|
15
|
+
job.lock!
|
|
16
|
+
yield
|
|
17
|
+
rescue LockError
|
|
18
|
+
conflict_instance.on_schedule(&block)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
#
|
|
22
|
+
# Acquire a lock for the job and trigger a conflict
|
|
23
|
+
# if the lock could not be acquired.
|
|
24
|
+
#
|
|
25
|
+
def execute(&block)
|
|
26
|
+
job.lock!
|
|
27
|
+
yield
|
|
28
|
+
# Unlock on successful completion
|
|
29
|
+
job.unlock!
|
|
30
|
+
rescue LockError
|
|
31
|
+
conflict_instance.on_execute(&block)
|
|
32
|
+
rescue Cloudtasker::DeadWorkerError
|
|
33
|
+
# Unlock when DeadWorkerError is raised
|
|
34
|
+
job.unlock!
|
|
35
|
+
raise
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cloudtasker
|
|
4
|
+
module UniqueJob
|
|
5
|
+
module Lock
|
|
6
|
+
# Conflict if any other job with the same args is scheduled or moved to execution
|
|
7
|
+
# while the first job is pending or executing.
|
|
8
|
+
class UntilExecuted < BaseLock
|
|
9
|
+
#
|
|
10
|
+
# Acquire a lock for the job and trigger a conflict
|
|
11
|
+
# if the lock could not be acquired.
|
|
12
|
+
#
|
|
13
|
+
def schedule(&block)
|
|
14
|
+
job.lock!
|
|
15
|
+
yield
|
|
16
|
+
rescue LockError
|
|
17
|
+
conflict_instance.on_schedule(&block)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
#
|
|
21
|
+
# Acquire a lock for the job and trigger a conflict
|
|
22
|
+
# if the lock could not be acquired.
|
|
23
|
+
#
|
|
24
|
+
def execute(&block)
|
|
25
|
+
job.lock!
|
|
26
|
+
yield
|
|
27
|
+
rescue LockError
|
|
28
|
+
conflict_instance.on_execute(&block)
|
|
29
|
+
ensure
|
|
30
|
+
# Unlock the job on any error to avoid deadlocks.
|
|
31
|
+
job.unlock!
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cloudtasker
|
|
4
|
+
module UniqueJob
|
|
5
|
+
module Lock
|
|
6
|
+
# Conflict if any other job with the same args is scheduled
|
|
7
|
+
# while the first job is pending.
|
|
8
|
+
class UntilExecuting < BaseLock
|
|
9
|
+
#
|
|
10
|
+
# Acquire a lock for the job and trigger a conflict
|
|
11
|
+
# if the lock could not be acquired.
|
|
12
|
+
#
|
|
13
|
+
def schedule(&block)
|
|
14
|
+
job.lock!
|
|
15
|
+
yield
|
|
16
|
+
rescue LockError
|
|
17
|
+
conflict_instance.on_schedule(&block)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
#
|
|
21
|
+
# Release the lock and perform the job.
|
|
22
|
+
#
|
|
23
|
+
def execute
|
|
24
|
+
job.unlock!
|
|
25
|
+
yield
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cloudtasker
|
|
4
|
+
module UniqueJob
|
|
5
|
+
module Lock
|
|
6
|
+
# Conflict if any other job with the same args is moved to execution
|
|
7
|
+
# while the first job is executing.
|
|
8
|
+
class WhileExecuting < BaseLock
|
|
9
|
+
#
|
|
10
|
+
# Acquire a lock for the job and trigger a conflict
|
|
11
|
+
# if the lock could not be acquired.
|
|
12
|
+
#
|
|
13
|
+
def execute(&block)
|
|
14
|
+
job.lock!
|
|
15
|
+
yield
|
|
16
|
+
rescue LockError
|
|
17
|
+
conflict_instance.on_execute(&block)
|
|
18
|
+
ensure
|
|
19
|
+
# Unlock the job on any error to avoid deadlocks.
|
|
20
|
+
job.unlock!
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cloudtasker
|
|
4
|
+
module UniqueJob
|
|
5
|
+
module Middleware
|
|
6
|
+
# TODO: kwargs to job otherwise it won't get the time_at
|
|
7
|
+
# Client middleware, invoked when jobs are scheduled
|
|
8
|
+
class Client
|
|
9
|
+
def call(worker, _opts = {}, &block)
|
|
10
|
+
Job.new(worker).lock_instance.schedule(&block)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|