resumable_job 0.1.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8f854f34208a918c0a0ef31c56c7fb4948c6049d
4
+ data.tar.gz: 5d55ec698d3f0647f613b989e17b13fd0c7bfeeb
5
+ SHA512:
6
+ metadata.gz: 97a12e2d542b4f891f61af9c9a4381679296f5d87780093c4e6ea312d0a63642c03d759159bccef3068ad054db9ccade026db5c99ac044f88728f56f73a2a2be
7
+ data.tar.gz: 6976695e4357d114cb31cf53fcaa6bd838e140b14cbeb316edc61b851508015bcee3b847832e75cdc985de2d022d827f24a4618661cfd797a60570fea74451ba
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ .rakeTasks
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.4.4
5
+ before_install: gem install bundler -v 1.16.2
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in resumable_job.gemspec
6
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,22 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ resumable_job (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ minitest (5.11.3)
10
+ rake (10.5.0)
11
+
12
+ PLATFORMS
13
+ x64-mingw32
14
+
15
+ DEPENDENCIES
16
+ bundler (~> 1.16)
17
+ minitest (~> 5.0)
18
+ rake (~> 10.0)
19
+ resumable_job!
20
+
21
+ BUNDLED WITH
22
+ 1.16.2
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Derk-Jan Karrenbeld
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,114 @@
1
+ # ResumableJob
2
+
3
+ Make any `ActiveJob` resumable.
4
+
5
+ Use exception flow to make jobs exceptionally resumable, whilst retaining other state, with automatic exponential
6
+ backoff handling. ActiveJob is not a dependency, so this could be used with "anything". Adds a `module` to `include`
7
+ somewhere that adds a method which yields a block. During this block, you can throw a `ResumableJob::ResumeLater` to
8
+ call the following:
9
+
10
+ ```Ruby
11
+ self.class
12
+ .set(wait_until: resume_at || ResumableJob::Backoff.to_time(attempt))
13
+ .perform_later(pause(state).merge(attempt: attempt + 1))
14
+ ````
15
+
16
+ State is passed through `pause` and when `pause` is not overridden will be all the arguments you passed to your job plus
17
+ an `attempt` argument that is steadily increased in order to to exponential backoff.
18
+
19
+ ## Installation
20
+
21
+ Add this line to your application's Gemfile:
22
+
23
+ ```ruby
24
+ gem 'resumable_job'
25
+ ```
26
+
27
+ And then execute:
28
+
29
+ $ bundle
30
+
31
+ Or install it yourself as:
32
+
33
+ $ gem install resumable_job
34
+
35
+ ## Usage
36
+
37
+ ### Make a job resumable
38
+
39
+ Simple example to implement pagination that resumes later if you receive a "Rate Limit Exceeded".
40
+
41
+ ```Ruby
42
+ class FetchDataJob < ApplicationJob
43
+ include ResumableJob::Resumable
44
+
45
+ def perform(state)
46
+ page = state.fetch(:page) { 1 }
47
+
48
+ resumable(state) do
49
+ loop do
50
+ result = DataFetcher.call(page: page)
51
+ raise ResumableJob::ResumeLater(state: state.merge(page: page)) if result.status == 429
52
+ break unless result.next_page?
53
+
54
+ page = result.next_page
55
+ end
56
+ end
57
+ end
58
+ end
59
+ ```
60
+
61
+ ### Turn inner exception into resumable
62
+
63
+ When the exception has more information (for example a "rate limit reset" value), it can be turned into a resume later.
64
+ Additionally, the `state` of the resume later exception will me merged into the original state, and then into the pause
65
+ state.
66
+
67
+
68
+ ```Ruby
69
+ class FetchDataJob < ApplicationJob
70
+ include ResumableJob::Resumable
71
+
72
+ def perform(state)
73
+ resumable(state) do
74
+ fetch_data(state)
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ def fetch_data(state)
81
+ RateLimitableFetcher.call(state)
82
+ rescue RateLimitableFetcher::RateLimited => ex
83
+ raise ResumableJob::ResumeLater.new(state: state, utc: ex.retry_at, message: ex.message)
84
+ end
85
+ end
86
+ ```
87
+
88
+ ### Filter out keys from the state
89
+ Some state is not serializable. You may be calling your job with `perform_now`, but when it resumes later, some
90
+ arguments can not be serialized. Use the `pause` override to include state not originally present, modify state that is
91
+ not passed by your exception (ResumeLater exception state), or remove state.
92
+
93
+ ```Ruby
94
+ class FetchDataJob < ApplicationJob
95
+ include ResumableJob::Resumable
96
+
97
+ def pause(state)
98
+ state.slice(:attempt, :page, :token)
99
+ end
100
+ end
101
+ ```
102
+
103
+ ## Development
104
+
105
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can
106
+ also run `bin/console` for an interactive prompt that will allow you to experiment.
107
+
108
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the
109
+ version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version,
110
+ push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
111
+
112
+ ## Contributing
113
+
114
+ Bug reports and pull requests are welcome on GitHub at https://github.com/SleeplessByte/resumable_job.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << 'test'
6
+ t.libs << 'lib'
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task default: :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'resumable_job'
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require 'irb'
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -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,35 @@
1
+ module ResumableJob
2
+ class Backoff
3
+ DEFAULT_BASE_IN_MINUTES = 1
4
+ SECONDS_PER_MINUTE = 60
5
+
6
+ class << self
7
+ def to_i(*args)
8
+ new(*args).to_i
9
+ end
10
+
11
+ def to_time(*args)
12
+ new(*args).to_time
13
+ end
14
+ end
15
+
16
+ delegate :to_i, to: :to_time
17
+
18
+ def initialize(attempt, base: DEFAULT_BASE_IN_MINUTES)
19
+ self.attempt = attempt
20
+ self.base = base
21
+ end
22
+
23
+ def to_time
24
+ Time.now + delay
25
+ end
26
+
27
+ private
28
+
29
+ attr_accessor :attempt, :base
30
+
31
+ def delay
32
+ (2**attempt) * base * SECONDS_PER_MINUTE
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,117 @@
1
+ module ResumableJob
2
+
3
+ ##
4
+ # Include in an {ActiveJob::Job} to make resumable.
5
+ #
6
+ # Adds a yield guard resumable that catches any thrown instance of {ResumeLater}, which enqueues the job at a
7
+ # later time, either by getting a utc in the future from the exception or using a backoff algorithm.
8
+ #
9
+ module Resumable
10
+ private
11
+
12
+ ##
13
+ # Resumable guard
14
+ #
15
+ # @param state [Hash] the job state
16
+ #
17
+ # @example Make a job resumable
18
+ #
19
+ # class FetchDataJob < ApplicationJob
20
+ # include ResumableJob::Resumable
21
+ #
22
+ # def perform(state)
23
+ # page = state.fetch(:page) { 1 }
24
+ #
25
+ # resumable(state) do
26
+ # loop do
27
+ # result = DataFetcher.call(page: page)
28
+ # raise ResumableJob::ResumeLater(state: state.merge(page: page)) if result.status == 429
29
+ # break unless result.next_page?
30
+ #
31
+ # page = result.next_page
32
+ # end
33
+ # end
34
+ # end
35
+ # end
36
+ #
37
+ # @example Filter out state
38
+ #
39
+ # class FetchDataJob < ApplicationJob
40
+ # include ResumableJob::Resumable
41
+ #
42
+ # def pause(state)
43
+ # state.slice(:attempt, :page, :token)
44
+ # end
45
+ #
46
+ # end
47
+ #
48
+ # @example Turn inner exception into resumable
49
+ #
50
+ # class FetchDataJob < ApplicationJob
51
+ # include ResumableJob::Resumable
52
+ # def perform(state)
53
+ # resumable(state) do
54
+ # fetch_data(state)
55
+ # end
56
+ # end
57
+ #
58
+ # private
59
+ #
60
+ # def fetch_data(state)
61
+ # RateLimitableFetcher.call(state)
62
+ # rescue RateLimitableFetcher::RateLimited => ex
63
+ # raise ResumableJob::ResumeLater.new(state: state, utc: ex.retry_at, message: ex.message)
64
+ # end
65
+ # end
66
+ #
67
+ def resumable(state)
68
+ attempt = state.fetch(:attempt) { 0 }
69
+ yield attempt
70
+ rescue ResumableJob::ResumeLater => ex
71
+ resume_later(
72
+ resume_at: ex.utc,
73
+ state: state.merge(ex.state),
74
+ attempt: attempt
75
+ )
76
+ end
77
+
78
+ ##
79
+ # Schedules the current job to +resume_at+ a later time, either given or calculated from +attempt+.
80
+ #
81
+ # @see #pause
82
+ # @see ResumableJob::Backoff.to_time
83
+ #
84
+ # @param [NilClass, Numeric] resume_at
85
+ # @param [Numeric] attempt the current attempt
86
+ # @param [Hash] state state to merge with the state from {#pause}
87
+ #
88
+ def resume_later(resume_at: nil, state: {}, attempt: 0)
89
+ self.class
90
+ .set(wait_until: resume_at || ResumableJob::Backoff.to_time(attempt))
91
+ .perform_later(pause(state).merge(attempt: attempt + 1))
92
+ end
93
+
94
+ ##
95
+ # Calculates the state to be passed for rescheduling this job.
96
+ # By default outputs input, override {#pause} to change what is passed to the job.
97
+ #
98
+ # @param [Hash] state
99
+ # @return [Hash] state
100
+ #
101
+ # @example Remove a key from the state
102
+ #
103
+ # def pause(state)
104
+ # state.except(:key_to_exclude)
105
+ # end
106
+ #
107
+ # @example Update the state before pausing
108
+ #
109
+ # def pause(state)
110
+ # state.merge!(foo: state[:foo] * 2)
111
+ # end
112
+ #
113
+ def pause(state)
114
+ state
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,12 @@
1
+ module ResumableJob
2
+ class ResumeLater < RuntimeError
3
+ def initialize(state: {}, utc: nil, message:)
4
+ self.state = state || {}
5
+ self.utc = utc
6
+
7
+ super message
8
+ end
9
+
10
+ attr_accessor :state, :utc
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ module ResumableJob
2
+ VERSION = '0.1.0'.freeze
3
+ end
@@ -0,0 +1,8 @@
1
+ require 'resumable_job/version'
2
+
3
+ require 'resumable_job/backoff'
4
+ require 'resumable_job/resumable'
5
+ require 'resumable_job/resume_later'
6
+
7
+ module ResumableJob
8
+ end
@@ -0,0 +1,41 @@
1
+
2
+ lib = File.expand_path('lib', __dir__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'resumable_job/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'resumable_job'
8
+ spec.version = ResumableJob::VERSION
9
+ spec.authors = ['Derk-Jan Karrenbeld']
10
+ spec.email = ['derk-jan+github@karrenbeld.info']
11
+
12
+ spec.summary = 'Make an ActiveJob resumable'
13
+ spec.license = 'MIT'
14
+
15
+ spec.metadata = {
16
+ 'bug_tracker_uri' => 'https://github.com/SleeplessByte/resumable_job/issues',
17
+ 'changelog_uri' => 'https://github.com/SleeplessByte/resumable_job/CHANGELOG.md',
18
+ 'homepage_uri' => 'https://github.com/SleeplessByte/resumable_job',
19
+ 'source_code_uri' => 'https://github.com/SleeplessByte/resumable_job'
20
+ }
21
+
22
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
23
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
24
+ if spec.respond_to?(:metadata)
25
+ # spec.metadata['allowed_push_host'] = 'https://gems.sleeplessbyte.technology'
26
+ else
27
+ raise 'RubyGems 2.0 or newer is required to protect against ' \
28
+ 'public gem pushes.'
29
+ end
30
+
31
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
32
+ f.match(%r{^(test|spec|features)/})
33
+ end
34
+ spec.bindir = 'exe'
35
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
36
+ spec.require_paths = ['lib']
37
+
38
+ spec.add_development_dependency 'bundler', '~> 1.16'
39
+ spec.add_development_dependency 'minitest', '~> 5.0'
40
+ spec.add_development_dependency 'rake', '~> 10.0'
41
+ end
metadata ADDED
@@ -0,0 +1,105 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: resumable_job
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Derk-Jan Karrenbeld
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-06-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.16'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.16'
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '5.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '5.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ description:
56
+ email:
57
+ - derk-jan+github@karrenbeld.info
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - ".travis.yml"
64
+ - Gemfile
65
+ - Gemfile.lock
66
+ - LICENSE.txt
67
+ - README.md
68
+ - Rakefile
69
+ - bin/console
70
+ - bin/setup
71
+ - lib/resumable_job.rb
72
+ - lib/resumable_job/backoff.rb
73
+ - lib/resumable_job/resumable.rb
74
+ - lib/resumable_job/resume_later.rb
75
+ - lib/resumable_job/version.rb
76
+ - resumable_job.gemspec
77
+ homepage:
78
+ licenses:
79
+ - MIT
80
+ metadata:
81
+ bug_tracker_uri: https://github.com/SleeplessByte/resumable_job/issues
82
+ changelog_uri: https://github.com/SleeplessByte/resumable_job/CHANGELOG.md
83
+ homepage_uri: https://github.com/SleeplessByte/resumable_job
84
+ source_code_uri: https://github.com/SleeplessByte/resumable_job
85
+ post_install_message:
86
+ rdoc_options: []
87
+ require_paths:
88
+ - lib
89
+ required_ruby_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ requirements: []
100
+ rubyforge_project:
101
+ rubygems_version: 2.6.14.1
102
+ signing_key:
103
+ specification_version: 4
104
+ summary: Make an ActiveJob resumable
105
+ test_files: []