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,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cloudtasker
|
|
4
|
+
# Manage meta information on workers. This meta stored is intended
|
|
5
|
+
# to be used by middlewares needing to store extra information on the
|
|
6
|
+
# job.
|
|
7
|
+
# The objective of this class is to provide a shared store to middleware
|
|
8
|
+
# while controlling access to its keys by preveenting access the hash directly
|
|
9
|
+
# (e.g. avoid wild merge or replace operations).
|
|
10
|
+
class MetaStore
|
|
11
|
+
#
|
|
12
|
+
# Build a new instance of the class.
|
|
13
|
+
#
|
|
14
|
+
# @param [<Type>] hash The worker meta hash
|
|
15
|
+
#
|
|
16
|
+
def initialize(hash = {})
|
|
17
|
+
@meta = JSON.parse((hash || {}).to_json, symbolize_names: true)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
#
|
|
21
|
+
# Retrieve meta entry.
|
|
22
|
+
#
|
|
23
|
+
# @param [String, Symbol] key The key of the meta entry.
|
|
24
|
+
#
|
|
25
|
+
# @return [Any] The value of the meta entry.
|
|
26
|
+
#
|
|
27
|
+
def get(key)
|
|
28
|
+
@meta[key.to_sym] if key
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
#
|
|
32
|
+
# Set meta entry
|
|
33
|
+
#
|
|
34
|
+
# @param [String, Symbol] key The key of the meta entry.
|
|
35
|
+
# @param [Any] val The value of the meta entry.
|
|
36
|
+
#
|
|
37
|
+
# @return [Any] The value set
|
|
38
|
+
#
|
|
39
|
+
def set(key, val)
|
|
40
|
+
@meta[key.to_sym] = val if key
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
#
|
|
44
|
+
# Remove a meta information.
|
|
45
|
+
#
|
|
46
|
+
# @param [String, Symbol] key The key of the entry to delete.
|
|
47
|
+
#
|
|
48
|
+
# @return [Any] The value of the deleted key
|
|
49
|
+
#
|
|
50
|
+
def del(key)
|
|
51
|
+
@meta.delete(key.to_sym) if key
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
#
|
|
55
|
+
# Return the meta store as Hash.
|
|
56
|
+
#
|
|
57
|
+
# @return [Hash] The meta store as Hash.
|
|
58
|
+
#
|
|
59
|
+
def to_h
|
|
60
|
+
# Deep dup
|
|
61
|
+
JSON.parse(@meta.to_json, symbolize_names: true)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
#
|
|
65
|
+
# Return the meta store as json.
|
|
66
|
+
#
|
|
67
|
+
# @param [Array<any>] *arg The to_json args.
|
|
68
|
+
#
|
|
69
|
+
# @return [String] The meta store as json.
|
|
70
|
+
#
|
|
71
|
+
def to_json(*arg)
|
|
72
|
+
@meta.to_json(*arg)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
#
|
|
76
|
+
# Equality operator.
|
|
77
|
+
#
|
|
78
|
+
# @param [Any] other The object being compared.
|
|
79
|
+
#
|
|
80
|
+
# @return [Boolean] True if the object is equal.
|
|
81
|
+
#
|
|
82
|
+
def ==(other)
|
|
83
|
+
to_json == other.try(:to_json)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cloudtasker
|
|
4
|
+
module Middleware
|
|
5
|
+
# The class below was originally taken from Sidekiq.
|
|
6
|
+
# See: https://github.com/mperham/sidekiq/blob/master/lib/sidekiq/middleware/chain.rb
|
|
7
|
+
#
|
|
8
|
+
# Middleware are callables configured to run before/after a message is processed.
|
|
9
|
+
# Middlewares can be configured to run on the client side (when jobs are pushed
|
|
10
|
+
# to Cloud Tasks) as well as on the server side (when jobs are processed by
|
|
11
|
+
# your application)
|
|
12
|
+
#
|
|
13
|
+
# To add middleware for the client:
|
|
14
|
+
#
|
|
15
|
+
# Cloudtasker.configure do |config|
|
|
16
|
+
# config.client_middleware do |chain|
|
|
17
|
+
# chain.add MyClientHook
|
|
18
|
+
# end
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# To modify middleware for the server, just call
|
|
22
|
+
# with another block:
|
|
23
|
+
#
|
|
24
|
+
# Cloudtasker.configure do |config|
|
|
25
|
+
# config.server_middleware do |chain|
|
|
26
|
+
# chain.add MyServerHook
|
|
27
|
+
# chain.remove ActiveRecord
|
|
28
|
+
# end
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
# To insert immediately preceding another entry:
|
|
32
|
+
#
|
|
33
|
+
# Cloudtasker.configure do |config|
|
|
34
|
+
# config.client_middleware do |chain|
|
|
35
|
+
# chain.insert_before ActiveRecord, MyClientHook
|
|
36
|
+
# end
|
|
37
|
+
# end
|
|
38
|
+
#
|
|
39
|
+
# To insert immediately after another entry:
|
|
40
|
+
#
|
|
41
|
+
# Cloudtasker.configure do |config|
|
|
42
|
+
# config.client_middleware do |chain|
|
|
43
|
+
# chain.insert_after ActiveRecord, MyClientHook
|
|
44
|
+
# end
|
|
45
|
+
# end
|
|
46
|
+
#
|
|
47
|
+
# This is an example of a minimal server middleware:
|
|
48
|
+
#
|
|
49
|
+
# class MyServerHook
|
|
50
|
+
# def call(worker_instance, msg, queue)
|
|
51
|
+
# puts "Before work"
|
|
52
|
+
# yield
|
|
53
|
+
# puts "After work"
|
|
54
|
+
# end
|
|
55
|
+
# end
|
|
56
|
+
#
|
|
57
|
+
# This is an example of a minimal client middleware, note
|
|
58
|
+
# the method must return the result or the job will not push
|
|
59
|
+
# to Redis:
|
|
60
|
+
#
|
|
61
|
+
# class MyClientHook
|
|
62
|
+
# def call(worker_class, msg, queue, redis_pool)
|
|
63
|
+
# puts "Before push"
|
|
64
|
+
# result = yield
|
|
65
|
+
# puts "After push"
|
|
66
|
+
# result
|
|
67
|
+
# end
|
|
68
|
+
# end
|
|
69
|
+
#
|
|
70
|
+
class Chain
|
|
71
|
+
include Enumerable
|
|
72
|
+
|
|
73
|
+
#
|
|
74
|
+
# Build a new middleware chain.
|
|
75
|
+
#
|
|
76
|
+
def initialize
|
|
77
|
+
@entries = nil
|
|
78
|
+
yield self if block_given?
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
#
|
|
82
|
+
# Iterate over the list middlewares and execute the block on each item.
|
|
83
|
+
#
|
|
84
|
+
# @param [Proc] &block The block to execute on each item.
|
|
85
|
+
#
|
|
86
|
+
def each(&block)
|
|
87
|
+
entries.each(&block)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
#
|
|
91
|
+
# Return the list of middlewares.
|
|
92
|
+
#
|
|
93
|
+
# @return [Array<Cloudtasker::Middleware::Chain::Entry>] The list of middlewares
|
|
94
|
+
#
|
|
95
|
+
def entries
|
|
96
|
+
@entries ||= []
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
#
|
|
100
|
+
# Remove a middleware from the list.
|
|
101
|
+
#
|
|
102
|
+
# @param [Class] klass The middleware class to remove.
|
|
103
|
+
#
|
|
104
|
+
def remove(klass)
|
|
105
|
+
entries.delete_if { |entry| entry.klass == klass }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
#
|
|
109
|
+
# Add a middleware at the end of the list.
|
|
110
|
+
#
|
|
111
|
+
# @param [Class] klass The middleware class to add.
|
|
112
|
+
# @param [Arry<any>] *args The list of arguments to the middleware.
|
|
113
|
+
#
|
|
114
|
+
# @return [Array<Cloudtasker::Middleware::Chain::Entry>] The updated list of middlewares
|
|
115
|
+
#
|
|
116
|
+
def add(klass, *args)
|
|
117
|
+
remove(klass) if exists?(klass)
|
|
118
|
+
entries << Entry.new(klass, *args)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
#
|
|
122
|
+
# Add a middleware at the beginning of the list.
|
|
123
|
+
#
|
|
124
|
+
# @param [Class] klass The middleware class to add.
|
|
125
|
+
# @param [Arry<any>] *args The list of arguments to the middleware.
|
|
126
|
+
#
|
|
127
|
+
# @return [Array<Cloudtasker::Middleware::Chain::Entry>] The updated list of middlewares
|
|
128
|
+
#
|
|
129
|
+
def prepend(klass, *args)
|
|
130
|
+
remove(klass) if exists?(klass)
|
|
131
|
+
entries.insert(0, Entry.new(klass, *args))
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
#
|
|
135
|
+
# Add a middleware before another middleware.
|
|
136
|
+
#
|
|
137
|
+
# @param [Class] oldklass The middleware class before which the new middleware should be inserted.
|
|
138
|
+
# @param [Class] newklass The middleware class to insert.
|
|
139
|
+
# @param [Arry<any>] *args The list of arguments for the inserted middleware.
|
|
140
|
+
#
|
|
141
|
+
# @return [Array<Cloudtasker::Middleware::Chain::Entry>] The updated list of middlewares
|
|
142
|
+
#
|
|
143
|
+
def insert_before(oldklass, newklass, *args)
|
|
144
|
+
i = entries.index { |entry| entry.klass == newklass }
|
|
145
|
+
new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
|
|
146
|
+
i = entries.index { |entry| entry.klass == oldklass } || 0
|
|
147
|
+
entries.insert(i, new_entry)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
#
|
|
151
|
+
# Add a middleware after another middleware.
|
|
152
|
+
#
|
|
153
|
+
# @param [Class] oldklass The middleware class after which the new middleware should be inserted.
|
|
154
|
+
# @param [Class] newklass The middleware class to insert.
|
|
155
|
+
# @param [Arry<any>] *args The list of arguments for the inserted middleware.
|
|
156
|
+
#
|
|
157
|
+
# @return [Array<Cloudtasker::Middleware::Chain::Entry>] The updated list of middlewares
|
|
158
|
+
#
|
|
159
|
+
def insert_after(oldklass, newklass, *args)
|
|
160
|
+
i = entries.index { |entry| entry.klass == newklass }
|
|
161
|
+
new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
|
|
162
|
+
i = entries.index { |entry| entry.klass == oldklass } || (entries.count - 1)
|
|
163
|
+
entries.insert(i + 1, new_entry)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
#
|
|
167
|
+
# Checks if middleware has been added to the list.
|
|
168
|
+
#
|
|
169
|
+
# @param [Class] klass The middleware class to check.
|
|
170
|
+
#
|
|
171
|
+
# @return [Boolean] Return true if the middleware is in the list.
|
|
172
|
+
#
|
|
173
|
+
def exists?(klass)
|
|
174
|
+
any? { |entry| entry.klass == klass }
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
#
|
|
178
|
+
# Checks if the middlware list is empty
|
|
179
|
+
#
|
|
180
|
+
# @return [Boolean] Return true if the middleware list is empty.
|
|
181
|
+
#
|
|
182
|
+
def empty?
|
|
183
|
+
@entries.nil? || @entries.empty?
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
#
|
|
187
|
+
# Return a list of instantiated middlewares. Each middleware gets
|
|
188
|
+
# initialize with the args originally passed to `add`, `insert_before` etc.
|
|
189
|
+
#
|
|
190
|
+
# @return [Array<any>] The list of instantiated middlewares.
|
|
191
|
+
#
|
|
192
|
+
def retrieve
|
|
193
|
+
map(&:make_new)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
#
|
|
197
|
+
# Empty the list of middlewares.
|
|
198
|
+
#
|
|
199
|
+
# @return [Array<Cloudtasker::Middleware::Chain::Entry>] The updated list of middlewares
|
|
200
|
+
#
|
|
201
|
+
def clear
|
|
202
|
+
entries.clear
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
#
|
|
206
|
+
# Invoke the chain of middlewares.
|
|
207
|
+
#
|
|
208
|
+
# @param [Array<any>] *args The args to pass to each middleware.
|
|
209
|
+
#
|
|
210
|
+
def invoke(*args)
|
|
211
|
+
return yield if empty?
|
|
212
|
+
|
|
213
|
+
chain = retrieve.dup
|
|
214
|
+
traverse_chain = lambda do
|
|
215
|
+
if chain.empty?
|
|
216
|
+
yield
|
|
217
|
+
else
|
|
218
|
+
chain.shift.call(*args, &traverse_chain)
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
traverse_chain.call
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Middleware list item.
|
|
226
|
+
class Entry
|
|
227
|
+
attr_reader :klass, :args
|
|
228
|
+
|
|
229
|
+
#
|
|
230
|
+
# Build a new entry.
|
|
231
|
+
#
|
|
232
|
+
# @param [Class] klass The middleware class.
|
|
233
|
+
# @param [Array<any>] *args The list of arguments for the middleware.
|
|
234
|
+
#
|
|
235
|
+
def initialize(klass, *args)
|
|
236
|
+
@klass = klass
|
|
237
|
+
@args = args
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
#
|
|
241
|
+
# Return an instantiated middleware.
|
|
242
|
+
#
|
|
243
|
+
# @return [Any] The instantiated middleware.
|
|
244
|
+
#
|
|
245
|
+
def make_new
|
|
246
|
+
@klass.new(*@args)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'redis'
|
|
4
|
+
require 'connection_pool'
|
|
5
|
+
|
|
6
|
+
module Cloudtasker
|
|
7
|
+
# A wrapper with helper methods for redis
|
|
8
|
+
class RedisClient
|
|
9
|
+
# Suffix added to cache keys when locking them
|
|
10
|
+
LOCK_KEY_PREFIX = 'cloudtasker/lock'
|
|
11
|
+
LOCK_DURATION = 2 # seconds
|
|
12
|
+
LOCK_WAIT_DURATION = 0.03 # seconds
|
|
13
|
+
|
|
14
|
+
# Default pool size used for Redis
|
|
15
|
+
DEFAULT_POOL_SIZE = ENV.fetch('RAILS_MAX_THREADS', 25)
|
|
16
|
+
DEFAULT_POOL_TIMEOUT = 5
|
|
17
|
+
|
|
18
|
+
def self.client
|
|
19
|
+
@client ||= begin
|
|
20
|
+
pool_size = Cloudtasker.config.redis&.dig(:pool_size) || DEFAULT_POOL_SIZE
|
|
21
|
+
pool_timeout = Cloudtasker.config.redis&.dig(:pool_timeout) || DEFAULT_POOL_TIMEOUT
|
|
22
|
+
ConnectionPool.new(size: pool_size, timeout: pool_timeout) do
|
|
23
|
+
Redis.new(Cloudtasker.config.redis || {})
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
#
|
|
29
|
+
# Return the underlying redis client.
|
|
30
|
+
#
|
|
31
|
+
# @return [Redis] The redis client.
|
|
32
|
+
#
|
|
33
|
+
def client
|
|
34
|
+
@client ||= self.class.client
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
#
|
|
38
|
+
# Get a cache entry and parse it as JSON.
|
|
39
|
+
#
|
|
40
|
+
# @param [String, Symbol] key The cache key to fetch.
|
|
41
|
+
#
|
|
42
|
+
# @return [Hash, Array] The content of the cache key, parsed as JSON.
|
|
43
|
+
#
|
|
44
|
+
def fetch(key)
|
|
45
|
+
return nil unless (val = get(key.to_s))
|
|
46
|
+
|
|
47
|
+
JSON.parse(val, symbolize_names: true)
|
|
48
|
+
rescue JSON::ParserError
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
#
|
|
53
|
+
# Write a cache entry as JSON.
|
|
54
|
+
#
|
|
55
|
+
# @param [String, Symbol] key The cache key to write.
|
|
56
|
+
# @param [Hash, Array] content The content to write.
|
|
57
|
+
#
|
|
58
|
+
# @return [String] Redis response code.
|
|
59
|
+
#
|
|
60
|
+
def write(key, content)
|
|
61
|
+
set(key.to_s, content.to_json)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
#
|
|
65
|
+
# Acquire a lock on a cache entry.
|
|
66
|
+
#
|
|
67
|
+
# Locks are enforced to be short-lived (2s).
|
|
68
|
+
# The yielded block should limit its logic to short operations (e.g. redis get/set).
|
|
69
|
+
#
|
|
70
|
+
# @example
|
|
71
|
+
# redis = RedisClient.new
|
|
72
|
+
# redis.with_lock('foo')
|
|
73
|
+
# content = redis.fetch('foo')
|
|
74
|
+
# redis.set(content.merge(bar: 'bar).to_json)
|
|
75
|
+
# end
|
|
76
|
+
#
|
|
77
|
+
# @param [String] cache_key The cache key to access.
|
|
78
|
+
# @param [Integer] max_wait The number of seconds after which the lock will be cleared anyway.
|
|
79
|
+
#
|
|
80
|
+
def with_lock(cache_key, max_wait: nil)
|
|
81
|
+
return nil unless cache_key
|
|
82
|
+
|
|
83
|
+
# Set max wait
|
|
84
|
+
max_wait = (max_wait || LOCK_DURATION).to_i
|
|
85
|
+
|
|
86
|
+
# Wait to acquire lock
|
|
87
|
+
lock_key = [LOCK_KEY_PREFIX, cache_key].join('/')
|
|
88
|
+
client.with do |conn|
|
|
89
|
+
sleep(LOCK_WAIT_DURATION) until conn.set(lock_key, true, nx: true, ex: max_wait)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# yield content
|
|
93
|
+
yield
|
|
94
|
+
ensure
|
|
95
|
+
del(lock_key)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
#
|
|
99
|
+
# Clear all redis keys
|
|
100
|
+
#
|
|
101
|
+
# @return [Integer] The number of keys deleted
|
|
102
|
+
#
|
|
103
|
+
def clear
|
|
104
|
+
all_keys = keys
|
|
105
|
+
return 0 if all_keys.empty?
|
|
106
|
+
|
|
107
|
+
# Delete all keys
|
|
108
|
+
del(*all_keys)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
#
|
|
112
|
+
# Return all keys matching the provided patterns.
|
|
113
|
+
#
|
|
114
|
+
# @param [String] pattern A redis compatible pattern.
|
|
115
|
+
#
|
|
116
|
+
# @return [Array<String>] The list of matching keys
|
|
117
|
+
#
|
|
118
|
+
def search(pattern)
|
|
119
|
+
# Initialize loop variables
|
|
120
|
+
cursor = nil
|
|
121
|
+
list = []
|
|
122
|
+
|
|
123
|
+
# Scan and capture matching keys
|
|
124
|
+
client.with do |conn|
|
|
125
|
+
while cursor != 0
|
|
126
|
+
scan = conn.scan(cursor || 0, match: pattern)
|
|
127
|
+
list += scan[1]
|
|
128
|
+
cursor = scan[0].to_i
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
list
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
#
|
|
136
|
+
# Delegate all methods to the redis client.
|
|
137
|
+
# Ruby 3 delegation method style.
|
|
138
|
+
#
|
|
139
|
+
# @param [String, Symbol] name The method to delegate.
|
|
140
|
+
# @param [Array<any>] *args The list of method positional arguments.
|
|
141
|
+
# @param [Hash<any>] *kwargs The list of method keyword arguments.
|
|
142
|
+
# @param [Proc] &block Block passed to the method.
|
|
143
|
+
#
|
|
144
|
+
# @return [Any] The method return value
|
|
145
|
+
#
|
|
146
|
+
def method_missing(name, ...)
|
|
147
|
+
if Redis.method_defined?(name)
|
|
148
|
+
client.with { |c| c.send(name, ...) }
|
|
149
|
+
else
|
|
150
|
+
super
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
#
|
|
155
|
+
# Check if the class respond to a certain method.
|
|
156
|
+
#
|
|
157
|
+
# @param [String, Symbol] name The name of the method.
|
|
158
|
+
# @param [Boolean] include_private Whether to check private methods or not. Default to false.
|
|
159
|
+
#
|
|
160
|
+
# @return [Boolean] Return true if the class respond to this method.
|
|
161
|
+
#
|
|
162
|
+
def respond_to_missing?(name, include_private = false)
|
|
163
|
+
Redis.method_defined?(name) || super
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cloudtasker
|
|
4
|
+
module Storable
|
|
5
|
+
# Add ability to store and pull workers in Redis under a specific namespace
|
|
6
|
+
module Worker
|
|
7
|
+
# Add class method to including class
|
|
8
|
+
def self.included(base)
|
|
9
|
+
base.extend(ClassMethods)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Module class methods
|
|
13
|
+
module ClassMethods
|
|
14
|
+
#
|
|
15
|
+
# Return the namespaced store key used to store jobs that
|
|
16
|
+
# have been parked and should be manually popped later.
|
|
17
|
+
#
|
|
18
|
+
# @param [String] namespace The user-provided store namespace
|
|
19
|
+
#
|
|
20
|
+
# @return [String] The full store cache key
|
|
21
|
+
#
|
|
22
|
+
def store_cache_key(namespace)
|
|
23
|
+
cache_key([Config::WORKER_STORE_PREFIX, namespace])
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
#
|
|
27
|
+
# Push the worker to a namespaced store.
|
|
28
|
+
#
|
|
29
|
+
# @param [String] namespace The store namespace
|
|
30
|
+
# @param [Array<any>] *args List of worker arguments
|
|
31
|
+
#
|
|
32
|
+
# @return [String] The number of elements added to the store
|
|
33
|
+
#
|
|
34
|
+
def push_to_store(namespace, *args)
|
|
35
|
+
redis.rpush(store_cache_key(namespace), [args.to_json])
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
#
|
|
39
|
+
# Push many workers to a namespaced store at once.
|
|
40
|
+
#
|
|
41
|
+
# @param [String] namespace The store namespace
|
|
42
|
+
# @param [Array<Array<any>>] args_list A list of arguments for each worker
|
|
43
|
+
#
|
|
44
|
+
# @return [String] The number of elements added to the store
|
|
45
|
+
#
|
|
46
|
+
def push_many_to_store(namespace, args_list)
|
|
47
|
+
redis.rpush(store_cache_key(namespace), args_list.map(&:to_json))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
#
|
|
51
|
+
# Pull the jobs from the namespaced store and enqueue them.
|
|
52
|
+
#
|
|
53
|
+
# @param [String] namespace The store namespace.
|
|
54
|
+
# @param [Integer] page_size The number of items to pull on each page. Defaults to 1000.
|
|
55
|
+
#
|
|
56
|
+
def pull_all_from_store(namespace, page_size: 1000)
|
|
57
|
+
items = nil
|
|
58
|
+
|
|
59
|
+
while items.nil? || items.present?
|
|
60
|
+
# Pull items
|
|
61
|
+
items = redis.lpop(store_cache_key(namespace), page_size).to_a
|
|
62
|
+
|
|
63
|
+
# For each item, execute block or enqueue it
|
|
64
|
+
items.each do |args_json|
|
|
65
|
+
worker_args = JSON.parse(args_json)
|
|
66
|
+
|
|
67
|
+
if block_given?
|
|
68
|
+
yield(worker_args)
|
|
69
|
+
else
|
|
70
|
+
perform_async(*worker_args)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|