activejob-traffic_control 0.1.0 → 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.
- checksums.yaml +4 -4
- data/.gitignore +18 -9
- data/.rubocop.yml +1 -0
- data/Gemfile +3 -1
- data/README.md +53 -10
- data/Rakefile +4 -2
- data/activejob-traffic_control.gemspec +3 -2
- data/lib/active_job/traffic_control.rb +3 -1
- data/lib/active_job/traffic_control/base.rb +20 -2
- data/lib/active_job/traffic_control/concurrency.rb +18 -9
- data/lib/active_job/traffic_control/disable.rb +7 -5
- data/lib/active_job/traffic_control/throttle.rb +15 -9
- data/lib/active_job/traffic_control/version.rb +3 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2906ae48cd5953f18837860c3685e3ebf8a2dd84
|
4
|
+
data.tar.gz: 5bb69b07b5439da6c1f56d5674fbfec37bd111d8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5887b615c100a481113967f5541c4e4d86621a4ce9a4ae604dcac4c524c53d0a097ec30ce9a5d88d6bd30d18fdcce4646d8731d660a190532fc496a31b19a39a
|
7
|
+
data.tar.gz: d5ccb7e7fa1c4782bcc1650a62d6f1ca17117aace77a130e2c6561714d97619faec95f86b5526516772f3ca429361ce4701e727c212585d8f896528b0de2170c
|
data/.gitignore
CHANGED
@@ -1,9 +1,18 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
data/Gemfile
CHANGED
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
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
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[
|
9
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
8
10
|
end
|
9
11
|
|
10
|
-
task :
|
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
|
13
|
-
spec.description = %q
|
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.
|
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.
|
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
|
-
|
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
|
15
|
-
|
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 |
|
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.
|
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"
|
49
|
+
drop("concurrency")
|
41
50
|
else
|
42
|
-
reenqueue(CONCURRENCY_REENQUEUE_DELAY, "concurrency"
|
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"
|
8
|
-
SHOULD_DISABLE = "true"
|
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}"
|
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"
|
40
|
+
drop("disabled")
|
39
41
|
elsif disabled == SHOULD_DISABLE
|
40
|
-
reenqueue(DISABLED_REENQUEUE_DELAY, "disabled"
|
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
|
-
|
13
|
+
|
14
|
+
@job_throttling = {
|
15
|
+
threshold: threshold,
|
16
|
+
period: period,
|
17
|
+
drop: drop,
|
18
|
+
key: key
|
19
|
+
}
|
12
20
|
end
|
13
21
|
|
14
|
-
def
|
15
|
-
|
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 |
|
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.
|
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"
|
43
|
+
drop("throttling")
|
38
44
|
else
|
39
45
|
period = self.class.job_throttling[:period]
|
40
|
-
reenqueue(period...period*5, "throttling"
|
46
|
+
reenqueue(period...(period * 5), "throttling")
|
41
47
|
end
|
42
48
|
end
|
43
49
|
else
|
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.
|
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-
|
11
|
+
date: 2016-07-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activejob
|