async_task 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ee19b9b762966a602d9963bff309cabe1b4e46d0
4
+ data.tar.gz: 5cb5522d5eb98fb0b6a6d0fb60d93a31b686ea0d
5
+ SHA512:
6
+ metadata.gz: 425b2c7a2a788a16b96805d342be67029b7c20b3c6f06ee55d3b84d8e2b0ddb1164be05c5b29bd6ef7dbbc7f4491d4f9a0b1d71ca3efc1159e87b0e37376f0bb
7
+ data.tar.gz: ca8c5f65e7fe98c2841740a2f49499790e33faa0a9658847704fa1dc230ec112b86aad582e6c5fa8b603b02e7ccfba4131c109e2987e77c43d64770fdeb548a8
@@ -0,0 +1,19 @@
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
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,29 @@
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
@@ -0,0 +1,3 @@
1
+ ### 0.0.1 / 2017-12-04
2
+
3
+ * Initial release
data/Gemfile ADDED
@@ -0,0 +1,22 @@
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'
data/LICENSE ADDED
@@ -0,0 +1,13 @@
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.
@@ -0,0 +1,202 @@
1
+ # AsyncTask
2
+
3
+ Lightweight, asynchronous, and database-backed execution of singleton methods.
4
+
5
+ This gem provides generators and mixins to queue up tasks in database transactions to be performed
6
+ later. Doing so prevents (1) tasks from being run twice if performed within a transaction and (2)
7
+ tasks from synchronously blocking.
8
+
9
+ ```ruby
10
+ transaction do
11
+ order = Order.create!(number: 7355608)
12
+
13
+ # RPC Call
14
+ OrderClient.fulfill(customer_token: customer_token, order_number: order.number)
15
+
16
+ raise
17
+ end
18
+ ```
19
+
20
+ Despite database transaction rolling back the creation of the `Order` record, the RPC call executes.
21
+ This problem becomes more difficult in nested transactions. To avoid doing something regrettable, we
22
+ create an `AsyncTask::Attempt` record inside the database. These records are then performed at a
23
+ later time using a job:
24
+
25
+ ```ruby
26
+ transaction do
27
+ order = Order.create!(number: 1)
28
+
29
+ # To be performed by a job later
30
+ AsyncTask::Attempt.create!(
31
+ target: OrderClient,
32
+ method_name: :fulfill,
33
+ method_args: { customer_token: customer_token, order_number: order.number },
34
+ )
35
+
36
+ raise
37
+ end
38
+ ```
39
+
40
+ The above pattern ensures we will not act when there is a rollback later in the transaction.
41
+
42
+ The gem provides the following:
43
+
44
+ * Models
45
+ * Generators for the `AsyncTask::Attempt` migration, model, factory, and specs.
46
+ * Choice between using async tasks with encrypted or unencrypted method arguments.
47
+ * Tracking completion using `completed_at`.
48
+ * Fields for `target`, `method_name`, and `method_args`.
49
+ * `AsyncTask::BaseAttempt` mixin to provide model methods.
50
+ * A `num_attempts` field gives you flexibility to handle retries and other failure scenarios.
51
+ * `status` and `completed_at` are fields that track state.
52
+ * `idempotence_token` field for rolling your own presence, uniqueness, or idempotence checks.
53
+
54
+ * Jobs
55
+ * Generators for `AsyncTask::AttemptJob`, `AsyncTask::AttemptBatchJob`, and their specs.
56
+ * `AsyncTask::BaseAttemptJob` and `AsyncTask::BaseAttemptBatchJob` mixins.
57
+
58
+ ## Getting Started
59
+
60
+ 1. Add the gem to your application's Gemfile and execute `bundle install` to install it:
61
+
62
+ ```ruby
63
+ gem 'async_task'
64
+ ```
65
+
66
+ 2. Generate migrations, base models, jobs, and specs. Feel free to add any additional columns you
67
+ need to the generated migration file:
68
+
69
+ `$ rails g async_task:install`
70
+
71
+ 3. Rename the model and migrations as you see fit. Make sure your model contains
72
+ `include AsyncTask::BaseAttempt`. Use `self.table_name=` if necessary.
73
+
74
+ ```ruby
75
+ class AsyncTask::Attempt < ApplicationRecord
76
+ include AsyncTask::BaseAttempt
77
+ end
78
+ ```
79
+
80
+ 4. Implement the `handle_perform_error` in your `AsyncTask::Attempt` model. This methods is used by
81
+ `AsyncTask::BaseAttempt` when exceptions are thrown performing the task.
82
+
83
+ 5. This gem provides no encryptor by default. Implement an encryptor (see below) if you need
84
+ encrypted params.
85
+
86
+ 6. Create `AsyncTask::Attempt`s to be sent later by a job (generated) that includes a
87
+ `AsyncTask::BaseAttemptJob`:
88
+
89
+ ```ruby
90
+ class AsyncTask::AttemptJob < ActiveJob::Base
91
+ include AsyncTask::BaseAttemptJob
92
+ end
93
+ ```
94
+
95
+ ```ruby
96
+ AsyncTask::Attempt.create!(
97
+ target: OrderClient,
98
+ method_name: :fulfill,
99
+ method_args: { customer_token: customer_token, order_number: order.number },
100
+ )
101
+ ```
102
+
103
+ 7. **Make sure to schedule the `AsyncTask::AttemptJob` to run frequently using something like [`Clockwork`](https://github.com/adamwiggins/clockwork).**
104
+
105
+ ## Cautionary Situations When Using This Gem
106
+
107
+ ### Task Idempotence
108
+
109
+ The `target`, `method_name`, and `method_args` should be idempotent because the
110
+ `AsyncTask::AttemptBatchJob` could schedule multiple `AsyncTask::AttemptJob`s if the job queue is
111
+ backed up.
112
+
113
+ ### Nested Transactions
114
+
115
+ Task execution occurs inside of a `with_lock` block, which executes the body inside of a database
116
+ transaction. Keep in mind that transactions inside the `#{target}.#{method_name}` will be nested.
117
+ You may have to consider implementing `transaction(require: new)` or creating transactions in
118
+ separate threads.
119
+
120
+ ## Cookbook
121
+
122
+ ### Custom Encryptors
123
+
124
+ Implement the interface present in `AsyncTask::NullEncryptor` to provide your own encryptor.
125
+
126
+ ```ruby
127
+ module AesEncryptor
128
+ extend self
129
+
130
+ def decrypt(content)
131
+ AesClient.decrypt(content)
132
+ end
133
+
134
+ def encrypt(content)
135
+ AesClient.encrypt(content)
136
+ end
137
+ end
138
+ ```
139
+
140
+ ### Delayed Execution
141
+
142
+ Setting the `scheduled_at` field allows delayed execution to be possible. A task that has an
143
+ `scheduled_at` before `Time.current` will be executed by `AsyncTask::BaseAttemptBatchJob`.
144
+
145
+ ### Handling AsyncTask::BaseAttempt Errors
146
+
147
+ ```ruby
148
+ class AsyncTask::Attempt < ActiveRecord::Base
149
+ include AsyncTask::BaseAttempt
150
+
151
+ def handle_perform_error(error)
152
+ Raven.capture_exception(error)
153
+ end
154
+ end
155
+ ```
156
+
157
+ Lastly, the `num_attempts` field in `AsyncTask::Attempt` allows you to track the number of attempts
158
+ the task has undergone. Use this to implement retries and permanent failure thresholds for your
159
+ tasks.
160
+
161
+ ### Proper Usage of `expire!` / `fail!`
162
+
163
+ `expire!` should be used for tasks that should no longer be run.
164
+
165
+ `fail!` should be used to mark permanent failure for a task.
166
+
167
+ ## Design Motivations
168
+
169
+ We're relying heavily on generators and mixins. Including the `AsyncTask::BaseAttempt` module allows
170
+ us to generate a model that can inherit from both `ActiveRecord::Base` (Rails 4) and
171
+ `ApplicationRecord` (Rails 5). The `BaseAttempt` module's methods can easily be overridden, giving
172
+ callers flexibility to handle errors, extend functionality, and inherit (STI). Lastly, the generated
173
+ migrations provide fields used by the `BaseAttempt` module, but the developer is free to add their
174
+ own fields and extend the module's methods while calling `super`.
175
+
176
+ ## Development
177
+
178
+ * Install dependencies with `bin/setup`.
179
+ * Run tests/lints with `rake`
180
+ * For an interactive prompt that will allow you to experiment, run `bin/console`.
181
+
182
+ ## Acknowledgments
183
+
184
+ * [RickCSong](https://github.com/RickCSong)
185
+
186
+ ## License
187
+
188
+ ```
189
+ Copyright 2017 Square, Inc.
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
202
+ ```
@@ -0,0 +1,9 @@
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]
@@ -0,0 +1,31 @@
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
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'async_task'
5
+
6
+ require 'pry'
7
+ Pry.start
@@ -0,0 +1,8 @@
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
@@ -0,0 +1,13 @@
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 }
@@ -0,0 +1,102 @@
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
@@ -0,0 +1,19 @@
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
@@ -0,0 +1,21 @@
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
@@ -0,0 +1,16 @@
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
@@ -0,0 +1,13 @@
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
@@ -0,0 +1,3 @@
1
+ module AsyncTask
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,37 @@
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
@@ -0,0 +1,10 @@
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
@@ -0,0 +1,3 @@
1
+ class AsyncTask::AttemptBatchJob < <% if Rails::VERSION::STRING >= '5' %>ApplicationJob<% else %>ActiveJob::Base<% end %>
2
+ include AsyncTask::BaseAttemptBatchJob
3
+ end
@@ -0,0 +1,5 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.describe AsyncTask::AttemptBatchJob, type: :job do
4
+ pending "add some examples to (or delete) #{__FILE__}"
5
+ end
@@ -0,0 +1,3 @@
1
+ class AsyncTask::AttemptJob < <% if Rails::VERSION::STRING >= '5' %>ApplicationJob<% else %>ActiveJob::Base<% end %>
2
+ include AsyncTask::BaseAttemptJob
3
+ end
@@ -0,0 +1,5 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.describe AsyncTask::AttemptJob, type: :job do
4
+ pending "add some examples to (or delete) #{__FILE__}"
5
+ end
@@ -0,0 +1,5 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.describe AsyncTask::Attempt, type: :model do
4
+ pending "add some examples to (or delete) #{__FILE__}"
5
+ end
@@ -0,0 +1,21 @@
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
@@ -0,0 +1,33 @@
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
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -eux -o pipefail
4
+
5
+ REQUIRED_BUNDLER_VERSION="~> 1.14.3"
6
+
7
+ function update_rubygems() {
8
+ gem update --system
9
+ }
10
+
11
+ function install_bundler() {
12
+ gem install bundler --conservative --version "$REQUIRED_BUNDLER_VERSION"
13
+ }
14
+
15
+ function install_gems() {
16
+ bundle check || bundle
17
+ }
18
+
19
+ function run_rake() {
20
+ bundle exec rake
21
+ }
22
+
23
+ update_rubygems &&
24
+ install_bundler &&
25
+ install_gems &&
26
+ run_rake
metadata ADDED
@@ -0,0 +1,161 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: async_task
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - James Chang
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-12-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activejob
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 4.2.0
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '5.2'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: 4.2.0
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '5.2'
33
+ - !ruby/object:Gem::Dependency
34
+ name: activerecord
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 4.2.0
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: '5.2'
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: 4.2.0
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '5.2'
53
+ - !ruby/object:Gem::Dependency
54
+ name: activesupport
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: 4.2.0
60
+ - - "<"
61
+ - !ruby/object:Gem::Version
62
+ version: '5.2'
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: 4.2.0
70
+ - - "<"
71
+ - !ruby/object:Gem::Version
72
+ version: '5.2'
73
+ - !ruby/object:Gem::Dependency
74
+ name: enumerize
75
+ requirement: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ type: :runtime
81
+ prerelease: false
82
+ version_requirements: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ - !ruby/object:Gem::Dependency
88
+ name: with_advisory_lock
89
+ requirement: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ type: :runtime
95
+ prerelease: false
96
+ version_requirements: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ description:
102
+ email:
103
+ - jchang@squareup.com
104
+ executables:
105
+ - console
106
+ - setup
107
+ extensions: []
108
+ extra_rdoc_files: []
109
+ files:
110
+ - ".gitignore"
111
+ - ".rspec"
112
+ - ".rubocop.yml"
113
+ - CHANGES.md
114
+ - Gemfile
115
+ - LICENSE
116
+ - README.md
117
+ - Rakefile
118
+ - async_task.gemspec
119
+ - bin/console
120
+ - bin/setup
121
+ - lib/async_task.rb
122
+ - lib/async_task/base_attempt.rb
123
+ - lib/async_task/jobs/base_attempt_batch_job.rb
124
+ - lib/async_task/jobs/base_attempt_job.rb
125
+ - lib/async_task/jobs/base_job.rb
126
+ - lib/async_task/null_encryptor.rb
127
+ - lib/async_task/version.rb
128
+ - lib/generators/async_task/install_generator.rb
129
+ - lib/generators/async_task/templates/async_task_attempt.rb.erb
130
+ - lib/generators/async_task/templates/async_task_attempt_batch_job.rb.erb
131
+ - lib/generators/async_task/templates/async_task_attempt_batch_job_spec.rb.erb
132
+ - lib/generators/async_task/templates/async_task_attempt_job.rb.erb
133
+ - lib/generators/async_task/templates/async_task_attempt_job_spec.rb.erb
134
+ - lib/generators/async_task/templates/async_task_attempt_spec.rb.erb
135
+ - lib/generators/async_task/templates/async_task_attempts.rb.erb
136
+ - lib/generators/async_task/templates/create_async_task_attempts.rb
137
+ - script/ci
138
+ homepage: https://github.com/square/async_task
139
+ licenses: []
140
+ metadata: {}
141
+ post_install_message:
142
+ rdoc_options: []
143
+ require_paths:
144
+ - lib
145
+ required_ruby_version: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - ">="
148
+ - !ruby/object:Gem::Version
149
+ version: '2.3'
150
+ required_rubygems_version: !ruby/object:Gem::Requirement
151
+ requirements:
152
+ - - ">="
153
+ - !ruby/object:Gem::Version
154
+ version: '0'
155
+ requirements: []
156
+ rubyforge_project:
157
+ rubygems_version: 2.5.1
158
+ signing_key:
159
+ specification_version: 4
160
+ summary: Lightweight, asynchronous, and database-backed execution of singleton methods.
161
+ test_files: []