cloudtasker 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/.gitignore +12 -0
- data/.rspec +3 -0
- data/.rubocop.yml +27 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +247 -0
- data/LICENSE.txt +21 -0
- data/README.md +43 -0
- data/Rakefile +8 -0
- data/app/controllers/cloudtasker/application_controller.rb +6 -0
- data/app/controllers/cloudtasker/worker_controller.rb +38 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/cloudtasker.gemspec +48 -0
- data/config/routes.rb +5 -0
- data/lib/cloudtasker.rb +31 -0
- data/lib/cloudtasker/authentication_error.rb +6 -0
- data/lib/cloudtasker/authenticator.rb +55 -0
- data/lib/cloudtasker/batch.rb +5 -0
- data/lib/cloudtasker/batch/batch_progress.rb +97 -0
- data/lib/cloudtasker/batch/config.rb +11 -0
- data/lib/cloudtasker/batch/extension/worker.rb +13 -0
- data/lib/cloudtasker/batch/job.rb +320 -0
- data/lib/cloudtasker/batch/middleware.rb +24 -0
- data/lib/cloudtasker/batch/middleware/server.rb +14 -0
- data/lib/cloudtasker/config.rb +122 -0
- data/lib/cloudtasker/cron.rb +5 -0
- data/lib/cloudtasker/cron/config.rb +11 -0
- data/lib/cloudtasker/cron/job.rb +207 -0
- data/lib/cloudtasker/cron/middleware.rb +21 -0
- data/lib/cloudtasker/cron/middleware/server.rb +14 -0
- data/lib/cloudtasker/cron/schedule.rb +227 -0
- data/lib/cloudtasker/engine.rb +20 -0
- data/lib/cloudtasker/invalid_worker_error.rb +6 -0
- data/lib/cloudtasker/meta_store.rb +86 -0
- data/lib/cloudtasker/middleware/chain.rb +250 -0
- data/lib/cloudtasker/redis_client.rb +115 -0
- data/lib/cloudtasker/task.rb +175 -0
- data/lib/cloudtasker/unique_job.rb +5 -0
- data/lib/cloudtasker/unique_job/config.rb +10 -0
- data/lib/cloudtasker/unique_job/conflict_strategy/base_strategy.rb +37 -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 +136 -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_executed.rb +34 -0
- data/lib/cloudtasker/unique_job/lock/until_executing.rb +30 -0
- data/lib/cloudtasker/unique_job/lock/while_executing.rb +23 -0
- data/lib/cloudtasker/unique_job/lock_error.rb +8 -0
- data/lib/cloudtasker/unique_job/middleware.rb +36 -0
- data/lib/cloudtasker/unique_job/middleware/client.rb +14 -0
- data/lib/cloudtasker/unique_job/middleware/server.rb +14 -0
- data/lib/cloudtasker/version.rb +5 -0
- data/lib/cloudtasker/worker.rb +211 -0
- metadata +286 -0
@@ -0,0 +1,175 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'google/cloud/tasks'
|
4
|
+
|
5
|
+
module Cloudtasker
|
6
|
+
# Build, serialize and schedule tasks on GCP Cloud Task
|
7
|
+
class Task
|
8
|
+
attr_reader :worker, :job_args
|
9
|
+
|
10
|
+
# Alrogith used to sign the verification token
|
11
|
+
JWT_ALG = 'HS256'
|
12
|
+
|
13
|
+
# TODO: Move to a dedicated CloudTask class
|
14
|
+
#
|
15
|
+
# Find a Cloud task
|
16
|
+
#
|
17
|
+
# @param [String] id The ID of the task.
|
18
|
+
#
|
19
|
+
# @return [Google::Cloud::Tasks::V2beta3::Task] The cloud task.
|
20
|
+
#
|
21
|
+
def self.find(id)
|
22
|
+
client.get_task(id)
|
23
|
+
rescue Google::Gax::RetryError
|
24
|
+
nil
|
25
|
+
end
|
26
|
+
|
27
|
+
# TODO: Move to a dedicated CloudTask class
|
28
|
+
#
|
29
|
+
# Delete a Cloud task
|
30
|
+
#
|
31
|
+
# @param [String] id The ID of the task.
|
32
|
+
#
|
33
|
+
def self.delete(id)
|
34
|
+
client.delete_task(id)
|
35
|
+
rescue Google::Gax::RetryError
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
|
39
|
+
#
|
40
|
+
# Execute a task worker from a task payload
|
41
|
+
#
|
42
|
+
# @param [Hash] payload The Cloud Task payload.
|
43
|
+
#
|
44
|
+
# @return [Any] The return value of the worker perform method.
|
45
|
+
#
|
46
|
+
def self.execute_from_payload!(payload)
|
47
|
+
worker = Cloudtasker::Worker.from_hash(payload) || raise(InvalidWorkerError)
|
48
|
+
worker.execute
|
49
|
+
end
|
50
|
+
|
51
|
+
#
|
52
|
+
# Return the Google Cloud Task client.
|
53
|
+
#
|
54
|
+
# @return [Google::Cloud::Tasks] The Google Cloud Task client.
|
55
|
+
#
|
56
|
+
def self.client
|
57
|
+
@client ||= ::Google::Cloud::Tasks.new(version: :v2beta3)
|
58
|
+
end
|
59
|
+
|
60
|
+
#
|
61
|
+
# Prepare a new cloud task.
|
62
|
+
#
|
63
|
+
# @param [Cloudtasker::Worker] worker The worker instance.
|
64
|
+
#
|
65
|
+
def initialize(worker)
|
66
|
+
@worker = worker
|
67
|
+
end
|
68
|
+
|
69
|
+
#
|
70
|
+
# Return the Google Cloud Task client.
|
71
|
+
#
|
72
|
+
# @return [Google::Cloud::Tasks] The Google Cloud Task client.
|
73
|
+
#
|
74
|
+
def client
|
75
|
+
self.class.client
|
76
|
+
end
|
77
|
+
|
78
|
+
#
|
79
|
+
# Return the cloudtasker configuration. See Cloudtasker#configure.
|
80
|
+
#
|
81
|
+
# @return [Cloudtasker::Config] The library configuration.
|
82
|
+
#
|
83
|
+
def config
|
84
|
+
Cloudtasker.config
|
85
|
+
end
|
86
|
+
|
87
|
+
#
|
88
|
+
# Return the fully qualified path for the Cloud Task queue.
|
89
|
+
#
|
90
|
+
# @return [String] The queue path.
|
91
|
+
#
|
92
|
+
def queue_path
|
93
|
+
client.queue_path(
|
94
|
+
config.gcp_project_id,
|
95
|
+
config.gcp_location_id,
|
96
|
+
config.gcp_queue_id
|
97
|
+
)
|
98
|
+
end
|
99
|
+
|
100
|
+
#
|
101
|
+
# Return the full task configuration sent to Cloud Task
|
102
|
+
#
|
103
|
+
# @return [Hash] The task body
|
104
|
+
#
|
105
|
+
def task_payload
|
106
|
+
{
|
107
|
+
http_request: {
|
108
|
+
http_method: 'POST',
|
109
|
+
url: config.processor_url,
|
110
|
+
headers: {
|
111
|
+
'Content-Type' => 'application/json',
|
112
|
+
'Authorization' => "Bearer #{Authenticator.verification_token}"
|
113
|
+
},
|
114
|
+
body: worker_payload.to_json
|
115
|
+
}
|
116
|
+
}
|
117
|
+
end
|
118
|
+
|
119
|
+
#
|
120
|
+
# Return the task payload that Google Task will eventually
|
121
|
+
# send to the job processor.
|
122
|
+
#
|
123
|
+
# The payload includes the worker name and the arguments to
|
124
|
+
# pass to the worker.
|
125
|
+
#
|
126
|
+
# The worker arguments should use primitive types as much
|
127
|
+
# as possible as all arguments will be serialized to JSON.
|
128
|
+
#
|
129
|
+
# @return [Hash] The job payload
|
130
|
+
#
|
131
|
+
def worker_payload
|
132
|
+
@worker_payload ||= {
|
133
|
+
worker: worker.class.to_s,
|
134
|
+
job_id: worker.job_id,
|
135
|
+
job_args: worker.job_args,
|
136
|
+
job_meta: worker.job_meta
|
137
|
+
}
|
138
|
+
end
|
139
|
+
|
140
|
+
#
|
141
|
+
# Return a protobuf timestamp specifying how to wait
|
142
|
+
# before running a task.
|
143
|
+
#
|
144
|
+
# @param [Integer, nil] interval The time to wait.
|
145
|
+
#
|
146
|
+
# @return [Google::Protobuf::Timestamp, nil] The protobuff timestamp
|
147
|
+
#
|
148
|
+
def schedule_time(interval: nil, time_at: nil)
|
149
|
+
return nil unless interval || time_at
|
150
|
+
|
151
|
+
# Generate protobuf timestamp
|
152
|
+
timestamp = Google::Protobuf::Timestamp.new
|
153
|
+
timestamp.seconds = (time_at || Time.now).to_i + interval.to_i
|
154
|
+
timestamp
|
155
|
+
end
|
156
|
+
|
157
|
+
#
|
158
|
+
# Schedule the task on GCP Cloud Task.
|
159
|
+
#
|
160
|
+
# @param [Integer, nil] interval How to wait before running the task.
|
161
|
+
# Leave to `nil` to run now.
|
162
|
+
#
|
163
|
+
# @return [Google::Cloud::Tasks::V2beta3::Task] The Google Task response
|
164
|
+
#
|
165
|
+
def schedule(interval: nil, time_at: nil)
|
166
|
+
# Generate task payload
|
167
|
+
task = task_payload.merge(
|
168
|
+
schedule_time: schedule_time(interval: interval, time_at: time_at)
|
169
|
+
).compact
|
170
|
+
|
171
|
+
# Create and return remote task
|
172
|
+
client.create_task(queue_path, task)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,37 @@
|
|
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
|
+
def on_schedule
|
24
|
+
true
|
25
|
+
end
|
26
|
+
|
27
|
+
#
|
28
|
+
# Handling logic to perform when a conflict occurs while
|
29
|
+
# executing a job.
|
30
|
+
#
|
31
|
+
def on_execute
|
32
|
+
true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
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,136 @@
|
|
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
|
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
|
+
#
|
18
|
+
def initialize(worker)
|
19
|
+
@worker = worker
|
20
|
+
end
|
21
|
+
|
22
|
+
#
|
23
|
+
# Return the worker configuration options.
|
24
|
+
#
|
25
|
+
# @return [Hash] The worker configuration options.
|
26
|
+
#
|
27
|
+
def options
|
28
|
+
worker.class.cloudtasker_options_hash
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
# Return the instantiated lock.
|
33
|
+
#
|
34
|
+
# @return [Any] The instantiated lock
|
35
|
+
#
|
36
|
+
def lock_instance
|
37
|
+
@lock_instance ||=
|
38
|
+
begin
|
39
|
+
# Infer lock class and get instance
|
40
|
+
lock_name = options[:lock] || options['lock']
|
41
|
+
lock_klass = Lock.const_get(lock_name.to_s.split('_').collect(&:capitalize).join)
|
42
|
+
lock_klass.new(self)
|
43
|
+
rescue NameError
|
44
|
+
DEFAULT_LOCK.new(self)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
#
|
49
|
+
# Return the list of arguments used for job uniqueness.
|
50
|
+
#
|
51
|
+
# @return [Array<any>] The list of unique arguments
|
52
|
+
#
|
53
|
+
def unique_args
|
54
|
+
worker.try(:unique_args, worker.job_args) || worker.job_args
|
55
|
+
end
|
56
|
+
|
57
|
+
#
|
58
|
+
# Return a unique description of the job in hash format.
|
59
|
+
#
|
60
|
+
# @return [Hash] Representation of the unique job in hash format.
|
61
|
+
#
|
62
|
+
def digest_hash
|
63
|
+
@digest_hash ||= {
|
64
|
+
class: worker.class.to_s,
|
65
|
+
unique_args: unique_args
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
#
|
70
|
+
# Return the worker job ID.
|
71
|
+
#
|
72
|
+
# @return [String] The worker job ID.
|
73
|
+
#
|
74
|
+
def id
|
75
|
+
worker.job_id
|
76
|
+
end
|
77
|
+
|
78
|
+
#
|
79
|
+
# Return the ID of the unique job.
|
80
|
+
#
|
81
|
+
# @return [String] The ID of the job.
|
82
|
+
#
|
83
|
+
def unique_id
|
84
|
+
Digest::SHA256.hexdigest(digest_hash.to_json)
|
85
|
+
end
|
86
|
+
|
87
|
+
#
|
88
|
+
# Return the Global ID of the unique job. The gid
|
89
|
+
# includes the UniqueJob namespace.
|
90
|
+
#
|
91
|
+
# @return [String] The global ID of the job
|
92
|
+
#
|
93
|
+
def unique_gid
|
94
|
+
[Config::KEY_NAMESPACE, unique_id].join('/')
|
95
|
+
end
|
96
|
+
|
97
|
+
#
|
98
|
+
# Return the Cloudtasker redis client.
|
99
|
+
#
|
100
|
+
# @return [Class] The Cloudtasker::RedisClient wrapper.
|
101
|
+
#
|
102
|
+
def redis
|
103
|
+
Cloudtasker::RedisClient
|
104
|
+
end
|
105
|
+
|
106
|
+
#
|
107
|
+
# Acquire a new unique job lock or check that the lock is
|
108
|
+
# currently allocated to this job.
|
109
|
+
#
|
110
|
+
# Raise a `Cloudtasker::UniqueJob::LockError` if the lock
|
111
|
+
# if taken by another job.
|
112
|
+
#
|
113
|
+
def lock!
|
114
|
+
redis.with_lock(unique_gid) do
|
115
|
+
locked_id = redis.get(unique_gid)
|
116
|
+
|
117
|
+
# Abort job lock process if lock is already taken by another job
|
118
|
+
raise(LockError, locked_id) if locked_id && locked_id != id
|
119
|
+
|
120
|
+
# Take job lock if the lock is currently free
|
121
|
+
redis.set(unique_gid, id) unless locked_id
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
#
|
126
|
+
# Delete the job lock.
|
127
|
+
#
|
128
|
+
def unlock!
|
129
|
+
redis.with_lock(unique_gid) do
|
130
|
+
locked_id = redis.get(unique_gid)
|
131
|
+
redis.del(unique_gid) if locked_id == id
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
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] || 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
|