├── .github
└── workflows
│ ├── lint_rubocop.yml
│ ├── test_ruby_2.7.yml
│ └── test_ruby_3.x.yml
├── .gitignore
├── .rspec
├── .rubocop.yml
├── Appraisals
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── _config.yml
├── app
└── controllers
│ └── cloudtasker
│ └── worker_controller.rb
├── bin
├── console
└── setup
├── cloudtasker.gemspec
├── config
└── routes.rb
├── docs
├── BATCH_JOBS.md
├── CRON_JOBS.md
├── STORABLE_JOBS.md
└── UNIQUE_JOBS.md
├── examples
├── cloud-run
│ ├── .ruby-version
│ ├── Dockerfile
│ ├── Gemfile
│ ├── Gemfile.lock
│ ├── README.md
│ ├── Rakefile
│ ├── app
│ │ ├── assets
│ │ │ ├── config
│ │ │ │ └── manifest.js
│ │ │ ├── images
│ │ │ │ └── .keep
│ │ │ └── stylesheets
│ │ │ │ └── application.css
│ │ ├── channels
│ │ │ └── application_cable
│ │ │ │ ├── channel.rb
│ │ │ │ └── connection.rb
│ │ ├── controllers
│ │ │ ├── application_controller.rb
│ │ │ ├── concerns
│ │ │ │ └── .keep
│ │ │ └── enqueue_job_controller.rb
│ │ ├── helpers
│ │ │ └── application_helper.rb
│ │ ├── javascript
│ │ │ └── packs
│ │ │ │ └── application.js
│ │ ├── jobs
│ │ │ ├── application_job.rb
│ │ │ └── example_job.rb
│ │ ├── mailers
│ │ │ └── application_mailer.rb
│ │ ├── models
│ │ │ ├── application_record.rb
│ │ │ └── concerns
│ │ │ │ └── .keep
│ │ ├── views
│ │ │ └── layouts
│ │ │ │ ├── application.html.erb
│ │ │ │ ├── mailer.html.erb
│ │ │ │ └── mailer.text.erb
│ │ └── workers
│ │ │ └── dummy_worker.rb
│ ├── bin
│ │ ├── rails
│ │ ├── rake
│ │ └── setup
│ ├── config.ru
│ ├── config
│ │ ├── application.rb
│ │ ├── boot.rb
│ │ ├── cable.yml
│ │ ├── credentials.yml.enc
│ │ ├── database.yml
│ │ ├── environment.rb
│ │ ├── environments
│ │ │ ├── development.rb
│ │ │ ├── production.rb
│ │ │ └── test.rb
│ │ ├── initializers
│ │ │ ├── application_controller_renderer.rb
│ │ │ ├── assets.rb
│ │ │ ├── backtrace_silencers.rb
│ │ │ ├── cloudtasker.rb
│ │ │ ├── content_security_policy.rb
│ │ │ ├── cookies_serializer.rb
│ │ │ ├── filter_parameter_logging.rb
│ │ │ ├── inflections.rb
│ │ │ ├── mime_types.rb
│ │ │ └── wrap_parameters.rb
│ │ ├── locales
│ │ │ └── en.yml
│ │ ├── master.key
│ │ ├── puma.rb
│ │ ├── routes.rb
│ │ ├── spring.rb
│ │ └── storage.yml
│ ├── db
│ │ ├── development.sqlite3
│ │ └── test.sqlite3
│ ├── lib
│ │ └── assets
│ │ │ └── .keep
│ ├── log
│ │ ├── .keep
│ │ └── development.log
│ ├── public
│ │ ├── 404.html
│ │ ├── 422.html
│ │ ├── 500.html
│ │ ├── apple-touch-icon-precomposed.png
│ │ ├── apple-touch-icon.png
│ │ └── favicon.ico
│ ├── storage
│ │ └── .keep
│ └── tmp
│ │ ├── .keep
│ │ ├── development_secret.txt
│ │ └── restart.txt
├── rails
│ ├── .ruby-version
│ ├── Gemfile
│ ├── Gemfile.lock
│ ├── Procfile
│ ├── README.md
│ ├── Rakefile
│ ├── app
│ │ ├── assets
│ │ │ ├── config
│ │ │ │ └── manifest.js
│ │ │ ├── images
│ │ │ │ └── .keep
│ │ │ └── stylesheets
│ │ │ │ └── application.css
│ │ ├── channels
│ │ │ └── application_cable
│ │ │ │ ├── channel.rb
│ │ │ │ └── connection.rb
│ │ ├── controllers
│ │ │ ├── application_controller.rb
│ │ │ ├── concerns
│ │ │ │ └── .keep
│ │ │ └── enqueue_job_controller.rb
│ │ ├── helpers
│ │ │ └── application_helper.rb
│ │ ├── javascript
│ │ │ └── packs
│ │ │ │ └── application.js
│ │ ├── jobs
│ │ │ ├── application_job.rb
│ │ │ └── example_job.rb
│ │ ├── mailers
│ │ │ └── application_mailer.rb
│ │ ├── models
│ │ │ ├── application_record.rb
│ │ │ └── concerns
│ │ │ │ └── .keep
│ │ ├── views
│ │ │ └── layouts
│ │ │ │ ├── application.html.erb
│ │ │ │ ├── mailer.html.erb
│ │ │ │ └── mailer.text.erb
│ │ └── workers
│ │ │ ├── batch_worker.rb
│ │ │ ├── critical_worker.rb
│ │ │ ├── cron_worker.rb
│ │ │ ├── dummy_worker.rb
│ │ │ └── uniq_executing_worker.rb
│ ├── bin
│ │ ├── rails
│ │ ├── rake
│ │ └── setup
│ ├── config.ru
│ ├── config
│ │ ├── application.rb
│ │ ├── boot.rb
│ │ ├── cable.yml
│ │ ├── credentials.yml.enc
│ │ ├── database.yml
│ │ ├── environment.rb
│ │ ├── environments
│ │ │ ├── development.rb
│ │ │ ├── production.rb
│ │ │ └── test.rb
│ │ ├── initializers
│ │ │ ├── application_controller_renderer.rb
│ │ │ ├── backtrace_silencers.rb
│ │ │ ├── cloudtasker.rb
│ │ │ ├── content_security_policy.rb
│ │ │ ├── cookies_serializer.rb
│ │ │ ├── filter_parameter_logging.rb
│ │ │ ├── inflections.rb
│ │ │ ├── mime_types.rb
│ │ │ └── wrap_parameters.rb
│ │ ├── locales
│ │ │ └── en.yml
│ │ ├── master.key
│ │ ├── puma.rb
│ │ ├── routes.rb
│ │ ├── spring.rb
│ │ └── storage.yml
│ ├── lib
│ │ └── assets
│ │ │ └── .keep
│ ├── log
│ │ └── .keep
│ ├── public
│ │ ├── 404.html
│ │ ├── 422.html
│ │ ├── 500.html
│ │ ├── apple-touch-icon-precomposed.png
│ │ ├── apple-touch-icon.png
│ │ └── favicon.ico
│ ├── storage
│ │ └── .keep
│ └── tmp
│ │ ├── .keep
│ │ └── development_secret.txt
└── sinatra
│ ├── .ruby-version
│ ├── Gemfile
│ ├── Gemfile.lock
│ ├── Procfile
│ ├── README.md
│ ├── app.rb
│ ├── app
│ └── workers
│ │ ├── batch_worker.rb
│ │ ├── cron_worker.rb
│ │ ├── dummy_worker.rb
│ │ └── uniq_executing_worker.rb
│ ├── bin
│ └── console
│ └── config
│ └── initializers
│ └── cloudtasker.rb
├── exe
└── cloudtasker
├── gemfiles
├── .bundle
│ └── config
├── google_cloud_tasks_1.0.gemfile
├── google_cloud_tasks_1.1.gemfile
├── google_cloud_tasks_1.2.gemfile
├── google_cloud_tasks_1.3.gemfile
├── google_cloud_tasks_1.4.gemfile
├── google_cloud_tasks_1.5.gemfile
├── google_cloud_tasks_2.0.gemfile
├── google_cloud_tasks_2.1.gemfile
├── rails_5.2.gemfile
├── rails_6.0.gemfile
├── rails_6.1.gemfile
├── rails_7.0.gemfile
├── rails_7.1.gemfile
├── semantic_logger_3.4.gemfile
├── semantic_logger_4.6.gemfile
├── semantic_logger_4.7.0.gemfile
└── semantic_logger_4.7.2.gemfile
├── lib
├── active_job
│ └── queue_adapters
│ │ └── cloudtasker_adapter.rb
├── cloudtasker.rb
├── cloudtasker
│ ├── authentication_error.rb
│ ├── authenticator.rb
│ ├── backend
│ │ ├── google_cloud_task_v1.rb
│ │ ├── google_cloud_task_v2.rb
│ │ ├── memory_task.rb
│ │ └── redis_task.rb
│ ├── batch.rb
│ ├── batch
│ │ ├── batch_progress.rb
│ │ ├── extension
│ │ │ └── worker.rb
│ │ ├── job.rb
│ │ ├── middleware.rb
│ │ └── middleware
│ │ │ └── server.rb
│ ├── cli.rb
│ ├── cloud_task.rb
│ ├── config.rb
│ ├── cron.rb
│ ├── cron
│ │ ├── job.rb
│ │ ├── middleware.rb
│ │ ├── middleware
│ │ │ └── server.rb
│ │ └── schedule.rb
│ ├── dead_worker_error.rb
│ ├── engine.rb
│ ├── invalid_worker_error.rb
│ ├── local_server.rb
│ ├── max_task_size_exceeded_error.rb
│ ├── meta_store.rb
│ ├── middleware
│ │ └── chain.rb
│ ├── missing_worker_arguments_error.rb
│ ├── redis_client.rb
│ ├── retry_worker_error.rb
│ ├── storable.rb
│ ├── storable
│ │ └── worker.rb
│ ├── testing.rb
│ ├── unique_job.rb
│ ├── unique_job
│ │ ├── conflict_strategy
│ │ │ ├── base_strategy.rb
│ │ │ ├── raise.rb
│ │ │ ├── reject.rb
│ │ │ └── reschedule.rb
│ │ ├── job.rb
│ │ ├── lock
│ │ │ ├── base_lock.rb
│ │ │ ├── no_op.rb
│ │ │ ├── until_executed.rb
│ │ │ ├── until_executing.rb
│ │ │ └── while_executing.rb
│ │ ├── lock_error.rb
│ │ ├── middleware.rb
│ │ └── middleware
│ │ │ ├── client.rb
│ │ │ └── server.rb
│ ├── version.rb
│ ├── worker.rb
│ ├── worker_handler.rb
│ ├── worker_logger.rb
│ └── worker_wrapper.rb
└── tasks
│ └── setup_queue.rake
└── spec
├── active_job
└── queue_adapters
│ ├── cloudtasker_adapter
│ └── job_wrapper_spec.rb
│ └── cloudtasker_adapter_spec.rb
├── cloudtasker
├── authenticator_spec.rb
├── backend
│ ├── google_cloud_task_v1_spec.rb
│ ├── google_cloud_task_v2_spec.rb
│ ├── memory_task_spec.rb
│ └── redis_task_spec.rb
├── batch
│ ├── batch_progress_spec.rb
│ ├── extension
│ │ └── worker_spec.rb
│ ├── job_spec.rb
│ ├── middleware
│ │ └── server_spec.rb
│ └── middleware_spec.rb
├── cloud_task_spec.rb
├── config_spec.rb
├── cron
│ ├── job_spec.rb
│ ├── middleware
│ │ └── server_spec.rb
│ ├── middleware_spec.rb
│ └── schedule_spec.rb
├── meta_store_spec.rb
├── middleware
│ └── chain_spec.rb
├── redis_client_spec.rb
├── storable
│ └── worker_spec.rb
├── testing_spec.rb
├── unique_job
│ ├── conflict_strategy
│ │ ├── raise_spec.rb
│ │ ├── reject_spec.rb
│ │ └── reschedule_spec.rb
│ ├── job_spec.rb
│ ├── lock
│ │ ├── no_op_spec.rb
│ │ ├── until_executed_spec.rb
│ │ ├── until_executing_spec.rb
│ │ └── while_executing_spec.rb
│ ├── middleware
│ │ ├── client_spec.rb
│ │ └── server_spec.rb
│ └── middleware_spec.rb
├── worker_controller_spec.rb
├── worker_handler_spec.rb
├── worker_logger_spec.rb
├── worker_spec.rb
└── worker_wrapper_spec.rb
├── cloudtasker_spec.rb
├── dummy
└── config
│ ├── application.rb
│ ├── boot.rb
│ └── environment.rb
├── integration
├── active_job_spec.rb
└── batch_worker_spec.rb
├── shared
├── active_job
│ └── instantiation_context.rb
└── cloudtasker
│ └── unique_job
│ ├── conflict_strategy
│ └── base_strategy.rb
│ └── lock
│ └── base_lock.rb
├── spec_helper.rb
└── support
├── dead_batch_worker.rb
├── test_batch_worker.rb
├── test_middleware.rb
├── test_middleware2.rb
├── test_middleware3.rb
├── test_non_worker.rb
├── test_storable_worker.rb
├── test_worker.rb
└── test_worker2.rb
/.github/workflows/lint_rubocop.yml:
--------------------------------------------------------------------------------
1 | name: Rubocop
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v2
10 | - uses: zhulik/redis-action@1.1.0
11 | - uses: ruby/setup-ruby@v1
12 | with:
13 | ruby-version: '3.3.0'
14 | bundler-cache: true
15 | - run: bundle exec rubocop
16 |
--------------------------------------------------------------------------------
/.github/workflows/test_ruby_2.7.yml:
--------------------------------------------------------------------------------
1 | name: Ruby 2.7
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | matrix:
10 | ruby:
11 | - '2.7'
12 | appraisal:
13 | - 'google_cloud_tasks_1.0'
14 | - 'google_cloud_tasks_1.1'
15 | - 'google_cloud_tasks_1.2'
16 | - 'google_cloud_tasks_1.3'
17 | - 'google_cloud_tasks_1.4'
18 | - 'google_cloud_tasks_1.5'
19 | - 'google_cloud_tasks_2.0'
20 | - 'google_cloud_tasks_2.1'
21 | - 'rails_5.2'
22 | - 'rails_6.0'
23 | - 'rails_6.1'
24 | - 'rails_7.0'
25 | - 'semantic_logger_3.4'
26 | - 'semantic_logger_4.6'
27 | - 'semantic_logger_4.7.0'
28 | - 'semantic_logger_4.7.2'
29 | env:
30 | BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.appraisal }}.gemfile
31 | steps:
32 | - uses: actions/checkout@v2
33 | - uses: zhulik/redis-action@1.1.0
34 | - uses: ruby/setup-ruby@v1
35 | with:
36 | ruby-version: ${{ matrix.ruby }}
37 | bundler-cache: true
38 | - run: bundle exec rspec
39 |
--------------------------------------------------------------------------------
/.github/workflows/test_ruby_3.x.yml:
--------------------------------------------------------------------------------
1 | name: Ruby 3.x
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | matrix:
10 | ruby:
11 | - '3.0'
12 | - '3.1'
13 | - '3.2'
14 | - '3.3'
15 | appraisal:
16 | - 'google_cloud_tasks_1.0'
17 | - 'google_cloud_tasks_1.1'
18 | - 'google_cloud_tasks_1.2'
19 | - 'google_cloud_tasks_1.3'
20 | - 'google_cloud_tasks_1.4'
21 | - 'google_cloud_tasks_1.5'
22 | - 'google_cloud_tasks_2.0'
23 | - 'google_cloud_tasks_2.1'
24 | - 'rails_6.1'
25 | - 'rails_7.0'
26 | - 'semantic_logger_3.4'
27 | - 'semantic_logger_4.6'
28 | - 'semantic_logger_4.7.0'
29 | - 'semantic_logger_4.7.2'
30 | env:
31 | BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.appraisal }}.gemfile
32 | steps:
33 | - uses: actions/checkout@v2
34 | - uses: zhulik/redis-action@1.1.0
35 | - uses: ruby/setup-ruby@v1
36 | with:
37 | ruby-version: ${{ matrix.ruby }}
38 | bundler-cache: true
39 | - run: bundle exec rspec
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.yardoc
3 | /_yardoc/
4 | /coverage/
5 | /doc/
6 | /examples/rails/log/*.log
7 | /examples/rails/tmp/
8 | /gemfiles/*.gemfile.lock
9 | /log/
10 | /pkg/
11 | /spec/reports/
12 | /spec/dummy/log/
13 | /spec/dummy/tmp/
14 | /tmp/
15 |
16 | # Ignore lock files (e.g. Gemfile.lock)
17 | /Gemfile.lock
18 |
19 | # rspec failure tracking
20 | .rspec_status
21 |
22 | # Dev databases
23 | *.sqlite3
24 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --format documentation
2 | --color
3 | --require spec_helper
4 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | require: rubocop-rspec
2 |
3 | AllCops:
4 | NewCops: enable
5 | SuggestExtensions: false
6 | TargetRubyVersion: 2.7
7 | Exclude:
8 | - 'gemfiles/**/*'
9 | - 'vendor/**/*'
10 |
11 | Metrics/ClassLength:
12 | Max: 300
13 |
14 | Metrics/ModuleLength:
15 | Max: 150
16 |
17 | Metrics/AbcSize:
18 | Max: 30
19 | Exclude:
20 | - 'spec/support/*'
21 |
22 | Metrics/PerceivedComplexity:
23 | Max: 20
24 |
25 | Layout/LineLength:
26 | Max: 120
27 |
28 | Metrics/MethodLength:
29 | Max: 20
30 |
31 | RSpec/DescribeClass:
32 | Exclude:
33 | - 'spec/integration/**/*_spec.rb'
34 |
35 | RSpec/ExpectInHook:
36 | Enabled: false
37 |
38 | RSpec/EmptyLineAfterHook:
39 | Enabled: false
40 |
41 | RSpec/ScatteredSetup:
42 | Enabled: false
43 |
44 | Metrics/BlockLength:
45 | Exclude:
46 | - cloudtasker.gemspec
47 | - 'spec/**/*'
48 |
49 | Style/Documentation:
50 | Exclude:
51 | - 'examples/**/*'
52 | - 'spec/**/*'
53 |
54 | Metrics/ParameterLists:
55 | CountKeywordArgs: false
56 |
57 | Metrics/CyclomaticComplexity:
58 | Max: 15
59 |
60 | Lint/EmptyBlock:
61 | Exclude:
62 | - 'examples/rails/config/routes.rb'
63 |
64 | RSpec/MessageSpies:
65 | Enabled: false
66 |
67 | RSpec/MultipleExpectations:
68 | Exclude:
69 | - 'examples/**/*'
70 | - 'spec/integration/**/*'
71 |
72 | RSpec/AnyInstance:
73 | Enabled: false
74 |
75 | RSpec/MultipleMemoizedHelpers:
76 | Enabled: false
77 |
78 | RSpec/NoExpectationExample:
79 | AllowedPatterns:
80 | - ^expect_
81 | - ^assert_
82 |
83 | RSpec/IndexedLet:
84 | Enabled: false
85 |
86 | RSpec/StubbedMock:
87 | Enabled: false
88 |
89 | RSpec/VerifiedDoubles:
90 | Exclude:
91 | - spec/cloudtasker/cloud_task_spec.rb
92 | - spec/cloudtasker/backend/google_cloud_task_v1_spec.rb
93 | - spec/cloudtasker/backend/google_cloud_task_v2_spec.rb
--------------------------------------------------------------------------------
/Appraisals:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | appraise 'google_cloud_tasks_1.0' do
4 | gem 'google-cloud-tasks', '~> 1.0.0'
5 | end
6 |
7 | appraise 'google_cloud_tasks_1.1' do
8 | gem 'google-cloud-tasks', '~> 1.1.0'
9 | end
10 |
11 | appraise 'google_cloud_tasks_1.2' do
12 | gem 'google-cloud-tasks', '~> 1.2.0'
13 | end
14 |
15 | appraise 'google_cloud_tasks_1.3' do
16 | gem 'google-cloud-tasks', '~> 1.3.0'
17 | end
18 |
19 | appraise 'google_cloud_tasks_1.4' do
20 | gem 'google-cloud-tasks', '~> 1.4.0'
21 | end
22 |
23 | appraise 'google_cloud_tasks_1.5' do
24 | gem 'google-cloud-tasks', '~> 1.5.0'
25 | end
26 |
27 | appraise 'google_cloud_tasks_2.0' do
28 | gem 'google-cloud-tasks', '~> 2.0.0'
29 | end
30 |
31 | appraise 'google_cloud_tasks_2.1' do
32 | gem 'google-cloud-tasks', '~> 2.1.0'
33 | end
34 |
35 | if RUBY_VERSION < '3'
36 | appraise 'rails_5.2' do
37 | gem 'rails', '~> 5.2.0'
38 | gem 'rspec-rails'
39 | end
40 |
41 | appraise 'rails_6.0' do
42 | gem 'rails', '~> 6.0.0'
43 | gem 'rspec-rails'
44 | end
45 | end
46 |
47 | appraise 'rails_6.1' do
48 | gem 'rails', '~> 6.1.0'
49 | gem 'rspec-rails'
50 | end
51 |
52 | if RUBY_VERSION >= '2.7'
53 | appraise 'rails_7.0' do
54 | gem 'rails', '~> 7.0.0'
55 | gem 'rspec-rails'
56 | end
57 |
58 | appraise 'rails_7.1' do
59 | gem 'rails', '~> 7.1'
60 | gem 'rspec-rails'
61 | end
62 | end
63 |
64 | appraise 'semantic_logger_3.4' do
65 | gem 'semantic_logger', '3.4.1'
66 | end
67 |
68 | appraise 'semantic_logger_4.6' do
69 | gem 'semantic_logger', '4.6.1'
70 | end
71 |
72 | appraise 'semantic_logger_4.7.0' do
73 | gem 'semantic_logger', '4.7.0'
74 | end
75 |
76 | appraise 'semantic_logger_4.7.2' do
77 | gem 'semantic_logger', '4.7.2'
78 | end
79 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at arnaud.lachaume@maestrano.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at [http://contributor-covenant.org/version/1/4][version]
72 |
73 | [homepage]: http://contributor-covenant.org
74 | [version]: http://contributor-covenant.org/version/1/4/
75 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source 'https://rubygems.org'
4 |
5 | # Specify your gem's dependencies in cloudtasker.gemspec
6 | gemspec
7 |
8 | # Dev dependencies
9 | gem 'appraisal', github: 'thoughtbot/appraisal'
10 | gem 'bundler', '~> 2.0'
11 | gem 'rake', '>= 12.3.3'
12 | gem 'rspec', '~> 3.0'
13 | gem 'rspec-json_expectations', '~> 2.2'
14 | gem 'rubocop', '~> 1.64.1'
15 | gem 'rubocop-rspec', '~> 3.0.1'
16 | gem 'semantic_logger'
17 | gem 'timecop'
18 | gem 'webmock'
19 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2019 Arnaud Lachaume
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'bundler/gem_tasks'
4 | require 'rspec/core/rake_task'
5 |
6 | RSpec::Core::RakeTask.new(:spec)
7 |
8 | task default: :spec
9 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-slate
--------------------------------------------------------------------------------
/app/controllers/cloudtasker/worker_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Cloudtasker
4 | # Handle execution of workers
5 | class WorkerController < ActionController::Base
6 | # No need for CSRF verification on API endpoints
7 | skip_forgery_protection
8 |
9 | # Authenticate all requests.
10 | before_action :authenticate!
11 |
12 | # Return 401 when API Token is invalid
13 | rescue_from AuthenticationError do
14 | head :unauthorized
15 | end
16 |
17 | # POST /cloudtasker/run
18 | #
19 | # Run a worker from a Cloud Task payload
20 | #
21 | def run
22 | # Process payload
23 | WorkerHandler.execute_from_payload!(payload)
24 | head :no_content
25 | rescue DeadWorkerError
26 | # 205: job will NOT be retried
27 | head :reset_content
28 | rescue InvalidWorkerError
29 | # 404: Job will be retried
30 | head :not_found
31 | rescue StandardError
32 | # 422: Job will be retried
33 | head :unprocessable_entity
34 | end
35 |
36 | private
37 |
38 | #
39 | # Parse the request body and return the JSON payload
40 | #
41 | # @return [String] The JSON payload
42 | #
43 | def json_payload
44 | @json_payload ||= begin
45 | # Get raw body
46 | content = request.body.read
47 |
48 | # Decode content if the body is Base64 encoded
49 | if request.headers[Cloudtasker::Config::ENCODING_HEADER].to_s.downcase == 'base64'
50 | content = Base64.decode64(content)
51 | end
52 |
53 | # Return the content
54 | content
55 | end
56 | end
57 |
58 | #
59 | # Parse the request body and return the actual job
60 | # payload.
61 | #
62 | # @return [Hash] The job payload
63 | #
64 | def payload
65 | # Return content parsed as JSON and add job retries count
66 | @payload ||= JSON.parse(json_payload).merge(job_retries: job_retries, task_id: task_id)
67 | end
68 |
69 | #
70 | # Extract the number of times this task failed at runtime.
71 | #
72 | # @return [Integer] The number of failures.
73 | #
74 | def job_retries
75 | request.headers[Cloudtasker::Config::RETRY_HEADER].to_i
76 | end
77 |
78 | #
79 | # Return the Google Cloud Task ID from headers.
80 | #
81 | # @return [String] The task ID.
82 | #
83 | def task_id
84 | request.headers[Cloudtasker::Config::TASK_ID_HEADER]
85 | end
86 |
87 | #
88 | # Authenticate incoming requests using a bearer token
89 | #
90 | # See Cloudtasker::Authenticator#verification_token
91 | #
92 | def authenticate!
93 | if (signature = request.headers[Cloudtasker::Config::CT_SIGNATURE_HEADER])
94 | # Verify content signature
95 | Authenticator.verify_signature!(signature, json_payload)
96 | else
97 | # Get authorization token from custom header (since v0.14.0) or fallback to
98 | # former authorization header (jobs enqueued by v0.13 and below)
99 | bearer_token = request.headers[Cloudtasker::Config::CT_AUTHORIZATION_HEADER].to_s.split.last ||
100 | request.headers[Cloudtasker::Config::OIDC_AUTHORIZATION_HEADER].to_s.split.last
101 |
102 | # Verify the token
103 | Authenticator.verify!(bearer_token)
104 | end
105 | end
106 | end
107 | end
108 |
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require 'bundler/setup'
5 | require 'cloudtasker'
6 |
7 | # You can add fixtures and/or initialization code here to make experimenting
8 | # with your gem easier. You can also use a different console, if you like.
9 |
10 | # (If you use this, don't forget to add pry to your Gemfile!)
11 | # require "pry"
12 | # Pry.start
13 |
14 | require 'irb'
15 | IRB.start(__FILE__)
16 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 | IFS=$'\n\t'
4 | set -vx
5 |
6 | bundle install
7 |
8 | # Do any other automated setup that you need to do here
9 |
--------------------------------------------------------------------------------
/cloudtasker.gemspec:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | lib = File.expand_path('lib', __dir__)
4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5 | require 'cloudtasker/version'
6 |
7 | Gem::Specification.new do |spec|
8 | spec.name = 'cloudtasker'
9 | spec.version = Cloudtasker::VERSION
10 | spec.authors = ['Arnaud Lachaume']
11 | spec.email = ['arnaud.lachaume@keypup.io']
12 |
13 | spec.summary = 'Background jobs for Ruby using Google Cloud Tasks (beta)'
14 | spec.description = 'Background jobs for Ruby using Google Cloud Tasks (beta)'
15 | spec.homepage = 'https://github.com/keypup-io/cloudtasker'
16 | spec.license = 'MIT'
17 |
18 | spec.metadata['homepage_uri'] = spec.homepage
19 | spec.metadata['source_code_uri'] = 'https://github.com/keypup-io/cloudtasker'
20 | spec.metadata['changelog_uri'] = 'https://github.com/keypup-io/cloudtasker/master/tree/CHANGELOG.md'
21 |
22 | # Specify which files should be added to the gem when it is released.
23 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24 | spec.files = Dir.chdir(File.expand_path(__dir__)) do
25 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(examples|test|spec|features)/}) }
26 | end
27 | spec.bindir = 'exe'
28 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
29 | spec.require_paths = ['lib']
30 |
31 | spec.required_ruby_version = '>= 2.7.0'
32 |
33 | spec.add_dependency 'activesupport'
34 | spec.add_dependency 'connection_pool'
35 | spec.add_dependency 'fugit'
36 | spec.add_dependency 'google-cloud-tasks'
37 | spec.add_dependency 'jwt'
38 | spec.add_dependency 'redis'
39 | spec.add_dependency 'retriable'
40 |
41 | spec.metadata['rubygems_mfa_required'] = 'true'
42 | end
43 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Cloudtasker::Engine.routes.draw do
4 | post '/run', to: 'worker#run'
5 | end
6 |
--------------------------------------------------------------------------------
/docs/STORABLE_JOBS.md:
--------------------------------------------------------------------------------
1 | # Cloudtasker Storable Jobs
2 |
3 | **Supported since**: `v0.14.0`
4 | **Note**: this extension requires redis
5 |
6 | The Cloudtasker storage extension allows you to park jobs in a specific garage lane and enqueue (pull) them when specific conditions have been met.
7 |
8 | This extension is useful when you need to prepare some jobs (e.g. you are retrieving data from an API and must process some of it asynchronously) but only process them when some programmatic conditions have been met (e.g. a series of preliminary preparation jobs have run successfully). Using parked jobs is a leaner (and cheaper) approach than using guard logic in the `perform` method to re-enqueue a job until a set of conditions is satisfied. The latter tends to generate a lot of jobs/logs pollution.
9 |
10 | ## Configuration
11 |
12 | You can enable storable jobs by adding the following to your cloudtasker initializer:
13 | ```ruby
14 | # The storable extension is optional and must be explicitly required
15 | require 'cloudtasker/storable'
16 |
17 | Cloudtasker.configure do |config|
18 | # Specify your redis url.
19 | # Defaults to `redis://localhost:6379/0` if unspecified
20 | config.redis = { url: 'redis://some-host:6379/0' }
21 | end
22 | ```
23 |
24 | Then you can make workers storable by including the `Cloudtasker::Storable::Worker` concern into your workers:
25 | ```ruby
26 | class MyWorker
27 | include Cloudtasker::Worker
28 | include Cloudtasker::Storable::Worker
29 |
30 | def perform(...)
31 | # Do stuff
32 | end
33 | end
34 | ```
35 |
36 | ## Parking jobs
37 | You can park jobs to a specific garage lane using the `push_to_store(store_name, *worker_args)` class method:
38 | ```ruby
39 | MyWorker.push_to_store('some-customer-reference:some-task-group', job_arg1, job_arg2)
40 | ```
41 |
42 | ## Pulling jobs
43 | You can pull and enqueue jobs using the `pull_all_from_store(store_name)` class method:
44 | ```ruby
45 | MyWorker.pull_all_from_store('some-customer-reference:some-task-group')
46 | ```
47 |
48 | If you need to enqueue jobs with specific options or using any special means, you can call `pull_all_from_store(store_name)` with a block. When a block is passed the method yield each worker's set of arguments.
49 | ```ruby
50 | # Delay the enqueuing of parked jobs by 30 seconds
51 | MyWorker.pull_all_from_store('some-customer-reference:some-task-group') do |args|
52 | MyWorker.perform_in(30, *args)
53 | end
54 |
55 | # Enqueue parked jobs on a specific queue, with a 10s delay
56 | MyWorker.pull_all_from_store('some-customer-reference:some-task-group') do |args|
57 | MyWorker.schedule(args: args, time_in: 10, queue: 'critical')
58 | end
59 |
60 | # Enqueue parked jobs as part of a job's current batch (the logic below assumes
61 | # we are inside a job's `perform` method)
62 | MyWorker.pull_all_from_store('some-customer-reference:some-task-group') do |args|
63 | batch.add(MyWorker, *args)
64 |
65 | # Or with a specific queue
66 | # batch.add_to_queue('critical', SubWorker, *args)
67 | end
68 | ```
69 |
--------------------------------------------------------------------------------
/examples/cloud-run/.ruby-version:
--------------------------------------------------------------------------------
1 | ruby-3.3.0
2 |
--------------------------------------------------------------------------------
/examples/cloud-run/Dockerfile:
--------------------------------------------------------------------------------
1 | # Use the official lightweight Ruby image.
2 | # https://hub.docker.com/_/ruby
3 | FROM ruby:3.2.1
4 |
5 | # Install bundler
6 | RUN gem update --system
7 | RUN gem install bundler
8 |
9 | # Install production dependencies.
10 | WORKDIR /usr/src/app
11 | COPY Gemfile Gemfile.lock ./
12 | RUN bundle install
13 | ENV BUNDLE_FROZEN=true
14 |
15 | # Copy local code to the container image.
16 | COPY . ./
17 |
18 | # Environment
19 | ENV RAILS_ENV production
20 | ENV RAILS_MAX_THREADS 60
21 | ENV RAILS_LOG_TO_STDOUT true
22 |
23 | # Run the web service on container startup.
24 | CMD ["bundle", "exec", "rails", "s", "-p", "8080"]
--------------------------------------------------------------------------------
/examples/cloud-run/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source 'https://rubygems.org'
4 | git_source(:github) { |repo| "https://github.com/#{repo}.git" }
5 |
6 | ruby '~> 3.3'
7 |
8 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
9 | gem 'rails', '~> 7.1'
10 |
11 | # Application server
12 | gem 'puma'
13 |
14 | # Background jobs via Cloud Tasks
15 | gem 'cloudtasker', github: 'keypup-io/cloudtasker'
16 |
17 | # Active record adapter
18 | gem 'sqlite3'
19 |
20 | group :development do
21 | gem 'foreman'
22 | end
23 |
--------------------------------------------------------------------------------
/examples/cloud-run/Rakefile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Add your own tasks in files placed in lib/tasks ending in .rake,
4 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
5 |
6 | require_relative 'config/application'
7 |
8 | Rails.application.load_tasks
9 |
--------------------------------------------------------------------------------
/examples/cloud-run/app/assets/config/manifest.js:
--------------------------------------------------------------------------------
1 | //= link_tree ../images
2 | //= link_directory ../stylesheets .css
3 | //= link cloudtasker_manifest.js
4 |
--------------------------------------------------------------------------------
/examples/cloud-run/app/assets/images/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keypup-io/cloudtasker/c9b50be501595b7bafdcd9ba852bcf87e2cd34bd/examples/cloud-run/app/assets/images/.keep
--------------------------------------------------------------------------------
/examples/cloud-run/app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7 | *
8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10 | * files in this directory. Styles in this file should be added after the last require_* statement.
11 | * It is generally better to create a new file per style scope.
12 | *
13 | *= require_tree .
14 | *= require_self
15 | */
16 |
--------------------------------------------------------------------------------
/examples/cloud-run/app/channels/application_cable/channel.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ApplicationCable
4 | class Channel < ActionCable::Channel::Base
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/examples/cloud-run/app/channels/application_cable/connection.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ApplicationCable
4 | class Connection < ActionCable::Connection::Base
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/examples/cloud-run/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationController < ActionController::Base
4 | end
5 |
--------------------------------------------------------------------------------
/examples/cloud-run/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keypup-io/cloudtasker/c9b50be501595b7bafdcd9ba852bcf87e2cd34bd/examples/cloud-run/app/controllers/concerns/.keep
--------------------------------------------------------------------------------
/examples/cloud-run/app/controllers/enqueue_job_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # A simple controller that enqueues jobs for test
4 | # purpose on Cloud Run
5 | class EnqueueJobController < ApplicationController
6 | # GET /enqueue/dummy
7 | def dummy
8 | DummyWorker.perform_async
9 |
10 | render plain: 'DummyWorker enqueued'
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/examples/cloud-run/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ApplicationHelper
4 | end
5 |
--------------------------------------------------------------------------------
/examples/cloud-run/app/javascript/packs/application.js:
--------------------------------------------------------------------------------
1 | // This is a manifest file that'll be compiled into application.js, which will include all the files
2 | // listed below.
3 | //
4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6 | //
7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8 | // compiled file. JavaScript code in this file should be added after the last require_* statement.
9 | //
10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
11 | // about supported directives.
12 | //
13 | //= require rails-ujs
14 | //= require activestorage
15 | //= require_tree .
16 |
--------------------------------------------------------------------------------
/examples/cloud-run/app/jobs/application_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationJob < ActiveJob::Base
4 | # Automatically retry jobs that encountered a deadlock
5 | # retry_on ActiveRecord::Deadlocked
6 |
7 | # Most jobs are safe to ignore if the underlying records are no longer available
8 | # discard_on ActiveJob::DeserializationError
9 | end
10 |
--------------------------------------------------------------------------------
/examples/cloud-run/app/jobs/example_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ExampleJob < ApplicationJob
4 | queue_as :default
5 |
6 | def perform(*args)
7 | Rails.logger.info('Example job starting...')
8 | Rails.logger.info(args.inspect)
9 | sleep(3)
10 | Rails.logger.info('Example job done!')
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/examples/cloud-run/app/mailers/application_mailer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationMailer < ActionMailer::Base
4 | default from: 'from@example.com'
5 | layout 'mailer'
6 | end
7 |
--------------------------------------------------------------------------------
/examples/cloud-run/app/models/application_record.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationRecord < ActiveRecord::Base
4 | self.abstract_class = true
5 | end
6 |
--------------------------------------------------------------------------------
/examples/cloud-run/app/models/concerns/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keypup-io/cloudtasker/c9b50be501595b7bafdcd9ba852bcf87e2cd34bd/examples/cloud-run/app/models/concerns/.keep
--------------------------------------------------------------------------------
/examples/cloud-run/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Dummy
5 | <%= csrf_meta_tags %>
6 | <%= csp_meta_tag %>
7 |
8 | <%= stylesheet_link_tag 'application', media: 'all' %>
9 |
10 |
11 |
12 | <%= yield %>
13 |
14 |
15 |
--------------------------------------------------------------------------------
/examples/cloud-run/app/views/layouts/mailer.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
--------------------------------------------------------------------------------
/examples/cloud-run/app/views/layouts/mailer.text.erb:
--------------------------------------------------------------------------------
1 | <%= yield %>
2 |
--------------------------------------------------------------------------------
/examples/cloud-run/app/workers/dummy_worker.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class DummyWorker
4 | include Cloudtasker::Worker
5 |
6 | def perform
7 | Rails.logger.info('Dummy worker starting...')
8 | sleep(3)
9 | Rails.logger.info('Dummy worker done!')
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/examples/cloud-run/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | APP_PATH = File.expand_path('../config/application', __dir__)
5 | require_relative '../config/boot'
6 | require 'rails/commands'
7 |
--------------------------------------------------------------------------------
/examples/cloud-run/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require_relative '../config/boot'
5 | require 'rake'
6 | Rake.application.run
7 |
--------------------------------------------------------------------------------
/examples/cloud-run/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require 'fileutils'
5 |
6 | # path to your application root.
7 | APP_ROOT = File.expand_path('..', __dir__)
8 |
9 | def system!(*args)
10 | system(*args) || abort("\n== Command #{args} failed ==")
11 | end
12 |
13 | FileUtils.chdir APP_ROOT do
14 | # This script is a way to setup or update your development environment automatically.
15 | # This script is idempotent, so that you can run it at anytime and get an expectable outcome.
16 | # Add necessary setup steps to this file.
17 |
18 | puts '== Installing dependencies =='
19 | system! 'gem install bundler --conservative'
20 | system('bundle check') || system!('bundle install')
21 |
22 | # puts "\n== Copying sample files =="
23 | # unless File.exist?('config/database.yml')
24 | # FileUtils.cp 'config/database.yml.sample', 'config/database.yml'
25 | # end
26 |
27 | puts "\n== Preparing database =="
28 | system! 'bin/rails db:prepare'
29 |
30 | puts "\n== Removing old logs and tempfiles =="
31 | system! 'bin/rails log:clear tmp:clear'
32 |
33 | puts "\n== Restarting application server =="
34 | system! 'bin/rails restart'
35 | end
36 |
--------------------------------------------------------------------------------
/examples/cloud-run/config.ru:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # This file is used by Rack-based servers to start the application.
4 |
5 | require_relative 'config/environment'
6 |
7 | run Rails.application
8 |
--------------------------------------------------------------------------------
/examples/cloud-run/config/application.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative 'boot'
4 |
5 | require 'rails/all'
6 |
7 | Bundler.require(*Rails.groups)
8 |
9 | module Dummy
10 | class Application < Rails::Application
11 | # Initialize configuration defaults for originally generated Rails version.
12 | config.load_defaults 6.0
13 |
14 | # Settings in config/environments/* take precedence over those specified here.
15 | # Application configuration can go into files in config/initializers
16 | # -- all .rb files in that directory are automatically loaded after loading
17 | # the framework and any gems in your application.
18 |
19 | # Use cloudtasker as the ActiveJob backend:
20 | config.active_job.queue_adapter = :cloudtasker
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/examples/cloud-run/config/boot.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Set up gems listed in the Gemfile.
4 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__)
5 |
6 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
7 | $LOAD_PATH.unshift File.expand_path('../../../lib', __dir__)
8 |
--------------------------------------------------------------------------------
/examples/cloud-run/config/cable.yml:
--------------------------------------------------------------------------------
1 | development:
2 | adapter: async
3 |
4 | test:
5 | adapter: test
6 |
7 | production:
8 | adapter: redis
9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
10 | channel_prefix: dummy_production
11 |
--------------------------------------------------------------------------------
/examples/cloud-run/config/credentials.yml.enc:
--------------------------------------------------------------------------------
1 | jBUFm9LaktoqjLw7egZ/rqypP+0j0BrZyENZx2s4oGW9zhkrjGCUQvOPP6mL1LZlE1NeQX7yYwd4tM0KV4gBbSXcoLGZUD/KtWXg8ZMJraxBz3Klgt1SSoEDnKXbziqSPXXy0e4w9SqBflD9HAdg4hYj4I7m7utnrWctLB446Xn8LhApr/EPwmtLImRXVcLkY5fCQPrQ367lIbFxqX3vgQhadtZZnF6jpnD8R0m+QvhAJompM2jK3xZPilEG0TFZ5E/yd1Um+5PV+OFaM949zTR9mrINR+N6EzqzQg2oLoRJtXizEGyS399Lj8fL2yWj6fXvpeyGLbZEJRMECdWkoIfuRt5d+SRFEe39jprTTs1KNaMv5ajT5pzTQqb64W7dlBjw2oYRswfarDDuOv0eAWS1U7fTlQX3zgl8--yinJxK0UdO3VWhEq--NZjJhOTLbYEO+LqaGs60qg==
--------------------------------------------------------------------------------
/examples/cloud-run/config/database.yml:
--------------------------------------------------------------------------------
1 | # SQLite. Versions 3.8.0 and up are supported.
2 | # gem install sqlite3
3 | #
4 | # Ensure the SQLite 3 gem is defined in your Gemfile
5 | # gem 'sqlite3'
6 | #
7 | default: &default
8 | adapter: sqlite3
9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
10 | timeout: 5000
11 |
12 | development:
13 | <<: *default
14 | database: db/development.sqlite3
15 |
16 | # Warning: The database defined as "test" will be erased and
17 | # re-generated from your development database when you run "rake".
18 | # Do not set this db to the same as development or production.
19 | test:
20 | <<: *default
21 | database: db/test.sqlite3
22 |
23 | production:
24 | <<: *default
25 | database: db/production.sqlite3
26 |
--------------------------------------------------------------------------------
/examples/cloud-run/config/environment.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Load the Rails application.
4 | require_relative 'application'
5 |
6 | # Initialize the Rails application.
7 | Rails.application.initialize!
8 |
--------------------------------------------------------------------------------
/examples/cloud-run/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Rails.application.configure do
4 | $stdout.sync = true
5 | # Settings specified here will take precedence over those in config/application.rb.
6 |
7 | # In the development environment your application's code is reloaded on
8 | # every request. This slows down response time but is perfect for development
9 | # since you don't have to restart the web server when you make code changes.
10 | config.cache_classes = false
11 |
12 | # Do not eager load code on boot.
13 | config.eager_load = false
14 |
15 | # Show full error reports.
16 | config.consider_all_requests_local = true
17 |
18 | # Enable/disable caching. By default caching is disabled.
19 | # Run rails dev:cache to toggle caching.
20 | if Rails.root.join('tmp', 'caching-dev.txt').exist?
21 | config.action_controller.perform_caching = true
22 | config.action_controller.enable_fragment_cache_logging = true
23 |
24 | config.cache_store = :memory_store
25 | config.public_file_server.headers = {
26 | 'Cache-Control' => "public, max-age=#{2.days.to_i}"
27 | }
28 | else
29 | config.action_controller.perform_caching = false
30 |
31 | config.cache_store = :null_store
32 | end
33 |
34 | # Store uploaded files on the local file system (see config/storage.yml for options).
35 | config.active_storage.service = :local
36 |
37 | # Don't care if the mailer can't send.
38 | config.action_mailer.raise_delivery_errors = false
39 |
40 | config.action_mailer.perform_caching = false
41 |
42 | # Print deprecation notices to the Rails logger.
43 | config.active_support.deprecation = :log
44 |
45 | # Raise an error on page load if there are pending migrations.
46 | config.active_record.migration_error = :page_load
47 |
48 | # Highlight code that triggered database queries in logs.
49 | config.active_record.verbose_query_logs = true
50 |
51 | # Debug mode disables concatenation and preprocessing of assets.
52 | # This option may cause significant delays in view rendering with a large
53 | # number of complex assets.
54 | config.assets.debug = true
55 |
56 | # Suppress logger output for asset requests.
57 | config.assets.quiet = true
58 |
59 | # Raises error for missing translations.
60 | # config.action_view.raise_on_missing_translations = true
61 |
62 | # Use an evented file watcher to asynchronously detect changes in source code,
63 | # routes, locales, etc. This feature depends on the listen gem.
64 | # config.file_watcher = ActiveSupport::EventedFileUpdateChecker
65 | end
66 |
--------------------------------------------------------------------------------
/examples/cloud-run/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # The test environment is used exclusively to run your application's
4 | # test suite. You never need to work with it otherwise. Remember that
5 | # your test database is "scratch space" for the test suite and is wiped
6 | # and recreated between test runs. Don't rely on the data there!
7 |
8 | Rails.application.configure do
9 | # Settings specified here will take precedence over those in config/application.rb.
10 |
11 | config.cache_classes = false
12 |
13 | # Do not eager load code on boot. This avoids loading your whole application
14 | # just for the purpose of running a single test. If you are using a tool that
15 | # preloads Rails for running tests, you may have to set it to true.
16 | config.eager_load = false
17 |
18 | # Configure public file server for tests with Cache-Control for performance.
19 | config.public_file_server.enabled = true
20 | config.public_file_server.headers = {
21 | 'Cache-Control' => "public, max-age=#{1.hour.to_i}"
22 | }
23 |
24 | # Show full error reports and disable caching.
25 | config.consider_all_requests_local = true
26 | config.action_controller.perform_caching = false
27 | config.cache_store = :null_store
28 |
29 | # Raise exceptions instead of rendering exception templates.
30 | config.action_dispatch.show_exceptions = false
31 |
32 | # Disable request forgery protection in test environment.
33 | config.action_controller.allow_forgery_protection = false
34 |
35 | # Store uploaded files on the local file system in a temporary directory.
36 | config.active_storage.service = :test
37 |
38 | config.action_mailer.perform_caching = false
39 |
40 | # Tell Action Mailer not to deliver emails to the real world.
41 | # The :test delivery method accumulates sent emails in the
42 | # ActionMailer::Base.deliveries array.
43 | config.action_mailer.delivery_method = :test
44 |
45 | # Print deprecation notices to the stderr.
46 | config.active_support.deprecation = :stderr
47 |
48 | # Raises error for missing translations.
49 | # config.action_view.raise_on_missing_translations = true
50 | end
51 |
--------------------------------------------------------------------------------
/examples/cloud-run/config/initializers/application_controller_renderer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # ActiveSupport::Reloader.to_prepare do
6 | # ApplicationController.renderer.defaults.merge!(
7 | # http_host: 'example.org',
8 | # https: false
9 | # )
10 | # end
11 |
--------------------------------------------------------------------------------
/examples/cloud-run/config/initializers/assets.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # Version of your assets, change this if you want to expire all your assets.
6 | Rails.application.config.assets.version = '1.0'
7 |
8 | # Add additional assets to the asset load path.
9 | # Rails.application.config.assets.paths << Emoji.images_path
10 |
11 | # Precompile additional assets.
12 | # application.js, application.css, and all non-JS/CSS in the app/assets
13 | # folder are already added.
14 | # Rails.application.config.assets.precompile += %w( admin.js admin.css )
15 |
--------------------------------------------------------------------------------
/examples/cloud-run/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
6 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
7 |
8 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
9 | # Rails.backtrace_cleaner.remove_silencers!
10 |
--------------------------------------------------------------------------------
/examples/cloud-run/config/initializers/cloudtasker.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Cloudtasker.configure do |config|
4 | #
5 | # GCP Configuration
6 | #
7 | config.gcp_project_id = 'your-project-id'
8 | config.gcp_location_id = 'us-central1'
9 | config.gcp_queue_prefix = 'cloudtasker-demo'
10 |
11 | #
12 | # Domain
13 | #
14 | # config.processor_host = 'https://xxxx.ngrok.io'
15 | #
16 | config.processor_host = 'https://your-cloud-run-service.a.run.app'
17 |
18 | # OpenID Connect configuration
19 | # You need to create a IAM service account first. See the README.
20 | # config.oidc = { service_account_email: 'cloudtasker-demo@your-project-id.iam.gserviceaccount.com' }
21 | end
22 |
--------------------------------------------------------------------------------
/examples/cloud-run/config/initializers/content_security_policy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # Define an application-wide content security policy
6 | # For further information see the following documentation
7 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
8 |
9 | # Rails.application.config.content_security_policy do |policy|
10 | # policy.default_src :self, :https
11 | # policy.font_src :self, :https, :data
12 | # policy.img_src :self, :https, :data
13 | # policy.object_src :none
14 | # policy.script_src :self, :https
15 | # policy.style_src :self, :https
16 |
17 | # # Specify URI for violation reports
18 | # # policy.report_uri "/csp-violation-report-endpoint"
19 | # end
20 |
21 | # If you are using UJS then enable automatic nonce generation
22 | # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
23 |
24 | # Set the nonce only to specific directives
25 | # Rails.application.config.content_security_policy_nonce_directives = %w(script-src)
26 |
27 | # Report CSP violations to a specified URI
28 | # For further information see the following documentation:
29 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
30 | # Rails.application.config.content_security_policy_report_only = true
31 |
--------------------------------------------------------------------------------
/examples/cloud-run/config/initializers/cookies_serializer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # Specify a serializer for the signed and encrypted cookie jars.
6 | # Valid options are :json, :marshal, and :hybrid.
7 | Rails.application.config.action_dispatch.cookies_serializer = :json
8 |
--------------------------------------------------------------------------------
/examples/cloud-run/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # Configure sensitive parameters which will be filtered from the log file.
6 | Rails.application.config.filter_parameters += [:password]
7 |
--------------------------------------------------------------------------------
/examples/cloud-run/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # Add new inflection rules using the following format. Inflections
6 | # are locale specific, and you may define rules for as many different
7 | # locales as you wish. All of these examples are active by default:
8 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
9 | # inflect.plural /^(ox)$/i, '\1en'
10 | # inflect.singular /^(ox)en/i, '\1'
11 | # inflect.irregular 'person', 'people'
12 | # inflect.uncountable %w( fish sheep )
13 | # end
14 |
15 | # These inflection rules are supported but not enabled by default:
16 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
17 | # inflect.acronym 'RESTful'
18 | # end
19 |
--------------------------------------------------------------------------------
/examples/cloud-run/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # Add new mime types for use in respond_to blocks:
6 | # Mime::Type.register "text/richtext", :rtf
7 |
--------------------------------------------------------------------------------
/examples/cloud-run/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # This file contains settings for ActionController::ParamsWrapper which
6 | # is enabled by default.
7 |
8 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
9 | ActiveSupport.on_load(:action_controller) do
10 | wrap_parameters format: [:json]
11 | end
12 |
13 | # To enable root element in JSON for ActiveRecord objects.
14 | # ActiveSupport.on_load(:active_record) do
15 | # self.include_root_in_json = true
16 | # end
17 |
--------------------------------------------------------------------------------
/examples/cloud-run/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization
2 | # and are automatically loaded by Rails. If you want to use locales other
3 | # than English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t 'hello'
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t('hello') %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # The following keys must be escaped otherwise they will not be retrieved by
20 | # the default I18n backend:
21 | #
22 | # true, false, on, off, yes, no
23 | #
24 | # Instead, surround them with single quotes.
25 | #
26 | # en:
27 | # 'true': 'foo'
28 | #
29 | # To learn more, please read the Rails Internationalization guide
30 | # available at https://guides.rubyonrails.org/i18n.html.
31 |
32 | en:
33 | hello: "Hello world"
34 |
--------------------------------------------------------------------------------
/examples/cloud-run/config/master.key:
--------------------------------------------------------------------------------
1 | adb9cb04c606955610b91d1219271d02
--------------------------------------------------------------------------------
/examples/cloud-run/config/puma.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Puma can serve each request in a thread from an internal thread pool.
4 | # The `threads` method setting takes two numbers: a minimum and maximum.
5 | # Any libraries that use thread pools should be configured to match
6 | # the maximum value specified for Puma. Default is set to 5 threads for minimum
7 | # and maximum; this matches the default thread size of Active Record.
8 | #
9 | max_threads_count = ENV.fetch('RAILS_MAX_THREADS', 5)
10 | min_threads_count = ENV.fetch('RAILS_MIN_THREADS') { max_threads_count }
11 | threads min_threads_count, max_threads_count
12 |
13 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000.
14 | #
15 | port ENV.fetch('PORT', 3000)
16 |
17 | # Specifies the `environment` that Puma will run in.
18 | #
19 | environment ENV.fetch('RAILS_ENV', 'development')
20 |
21 | # Specifies the number of `workers` to boot in clustered mode.
22 | # Workers are forked web server processes. If using threads and workers together
23 | # the concurrency of the application would be max `threads` * `workers`.
24 | # Workers do not work on JRuby or Windows (both of which do not support
25 | # processes).
26 | #
27 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 }
28 |
29 | # Use the `preload_app!` method when specifying a `workers` number.
30 | # This directive tells Puma to first boot the application and load code
31 | # before forking the application. This takes advantage of Copy On Write
32 | # process behavior so workers use less memory.
33 | #
34 | # preload_app!
35 |
36 | # Allow puma to be restarted by `rails restart` command.
37 | plugin :tmp_restart
38 |
--------------------------------------------------------------------------------
/examples/cloud-run/config/routes.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Rails.application.routes.draw do
4 | get '/enqueue/dummy', to: 'enqueue_job#dummy'
5 | end
6 |
--------------------------------------------------------------------------------
/examples/cloud-run/config/spring.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Spring.watch(
4 | '.ruby-version',
5 | '.rbenv-vars',
6 | 'tmp/restart.txt',
7 | 'tmp/caching-dev.txt'
8 | )
9 |
--------------------------------------------------------------------------------
/examples/cloud-run/config/storage.yml:
--------------------------------------------------------------------------------
1 | test:
2 | service: Disk
3 | root: <%= Rails.root.join("tmp/storage") %>
4 |
5 | local:
6 | service: Disk
7 | root: <%= Rails.root.join("storage") %>
8 |
9 | # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
10 | # amazon:
11 | # service: S3
12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
14 | # region: us-east-1
15 | # bucket: your_own_bucket
16 |
17 | # Remember not to checkin your GCS keyfile to a repository
18 | # google:
19 | # service: GCS
20 | # project: your_project
21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
22 | # bucket: your_own_bucket
23 |
24 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
25 | # microsoft:
26 | # service: AzureStorage
27 | # storage_account_name: your_account_name
28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
29 | # container: your_container_name
30 |
31 | # mirror:
32 | # service: Mirror
33 | # primary: local
34 | # mirrors: [ amazon, google, microsoft ]
35 |
--------------------------------------------------------------------------------
/examples/cloud-run/db/development.sqlite3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keypup-io/cloudtasker/c9b50be501595b7bafdcd9ba852bcf87e2cd34bd/examples/cloud-run/db/development.sqlite3
--------------------------------------------------------------------------------
/examples/cloud-run/db/test.sqlite3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keypup-io/cloudtasker/c9b50be501595b7bafdcd9ba852bcf87e2cd34bd/examples/cloud-run/db/test.sqlite3
--------------------------------------------------------------------------------
/examples/cloud-run/lib/assets/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keypup-io/cloudtasker/c9b50be501595b7bafdcd9ba852bcf87e2cd34bd/examples/cloud-run/lib/assets/.keep
--------------------------------------------------------------------------------
/examples/cloud-run/log/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keypup-io/cloudtasker/c9b50be501595b7bafdcd9ba852bcf87e2cd34bd/examples/cloud-run/log/.keep
--------------------------------------------------------------------------------
/examples/cloud-run/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The page you were looking for doesn't exist.
62 |
You may have mistyped the address or the page may have moved.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/examples/cloud-run/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The change you wanted was rejected.
62 |
Maybe you tried to change something you didn't have access to.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/examples/cloud-run/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
We're sorry, but something went wrong.
62 |
63 |
If you are the application owner check the logs for more information.
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/examples/cloud-run/public/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keypup-io/cloudtasker/c9b50be501595b7bafdcd9ba852bcf87e2cd34bd/examples/cloud-run/public/apple-touch-icon-precomposed.png
--------------------------------------------------------------------------------
/examples/cloud-run/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keypup-io/cloudtasker/c9b50be501595b7bafdcd9ba852bcf87e2cd34bd/examples/cloud-run/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/examples/cloud-run/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keypup-io/cloudtasker/c9b50be501595b7bafdcd9ba852bcf87e2cd34bd/examples/cloud-run/public/favicon.ico
--------------------------------------------------------------------------------
/examples/cloud-run/storage/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keypup-io/cloudtasker/c9b50be501595b7bafdcd9ba852bcf87e2cd34bd/examples/cloud-run/storage/.keep
--------------------------------------------------------------------------------
/examples/cloud-run/tmp/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keypup-io/cloudtasker/c9b50be501595b7bafdcd9ba852bcf87e2cd34bd/examples/cloud-run/tmp/.keep
--------------------------------------------------------------------------------
/examples/cloud-run/tmp/development_secret.txt:
--------------------------------------------------------------------------------
1 | 6a0868194e931706b65bfaee87eea7197c9c4725dea92499f24461fe9c9cf8f3d3b1a2874ccab440ef36b898ae47bba4621c01343821afcb6b67186977fb80ea
--------------------------------------------------------------------------------
/examples/cloud-run/tmp/restart.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keypup-io/cloudtasker/c9b50be501595b7bafdcd9ba852bcf87e2cd34bd/examples/cloud-run/tmp/restart.txt
--------------------------------------------------------------------------------
/examples/rails/.ruby-version:
--------------------------------------------------------------------------------
1 | ruby-3.3.0
2 |
--------------------------------------------------------------------------------
/examples/rails/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source 'https://rubygems.org'
4 | git_source(:github) { |repo| "https://github.com/#{repo}.git" }
5 |
6 | ruby '~> 3.3'
7 |
8 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
9 | gem 'rails', '~> 7.1'
10 |
11 | # Application server
12 | gem 'puma'
13 |
14 | # Background jobs via Cloud Tasks
15 | gem 'cloudtasker', path: '../../'
16 |
17 | # Active record adapter
18 | gem 'sqlite3'
19 |
20 | group :development do
21 | gem 'foreman'
22 | end
23 |
--------------------------------------------------------------------------------
/examples/rails/Procfile:
--------------------------------------------------------------------------------
1 | web: bundle exec rails s -p 3000
2 | worker: bundle exec cloudtasker -q critical,2 -q default,3
--------------------------------------------------------------------------------
/examples/rails/README.md:
--------------------------------------------------------------------------------
1 | # Example usage with Rails
2 |
3 | ## Run using the local processing server
4 |
5 | 1. Install dependencies: `bundle install`
6 | 2. Launch the server: `foreman start`
7 | 3. Open a Rails console: `rails c`
8 | 4. Enqueue workers:
9 | ```ruby
10 | DummyWorker.perform_async
11 | ```
12 |
13 | ## Run using Cloud Tasks
14 |
15 | 1. Ensure that your [Google Cloud SDK](https://cloud.google.com/sdk/docs/quickstarts) is setup.
16 | 2. Install dependencies: `bundle install`
17 | 3. Start an [ngrok](https://ngrok.com) tunnel: `ngrok http 3000`
18 | 4. Edit the [initializer](./config/initializers/cloudtasker.rb)
19 | * Add the configuration of your GCP queue
20 | * Set `config.processor_host` to the ngrok http or https url
21 | * Set `config.mode` to `:production`
22 | 5. Setup your queues on Google Cloud Tasks:
23 | ```
24 | # Default and critical queues
25 | rake cloudtasker:setup_queue
26 | rake cloudtasker:setup_queue name=critical
27 | ```
28 | 6. Launch the server: `foreman start web`
29 | 7. Open a Rails console: `rails c`
30 | 8. Enqueue workers:
31 | ```ruby
32 | DummyWorker.perform_async
33 | ```
34 |
--------------------------------------------------------------------------------
/examples/rails/Rakefile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Add your own tasks in files placed in lib/tasks ending in .rake,
4 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
5 |
6 | require_relative 'config/application'
7 |
8 | Rails.application.load_tasks
9 |
--------------------------------------------------------------------------------
/examples/rails/app/assets/config/manifest.js:
--------------------------------------------------------------------------------
1 | //= link_tree ../images
2 | //= link_directory ../stylesheets .css
3 | //= link cloudtasker_manifest.js
4 |
--------------------------------------------------------------------------------
/examples/rails/app/assets/images/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keypup-io/cloudtasker/c9b50be501595b7bafdcd9ba852bcf87e2cd34bd/examples/rails/app/assets/images/.keep
--------------------------------------------------------------------------------
/examples/rails/app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7 | *
8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10 | * files in this directory. Styles in this file should be added after the last require_* statement.
11 | * It is generally better to create a new file per style scope.
12 | *
13 | *= require_tree .
14 | *= require_self
15 | */
16 |
--------------------------------------------------------------------------------
/examples/rails/app/channels/application_cable/channel.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ApplicationCable
4 | class Channel < ActionCable::Channel::Base
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/examples/rails/app/channels/application_cable/connection.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ApplicationCable
4 | class Connection < ActionCable::Connection::Base
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/examples/rails/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationController < ActionController::Base
4 | end
5 |
--------------------------------------------------------------------------------
/examples/rails/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keypup-io/cloudtasker/c9b50be501595b7bafdcd9ba852bcf87e2cd34bd/examples/rails/app/controllers/concerns/.keep
--------------------------------------------------------------------------------
/examples/rails/app/controllers/enqueue_job_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # A simple controller that enqueues jobs for test
4 | # purpose on Cloud Run
5 | class EnqueueJobController < ApplicationController
6 | # GET /enqueue/dummy
7 | def dummy
8 | DummyWorker.perform_async
9 |
10 | render plain: 'DummyWorker enqueued'
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/examples/rails/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ApplicationHelper
4 | end
5 |
--------------------------------------------------------------------------------
/examples/rails/app/javascript/packs/application.js:
--------------------------------------------------------------------------------
1 | // This is a manifest file that'll be compiled into application.js, which will include all the files
2 | // listed below.
3 | //
4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6 | //
7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8 | // compiled file. JavaScript code in this file should be added after the last require_* statement.
9 | //
10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
11 | // about supported directives.
12 | //
13 | //= require rails-ujs
14 | //= require activestorage
15 | //= require_tree .
16 |
--------------------------------------------------------------------------------
/examples/rails/app/jobs/application_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationJob < ActiveJob::Base
4 | # Automatically retry jobs that encountered a deadlock
5 | # retry_on ActiveRecord::Deadlocked
6 |
7 | # Most jobs are safe to ignore if the underlying records are no longer available
8 | # discard_on ActiveJob::DeserializationError
9 | end
10 |
--------------------------------------------------------------------------------
/examples/rails/app/jobs/example_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ExampleJob < ApplicationJob
4 | queue_as :default
5 |
6 | def perform(*args)
7 | Rails.logger.info('Example job starting...')
8 | Rails.logger.info(args.inspect)
9 | sleep(3)
10 | Rails.logger.info('Example job done!')
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/examples/rails/app/mailers/application_mailer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationMailer < ActionMailer::Base
4 | default from: 'from@example.com'
5 | layout 'mailer'
6 | end
7 |
--------------------------------------------------------------------------------
/examples/rails/app/models/application_record.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationRecord < ActiveRecord::Base
4 | self.abstract_class = true
5 | end
6 |
--------------------------------------------------------------------------------
/examples/rails/app/models/concerns/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keypup-io/cloudtasker/c9b50be501595b7bafdcd9ba852bcf87e2cd34bd/examples/rails/app/models/concerns/.keep
--------------------------------------------------------------------------------
/examples/rails/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Dummy
5 | <%= csrf_meta_tags %>
6 | <%= csp_meta_tag %>
7 |
8 | <%= stylesheet_link_tag 'application', media: 'all' %>
9 |
10 |
11 |
12 | <%= yield %>
13 |
14 |
15 |
--------------------------------------------------------------------------------
/examples/rails/app/views/layouts/mailer.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
--------------------------------------------------------------------------------
/examples/rails/app/views/layouts/mailer.text.erb:
--------------------------------------------------------------------------------
1 | <%= yield %>
2 |
--------------------------------------------------------------------------------
/examples/rails/app/workers/batch_worker.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class BatchWorker
4 | include Cloudtasker::Worker
5 |
6 | def perform(level = 0, instance = 0)
7 | Rails.logger.info("#{self.class} level=#{level} instance=#{instance} | starting...")
8 | # sleep(1)
9 |
10 | # Enqueue children
11 | 10.times { |n| batch.add(BatchWorker, level + 1, n) } if level < 2
12 |
13 | Rails.logger.info("#{self.class} level=#{level} instance=#{instance} | done!")
14 | end
15 |
16 | def on_child_complete(child)
17 | msg = [
18 | "#{self.class} level=#{job_args[0].to_i} instance=#{job_args[1].to_i}",
19 | "on_child_complete level=#{child.job_args[0]} instance=#{child.job_args[1]}"
20 | ].join(' | ')
21 | Rails.logger.info(msg)
22 | end
23 |
24 | def on_batch_node_complete(child)
25 | msg = [
26 | "#{self.class} level=#{job_args[0].to_i} instance=#{job_args[1].to_i}",
27 | "on_batch_node_complete level=#{child.job_args[0].to_i} instance=#{child.job_args[1].to_i}"
28 | ].join(' | ')
29 | Rails.logger.info(msg)
30 | end
31 |
32 | def on_batch_complete
33 | msg = [
34 | "#{self.class} level=#{job_args[0].to_i} instance=#{job_args[1].to_i}",
35 | 'on_batch_complete'
36 | ].join(' | ')
37 | Rails.logger.info(msg)
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/examples/rails/app/workers/critical_worker.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class CriticalWorker
4 | include Cloudtasker::Worker
5 |
6 | cloudtasker_options queue: :critical
7 |
8 | def perform
9 | Rails.logger.info('Critical worker starting...')
10 | sleep(3)
11 | Rails.logger.info('Critical worker done!')
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/examples/rails/app/workers/cron_worker.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class CronWorker
4 | include Cloudtasker::Worker
5 |
6 | def perform(arg1)
7 | Rails.logger.info("#{self.class} starting with arg1=#{arg1}...")
8 | sleep(3)
9 | Rails.logger.info("#{self.class} done!")
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/examples/rails/app/workers/dummy_worker.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class DummyWorker
4 | include Cloudtasker::Worker
5 |
6 | def perform
7 | Rails.logger.info('Dummy worker starting...')
8 | sleep(3)
9 | Rails.logger.info('Dummy worker done!')
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/examples/rails/app/workers/uniq_executing_worker.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class UniqExecutingWorker
4 | include Cloudtasker::Worker
5 |
6 | cloudtasker_options lock: :while_executing, on_conflict: :reschedule, lock_ttl: 90
7 |
8 | def unique_args(args)
9 | [args[0], args[1]]
10 | end
11 |
12 | def perform(arg1, arg2, arg3)
13 | Rails.logger.info("#{self.class} with args=#{[arg1, arg2, arg3].inspect} starting...")
14 | sleep(10)
15 | Rails.logger.info("#{self.class} with args=#{[arg1, arg2, arg3].inspect} done!")
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/examples/rails/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | APP_PATH = File.expand_path('../config/application', __dir__)
5 | require_relative '../config/boot'
6 | require 'rails/commands'
7 |
--------------------------------------------------------------------------------
/examples/rails/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require_relative '../config/boot'
5 | require 'rake'
6 | Rake.application.run
7 |
--------------------------------------------------------------------------------
/examples/rails/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require 'fileutils'
5 |
6 | # path to your application root.
7 | APP_ROOT = File.expand_path('..', __dir__)
8 |
9 | def system!(*args)
10 | system(*args) || abort("\n== Command #{args} failed ==")
11 | end
12 |
13 | FileUtils.chdir APP_ROOT do
14 | # This script is a way to setup or update your development environment automatically.
15 | # This script is idempotent, so that you can run it at anytime and get an expectable outcome.
16 | # Add necessary setup steps to this file.
17 |
18 | puts '== Installing dependencies =='
19 | system! 'gem install bundler --conservative'
20 | system('bundle check') || system!('bundle install')
21 |
22 | # puts "\n== Copying sample files =="
23 | # unless File.exist?('config/database.yml')
24 | # FileUtils.cp 'config/database.yml.sample', 'config/database.yml'
25 | # end
26 |
27 | puts "\n== Preparing database =="
28 | system! 'bin/rails db:prepare'
29 |
30 | puts "\n== Removing old logs and tempfiles =="
31 | system! 'bin/rails log:clear tmp:clear'
32 |
33 | puts "\n== Restarting application server =="
34 | system! 'bin/rails restart'
35 | end
36 |
--------------------------------------------------------------------------------
/examples/rails/config.ru:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # This file is used by Rack-based servers to start the application.
4 |
5 | require_relative 'config/environment'
6 |
7 | run Rails.application
8 |
--------------------------------------------------------------------------------
/examples/rails/config/application.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative 'boot'
4 |
5 | require 'rails/all'
6 |
7 | Bundler.require(*Rails.groups)
8 |
9 | module Dummy
10 | class Application < Rails::Application
11 | # Initialize configuration defaults for originally generated Rails version.
12 | config.load_defaults 7.0
13 |
14 | # Settings in config/environments/* take precedence over those specified here.
15 | # Application configuration can go into files in config/initializers
16 | # -- all .rb files in that directory are automatically loaded after loading
17 | # the framework and any gems in your application.
18 |
19 | # Use cloudtasker as the ActiveJob backend:
20 | config.active_job.queue_adapter = :cloudtasker
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/examples/rails/config/boot.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Set up gems listed in the Gemfile.
4 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__)
5 |
6 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
7 | $LOAD_PATH.unshift File.expand_path('../../../lib', __dir__)
8 |
--------------------------------------------------------------------------------
/examples/rails/config/cable.yml:
--------------------------------------------------------------------------------
1 | development:
2 | adapter: async
3 |
4 | test:
5 | adapter: test
6 |
7 | production:
8 | adapter: redis
9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
10 | channel_prefix: dummy_production
11 |
--------------------------------------------------------------------------------
/examples/rails/config/credentials.yml.enc:
--------------------------------------------------------------------------------
1 | jBUFm9LaktoqjLw7egZ/rqypP+0j0BrZyENZx2s4oGW9zhkrjGCUQvOPP6mL1LZlE1NeQX7yYwd4tM0KV4gBbSXcoLGZUD/KtWXg8ZMJraxBz3Klgt1SSoEDnKXbziqSPXXy0e4w9SqBflD9HAdg4hYj4I7m7utnrWctLB446Xn8LhApr/EPwmtLImRXVcLkY5fCQPrQ367lIbFxqX3vgQhadtZZnF6jpnD8R0m+QvhAJompM2jK3xZPilEG0TFZ5E/yd1Um+5PV+OFaM949zTR9mrINR+N6EzqzQg2oLoRJtXizEGyS399Lj8fL2yWj6fXvpeyGLbZEJRMECdWkoIfuRt5d+SRFEe39jprTTs1KNaMv5ajT5pzTQqb64W7dlBjw2oYRswfarDDuOv0eAWS1U7fTlQX3zgl8--yinJxK0UdO3VWhEq--NZjJhOTLbYEO+LqaGs60qg==
--------------------------------------------------------------------------------
/examples/rails/config/database.yml:
--------------------------------------------------------------------------------
1 | # SQLite. Versions 3.8.0 and up are supported.
2 | # gem install sqlite3
3 | #
4 | # Ensure the SQLite 3 gem is defined in your Gemfile
5 | # gem 'sqlite3'
6 | #
7 | default: &default
8 | adapter: sqlite3
9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
10 | timeout: 5000
11 |
12 | development:
13 | <<: *default
14 | database: db/development.sqlite3
15 |
16 | # Warning: The database defined as "test" will be erased and
17 | # re-generated from your development database when you run "rake".
18 | # Do not set this db to the same as development or production.
19 | test:
20 | <<: *default
21 | database: db/test.sqlite3
22 |
23 | production:
24 | <<: *default
25 | database: db/production.sqlite3
26 |
--------------------------------------------------------------------------------
/examples/rails/config/environment.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Load the Rails application.
4 | require_relative 'application'
5 |
6 | # Initialize the Rails application.
7 | Rails.application.initialize!
8 |
--------------------------------------------------------------------------------
/examples/rails/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Rails.application.configure do
4 | $stdout.sync = true
5 | # Settings specified here will take precedence over those in config/application.rb.
6 |
7 | # In the development environment your application's code is reloaded on
8 | # every request. This slows down response time but is perfect for development
9 | # since you don't have to restart the web server when you make code changes.
10 | config.cache_classes = false
11 |
12 | # Do not eager load code on boot.
13 | config.eager_load = false
14 |
15 | # Show full error reports.
16 | config.consider_all_requests_local = true
17 |
18 | # Enable/disable caching. By default caching is disabled.
19 | # Run rails dev:cache to toggle caching.
20 | if Rails.root.join('tmp', 'caching-dev.txt').exist?
21 | config.action_controller.perform_caching = true
22 | config.action_controller.enable_fragment_cache_logging = true
23 |
24 | config.cache_store = :memory_store
25 | config.public_file_server.headers = {
26 | 'Cache-Control' => "public, max-age=#{2.days.to_i}"
27 | }
28 | else
29 | config.action_controller.perform_caching = false
30 |
31 | config.cache_store = :null_store
32 | end
33 |
34 | # Store uploaded files on the local file system (see config/storage.yml for options).
35 | config.active_storage.service = :local
36 |
37 | # Don't care if the mailer can't send.
38 | config.action_mailer.raise_delivery_errors = false
39 |
40 | config.action_mailer.perform_caching = false
41 |
42 | # Print deprecation notices to the Rails logger.
43 | config.active_support.deprecation = :log
44 |
45 | # Raise an error on page load if there are pending migrations.
46 | config.active_record.migration_error = :page_load
47 |
48 | # Highlight code that triggered database queries in logs.
49 | config.active_record.verbose_query_logs = true
50 |
51 | # Raises error for missing translations.
52 | # config.action_view.raise_on_missing_translations = true
53 |
54 | # Use an evented file watcher to asynchronously detect changes in source code,
55 | # routes, locales, etc. This feature depends on the listen gem.
56 | # config.file_watcher = ActiveSupport::EventedFileUpdateChecker
57 | end
58 |
--------------------------------------------------------------------------------
/examples/rails/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # The test environment is used exclusively to run your application's
4 | # test suite. You never need to work with it otherwise. Remember that
5 | # your test database is "scratch space" for the test suite and is wiped
6 | # and recreated between test runs. Don't rely on the data there!
7 |
8 | Rails.application.configure do
9 | # Settings specified here will take precedence over those in config/application.rb.
10 |
11 | config.cache_classes = false
12 |
13 | # Do not eager load code on boot. This avoids loading your whole application
14 | # just for the purpose of running a single test. If you are using a tool that
15 | # preloads Rails for running tests, you may have to set it to true.
16 | config.eager_load = false
17 |
18 | # Configure public file server for tests with Cache-Control for performance.
19 | config.public_file_server.enabled = true
20 | config.public_file_server.headers = {
21 | 'Cache-Control' => "public, max-age=#{1.hour.to_i}"
22 | }
23 |
24 | # Show full error reports and disable caching.
25 | config.consider_all_requests_local = true
26 | config.action_controller.perform_caching = false
27 | config.cache_store = :null_store
28 |
29 | # Raise exceptions instead of rendering exception templates.
30 | config.action_dispatch.show_exceptions = false
31 |
32 | # Disable request forgery protection in test environment.
33 | config.action_controller.allow_forgery_protection = false
34 |
35 | # Store uploaded files on the local file system in a temporary directory.
36 | config.active_storage.service = :test
37 |
38 | config.action_mailer.perform_caching = false
39 |
40 | # Tell Action Mailer not to deliver emails to the real world.
41 | # The :test delivery method accumulates sent emails in the
42 | # ActionMailer::Base.deliveries array.
43 | config.action_mailer.delivery_method = :test
44 |
45 | # Print deprecation notices to the stderr.
46 | config.active_support.deprecation = :stderr
47 |
48 | # Raises error for missing translations.
49 | # config.action_view.raise_on_missing_translations = true
50 | end
51 |
--------------------------------------------------------------------------------
/examples/rails/config/initializers/application_controller_renderer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # ActiveSupport::Reloader.to_prepare do
6 | # ApplicationController.renderer.defaults.merge!(
7 | # http_host: 'example.org',
8 | # https: false
9 | # )
10 | # end
11 |
--------------------------------------------------------------------------------
/examples/rails/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
6 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
7 |
8 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
9 | # Rails.backtrace_cleaner.remove_silencers!
10 |
--------------------------------------------------------------------------------
/examples/rails/config/initializers/cloudtasker.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'cloudtasker/unique_job'
4 | require 'cloudtasker/cron'
5 | require 'cloudtasker/batch'
6 |
7 | Cloudtasker.configure do |config|
8 | #
9 | # GCP Configuration
10 | #
11 | config.gcp_project_id = 'some-project'
12 | config.gcp_location_id = 'us-east1'
13 | config.gcp_queue_prefix = 'my-app'
14 |
15 | #
16 | # Domain
17 | #
18 | # config.processor_host = 'https://xxxx.ngrok.io'
19 | #
20 | config.processor_host = 'http://localhost:3000'
21 |
22 | #
23 | # Uncomment to process tasks via Cloud Task.
24 | # Requires a ngrok tunnel.
25 | #
26 | # config.mode = :production
27 |
28 | #
29 | # Global error Hooks
30 | #
31 | config.on_error = lambda { |error, worker|
32 | Rails.logger.error("Uh oh... worker #{worker&.job_id} had the following error: #{error}")
33 | }
34 | config.on_dead = ->(error, worker) { Rails.logger.error("Damn... worker #{worker&.job_id} died with: #{error}") }
35 | end
36 |
37 | #
38 | # Setup cron job
39 | #
40 | # Cloudtasker::Cron::Schedule.load_from_hash!(
41 | # 'my_worker' => {
42 | # 'worker' => 'CronWorker',
43 | # 'cron' => '* * * * *',
44 | # 'queue' => 'critical',
45 | # 'args' => ['foo']
46 | # }
47 | # )
48 |
--------------------------------------------------------------------------------
/examples/rails/config/initializers/content_security_policy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # Define an application-wide content security policy
6 | # For further information see the following documentation
7 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
8 |
9 | # Rails.application.config.content_security_policy do |policy|
10 | # policy.default_src :self, :https
11 | # policy.font_src :self, :https, :data
12 | # policy.img_src :self, :https, :data
13 | # policy.object_src :none
14 | # policy.script_src :self, :https
15 | # policy.style_src :self, :https
16 |
17 | # # Specify URI for violation reports
18 | # # policy.report_uri "/csp-violation-report-endpoint"
19 | # end
20 |
21 | # If you are using UJS then enable automatic nonce generation
22 | # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
23 |
24 | # Set the nonce only to specific directives
25 | # Rails.application.config.content_security_policy_nonce_directives = %w(script-src)
26 |
27 | # Report CSP violations to a specified URI
28 | # For further information see the following documentation:
29 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
30 | # Rails.application.config.content_security_policy_report_only = true
31 |
--------------------------------------------------------------------------------
/examples/rails/config/initializers/cookies_serializer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # Specify a serializer for the signed and encrypted cookie jars.
6 | # Valid options are :json, :marshal, and :hybrid.
7 | Rails.application.config.action_dispatch.cookies_serializer = :json
8 |
--------------------------------------------------------------------------------
/examples/rails/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # Configure sensitive parameters which will be filtered from the log file.
6 | Rails.application.config.filter_parameters += [:password]
7 |
--------------------------------------------------------------------------------
/examples/rails/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # Add new inflection rules using the following format. Inflections
6 | # are locale specific, and you may define rules for as many different
7 | # locales as you wish. All of these examples are active by default:
8 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
9 | # inflect.plural /^(ox)$/i, '\1en'
10 | # inflect.singular /^(ox)en/i, '\1'
11 | # inflect.irregular 'person', 'people'
12 | # inflect.uncountable %w( fish sheep )
13 | # end
14 |
15 | # These inflection rules are supported but not enabled by default:
16 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
17 | # inflect.acronym 'RESTful'
18 | # end
19 |
--------------------------------------------------------------------------------
/examples/rails/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # Add new mime types for use in respond_to blocks:
6 | # Mime::Type.register "text/richtext", :rtf
7 |
--------------------------------------------------------------------------------
/examples/rails/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # This file contains settings for ActionController::ParamsWrapper which
6 | # is enabled by default.
7 |
8 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
9 | ActiveSupport.on_load(:action_controller) do
10 | wrap_parameters format: [:json]
11 | end
12 |
13 | # To enable root element in JSON for ActiveRecord objects.
14 | # ActiveSupport.on_load(:active_record) do
15 | # self.include_root_in_json = true
16 | # end
17 |
--------------------------------------------------------------------------------
/examples/rails/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization
2 | # and are automatically loaded by Rails. If you want to use locales other
3 | # than English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t 'hello'
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t('hello') %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # The following keys must be escaped otherwise they will not be retrieved by
20 | # the default I18n backend:
21 | #
22 | # true, false, on, off, yes, no
23 | #
24 | # Instead, surround them with single quotes.
25 | #
26 | # en:
27 | # 'true': 'foo'
28 | #
29 | # To learn more, please read the Rails Internationalization guide
30 | # available at https://guides.rubyonrails.org/i18n.html.
31 |
32 | en:
33 | hello: "Hello world"
34 |
--------------------------------------------------------------------------------
/examples/rails/config/master.key:
--------------------------------------------------------------------------------
1 | adb9cb04c606955610b91d1219271d02
--------------------------------------------------------------------------------
/examples/rails/config/puma.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Puma can serve each request in a thread from an internal thread pool.
4 | # The `threads` method setting takes two numbers: a minimum and maximum.
5 | # Any libraries that use thread pools should be configured to match
6 | # the maximum value specified for Puma. Default is set to 5 threads for minimum
7 | # and maximum; this matches the default thread size of Active Record.
8 | #
9 | max_threads_count = ENV.fetch('RAILS_MAX_THREADS', 5)
10 | min_threads_count = ENV.fetch('RAILS_MIN_THREADS') { max_threads_count }
11 | threads min_threads_count, max_threads_count
12 |
13 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000.
14 | #
15 | port ENV.fetch('PORT', 3000)
16 |
17 | # Specifies the `environment` that Puma will run in.
18 | #
19 | environment ENV.fetch('RAILS_ENV', 'development')
20 |
21 | # Specifies the number of `workers` to boot in clustered mode.
22 | # Workers are forked web server processes. If using threads and workers together
23 | # the concurrency of the application would be max `threads` * `workers`.
24 | # Workers do not work on JRuby or Windows (both of which do not support
25 | # processes).
26 | #
27 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 }
28 |
29 | # Use the `preload_app!` method when specifying a `workers` number.
30 | # This directive tells Puma to first boot the application and load code
31 | # before forking the application. This takes advantage of Copy On Write
32 | # process behavior so workers use less memory.
33 | #
34 | # preload_app!
35 |
36 | # Allow puma to be restarted by `rails restart` command.
37 | plugin :tmp_restart
38 |
--------------------------------------------------------------------------------
/examples/rails/config/routes.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Rails.application.routes.draw do
4 | get '/enqueue/dummy', to: 'enqueue_job#dummy'
5 | end
6 |
--------------------------------------------------------------------------------
/examples/rails/config/spring.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Spring.watch(
4 | '.ruby-version',
5 | '.rbenv-vars',
6 | 'tmp/restart.txt',
7 | 'tmp/caching-dev.txt'
8 | )
9 |
--------------------------------------------------------------------------------
/examples/rails/config/storage.yml:
--------------------------------------------------------------------------------
1 | test:
2 | service: Disk
3 | root: <%= Rails.root.join("tmp/storage") %>
4 |
5 | local:
6 | service: Disk
7 | root: <%= Rails.root.join("storage") %>
8 |
9 | # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
10 | # amazon:
11 | # service: S3
12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
14 | # region: us-east-1
15 | # bucket: your_own_bucket
16 |
17 | # Remember not to checkin your GCS keyfile to a repository
18 | # google:
19 | # service: GCS
20 | # project: your_project
21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
22 | # bucket: your_own_bucket
23 |
24 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
25 | # microsoft:
26 | # service: AzureStorage
27 | # storage_account_name: your_account_name
28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
29 | # container: your_container_name
30 |
31 | # mirror:
32 | # service: Mirror
33 | # primary: local
34 | # mirrors: [ amazon, google, microsoft ]
35 |
--------------------------------------------------------------------------------
/examples/rails/lib/assets/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keypup-io/cloudtasker/c9b50be501595b7bafdcd9ba852bcf87e2cd34bd/examples/rails/lib/assets/.keep
--------------------------------------------------------------------------------
/examples/rails/log/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keypup-io/cloudtasker/c9b50be501595b7bafdcd9ba852bcf87e2cd34bd/examples/rails/log/.keep
--------------------------------------------------------------------------------
/examples/rails/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The page you were looking for doesn't exist.
62 |
You may have mistyped the address or the page may have moved.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/examples/rails/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The change you wanted was rejected.
62 |
Maybe you tried to change something you didn't have access to.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/examples/rails/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
We're sorry, but something went wrong.
62 |
63 |
If you are the application owner check the logs for more information.
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/examples/rails/public/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keypup-io/cloudtasker/c9b50be501595b7bafdcd9ba852bcf87e2cd34bd/examples/rails/public/apple-touch-icon-precomposed.png
--------------------------------------------------------------------------------
/examples/rails/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keypup-io/cloudtasker/c9b50be501595b7bafdcd9ba852bcf87e2cd34bd/examples/rails/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/examples/rails/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keypup-io/cloudtasker/c9b50be501595b7bafdcd9ba852bcf87e2cd34bd/examples/rails/public/favicon.ico
--------------------------------------------------------------------------------
/examples/rails/storage/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keypup-io/cloudtasker/c9b50be501595b7bafdcd9ba852bcf87e2cd34bd/examples/rails/storage/.keep
--------------------------------------------------------------------------------
/examples/rails/tmp/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keypup-io/cloudtasker/c9b50be501595b7bafdcd9ba852bcf87e2cd34bd/examples/rails/tmp/.keep
--------------------------------------------------------------------------------
/examples/rails/tmp/development_secret.txt:
--------------------------------------------------------------------------------
1 | 6a0868194e931706b65bfaee87eea7197c9c4725dea92499f24461fe9c9cf8f3d3b1a2874ccab440ef36b898ae47bba4621c01343821afcb6b67186977fb80ea
--------------------------------------------------------------------------------
/examples/sinatra/.ruby-version:
--------------------------------------------------------------------------------
1 | ruby-3.3.0
2 |
--------------------------------------------------------------------------------
/examples/sinatra/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source 'https://rubygems.org'
4 | git_source(:github) { |repo| "https://github.com/#{repo}.git" }
5 |
6 | ruby '~> 3.3'
7 |
8 | # Server
9 | gem 'puma'
10 |
11 | # Web framework
12 | gem 'rackup', '~> 2.1'
13 | gem 'sinatra'
14 |
15 | # Background jobs via Cloud Tasks
16 | gem 'cloudtasker', path: '../../'
17 |
18 | group :development do
19 | gem 'foreman'
20 | end
21 |
--------------------------------------------------------------------------------
/examples/sinatra/Procfile:
--------------------------------------------------------------------------------
1 | web: bundle exec ruby app.rb -p 3000
2 | worker: bundle exec cloudtasker -q critical,2 -q default,3
--------------------------------------------------------------------------------
/examples/sinatra/README.md:
--------------------------------------------------------------------------------
1 | # Example usage with Sinatra
2 |
3 | ## Run using the local processing server
4 |
5 | 1. Install dependencies: `bundle install`
6 | 2. Launch the server: `foreman start`
7 | 3. Open a Sinatra console: `./bin/console`
8 | 4. Enqueue workers:
9 | ```ruby
10 | DummyWorker.perform_async
11 | ```
12 |
13 | ## Run using Cloud Tasks
14 |
15 | 1. Ensure that your [Google Cloud SDK](https://cloud.google.com/sdk/docs/quickstarts) is setup.
16 | 2. Install dependencies: `bundle install`
17 | 3. Start an [ngrok](https://ngrok.com) tunnel: `ngrok http 3000`
18 | 4. Edit the [initializer](./config/initializers/cloudtasker.rb)
19 | * Add the configuration of your GCP queue
20 | * Set `config.processor_host` to the ngrok http or https url
21 | * Set `config.mode` to `:production`
22 | 5. Launch the server: `foreman start web`
23 | 6. Open a Sinatra console: `./bin/console`
24 | 7. Setup your queues on Google Cloud Tasks:
25 | ```ruby
26 | # Default and critical queues
27 | Cloudtasker::CloudTask.setup_production_queue
28 | Cloudtasker::CloudTask.setup_production_queue(name: 'critical')
29 | ```
30 | 8. Enqueue workers:
31 | ```ruby
32 | DummyWorker.perform_async
33 | ```
34 |
--------------------------------------------------------------------------------
/examples/sinatra/app.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'sinatra'
4 |
5 | # Require project files
6 | Dir.glob('./config/initializers/*.rb').sort.each { |file| require file }
7 | Dir.glob('./app/workers/*.rb').sort.each { |file| require file }
8 |
9 | #---------------------------------------------------
10 | # Routes
11 | #---------------------------------------------------
12 |
13 | get '/' do
14 | 'Hello!'
15 | end
16 |
17 | # rubocop:disable Metrics/BlockLength
18 | post '/cloudtasker/run' do
19 | # Capture content and decode content
20 | json_payload = request.body.read
21 | if request.env['HTTP_CONTENT_TRANSFER_ENCODING'].to_s.downcase == 'base64'
22 | json_payload = Base64.decode64(json_payload)
23 | end
24 |
25 | # Authenticate the request
26 | if (signature = request.env['HTTP_X_CLOUDTASKER_SIGNATURE'])
27 | # Verify content signature
28 | Cloudtasker::Authenticator.verify_signature!(signature, json_payload)
29 | else
30 | # Get authorization token from custom header (since v0.14.0) or fallback to
31 | # former authorization header (jobs enqueued by v0.13 and below)
32 | auth_token = request.env['HTTP_X_CLOUDTASKER_AUTHORIZATION'].to_s.split.last ||
33 | request.env['HTTP_AUTHORIZATION'].to_s.split.last
34 |
35 | # Verify the token
36 | Cloudtasker::Authenticator.verify!(auth_token)
37 | end
38 |
39 | # Format job payload
40 | payload = JSON.parse(json_payload)
41 | .merge(
42 | job_retries: request.env[Cloudtasker::Config::RETRY_HEADER].to_i,
43 | task_id: request.env[Cloudtasker::Config::TASK_ID_HEADER]
44 | )
45 |
46 | # Process payload
47 | Cloudtasker::WorkerHandler.execute_from_payload!(payload)
48 | return 204
49 | rescue Cloudtasker::DeadWorkerError
50 | # 205: job will NOT be retried
51 | return 205
52 | rescue Cloudtasker::AuthenticationError
53 | # 401: Unauthorized
54 | return 401
55 | rescue Cloudtasker::InvalidWorkerError
56 | # 404: Job will be retried
57 | return 404
58 | rescue StandardError
59 | # 422: Job will be retried
60 | return 423
61 | end
62 | # rubocop:enable Metrics/BlockLength
63 |
--------------------------------------------------------------------------------
/examples/sinatra/app/workers/batch_worker.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class BatchWorker
4 | include Cloudtasker::Worker
5 |
6 | def perform(level = 0, instance = 0)
7 | logger.info("#{self.class} level=#{level} instance=#{instance} | starting...")
8 | # sleep(1)
9 |
10 | # Enqueue children
11 | 10.times { |n| batch.add(BatchWorker, level + 1, n) } if level < 2
12 |
13 | logger.info("#{self.class} level=#{level} instance=#{instance} | done!")
14 | end
15 |
16 | def on_child_complete(child)
17 | msg = [
18 | "#{self.class} level=#{job_args[0].to_i} instance=#{job_args[1].to_i}",
19 | "on_child_complete level=#{child.job_args[0].to_i} instance=#{child.job_args[1].to_i}"
20 | ].join(' | ')
21 | logger.info(msg)
22 | end
23 |
24 | def on_batch_node_complete(child)
25 | msg = [
26 | "#{self.class} level=#{job_args[0].to_i} instance=#{job_args[1].to_i}",
27 | "on_batch_node_complete level=#{child.job_args[0].to_i} instance=#{child.job_args[1].to_i}"
28 | ].join(' | ')
29 | logger.info(msg)
30 | end
31 |
32 | def on_batch_complete
33 | msg = [
34 | "#{self.class} level=#{job_args[0].to_i} instance=#{job_args[1].to_i}",
35 | 'on_batch_complete'
36 | ].join(' | ')
37 | logger.info(msg)
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/examples/sinatra/app/workers/cron_worker.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class CronWorker
4 | include Cloudtasker::Worker
5 |
6 | def perform(arg1)
7 | logger.info("#{self.class} starting with arg1=#{arg1}...")
8 | sleep(3)
9 | logger.info("#{self.class} done!")
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/examples/sinatra/app/workers/dummy_worker.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class DummyWorker
4 | include Cloudtasker::Worker
5 |
6 | def perform
7 | logger.info('Dummy worker starting...')
8 | sleep(3)
9 | logger.info('Dummy worker done!')
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/examples/sinatra/app/workers/uniq_executing_worker.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class UniqExecutingWorker
4 | include Cloudtasker::Worker
5 |
6 | cloudtasker_options lock: :while_executing, on_conflict: :reschedule, lock_ttl: 90
7 |
8 | def unique_args(args)
9 | [args[0], args[1]]
10 | end
11 |
12 | def perform(arg1, arg2, arg3)
13 | logger.info("#{self.class} with args=#{[arg1, arg2, arg3].inspect} starting...")
14 | sleep(10)
15 | logger.info("#{self.class} with args=#{[arg1, arg2, arg3].inspect} done!")
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/examples/sinatra/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require 'bundler/setup'
5 | require File.expand_path('../app.rb', __dir__)
6 |
7 | require 'irb'
8 | IRB.start(__FILE__)
9 |
--------------------------------------------------------------------------------
/examples/sinatra/config/initializers/cloudtasker.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Require cloudtasker and its extensions
4 | require 'cloudtasker'
5 | require 'cloudtasker/unique_job'
6 | require 'cloudtasker/cron'
7 | require 'cloudtasker/batch'
8 |
9 | Cloudtasker.configure do |config|
10 | #
11 | # Secret used to authenticate job requests
12 | #
13 | config.secret = 'some-secret'
14 |
15 | #
16 | # GCP Configuration
17 | #
18 | config.gcp_project_id = 'some-project'
19 | config.gcp_location_id = 'us-east1'
20 | config.gcp_queue_prefix = 'my-app'
21 |
22 | #
23 | # Domain
24 | #
25 | # config.processor_host = 'https://xxxx.ngrok.io'
26 | #
27 | config.processor_host = 'http://localhost:3000'
28 |
29 | #
30 | # Uncomment to process tasks via Cloud Task.
31 | # Requires a ngrok tunnel.
32 | #
33 | # config.mode = :production
34 | end
35 |
36 | #
37 | # Setup cron job
38 | #
39 | # Cloudtasker::Cron::Schedule.load_from_hash!(
40 | # 'my_worker' => {
41 | # 'worker' => 'CronWorker',
42 | # 'cron' => '* * * * *',
43 | # 'queue' => 'critical',
44 | # 'args' => ['foo']
45 | # }
46 | # )
47 |
--------------------------------------------------------------------------------
/exe/cloudtasker:
--------------------------------------------------------------------------------
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
31 |
--------------------------------------------------------------------------------
/gemfiles/.bundle/config:
--------------------------------------------------------------------------------
1 | ---
2 | BUNDLE_RETRY: "1"
3 |
--------------------------------------------------------------------------------
/gemfiles/google_cloud_tasks_1.0.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal", github: "thoughtbot/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: "../"
18 |
--------------------------------------------------------------------------------
/gemfiles/google_cloud_tasks_1.1.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal", github: "thoughtbot/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: "../"
18 |
--------------------------------------------------------------------------------
/gemfiles/google_cloud_tasks_1.2.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal", github: "thoughtbot/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: "../"
18 |
--------------------------------------------------------------------------------
/gemfiles/google_cloud_tasks_1.3.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal", github: "thoughtbot/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: "../"
18 |
--------------------------------------------------------------------------------
/gemfiles/google_cloud_tasks_1.4.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal", github: "thoughtbot/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: "../"
18 |
--------------------------------------------------------------------------------
/gemfiles/google_cloud_tasks_1.5.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal", github: "thoughtbot/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: "../"
18 |
--------------------------------------------------------------------------------
/gemfiles/google_cloud_tasks_2.0.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal", github: "thoughtbot/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: "../"
18 |
--------------------------------------------------------------------------------
/gemfiles/google_cloud_tasks_2.1.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal", github: "thoughtbot/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: "../"
18 |
--------------------------------------------------------------------------------
/gemfiles/rails_5.2.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal", github: "thoughtbot/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", "~> 5.2.0"
16 | gem "rspec-rails"
17 |
18 | gemspec path: "../"
19 |
--------------------------------------------------------------------------------
/gemfiles/rails_6.0.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal", github: "thoughtbot/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.0.0"
16 | gem "rspec-rails"
17 |
18 | gemspec path: "../"
19 |
--------------------------------------------------------------------------------
/gemfiles/rails_6.1.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal", github: "thoughtbot/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 |
18 | gemspec path: "../"
19 |
--------------------------------------------------------------------------------
/gemfiles/rails_7.0.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal", github: "thoughtbot/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: "../"
19 |
--------------------------------------------------------------------------------
/gemfiles/rails_7.1.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal", github: "thoughtbot/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: "../"
19 |
--------------------------------------------------------------------------------
/gemfiles/semantic_logger_3.4.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal", github: "thoughtbot/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: "../"
17 |
--------------------------------------------------------------------------------
/gemfiles/semantic_logger_4.6.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal", github: "thoughtbot/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: "../"
17 |
--------------------------------------------------------------------------------
/gemfiles/semantic_logger_4.7.0.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal", github: "thoughtbot/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: "../"
17 |
--------------------------------------------------------------------------------
/gemfiles/semantic_logger_4.7.2.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal", github: "thoughtbot/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: "../"
17 |
--------------------------------------------------------------------------------
/lib/active_job/queue_adapters/cloudtasker_adapter.rb:
--------------------------------------------------------------------------------
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
90 |
--------------------------------------------------------------------------------
/lib/cloudtasker.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'active_support/core_ext/object/blank'
4 | require 'active_support/core_ext/object/try'
5 | require 'active_support/core_ext/string/inflections'
6 | require 'active_support/core_ext/string/filters'
7 | require 'active_support/security_utils'
8 |
9 | require 'cloudtasker/version'
10 | require 'cloudtasker/config'
11 |
12 | require 'cloudtasker/authentication_error'
13 | require 'cloudtasker/dead_worker_error'
14 | require 'cloudtasker/retry_worker_error'
15 | require 'cloudtasker/invalid_worker_error'
16 | require 'cloudtasker/missing_worker_arguments_error'
17 | require 'cloudtasker/max_task_size_exceeded_error'
18 |
19 | require 'cloudtasker/middleware/chain'
20 | require 'cloudtasker/authenticator'
21 | require 'cloudtasker/cloud_task'
22 | require 'cloudtasker/worker_logger'
23 | require 'cloudtasker/worker_handler'
24 | require 'cloudtasker/meta_store'
25 | require 'cloudtasker/worker'
26 |
27 | # Define and manage Cloud Task based workers
28 | module Cloudtasker
29 | attr_writer :config
30 |
31 | #
32 | # Cloudtasker configurator.
33 | #
34 | def self.configure
35 | yield(config)
36 | end
37 |
38 | #
39 | # Return the Cloudtasker configuration.
40 | #
41 | # @return [Cloudtasker::Config] The Cloudtasker configuration.
42 | #
43 | def self.config
44 | @config ||= Config.new
45 | end
46 |
47 | #
48 | # Return the Cloudtasker logger.
49 | #
50 | # @return [Logger] The Cloudtasker logger.
51 | #
52 | def self.logger
53 | config.logger
54 | end
55 | end
56 |
57 | require 'cloudtasker/engine' if defined?(Rails::Engine)
58 |
--------------------------------------------------------------------------------
/lib/cloudtasker/authentication_error.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Cloudtasker
4 | class AuthenticationError < StandardError
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/lib/cloudtasker/authenticator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'openssl'
4 |
5 | module Cloudtasker
6 | # Manage token generation and verification
7 | module Authenticator
8 | module_function
9 |
10 | # Algorithm used to sign the verification token
11 | JWT_ALG = 'HS256'
12 |
13 | #
14 | # Return the cloudtasker configuration. See Cloudtasker#configure.
15 | #
16 | # @return [Cloudtasker::Config] The library configuration.
17 | #
18 | def config
19 | Cloudtasker.config
20 | end
21 |
22 | #
23 | # A Json Web Token (JWT) which will be used by the processor
24 | # to authenticate the job.
25 | #
26 | # @return [String] The jwt token
27 | #
28 | def verification_token
29 | JWT.encode({ iat: Time.now.to_i }, config.secret, JWT_ALG)
30 | end
31 |
32 | #
33 | # The Authorization header content
34 | #
35 | # @return [String] The Bearer authorization header
36 | #
37 | def bearer_token
38 | "Bearer #{verification_token}"
39 | end
40 |
41 | #
42 | # Verify a bearer token (jwt token)
43 | #
44 | # @param [String] bearer_token The token to verify.
45 | #
46 | # @return [Boolean] Return true if the token is valid
47 | #
48 | def verify(bearer_token)
49 | JWT.decode(bearer_token, config.secret)
50 | rescue JWT::VerificationError, JWT::DecodeError
51 | false
52 | end
53 |
54 | #
55 | # Verify a bearer token and raise a `Cloudtasker::AuthenticationError`
56 | # if the token is invalid.
57 | #
58 | # @param [String] bearer_token The token to verify.
59 | #
60 | # @return [Boolean] Return true if the token is valid
61 | #
62 | def verify!(bearer_token)
63 | verify(bearer_token) || raise(AuthenticationError)
64 | end
65 |
66 | #
67 | # Generate a signature for a payload
68 | #
69 | # @param [String] payload The JSON payload
70 | #
71 | # @return [String] The HMAC signature
72 | #
73 | def sign_payload(payload)
74 | OpenSSL::HMAC.hexdigest('sha256', config.secret, payload)
75 | end
76 |
77 | #
78 | # Verify that a signature matches the payload and raise a `Cloudtasker::AuthenticationError`
79 | # if the signature is invalid.
80 | #
81 | # @param [String] signature The tested signature
82 | # @param [String] payload The JSON payload
83 | #
84 | # @return [Boolean] Return true if the signature is valid
85 | #
86 | def verify_signature!(signature, payload)
87 | ActiveSupport::SecurityUtils.secure_compare(signature, sign_payload(payload)) || raise(AuthenticationError)
88 | end
89 | end
90 | end
91 |
--------------------------------------------------------------------------------
/lib/cloudtasker/batch.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative 'batch/middleware'
4 |
5 | Cloudtasker::Batch::Middleware.configure
6 |
--------------------------------------------------------------------------------
/lib/cloudtasker/batch/extension/worker.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Cloudtasker
4 | module Batch
5 | module Extension
6 | # Include batch related methods onto Cloudtasker::Worker
7 | # See: Cloudtasker::Batch::Middleware#configure
8 | module Worker
9 | attr_accessor :batch, :parent_batch
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/cloudtasker/batch/middleware.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'cloudtasker/redis_client'
4 |
5 | require_relative 'extension/worker'
6 | require_relative 'batch_progress'
7 | require_relative 'job'
8 |
9 | require_relative 'middleware/server'
10 |
11 | module Cloudtasker
12 | module Batch
13 | # Registration module
14 | module Middleware
15 | def self.configure
16 | Cloudtasker.configure do |config|
17 | config.server_middleware { |c| c.add(Middleware::Server) }
18 | end
19 |
20 | # Inject worker extension on main module
21 | Cloudtasker::Worker.include(Extension::Worker)
22 | end
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/cloudtasker/batch/middleware/server.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Cloudtasker
4 | module Batch
5 | module Middleware
6 | # Server middleware, invoked when jobs are executed
7 | class Server
8 | def call(worker, **_kwargs, &block)
9 | Job.for(worker).execute(&block)
10 | end
11 | end
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/cloudtasker/cron.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative 'cron/middleware'
4 |
5 | Cloudtasker::Cron::Middleware.configure
6 |
--------------------------------------------------------------------------------
/lib/cloudtasker/cron/middleware.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'cloudtasker/redis_client'
4 |
5 | require_relative 'schedule'
6 | require_relative 'job'
7 | require_relative 'middleware/server'
8 |
9 | module Cloudtasker
10 | module Cron
11 | # Registration module
12 | module Middleware
13 | def self.configure
14 | Cloudtasker.configure do |config|
15 | config.server_middleware { |c| c.add(Middleware::Server) }
16 | end
17 | end
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/cloudtasker/cron/middleware/server.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Cloudtasker
4 | module Cron
5 | module Middleware
6 | # Server middleware, invoked when jobs are executed
7 | class Server
8 | def call(worker, **_kwargs, &block)
9 | Job.new(worker).execute(&block)
10 | end
11 | end
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/cloudtasker/dead_worker_error.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Cloudtasker
4 | class DeadWorkerError < StandardError
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/lib/cloudtasker/engine.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Cloudtasker
4 | # Cloudtasker Rails engine
5 | class Engine < ::Rails::Engine
6 | isolate_namespace Cloudtasker
7 |
8 | config.before_initialize do
9 | # Mount cloudtasker processing endpoint
10 | Rails.application.routes.append do
11 | mount Cloudtasker::Engine, at: '/cloudtasker'
12 | end
13 |
14 | # Add ActiveJob adapter
15 | require 'active_job/queue_adapters/cloudtasker_adapter' if defined?(::ActiveJob::Railtie)
16 | end
17 |
18 | config.generators do |g|
19 | g.test_framework :rspec, fixture: false
20 | g.assets false
21 | g.helper false
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/cloudtasker/invalid_worker_error.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Cloudtasker
4 | class InvalidWorkerError < StandardError
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/lib/cloudtasker/local_server.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'cloudtasker/backend/redis_task'
4 |
5 | module Cloudtasker
6 | # Process jobs stored in Redis.
7 | # Only to be used in development.
8 | class LocalServer
9 | # Max number of task requests sent to the processing server
10 | CONCURRENCY = (ENV['CLOUDTASKER_CONCURRENCY'] || 5).to_i
11 |
12 | # Default number of threads to allocate to process a specific queue
13 | QUEUE_CONCURRENCY = 1
14 |
15 | # Job Polling. How frequently to poll jobs in redis.
16 | JOB_POLLING_FREQUENCY = 0.5 # seconds
17 |
18 | #
19 | # Stop the local server.
20 | #
21 | def stop
22 | @done = true
23 |
24 | # Terminate threads and repush tasks
25 | @threads&.values&.flatten&.each do |t|
26 | t.terminate
27 | t['task']&.retry_later(0, is_error: false)
28 | end
29 |
30 | # Wait for main server to be done
31 | sleep 1 while @start&.alive?
32 | end
33 |
34 | #
35 | # Start the local server
36 | #
37 | # @param [Hash] opts Server options.
38 | #
39 | #
40 | def start(opts = {})
41 | # Extract queues to process
42 | queues = opts[:queues].to_a.any? ? opts[:queues] : [[nil, CONCURRENCY]]
43 |
44 | # Display start banner
45 | queue_labels = queues.map { |n, c| "#{n || 'all'}=#{c || QUEUE_CONCURRENCY}" }.join(' ')
46 | Cloudtasker.logger.info("[Cloudtasker/Server] Processing queues: #{queue_labels}")
47 |
48 | # Start processing queues
49 | @start ||= Thread.new do
50 | until @done
51 | queues.each { |(n, c)| process_jobs(n, c) }
52 | sleep JOB_POLLING_FREQUENCY
53 | end
54 | Cloudtasker.logger.info('[Cloudtasker/Server] Local server exiting...')
55 | end
56 | end
57 |
58 | #
59 | # Process enqueued workers.
60 | #
61 | #
62 | def process_jobs(queue = nil, concurrency = nil)
63 | @threads ||= {}
64 | @threads[queue] ||= []
65 | max_threads = (concurrency || QUEUE_CONCURRENCY).to_i
66 |
67 | # Remove any done thread
68 | @threads[queue].select!(&:alive?)
69 |
70 | # Process tasks
71 | while @threads[queue].count < max_threads && (task = Cloudtasker::Backend::RedisTask.pop(queue))
72 | @threads[queue] << Thread.new(task) { |t| process_task(t) }
73 | end
74 | end
75 |
76 | #
77 | # Process a given task
78 | #
79 | # @param [Cloudtasker::CloudTask] task The task to process
80 | #
81 | def process_task(task)
82 | Thread.current['task'] = task
83 | Thread.current['attempts'] = 0
84 |
85 | # Deliver task
86 | begin
87 | Thread.current['task']&.deliver
88 | rescue Errno::EBADF, Errno::ECONNREFUSED => e
89 | raise(e) unless Thread.current['attempts'] < 3
90 |
91 | # Retry on connection error, in case the web server is not
92 | # started yet.
93 | Thread.current['attempts'] += 1
94 | sleep(3)
95 | retry
96 | end
97 | end
98 | end
99 | end
100 |
--------------------------------------------------------------------------------
/lib/cloudtasker/max_task_size_exceeded_error.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Cloudtasker
4 | # Handle Cloud Task size quota
5 | # See: https://cloud.google.com/appengine/quotas#Task_Queue
6 | #
7 | class MaxTaskSizeExceededError < StandardError
8 | MSG = 'The size of Cloud Tasks must not exceed 100KB'
9 |
10 | def initialize(msg = MSG)
11 | super
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/cloudtasker/meta_store.rb:
--------------------------------------------------------------------------------
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 [] 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] *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
87 |
--------------------------------------------------------------------------------
/lib/cloudtasker/missing_worker_arguments_error.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Cloudtasker
4 | class MissingWorkerArgumentsError < StandardError
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/lib/cloudtasker/retry_worker_error.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Cloudtasker
4 | class RetryWorkerError < StandardError
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/lib/cloudtasker/storable.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative 'storable/worker'
4 |
--------------------------------------------------------------------------------
/lib/cloudtasker/storable/worker.rb:
--------------------------------------------------------------------------------
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] *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>] 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
79 |
--------------------------------------------------------------------------------
/lib/cloudtasker/testing.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'cloudtasker/backend/memory_task'
4 |
5 | module Cloudtasker
6 | # Enable/Disable test mode for Cloudtasker
7 | module Testing
8 | module_function
9 |
10 | #
11 | # Set the test mode, either permanently or
12 | # temporarily (via block).
13 | #
14 | # @param [Symbol] mode The test mode.
15 | #
16 | # @return [Symbol] The test mode.
17 | #
18 | def switch_test_mode(mode)
19 | if block_given?
20 | current_mode = @test_mode
21 | begin
22 | @test_mode = mode
23 | yield
24 | ensure
25 | @test_mode = current_mode
26 | end
27 | else
28 | @test_mode = mode
29 | end
30 | end
31 |
32 | #
33 | # Set cloudtasker to real mode temporarily
34 | #
35 | # @param [Proc] &block The block to run in real mode
36 | #
37 | def enable!(&block)
38 | switch_test_mode(:enabled, &block)
39 | end
40 |
41 | #
42 | # Set cloudtasker to fake mode temporarily
43 | #
44 | # @param [Proc] &block The block to run in fake mode
45 | #
46 | def fake!(&block)
47 | switch_test_mode(:fake, &block)
48 | end
49 |
50 | #
51 | # Set cloudtasker to inline mode temporarily
52 | #
53 | # @param [Proc] &block The block to run in inline mode
54 | #
55 | def inline!(&block)
56 | switch_test_mode(:inline, &block)
57 | end
58 |
59 | #
60 | # Return true if Cloudtasker is enabled.
61 | #
62 | def enabled?
63 | !@test_mode || @test_mode == :enabled
64 | end
65 |
66 | #
67 | # Return true if Cloudtasker is in fake mode.
68 | #
69 | # @return [Boolean] True if jobs must be processed through drain calls.
70 | #
71 | def fake?
72 | @test_mode == :fake
73 | end
74 |
75 | #
76 | # Return true if Cloudtasker is in inline mode.
77 | #
78 | # @return [Boolean] True if jobs are run inline.
79 | #
80 | def inline?
81 | @test_mode == :inline
82 | end
83 |
84 | #
85 | # Return true if tasks should be managed in memory.
86 | #
87 | # @return [Boolean] True if jobs are managed in memory.
88 | #
89 | def in_memory?
90 | !enabled?
91 | end
92 | end
93 |
94 | # Add extra methods for testing purpose
95 | module Worker
96 | #
97 | # Clear all jobs.
98 | #
99 | def self.clear_all
100 | Backend::MemoryTask.clear
101 | end
102 |
103 | #
104 | # Run all the jobs.
105 | #
106 | # @return [Array] The return values of the workers perform method.
107 | #
108 | def self.drain_all
109 | Backend::MemoryTask.drain
110 | end
111 |
112 | # Module class methods
113 | module ClassMethods
114 | #
115 | # Return all jobs related to this worker class.
116 | #
117 | # @return [Array] The list of tasks
118 | #
119 | def jobs
120 | Backend::MemoryTask.all(to_s)
121 | end
122 |
123 | #
124 | # Run all jobs related to this worker class.
125 | #
126 | # @return [Array] The return values of the workers perform method.
127 | #
128 | def drain
129 | Backend::MemoryTask.drain(to_s)
130 | end
131 | end
132 | end
133 | end
134 |
--------------------------------------------------------------------------------
/lib/cloudtasker/unique_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative 'unique_job/middleware'
4 |
5 | Cloudtasker::UniqueJob::Middleware.configure
6 |
7 | module Cloudtasker
8 | # UniqueJob configurator
9 | module UniqueJob
10 | # The maximum duration a lock can remain in place
11 | # after schedule time.
12 | DEFAULT_LOCK_TTL = 10 * 60 # 10 minutes
13 |
14 | class << self
15 | attr_writer :lock_ttl
16 |
17 | # Configure the middleware
18 | def configure
19 | yield(self)
20 | end
21 |
22 | #
23 | # Return the max TTL for locks
24 | #
25 | # @return [Integer] The lock TTL.
26 | #
27 | def lock_ttl
28 | @lock_ttl || DEFAULT_LOCK_TTL
29 | end
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/cloudtasker/unique_job/conflict_strategy/base_strategy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Cloudtasker
4 | module UniqueJob
5 | module ConflictStrategy
6 | # Base behaviour for conflict strategies
7 | class BaseStrategy
8 | attr_reader :job
9 |
10 | #
11 | # Build a new instance of the class.
12 | #
13 | # @param [Cloudtasker::UniqueJob::Job] job The UniqueJob job
14 | #
15 | def initialize(job)
16 | @job = job
17 | end
18 |
19 | #
20 | # Handling logic to perform when a conflict occurs while
21 | # scheduling a job.
22 | #
23 | # We return nil to flag the job as not scheduled
24 | #
25 | def on_schedule
26 | nil
27 | end
28 |
29 | #
30 | # Handling logic to perform when a conflict occurs while
31 | # executing a job.
32 | #
33 | def on_execute
34 | nil
35 | end
36 | end
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/lib/cloudtasker/unique_job/conflict_strategy/raise.rb:
--------------------------------------------------------------------------------
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
29 |
--------------------------------------------------------------------------------
/lib/cloudtasker/unique_job/conflict_strategy/reject.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Cloudtasker
4 | module UniqueJob
5 | module ConflictStrategy
6 | # This strategy rejects the job on conflict. This is equivalent to "do nothing".
7 | class Reject < BaseStrategy
8 | end
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/cloudtasker/unique_job/conflict_strategy/reschedule.rb:
--------------------------------------------------------------------------------
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
31 |
--------------------------------------------------------------------------------
/lib/cloudtasker/unique_job/lock/base_lock.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Cloudtasker
4 | module UniqueJob
5 | module Lock
6 | # Base behaviour for locks
7 | class BaseLock
8 | attr_reader :job
9 |
10 | #
11 | # Build a new instance of the class.
12 | #
13 | # @param [Cloudtasker::UniqueJob::Job] job The UniqueJob job
14 | #
15 | def initialize(job)
16 | @job = job
17 | end
18 |
19 | #
20 | # Return the worker configuration options.
21 | #
22 | # @return [Hash] The worker configuration options.
23 | #
24 | def options
25 | job.options
26 | end
27 |
28 | #
29 | # Return the strategy to use by default. Can be overriden in each lock.
30 | #
31 | # @return [Cloudtasker::UniqueJob::ConflictStrategy::BaseStrategy] The strategy to use by default.
32 | #
33 | def default_conflict_strategy
34 | ConflictStrategy::Reject
35 | end
36 |
37 | #
38 | # Return the conflict strategy to use on conflict
39 | #
40 | # @return [Cloudtasker::UniqueJob::ConflictStrategy::BaseStrategy] The instantiated strategy.
41 | #
42 | def conflict_instance
43 | @conflict_instance ||=
44 | begin
45 | # Infer lock class and get instance
46 | strategy_name = options[:on_conflict]
47 | strategy_klass = ConflictStrategy.const_get(strategy_name.to_s.split('_').collect(&:capitalize).join)
48 | strategy_klass.new(job)
49 | rescue NameError
50 | default_conflict_strategy.new(job)
51 | end
52 | end
53 |
54 | #
55 | # Lock logic invoked when a job is scheduled (client middleware).
56 | #
57 | def schedule
58 | yield
59 | end
60 |
61 | #
62 | # Lock logic invoked when a job is executed (server middleware).
63 | #
64 | def execute
65 | yield
66 | end
67 | end
68 | end
69 | end
70 | end
71 |
--------------------------------------------------------------------------------
/lib/cloudtasker/unique_job/lock/no_op.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Cloudtasker
4 | module UniqueJob
5 | module Lock
6 | # Equivalent to no lock
7 | class NoOp < BaseLock
8 | end
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/cloudtasker/unique_job/lock/until_executed.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Cloudtasker
4 | module UniqueJob
5 | module Lock
6 | # Conflict if any other job with the same args is scheduled or moved to execution
7 | # while the first job is pending or executing.
8 | class UntilExecuted < BaseLock
9 | #
10 | # Acquire a lock for the job and trigger a conflict
11 | # if the lock could not be acquired.
12 | #
13 | def schedule(&block)
14 | job.lock!
15 | yield
16 | rescue LockError
17 | conflict_instance.on_schedule(&block)
18 | end
19 |
20 | #
21 | # Acquire a lock for the job and trigger a conflict
22 | # if the lock could not be acquired.
23 | #
24 | def execute(&block)
25 | job.lock!
26 | yield
27 | rescue LockError
28 | conflict_instance.on_execute(&block)
29 | ensure
30 | # Unlock the job on any error to avoid deadlocks.
31 | job.unlock!
32 | end
33 | end
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/lib/cloudtasker/unique_job/lock/until_executing.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Cloudtasker
4 | module UniqueJob
5 | module Lock
6 | # Conflict if any other job with the same args is scheduled
7 | # while the first job is pending.
8 | class UntilExecuting < BaseLock
9 | #
10 | # Acquire a lock for the job and trigger a conflict
11 | # if the lock could not be acquired.
12 | #
13 | def schedule(&block)
14 | job.lock!
15 | yield
16 | rescue LockError
17 | conflict_instance.on_schedule(&block)
18 | end
19 |
20 | #
21 | # Release the lock and perform the job.
22 | #
23 | def execute
24 | job.unlock!
25 | yield
26 | end
27 | end
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/lib/cloudtasker/unique_job/lock/while_executing.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Cloudtasker
4 | module UniqueJob
5 | module Lock
6 | # Conflict if any other job with the same args is moved to execution
7 | # while the first job is executing.
8 | class WhileExecuting < BaseLock
9 | #
10 | # Acquire a lock for the job and trigger a conflict
11 | # if the lock could not be acquired.
12 | #
13 | def execute(&block)
14 | job.lock!
15 | yield
16 | rescue LockError
17 | conflict_instance.on_execute(&block)
18 | ensure
19 | # Unlock the job on any error to avoid deadlocks.
20 | job.unlock!
21 | end
22 | end
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/cloudtasker/unique_job/lock_error.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Cloudtasker
4 | module UniqueJob
5 | class LockError < StandardError
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/cloudtasker/unique_job/middleware.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'cloudtasker/redis_client'
4 |
5 | require_relative 'lock_error'
6 |
7 | require_relative 'conflict_strategy/base_strategy'
8 | require_relative 'conflict_strategy/raise'
9 | require_relative 'conflict_strategy/reject'
10 | require_relative 'conflict_strategy/reschedule'
11 |
12 | require_relative 'lock/base_lock'
13 | require_relative 'lock/no_op'
14 | require_relative 'lock/until_executed'
15 | require_relative 'lock/until_executing'
16 | require_relative 'lock/while_executing'
17 |
18 | require_relative 'job'
19 |
20 | require_relative 'middleware/client'
21 | require_relative 'middleware/server'
22 |
23 | module Cloudtasker
24 | module UniqueJob
25 | # Registration module
26 | module Middleware
27 | def self.configure
28 | Cloudtasker.configure do |config|
29 | config.client_middleware { |c| c.add(Middleware::Client) }
30 | config.server_middleware { |c| c.add(Middleware::Server) }
31 | end
32 | end
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/lib/cloudtasker/unique_job/middleware/client.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Cloudtasker
4 | module UniqueJob
5 | module Middleware
6 | # TODO: kwargs to job otherwise it won't get the time_at
7 | # Client middleware, invoked when jobs are scheduled
8 | class Client
9 | def call(worker, _opts = {}, &block)
10 | Job.new(worker).lock_instance.schedule(&block)
11 | end
12 | end
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/cloudtasker/unique_job/middleware/server.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Cloudtasker
4 | module UniqueJob
5 | module Middleware
6 | # Server middleware, invoked when jobs are executed
7 | class Server
8 | def call(worker, **_kwargs, &block)
9 | Job.new(worker).lock_instance.execute(&block)
10 | end
11 | end
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/cloudtasker/version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Cloudtasker
4 | VERSION = '0.14.0'
5 | end
6 |
--------------------------------------------------------------------------------
/lib/cloudtasker/worker_wrapper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'cloudtasker/worker'
4 |
5 | module Cloudtasker
6 | # A worker class used to schedule jobs without actually
7 | # instantiating the worker class. This is useful for middlewares
8 | # needing to enqueue jobs in a Rails initializer. Rails 6 complains
9 | # about instantiating workers in an iniitializer because of autoloading
10 | # in zeitwerk mode.
11 | #
12 | # Downside of this wrapper: any cloudtasker_options specified on on the
13 | # worker_class will be ignored.
14 | #
15 | # See: https://github.com/rails/rails/issues/36363
16 | #
17 | class WorkerWrapper
18 | include Worker
19 |
20 | attr_accessor :worker_name
21 |
22 | #
23 | # Build a new instance of the class.
24 | #
25 | # @param [String] worker_class The name of the worker class.
26 | # @param [Hash] **opts The worker arguments.
27 | #
28 | def initialize(worker_name:, **opts)
29 | @worker_name = worker_name
30 | super(**opts)
31 | end
32 |
33 | #
34 | # Override parent. Return the underlying worker class name.
35 | #
36 | # @return [String] The worker class.
37 | #
38 | def job_class_name
39 | worker_name
40 | end
41 |
42 | #
43 | # Return a new instance of the worker with the same args and metadata
44 | # but with a different id.
45 | #
46 | # @return [Cloudtasker::WorkerWrapper]
47 | #
48 | def new_instance
49 | self.class.new(worker_name: worker_name, job_queue: job_queue, job_args: job_args, job_meta: job_meta)
50 | end
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/lib/tasks/setup_queue.rake:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'cloudtasker/config'
4 | require 'cloudtasker/cloud_task'
5 |
6 | ENV['GOOGLE_AUTH_SUPPRESS_CREDENTIALS_WARNINGS'] ||= 'true'
7 |
8 | namespace :cloudtasker do
9 | desc 'Setup a Cloud Task queue. (default options: ' \
10 | "name=#{Cloudtasker::Config::DEFAULT_JOB_QUEUE}, " \
11 | "concurrency=#{Cloudtasker::Config::DEFAULT_QUEUE_CONCURRENCY}, " \
12 | "retries=#{Cloudtasker::Config::DEFAULT_QUEUE_RETRIES})"
13 | task setup_queue: :environment do
14 | puts Cloudtasker::CloudTask.setup_production_queue(
15 | name: ENV.fetch('name', nil),
16 | concurrency: ENV.fetch('concurrency', nil),
17 | retries: ENV.fetch('retries', nil)
18 | )
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/spec/active_job/queue_adapters/cloudtasker_adapter/job_wrapper_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | if defined?(Rails)
6 | RSpec.describe ActiveJob::QueueAdapters::CloudtaskerAdapter::JobWrapper do
7 | include_context 'of Cloudtasker ActiveJob instantiation'
8 |
9 | subject(:worker) { described_class.new(**example_job_wrapper_args.merge(task_id: '00000001')) }
10 |
11 | let(:example_unreconstructed_job_serialization) do
12 | example_job_serialization.except('job_id', 'queue_name', 'provider_job_id', 'executions', 'priority')
13 | end
14 |
15 | let(:example_reconstructed_job_serialization) do
16 | example_job_serialization.merge(
17 | 'job_id' => worker.job_id,
18 | 'queue_name' => worker.job_queue,
19 | 'provider_job_id' => worker.task_id,
20 | 'executions' => 0,
21 | 'priority' => nil
22 | )
23 | end
24 |
25 | describe '#perform' do
26 | it "calls 'ActiveJob::Base.execute' with the job serialization" do
27 | expect(ActiveJob::Base).to receive(:execute).with(example_reconstructed_job_serialization)
28 | worker.perform(example_unreconstructed_job_serialization)
29 | end
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/spec/active_job/queue_adapters/cloudtasker_adapter_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | if defined?(Rails)
6 | RSpec.describe ActiveJob::QueueAdapters::CloudtaskerAdapter do
7 | include_context 'of Cloudtasker ActiveJob instantiation'
8 |
9 | subject(:adapter) { described_class.new }
10 |
11 | let(:example_job_wrapper_double) do
12 | instance_double("#{described_class.name}::JobWrapper", example_job_wrapper_args)
13 | .tap { |double| allow(double).to receive(:schedule) }
14 | end
15 |
16 | around { |e| Timecop.freeze { e.run } }
17 | before { allow(described_class::JobWrapper).to receive(:new).and_return(example_job_wrapper_double) }
18 |
19 | shared_examples 'of instantiating a Cloudtasker JobWrapper from ActiveJob' do
20 | it 'instantiates a new CloudtaskerAdapter JobWrapper for the given job' do
21 | expect(described_class::JobWrapper).to receive(:new).with(example_job_wrapper_args)
22 | adapter.enqueue(example_job)
23 | end
24 | end
25 |
26 | describe '#enqueue' do
27 | include_examples 'of instantiating a Cloudtasker JobWrapper from ActiveJob'
28 |
29 | it 'enqueues the new CloudtaskerAdapter JobWrapper to execute' do
30 | expect(example_job_wrapper_double).to receive(:schedule)
31 | adapter.enqueue(example_job)
32 | end
33 | end
34 |
35 | describe '#enqueue_at' do
36 | let(:example_execution_timestamp) { 1.week.from_now.to_i }
37 | let(:expected_execution_time) { Time.at(example_execution_timestamp) }
38 |
39 | include_examples 'of instantiating a Cloudtasker JobWrapper from ActiveJob'
40 |
41 | it 'enqueues the new CloudtaskerAdapter JobWrapper to execute at the given time' do
42 | expect(example_job_wrapper_double).to receive(:schedule).with(time_at: expected_execution_time)
43 | adapter.enqueue_at(example_job, example_execution_timestamp)
44 | end
45 | end
46 |
47 | describe '#enqueue_after_transaction_commit?' do
48 | it { expect(adapter).to be_enqueue_after_transaction_commit }
49 | end
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/spec/cloudtasker/authenticator_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Cloudtasker::Authenticator do
4 | let(:config) { Cloudtasker.config }
5 |
6 | describe '.verification_token' do
7 | subject { described_class.verification_token }
8 |
9 | let(:expected_token) { JWT.encode({ iat: Time.now.to_i }, config.secret, described_class::JWT_ALG) }
10 |
11 | around { |e| Timecop.freeze { e.run } }
12 |
13 | it { is_expected.to eq(expected_token) }
14 | end
15 |
16 | describe '.bearer_token' do
17 | subject { described_class.bearer_token }
18 |
19 | let(:verification_token) { '123456789' }
20 |
21 | before { expect(described_class).to receive(:verification_token).and_return(verification_token) }
22 | it { is_expected.to eq("Bearer #{verification_token}") }
23 | end
24 |
25 | describe '.verify' do
26 | subject { described_class.verify(token) }
27 |
28 | let(:token) { JWT.encode({ iat: Time.now.to_i }, secret, described_class::JWT_ALG) }
29 |
30 | context 'with valid token' do
31 | let(:secret) { config.secret }
32 |
33 | it { is_expected.to be_truthy }
34 | end
35 |
36 | context 'with invalid token' do
37 | let(:secret) { "#{config.secret}a" }
38 |
39 | it { is_expected.to be_falsey }
40 | end
41 | end
42 |
43 | describe '.verify!' do
44 | subject(:verify!) { described_class.verify!(token) }
45 |
46 | let(:token) { JWT.encode({ iat: Time.now.to_i }, secret, described_class::JWT_ALG) }
47 |
48 | context 'with valid token' do
49 | let(:secret) { config.secret }
50 |
51 | it { is_expected.to be_truthy }
52 | end
53 |
54 | context 'with invalid token' do
55 | let(:secret) { "#{config.secret}a" }
56 |
57 | it { expect { verify! }.to raise_error(Cloudtasker::AuthenticationError) }
58 | end
59 | end
60 |
61 | describe '.sign_payload' do
62 | subject { described_class.sign_payload(payload) }
63 |
64 | let(:payload) { { 'foo' => 'bar' }.to_json }
65 |
66 | it { is_expected.to eq(OpenSSL::HMAC.hexdigest('sha256', config.secret, payload)) }
67 | end
68 |
69 | describe '.verify_signature!' do
70 | subject(:verify!) { described_class.verify_signature!(signature, payload) }
71 |
72 | let(:payload) { { 'foo' => 'bar' }.to_json }
73 |
74 | context 'with valid token' do
75 | let(:signature) { described_class.sign_payload(payload) }
76 |
77 | it { is_expected.to be_truthy }
78 | end
79 |
80 | context 'with invalid token' do
81 | let(:signature) { 'some-invalid-signature' }
82 |
83 | it { expect { verify! }.to raise_error(Cloudtasker::AuthenticationError) }
84 | end
85 | end
86 | end
87 |
--------------------------------------------------------------------------------
/spec/cloudtasker/batch/extension/worker_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'cloudtasker/batch/middleware'
4 |
5 | RSpec.describe Cloudtasker::Batch::Extension::Worker do
6 | describe '#batch' do
7 | subject { worker.batch }
8 |
9 | let(:worker) { TestWorker.new }
10 |
11 | before { worker.batch = Cloudtasker::Batch::Job.new(worker) }
12 | it { is_expected.to be_a(Cloudtasker::Batch::Job) }
13 | it { is_expected.to have_attributes(worker: worker) }
14 | end
15 |
16 | describe '#parent_batch' do
17 | subject { worker.parent_batch }
18 |
19 | let(:worker) { TestWorker.new }
20 |
21 | before { worker.parent_batch = Cloudtasker::Batch::Job.new(worker) }
22 | it { is_expected.to be_a(Cloudtasker::Batch::Job) }
23 | it { is_expected.to have_attributes(worker: worker) }
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/spec/cloudtasker/batch/middleware/server_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'cloudtasker/batch/middleware'
4 |
5 | RSpec.describe Cloudtasker::Batch::Middleware::Server do
6 | let(:middleware) { described_class.new }
7 |
8 | describe '#call' do
9 | let(:worker) { instance_double(Cloudtasker::Worker) }
10 | let(:job) { instance_double(Cloudtasker::Batch::Job) }
11 |
12 | before { allow(Cloudtasker::Batch::Job).to receive(:for).with(worker).and_return(job) }
13 | before { allow(job).to receive(:execute).and_yield }
14 | it { expect { |b| middleware.call(worker, &b) }.to yield_control }
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/spec/cloudtasker/batch/middleware_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'cloudtasker/cron/middleware'
4 |
5 | RSpec.describe Cloudtasker::Batch::Middleware do
6 | describe '.configure' do
7 | before { described_class.configure }
8 |
9 | it { expect(Cloudtasker.config.server_middleware).to exist(Cloudtasker::Batch::Middleware::Server) }
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/spec/cloudtasker/cron/middleware/server_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'cloudtasker/cron/middleware'
4 |
5 | RSpec.describe Cloudtasker::Cron::Middleware::Server do
6 | let(:middleware) { described_class.new }
7 |
8 | describe '#call' do
9 | let(:worker) { instance_double(Cloudtasker::Worker) }
10 | let(:job) { instance_double(Cloudtasker::Cron::Job) }
11 |
12 | before { allow(Cloudtasker::Cron::Job).to receive(:new).with(worker).and_return(job) }
13 | before { allow(job).to receive(:execute).and_yield }
14 | it { expect { |b| middleware.call(worker, &b) }.to yield_control }
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/spec/cloudtasker/cron/middleware_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'cloudtasker/cron/middleware'
4 |
5 | RSpec.describe Cloudtasker::Cron::Middleware do
6 | describe '.configure' do
7 | before { described_class.configure }
8 |
9 | it { expect(Cloudtasker.config.server_middleware).to exist(Cloudtasker::Cron::Middleware::Server) }
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/spec/cloudtasker/meta_store_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Cloudtasker::MetaStore do
4 | describe '.new' do
5 | let(:hash) { { 'foo' => 'bar' } }
6 | let(:meta) { described_class.new(hash) }
7 |
8 | it { expect(meta.to_h).to eq(JSON.parse(hash.to_json, symbolize_names: true)) }
9 | end
10 |
11 | describe '#set' do
12 | let(:meta) { described_class.new }
13 | let(:key) { 'some_id' }
14 | let(:val) { 'foo' }
15 |
16 | before { meta.set(key, val) }
17 | it { expect(meta.to_h[key.to_sym]).to eq(val) }
18 | end
19 |
20 | describe '#get' do
21 | let(:meta) { described_class.new }
22 | let(:key) { 'some_id' }
23 | let(:val) { 'foo' }
24 |
25 | before { meta.set(key, val) }
26 | it { expect(meta.get(key)).to eq(val) }
27 | end
28 |
29 | describe '#del' do
30 | let(:meta) { described_class.new }
31 | let(:key) { 'some_id' }
32 |
33 | before { meta.set(key, 'foo') }
34 | before { meta.del(key) }
35 | it { expect(meta.to_h).not_to have_key(key.to_sym) }
36 | end
37 |
38 | describe '#to_h' do
39 | subject { described_class.new(hash).to_h }
40 |
41 | let(:hash) { { 'foo' => 'bar' } }
42 |
43 | it { is_expected.to eq(JSON.parse(hash.to_json, symbolize_names: true)) }
44 | end
45 |
46 | describe '#to_json' do
47 | subject { described_class.new(hash).to_json }
48 |
49 | let(:hash) { { 'foo' => 'bar' } }
50 |
51 | it { is_expected.to eq(hash.to_json) }
52 | end
53 |
54 | describe '#==' do
55 | subject { described_class.new(hash) }
56 |
57 | let(:hash) { { 'foo' => 'bar' } }
58 |
59 | context 'with identical hash' do
60 | it { is_expected.to eq(hash) }
61 | end
62 |
63 | context 'with identical meta' do
64 | it { is_expected.to eq(described_class.new(hash)) }
65 | end
66 |
67 | context 'with different object' do
68 | it { is_expected.not_to eq('foo') }
69 | end
70 | end
71 | end
72 |
--------------------------------------------------------------------------------
/spec/cloudtasker/redis_client_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'cloudtasker/redis_client'
4 |
5 | RSpec.describe Cloudtasker::RedisClient do
6 | let(:redis_client) { described_class.new }
7 |
8 | describe '.client' do
9 | subject { described_class.client.with { |c| c } }
10 |
11 | it { expect(described_class.client).to be_a(ConnectionPool) }
12 | it { is_expected.to be_a(Redis) }
13 | it { is_expected.to have_attributes(id: Cloudtasker.config.redis[:url]) }
14 | end
15 |
16 | describe '#client' do
17 | subject { redis_client.client }
18 |
19 | it { is_expected.to eq(described_class.client) }
20 | end
21 |
22 | describe '#fetch' do
23 | subject { redis_client.fetch(key) }
24 |
25 | let(:key) { 'foo' }
26 | let(:content) { { 'foo' => 'bar' } }
27 |
28 | before { redis_client.set(key, content.to_json) }
29 | it { is_expected.to eq(JSON.parse(content.to_json, symbolize_names: true)) }
30 | end
31 |
32 | describe '#write' do
33 | subject { redis_client.fetch(key) }
34 |
35 | let(:key) { 'foo' }
36 | let(:content) { { 'foo' => 'bar' } }
37 |
38 | before { redis_client.write(key, content) }
39 | it { is_expected.to eq(JSON.parse(content.to_json, symbolize_names: true)) }
40 | end
41 |
42 | describe '#clear' do
43 | subject { redis_client.keys }
44 |
45 | before do
46 | redis_client.set('foo', 'bar')
47 | redis_client.clear
48 | end
49 |
50 | it { is_expected.to be_empty }
51 | end
52 |
53 | describe '#with_lock' do
54 | let(:key) { 'cache-key' }
55 | let(:lock_key) { 'cloudtasker/lock/cache-key' }
56 | let(:redis) { instance_double(Redis) }
57 | let(:lock_duration) { 30 }
58 | let(:expected_lock_duration) { described_class::LOCK_DURATION }
59 |
60 | before do
61 | allow(redis_client.client).to receive(:with).and_yield(redis)
62 | allow(redis).to receive(:del)
63 | allow(redis).to receive(:set)
64 | .with(lock_key, true, nx: true, ex: expected_lock_duration)
65 | .and_return(true)
66 | end
67 | after { expect(redis).to have_received(:set) }
68 |
69 | context 'with no max_wait' do
70 | it { expect { |b| redis_client.with_lock(key, &b) }.to yield_control }
71 | end
72 |
73 | context 'with max_wait' do
74 | let(:expected_lock_duration) { lock_duration }
75 |
76 | it { expect { |b| redis_client.with_lock(key, max_wait: lock_duration, &b) }.to yield_control }
77 | end
78 | end
79 |
80 | describe '#search' do
81 | subject { redis_client.search(pattern).sort }
82 |
83 | let(:keys) { 50.times.map { |n| "foo/#{n}" }.sort }
84 |
85 | before { keys.each { |e| redis_client.set(e, true) } }
86 |
87 | context 'with keys matching pattern' do
88 | let(:pattern) { 'foo/*' }
89 |
90 | it { is_expected.to eq(keys) }
91 | end
92 |
93 | context 'with no keys matching' do
94 | let(:pattern) { 'bar/*' }
95 |
96 | it { is_expected.to be_empty }
97 | end
98 | end
99 |
100 | describe '#get' do
101 | subject { redis_client.get(key) }
102 |
103 | let(:key) { 'foo' }
104 | let(:content) { 'bar' }
105 |
106 | before { redis_client.set(key, content) }
107 | it { is_expected.to eq(content) }
108 | end
109 | end
110 |
--------------------------------------------------------------------------------
/spec/cloudtasker/storable/worker_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Cloudtasker::Storable::Worker do
4 | let(:worker_class) { TestStorableWorker }
5 |
6 | describe '.store_cache_key' do
7 | subject { worker_class.store_cache_key(namespace) }
8 |
9 | let(:namespace) { :some_key }
10 |
11 | it { is_expected.to eq(worker_class.cache_key([Cloudtasker::Config::WORKER_STORE_PREFIX, namespace])) }
12 | end
13 |
14 | describe '.push_to_store' do
15 | subject { worker_class.push_to_store(namespace, *args) }
16 |
17 | let(:namespace) { 'some-namespace' }
18 | let(:args) { [1, 'two', { three: true }] }
19 | let(:store_cache_key) { worker_class.store_cache_key(namespace) }
20 |
21 | after { expect(worker_class.redis.lrange(store_cache_key, 0, -1)).to eq([args.to_json]) }
22 | it { is_expected.to eq(1) }
23 | end
24 |
25 | describe '.push_many_to_store' do
26 | subject { worker_class.push_many_to_store(namespace, args_list) }
27 |
28 | let(:namespace) { 'some-namespace' }
29 | let(:args_list) { [[2, 'four'], [1, 'two', { three: true }]] }
30 | let(:store_cache_key) { worker_class.store_cache_key(namespace) }
31 |
32 | after { expect(worker_class.redis.lrange(store_cache_key, 0, -1)).to eq(args_list.map(&:to_json)) }
33 | it { is_expected.to eq(args_list.size) }
34 | end
35 |
36 | describe '.pull_all_from_store' do
37 | subject { worker_class.pull_all_from_store(namespace, page_size: page_size) }
38 |
39 | let(:namespace) { 'some-namespace' }
40 | let(:page_size) { 5 }
41 | let(:results) { [] }
42 | let(:arg_list) { Array.new((page_size * 2) + 1) { |n| [n] } }
43 | let(:store_cache_key) { worker_class.store_cache_key(namespace) }
44 |
45 | before { worker_class.push_many_to_store(namespace, arg_list) }
46 | after { expect(worker_class.redis.lrange(store_cache_key, 0, -1)).to be_empty }
47 |
48 | context 'with no block' do
49 | before { allow(worker_class).to receive(:perform_async) { |*args| results << args } }
50 | after { expect(results.sort).to eq(arg_list.sort) }
51 | it { is_expected.to be_nil }
52 | end
53 |
54 | context 'with block' do
55 | subject do
56 | worker_class.pull_all_from_store(namespace, page_size: page_size) do |args|
57 | results << args
58 | end
59 | end
60 |
61 | before { expect(worker_class).not_to receive(:perform_async) }
62 | after { expect(results.sort).to eq(arg_list.sort) }
63 | it { is_expected.to be_nil }
64 | end
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/spec/cloudtasker/testing_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Cloudtasker::Testing do
4 | before { Cloudtasker::Backend::MemoryTask.clear }
5 | before { TestWorker.has_run = false }
6 | after { described_class.enable! }
7 |
8 | describe '.fake!' do
9 | subject { Cloudtasker::Backend::MemoryTask.all }
10 |
11 | context 'with option set' do
12 | before { described_class.fake! }
13 | before { TestWorker.perform_async(1, 2) }
14 | it { is_expected.to match([be_a(Cloudtasker::Backend::MemoryTask)]) }
15 | end
16 |
17 | context 'with block' do
18 | around do |e|
19 | described_class.fake! { e.run }
20 | expect(described_class).to be_enabled
21 | end
22 | before { TestWorker.perform_async(1, 2) }
23 | it { is_expected.to match([be_a(Cloudtasker::Backend::MemoryTask)]) }
24 | end
25 | end
26 |
27 | describe '.inline!' do
28 | subject { Cloudtasker::Backend::MemoryTask.all }
29 |
30 | context 'with option set' do
31 | before { described_class.inline! }
32 | before { TestWorker.perform_async(1, 2) }
33 | after { expect(TestWorker).to have_run }
34 | it { is_expected.to eq([]) }
35 | end
36 |
37 | context 'with block' do
38 | around do |e|
39 | described_class.inline! { e.run }
40 | expect(described_class).to be_enabled
41 | end
42 | before { TestWorker.perform_async(1, 2) }
43 | after { expect(TestWorker).to have_run }
44 | it { is_expected.to eq([]) }
45 | end
46 | end
47 |
48 | describe 'job draining' do
49 | before { described_class.fake! }
50 | before { TestWorker.perform_async(1, 2) }
51 | before { TestWorker.drain }
52 | it { expect(TestWorker).to have_run }
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/spec/cloudtasker/unique_job/conflict_strategy/raise_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'cloudtasker/unique_job/middleware'
4 |
5 | RSpec.describe Cloudtasker::UniqueJob::ConflictStrategy::Raise do
6 | let(:worker) { TestWorker.new(job_args: [1, 2]) }
7 | let(:job) { Cloudtasker::UniqueJob::Job.new(worker) }
8 | let(:strategy) { described_class.new(job) }
9 |
10 | it_behaves_like Cloudtasker::UniqueJob::ConflictStrategy::BaseStrategy
11 |
12 | describe '#on_schedule' do
13 | it { expect { strategy.on_schedule }.to raise_error(Cloudtasker::UniqueJob::LockError) }
14 | end
15 |
16 | describe '#on_execute' do
17 | it { expect { strategy.on_execute }.to raise_error(Cloudtasker::UniqueJob::LockError) }
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/spec/cloudtasker/unique_job/conflict_strategy/reject_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'cloudtasker/unique_job/middleware'
4 |
5 | RSpec.describe Cloudtasker::UniqueJob::ConflictStrategy::Reject do
6 | let(:worker) { TestWorker.new(job_args: [1, 2]) }
7 | let(:job) { Cloudtasker::UniqueJob::Job.new(worker) }
8 | let(:strategy) { described_class.new(job) }
9 |
10 | it_behaves_like Cloudtasker::UniqueJob::ConflictStrategy::BaseStrategy
11 |
12 | describe '#on_schedule' do
13 | subject { strategy.on_schedule }
14 |
15 | it { is_expected.to be_falsey }
16 | it { expect { |b| strategy.on_schedule(&b) }.not_to yield_control }
17 | end
18 |
19 | describe '#on_execute' do
20 | subject { strategy.on_execute }
21 |
22 | it { is_expected.to be_falsey }
23 | it { expect { |b| strategy.on_execute(&b) }.not_to yield_control }
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/spec/cloudtasker/unique_job/conflict_strategy/reschedule_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'cloudtasker/unique_job/middleware'
4 |
5 | RSpec.describe Cloudtasker::UniqueJob::ConflictStrategy::Reschedule do
6 | let(:worker) { TestWorker.new(job_args: [1, 2]) }
7 | let(:job) { Cloudtasker::UniqueJob::Job.new(worker) }
8 | let(:strategy) { described_class.new(job) }
9 |
10 | it_behaves_like Cloudtasker::UniqueJob::ConflictStrategy::BaseStrategy
11 |
12 | describe '#on_schedule' do
13 | it { expect { |b| strategy.on_schedule(&b) }.to yield_control }
14 | end
15 |
16 | describe '#on_execute' do
17 | subject { strategy.on_execute }
18 |
19 | before { allow(worker).to receive(:reenqueue).with(described_class::RESCHEDULE_DELAY).and_return(true) }
20 | after { expect(worker).to have_received(:reenqueue) }
21 |
22 | it { is_expected.to be_truthy }
23 | it { expect { |b| strategy.on_execute(&b) }.not_to yield_control }
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/spec/cloudtasker/unique_job/lock/no_op_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'cloudtasker/unique_job/middleware'
4 |
5 | RSpec.describe Cloudtasker::UniqueJob::Lock::NoOp do
6 | let(:worker) { TestWorker.new(job_args: [1, 2]) }
7 | let(:job) { Cloudtasker::UniqueJob::Job.new(worker) }
8 | let(:lock) { described_class.new(job) }
9 |
10 | it_behaves_like Cloudtasker::UniqueJob::Lock::BaseLock
11 |
12 | describe '#schedule' do
13 | it { expect { |b| lock.schedule(&b) }.to yield_control }
14 | end
15 |
16 | describe '#execute' do
17 | it { expect { |b| lock.execute(&b) }.to yield_control }
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/spec/cloudtasker/unique_job/lock/until_executed_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'cloudtasker/unique_job/middleware'
4 |
5 | RSpec.describe Cloudtasker::UniqueJob::Lock::UntilExecuted do
6 | let(:worker) { TestWorker.new(job_args: [1, 2]) }
7 | let(:job) { Cloudtasker::UniqueJob::Job.new(worker) }
8 | let(:lock) { described_class.new(job) }
9 |
10 | it_behaves_like Cloudtasker::UniqueJob::Lock::BaseLock
11 |
12 | describe '#schedule' do
13 | context 'with lock available' do
14 | before { allow(job).to receive(:lock!) }
15 | it { expect { |b| lock.schedule(&b) }.to yield_control }
16 | end
17 |
18 | context 'with lock acquired by another job' do
19 | before { allow(job).to receive(:lock!).and_raise(Cloudtasker::UniqueJob::LockError) }
20 | before { allow(lock.conflict_instance).to receive(:on_schedule) }
21 | after { expect(lock.conflict_instance).to have_received(:on_schedule) { |&b| expect(b).to be_a(Proc) } }
22 | it { expect { |b| lock.schedule(&b) }.not_to yield_control }
23 | end
24 | end
25 |
26 | describe '#execute' do
27 | before { allow(job).to receive(:lock!) }
28 | before { allow(job).to receive(:unlock!) }
29 | after { expect(job).to have_received(:unlock!) }
30 |
31 | context 'with lock available' do
32 | it { expect { |b| lock.execute(&b) }.to yield_control }
33 | end
34 |
35 | context 'with lock acquired by another job' do
36 | before { allow(job).to receive(:lock!).and_raise(Cloudtasker::UniqueJob::LockError) }
37 | before { allow(lock.conflict_instance).to receive(:on_execute) }
38 | after { expect(lock.conflict_instance).to have_received(:on_execute) { |&b| expect(b).to be_a(Proc) } }
39 | it { expect { |b| lock.execute(&b) }.not_to yield_control }
40 | end
41 |
42 | context 'with runtime error' do
43 | let(:error) { ArgumentError }
44 | let(:block) { proc { raise(error) } }
45 |
46 | it { expect { lock.execute(&block) }.to raise_error(error) }
47 | end
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/spec/cloudtasker/unique_job/lock/until_executing_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'cloudtasker/unique_job/middleware'
4 |
5 | RSpec.describe Cloudtasker::UniqueJob::Lock::UntilExecuting do
6 | let(:worker) { TestWorker.new(job_args: [1, 2]) }
7 | let(:job) { Cloudtasker::UniqueJob::Job.new(worker) }
8 | let(:lock) { described_class.new(job) }
9 |
10 | it_behaves_like Cloudtasker::UniqueJob::Lock::BaseLock
11 |
12 | describe '#schedule' do
13 | context 'with lock available' do
14 | before { allow(job).to receive(:lock!) }
15 | it { expect { |b| lock.schedule(&b) }.to yield_control }
16 | end
17 |
18 | context 'with lock acquired by another job' do
19 | before { allow(job).to receive(:lock!).and_raise(Cloudtasker::UniqueJob::LockError) }
20 | before { allow(lock.conflict_instance).to receive(:on_schedule) }
21 | after { expect(lock.conflict_instance).to have_received(:on_schedule) { |&b| expect(b).to be_a(Proc) } }
22 | it { expect { |b| lock.schedule(&b) }.not_to yield_control }
23 | end
24 | end
25 |
26 | describe '#execute' do
27 | before { allow(job).to receive(:unlock!) }
28 | after { expect(job).to have_received(:unlock!) }
29 | it { expect { |b| lock.execute(&b) }.to yield_control }
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/spec/cloudtasker/unique_job/lock/while_executing_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'cloudtasker/unique_job/middleware'
4 |
5 | RSpec.describe Cloudtasker::UniqueJob::Lock::WhileExecuting do
6 | let(:worker) { TestWorker.new(job_args: [1, 2]) }
7 | let(:job) { Cloudtasker::UniqueJob::Job.new(worker) }
8 | let(:lock) { described_class.new(job) }
9 |
10 | it_behaves_like Cloudtasker::UniqueJob::Lock::BaseLock
11 |
12 | describe '#schedule' do
13 | it { expect { |b| lock.schedule(&b) }.to yield_control }
14 | end
15 |
16 | describe '#execute' do
17 | before { allow(job).to receive(:lock!) }
18 | before { allow(job).to receive(:unlock!) }
19 | after { expect(job).to have_received(:unlock!) }
20 |
21 | context 'with lock available' do
22 | it { expect { |b| lock.execute(&b) }.to yield_control }
23 | end
24 |
25 | context 'with lock acquired by another job' do
26 | before { allow(job).to receive(:lock!).and_raise(Cloudtasker::UniqueJob::LockError) }
27 | before { allow(lock.conflict_instance).to receive(:on_execute) }
28 | after { expect(lock.conflict_instance).to have_received(:on_execute) { |&b| expect(b).to be_a(Proc) } }
29 | it { expect { |b| lock.execute(&b) }.not_to yield_control }
30 | end
31 |
32 | context 'with runtime error' do
33 | let(:error) { ArgumentError }
34 | let(:block) { proc { raise(error) } }
35 |
36 | it { expect { lock.execute(&block) }.to raise_error(error) }
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/spec/cloudtasker/unique_job/middleware/client_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'cloudtasker/unique_job/middleware'
4 |
5 | RSpec.describe Cloudtasker::UniqueJob::Middleware::Client do
6 | let(:middleware) { described_class.new }
7 |
8 | describe '#call' do
9 | let(:lock_instance) { instance_double(Cloudtasker::UniqueJob::Lock::UntilExecuted) }
10 | let(:worker) { instance_double(Cloudtasker::Worker) }
11 | let(:job) { instance_double(Cloudtasker::UniqueJob::Job) }
12 |
13 | before { allow(Cloudtasker::UniqueJob::Job).to receive(:new).with(worker).and_return(job) }
14 | before { allow(job).to receive(:lock_instance).and_return(lock_instance) }
15 | before { allow(lock_instance).to receive(:schedule).and_yield }
16 | it { expect { |b| middleware.call(worker, &b) }.to yield_control }
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/spec/cloudtasker/unique_job/middleware/server_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'cloudtasker/unique_job/middleware'
4 |
5 | RSpec.describe Cloudtasker::UniqueJob::Middleware::Server do
6 | let(:middleware) { described_class.new }
7 |
8 | describe '#call' do
9 | let(:lock_instance) { instance_double(Cloudtasker::UniqueJob::Lock::UntilExecuted) }
10 | let(:worker) { instance_double(Cloudtasker::Worker) }
11 | let(:job) { instance_double(Cloudtasker::UniqueJob::Job) }
12 |
13 | before { allow(Cloudtasker::UniqueJob::Job).to receive(:new).with(worker).and_return(job) }
14 | before { allow(job).to receive(:lock_instance).and_return(lock_instance) }
15 | before { allow(lock_instance).to receive(:execute).and_yield }
16 | it { expect { |b| middleware.call(worker, &b) }.to yield_control }
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/spec/cloudtasker/unique_job/middleware_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'cloudtasker/unique_job/middleware'
4 |
5 | RSpec.describe Cloudtasker::UniqueJob::Middleware do
6 | describe '.configure' do
7 | before { described_class.configure }
8 |
9 | it { expect(Cloudtasker.config.client_middleware).to exist(Cloudtasker::UniqueJob::Middleware::Client) }
10 | it { expect(Cloudtasker.config.server_middleware).to exist(Cloudtasker::UniqueJob::Middleware::Server) }
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/spec/cloudtasker/worker_wrapper_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'cloudtasker/worker_wrapper'
4 |
5 | RSpec.describe Cloudtasker::WorkerWrapper do
6 | let(:worker_class) { 'TestWorker' }
7 |
8 | describe '.ancestors' do
9 | subject { described_class.ancestors }
10 |
11 | it { is_expected.to include(Cloudtasker::Worker) }
12 | end
13 |
14 | describe '.new' do
15 | subject { described_class.new(**worker_args.merge(worker_name: worker_class)) }
16 |
17 | let(:id) { SecureRandom.uuid }
18 | let(:args) { [1, 2] }
19 | let(:meta) { { foo: 'bar' } }
20 | let(:retries) { 3 }
21 | let(:queue) { 'critical' }
22 | let(:worker_args) { { job_queue: queue, job_args: args, job_id: id, job_meta: meta, job_retries: retries } }
23 | let(:expected_args) do
24 | {
25 | job_queue: queue,
26 | job_args: args,
27 | job_id: id,
28 | job_meta: eq(meta),
29 | job_retries: retries,
30 | worker_name: worker_class
31 | }
32 | end
33 |
34 | it { is_expected.to have_attributes(expected_args) }
35 | end
36 |
37 | describe '#job_class_name' do
38 | subject { described_class.new(worker_name: worker_class).job_class_name }
39 |
40 | it { is_expected.to eq(worker_class) }
41 | end
42 |
43 | describe '#new_instance' do
44 | subject(:new_instance) { worker.new_instance }
45 |
46 | let(:job_args) { [1, 2] }
47 | let(:job_meta) { { foo: 'bar' } }
48 | let(:job_queue) { 'critical' }
49 | let(:attrs) { { worker_name: worker_class, job_queue: job_queue, job_args: job_args, job_meta: job_meta } }
50 | let(:worker) { described_class.new(**attrs) }
51 |
52 | it { is_expected.to have_attributes(attrs.merge(job_meta: eq(job_meta))) }
53 | it { expect(new_instance.job_id).not_to eq(worker.job_id) }
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/spec/cloudtasker_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Cloudtasker do
4 | describe '::VERSION' do
5 | subject { Cloudtasker::VERSION }
6 |
7 | it { is_expected.not_to be_nil }
8 | end
9 |
10 | describe '.logger' do
11 | subject { described_class.logger }
12 |
13 | it { is_expected.to eq(described_class.config.logger) }
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/spec/dummy/config/application.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative 'boot'
4 |
5 | require 'rails'
6 | require 'action_controller/railtie'
7 | require 'active_job/railtie'
8 |
9 | Bundler.require(*Rails.groups)
10 | require 'cloudtasker'
11 |
12 | module Dummy
13 | class Application < Rails::Application
14 | # Initialize configuration defaults for originally generated Rails version.
15 | config.load_defaults Rails.version.to_f
16 |
17 | # Settings in config/environments/* take precedence over those specified here.
18 | # Application configuration can go into files in config/initializers
19 | # -- all .rb files in that directory are automatically loaded after loading
20 | # the framework and any gems in your application.
21 | config.eager_load = false
22 |
23 | # Use cloudtasker as the ActiveJob backend:
24 | config.active_job.queue_adapter = :cloudtasker
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/spec/dummy/config/boot.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Set up gems listed in the Gemfile.
4 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__)
5 |
6 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
7 | $LOAD_PATH.unshift File.expand_path('../../../lib', __dir__)
8 |
--------------------------------------------------------------------------------
/spec/dummy/config/environment.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Load the Rails application.
4 | require_relative 'application'
5 |
6 | # Initialize the Rails application.
7 | Rails.application.initialize!
8 |
--------------------------------------------------------------------------------
/spec/integration/active_job_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | if defined?(Rails)
6 | RSpec.describe 'ActiveJob integration' do
7 | let(:example_job_arguments) { [1, 'two', { three: 3 }] }
8 |
9 | let(:example_job_class) do
10 | Class.new(ActiveJob::Base) do
11 | def self.name
12 | 'ExampleJob'
13 | end
14 | end
15 | end
16 |
17 | let(:expected_cloud_task_http_request_data) do
18 | a_hash_including(body: expected_cloud_task_body)
19 | end
20 |
21 | let(:expected_cloud_task_create_argument) do
22 | a_hash_including(http_request: expected_cloud_task_http_request_data, queue: 'default')
23 | end
24 |
25 | describe 'Calling .perform_later on an ActiveJob class' do
26 | let(:expected_cloud_task_body) do
27 | include_json(
28 | 'worker' => 'ActiveJob::QueueAdapters::CloudtaskerAdapter::JobWrapper',
29 | 'job_args' => a_collection_including(
30 | a_hash_including(
31 | 'job_class' => example_job_class.name,
32 | 'arguments' => ActiveJob::Arguments.serialize(example_job_arguments)
33 | )
34 | )
35 | )
36 | end
37 |
38 | context 'without any custom execution setup' do
39 | it 'enqueues the job to run as soon as possible' do
40 | expect(Cloudtasker::CloudTask).to receive(:create).with(expected_cloud_task_create_argument)
41 | example_job_class.perform_later(*example_job_arguments)
42 | end
43 | end
44 |
45 | context 'with a custom execution wait time' do
46 | let(:wait_time) { 1.week }
47 | let(:expected_calculated_datetime) { wait_time.from_now }
48 | let(:expected_cloud_task_create_argument) do
49 | a_hash_including(
50 | http_request: expected_cloud_task_http_request_data,
51 | queue: 'default',
52 | schedule_time: expected_calculated_datetime.to_i
53 | )
54 | end
55 |
56 | around { |e| Timecop.freeze { e.run } }
57 |
58 | it 'enqueues the job to run at the calculated datetime' do
59 | expect(Cloudtasker::CloudTask).to receive(:create).with(expected_cloud_task_create_argument)
60 | example_job_class.set(wait: wait_time).perform_later(*example_job_arguments)
61 | end
62 | end
63 |
64 | context 'with a different queue to execute the job' do
65 | let(:example_queue_name) { 'another-queue' }
66 | let(:expected_cloud_task_create_argument) do
67 | a_hash_including(
68 | http_request: expected_cloud_task_http_request_data,
69 | queue: example_queue_name
70 | )
71 | end
72 |
73 | it 'enqueues the job in the specified queue' do
74 | expect(Cloudtasker::CloudTask).to receive(:create).with(expected_cloud_task_create_argument)
75 | example_job_class.set(queue: example_queue_name).perform_later(*example_job_arguments)
76 | end
77 | end
78 | end
79 | end
80 | end
81 |
--------------------------------------------------------------------------------
/spec/integration/batch_worker_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 | require 'cloudtasker/batch/middleware'
5 |
6 | RSpec.describe 'Batch Worker' do
7 | # Activate middleware
8 | before { Cloudtasker::Batch::Middleware.configure }
9 |
10 | describe 'regular batch' do
11 | let(:worker_class) { TestBatchWorker }
12 | let(:expected_callback_counts) do
13 | {
14 | # 1 level 0
15 | 0 => 1,
16 | # 1 (level 0) * 2 (level 1)
17 | 1 => 2,
18 | # 1 (level 0) * 2 (level 1) * 2 (level 2)
19 | 2 => 4,
20 | # 1 (level 0) * 2 (level 1) * 2 (level 2) * 2 (level 3 / batch expansion) *
21 | 3 => 8
22 | }
23 | end
24 |
25 | before do
26 | # Perform jobs
27 | Cloudtasker::Testing.fake! do
28 | worker_class.perform_async
29 |
30 | # Process jobs iteratively until the batch is complete
31 | # Limit the number of iterations to 50 to prevent unexpected infinite loops
32 | 50.times do
33 | Cloudtasker::Worker.drain_all
34 | break if worker_class.jobs.to_a.empty?
35 | end
36 | end
37 | end
38 |
39 | it 'completes the batch' do
40 | expect(TestBatchWorker.jobs).to be_empty
41 | expect(TestBatchWorker.callback_counts).to eq(expected_callback_counts)
42 | end
43 | end
44 |
45 | describe 'dead batch' do
46 | let(:worker_class) { DeadBatchWorker }
47 |
48 | let(:expected_callback_counts) do
49 | {
50 | # (1 level 0 succeed)
51 | 0 => 1,
52 | # (2 level 1 success)
53 | 1 => 2
54 | # No level 2 - they all failed
55 | }
56 | end
57 |
58 | before do
59 | # Perform jobs
60 | Cloudtasker::Testing.fake! do
61 | worker_class.perform_async
62 |
63 | # Process jobs iteratively until the batch is complete
64 | # Limit the number of iterations to 50 to prevent unexpected infinite loops
65 | # The batch
66 | 50.times do
67 | Cloudtasker::Worker.drain_all
68 | break if worker_class.jobs.to_a.empty?
69 | end
70 | end
71 | end
72 |
73 | it 'completes the batch' do
74 | expect(worker_class.jobs.to_a).to be_empty
75 | expect(worker_class.callback_counts).to eq(expected_callback_counts)
76 | end
77 | end
78 | end
79 |
--------------------------------------------------------------------------------
/spec/shared/active_job/instantiation_context.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # rubocop:disable RSpec/ContextWording
4 | RSpec.shared_context 'of Cloudtasker ActiveJob instantiation' do
5 | let(:example_job_class) do
6 | Class.new(ActiveJob::Base) do
7 | def self.name
8 | 'ExampleJob'
9 | end
10 | end
11 | end
12 |
13 | let(:example_job_setup) { {} }
14 | let(:example_job_arguments) { [1, 'two', { three: 3 }] }
15 | let(:example_job) { example_job_class.new(*example_job_arguments) }
16 |
17 | let(:example_job_serialization) do
18 | example_job.serialize.except(
19 | 'job_id', 'priority', 'executions', 'queue_name', 'provider_job_id'
20 | )
21 | end
22 |
23 | let(:example_job_wrapper_args) do
24 | {
25 | job_queue: example_job.queue_name,
26 | job_args: [example_job_serialization],
27 | job_id: example_job.job_id
28 | }
29 | end
30 | end
31 | # rubocop:enable RSpec/ContextWording
32 |
--------------------------------------------------------------------------------
/spec/shared/cloudtasker/unique_job/conflict_strategy/base_strategy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'cloudtasker/unique_job/middleware'
4 |
5 | RSpec.shared_examples Cloudtasker::UniqueJob::ConflictStrategy::BaseStrategy do
6 | describe '.new' do
7 | subject { described_class.new(job) }
8 |
9 | let(:worker) { TestWorker.new(job_args: [1, 2]) }
10 | let(:job) { Cloudtasker::UniqueJob::Job.new(worker) }
11 |
12 | it { is_expected.to have_attributes(job: job) }
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/spec/shared/cloudtasker/unique_job/lock/base_lock.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'cloudtasker/unique_job/middleware'
4 |
5 | RSpec.shared_examples Cloudtasker::UniqueJob::Lock::BaseLock do
6 | let(:worker) { TestWorker.new(job_args: [1, 2]) }
7 | let(:job) { Cloudtasker::UniqueJob::Job.new(worker) }
8 | let(:lock) { described_class.new(job) }
9 |
10 | describe '.new' do
11 | subject { lock }
12 |
13 | it { is_expected.to have_attributes(job: job) }
14 | end
15 |
16 | describe '#default_conflict_strategy' do
17 | subject { lock.default_conflict_strategy }
18 |
19 | it { is_expected.to be < Cloudtasker::UniqueJob::ConflictStrategy::BaseStrategy }
20 | end
21 |
22 | describe '#options' do
23 | subject { lock.options }
24 |
25 | it { is_expected.to eq(job.options) }
26 | end
27 |
28 | describe '#conflict_instance' do
29 | subject { lock.conflict_instance }
30 |
31 | before { allow(lock).to receive(:options).and_return(job_opts) }
32 |
33 | context 'with no conflict strategy' do
34 | let(:job_opts) { {} }
35 |
36 | it { is_expected.to be_a(lock.default_conflict_strategy) }
37 | it { is_expected.to have_attributes(job: job) }
38 | end
39 |
40 | context 'with invalid conflict strategy' do
41 | let(:job_opts) { { on_conflict: 'foo' } }
42 |
43 | it { is_expected.to be_a(lock.default_conflict_strategy) }
44 | it { is_expected.to have_attributes(job: job) }
45 | end
46 |
47 | context 'with valid lock strategy' do
48 | let(:job_opts) { { on_conflict: 'reschedule' } }
49 |
50 | it { is_expected.to be_a(Cloudtasker::UniqueJob::ConflictStrategy::Reschedule) }
51 | it { is_expected.to have_attributes(job: job) }
52 | end
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'bundler/setup'
4 | require 'timecop'
5 | require 'webmock/rspec'
6 | require 'semantic_logger'
7 | require 'rspec/json_expectations'
8 |
9 | # Configure Rails dummary app if Rails is in context
10 | if Gem.loaded_specs.key?('rails')
11 | ENV['RAILS_ENV'] ||= 'test'
12 | require File.expand_path('dummy/config/environment.rb', __dir__)
13 | require 'rspec/rails'
14 | end
15 |
16 | # Require main library (after Rails has done so)
17 | require 'cloudtasker'
18 | require 'cloudtasker/testing'
19 | require 'cloudtasker/unique_job'
20 | require 'cloudtasker/cron'
21 | require 'cloudtasker/batch'
22 | require 'cloudtasker/storable'
23 |
24 | # Require supporting files
25 | Dir['./spec/support/**/*.rb'].sort.each { |f| require f }
26 | Dir['./spec/shared/**/*.rb'].sort.each { |f| require f }
27 |
28 | RSpec.configure do |config|
29 | # Enable flags like --only-failures and --next-failure
30 | config.example_status_persistence_file_path = '.rspec_status'
31 |
32 | # Disable RSpec exposing methods globally on `Module` and `main`
33 | config.disable_monkey_patching!
34 |
35 | config.expect_with :rspec do |c|
36 | c.syntax = :expect
37 | end
38 |
39 | # Ensure cache is clean before each test
40 | config.before do
41 | Cloudtasker.config.client_middleware.clear
42 | Cloudtasker.config.server_middleware.clear
43 |
44 | # Flush redis keys
45 | Cloudtasker::RedisClient.new.clear
46 | end
47 |
48 | # NOTE: Retriable is configured in a conditional before
49 | # block to avoid requiring the gem in the spec helper. This
50 | # ensures that classes have defined the proper requires.
51 | config.before(:all) do
52 | if defined?(Retriable)
53 | # Do not wait between retries
54 | Retriable.configure do |c|
55 | c.multiplier = 1.0
56 | c.rand_factor = 0.0
57 | c.base_interval = 0
58 | end
59 | end
60 | end
61 | end
62 |
63 | # Configure for tests
64 | Cloudtasker.configure do |config|
65 | # GCP
66 | config.gcp_project_id = 'my-project-id'
67 | config.gcp_location_id = 'us-east2'
68 | config.gcp_queue_prefix = 'my-queue'
69 |
70 | # Processor
71 | config.secret = 'my$s3cr3t'
72 | config.processor_host = 'http://localhost'
73 | config.processor_path = '/mynamespace/run'
74 |
75 | # Redis
76 | config.redis = { url: "redis://#{ENV['REDIS_HOST'] || 'localhost'}:6379/15" }
77 |
78 | # Logger
79 | config.logger = Logger.new(nil)
80 |
81 | # Hooks
82 | config.on_error = ->(w, e) {}
83 | config.on_dead = ->(w, e) {}
84 | end
85 |
--------------------------------------------------------------------------------
/spec/support/dead_batch_worker.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class DeadBatchWorker
4 | include Cloudtasker::Worker
5 |
6 | class << self
7 | attr_accessor :callback_counts, :callback_error_counts
8 | end
9 |
10 | def perform(level = 0)
11 | # Flag parent as incomplete
12 | if level == 0
13 | self.class.callback_counts = {}
14 | self.class.callback_error_counts = {}
15 | end
16 |
17 | # Fail jobs on their first few runs
18 | raise(StandardError, 'batch worker error') if job_retries < level
19 |
20 | # Enqueue child jobs
21 | 2.times { batch.add(self.class, level + 1) } if level < 2
22 | end
23 |
24 | def on_batch_complete
25 | level = job_args[0].to_i
26 |
27 | # Alway fail grand children callbacks
28 | raise(StandardError, 'batch callback error') if level == 2
29 |
30 | # Add callback result
31 | self.class.callback_counts[level] ||= 0
32 | self.class.callback_counts[level] += 1
33 | end
34 |
35 | def on_dead
36 | # Make it worse. Make the on_dead callback fail.
37 | raise(StandardError, 'on_dead error')
38 | end
39 |
40 | def on_child_dead(_child_worker)
41 | # Make it worse. Make the on_child_dead callback fail.
42 | raise(StandardError, 'on_child_dead error')
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/spec/support/test_batch_worker.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class TestBatchWorker
4 | include Cloudtasker::Worker
5 |
6 | class << self
7 | attr_accessor :callback_registry, :callback_error_counts
8 | end
9 |
10 | def perform(level = 0)
11 | # Initialize callback counters on top parent's run
12 | if level == 0
13 | self.class.callback_registry = {}
14 | self.class.callback_error_counts = {}
15 | end
16 |
17 | # Fail jobs on their first few runs
18 | raise(StandardError, 'batch worker error') if job_retries < level
19 |
20 | # Enqueue child jobs
21 | 2.times { batch.add(self.class, level + 1) } if level < 2
22 |
23 | # Expand parent batch. Limit batch expansion to level 2 only (last child level)
24 | # to avoid infinite loops. Expand batch before the job starts failing on on_batch_complete.
25 | 2.times { parent_batch.add(self.class, level + 1) } if level == 2 && job_retries < 3
26 | end
27 |
28 | # Hook invoked when a batch completes
29 | def on_batch_complete
30 | level = job_args[0].to_i
31 |
32 | # Fail callbacks on their first few runs
33 | self.class.callback_error_counts[job_id] ||= 0
34 | self.class.callback_error_counts[job_id] += 1
35 | raise(StandardError, 'batch callback error') if self.class.callback_error_counts[job_id] <= 2
36 |
37 | # Register batch as complete
38 | self.class.callback_registry[level] ||= Set.new
39 | self.class.callback_registry[level].add(job_id)
40 | end
41 |
42 | # Return the number of jobs completed per level
43 | def self.callback_counts
44 | callback_registry.transform_values(&:size)
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/spec/support/test_middleware.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class TestMiddleware
4 | attr_accessor :arg, :called
5 |
6 | def initialize(arg = nil)
7 | @arg = arg
8 | end
9 |
10 | def call(worker, opts = {})
11 | @called = true
12 | worker.middleware_called = true if worker.respond_to?(:middleware_called)
13 | worker.middleware_opts = opts if worker.respond_to?(:middleware_opts)
14 | yield
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/spec/support/test_middleware2.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class TestMiddleware2
4 | def call(_worker)
5 | yield
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/support/test_middleware3.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class TestMiddleware3
4 | def call(_worker)
5 | yield
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/support/test_non_worker.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class TestNonWorker
4 | attr_accessor :job_id, :job_meta
5 |
6 | def initialize(*_args)
7 | @job_meta = Cloudtasker::MetaStore.new
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/spec/support/test_storable_worker.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class TestStorableWorker
4 | include Cloudtasker::Worker
5 | include Cloudtasker::Storable::Worker
6 |
7 | attr_accessor :middleware_called, :middleware_opts, :has_run
8 |
9 | class << self
10 | attr_accessor :has_run
11 |
12 | def has_run?
13 | has_run
14 | end
15 | end
16 |
17 | cloudtasker_options foo: 'bar'
18 |
19 | def perform(arg1, arg2)
20 | self.class.has_run = true
21 | arg1 + arg2
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/spec/support/test_worker.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class TestWorker
4 | include Cloudtasker::Worker
5 |
6 | attr_accessor :middleware_called, :middleware_opts, :has_run
7 |
8 | class << self
9 | attr_accessor :has_run
10 |
11 | def has_run?
12 | has_run
13 | end
14 | end
15 |
16 | cloudtasker_options foo: 'bar'
17 |
18 | def perform(arg1, arg2)
19 | self.class.has_run = true
20 | arg1 + arg2
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/spec/support/test_worker2.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class TestWorker2
4 | include Cloudtasker::Worker
5 |
6 | attr_accessor :middleware_called, :middleware_opts
7 |
8 | cloudtasker_options foo: 'bar'
9 |
10 | def perform(arg1, arg2)
11 | arg1 + arg2
12 | end
13 | end
14 |
--------------------------------------------------------------------------------