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,190 @@
1
+ # Cloudtasker Unique Jobs
2
+
3
+ **Note**: this extension requires redis
4
+
5
+ The Cloudtasker unique job extension allows you to define uniqueness rules for jobs you schedule or process based on job arguments.
6
+
7
+ ## Configuration
8
+
9
+ You can enable unique jobs by adding the following to your cloudtasker initializer:
10
+ ```ruby
11
+ # The unique job extension is optional and must be explicitly required
12
+ require 'cloudtasker/unique_job'
13
+
14
+ Cloudtasker.configure do |config|
15
+ # Specify your redis url.
16
+ # Defaults to `redis://localhost:6379/0` if unspecified
17
+ config.redis = { url: 'redis://some-host:6379/0' }
18
+ end
19
+ ```
20
+
21
+ ## Example
22
+
23
+ The following example defines a worker that prevents more than one instance to run at the same time for the set of provided arguments. Any identical job scheduled after the first one will be re-enqueued until the first job has finished running.
24
+
25
+ ```ruby
26
+ class UniqAtRuntimeWorker
27
+ include Cloudtasker::Worker
28
+
29
+ #
30
+ # lock: specify the phase during which a worker must be unique based on class and arguments.
31
+ # In this case the worker will be unique while it is processed.
32
+ # Other types of locks are available - see below the rest of the documentation.
33
+ #
34
+ # on_conflict: specify what to do if another identical instance enter the lock phase.
35
+ # In this case the worker will be rescheduled until the lock becomes available.
36
+ #
37
+ cloudtasker_options lock: :while_executing, on_conflict: :reschedule
38
+
39
+ def perform(arg1, arg2)
40
+ sleep(10)
41
+ end
42
+ end
43
+ ```
44
+
45
+ Considering the worker and the code below:
46
+ ```ruby
47
+ # Enqueue two jobs successively
48
+ UniqAtRuntimeWorker.perform_async # Job 1
49
+ UniqAtRuntimeWorker.perform_async # Job 2
50
+ ```
51
+
52
+ The following will happen
53
+ 1) Cloud Tasks sends job 1 and job 2 for processing to Rails
54
+ 2) Job 1 acquires a `while_executing` lock
55
+ 3) Job 2 does not acquire the lock and moves to `on_conflict` which is `reschedule`
56
+ 4) Job 2 gets rescheduled in 5 seconds
57
+ 5) Job 1 keeps processing for 5 seconds
58
+ 6) Job 2 is re-sent by Cloud Tasks and cannot acquire the lock, therefore is rescheduled.
59
+ 7) Job 1 processes for another 5 seconds and finishes (total = 10 seconds of processing)
60
+ 8) Job 2 is re-sent by Cloud Tasks, can acquire the lock this time and starts processing
61
+
62
+ ## Available locks
63
+
64
+ Below is the list of available locks that can be specified through the `cloudtasker_options lock: ...` configuration option.
65
+
66
+ For each lock strategy the table specifies the lock period (start/end) and which `on_conflict` strategies are available.
67
+
68
+ | Lock | Starts when | Ends when | On Conflict strategies |
69
+ |------|-------------|-----------|------------------------|
70
+ | `until_executing` | The job is scheduled | The job starts processing | `reject` (default) or `raise` |
71
+ | `while_executing` | The job starts processing | The job ends processing | `reject` (default), `reschedule` or `raise` |
72
+ | `until_executed` | The job is scheduled | The job ends processing | `reject` (default) or `raise` |
73
+ | `until_completed` | The job is scheduled | The job completes successfully or a `DeadWorkerError` is raised. Supported since `v0.15.rc1`. | `reject` (default) or `raise` |
74
+
75
+ ## Available conflict strategies
76
+
77
+ Below is the list of available conflict strategies can be specified through the `cloudtasker_options on_conflict: ...` configuration option.
78
+
79
+ | Strategy | Available with | Description |
80
+ |----------|----------------|----------------|
81
+ | `reject` | All locks | This is the default strategy. The job will be discarded when a conflict occurs |
82
+ | `raise` | All locks | A `Cloudtasker::UniqueJob::LockError` will be raised when a conflict occurs |
83
+ | `reschedule` | `while_executing` | The job will be rescheduled 5 seconds later when a conflict occurs |
84
+
85
+ ## Lock Time To Live (TTL) & deadlocks
86
+ **Note**: Lock TTL has been introduced in `v0.10.rc6`
87
+
88
+ To make jobs unique Cloudtasker sets a lock key - a hash of class name + job arguments - in Redis. Unique crash situations may lead to lock keys not being cleaned up when jobs complete - e.g. Redis crash with rollback from last known state on disk. Situations like these may lead to having a unique job deadlock: jobs with the same class and arguments would stop being processed because they're unable to acquire a lock that will never be cleaned up.
89
+
90
+ In order to prevent deadlocks Cloudtasker configures lock keys to automatically expire in Redis after `job schedule time + lock_ttl (default: 10 minutes)`. This forced expiration ensures that deadlocks eventually get cleaned up shortly after the expected run time of a job.
91
+
92
+ The `lock_ttl (default: 10 minutes)` duration represent the expected max duration of the job. The default 10 minutes value was chosen because it's twice the default request timeout value in Cloud Run. This usually leaves enough room for queue lag (5 minutes) + job processing (5 minutes).
93
+
94
+ Queue lag is certainly the most unpredictable factor here. Job processing time is less of a factor. Jobs running for more than 5 minutes should be split into sub-jobs to limit invocation time over HTTP anyway. Cloudtasker [batch jobs](BATCH_JOBS.md) can help split big jobs into sub-jobs in an atomic way.
95
+
96
+ The default lock key expiration of `job schedule time + 10 minutes` may look aggressive but it is a better choice than having real-time jobs stuck for X hours after a crash recovery.
97
+
98
+ We **strongly recommend** adapting the `lock_ttl` option either globally or for each worker based on expected queue lag and job duration.
99
+
100
+ **Example 1**: Global configuration
101
+ ```ruby
102
+ # config/initializers/cloudtasker.rb
103
+
104
+ # General Cloudtasker configuration
105
+ Cloudtasker.configure do |config|
106
+ # ...
107
+ end
108
+
109
+ # Unique job extension configuration
110
+ Cloudtasker::UniqueJob.configure do |config|
111
+ config.lock_ttl = 3 * 60 # 3 minutes
112
+ end
113
+ ```
114
+
115
+ **Example 2**: Worker-level - fast
116
+ ```ruby
117
+ # app/workers/realtime_worker_on_fast_queue.rb
118
+
119
+ class RealtimeWorkerOnFastQueue
120
+ include Cloudtasker::Worker
121
+
122
+ # Ensure lock is removed 30 seconds after schedule time
123
+ cloudtasker_options lock: :until_executing, lock_ttl: 30
124
+
125
+ def perform(arg1, arg2)
126
+ # ...
127
+ end
128
+ end
129
+ ```
130
+
131
+ **Example 3**: Worker-level - slow
132
+ ```ruby
133
+ # app/workers/non_critical_worker_on_slow_queue.rb
134
+
135
+ class NonCriticalWorkerOnSlowQueue
136
+ include Cloudtasker::Worker
137
+
138
+ # Ensure lock is removed 24 hours after schedule time
139
+ cloudtasker_options lock: :until_executing, lock_ttl: 3600 * 24
140
+
141
+ def perform(arg1, arg2)
142
+ # ...
143
+ end
144
+ end
145
+ ```
146
+
147
+ ## Configuring unique arguments
148
+
149
+ By default Cloudtasker considers all job arguments to evaluate the uniqueness of a job. This behaviour is configurable per worker by defining a `unique_args` method on the worker itself returning the list of args defining uniqueness.
150
+
151
+ Example 1: Uniqueness based on a subset of arguments
152
+ ```ruby
153
+ class UniqBasedOnTwoArgsWorker
154
+ include Cloudtasker::Worker
155
+
156
+ cloudtasker_options lock: :until_executed
157
+
158
+ # Only consider the first two args when evaluating uniqueness
159
+ def unique_args(args)
160
+ [arg[0], arg[1]]
161
+ end
162
+
163
+ def perform(arg1, arg2, arg3)
164
+ # ...
165
+ end
166
+ end
167
+ ```
168
+
169
+ Example 2: Uniqueness based on modified arguments
170
+ ```ruby
171
+ class ModuloArgsWorker
172
+ include Cloudtasker::Worker
173
+
174
+ cloudtasker_options lock: :until_executed
175
+
176
+ # The remainder of `some_int` modulo 5 will be considered for
177
+ # uniqueness instead of the full value of `some_int`
178
+ def unique_args(args)
179
+ [arg[0], arg[1], arg[2] % 5]
180
+ end
181
+
182
+ def perform(arg1, arg2, some_int)
183
+ # ...
184
+ end
185
+ end
186
+ ```
187
+
188
+ ## Beware of default method arguments
189
+
190
+ Default method arguments are ignored when evaluating worker uniqueness. See [this section](../../../#be-careful-with-default-arguments) for more details.
data/exe/cloudtasker ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'cloudtasker/cli'
6
+ require 'optparse'
7
+
8
+ options = {}
9
+ OptionParser.new do |opts|
10
+ opts.banner = 'Usage: cloudtasker [options]'
11
+
12
+ opts.on(
13
+ '-q QUEUE', '--queue=QUEUE',
14
+ 'Queue to process and number of threads. ' \
15
+ "Examples: '-q critical' | '-q critical,2' | '-q critical,3 -q defaults,2'"
16
+ ) do |o|
17
+ options[:queues] ||= []
18
+ options[:queues] << o.split(',')
19
+ end
20
+ end.parse!
21
+
22
+ begin
23
+ Cloudtasker::CLI.run(options)
24
+ rescue StandardError => e
25
+ raise e if $DEBUG
26
+
27
+ warn e.message
28
+ warn e.backtrace.join("\n")
29
+ exit 1
30
+ end
@@ -0,0 +1,2 @@
1
+ ---
2
+ BUNDLE_RETRY: "1"
@@ -0,0 +1,17 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "bundler", "~> 2.0"
7
+ gem "rake", ">= 12.3.3"
8
+ gem "rspec", "~> 3.0"
9
+ gem "rspec-json_expectations", "~> 2.2"
10
+ gem "rubocop", "~> 1.64.1"
11
+ gem "rubocop-rspec", "~> 3.0.1"
12
+ gem "semantic_logger"
13
+ gem "timecop"
14
+ gem "webmock"
15
+ gem "google-cloud-tasks", "~> 1.0.0"
16
+
17
+ gemspec path: "../"
@@ -0,0 +1,17 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "bundler", "~> 2.0"
7
+ gem "rake", ">= 12.3.3"
8
+ gem "rspec", "~> 3.0"
9
+ gem "rspec-json_expectations", "~> 2.2"
10
+ gem "rubocop", "~> 1.64.1"
11
+ gem "rubocop-rspec", "~> 3.0.1"
12
+ gem "semantic_logger"
13
+ gem "timecop"
14
+ gem "webmock"
15
+ gem "google-cloud-tasks", "~> 1.1.0"
16
+
17
+ gemspec path: "../"
@@ -0,0 +1,17 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "bundler", "~> 2.0"
7
+ gem "rake", ">= 12.3.3"
8
+ gem "rspec", "~> 3.0"
9
+ gem "rspec-json_expectations", "~> 2.2"
10
+ gem "rubocop", "~> 1.64.1"
11
+ gem "rubocop-rspec", "~> 3.0.1"
12
+ gem "semantic_logger"
13
+ gem "timecop"
14
+ gem "webmock"
15
+ gem "google-cloud-tasks", "~> 1.2.0"
16
+
17
+ gemspec path: "../"
@@ -0,0 +1,17 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "bundler", "~> 2.0"
7
+ gem "rake", ">= 12.3.3"
8
+ gem "rspec", "~> 3.0"
9
+ gem "rspec-json_expectations", "~> 2.2"
10
+ gem "rubocop", "~> 1.64.1"
11
+ gem "rubocop-rspec", "~> 3.0.1"
12
+ gem "semantic_logger"
13
+ gem "timecop"
14
+ gem "webmock"
15
+ gem "google-cloud-tasks", "~> 1.3.0"
16
+
17
+ gemspec path: "../"
@@ -0,0 +1,17 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "bundler", "~> 2.0"
7
+ gem "rake", ">= 12.3.3"
8
+ gem "rspec", "~> 3.0"
9
+ gem "rspec-json_expectations", "~> 2.2"
10
+ gem "rubocop", "~> 1.64.1"
11
+ gem "rubocop-rspec", "~> 3.0.1"
12
+ gem "semantic_logger"
13
+ gem "timecop"
14
+ gem "webmock"
15
+ gem "google-cloud-tasks", "~> 1.4.0"
16
+
17
+ gemspec path: "../"
@@ -0,0 +1,17 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "bundler", "~> 2.0"
7
+ gem "rake", ">= 12.3.3"
8
+ gem "rspec", "~> 3.0"
9
+ gem "rspec-json_expectations", "~> 2.2"
10
+ gem "rubocop", "~> 1.64.1"
11
+ gem "rubocop-rspec", "~> 3.0.1"
12
+ gem "semantic_logger"
13
+ gem "timecop"
14
+ gem "webmock"
15
+ gem "google-cloud-tasks", "~> 1.5.0"
16
+
17
+ gemspec path: "../"
@@ -0,0 +1,17 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "bundler", "~> 2.0"
7
+ gem "rake", ">= 12.3.3"
8
+ gem "rspec", "~> 3.0"
9
+ gem "rspec-json_expectations", "~> 2.2"
10
+ gem "rubocop", "~> 1.64.1"
11
+ gem "rubocop-rspec", "~> 3.0.1"
12
+ gem "semantic_logger"
13
+ gem "timecop"
14
+ gem "webmock"
15
+ gem "google-cloud-tasks", "~> 2.0.0"
16
+
17
+ gemspec path: "../"
@@ -0,0 +1,17 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "bundler", "~> 2.0"
7
+ gem "rake", ">= 12.3.3"
8
+ gem "rspec", "~> 3.0"
9
+ gem "rspec-json_expectations", "~> 2.2"
10
+ gem "rubocop", "~> 1.64.1"
11
+ gem "rubocop-rspec", "~> 3.0.1"
12
+ gem "semantic_logger"
13
+ gem "timecop"
14
+ gem "webmock"
15
+ gem "google-cloud-tasks", "~> 2.1.0"
16
+
17
+ gemspec path: "../"
@@ -0,0 +1,20 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "bundler", "~> 2.0"
7
+ gem "rake", ">= 12.3.3"
8
+ gem "rspec", "~> 3.0"
9
+ gem "rspec-json_expectations", "~> 2.2"
10
+ gem "rubocop", "~> 1.64.1"
11
+ gem "rubocop-rspec", "~> 3.0.1"
12
+ gem "semantic_logger"
13
+ gem "timecop"
14
+ gem "webmock"
15
+ gem "rails", "~> 6.1.0"
16
+ gem "rspec-rails"
17
+ gem "mutex_m"
18
+ gem "drb"
19
+
20
+ gemspec path: "../"
@@ -0,0 +1,18 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "bundler", "~> 2.0"
7
+ gem "rake", ">= 12.3.3"
8
+ gem "rspec", "~> 3.0"
9
+ gem "rspec-json_expectations", "~> 2.2"
10
+ gem "rubocop", "~> 1.64.1"
11
+ gem "rubocop-rspec", "~> 3.0.1"
12
+ gem "semantic_logger"
13
+ gem "timecop"
14
+ gem "webmock"
15
+ gem "rails", "~> 7.0.0"
16
+ gem "rspec-rails"
17
+
18
+ gemspec path: "../"
@@ -0,0 +1,18 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "bundler", "~> 2.0"
7
+ gem "rake", ">= 12.3.3"
8
+ gem "rspec", "~> 3.0"
9
+ gem "rspec-json_expectations", "~> 2.2"
10
+ gem "rubocop", "~> 1.64.1"
11
+ gem "rubocop-rspec", "~> 3.0.1"
12
+ gem "semantic_logger"
13
+ gem "timecop"
14
+ gem "webmock"
15
+ gem "rails", "~> 7.1"
16
+ gem "rspec-rails"
17
+
18
+ gemspec path: "../"
@@ -0,0 +1,18 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "bundler", "~> 2.0"
7
+ gem "rake", ">= 12.3.3"
8
+ gem "rspec", "~> 3.0"
9
+ gem "rspec-json_expectations", "~> 2.2"
10
+ gem "rubocop", "~> 1.64.1"
11
+ gem "rubocop-rspec", "~> 3.0.1"
12
+ gem "semantic_logger"
13
+ gem "timecop"
14
+ gem "webmock"
15
+ gem "rails", "~> 8.0"
16
+ gem "rspec-rails"
17
+
18
+ gemspec path: "../"
@@ -0,0 +1,18 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "bundler", "~> 2.0"
7
+ gem "rake", ">= 12.3.3"
8
+ gem "rspec", "~> 3.0"
9
+ gem "rspec-json_expectations", "~> 2.2"
10
+ gem "rubocop", "~> 1.64.1"
11
+ gem "rubocop-rspec", "~> 3.0.1"
12
+ gem "semantic_logger"
13
+ gem "timecop"
14
+ gem "webmock"
15
+ gem "rails", "~> 8.1"
16
+ gem "rspec-rails"
17
+
18
+ gemspec path: "../"
@@ -0,0 +1,16 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "bundler", "~> 2.0"
7
+ gem "rake", ">= 12.3.3"
8
+ gem "rspec", "~> 3.0"
9
+ gem "rspec-json_expectations", "~> 2.2"
10
+ gem "rubocop", "~> 1.64.1"
11
+ gem "rubocop-rspec", "~> 3.0.1"
12
+ gem "semantic_logger", "3.4.1"
13
+ gem "timecop"
14
+ gem "webmock"
15
+
16
+ gemspec path: "../"
@@ -0,0 +1,16 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "bundler", "~> 2.0"
7
+ gem "rake", ">= 12.3.3"
8
+ gem "rspec", "~> 3.0"
9
+ gem "rspec-json_expectations", "~> 2.2"
10
+ gem "rubocop", "~> 1.64.1"
11
+ gem "rubocop-rspec", "~> 3.0.1"
12
+ gem "semantic_logger", "4.6.1"
13
+ gem "timecop"
14
+ gem "webmock"
15
+
16
+ gemspec path: "../"
@@ -0,0 +1,16 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "bundler", "~> 2.0"
7
+ gem "rake", ">= 12.3.3"
8
+ gem "rspec", "~> 3.0"
9
+ gem "rspec-json_expectations", "~> 2.2"
10
+ gem "rubocop", "~> 1.64.1"
11
+ gem "rubocop-rspec", "~> 3.0.1"
12
+ gem "semantic_logger", "4.7.0"
13
+ gem "timecop"
14
+ gem "webmock"
15
+
16
+ gemspec path: "../"
@@ -0,0 +1,16 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "bundler", "~> 2.0"
7
+ gem "rake", ">= 12.3.3"
8
+ gem "rspec", "~> 3.0"
9
+ gem "rspec-json_expectations", "~> 2.2"
10
+ gem "rubocop", "~> 1.64.1"
11
+ gem "rubocop-rspec", "~> 3.0.1"
12
+ gem "semantic_logger", "4.7.2"
13
+ gem "timecop"
14
+ gem "webmock"
15
+
16
+ gemspec path: "../"
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ActiveJob docs: http://guides.rubyonrails.org/active_job_basics.html
4
+ # Example adapters ref: https://github.com/rails/rails/tree/master/activejob/lib/active_job/queue_adapters
5
+
6
+ module ActiveJob
7
+ module QueueAdapters
8
+ # == Cloudtasker adapter for Active Job
9
+ #
10
+ # To use Cloudtasker set the queue_adapter config to +:cloudtasker+.
11
+ #
12
+ # Rails.application.config.active_job.queue_adapter = :cloudtasker
13
+ class CloudtaskerAdapter
14
+ SERIALIZATION_FILTERED_KEYS = [
15
+ 'executions', # Given by the worker at processing
16
+ 'provider_job_id', # Also given by the worker at processing
17
+ 'priority' # Not used
18
+ ].freeze
19
+
20
+ # Enqueues the given ActiveJob instance for execution
21
+ #
22
+ # @param job [ActiveJob::Base] The ActiveJob instance
23
+ #
24
+ # @return [Cloudtasker::CloudTask] The Google Task response
25
+ #
26
+ def enqueue(job)
27
+ build_worker(job).schedule
28
+ end
29
+
30
+ # Enqueues the given ActiveJob instance for execution at a given time
31
+ #
32
+ # @param job [ActiveJob::Base] The ActiveJob instance
33
+ # @param precise_timestamp [Integer] The timestamp at which the job must be executed
34
+ #
35
+ # @return [Cloudtasker::CloudTask] The Google Task response
36
+ #
37
+ def enqueue_at(job, precise_timestamp)
38
+ build_worker(job).schedule(time_at: Time.at(precise_timestamp))
39
+ end
40
+
41
+ # Determines if enqueuing will check and wait for an associated transaction completes before enqueuing
42
+ #
43
+ # @return [Boolean] True always as this is the default from QueueAdapters::AbstractAdapter
44
+ def enqueue_after_transaction_commit?
45
+ true
46
+ end
47
+
48
+ private
49
+
50
+ def build_worker(job)
51
+ job_serialization = job.serialize.except(*SERIALIZATION_FILTERED_KEYS)
52
+
53
+ JobWrapper.new(
54
+ job_id: job_serialization.delete('job_id'),
55
+ job_queue: job_serialization.delete('queue_name'),
56
+ job_args: [job_serialization]
57
+ )
58
+ end
59
+
60
+ # == Job Wrapper for the Cloudtasker adapter
61
+ #
62
+ # Executes jobs scheduled by the Cloudtasker ActiveJob adapter
63
+ class JobWrapper # :nodoc:
64
+ include Cloudtasker::Worker
65
+
66
+ # Executes the given serialized ActiveJob call.
67
+ # - See https://api.rubyonrails.org/classes/ActiveJob/Core.html#method-i-serialize
68
+ #
69
+ # @param [Hash] job_serialization The serialized ActiveJob call
70
+ #
71
+ # @return [any] The execution of the ActiveJob call
72
+ #
73
+ def perform(job_serialization, *_extra_options)
74
+ job_executions = job_retries < 1 ? 0 : (job_retries + 1)
75
+
76
+ job_serialization.merge!(
77
+ 'job_id' => job_id,
78
+ 'queue_name' => job_queue,
79
+ 'provider_job_id' => task_id,
80
+ 'executions' => job_executions,
81
+ 'priority' => nil
82
+ )
83
+
84
+ Base.execute job_serialization
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudtasker
4
+ class AuthenticationError < StandardError
5
+ end
6
+ end