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,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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudtasker
4
+ module UniqueJob
5
+ module ConflictStrategy
6
+ # This strategy rejects the job on conflict. This is equivalent to "do nothing".
7
+ class Reject < BaseStrategy
8
+ end
9
+ end
10
+ end
11
+ 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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudtasker
4
+ module UniqueJob
5
+ module Lock
6
+ # Equivalent to no lock
7
+ class NoOp < BaseLock
8
+ end
9
+ end
10
+ end
11
+ 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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudtasker
4
+ module UniqueJob
5
+ class LockError < StandardError
6
+ end
7
+ end
8
+ 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