async_task 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []