cloudtasker 0.1.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -0
  3. data/.rubocop.yml +5 -0
  4. data/.travis.yml +10 -1
  5. data/Appraisals +25 -0
  6. data/CHANGELOG.md +25 -0
  7. data/Gemfile.lock +37 -4
  8. data/README.md +573 -6
  9. data/Rakefile +6 -0
  10. data/app/controllers/cloudtasker/application_controller.rb +2 -0
  11. data/app/controllers/cloudtasker/worker_controller.rb +24 -2
  12. data/cloudtasker.gemspec +7 -3
  13. data/docs/BATCH_JOBS.md +66 -0
  14. data/docs/CRON_JOBS.md +63 -0
  15. data/docs/UNIQUE_JOBS.md +127 -0
  16. data/exe/cloudtasker +15 -0
  17. data/gemfiles/.bundle/config +2 -0
  18. data/gemfiles/google_cloud_tasks_1.0.gemfile +9 -0
  19. data/gemfiles/google_cloud_tasks_1.0.gemfile.lock +263 -0
  20. data/gemfiles/google_cloud_tasks_1.1.gemfile +9 -0
  21. data/gemfiles/google_cloud_tasks_1.1.gemfile.lock +263 -0
  22. data/gemfiles/google_cloud_tasks_1.2.gemfile +9 -0
  23. data/gemfiles/google_cloud_tasks_1.2.gemfile.lock +263 -0
  24. data/gemfiles/google_cloud_tasks_1.3.gemfile +9 -0
  25. data/gemfiles/google_cloud_tasks_1.3.gemfile.lock +264 -0
  26. data/gemfiles/rails_4.0.gemfile +10 -0
  27. data/gemfiles/rails_4.1.gemfile +9 -0
  28. data/gemfiles/rails_4.2.gemfile +9 -0
  29. data/gemfiles/rails_5.0.gemfile +9 -0
  30. data/gemfiles/rails_5.1.gemfile +9 -0
  31. data/gemfiles/rails_5.2.gemfile +9 -0
  32. data/gemfiles/rails_5.2.gemfile.lock +247 -0
  33. data/gemfiles/rails_6.0.gemfile +9 -0
  34. data/gemfiles/rails_6.0.gemfile.lock +263 -0
  35. data/lib/cloudtasker.rb +21 -1
  36. data/lib/cloudtasker/backend/google_cloud_task.rb +139 -0
  37. data/lib/cloudtasker/backend/memory_task.rb +190 -0
  38. data/lib/cloudtasker/backend/redis_task.rb +249 -0
  39. data/lib/cloudtasker/batch/batch_progress.rb +19 -1
  40. data/lib/cloudtasker/batch/job.rb +88 -23
  41. data/lib/cloudtasker/batch/middleware.rb +0 -1
  42. data/lib/cloudtasker/cli.rb +194 -0
  43. data/lib/cloudtasker/cloud_task.rb +91 -0
  44. data/lib/cloudtasker/config.rb +64 -2
  45. data/lib/cloudtasker/cron/job.rb +6 -3
  46. data/lib/cloudtasker/cron/middleware.rb +0 -1
  47. data/lib/cloudtasker/cron/schedule.rb +73 -13
  48. data/lib/cloudtasker/dead_worker_error.rb +6 -0
  49. data/lib/cloudtasker/local_server.rb +74 -0
  50. data/lib/cloudtasker/railtie.rb +10 -0
  51. data/lib/cloudtasker/redis_client.rb +24 -2
  52. data/lib/cloudtasker/testing.rb +133 -0
  53. data/lib/cloudtasker/unique_job/job.rb +5 -2
  54. data/lib/cloudtasker/unique_job/lock/base_lock.rb +1 -1
  55. data/lib/cloudtasker/unique_job/lock/until_executed.rb +3 -1
  56. data/lib/cloudtasker/unique_job/lock/while_executing.rb +3 -1
  57. data/lib/cloudtasker/unique_job/middleware.rb +0 -1
  58. data/lib/cloudtasker/version.rb +1 -1
  59. data/lib/cloudtasker/worker.rb +59 -16
  60. data/lib/cloudtasker/{task.rb → worker_handler.rb} +10 -77
  61. data/lib/cloudtasker/worker_logger.rb +155 -0
  62. data/lib/tasks/setup_queue.rake +10 -0
  63. metadata +98 -9
  64. data/lib/cloudtasker/batch/config.rb +0 -11
  65. data/lib/cloudtasker/cron/config.rb +0 -11
  66. data/lib/cloudtasker/unique_job/config.rb +0 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ac6e1474db922e6509a54912fb42d5522c066c7926c0bb0c45e4eec80335c83c
4
- data.tar.gz: af4b423e1ed2d17fdd9c1228f34a965ac8d17078f96331d01b64bc3766f3fbef
3
+ metadata.gz: 8721e878a098657f0c3974670df6373c873d1df2fa1042b97d9b37d7a1118299
4
+ data.tar.gz: ffd5652b0220a833f603682fdb4392a850b13b185bd7516dd170f5314d1b4af6
5
5
  SHA512:
6
- metadata.gz: 669d907248880e4de8f8a1d0ab83aad96540d93bf3b70dec804184b5a0507244a891e5d66f03593163d6e581a85ad1686e348ac3413b4c250512a5ec877c9209
7
- data.tar.gz: 2b6a09ff60f5105f167c818dcf284881afbbd96a4818a9e1fc432c8fd8ee19014127e6292bb5749c09f4a10458cbecd94c48b0f8486e4f8fed91dbf7d3f36772
6
+ metadata.gz: f0f98ff0e0062b28be192f8353363a736f5cb315eb14f86f577586de070127642d123617e8e831dea84271cada640ea3b9ec642ea75f3f9a12cd494432b9e178
7
+ data.tar.gz: a04500924970efdbc0ceb9364314e0426c92800a4c571b839432de5b9f2faa04e678a67186b0dfd2b32222e8d8606e39ce51b0922ba9687ea827271174dcb23d
data/.gitignore CHANGED
@@ -3,9 +3,13 @@
3
3
  /_yardoc/
4
4
  /coverage/
5
5
  /doc/
6
+ /examples/rails/log/*.log
7
+ /examples/rails/tmp/
8
+ /log/
6
9
  /pkg/
7
10
  /spec/reports/
8
11
  /spec/dummy/log/
12
+ /spec/dummy/tmp/
9
13
  /tmp/
10
14
 
11
15
  # rspec failure tracking
@@ -24,4 +24,9 @@ RSpec/ScatteredSetup:
24
24
  Metrics/BlockLength:
25
25
  Exclude:
26
26
  - cloudtasker.gemspec
27
+ - 'spec/**/*'
28
+
29
+ Style/Documentation:
30
+ Exclude:
31
+ - 'examples/**/*'
27
32
  - 'spec/**/*'
@@ -1,7 +1,16 @@
1
1
  ---
2
- sudo: false
3
2
  language: ruby
4
3
  cache: bundler
5
4
  rvm:
5
+ - 2.3
6
+ - 2.4
6
7
  - 2.5.5
7
8
  before_install: gem install bundler -v 2.0.2
9
+ before_script: rubocop
10
+ gemfile:
11
+ - gemfiles/google_cloud_tasks_1.0.gemfile
12
+ - gemfiles/google_cloud_tasks_1.1.gemfile
13
+ - gemfiles/google_cloud_tasks_1.2.gemfile
14
+ - gemfiles/google_cloud_tasks_1.3.gemfile
15
+ - gemfiles/rails_5.2.gemfile
16
+ - gemfiles/rails_6.0.gemfile
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ appraise 'google-cloud-tasks-1.0' do
4
+ gem 'google-cloud-tasks', '1.0'
5
+ end
6
+
7
+ appraise 'google-cloud-tasks-1.1' do
8
+ gem 'google-cloud-tasks', '1.1'
9
+ end
10
+
11
+ appraise 'google-cloud-tasks-1.2' do
12
+ gem 'google-cloud-tasks', '1.2'
13
+ end
14
+
15
+ appraise 'google-cloud-tasks-1.3' do
16
+ gem 'google-cloud-tasks', '1.3'
17
+ end
18
+
19
+ appraise 'rails-5.2' do
20
+ gem 'rails', '5.2'
21
+ end
22
+
23
+ appraise 'rails-6.0' do
24
+ gem 'rails', '6.0'
25
+ end
@@ -0,0 +1,25 @@
1
+ # Changelog
2
+
3
+ ## [v0.5.0](https://github.com/keypup-io/cloudtasker/tree/v0.5.0) (2019-11-25)
4
+
5
+ [Full Changelog](https://github.com/keypup-io/cloudtasker/compare/v0.4.0...v0.5.0)
6
+
7
+ ## [v0.4.0](https://github.com/keypup-io/cloudtasker/tree/v0.4.0) (2019-11-25)
8
+
9
+ [Full Changelog](https://github.com/keypup-io/cloudtasker/compare/v0.3.0...v0.4.0)
10
+
11
+ ## [v0.3.0](https://github.com/keypup-io/cloudtasker/tree/v0.3.0) (2019-11-25)
12
+
13
+ [Full Changelog](https://github.com/keypup-io/cloudtasker/compare/v0.2.0...v0.3.0)
14
+
15
+ ## [v0.2.0](https://github.com/keypup-io/cloudtasker/tree/v0.2.0) (2019-11-18)
16
+
17
+ [Full Changelog](https://github.com/keypup-io/cloudtasker/compare/v0.1.0...v0.2.0)
18
+
19
+ ## [v0.1.0](https://github.com/keypup-io/cloudtasker/tree/v0.1.0) (2019-11-17)
20
+
21
+ [Full Changelog](https://github.com/keypup-io/cloudtasker/compare/c137feb1ceaaaa4e2fecac0d1f0b4c73151ae002...v0.1.0)
22
+
23
+
24
+
25
+ \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
@@ -1,7 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- cloudtasker (0.1.0)
4
+ cloudtasker (0.6.0)
5
+ activesupport
5
6
  fugit
6
7
  google-cloud-tasks
7
8
  jwt
@@ -67,9 +68,15 @@ GEM
67
68
  zeitwerk (~> 2.1, >= 2.1.8)
68
69
  addressable (2.7.0)
69
70
  public_suffix (>= 2.0.2, < 5.0)
71
+ appraisal (2.2.0)
72
+ bundler
73
+ rake
74
+ thor (>= 0.14.0)
70
75
  ast (2.4.0)
71
76
  builder (3.2.3)
72
77
  concurrent-ruby (1.1.5)
78
+ crack (0.4.3)
79
+ safe_yaml (~> 1.0.0)
73
80
  crass (1.0.5)
74
81
  diff-lcs (1.3)
75
82
  erubi (1.9.0)
@@ -77,14 +84,25 @@ GEM
77
84
  tzinfo
78
85
  faraday (0.17.0)
79
86
  multipart-post (>= 1.2, < 3)
87
+ faraday-http-cache (2.0.0)
88
+ faraday (~> 0.8)
80
89
  fugit (1.3.3)
81
90
  et-orbi (~> 1.1, >= 1.1.8)
82
91
  raabro (~> 1.1)
92
+ github_changelog_generator (1.15.0)
93
+ activesupport
94
+ faraday-http-cache
95
+ multi_json
96
+ octokit (~> 4.6)
97
+ rainbow (>= 2.2.1)
98
+ rake (>= 10.0)
99
+ retriable (~> 3.0)
83
100
  globalid (0.4.2)
84
101
  activesupport (>= 4.2.0)
85
- google-cloud-tasks (1.2.0)
102
+ google-cloud-tasks (1.3.1)
86
103
  google-gax (~> 1.8)
87
104
  googleapis-common-protos (>= 1.3.9, < 2.0)
105
+ googleapis-common-protos-types (>= 1.0.4, < 2.0)
88
106
  grpc-google-iam-v1 (~> 0.6.9)
89
107
  google-gax (1.8.1)
90
108
  google-protobuf (~> 3.9)
@@ -106,12 +124,13 @@ GEM
106
124
  multi_json (~> 1.11)
107
125
  os (>= 0.9, < 2.0)
108
126
  signet (~> 0.12)
109
- grpc (1.24.0-universal-darwin)
127
+ grpc (1.25.0-universal-darwin)
110
128
  google-protobuf (~> 3.8)
111
129
  googleapis-common-protos-types (~> 1.0)
112
130
  grpc-google-iam-v1 (0.6.9)
113
131
  googleapis-common-protos (>= 1.3.1, < 2.0)
114
132
  grpc (~> 1.0)
133
+ hashdiff (1.0.0)
115
134
  i18n (1.7.0)
116
135
  concurrent-ruby (~> 1.0)
117
136
  jaro_winkler (1.5.4)
@@ -123,7 +142,7 @@ GEM
123
142
  mini_mime (>= 0.1.1)
124
143
  marcel (0.3.3)
125
144
  mimemagic (~> 0.3.2)
126
- memoist (0.16.0)
145
+ memoist (0.16.1)
127
146
  method_source (0.9.2)
128
147
  mimemagic (0.3.3)
129
148
  mini_mime (1.0.2)
@@ -134,6 +153,8 @@ GEM
134
153
  nio4r (2.5.2)
135
154
  nokogiri (1.10.5)
136
155
  mini_portile2 (~> 2.4.0)
156
+ octokit (4.14.0)
157
+ sawyer (~> 0.8.0, >= 0.5.3)
137
158
  os (1.0.1)
138
159
  parallel (1.18.0)
139
160
  parser (2.6.5.0)
@@ -172,6 +193,7 @@ GEM
172
193
  rainbow (3.0.0)
173
194
  rake (10.5.0)
174
195
  redis (4.1.3)
196
+ retriable (3.1.2)
175
197
  rly (0.2.3)
176
198
  rspec (3.9.0)
177
199
  rspec-core (~> 3.9.0)
@@ -204,6 +226,10 @@ GEM
204
226
  rubocop-rspec (1.36.0)
205
227
  rubocop (>= 0.68.1)
206
228
  ruby-progressbar (1.10.1)
229
+ safe_yaml (1.0.5)
230
+ sawyer (0.8.2)
231
+ addressable (>= 2.3.5)
232
+ faraday (> 0.8, < 2.0)
207
233
  signet (0.12.0)
208
234
  addressable (~> 2.3)
209
235
  faraday (~> 0.9)
@@ -223,6 +249,10 @@ GEM
223
249
  tzinfo (1.2.5)
224
250
  thread_safe (~> 0.1)
225
251
  unicode-display_width (1.6.0)
252
+ webmock (3.7.6)
253
+ addressable (>= 2.3.6)
254
+ crack (>= 0.3.2)
255
+ hashdiff (>= 0.4.0, < 2.0.0)
226
256
  websocket-driver (0.7.1)
227
257
  websocket-extensions (>= 0.1.0)
228
258
  websocket-extensions (0.1.4)
@@ -232,8 +262,10 @@ PLATFORMS
232
262
  ruby
233
263
 
234
264
  DEPENDENCIES
265
+ appraisal
235
266
  bundler (~> 2.0)
236
267
  cloudtasker!
268
+ github_changelog_generator
237
269
  rails
238
270
  rake (~> 10.0)
239
271
  rspec (~> 3.0)
@@ -242,6 +274,7 @@ DEPENDENCIES
242
274
  rubocop-rspec
243
275
  sqlite3
244
276
  timecop
277
+ webmock
245
278
 
246
279
  BUNDLED WITH
247
280
  2.0.2
data/README.md CHANGED
@@ -1,8 +1,35 @@
1
1
  # Cloudtasker
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/cloudtasker`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ Background jobs for Ruby using Google Cloud Tasks.
4
4
 
5
- TODO: Delete this and the text above, and describe your gem
5
+ Cloudtasker provides an easy to manage interface to Google Cloud Tasks for background job processing. Workers can be defined programmatically using the Cloudtasker DSL and enqueued for processing using a simple to use API.
6
+
7
+ Cloudtasker is particularly suited for serverless applications only responding to HTTP requests and where running a dedicated job processing is not an option (e.g. deploy via [Cloud Run](https://cloud.google.com/run)). All jobs enqueued in Cloud Tasks via Cloudtasker eventually gets processed by your application via HTTP requests.
8
+
9
+ Cloudtasker also provides optional modules for running [cron jobs](docs/CRON_JOBS.md), [batch jobs](docs/BATCH_JOBS.md) and [unique jobs](docs/UNIQUE_JOBS.md).
10
+
11
+ A local processing server is also available in development. This local server processes jobs in lieu of Cloud Tasks and allow you to work offline.
12
+
13
+ ## Summary
14
+
15
+ 1. [Installation](#installation)
16
+ 2. [Get started with Rails](#get-started-with-rails)
17
+ 3. [Configuring Cloudtasker](#configuring-cloudtasker)
18
+ 1. [Cloud Tasks authentication & permissions](#cloud-tasks-authentication--permissions)
19
+ 2. [Cloudtasker initializer](#cloudtasker-initializer)
20
+ 4. [Enqueuing jobs](#enqueuing-jobs)
21
+ 5. [Extensions](#extensions)
22
+ 6. [Working locally](#working-locally)
23
+ 1. [Option 1: Cloudtasker local server](#option-1-cloudtasker-local-server)
24
+ 2. [Option 2: Using ngrok](#option-2-using-ngrok)
25
+ 7. [Logging](#logging)
26
+ 1. [Configuring a logger](#configuring-a-logger)
27
+ 2. [Logging context](#logging-context)
28
+ 8. [Error Handling](#error-handling)
29
+ 1. [HTTP Error codes](#http-error-codes)
30
+ 2. [Error callbacks](#error-callbacks)
31
+ 3. [Max retries](#max-retries)
32
+ 9. [Best practices building workers](#best-practices-building-workers)
6
33
 
7
34
  ## Installation
8
35
 
@@ -20,9 +47,545 @@ Or install it yourself as:
20
47
 
21
48
  $ gem install cloudtasker
22
49
 
23
- ## Usage
50
+ ## Get started with Rails
51
+
52
+ Cloudtasker is pre-integrated with Rails. Follow the steps below to get started.
53
+
54
+ Install redis on your machine (this is required by the Cloudtasker local processing server)
55
+ ```bash
56
+ # E.g. using brew
57
+ brew install redis
58
+ ```
59
+
60
+ Add the following initializer
61
+ ```ruby
62
+ # config/initializers/cloudtasker.rb
63
+
64
+ Cloudtasker.configure do |config|
65
+ #
66
+ # Adapt the server port to be the one used by your Rails web process
67
+ #
68
+ config.processor_host = 'http://localhost:3000'
69
+
70
+ #
71
+ # If you do not have any Rails secret_key_base defined, uncomment the following
72
+ # This secret is used to authenticate jobs sent to the processing endpoint
73
+ # of your application.
74
+ #
75
+ # config.secret = 'some-long-token'
76
+ end
77
+ ```
78
+
79
+ Define your first worker:
80
+ ```ruby
81
+ # app/workers/dummy_worker.rb
82
+
83
+ class DummyWorker
84
+ include Cloudtasker::Worker
85
+
86
+ def perform(some_arg)
87
+ logger.info("Job run with #{some_arg}. This is working!")
88
+ end
89
+ end
90
+ ```
91
+
92
+ Launch Rails and the local Cloudtasker processing server (or add `cloudtasker` to your foreman config as a `worker` process)
93
+ ```bash
94
+ # In one terminal
95
+ > rails s -p 3000
96
+
97
+ # In another terminal
98
+ > cloudtasker
99
+ ```
100
+
101
+ Open a Rails console and enqueue some jobs
102
+ ```ruby
103
+ # Process job as soon as possible
104
+ DummyWorker.perform_async('foo')
105
+
106
+ # Process job in 60 seconds
107
+ DummyWorker.perform_in(10, 'foo')
108
+ ```
109
+
110
+ Your Rails logs should display the following:
111
+ ```log
112
+ Started POST "/cloudtasker/run" for ::1 at 2019-11-22 09:20:09 +0100
113
+
114
+ Processing by Cloudtasker::WorkerController#run as */*
115
+ Parameters: {"worker"=>"DummyWorker", "job_id"=>"d76040a1-367e-4e3b-854e-e05a74d5f773", "job_args"=>["foo"], "job_meta"=>{}}
116
+
117
+ I, [2019-11-22T09:20:09.319336 #49257] INFO -- [Cloudtasker][d76040a1-367e-4e3b-854e-e05a74d5f773] Starting job...: {:worker=>"DummyWorker", :job_id=>"d76040a1-367e-4e3b-854e-e05a74d5f773", :job_meta=>{}}
118
+ I, [2019-11-22T09:20:09.319938 #49257] INFO -- [Cloudtasker][d76040a1-367e-4e3b-854e-e05a74d5f773] Job run with foo. This is working!: {:worker=>"DummyWorker", :job_id=>"d76040a1-367e-4e3b-854e-e05a74d5f773", :job_meta=>{}}
119
+ I, [2019-11-22T09:20:09.320966 #49257] INFO -- [Cloudtasker][d76040a1-367e-4e3b-854e-e05a74d5f773] Job done: {:worker=>"DummyWorker", :job_id=>"d76040a1-367e-4e3b-854e-e05a74d5f773", :job_meta=>{}}
120
+ ```
121
+
122
+ That's it! Your job was picked up by the Cloudtasker local server and sent for processing to your Rails web process.
123
+
124
+ Now jump to the next section to configure your app to use Google Cloud Tasks as a backend.
125
+
126
+ ## Configuring Cloudtasker
127
+
128
+ ### Cloud Tasks authentication & permissions
129
+
130
+ The Google Cloud library authenticates via the Google Cloud SDK by default. If you do not have it setup then we recommend you [install it](https://cloud.google.com/sdk/docs/quickstarts).
131
+
132
+ Other options are available such as using a service account. You can see all authentication options in the [Google Cloud Authentication guide](https://github.com/googleapis/google-cloud-ruby/blob/master/google-cloud-bigquery/AUTHENTICATION.md).
133
+
134
+ In order to function properly Cloudtasker requires the authenticated account to have the following IAM permissions:
135
+ - `cloudtasks.tasks.get`
136
+ - `cloudtasks.tasks.create`
137
+ - `cloudtasks.tasks.delete`
138
+
139
+ To get started quickly you can add the `roles/cloudtasks.queueAdmin` role to your account via the [IAM Console](https://console.cloud.google.com/iam-admin/iam). This is not required if your account is a project admin account.
140
+
141
+
142
+ ### Cloudtasker initializer
143
+
144
+ The gem can be configured through an initializer. See below all the available configuration options.
145
+
146
+ ```ruby
147
+ # config/initializers/cloudtasker.rb
148
+
149
+ Cloudtasker.configure do |config|
150
+ #
151
+ # If you do not have any Rails secret_key_base defined, uncomment the following.
152
+ # This secret is used to authenticate jobs sent to the processing endpoint
153
+ # of your application.
154
+ #
155
+ # Default with Rails: Rails.application.credentials.secret_key_base
156
+ #
157
+ # config.secret = 'some-long-token'
158
+
159
+ #
160
+ # Specify the details of your Google Cloud Task queue.
161
+ #
162
+ # This not required in development using the Cloudtasker local server.
163
+ #
164
+ config.gcp_location_id = 'us-central1' # defaults to 'us-east1'
165
+ config.gcp_project_id = 'my-gcp-project'
166
+ config.gcp_queue_id = 'my-queue'
167
+
168
+ #
169
+ # Specify the publicly accessible host for your application
170
+ #
171
+ # > E.g. in development, using the cloudtasker local server
172
+ # config.processor_host = 'http://localhost:3000'
173
+ #
174
+ # > E.g. in development, using `config.mode = :production` and ngrok
175
+ # config.processor_host = 'https://111111.ngrok.io'
176
+ #
177
+ config.processor_host = 'https://app.mydomain.com'
178
+
179
+ #
180
+ # Specify the mode of operation:
181
+ # - :development => jobs will be pushed to Redis and picked up by the Cloudtasker local server
182
+ # - :production => jobs will be pushed to Google Cloud Tasks. Requires a publicly accessible domain.
183
+ #
184
+ # Defaults to :development unless CLOUDTASKER_ENV or RAILS_ENV or RACK_ENV is set to something else.
185
+ #
186
+ # config.mode = Rails.env.production? || Rails.env.my_other_env? ? :production : :development
187
+
188
+ #
189
+ # Specify the logger to use
190
+ #
191
+ # Default with Rails: Rails.logger
192
+ # Default without Rails: Logger.new(STDOUT)
193
+ #
194
+ # config.logger = MyLogger.new(STDOUT)
195
+
196
+ #
197
+ # Specify how many retries are allowed on jobs. This number of retries excludes any
198
+ # connectivity error that would be due to the application being down or unreachable.
199
+ #
200
+ # Default: 25
201
+ #
202
+ # config.max_retries = 10
24
203
 
25
- TODO: Write usage instructions here
204
+ #
205
+ # Specify the redis connection hash.
206
+ #
207
+ # This is ONLY required in development for the Cloudtasker local server and in
208
+ # all environments if you use any cloudtasker extension (unique jobs, cron jobs or batch jobs)
209
+ #
210
+ # See https://github.com/redis/redis-rb for examples of configuration hashes.
211
+ #
212
+ # Default: redis-rb connects to redis://127.0.0.1:6379/0
213
+ #
214
+ # config.redis = { url: 'redis://localhost:6379/5' }
215
+ end
216
+ ```
217
+
218
+ If your queue does not exist in Cloud Tasks you should [create it using the gcloud sdk](https://cloud.google.com/tasks/docs/creating-queues).
219
+
220
+ Alternatively with Rails you can simply run the following rake task if you have queue admin permissions (`cloudtasks.queues.get` and `cloudtasks.queues.create`).
221
+ ```bash
222
+ bundle exec rake cloudtasker:setup_queue
223
+ ```
224
+
225
+ ## Enqueuing jobs
226
+
227
+ Cloudtasker provides multiple ways of enqueuing jobs.
228
+
229
+ ```ruby
230
+ # Worker will be processed as soon as possible
231
+ MyWorker.perform_async(arg1, arg2)
232
+
233
+ # Worker will be processed in 5 minutes
234
+ MyWorker.perform_in(5 * 60, arg1, arg2)
235
+ # or with Rails
236
+ MyWorker.perform_in(5.minutes, arg1, arg2)
237
+
238
+ # Worker will be processed on specific date
239
+ MyWorker.perform_at(Time.parse('2025-01-01 00:50:00Z'), arg1, arg2)
240
+ # also with Rails
241
+ MyWorker.perform_at(3.days.from_now, arg1, arg2)
242
+ ```
243
+
244
+ Cloudtasker also provides a helper for re-enqueuing jobs. Re-enqueued jobs keep the same worker id. Some middlewares may rely on this to track the fact that that a job didn't actually complete (e.g. Cloustasker batch). This is optional and you can always fallback to using exception management (raise an error) to retry/re-enqueue jobs.
245
+
246
+ E.g.
247
+ ```ruby
248
+ # app/workers/fetch_resource_worker.rb
249
+
250
+ class FetchResourceWorker
251
+ include Cloudtasker::Worker
252
+
253
+ def perform(id)
254
+ # ...do some logic...
255
+ if some_condition
256
+ # Stop and re-enqueue the job to be run again in 10 seconds.
257
+ return reenqueue(10)
258
+ else
259
+ # ...keep going...
260
+ end
261
+ end
262
+ end
263
+ ```
264
+
265
+ ## Extensions
266
+ Cloudtasker comes with three optional features:
267
+ - Cron Jobs [[docs](docs/CRON_JOBS.md)]: Run jobs at fixed intervals.
268
+ - Batch Jobs [[docs](docs/BATCH_JOBS.md)]: Run jobs in jobs and track completion of the overall batch.
269
+ - Unique Jobs [[docs](docs/UNIQUE_JOBS.md)]: Ensure uniqueness of jobs based on job arguments.
270
+
271
+ ## Working locally
272
+
273
+ Cloudtasker pushes jobs to Google Cloud Tasks, which in turn sends jobs for processing to your application via HTTP POST requests to the `/cloudtasker/run` endpoint of the publicly accessible domain of your application.
274
+
275
+ When working locally on your application it is usually not possible to have a public domain. So what are the options?
276
+
277
+ ### Option 1: Cloudtasker local server
278
+ The Cloudtasker local server is a ruby daemon that looks for jobs pushed to Redis and sends them to your application via HTTP POST requests. The server mimics the way Google Cloud Tasks works, but locally!
279
+
280
+ You can configure your applicatiion to use the Cloudtasker local server using the following initializer:
281
+ ```ruby
282
+ # config/initializers/cloudtasker.rb
283
+
284
+ Cloudtasker.configure do |config|
285
+ # ... other options
286
+
287
+ # Push jobs to redis and let the Cloudtasker local server collect them
288
+ # This is the default mode unless CLOUDTASKER_ENV or RAILS_ENV or RACK_ENV is set
289
+ # to a non-development environment
290
+ config.mode = :development
291
+ end
292
+ ```
293
+
294
+ The Cloudtasker server can then be started using:
295
+ ```bash
296
+ cloudtasker
297
+ # or
298
+ bundle exec cloudtasker
299
+ ```
300
+
301
+ You can as well define a Procfile to manage the cloudtasker process via foreman. Then use `foreman start` to launch both your Rails server and the Cloudtasker local server.
302
+ ```yaml
303
+ # Procfile
304
+ web: rails s
305
+ worker: cloudtasker
306
+ ```
307
+
308
+ ### Option 2: Using ngrok
309
+
310
+ Want to test your application end to end with Google Cloud Task? Then [ngrok](https://ngrok.io) is the way to go.
311
+
312
+ First start your ngrok tunnel and take note of the :
313
+ ```bash
314
+ ngrok http 3000
315
+ ```
316
+
317
+ Take note of your ngrok domain and configure Cloudtasker to use Google Cloud Task in development via ngrok.
318
+ ```ruby
319
+ # config/initializers/cloudtasker.rb
320
+
321
+ Cloudtasker.configure do |config|
322
+ # Specify your Google Cloud Task queue configuration
323
+ # config.gcp_location_id = 'us-central1'
324
+ # config.gcp_project_id = 'my-gcp-project'
325
+ # config.gcp_queue_id = 'my-queue'
326
+
327
+ # Use your ngrok domain as the processor host
328
+ config.processor_host = 'https://your-tunnel-id.ngrok.io'
329
+
330
+ # Force Cloudtasker to use Google Cloud Tasks in development
331
+ config.mode = :production
332
+ end
333
+ ```
334
+
335
+ Finally start Rails to accept jobs from Google Cloud Tasks
336
+ ```bash
337
+ rails s
338
+ ```
339
+
340
+ ## Logging
341
+ There are several options available to configure logging and logging context.
342
+
343
+ ### Configuring a logger
344
+ Cloudtasker uses `Rails.logger` if Rails is available and falls back on a plain ruby logger `Logger.new(STDOUT)` if not.
345
+
346
+ It is also possible to configure your own logger. For example you can setup Cloudtasker with [semantic_logger](http://rocketjob.github.io/semantic_logger) by doing the following your initializer:
347
+ ```ruby
348
+ # config/initializers/cloudtasker.rb
349
+
350
+ Cloudtasker.configure do |config|
351
+ config.logger = SemanticLogger[Cloudtasker]
352
+ end
353
+ ```
354
+
355
+ ### Logging context
356
+ Cloudtasker provides worker contextual information to the worker `logger` method inside your worker methods.
357
+
358
+ For example:
359
+ ```ruby
360
+ # app/workers/dummy_worker.rb
361
+
362
+ class DummyWorker
363
+ include Cloudtasker::Worker
364
+
365
+ def perform(some_arg)
366
+ logger.info("Job run with #{some_arg}. This is working!")
367
+ end
368
+ end
369
+ ```
370
+
371
+ Will generate the following log with context `{:worker=> ..., :job_id=> ..., :job_meta=> ...}`
372
+ ```log
373
+ [Cloudtasker][d76040a1-367e-4e3b-854e-e05a74d5f773] Job run with foo. This is working!: {:worker=>"DummyWorker", :job_id=>"d76040a1-367e-4e3b-854e-e05a74d5f773", :job_meta=>{}}
374
+ ```
375
+
376
+ The way contextual information is displayed depends on the logger itself. For example with [semantic_logger](http://rocketjob.github.io/semantic_logger) contextual information might not appear in the log message but show up as payload data on the log entry itself (e.g. using the fluentd adapter).
377
+
378
+ Contextual information can be customised globally and locally using a log context_processor. By default the `Cloudtasker::WorkerLogger` is configured the following way:
379
+ ```ruby
380
+ Cloudtasker::WorkerLogger.log_context_processor = ->(worker) { worker.to_h.slice(:worker, :job_id, :job_meta) }
381
+ ```
382
+
383
+ You can decide to add a global identifier for your worker logs using the following:
384
+ ```ruby
385
+ # config/initializers/cloudtasker.rb
386
+
387
+ Cloudtasker::WorkerLogger.log_context_processor = lambda { |worker|
388
+ worker.to_h.slice(:worker, :job_id, :job_meta).merge(app: 'my-app')
389
+ }
390
+ ```
391
+
392
+ You could also decide to log all available context (including arguments passed to perform) for specific workers only:
393
+ ```ruby
394
+ # app/workers/full_context_worker.rb
395
+
396
+ class FullContextWorker
397
+ include Cloudtasker::Worker
398
+
399
+ cloudtasker_options log_context_processor: ->(worker) { worker.to_h }
400
+
401
+ def perform(some_arg)
402
+ logger.info("This log entry will have full context!")
403
+ end
404
+ end
405
+ ```
406
+
407
+ See the [Cloudtasker::Worker class](blob/master/lib/cloudtasker/worker.rb) for more information on attributes available to be logged in your `log_context_processor` proc.
408
+
409
+ ## Error Handling
410
+
411
+ Jobs failing will automatically return an HTTP error to Cloud Task and trigger a retry at a later time. The number of retries Cloud Task will do depends on the configuration of your queue in Cloud Tasks.
412
+
413
+ ### HTTP Error codes
414
+
415
+ Jobs failing will automatically return the following HTTP error code to Cloud Tasks, based on the actual reason:
416
+
417
+ | Code | Description |
418
+ |------|-------------|
419
+ | 205 | The job is dead and has been removed from the queue |
420
+ | 404 | The job has specified an incorrect worker class. |
421
+ | 422 | An error happened during the execution of the worker (`perform` method) |
422
+
423
+ ### Error callbacks
424
+
425
+ Workers can implement the `on_error(error)` and `on_dead(error)` callbacks to do things when a job fails during its execution:
426
+
427
+ E.g.
428
+ ```ruby
429
+ # app/workers/handle_error_worker.rb
430
+
431
+ class HandleErrorWorker
432
+ include Cloudtasker::Worker
433
+
434
+ def perform
435
+ raise(ArgumentError)
436
+ end
437
+
438
+ # The runtime error is passed as an argument.
439
+ def on_error(error)
440
+ logger.error("The following error happened: #{error}")
441
+ end
442
+
443
+ # The job has been retried too many times and will be removed
444
+ # from the queue.
445
+ def on_dead(error)
446
+ logger.error("The job died with the following error: #{error}")
447
+ end
448
+ end
449
+ ```
450
+
451
+ ### Max retries
452
+
453
+ By default jobs are retried 25 times - using an exponential backoff - before being declared dead. This number of retries can be customized locally on workers and/or globally via the Cloudtasker initializer.
454
+
455
+ Note that the number of retries set on your Cloud Task queue should be many times higher than the number of retries configured in Cloudtasker because Cloud Task also includes failures to connect to your application. Ideally set the number of retries to `unlimited` in Cloud Tasks.
456
+
457
+ E.g. Set max number of retries globally via the cloudtasker initializer.
458
+ ```ruby
459
+ # config/initializers/cloudtasker.rb
460
+
461
+ Cloudtasker.configure do |config|
462
+ #
463
+ # Specify how many retries are allowed on jobs. This number of retries excludes any
464
+ # connectivity error that would be due to the application being down or unreachable.
465
+ #
466
+ # Default: 25
467
+ #
468
+ config.max_retries = 10
469
+ end
470
+ ```
471
+
472
+ E.g. Set max number of retries to 3 on a given worker
473
+
474
+ E.g.
475
+ ```ruby
476
+ # app/workers/some_error_worker.rb
477
+
478
+ class SomeErrorWorker
479
+ include Cloudtasker::Worker
480
+
481
+ # This will override the global setting
482
+ cloudtasker_options max_retries: 3
483
+
484
+ def perform()
485
+ raise(ArgumentError)
486
+ end
487
+ end
488
+ ```
489
+
490
+
491
+
492
+ ## Best practices building workers
493
+
494
+ Below are recommendations and notes about creating workers.
495
+
496
+ ### Use primitive arguments
497
+ Pushing a job via `MyWorker.perform_async(arg1, arg2)` will serialize all arguments as JSON. Cloudtasker does not do any magic marshalling and therefore passing user-defined class instance as arguments is likely to make your jobs fail because of JSON serialization/deserialization.
498
+
499
+ When defining your worker `perform` method, use primitive arguments (integers, strings, hashes).
500
+
501
+ Don't do that:
502
+ ```ruby
503
+ # app/workers/user_email_worker.rb
504
+
505
+ class UserEmailWorker
506
+ include Cloudtasker::Worker
507
+
508
+ def perform(user)
509
+ user.reload.send_email
510
+ end
511
+ end
512
+ ```
513
+
514
+ Do that:
515
+ ```ruby
516
+ # app/workers/user_email_worker.rb
517
+
518
+ class UserEmailWorker
519
+ include Cloudtasker::Worker
520
+
521
+ def perform(user_id)
522
+ User.find_by(id: user_id)&.send_email
523
+ end
524
+ end
525
+ ```
526
+
527
+ ### Assume hash arguments are stringified
528
+ Because of JSON serialization/deserialization hashes passed to `perform_*` methods will eventually be passed as stringified hashes to the worker `perform` method.
529
+
530
+ ```ruby
531
+ # Enqueuing a job with:
532
+ MyWorker.perform_async({ foo: 'bar', 'baz' => { key: 'value' } })
533
+
534
+ # will be processed as
535
+ MyWorker.new.perform({ 'foo' => 'bar', 'baz' => { 'key' => 'value' } })
536
+ ```
537
+
538
+ ### Be careful with default arguments
539
+ Default arguments passed to the `perform` method are not actually considered as job arguments. Default arguments will therefore be ignored in contextual logging and by extensions relying on arguments such as the `unique-job` extension.
540
+
541
+ Consider the following worker:
542
+ ```ruby
543
+ # app/workers/user_email_worker.rb
544
+
545
+ class UserEmailWorker
546
+ include Cloudtasker::Worker
547
+
548
+ cloudtasker_options lock: :until_executed
549
+
550
+ def perform(user_id, time_at = Time.now.iso8601)
551
+ User.find_by(id: user_id)&.send_email(Time.parse(time_at))
552
+ end
553
+ end
554
+ ```
555
+
556
+ If you enqueue this worker by omitting the second argument `MyWorker.perform_async(123)` then:
557
+ - The `time_at` argument will not be included in contextual logging
558
+ - The `time_at` argument will be ignored by the `unique-job` extension, meaning that job uniqueness will be only based on the `user_id` argument.
559
+
560
+ ### Handling big job payloads
561
+ Keep in mind that jobs are pushed to Google Cloud Tasks via API and then delivered to your application via API as well. Therefore any excessive job payload will slow down the enqueuing of jobs and create additional processing when receiving the job.
562
+
563
+ If you feel that a job payload is going to get big, prefer to store the payload using a datastore (e.g. Redis) and pass a reference to the job to retrieve the payload inside your job `perform` method.
564
+
565
+ E.g. Define a job like this
566
+ ```ruby
567
+ # app/workers/big_payload_worker.rb
568
+
569
+ class BigPayloadWorker
570
+ include Cloudtasker::Worker
571
+
572
+ def perform(payload_id)
573
+ data = Rails.cache.fetch(payload_id)
574
+ # ...do some processing...
575
+ end
576
+ end
577
+ ```
578
+
579
+ Then enqueue your job like this:
580
+ ```ruby
581
+ # Fetch and store the payload
582
+ data = ApiClient.fetch_thousands_of_records
583
+ payload_id = SecureRandom.uuid
584
+ Rails.cache.write(payload_id, data)
585
+
586
+ # Enqueue the processing job
587
+ BigPayloadWorker.perform_async(payload_id)
588
+ ```
26
589
 
27
590
  ## Development
28
591
 
@@ -32,7 +595,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
32
595
 
33
596
  ## Contributing
34
597
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/alachaum/cloudtasker. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
598
+ Bug reports and pull requests are welcome on GitHub at https://github.com/keypup-io/cloudtasker. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
36
599
 
37
600
  ## License
38
601
 
@@ -40,4 +603,8 @@ The gem is available as open source under the terms of the [MIT License](https:/
40
603
 
41
604
  ## Code of Conduct
42
605
 
43
- Everyone interacting in the Cloudtasker project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/alachaum/cloudtasker/blob/master/CODE_OF_CONDUCT.md).
606
+ Everyone interacting in the Cloudtasker project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/keypup-io/cloudtasker/blob/master/CODE_OF_CONDUCT.md).
607
+
608
+ ## Author
609
+
610
+ Provided with :heart: by [keypup.io](https://keypup.io/)