activejob-traffic_control 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3a536acd2cfc994594b0ae3c7e463b4aac834ba5
4
- data.tar.gz: 87ab13103b57fe115c1b287c18714662892a42e9
3
+ metadata.gz: 2906ae48cd5953f18837860c3685e3ebf8a2dd84
4
+ data.tar.gz: 5bb69b07b5439da6c1f56d5674fbfec37bd111d8
5
5
  SHA512:
6
- metadata.gz: fa226f177e0f5cb5408222de47f3652ccf88ee591719a39521db19fbc68ff21fca906e6a8c06e2eba7615f5be7b3ec04561121a539198d5e75a297d8cbd151da
7
- data.tar.gz: fe358db32250fbb19b129d7061d2abd3cd5129e86a149a16e9b6c234d05cca72a400d06692d691426bb3927b5384cfd9544db39d570fcf97a1727e22fe08410e
6
+ metadata.gz: 5887b615c100a481113967f5541c4e4d86621a4ce9a4ae604dcac4c524c53d0a097ec30ce9a5d88d6bd30d18fdcce4646d8731d660a190532fc496a31b19a39a
7
+ data.tar.gz: d5ccb7e7fa1c4782bcc1650a62d6f1ca17117aace77a130e2c6561714d97619faec95f86b5526516772f3ca429361ce4701e727c212585d8f896528b0de2170c
data/.gitignore CHANGED
@@ -1,9 +1,18 @@
1
- /.bundle/
2
- /.yardoc
3
- /Gemfile.lock
4
- /_yardoc/
5
- /coverage/
6
- /doc/
7
- /pkg/
8
- /spec/reports/
9
- /tmp/
1
+ .DS_Store
2
+ *.gem
3
+ *.rbc
4
+ .bundle
5
+ .config
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/.rubocop.yml CHANGED
@@ -1,4 +1,5 @@
1
1
  AllCops:
2
+ TargetRubyVersion: 2.3
2
3
  Exclude:
3
4
  - .git/**/*
4
5
  - tmp/**/*
data/Gemfile CHANGED
@@ -1,4 +1,6 @@
1
- source 'https://rubygems.org'
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
2
4
 
3
5
  # Specify your gem's dependencies in activejob-traffic_control.gemspec
4
6
  gemspec
data/README.md CHANGED
@@ -23,18 +23,15 @@ Or install it yourself as:
23
23
  `ActiveJob::TrafficControl` adds three modules you can mixin to your job classes as needed, or to `ApplicationJob` if you are using ActiveJob 5+ (or you have created a base job class yourself).
24
24
 
25
25
  ```ruby
26
- ActiveJob::TrafficControl.client = ConnectionPool.new(size: 5, timeout: 5) { Redis.new } # set thresholds as needed
27
-
28
- class CanDisableJob < ActiveJob::Base
29
- include ActiveJob::TrafficControl::Disable
26
+ # to initialize the type of locking client (memcached vs. redis):
27
+ ActiveJob::TrafficControl.client = ConnectionPool.new(size: 5, timeout: 5) { Redis.new } # set poolthresholds as needed
28
+ # or, ActiveJob::TrafficControl.client = ConnectionPool.new(size: 5, timeout: 5) { Dalli::Client.new }
29
+ # or if not multithreaded, ActiveJob::TrafficControl.client = Redis.new
30
+ ```
30
31
 
31
- def perform
32
- # you can pause this job from running by executing `CanDisableJob.disable!` (which will cause the job to be re-enqueued),
33
- # or have it be dropped entirely via `CanDisableJob.disable!(drop: true)`
34
- # enable it again via `CanDisableJob.enable!`
35
- end
36
- end
32
+ ### `Throttle`
37
33
 
34
+ ```ruby
38
35
  class CanThrottleJob < ActiveJob::Base
39
36
  include ActiveJob::TrafficControl::Throttle
40
37
 
@@ -45,7 +42,11 @@ class CanThrottleJob < ActiveJob::Base
45
42
  # if more than that attempt to run, they will be dropped (you can set `drop: false` to have the re-enqueued instead)
46
43
  end
47
44
  end
45
+ ```
48
46
 
47
+ ### `Concurrency`
48
+
49
+ ```ruby
49
50
  class ConcurrencyTestJob < ActiveJob::Base
50
51
  include ActiveJob::TrafficControl::Concurrency
51
52
 
@@ -54,6 +55,48 @@ class ConcurrencyTestJob < ActiveJob::Base
54
55
  def perform
55
56
  # only five `ConcurrencyTestJob` will ever run simultaneously
56
57
  end
58
+ end
59
+ ```
60
+
61
+ ### `Disable`
62
+
63
+ For `Disable`, you also need to configure the cache client:
64
+
65
+ ```ruby
66
+ ActiveJob::TrafficControl.cache_client = Rails.cache.dalli # if using :dalli_store
67
+ # or ActiveJob::TrafficControl.cache_client = ActiveSupport::Cache.lookup_store(:dalli_store, "localhost:11211")
68
+ ```
69
+
70
+ ```ruby
71
+ class CanDisableJob < ActiveJob::Base
72
+ include ActiveJob::TrafficControl::Disable
73
+
74
+ def perform
75
+ # you can pause this job from running by executing `CanDisableJob.disable!` (which will cause the job to be re-enqueued),
76
+ # or have it be dropped entirely via `CanDisableJob.disable!(drop: true)`
77
+ # enable it again via `CanDisableJob.enable!`
78
+ end
79
+ end
80
+ ```
81
+
82
+ ### `ApplicationJob`
83
+
84
+ To provide all of the above functionality to your jobs
85
+
86
+ ```ruby
87
+ class ApplicationJob < ActiveJob::Base
88
+ include ActiveJob::TrafficControl::Throttle
89
+ include ActiveJob::TrafficControl::Concurrency
90
+ include ActiveJob::TrafficControl::Disable
91
+
92
+ concurrency 2, drop: false
93
+ throttle threshold: 10, period: 1.minute, drop: false
94
+
95
+ def perform
96
+ # will have all of the behaviors
97
+ end
98
+ end
99
+ ```
57
100
 
58
101
  ## Development
59
102
 
data/Rakefile CHANGED
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/gem_tasks"
2
4
  require "rake/testtask"
3
5
 
4
6
  Rake::TestTask.new(:test) do |t|
5
7
  t.libs << "test"
6
8
  t.libs << "lib"
7
- t.test_files = FileList['test/**/*_test.rb']
9
+ t.test_files = FileList["test/**/*_test.rb"]
8
10
  end
9
11
 
10
- task :default => :test
12
+ task default: :test
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  # coding: utf-8
2
3
  lib = File.expand_path("../lib", __FILE__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
@@ -9,8 +10,8 @@ Gem::Specification.new do |spec|
9
10
  spec.authors = ["Nick Elser"]
10
11
  spec.email = ["nick.elser@gmail.com"]
11
12
 
12
- spec.summary = %q{Traffic control for ActiveJob}
13
- spec.description = %q{Traffic control for ActiveJob: Concurrency/enabling/throttling}
13
+ spec.summary = %q(Traffic control for ActiveJob)
14
+ spec.description = %q(Traffic control for ActiveJob: Concurrency/enabling/throttling)
14
15
  spec.homepage = "https://www.activejobtrafficcontrol.com"
15
16
 
16
17
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "active_job"
2
4
  require "active_support/all"
3
5
  require "suo"
@@ -34,7 +36,7 @@ module ActiveJob
34
36
  def client
35
37
  @client ||= begin
36
38
  logger.error "defaulting to Redis as the lock client; please set "\
37
- " `ActiveJob::TrafficControl.client` to a Redis or Memcached client,"
39
+ " `ActiveJob::TrafficControl.client` to a Redis or Memcached client."
38
40
  @client_klass = Suo::Client::Redis
39
41
  Redis.new(url: ENV["REDIS_URL"])
40
42
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "forwardable"
2
4
 
3
5
  module ActiveJob
@@ -16,6 +18,22 @@ module ActiveJob
16
18
  def cache_client
17
19
  ActiveJob::TrafficControl.cache_client
18
20
  end
21
+
22
+ def lock_key(prefix, job, config_attr)
23
+ if config_attr
24
+ if config_attr[:key].respond_to?(:call)
25
+ "traffic_control:#{prefix}:#{config_attr[:key].call(job)}"
26
+ else
27
+ @static_job_key ||= begin
28
+ if config_attr[:key].present?
29
+ "traffic_control:#{prefix}:#{config_attr[:key]}"
30
+ else
31
+ "traffic_control:#{prefix}:#{cleaned_name}"
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
19
37
  end
20
38
 
21
39
  # convenience methods
@@ -26,12 +44,12 @@ module ActiveJob
26
44
  def reenqueue(range, reason)
27
45
  later_delay = rand(range).seconds
28
46
  retry_job(wait: later_delay)
29
- logger.error "Re-enqueing #{self.class.name} to run in #{later_delay}s due to #{reason}"
47
+ logger.info "Re-enqueing #{self.class.name} to run in #{later_delay}s due to #{reason}"
30
48
  ActiveSupport::Notifications.instrument "re_enqueue.active_job", job: self, reason: reason
31
49
  end
32
50
 
33
51
  def drop(reason)
34
- logger.error "Dropping #{self.class.name} due to #{reason}"
52
+ logger.info "Dropping #{self.class.name} due to #{reason}"
35
53
  ActiveSupport::Notifications.instrument "dropped.active_job", job: self, reason: reason
36
54
  end
37
55
 
@@ -1,27 +1,36 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveJob
2
4
  module TrafficControl
3
5
  module Concurrency
4
6
  extend ::ActiveSupport::Concern
5
7
 
8
+ CONCURRENCY_REENQUEUE_DELAY = ENV["RACK_ENV"] == "test" ? 1...5 : 30...(60 * 5)
9
+
6
10
  class_methods do
7
11
  attr_accessor :job_concurrency
8
12
 
9
13
  def concurrency(threshold, drop: true, key: nil, wait_timeout: 0.1, stale_timeout: 60 * 10)
10
14
  raise ArgumentError, "Concurrent jobs needs to be an integer > 0" if threshold.to_i < 1
11
- @job_concurrency = {threshold: threshold.to_i, drop: drop, wait_timeout: wait_timeout.to_f, stale_timeout: stale_timeout.to_f, key: key}
15
+
16
+ @job_concurrency = {
17
+ threshold: threshold.to_i,
18
+ drop: drop,
19
+ wait_timeout: wait_timeout.to_f,
20
+ stale_timeout: stale_timeout.to_f,
21
+ key: key
22
+ }
12
23
  end
13
24
 
14
- def concurrency_key
15
- if job_concurrency
16
- @concurrency_key ||= job_concurrency[:key].present? ? job_concurrency[:key] : "traffic_control:concurrency:#{cleaned_name}".freeze
17
- end
25
+ def concurrency_lock_key(job)
26
+ lock_key("concurrency", job, job_concurrency)
18
27
  end
19
28
  end
20
29
 
21
30
  included do
22
31
  include ActiveJob::TrafficControl::Base
23
32
 
24
- around_perform do |_, block|
33
+ around_perform do |job, block|
25
34
  if self.class.job_concurrency.present?
26
35
  lock_options = {
27
36
  resources: self.class.job_concurrency[:threshold],
@@ -29,7 +38,7 @@ module ActiveJob
29
38
  stale_lock_expiration: self.class.job_concurrency[:stale_timeout]
30
39
  }
31
40
 
32
- with_lock_client(self.class.concurrency_key, lock_options) do |client|
41
+ with_lock_client(self.class.concurrency_lock_key(job), lock_options) do |client|
33
42
  locked = client.lock do
34
43
  block.call
35
44
  true
@@ -37,9 +46,9 @@ module ActiveJob
37
46
 
38
47
  unless locked
39
48
  if self.class.job_concurrency[:drop]
40
- drop("concurrency".freeze)
49
+ drop("concurrency")
41
50
  else
42
- reenqueue(CONCURRENCY_REENQUEUE_DELAY, "concurrency".freeze)
51
+ reenqueue(CONCURRENCY_REENQUEUE_DELAY, "concurrency")
43
52
  end
44
53
  end
45
54
  end
@@ -1,11 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveJob
2
4
  module TrafficControl
3
5
  module Disable
4
6
  extend ::ActiveSupport::Concern
5
7
 
6
8
  DISABLED_REENQUEUE_DELAY = 60...60 * 10
7
- SHOULD_DROP = "drop".freeze
8
- SHOULD_DISABLE = "true".freeze
9
+ SHOULD_DROP = "drop"
10
+ SHOULD_DISABLE = "true"
9
11
 
10
12
  private_constant :SHOULD_DROP, :SHOULD_DISABLE, :DISABLED_REENQUEUE_DELAY
11
13
 
@@ -23,7 +25,7 @@ module ActiveJob
23
25
  end
24
26
 
25
27
  def disable_key
26
- @disable_key ||= "traffic_control:disable:#{cleaned_name}".freeze
28
+ @disable_key ||= "traffic_control:disable:#{cleaned_name}"
27
29
  end
28
30
  end
29
31
 
@@ -35,9 +37,9 @@ module ActiveJob
35
37
  disabled = cache_client.read(self.class.disable_key)
36
38
 
37
39
  if disabled == SHOULD_DROP
38
- drop("disabled".freeze)
40
+ drop("disabled")
39
41
  elsif disabled == SHOULD_DISABLE
40
- reenqueue(DISABLED_REENQUEUE_DELAY, "disabled".freeze)
42
+ reenqueue(DISABLED_REENQUEUE_DELAY, "disabled")
41
43
  else
42
44
  block.call
43
45
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveJob
2
4
  module TrafficControl
3
5
  module Throttle
@@ -8,36 +10,40 @@ module ActiveJob
8
10
 
9
11
  def throttle(threshold:, period:, drop: false, key: nil)
10
12
  raise ArgumentError, "Threshold needs to be an integer > 0" if threshold.to_i < 1
11
- @job_throttling = {threshold: threshold, period: period, drop: drop, key: key}
13
+
14
+ @job_throttling = {
15
+ threshold: threshold,
16
+ period: period,
17
+ drop: drop,
18
+ key: key
19
+ }
12
20
  end
13
21
 
14
- def throttling_key
15
- if job_throttling
16
- @throttling_key ||= job_throttling[:key].present? ? job_throttling[:key] : "traffic_control:throttling:#{cleaned_name}".freeze
17
- end
22
+ def throttling_lock_key(job)
23
+ lock_key("throttle", job, job_throttling)
18
24
  end
19
25
  end
20
26
 
21
27
  included do
22
28
  include ActiveJob::TrafficControl::Base
23
29
 
24
- around_perform do |_, block|
30
+ around_perform do |job, block|
25
31
  if self.class.job_throttling.present?
26
32
  lock_options = {
27
33
  resources: self.class.job_throttling[:threshold],
28
34
  stale_lock_expiration: self.class.job_throttling[:period]
29
35
  }
30
36
 
31
- with_lock_client(self.class.throttling_key, lock_options) do |client|
37
+ with_lock_client(self.class.throttling_lock_key(job), lock_options) do |client|
32
38
  token = client.lock
33
39
 
34
40
  if token
35
41
  block.call
36
42
  elsif self.class.job_throttling[:drop]
37
- drop("throttling".freeze)
43
+ drop("throttling")
38
44
  else
39
45
  period = self.class.job_throttling[:period]
40
- reenqueue(period...period*5, "throttling".freeze)
46
+ reenqueue(period...(period * 5), "throttling")
41
47
  end
42
48
  end
43
49
  else
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveJob
2
4
  module TrafficControl
3
- VERSION = "0.1.0".freeze
5
+ VERSION = "0.1.1"
4
6
  end
5
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activejob-traffic_control
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Elser
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-06-30 00:00:00.000000000 Z
11
+ date: 2016-07-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob