├── spec
├── dummy
│ ├── log
│ │ └── .keep
│ ├── tmp
│ │ └── .keep
│ ├── lib
│ │ └── assets
│ │ │ └── .keep
│ ├── public
│ │ ├── favicon.ico
│ │ ├── apple-touch-icon.png
│ │ ├── apple-touch-icon-precomposed.png
│ │ ├── 500.html
│ │ ├── 422.html
│ │ └── 404.html
│ ├── app
│ │ ├── assets
│ │ │ ├── images
│ │ │ │ └── .keep
│ │ │ ├── javascripts
│ │ │ │ ├── channels
│ │ │ │ │ └── .keep
│ │ │ │ ├── cable.js
│ │ │ │ └── application.js
│ │ │ ├── config
│ │ │ │ └── manifest.js
│ │ │ └── stylesheets
│ │ │ │ └── application.css
│ │ ├── models
│ │ │ ├── concerns
│ │ │ │ └── .keep
│ │ │ ├── application_record.rb
│ │ │ └── async_task
│ │ │ │ └── attempt.rb
│ │ ├── controllers
│ │ │ ├── concerns
│ │ │ │ └── .keep
│ │ │ └── application_controller.rb
│ │ ├── views
│ │ │ └── layouts
│ │ │ │ ├── mailer.text.erb
│ │ │ │ ├── mailer.html.erb
│ │ │ │ └── application.html.erb
│ │ ├── helpers
│ │ │ └── application_helper.rb
│ │ ├── jobs
│ │ │ ├── application_job.rb
│ │ │ └── async_task
│ │ │ │ ├── attempt_job.rb
│ │ │ │ └── attempt_batch_job.rb
│ │ ├── lib
│ │ │ └── test_client.rb
│ │ ├── channels
│ │ │ └── application_cable
│ │ │ │ ├── channel.rb
│ │ │ │ └── connection.rb
│ │ └── mailers
│ │ │ └── application_mailer.rb
│ ├── package.json
│ ├── bin
│ │ ├── rake
│ │ ├── bundle
│ │ ├── rails
│ │ ├── yarn
│ │ ├── update
│ │ └── setup
│ ├── config
│ │ ├── spring.rb
│ │ ├── routes.rb
│ │ ├── environment.rb
│ │ ├── initializers
│ │ │ ├── mime_types.rb
│ │ │ ├── application_controller_renderer.rb
│ │ │ ├── filter_parameter_logging.rb
│ │ │ ├── cookies_serializer.rb
│ │ │ ├── backtrace_silencers.rb
│ │ │ ├── wrap_parameters.rb
│ │ │ ├── assets.rb
│ │ │ └── inflections.rb
│ │ ├── cable.yml
│ │ ├── boot.rb
│ │ ├── application.rb
│ │ ├── database.yml
│ │ ├── locales
│ │ │ └── en.yml
│ │ ├── secrets.yml
│ │ ├── environments
│ │ │ ├── test.rb
│ │ │ ├── development.rb
│ │ │ └── production.rb
│ │ └── puma.rb
│ ├── config.ru
│ ├── spec
│ │ ├── jobs
│ │ │ └── async_task
│ │ │ │ ├── attempt_job_spec.rb
│ │ │ │ └── attempt_batch_job_spec.rb
│ │ ├── models
│ │ │ └── async_task
│ │ │ │ └── attempt_spec.rb
│ │ └── factories
│ │ │ └── async_task
│ │ │ └── attempts.rb
│ ├── Rakefile
│ └── db
│ │ ├── migrate
│ │ └── 20171205080843_create_async_task_attempts.rb
│ │ └── schema.rb
├── async_task_spec.rb
├── spec_helper.rb
├── async_task
│ ├── jobs
│ │ ├── attempt_job_spec.rb
│ │ └── attempt_batch_job_spec.rb
│ └── models
│ │ └── async_task_spec.rb
└── rails_helper.rb
├── .rspec
├── CHANGES.md
├── lib
├── async_task
│ ├── version.rb
│ ├── null_encryptor.rb
│ ├── jobs
│ │ ├── base_job.rb
│ │ ├── base_attempt_job.rb
│ │ └── base_attempt_batch_job.rb
│ └── base_attempt.rb
├── generators
│ └── async_task
│ │ ├── templates
│ │ ├── async_task_attempt_spec.rb.erb
│ │ ├── async_task_attempt_job_spec.rb.erb
│ │ ├── async_task_attempt_batch_job_spec.rb.erb
│ │ ├── async_task_attempt_job.rb.erb
│ │ ├── async_task_attempt_batch_job.rb.erb
│ │ ├── async_task_attempt.rb.erb
│ │ ├── async_task_attempts.rb.erb
│ │ └── create_async_task_attempts.rb
│ │ └── install_generator.rb
└── async_task.rb
├── bin
├── console
└── setup
├── Rakefile
├── .gitignore
├── Gemfile
├── .rubocop.yml
├── LICENSE
├── async_task.gemspec
└── README.md
/spec/dummy/log/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/dummy/tmp/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/dummy/lib/assets/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/dummy/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/dummy/app/assets/images/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/dummy/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/dummy/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --format documentation
2 | --color
3 |
--------------------------------------------------------------------------------
/spec/dummy/app/assets/javascripts/channels/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/dummy/public/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/dummy/app/views/layouts/mailer.text.erb:
--------------------------------------------------------------------------------
1 | <%= yield %>
2 |
--------------------------------------------------------------------------------
/CHANGES.md:
--------------------------------------------------------------------------------
1 | ### 0.0.1 / 2017-12-04
2 |
3 | * Initial release
4 |
--------------------------------------------------------------------------------
/lib/async_task/version.rb:
--------------------------------------------------------------------------------
1 | module AsyncTask
2 | VERSION = '0.0.1'
3 | end
4 |
--------------------------------------------------------------------------------
/spec/dummy/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/spec/dummy/app/jobs/application_job.rb:
--------------------------------------------------------------------------------
1 | class ApplicationJob < ActiveJob::Base
2 | end
3 |
--------------------------------------------------------------------------------
/spec/dummy/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dummy",
3 | "private": true,
4 | "dependencies": {}
5 | }
6 |
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require 'bundler/setup'
4 | require 'async_task'
5 |
6 | require 'pry'
7 | Pry.start
8 |
--------------------------------------------------------------------------------
/spec/dummy/app/lib/test_client.rb:
--------------------------------------------------------------------------------
1 | module TestClient
2 | module_function
3 |
4 | def do_something(with:, as:); end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/application_record.rb:
--------------------------------------------------------------------------------
1 | class ApplicationRecord < ActiveRecord::Base
2 | self.abstract_class = true
3 | end
4 |
--------------------------------------------------------------------------------
/spec/dummy/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require_relative '../config/boot'
4 | require 'rake'
5 | Rake.application.run
6 |
--------------------------------------------------------------------------------
/spec/dummy/app/jobs/async_task/attempt_job.rb:
--------------------------------------------------------------------------------
1 | class AsyncTask::AttemptJob < ApplicationJob
2 | include AsyncTask::BaseAttemptJob
3 | end
4 |
--------------------------------------------------------------------------------
/spec/dummy/app/channels/application_cable/channel.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Channel < ActionCable::Channel::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/spec/dummy/app/assets/config/manifest.js:
--------------------------------------------------------------------------------
1 |
2 | //= link_tree ../images
3 | //= link_directory ../javascripts .js
4 | //= link_directory ../stylesheets .css
5 |
--------------------------------------------------------------------------------
/spec/dummy/app/channels/application_cable/connection.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Connection < ActionCable::Connection::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/spec/dummy/app/jobs/async_task/attempt_batch_job.rb:
--------------------------------------------------------------------------------
1 | class AsyncTask::AttemptBatchJob < ApplicationJob
2 | include AsyncTask::BaseAttemptBatchJob
3 | end
4 |
--------------------------------------------------------------------------------
/spec/dummy/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | protect_from_forgery with: :exception
3 | end
4 |
--------------------------------------------------------------------------------
/spec/dummy/app/mailers/application_mailer.rb:
--------------------------------------------------------------------------------
1 | class ApplicationMailer < ActionMailer::Base
2 | default from: 'from@example.com'
3 | layout 'mailer'
4 | end
5 |
--------------------------------------------------------------------------------
/spec/dummy/config/spring.rb:
--------------------------------------------------------------------------------
1 | %w[
2 | .ruby-version
3 | .rbenv-vars
4 | tmp/restart.txt
5 | tmp/caching-dev.txt
6 | ].each { |path| Spring.watch(path) }
7 |
--------------------------------------------------------------------------------
/spec/dummy/bin/bundle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
4 | load Gem.bin_path('bundler', 'bundle')
5 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/spec/dummy/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require_relative 'config/environment'
4 |
5 | run Rails.application
6 |
--------------------------------------------------------------------------------
/spec/dummy/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
3 | end
4 |
--------------------------------------------------------------------------------
/spec/dummy/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | APP_PATH = File.expand_path('../config/application', __dir__)
4 | require_relative '../config/boot'
5 | require 'rails/commands'
6 |
--------------------------------------------------------------------------------
/spec/dummy/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require_relative 'application'
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
--------------------------------------------------------------------------------
/spec/async_task_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | RSpec.describe AsyncTask do
4 | it 'has a version number' do
5 | expect(AsyncTask::VERSION).not_to be_nil
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/dummy/spec/jobs/async_task/attempt_job_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | RSpec.describe AsyncTask::AttemptJob, type: :job do
4 | pending "add some examples to (or delete) #{__FILE__}"
5 | end
6 |
--------------------------------------------------------------------------------
/spec/dummy/spec/models/async_task/attempt_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | RSpec.describe AsyncTask::Attempt, type: :model do
4 | pending "add some examples to (or delete) #{__FILE__}"
5 | end
6 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new mime types for use in respond_to blocks:
4 | # Mime::Type.register "text/richtext", :rtf
5 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'bundler/setup'
2 |
3 | require 'rspec/core/rake_task'
4 | RSpec::Core::RakeTask.new
5 |
6 | require 'rubocop/rake_task'
7 | RuboCop::RakeTask.new
8 |
9 | task default: %w[spec rubocop]
10 |
--------------------------------------------------------------------------------
/lib/generators/async_task/templates/async_task_attempt_spec.rb.erb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | RSpec.describe AsyncTask::Attempt, type: :model do
4 | pending "add some examples to (or delete) #{__FILE__}"
5 | end
6 |
--------------------------------------------------------------------------------
/spec/dummy/spec/jobs/async_task/attempt_batch_job_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | RSpec.describe AsyncTask::AttemptBatchJob, type: :job do
4 | pending "add some examples to (or delete) #{__FILE__}"
5 | end
6 |
--------------------------------------------------------------------------------
/lib/generators/async_task/templates/async_task_attempt_job_spec.rb.erb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | RSpec.describe AsyncTask::AttemptJob, type: :job do
4 | pending "add some examples to (or delete) #{__FILE__}"
5 | end
6 |
--------------------------------------------------------------------------------
/spec/dummy/config/cable.yml:
--------------------------------------------------------------------------------
1 | development:
2 | adapter: async
3 |
4 | test:
5 | adapter: async
6 |
7 | production:
8 | adapter: redis
9 | url: redis://localhost:6379/1
10 | channel_prefix: dummy_production
11 |
--------------------------------------------------------------------------------
/lib/generators/async_task/templates/async_task_attempt_batch_job_spec.rb.erb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | RSpec.describe AsyncTask::AttemptBatchJob, type: :job do
4 | pending "add some examples to (or delete) #{__FILE__}"
5 | end
6 |
--------------------------------------------------------------------------------
/lib/generators/async_task/templates/async_task_attempt_job.rb.erb:
--------------------------------------------------------------------------------
1 | class AsyncTask::AttemptJob < <% if Rails::VERSION::STRING >= '5' %>ApplicationJob<% else %>ActiveJob::Base<% end %>
2 | include AsyncTask::BaseAttemptJob
3 | end
4 |
--------------------------------------------------------------------------------
/lib/generators/async_task/templates/async_task_attempt_batch_job.rb.erb:
--------------------------------------------------------------------------------
1 | class AsyncTask::AttemptBatchJob < <% if Rails::VERSION::STRING >= '5' %>ApplicationJob<% else %>ActiveJob::Base<% end %>
2 | include AsyncTask::BaseAttemptBatchJob
3 | end
4 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/application_controller_renderer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # ApplicationController.renderer.defaults.merge!(
4 | # http_host: 'example.org',
5 | # https: false
6 | # )
7 |
--------------------------------------------------------------------------------
/lib/async_task/null_encryptor.rb:
--------------------------------------------------------------------------------
1 | module AsyncTask
2 | module NullEncryptor
3 | module_function
4 |
5 | def decrypt(content)
6 | content
7 | end
8 |
9 | def encrypt(content)
10 | content
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Configure sensitive parameters which will be filtered from the log file.
4 | Rails.application.config.filter_parameters += [:password]
5 |
--------------------------------------------------------------------------------
/spec/dummy/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require_relative 'config/application'
5 |
6 | Rails.application.load_tasks
7 |
--------------------------------------------------------------------------------
/spec/dummy/config/boot.rb:
--------------------------------------------------------------------------------
1 | # Set up gems listed in the Gemfile.
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__)
3 |
4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
5 | $LOAD_PATH.unshift File.expand_path('../../../lib', __dir__)
6 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/async_task/attempt.rb:
--------------------------------------------------------------------------------
1 | class AsyncTask::Attempt < ApplicationRecord
2 | include AsyncTask::BaseAttempt
3 |
4 | # @override
5 | #
6 | # This method is used by AsyncTask::Base when #perform! fails.
7 | def handle_perform_error(error)
8 | raise error
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/cookies_serializer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Specify a serializer for the signed and encrypted cookie jars.
4 | # Valid options are :json, :marshal, and :hybrid.
5 | Rails.application.config.action_dispatch.cookies_serializer = :json
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.yardoc
3 | /Gemfile.lock
4 | /_yardoc/
5 | /coverage/
6 | /doc/
7 | /pkg/
8 | /spec/reports/
9 | /tmp/
10 |
11 | # Ignore all logfiles and tempfiles
12 | *.log
13 | *.swp
14 |
15 | # Ignore all sqlite3 files
16 | *.sqlite3
17 |
18 | # rspec failure tracking
19 | .rspec_status
20 |
21 | *.gem
22 |
--------------------------------------------------------------------------------
/spec/dummy/app/views/layouts/mailer.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
--------------------------------------------------------------------------------
/spec/dummy/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Dummy
5 | <%= csrf_meta_tags %>
6 |
7 | <%= stylesheet_link_tag 'application', media: 'all' %>
8 | <%= javascript_include_tag 'application' %>
9 |
10 |
11 |
12 | <%= yield %>
13 |
14 |
15 |
--------------------------------------------------------------------------------
/spec/dummy/bin/yarn:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | VENDOR_PATH = File.expand_path('..', __dir__)
4 | Dir.chdir(VENDOR_PATH) do
5 | begin
6 | exec "yarnpkg #{ARGV.join(' ')}"
7 | rescue Errno::ENOENT
8 | $stderr.puts 'Yarn executable was not detected in the system.'
9 | $stderr.puts 'Download Yarn at https://yarnpkg.com/en/docs/install'
10 | exit 1
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/lib/async_task.rb:
--------------------------------------------------------------------------------
1 | require 'active_job'
2 | require 'active_record'
3 | require 'active_support'
4 | require 'active_support/core_ext'
5 | require 'enumerize'
6 | require 'with_advisory_lock'
7 |
8 | require 'async_task/version'
9 |
10 | require 'async_task/base_attempt'
11 | require 'async_task/null_encryptor'
12 |
13 | Dir["#{File.dirname(__FILE__)}/async_task/jobs/**/*.rb"].each { |file| require file }
14 |
--------------------------------------------------------------------------------
/lib/generators/async_task/templates/async_task_attempt.rb.erb:
--------------------------------------------------------------------------------
1 | class AsyncTask::Attempt < <% if Rails::VERSION::STRING >= '5' %>ApplicationRecord<% else %>ActiveRecord::Base<% end %>
2 | include AsyncTask::BaseAttempt
3 |
4 | # @override
5 | #
6 | # This method is used by AsyncTask::Base when #perform! fails.
7 | def handle_perform_error(error)
8 | Raven.capture_exception(error)
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gemspec
4 |
5 | # Core
6 | gem 'bundler', '~> 1.14'
7 | gem 'rake', '~> 11.0'
8 |
9 | # Development Experience
10 | gem 'pry'
11 | gem 'pry-byebug'
12 | gem 'rubocop'
13 |
14 | # Testing (see spec/dummy)
15 | gem 'factory_bot'
16 | gem 'rails', '~> 5.1.0'
17 | gem 'rspec', '~> 3.5'
18 | gem 'rspec-rails', '~> 3.0'
19 | gem 'sq-protos'
20 | gem 'sqlite3'
21 |
22 | gem 'timecop'
23 |
--------------------------------------------------------------------------------
/spec/dummy/app/assets/javascripts/cable.js:
--------------------------------------------------------------------------------
1 | // Action Cable provides the framework to deal with WebSockets in Rails.
2 | // You can generate new channels where WebSocket features live using the `rails generate channel` command.
3 | //
4 | //= require action_cable
5 | //= require_self
6 | //= require_tree ./channels
7 |
8 | (function() {
9 | this.App || (this.App = {});
10 |
11 | App.cable = ActionCable.createConsumer();
12 |
13 | }).call(this);
14 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7 | # Rails.backtrace_cleaner.remove_silencers!
8 |
--------------------------------------------------------------------------------
/lib/async_task/jobs/base_job.rb:
--------------------------------------------------------------------------------
1 | module AsyncTask
2 | module BaseJob
3 | extend ActiveSupport::Concern
4 |
5 | private
6 |
7 | def lock_key
8 | self.class.name
9 | end
10 |
11 | def unless_already_executing(&block)
12 | result = ActiveRecord::Base.with_advisory_lock_result(lock_key, timeout_seconds: 0, &block)
13 | warn("AdvisoryLock owned by other instance of job: #{lock_key}. Exiting.") unless result.lock_was_acquired?
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/async_task/jobs/base_attempt_job.rb:
--------------------------------------------------------------------------------
1 | module AsyncTask
2 | module BaseAttemptJob
3 | extend ActiveSupport::Concern
4 |
5 | included do
6 | include BaseJob
7 |
8 | queue_as :default
9 |
10 | # @override
11 | private def lock_key
12 | [self.class.name, @task.id]
13 | end
14 | end
15 |
16 | def perform(task)
17 | @task = task
18 | unless_already_executing { @task.perform! if @task.reload.pending? }
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/async_task/jobs/base_attempt_batch_job.rb:
--------------------------------------------------------------------------------
1 | module AsyncTask
2 | module BaseAttemptBatchJob
3 | extend ActiveSupport::Concern
4 |
5 | included do
6 | include BaseJob
7 |
8 | queue_as :default
9 | end
10 |
11 | def perform
12 | unless_already_executing do
13 | ::AsyncTask::Attempt.pending.where('scheduled_at IS ? OR scheduled_at < ?', nil, Time.current).find_each do |task|
14 | ::AsyncTask::AttemptJob.perform_later(task)
15 | end
16 | end
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | Lint/AmbiguousBlockAssociation:
2 | Enabled: false
3 |
4 | Metrics/AbcSize:
5 | Enabled: false
6 |
7 | Metrics/BlockLength:
8 | Enabled: false
9 |
10 | Metrics/CyclomaticComplexity:
11 | Enabled: false
12 |
13 | Metrics/MethodLength:
14 | Enabled: false
15 |
16 | Metrics/LineLength:
17 | Enabled: false
18 |
19 | Metrics/PerceivedComplexity:
20 | Enabled: false
21 |
22 | Security/YAMLLoad:
23 | Enabled: false
24 |
25 | Style/Documentation:
26 | Enabled: false
27 |
28 | Style/FrozenStringLiteralComment:
29 | Enabled: false
30 |
--------------------------------------------------------------------------------
/spec/dummy/spec/factories/async_task/attempts.rb:
--------------------------------------------------------------------------------
1 | FactoryBot.define do
2 | factory :async_task_attempt, class: 'AsyncTask::Attempt' do
3 | status { 'pending' }
4 | encryptor { AsyncTask::NullEncryptor }
5 |
6 | trait :succeeded do
7 | status { :succeeded }
8 | completed_at { Time.current }
9 | end
10 |
11 | trait :expired do
12 | status { 'expired' }
13 | completed_at { Time.current }
14 | end
15 |
16 | trait :failed do
17 | status { 'failed' }
18 | completed_at { Time.current }
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # This file contains settings for ActionController::ParamsWrapper which
4 | # is enabled by default.
5 |
6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
7 | ActiveSupport.on_load(:action_controller) do
8 | wrap_parameters format: [:json]
9 | end
10 |
11 | # To enable root element in JSON for ActiveRecord objects.
12 | # ActiveSupport.on_load(:active_record) do
13 | # self.include_root_in_json = true
14 | # end
15 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require 'bundler/setup'
2 | require 'factory_bot'
3 | require 'async_task'
4 |
5 | RSpec.configure do |config|
6 | config.include FactoryBot::Syntax::Methods
7 |
8 | # Enable flags like --only-failures and --next-failure
9 | config.example_status_persistence_file_path = '.rspec_status'
10 |
11 | config.expect_with :rspec do |c|
12 | c.syntax = :expect
13 | end
14 | end
15 |
16 | FactoryBot.definition_file_paths = [
17 | File.expand_path('../factories', __FILE__),
18 | File.expand_path('../dummy/spec/factories', __FILE__)
19 | ]
20 | FactoryBot.find_definitions
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2017 Square, Inc.
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/lib/generators/async_task/templates/async_task_attempts.rb.erb:
--------------------------------------------------------------------------------
1 | <% if defined?(FactoryBot) %>FactoryBot<% else %>FactoryGirl<%end%>.define do
2 | factory :async_task_attempt, class: 'AsyncTask::Attempt' do
3 | status { 'pending' }
4 | encryptor { AsyncTask::NullEncryptor }
5 |
6 | trait :succeeded do
7 | status { :succeeded }
8 | completed_at { Time.current }
9 | end
10 |
11 | trait :expired do
12 | status { 'expired' }
13 | completed_at { Time.current }
14 | end
15 |
16 | trait :failed do
17 | status { 'failed' }
18 | completed_at { Time.current }
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/spec/async_task/jobs/attempt_job_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | RSpec.describe AsyncTask::AttemptJob, type: :job do
4 | describe '#perform' do
5 | let!(:task) do
6 | create(
7 | :async_task_attempt,
8 | target: TestClient,
9 | method_name: :do_something,
10 | method_args: { with: :foo, as: :bar },
11 | )
12 | end
13 |
14 | subject { described_class.new.perform(task) }
15 |
16 | before { allow(task).to receive(:perform!).and_call_original }
17 |
18 | it 'calls perform! on the async task' do
19 | subject
20 | expect(task).to have_received(:perform!)
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/spec/dummy/config/application.rb:
--------------------------------------------------------------------------------
1 | require_relative 'boot'
2 |
3 | require 'rails/all'
4 |
5 | Bundler.require(*Rails.groups)
6 | require 'async_task'
7 |
8 | module Dummy
9 | class Application < Rails::Application
10 | # Initialize configuration defaults for originally generated Rails version.
11 | config.load_defaults 5.1
12 |
13 | # Settings in config/environments/* take precedence over those specified here.
14 | # Application configuration should go into files in config/initializers
15 | # -- all .rb files in that directory are automatically loaded.
16 |
17 | # Set the queue adapter
18 | config.active_job.queue_adapter = :test
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/assets.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Version of your assets, change this if you want to expire all your assets.
4 | Rails.application.config.assets.version = '1.0'
5 |
6 | # Add additional assets to the asset load path.
7 | # Rails.application.config.assets.paths << Emoji.images_path
8 | # Add Yarn node_modules folder to the asset load path.
9 | Rails.application.config.assets.paths << Rails.root.join('node_modules')
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 |
--------------------------------------------------------------------------------
/spec/dummy/config/database.yml:
--------------------------------------------------------------------------------
1 | # SQLite version 3.x
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 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format. Inflections
4 | # are locale specific, and you may define rules for as many different
5 | # locales as you wish. All of these examples are active by default:
6 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
7 | # inflect.plural /^(ox)$/i, '\1en'
8 | # inflect.singular /^(ox)en/i, '\1'
9 | # inflect.irregular 'person', 'people'
10 | # inflect.uncountable %w( fish sheep )
11 | # end
12 |
13 | # These inflection rules are supported but not enabled by default:
14 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
15 | # inflect.acronym 'RESTful'
16 | # end
17 |
--------------------------------------------------------------------------------
/spec/dummy/app/assets/javascripts/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_tree .
14 |
--------------------------------------------------------------------------------
/spec/dummy/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 |
--------------------------------------------------------------------------------
/spec/dummy/bin/update:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require 'pathname'
4 | require 'fileutils'
5 | include FileUtils
6 |
7 | # path to your application root.
8 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
9 |
10 | def system!(*args)
11 | system(*args) || abort("\n== Command #{args} failed ==")
12 | end
13 |
14 | chdir APP_ROOT do
15 | # This script is a way to update your development environment automatically.
16 | # Add necessary update 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== Updating database =="
23 | system! 'bin/rails db:migrate'
24 |
25 | puts "\n== Removing old logs and tempfiles =="
26 | system! 'bin/rails log:clear tmp:clear'
27 |
28 | puts "\n== Restarting application server =="
29 | system! 'bin/rails restart'
30 | end
31 |
--------------------------------------------------------------------------------
/spec/dummy/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 http://guides.rubyonrails.org/i18n.html.
31 |
32 | en:
33 | hello: "Hello world"
34 |
--------------------------------------------------------------------------------
/lib/generators/async_task/templates/create_async_task_attempts.rb:
--------------------------------------------------------------------------------
1 | class CreateAsyncTaskAttempts < ActiveRecord::Migration[5.1]
2 | def change
3 | create_table :async_task_attempts do |t|
4 | t.integer :lock_version, null: false, default: 0
5 |
6 | t.string :idempotence_token
7 |
8 | t.string :status, null: false
9 |
10 | t.string :target, null: false
11 | t.string :method_name, null: false
12 | t.text :method_args
13 |
14 | t.string :encryptor, null: false
15 |
16 | t.integer :num_attempts, null: false, default: 0
17 |
18 | t.datetime :scheduled_at
19 | t.datetime :completed_at
20 |
21 | t.timestamps null: false
22 |
23 | t.index :status
24 |
25 | t.index %i[target method_name idempotence_token], unique: true, name: 'index_async_tasks_on_target_method_name_and_idempotence_token'
26 |
27 | t.index :scheduled_at
28 | t.index :completed_at
29 | t.index :created_at
30 | t.index :updated_at
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20171205080843_create_async_task_attempts.rb:
--------------------------------------------------------------------------------
1 | class CreateAsyncTaskAttempts < ActiveRecord::Migration[5.1]
2 | def change
3 | create_table :async_task_attempts do |t|
4 | t.integer :lock_version, null: false, default: 0
5 |
6 | t.string :idempotence_token
7 |
8 | t.string :status, null: false
9 |
10 | t.string :target, null: false
11 | t.string :method_name, null: false
12 | t.text :method_args
13 |
14 | t.string :encryptor, null: false
15 |
16 | t.integer :num_attempts, null: false, default: 0
17 |
18 | t.datetime :scheduled_at
19 | t.datetime :completed_at
20 |
21 | t.timestamps null: false
22 |
23 | t.index :status
24 |
25 | t.index %i[target method_name idempotence_token], unique: true, name: 'index_async_tasks_on_target_method_name_and_idempotence_token'
26 |
27 | t.index :scheduled_at
28 | t.index :completed_at
29 | t.index :created_at
30 | t.index :updated_at
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/spec/rails_helper.rb:
--------------------------------------------------------------------------------
1 | ENV['RAILS_ENV'] = 'test'
2 |
3 | require 'spec_helper'
4 |
5 | require File.expand_path('../../spec/dummy/config/environment', __FILE__)
6 | require 'rspec/rails'
7 |
8 | Rails.application.load_tasks
9 | Rake.application['db:reset'].tap(&:invoke)
10 |
11 | RSpec.configure do |config|
12 | config.fixture_path = "#{::Rails.root}/spec/fixtures"
13 |
14 | # If you're not using ActiveRecord, or you'd prefer not to run each of your
15 | # examples within a transaction, remove the following line or assign false
16 | # instead of true.
17 | config.use_transactional_fixtures = true
18 |
19 | config.include ActiveJob::TestHelper, type: :job
20 |
21 | # The different available types are documented in the features, such as in
22 | # https://relishapp.com/rspec/rspec-rails/docs
23 | # config.infer_spec_type_from_file_location!
24 | config.before do
25 | # For testing with_advisory_lock (which creates a lot of junk files)
26 | # https://github.com/mceachen/with_advisory_lock
27 | ENV['FLOCK_DIR'] = Dir.mktmpdir
28 | end
29 |
30 | config.after do
31 | FileUtils.remove_entry_secure ENV['FLOCK_DIR']
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/spec/dummy/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require 'pathname'
4 | require 'fileutils'
5 | include FileUtils
6 |
7 | # path to your application root.
8 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
9 |
10 | def system!(*args)
11 | system(*args) || abort("\n== Command #{args} failed ==")
12 | end
13 |
14 | chdir APP_ROOT do
15 | # This script is a starting point to setup your application.
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 | # Install JavaScript dependencies if using Yarn
23 | # system('bin/yarn')
24 |
25 | # puts "\n== Copying sample files =="
26 | # unless File.exist?('config/database.yml')
27 | # cp 'config/database.yml.sample', 'config/database.yml'
28 | # end
29 |
30 | puts "\n== Preparing database =="
31 | system! 'bin/rails db:setup'
32 |
33 | puts "\n== Removing old logs and tempfiles =="
34 | system! 'bin/rails log:clear tmp:clear'
35 |
36 | puts "\n== Restarting application server =="
37 | system! 'bin/rails restart'
38 | end
39 |
--------------------------------------------------------------------------------
/async_task.gemspec:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | lib = File.expand_path('../lib', __FILE__)
4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5 | require 'async_task/version'
6 |
7 | Gem::Specification.new do |spec|
8 | spec.name = 'async_task'
9 | spec.version = AsyncTask::VERSION
10 | spec.authors = ['James Chang']
11 | spec.email = ['jchang@squareup.com']
12 |
13 | spec.summary = 'Lightweight, asynchronous, and database-backed execution of singleton methods.'
14 | spec.homepage = 'https://github.com/square/async_task'
15 |
16 | spec.files = `git ls-files -z`.split("\x0").reject do |f|
17 | f.match(%r{^(test|spec|features)/})
18 | end
19 | spec.executables = spec.files.grep(%r{^bin/}).map { |f| File.basename(f) }
20 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21 | spec.require_paths = ['lib']
22 |
23 | spec.required_ruby_version = '>= 2.3'
24 |
25 | spec.add_runtime_dependency 'activejob', '>= 4.2.0', '< 5.2'
26 | spec.add_runtime_dependency 'activerecord', '>= 4.2.0', '< 5.2'
27 | spec.add_runtime_dependency 'activesupport', '>= 4.2.0', '< 5.2'
28 |
29 | spec.add_runtime_dependency 'enumerize'
30 | spec.add_runtime_dependency 'with_advisory_lock'
31 | end
32 |
--------------------------------------------------------------------------------
/spec/async_task/jobs/attempt_batch_job_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | RSpec.describe AsyncTask::AttemptBatchJob, type: :job do
4 | describe '#perform' do
5 | subject { described_class.new.perform }
6 |
7 | context 'with tasks that are scheduled now' do
8 | let!(:task_1) do
9 | create(:async_task_attempt, target: TestClient, method_name: :do_something)
10 | end
11 |
12 | let!(:task_2) do
13 | create(:async_task_attempt, target: TestClient, method_name: :do_something)
14 | end
15 |
16 | let(:global_ids) { [{ '_aj_globalid' => 'gid://dummy/AsyncTask::Attempt/1' }, { '_aj_globalid' => 'gid://dummy/AsyncTask::Attempt/2' }] }
17 |
18 | it do
19 | subject
20 | expect(enqueued_jobs.map { |job| job.fetch(:args).first }).to include(*global_ids)
21 | end
22 | end
23 |
24 | context 'with a task that is not scheduled until later' do
25 | let!(:task) do
26 | create(
27 | :async_task_attempt,
28 | target: TestClient,
29 | method_name: :do_something,
30 | scheduled_at: Time.current + 1000.days
31 | )
32 | end
33 |
34 | it do
35 | subject
36 | expect(enqueued_jobs).to be_empty
37 | end
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/spec/dummy/config/secrets.yml:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key is used for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 |
6 | # Make sure the secret is at least 30 characters and all random,
7 | # no regular words or you'll be exposed to dictionary attacks.
8 | # You can use `rails secret` to generate a secure secret key.
9 |
10 | # Make sure the secrets in this file are kept private
11 | # if you're sharing your code publicly.
12 |
13 | # Shared secrets are available across all environments.
14 |
15 | # shared:
16 | # api_key: a1B2c3D4e5F6
17 |
18 | # Environmental secrets are only available for that specific environment.
19 |
20 | development:
21 | secret_key_base: 07a6d0d5fee7a2413e587f59ae78b22df924c6b3964f8681c9a189bab3af844a97a8bcb1bcda4c30dfd75a79e792726b7224c84811d05ca468504e135f090c8d
22 |
23 | test:
24 | secret_key_base: aff23c4d1027abc8a49a27ede44a4f6150fb57216eda609c0390314b3e2cf01b9ef73dd12e17dafc0c9b5ceafa769e1f7c5d92b7ce36e7e69f3cba7efbe1b002
25 |
26 | # Do not keep production secrets in the unencrypted secrets file.
27 | # Instead, either read values from the environment.
28 | # Or, use `bin/rails secrets:setup` to configure encrypted secrets
29 | # and move the `production:` environment over there.
30 |
31 | production:
32 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
33 |
--------------------------------------------------------------------------------
/lib/generators/async_task/install_generator.rb:
--------------------------------------------------------------------------------
1 | require 'rails/generators'
2 | require 'rails/generators/active_record'
3 |
4 | module AsyncTask
5 | class InstallGenerator < ::Rails::Generators::Base
6 | include ::Rails::Generators::Migration
7 |
8 | source_root File.expand_path('../templates/', __FILE__)
9 |
10 | desc 'Generates (but does not run) migrations to add the' \
11 | ' async_tasks table and creates the base model'
12 |
13 | def self.next_migration_number(dirname)
14 | ::ActiveRecord::Generators::Base.next_migration_number(dirname)
15 | end
16 |
17 | def create_migration_file
18 | migration_template 'create_async_task_attempts.rb', 'db/migrate/create_async_task_attempts.rb'
19 | end
20 |
21 | def create_async_task_files
22 | template 'async_task_attempt.rb.erb', 'app/models/async_task/attempt.rb'
23 | template 'async_task_attempt_job.rb.erb', 'app/jobs/async_task/attempt_job.rb'
24 | template 'async_task_attempt_batch_job.rb.erb', 'app/jobs/async_task/attempt_batch_job.rb'
25 |
26 | if defined?(RSpec)
27 | template 'async_task_attempt_spec.rb.erb', 'spec/models/async_task/attempt_spec.rb'
28 | template 'async_task_attempt_job_spec.rb.erb', 'spec/jobs/async_task/attempt_job_spec.rb'
29 | template 'async_task_attempt_batch_job_spec.rb.erb', 'spec/jobs/async_task/attempt_batch_job_spec.rb'
30 | end
31 |
32 | if defined?(FactoryBot) || defined?(FactoryGirl)
33 | template 'async_task_attempts.rb.erb', 'spec/factories/async_task/attempts.rb'
34 | end
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/spec/dummy/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 |
--------------------------------------------------------------------------------
/spec/dummy/db/schema.rb:
--------------------------------------------------------------------------------
1 | # This file is auto-generated from the current state of the database. Instead
2 | # of editing this file, please use the migrations feature of Active Record to
3 | # incrementally modify your database, and then regenerate this schema definition.
4 | #
5 | # Note that this schema.rb definition is the authoritative source for your
6 | # database schema. If you need to create the application database on another
7 | # system, you should be using db:schema:load, not running all the migrations
8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations
9 | # you'll amass, the slower it'll run and the greater likelihood for issues).
10 | #
11 | # It's strongly recommended that you check this file into your version control system.
12 |
13 | ActiveRecord::Schema.define(version: 20_171_205_080_843) do
14 | create_table 'async_task_attempts', force: :cascade do |t|
15 | t.integer 'lock_version', default: 0, null: false
16 | t.string 'idempotence_token'
17 | t.string 'status', null: false
18 | t.string 'target', null: false
19 | t.string 'method_name', null: false
20 | t.text 'method_args'
21 | t.string 'encryptor', null: false
22 | t.integer 'num_attempts', default: 0, null: false
23 | t.datetime 'scheduled_at'
24 | t.datetime 'completed_at'
25 | t.datetime 'created_at', null: false
26 | t.datetime 'updated_at', null: false
27 | t.index ['completed_at'], name: 'index_async_task_attempts_on_completed_at'
28 | t.index ['created_at'], name: 'index_async_task_attempts_on_created_at'
29 | t.index ['scheduled_at'], name: 'index_async_task_attempts_on_scheduled_at'
30 | t.index ['status'], name: 'index_async_task_attempts_on_status'
31 | t.index %w[target method_name idempotence_token], name: 'index_async_tasks_on_target_method_name_and_idempotence_token', unique: true
32 | t.index ['updated_at'], name: 'index_async_task_attempts_on_updated_at'
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/spec/dummy/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # The test environment is used exclusively to run your application's
5 | # test suite. You never need to work with it otherwise. Remember that
6 | # your test database is "scratch space" for the test suite and is wiped
7 | # and recreated between test runs. Don't rely on the data there!
8 | config.cache_classes = true
9 |
10 | # Do not eager load code on boot. This avoids loading your whole application
11 | # just for the purpose of running a single test. If you are using a tool that
12 | # preloads Rails for running tests, you may have to set it to true.
13 | config.eager_load = false
14 |
15 | # Configure public file server for tests with Cache-Control for performance.
16 | config.public_file_server.enabled = true
17 | config.public_file_server.headers = {
18 | 'Cache-Control' => "public, max-age=#{1.hour.seconds.to_i}"
19 | }
20 |
21 | # Show full error reports and disable caching.
22 | config.consider_all_requests_local = true
23 | config.action_controller.perform_caching = false
24 |
25 | # Raise exceptions instead of rendering exception templates.
26 | config.action_dispatch.show_exceptions = false
27 |
28 | # Disable request forgery protection in test environment.
29 | config.action_controller.allow_forgery_protection = false
30 | config.action_mailer.perform_caching = false
31 |
32 | # Tell Action Mailer not to deliver emails to the real world.
33 | # The :test delivery method accumulates sent emails in the
34 | # ActionMailer::Base.deliveries array.
35 | config.action_mailer.delivery_method = :test
36 |
37 | # Print deprecation notices to the stderr.
38 | config.active_support.deprecation = :stderr
39 |
40 | # Raises error for missing translations
41 | # config.action_view.raise_on_missing_translations = true
42 |
43 | config.active_job.queue_adapter = :test
44 | end
45 |
--------------------------------------------------------------------------------
/spec/dummy/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 |
--------------------------------------------------------------------------------
/spec/dummy/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 |
--------------------------------------------------------------------------------
/spec/dummy/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # In the development environment your application's code is reloaded on
5 | # every request. This slows down response time but is perfect for development
6 | # since you don't have to restart the web server when you make code changes.
7 | config.cache_classes = false
8 |
9 | # Do not eager load code on boot.
10 | config.eager_load = false
11 |
12 | # Show full error reports.
13 | config.consider_all_requests_local = true
14 |
15 | # Enable/disable caching. By default caching is disabled.
16 | if Rails.root.join('tmp/caching-dev.txt').exist?
17 | config.action_controller.perform_caching = true
18 |
19 | config.cache_store = :memory_store
20 | config.public_file_server.headers = {
21 | 'Cache-Control' => "public, max-age=#{2.days.seconds.to_i}"
22 | }
23 | else
24 | config.action_controller.perform_caching = false
25 |
26 | config.cache_store = :null_store
27 | end
28 |
29 | # Don't care if the mailer can't send.
30 | config.action_mailer.raise_delivery_errors = false
31 |
32 | config.action_mailer.perform_caching = false
33 |
34 | # Print deprecation notices to the Rails logger.
35 | config.active_support.deprecation = :log
36 |
37 | # Raise an error on page load if there are pending migrations.
38 | config.active_record.migration_error = :page_load
39 |
40 | # Debug mode disables concatenation and preprocessing of assets.
41 | # This option may cause significant delays in view rendering with a large
42 | # number of complex assets.
43 | config.assets.debug = true
44 |
45 | # Suppress logger output for asset requests.
46 | config.assets.quiet = true
47 |
48 | # Raises error for missing translations
49 | # config.action_view.raise_on_missing_translations = true
50 |
51 | # Use an evented file watcher to asynchronously detect changes in source code,
52 | # routes, locales, etc. This feature depends on the listen gem.
53 | # config.file_watcher = ActiveSupport::EventedFileUpdateChecker
54 | end
55 |
--------------------------------------------------------------------------------
/spec/dummy/config/puma.rb:
--------------------------------------------------------------------------------
1 | # Puma can serve each request in a thread from an internal thread pool.
2 | # The `threads` method setting takes two numbers: a minimum and maximum.
3 | # Any libraries that use thread pools should be configured to match
4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum
5 | # and maximum; this matches the default thread size of Active Record.
6 | #
7 | threads_count = ENV.fetch('RAILS_MAX_THREADS') { 5 }
8 | threads threads_count, threads_count
9 |
10 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000.
11 | #
12 | port ENV.fetch('PORT') { 3000 }
13 |
14 | # Specifies the `environment` that Puma will run in.
15 | #
16 | environment ENV.fetch('RAILS_ENV') { 'development' }
17 |
18 | # Specifies the number of `workers` to boot in clustered mode.
19 | # Workers are forked webserver processes. If using threads and workers together
20 | # the concurrency of the application would be max `threads` * `workers`.
21 | # Workers do not work on JRuby or Windows (both of which do not support
22 | # processes).
23 | #
24 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 }
25 |
26 | # Use the `preload_app!` method when specifying a `workers` number.
27 | # This directive tells Puma to first boot the application and load code
28 | # before forking the application. This takes advantage of Copy On Write
29 | # process behavior so workers use less memory. If you use this option
30 | # you need to make sure to reconnect any threads in the `on_worker_boot`
31 | # block.
32 | #
33 | # preload_app!
34 |
35 | # If you are preloading your application and using Active Record, it's
36 | # recommended that you close any connections to the database before workers
37 | # are forked to prevent connection leakage.
38 | #
39 | # before_fork do
40 | # ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord)
41 | # end
42 |
43 | # The code in the `on_worker_boot` will be called if you are using
44 | # clustered mode by specifying a number of `workers`. After each worker
45 | # process is booted, this block will be run. If you are using the `preload_app!`
46 | # option, you will want to use this block to reconnect to any threads
47 | # or connections that may have been created at application boot, as Ruby
48 | # cannot share connections between processes.
49 | #
50 | # on_worker_boot do
51 | # ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
52 | # end
53 | #
54 |
55 | # Allow puma to be restarted by `rails restart` command.
56 | plugin :tmp_restart
57 |
--------------------------------------------------------------------------------
/lib/async_task/base_attempt.rb:
--------------------------------------------------------------------------------
1 | module AsyncTask
2 | class InvalidStateError < StandardError; end
3 |
4 | module BaseAttempt
5 | extend ActiveSupport::Concern
6 |
7 | included do
8 | extend Enumerize
9 |
10 | self.table_name = 'async_task_attempts'
11 |
12 | attr_readonly :idempotence_token
13 | attr_readonly :target
14 | attr_readonly :method_name
15 | attr_readonly :method_args
16 | attr_readonly :encryptor
17 |
18 | validates :target, presence: true
19 | validates :method_name, presence: true
20 | validates :encryptor, presence: true
21 |
22 | serialize :method_args
23 |
24 | enumerize :status,
25 | in: %i[pending succeeded failed expired],
26 | predicates: true
27 |
28 | scope :pending, -> { where(status: :pending) }
29 | scope :succeeded, -> { where(status: :succeeded) }
30 | scope :failed, -> { where(status: :failed) }
31 | scope :expired, -> { where(status: :expired) }
32 |
33 | before_create do
34 | self.target = target.to_s
35 | self.status ||= 'pending'
36 | self.encryptor = (encryptor.presence || AsyncTask::NullEncryptor).to_s
37 | end
38 |
39 | def method_args
40 | return if super.blank?
41 | YAML.load(encryptor.constantize.decrypt(super))
42 | end
43 |
44 | def method_args=(args)
45 | super(encryptor.constantize.encrypt(args.to_yaml))
46 | end
47 | end
48 |
49 | def perform!
50 | return unless may_schedule?
51 |
52 | begin
53 | reload
54 | raise AsyncTask::InvalidStateError unless pending?
55 | increment!(:num_attempts)
56 | rescue ActiveRecord::StaleObjectError
57 | retry
58 | end
59 |
60 | with_lock do
61 | raise AsyncTask::InvalidStateError unless pending?
62 |
63 | if method_args.present?
64 | target.constantize.__send__(method_name, **method_args)
65 | else
66 | target.constantize.__send__(method_name)
67 | end
68 |
69 | update_status!('succeeded')
70 | end
71 | rescue StandardError => e
72 | handle_perform_error(e)
73 | end
74 |
75 | def expire!
76 | with_lock do
77 | raise AsyncTask::InvalidStateError unless pending?
78 | update_status!('expired')
79 | end
80 | end
81 |
82 | def fail!
83 | with_lock do
84 | raise AsyncTask::InvalidStateError unless pending?
85 | update_status!('failed')
86 | end
87 | end
88 |
89 | def may_schedule?
90 | scheduled_at.blank? || scheduled_at < Time.current
91 | end
92 |
93 | private
94 |
95 | # Does nothing, override this.
96 | def handle_perform_error(_e); end
97 |
98 | def update_status!(status)
99 | update!(status: status, completed_at: Time.current)
100 | end
101 | end
102 | end
103 |
--------------------------------------------------------------------------------
/spec/async_task/models/async_task_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | RSpec.describe AsyncTask::Attempt, type: :model do
4 | describe '#perform!' do
5 | subject { task.perform! }
6 |
7 | context 'when the task is not scheduled yet' do
8 | let!(:task) do
9 | create(
10 | :async_task_attempt,
11 | target: TestClient,
12 | method_name: :do_something,
13 | scheduled_at: Time.current + 1000.days
14 | )
15 | end
16 |
17 | it { expect { subject }.to_not change { task.num_attempts } }
18 | end
19 |
20 | context 'when the task is not pending' do
21 | let!(:task) do
22 | create(
23 | :async_task_attempt,
24 | :failed,
25 | target: TestClient,
26 | method_name: :do_something
27 | )
28 | end
29 |
30 | it { expect { subject }.to raise_error(AsyncTask::InvalidStateError) }
31 | end
32 |
33 | context 'when the task is pending and will run successfully' do
34 | let!(:task) do
35 | create(
36 | :async_task_attempt,
37 | target: TestClient,
38 | method_name: :do_something,
39 | method_args: { with: :foo, as: :bar },
40 | )
41 | end
42 |
43 | it { expect { subject }.to change { task.status }.from('pending').to('succeeded') }
44 |
45 | it { expect { subject }.to change { task.num_attempts }.from(0).to(1) }
46 | end
47 |
48 | context 'when the task fails with any error' do
49 | let!(:task) do
50 | create(:async_task_attempt, target: TestClient, method_name: :do_something)
51 | end
52 |
53 | before { allow(TestClient).to receive(:do_something).and_raise(RuntimeError) }
54 |
55 | it do
56 | expect { subject }.to raise_error(RuntimeError)
57 | expect(task.num_attempts).to eq(1)
58 | end
59 | end
60 | end
61 |
62 | describe '#expire!' do
63 | subject { task.expire! }
64 |
65 | context 'with a pending task' do
66 | let!(:task) { create(:async_task_attempt, target: TestClient, method_name: :do_something) }
67 |
68 | it 'sets completed_at and status to expired' do
69 | expect { subject }.to change { task.status }.from('pending').to('expired')
70 | expect(task.completed_at).not_to be_nil
71 | end
72 | end
73 |
74 | context 'with a non-pending task' do
75 | let!(:task) do
76 | create(
77 | :async_task_attempt,
78 | :failed,
79 | target: TestClient,
80 | method_name: :do_something,
81 | )
82 | end
83 |
84 | it { expect { subject }.to raise_error(AsyncTask::InvalidStateError) }
85 | end
86 | end
87 |
88 | describe '#fail!' do
89 | subject { task.fail! }
90 |
91 | context 'with a pending task' do
92 | let!(:task) { create(:async_task_attempt, target: TestClient, method_name: :do_something) }
93 |
94 | it 'sets completed_at and status to failed' do
95 | expect { subject }.to change { task.status }.from('pending').to('failed')
96 | expect(task.completed_at).not_to be_nil
97 | end
98 | end
99 |
100 | context 'with a non-pending task' do
101 | let!(:task) do
102 | create(
103 | :async_task_attempt,
104 | :failed,
105 | target: TestClient,
106 | method_name: :do_something,
107 | )
108 | end
109 |
110 | it { expect { subject }.to raise_error(AsyncTask::InvalidStateError) }
111 | end
112 | end
113 | end
114 |
--------------------------------------------------------------------------------
/spec/dummy/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # Code is not reloaded between requests.
5 | config.cache_classes = true
6 |
7 | # Eager load code on boot. This eager loads most of Rails and
8 | # your application in memory, allowing both threaded web servers
9 | # and those relying on copy on write to perform better.
10 | # Rake tasks automatically ignore this option for performance.
11 | config.eager_load = true
12 |
13 | # Full error reports are disabled and caching is turned on.
14 | config.consider_all_requests_local = false
15 | config.action_controller.perform_caching = true
16 |
17 | # Attempt to read encrypted secrets from `config/secrets.yml.enc`.
18 | # Requires an encryption key in `ENV["RAILS_MASTER_KEY"]` or
19 | # `config/secrets.yml.key`.
20 | config.read_encrypted_secrets = true
21 |
22 | # Disable serving static files from the `/public` folder by default since
23 | # Apache or NGINX already handles this.
24 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
25 |
26 | # Compress JavaScripts and CSS.
27 | config.assets.js_compressor = :uglifier
28 | # config.assets.css_compressor = :sass
29 |
30 | # Do not fallback to assets pipeline if a precompiled asset is missed.
31 | config.assets.compile = false
32 |
33 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
34 |
35 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
36 | # config.action_controller.asset_host = 'http://assets.example.com'
37 |
38 | # Specifies the header that your server uses for sending files.
39 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
40 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
41 |
42 | # Mount Action Cable outside main process or domain
43 | # config.action_cable.mount_path = nil
44 | # config.action_cable.url = 'wss://example.com/cable'
45 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
46 |
47 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
48 | # config.force_ssl = true
49 |
50 | # Use the lowest log level to ensure availability of diagnostic information
51 | # when problems arise.
52 | config.log_level = :debug
53 |
54 | # Prepend all log lines with the following tags.
55 | config.log_tags = [:request_id]
56 |
57 | # Use a different cache store in production.
58 | # config.cache_store = :mem_cache_store
59 |
60 | # Use a real queuing backend for Active Job (and separate queues per environment)
61 | # config.active_job.queue_adapter = :resque
62 | # config.active_job.queue_name_prefix = "dummy_#{Rails.env}"
63 | config.action_mailer.perform_caching = false
64 |
65 | # Ignore bad email addresses and do not raise email delivery errors.
66 | # Set this to true and configure the email server for immediate delivery to raise delivery errors.
67 | # config.action_mailer.raise_delivery_errors = false
68 |
69 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
70 | # the I18n.default_locale when a translation cannot be found).
71 | config.i18n.fallbacks = true
72 |
73 | # Send deprecation notices to registered listeners.
74 | config.active_support.deprecation = :notify
75 |
76 | # Use default logging formatter so that PID and timestamp are not suppressed.
77 | config.log_formatter = ::Logger::Formatter.new
78 |
79 | # Use a different logger for distributed setups.
80 | # require 'syslog/logger'
81 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
82 |
83 | if ENV['RAILS_LOG_TO_STDOUT'].present?
84 | logger = ActiveSupport::Logger.new(STDOUT)
85 | logger.formatter = config.log_formatter
86 | config.logger = ActiveSupport::TaggedLogging.new(logger)
87 | end
88 |
89 | # Do not dump schema after migrations.
90 | config.active_record.dump_schema_after_migration = false
91 | end
92 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AsyncTask
2 |
3 | [](http://badge.fury.io/rb/async_task)
4 | [](https://github.com/square/async_task/blob/master/LICENSE)
5 |
6 | Lightweight, asynchronous, and database-backed execution of singleton methods.
7 |
8 | This gem provides generators and mixins to queue up tasks in database transactions to be performed
9 | later. Doing so prevents (1) tasks from being run twice if performed within a transaction and (2)
10 | tasks from synchronously blocking.
11 |
12 | ```ruby
13 | transaction do
14 | order = Order.create!(number: 7355608)
15 |
16 | # RPC Call
17 | OrderClient.fulfill(customer_token: customer_token, order_number: order.number)
18 |
19 | raise
20 | end
21 | ```
22 |
23 | Despite database transaction rolling back the creation of the `Order` record, the RPC call executes.
24 | This problem becomes more difficult in nested transactions. To avoid doing something regrettable, we
25 | create an `AsyncTask::Attempt` record inside the database. These records are then performed at a
26 | later time using a job:
27 |
28 | ```ruby
29 | transaction do
30 | order = Order.create!(number: 1)
31 |
32 | # To be performed by a job later
33 | AsyncTask::Attempt.create!(
34 | target: OrderClient,
35 | method_name: :fulfill,
36 | method_args: { customer_token: customer_token, order_number: order.number },
37 | )
38 |
39 | raise
40 | end
41 | ```
42 |
43 | The above pattern ensures we will not act when there is a rollback later in the transaction.
44 |
45 | The gem provides the following:
46 |
47 | * Models
48 | * Generators for the `AsyncTask::Attempt` migration, model, factory, and specs.
49 | * Choice between using async tasks with encrypted or unencrypted method arguments.
50 | * Tracking completion using `completed_at`.
51 | * Fields for `target`, `method_name`, and `method_args`.
52 | * `AsyncTask::BaseAttempt` mixin to provide model methods.
53 | * A `num_attempts` field gives you flexibility to handle retries and other failure scenarios.
54 | * `status` and `completed_at` are fields that track state.
55 | * `idempotence_token` field for rolling your own presence, uniqueness, or idempotence checks.
56 |
57 | * Jobs
58 | * Generators for `AsyncTask::AttemptJob`, `AsyncTask::AttemptBatchJob`, and their specs.
59 | * `AsyncTask::BaseAttemptJob` and `AsyncTask::BaseAttemptBatchJob` mixins.
60 |
61 | ## Getting Started
62 |
63 | 1. Add the gem to your application's Gemfile and execute `bundle install` to install it:
64 |
65 | ```ruby
66 | gem 'async_task'
67 | ```
68 |
69 | 2. Generate migrations, base models, jobs, and specs. Feel free to add any additional columns you
70 | need to the generated migration file:
71 |
72 | `$ rails g async_task:install`
73 |
74 | 3. Rename the model and migrations as you see fit. Make sure your model contains
75 | `include AsyncTask::BaseAttempt`. Use `self.table_name=` if necessary.
76 |
77 | ```ruby
78 | class AsyncTask::Attempt < ApplicationRecord
79 | include AsyncTask::BaseAttempt
80 | end
81 | ```
82 |
83 | 4. Implement the `handle_perform_error` in your `AsyncTask::Attempt` model. This methods is used by
84 | `AsyncTask::BaseAttempt` when exceptions are thrown performing the task.
85 |
86 | 5. This gem provides no encryptor by default. Implement an encryptor (see below) if you need
87 | encrypted params.
88 |
89 | 6. Create `AsyncTask::Attempt`s to be sent later by a job (generated) that includes a
90 | `AsyncTask::BaseAttemptJob`:
91 |
92 | ```ruby
93 | class AsyncTask::AttemptJob < ActiveJob::Base
94 | include AsyncTask::BaseAttemptJob
95 | end
96 | ```
97 |
98 | ```ruby
99 | AsyncTask::Attempt.create!(
100 | target: OrderClient,
101 | method_name: :fulfill,
102 | method_args: { customer_token: customer_token, order_number: order.number },
103 | )
104 | ```
105 |
106 | 7. **Make sure to schedule the `AsyncTask::AttemptJob` to run frequently using something like [`Clockwork`](https://github.com/adamwiggins/clockwork).**
107 |
108 | ## Cautionary Situations When Using This Gem
109 |
110 | ### Idempotence
111 |
112 | The `target`, `method_name`, and `method_args` should be idempotent because the
113 | `AsyncTask::AttemptBatchJob` could schedule multiple `AsyncTask::AttemptJob`s if the job queue is
114 | backed up.
115 |
116 | ### Nested Transactions
117 |
118 | Task execution occurs inside of a `with_lock` block, which executes the body inside of a database
119 | transaction. Keep in mind that transactions inside the `#{target}.#{method_name}` will be nested.
120 | You may have to consider implementing `transaction(require: new)` or creating transactions in
121 | separate threads.
122 |
123 | ## Cookbook
124 |
125 | ### Custom Encryptors
126 |
127 | Implement the interface present in `AsyncTask::NullEncryptor` to provide your own encryptor.
128 |
129 | ```ruby
130 | module AesEncryptor
131 | extend self
132 |
133 | def decrypt(content)
134 | AesClient.decrypt(content)
135 | end
136 |
137 | def encrypt(content)
138 | AesClient.encrypt(content)
139 | end
140 | end
141 | ```
142 |
143 | ### Delayed Execution
144 |
145 | Setting the `scheduled_at` field allows delayed execution to be possible. A task that has an
146 | `scheduled_at` before `Time.current` will be executed by `AsyncTask::BaseAttemptBatchJob`.
147 |
148 | ### Handling AsyncTask::BaseAttempt Errors
149 |
150 | ```ruby
151 | class AsyncTask::Attempt < ActiveRecord::Base
152 | include AsyncTask::BaseAttempt
153 |
154 | def handle_perform_error(error)
155 | Raven.capture_exception(error)
156 | end
157 | end
158 | ```
159 |
160 | Lastly, the `num_attempts` field in `AsyncTask::Attempt` allows you to track the number of attempts
161 | the task has undergone. Use this to implement retries and permanent failure thresholds for your
162 | tasks.
163 |
164 | ### Proper Usage of `expire!` / `fail!`
165 |
166 | `expire!` should be used for tasks that should no longer be run.
167 |
168 | `fail!` should be used to mark permanent failure for a task.
169 |
170 | ## Design Motivations
171 |
172 | We're relying heavily on generators and mixins. Including the `AsyncTask::BaseAttempt` module allows
173 | us to generate a model that can inherit from both `ActiveRecord::Base` (Rails 4) and
174 | `ApplicationRecord` (Rails 5). The `BaseAttempt` module's methods can easily be overridden, giving
175 | callers flexibility to handle errors, extend functionality, and inherit (STI). Lastly, the generated
176 | migrations provide fields used by the `BaseAttempt` module, but the developer is free to add their
177 | own fields and extend the module's methods while calling `super`.
178 |
179 | ## Development
180 |
181 | * Install dependencies with `bin/setup`.
182 | * Run tests/lints with `rake`
183 | * For an interactive prompt that will allow you to experiment, run `bin/console`.
184 |
185 | ## Acknowledgments
186 |
187 | * [RickCSong](https://github.com/RickCSong)
188 |
189 | ## License
190 |
191 | ```
192 | Copyright 2017 Square, Inc.
193 |
194 | Licensed under the Apache License, Version 2.0 (the "License");
195 | you may not use this file except in compliance with the License.
196 | You may obtain a copy of the License at
197 |
198 | http://www.apache.org/licenses/LICENSE-2.0
199 |
200 | Unless required by applicable law or agreed to in writing, software
201 | distributed under the License is distributed on an "AS IS" BASIS,
202 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
203 | See the License for the specific language governing permissions and
204 | limitations under the License.
205 | ```
206 |
--------------------------------------------------------------------------------