activejob-trackable 0.1.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
+ SHA256:
3
+ metadata.gz: 594ced411f40c4d5be3910a94c4ee0bb0700ac1801f64f8504649205cafb4c06
4
+ data.tar.gz: 8012522f364548559eedc0d2893f47b6b22db70833609fe0f6ea33032dc07430
5
+ SHA512:
6
+ metadata.gz: ac3b3c478e2fba992514ee479363224cd552c75af1f1cbd377b78042b32cad14ebf26c57e206704b990e3e96c16a333a525573e4bb837d9d9e99e7feee71237a
7
+ data.tar.gz: 1ab6df8a2f86e0eec8f26245403e555cabb2dcb29ccb822681c999c3823624dae68a6d964e9700ba9e0950673913c06b6742c05aad8e4b2a47f4924fe4ae0b2c
@@ -0,0 +1,20 @@
1
+ Copyright 2019 Ignatius Reza
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,65 @@
1
+ # ActiveJob::Trackable
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/activejob-trackable.svg)](https://badge.fury.io/rb/activejob-trackable)
4
+ [![CircleCI](https://circleci.com/gh/ignatiusreza/activejob-trackable.svg?style=svg)](https://circleci.com/gh/ignatiusreza/activejob-trackable)
5
+ [![Maintainability](https://api.codeclimate.com/v1/badges/871ec3dbca5f74174fb4/maintainability)](https://codeclimate.com/github/ignatiusreza/activejob-trackable/maintainability)
6
+
7
+ `include ActiveJob::Trackable` into any jobs you want to track. Tracking jobs will grant you
8
+ access into the lifetime of each jobs and give you the ability to throttle and debounce similar jobs.
9
+
10
+ This is useful for cases where you want to make sure that certain jobs are only done at most once
11
+ per certain period or when you want to reschedule/reconfigure previously scheduled jobs
12
+
13
+ ## Usage
14
+
15
+ This gem is build with composition over inheritance in mind, and so to benefit from it
16
+ you can start by adding `include ActiveJob::Trackable` into any jobs you want to track, e.g.
17
+
18
+ ```ruby
19
+ class SampleJob < ApplicationJob
20
+ include ActiveJob::Trackable
21
+
22
+ trackable debounced: true, throttled: 1.day
23
+
24
+ def perform(one, two, three); end
25
+ end
26
+ ```
27
+
28
+ Calling `trackable` configures the trackers behavior, which defaulted to doing nothing.
29
+ Using this, you can tell the trackers to either `:debounced`, `:throttled`, or both.
30
+
31
+ `ActiveJob::Trackable::Debounced` and `ActiveJob::Trackable::Throttled` is also available as syntactic sugar
32
+
33
+ ## Compatibility
34
+
35
+ For now, this gem only support `delayed_job` with `activerecord` backend,
36
+ but support for other delayed job backend and other queue adapters are desired.
37
+
38
+ ## Installation
39
+ Add this line to your application's Gemfile:
40
+
41
+ ```ruby
42
+ gem 'activejob-trackable'
43
+ ```
44
+
45
+ And then execute:
46
+ ```bash
47
+ $ bundle
48
+ ```
49
+
50
+ Or install it yourself as:
51
+ ```bash
52
+ $ gem install activejob-trackable
53
+ ```
54
+
55
+ ## Contributing
56
+
57
+ Any and all kind of help are welcomed! Especially interested in:
58
+
59
+ - support for other delayed job backend
60
+ - support for other queue adapters officially supported by `activejob` itself
61
+
62
+ feel free to file an issue/PR!
63
+
64
+ ## License
65
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'bundler/setup'
5
+ rescue LoadError
6
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
7
+ end
8
+
9
+ require 'rdoc/task'
10
+
11
+ RDoc::Task.new(:rdoc) do |rdoc|
12
+ rdoc.rdoc_dir = 'rdoc'
13
+ rdoc.title = 'ActiveJob::Trackable'
14
+ rdoc.options << '--line-numbers'
15
+ rdoc.rdoc_files.include('README.md')
16
+ rdoc.rdoc_files.include('lib/**/*.rb')
17
+ end
18
+
19
+ require 'bundler/gem_tasks'
20
+
21
+ require 'rake/testtask'
22
+
23
+ Rake::TestTask.new(:test) do |t|
24
+ t.libs << 'test'
25
+ t.pattern = 'test/**/*_test.rb'
26
+ t.verbose = false
27
+ end
28
+
29
+ task default: :test
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'activejob/trackable/railtie'
4
+ require_relative './trackable/tracker'
5
+ require_relative './trackable/core'
6
+ require_relative './trackable/debounced'
7
+ require_relative './trackable/throttled'
8
+
9
+ module ActiveJob
10
+ # Extend `ActiveJob::Base` with the ability to track (cancel, reschedule, etc) jobs
11
+ module Trackable
12
+ extend ActiveSupport::Concern
13
+
14
+ included do
15
+ include Core
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ module Trackable
5
+ ##
6
+ # Extend `ActiveJob::Base` to automatically create a tracker for every enqueued jobs
7
+ #
8
+ # Tracker is only created if jobs is registered with a schedule (e.g by setting :wait options)
9
+ #
10
+ # Every Tracker will have their own `key`, which will be automatically generated from the
11
+ # related job class name and the arguments passed in to #perform_later. Trackers are expected
12
+ # to be unique by `key`.
13
+ #
14
+ # The default behaviour for generating key is quite minimalistic, so you might want to override
15
+ # it if you're passing non-simple-value arguments
16
+ #
17
+ # Example:
18
+ #
19
+ # ```
20
+ # class DefaultKeyJob < ActiveJob::Base
21
+ # include ActiveJob::Trackable
22
+ #
23
+ # def perform(one, two, three); end
24
+ # end
25
+ #
26
+ # # will generate tracker whose key = sample_job/foo/bar/1
27
+ # DefaultKeyJob.set(wait: 1.day).perform_later('foo', 'bar', 1)
28
+ #
29
+ # class CustomKeyJob < ActiveJob::Base
30
+ # include ActiveJob::Trackable
31
+ #
32
+ # def perform(one, two, three, four); end
33
+ #
34
+ # private
35
+ #
36
+ # def key(one, two, three, four)
37
+ # "and-a-#{one}-and-a-#{two}-and-a-#{one}-#{two}-#{three}-#{four}"
38
+ # end
39
+ # end
40
+ #
41
+ # # will generate tracker whose key = "and-a-1-and-a-2-and-a-1-2-3-4"
42
+ # CustomKeyJob.set(wait: 1.day).perform_later(1, 2, 3, 4)
43
+ # ```
44
+ #
45
+ module Core
46
+ extend ActiveSupport::Concern
47
+
48
+ included do
49
+ mattr_accessor :trackable_options, default: { debounced: false }
50
+
51
+ around_enqueue do |_job, block|
52
+ throttle do
53
+ block.call
54
+ end
55
+ end
56
+
57
+ before_enqueue do
58
+ @tracker = nil
59
+ end
60
+
61
+ after_enqueue do
62
+ next unless trackable?
63
+
64
+ tracker.track_job! self
65
+ end
66
+
67
+ after_perform do
68
+ tracker&.destroy
69
+ end
70
+ end
71
+
72
+ ##
73
+ # Provide `.trackable` class method which can be used to configure tracker behavior
74
+ #
75
+ module ClassMethods
76
+ ##
77
+ # Configure trackable, supported options:
78
+ #
79
+ # - debounced: boolean (default: false)
80
+ # - throttled: duration (default: nil)
81
+ #
82
+ def trackable(options)
83
+ trackable_options.merge! options
84
+ end
85
+ end
86
+
87
+ def tracker
88
+ @tracker ||= reuse_tracker? ?
89
+ Tracker.find_or_initialize_by(key: key(*arguments)) :
90
+ Tracker.new(key: key(*arguments))
91
+ end
92
+
93
+ private
94
+
95
+ def key(*arguments)
96
+ ([self.class.to_s.underscore] + arguments.map(&:to_s)).join('/')
97
+ end
98
+
99
+ def trackable?
100
+ if reuse_tracker?
101
+ tracker.persisted? || (scheduled_at && provider_job_id)
102
+ else
103
+ scheduled_at && provider_job_id
104
+ end
105
+ end
106
+
107
+ def reuse_tracker?
108
+ debounced? || throttled?
109
+ end
110
+
111
+ def debounced?
112
+ trackable_options[:debounced]
113
+ end
114
+
115
+ def throttled?
116
+ trackable_options[:throttled]
117
+ end
118
+
119
+ def throttle
120
+ return yield unless throttled?
121
+
122
+ expires_in = trackable_options[:throttled] + (scheduled_at - Time.current.to_f)
123
+
124
+ debounced_throttle(expires_in) { yield } || Rails.cache.fetch(key(*arguments), expires_in: expires_in) do
125
+ yield
126
+
127
+ true # put true into cache instead of serializing the job
128
+ end
129
+ end
130
+
131
+ def debounced_throttle(expires_in)
132
+ return unless debounced? && tracker.persisted?
133
+
134
+ Rails.cache.write key(*arguments), true, expires_in: expires_in
135
+
136
+ yield
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ module Trackable
5
+ ##
6
+ # Include `ActiveJob::Trackable::Debounced` to allow debouncing job when multiple jobs with identical keys
7
+ # are scheduled before it get performed
8
+ #
9
+ # Example:
10
+ #
11
+ # ```
12
+ # class SampleJob < ActiveJob::Base
13
+ # include ActiveJob::Trackable::Debounced
14
+ #
15
+ # def perform(foo, bar)
16
+ # # do something
17
+ # end
18
+ #
19
+ # private
20
+ #
21
+ # def key(foo, bar)
22
+ # foo
23
+ # end
24
+ # end
25
+ #
26
+ # # schedule a job to run 1.day.from_now
27
+ # SampleJob.set(wait: 1.day).perform_later('foo', 'bar')
28
+ #
29
+ # # less than 1 day later, reschedule with updated options & arguments
30
+ # SampleJob.set(wait: 2.day).perform_later('foo', 'baz')
31
+ # ```
32
+ #
33
+ module Debounced
34
+ extend ActiveSupport::Concern
35
+
36
+ included do
37
+ include Core
38
+
39
+ trackable debounced: true
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ module Trackable
5
+ class Railtie < ::Rails::Railtie
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ module Trackable
5
+ ##
6
+ # Include `ActiveJob::Trackable::Throttled` to throttle scheduling job when multiple jobs
7
+ # with identical keys tried to be scheduled before it get performed
8
+ #
9
+ # Throtlling period are counted in relative to when the job is scheduled to run.
10
+ # For example, is a job is configured to throttled every 1 day, and it is scheduled
11
+ # to run in 5 hours; the throttling will be active for the full duration of 1 day and 5 hours
12
+ #
13
+ # Example:
14
+ #
15
+ # ```
16
+ # class SampleJob < ActiveJob::Base
17
+ # include ActiveJob::Trackable::Throttled
18
+ #
19
+ # trackable throttled: 1.day
20
+ #
21
+ # def perform(one, two, three); end
22
+ # end
23
+ #
24
+ # # schedule a job to run 1.hour.from_now
25
+ # SampleJob.set(wait: 1.hour).perform_later('foo', 'bar')
26
+ #
27
+ # # less than 1 day later, trying to schedule the same job will silently fail
28
+ # SampleJob.set(wait: 2.hour).perform_later('foo', 'bar')
29
+ # ```
30
+ #
31
+ module Throttled
32
+ extend ActiveSupport::Concern
33
+
34
+ included do
35
+ include Core
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ module Trackable
5
+ class Tracker < ActiveRecord::Base # :nodoc:
6
+ self.table_name = 'active_job_trackers'
7
+
8
+ validates :provider_job_id, :key, presence: true
9
+
10
+ def track_job!(job)
11
+ self.class.transaction do
12
+ job.tracker.provider_job&.destroy!
13
+ self.provider_job_id = job.provider_job_id
14
+ save!
15
+ dememoized!
16
+ end
17
+ end
18
+
19
+ def job
20
+ @job ||= ActiveJob::Base.deserialize(job_data).tap do |job|
21
+ job.provider_job_id = provider_job_id.to_i
22
+
23
+ # this sux, but can't find other way around it
24
+ job.send :deserialize_arguments_if_needed
25
+ job.scheduled_at = provider_job.run_at.to_f
26
+ end
27
+ end
28
+
29
+ def reload
30
+ dememoized!
31
+ super
32
+ end
33
+
34
+ protected
35
+
36
+ def provider_job
37
+ @provider_job ||= provider_job_id.presence && Delayed::Job.find(provider_job_id)
38
+ end
39
+
40
+ private
41
+
42
+ def dememoized!
43
+ @job = nil
44
+ @provider_job = nil
45
+ end
46
+
47
+ def job_data
48
+ provider_job.payload_object.job_data
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ module Trackable
5
+ VERSION = '0.1.1'
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Generate activerecord migration for the table related to ActiveJob::Trackable::Tracker
3
+
4
+ Example:
5
+ rails generate active_job:trackable
6
+
7
+ will create:
8
+ db/migrate/*_create_active_job_trackers.rb
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateActiveJobTrackers < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ create_table :active_job_trackers, force: true do |t|
6
+ t.string :provider_job_id, index: true
7
+ t.string :key, index: { unique: true }
8
+
9
+ t.timestamps
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ ##
5
+ #
6
+ # Generator for generating activerecord migration for the table related to ActiveJob::Trackable::Tracker
7
+ #
8
+ # invoke with `rails generate active_job:trackable`
9
+ #
10
+ class TrackableGenerator < Rails::Generators::Base
11
+ include Rails::Generators::Migration
12
+
13
+ source_root File.expand_path('templates', __dir__)
14
+
15
+ def self.next_migration_number(dirname)
16
+ next_migration_number = current_migration_number(dirname) + 1
17
+
18
+ if ActiveRecord::Base.timestamped_migrations
19
+ [Time.now.utc.strftime('%Y%m%d%H%M%S'), format('%.14d', next_migration_number)].max
20
+ else
21
+ format('%.3d', next_migration_number)
22
+ end
23
+ end
24
+
25
+ def create_migration_file
26
+ migration_template 'migration.rb', migration_destination, migration_version: migration_version
27
+ end
28
+
29
+ private
30
+
31
+ def migration_destination
32
+ 'db/migrate/create_active_job_trackers.rb'
33
+ end
34
+
35
+ def migration_version
36
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]" if ActiveRecord::VERSION::MAJOR >= 5
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # desc "Explaining what the task does"
4
+ # task :activejob_trackable do
5
+ # # Task goes here
6
+ # end
metadata ADDED
@@ -0,0 +1,114 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activejob-trackable
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Ignatius Reza
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-02-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 5.2.2
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 5.2.2
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest-ci
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubocop
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: sqlite3
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 1.3.6
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 1.3.6
69
+ description: Get more control into your jobs with the ability to track (debounce,
70
+ throttle) jobs
71
+ email:
72
+ - lyoneil.de.sire@gmail.com
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - MIT-LICENSE
78
+ - README.md
79
+ - Rakefile
80
+ - lib/activejob/trackable.rb
81
+ - lib/activejob/trackable/core.rb
82
+ - lib/activejob/trackable/debounced.rb
83
+ - lib/activejob/trackable/railtie.rb
84
+ - lib/activejob/trackable/throttled.rb
85
+ - lib/activejob/trackable/tracker.rb
86
+ - lib/activejob/trackable/version.rb
87
+ - lib/generators/active_job/trackable/USAGE
88
+ - lib/generators/active_job/trackable/templates/migration.rb
89
+ - lib/generators/active_job/trackable/trackable_generator.rb
90
+ - lib/tasks/activejob/trackable_tasks.rake
91
+ homepage: https://github.com/ignatiusreza/activejob-trackable
92
+ licenses:
93
+ - MIT
94
+ metadata: {}
95
+ post_install_message:
96
+ rdoc_options: []
97
+ require_paths:
98
+ - lib
99
+ required_ruby_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ required_rubygems_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ requirements: []
110
+ rubygems_version: 3.0.2
111
+ signing_key:
112
+ specification_version: 4
113
+ summary: Extend ActiveJob with the ability to track (debounce, throttle) jobs
114
+ test_files: []