activejob-trackable 0.1.1

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