async-limiter 0.0.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 +7 -0
- data/lib/async/limiter.rb +114 -0
- data/lib/async/limiter/concurrent.rb +10 -0
- data/lib/async/limiter/delay.rb +47 -0
- data/lib/async/limiter/fixed_window.rb +53 -0
- data/lib/async/limiter/sliding_window.rb +53 -0
- data/lib/async/limiter/version.rb +5 -0
- metadata +76 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: c1e4bf6b1b1477475fc5bf247364592ebd062e86cd5039614a9963cfdc9e23ab
|
4
|
+
data.tar.gz: 82f5fc81a61d6d8cc0400d86b4a84977379f3edc8f6951bba738e2215e1e3445
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8c2698448ec6846405dc647c0e11b37616fbd80027b92a03e28a849d511164835909d4982ddbe6cfff7e963b00bedc6699dd90dbc33067f52969480170bc1c4a
|
7
|
+
data.tar.gz: 62e777daf244f5cfe6b9ef89cdcdd8b07791808324713db58a7242825cf9d3d82f4978c920639bbc603dae625fbe819a41fdb7b95897d3f619d8d80eb9195794
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require "async/task"
|
2
|
+
|
3
|
+
module Async
|
4
|
+
# Base class for all the limiters.
|
5
|
+
class Limiter
|
6
|
+
Error = Class.new(StandardError)
|
7
|
+
ArgumentError = Class.new(Error)
|
8
|
+
|
9
|
+
MAX_LIMIT = Float::INFINITY
|
10
|
+
MIN_LIMIT = Float::MIN
|
11
|
+
|
12
|
+
attr_reader :count
|
13
|
+
|
14
|
+
attr_reader :limit
|
15
|
+
|
16
|
+
attr_reader :waiting
|
17
|
+
|
18
|
+
def initialize(limit = 1, parent: nil,
|
19
|
+
max_limit: MAX_LIMIT, min_limit: MIN_LIMIT)
|
20
|
+
@count = 0
|
21
|
+
@limit = limit
|
22
|
+
@waiting = []
|
23
|
+
@parent = parent
|
24
|
+
@max_limit = max_limit
|
25
|
+
@min_limit = min_limit
|
26
|
+
|
27
|
+
validate!
|
28
|
+
end
|
29
|
+
|
30
|
+
def blocking?
|
31
|
+
@count >= @limit
|
32
|
+
end
|
33
|
+
|
34
|
+
def async(parent: (@parent or Task.current), **options)
|
35
|
+
acquire
|
36
|
+
parent.async(**options) do |task|
|
37
|
+
yield task
|
38
|
+
ensure
|
39
|
+
release
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def acquire
|
44
|
+
wait
|
45
|
+
@count += 1
|
46
|
+
end
|
47
|
+
|
48
|
+
def release
|
49
|
+
@count -= 1
|
50
|
+
|
51
|
+
while under_limit? && (fiber = @waiting.shift)
|
52
|
+
fiber.resume if fiber.alive?
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def increase_limit(number = 1)
|
57
|
+
new_limit = @limit + number
|
58
|
+
return false if new_limit > @max_limit
|
59
|
+
|
60
|
+
@limit = new_limit
|
61
|
+
end
|
62
|
+
|
63
|
+
def decrease_limit(number = 1)
|
64
|
+
new_limit = @limit - number
|
65
|
+
return false if new_limit < @min_limit
|
66
|
+
|
67
|
+
@limit = new_limit
|
68
|
+
end
|
69
|
+
|
70
|
+
def waiting_count
|
71
|
+
@waiting.size
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def under_limit?
|
77
|
+
available_units.positive?
|
78
|
+
end
|
79
|
+
|
80
|
+
def available_units
|
81
|
+
@limit - @count
|
82
|
+
end
|
83
|
+
|
84
|
+
def wait
|
85
|
+
fiber = Fiber.current
|
86
|
+
|
87
|
+
if blocking?
|
88
|
+
@waiting << fiber
|
89
|
+
Task.yield while blocking?
|
90
|
+
end
|
91
|
+
rescue Exception
|
92
|
+
@waiting.delete(fiber)
|
93
|
+
raise
|
94
|
+
end
|
95
|
+
|
96
|
+
def validate!
|
97
|
+
if @max_limit < @min_limit
|
98
|
+
raise ArgumentError, "max_limit is lower than min_limit"
|
99
|
+
end
|
100
|
+
|
101
|
+
unless @max_limit.positive?
|
102
|
+
raise ArgumentError, "max_limit must be positive"
|
103
|
+
end
|
104
|
+
|
105
|
+
unless @min_limit.positive?
|
106
|
+
raise ArgumentError, "min_limit must be positive"
|
107
|
+
end
|
108
|
+
|
109
|
+
unless @limit.between?(@min_limit, @max_limit)
|
110
|
+
raise ArgumentError, "limit not between min_limit and max_limit"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require "async/clock"
|
2
|
+
require_relative "../limiter"
|
3
|
+
|
4
|
+
module Async
|
5
|
+
class Limiter
|
6
|
+
# Ensures units are evenly acquired during the sliding time window.
|
7
|
+
# Example: If limit is 2 you can perform one operation every 500ms. First
|
8
|
+
# operation at 10:10:10.000, and then another one at 10:10:10.500.
|
9
|
+
class Throttle < Limiter
|
10
|
+
attr_reader :window
|
11
|
+
|
12
|
+
def initialize(*args, window: 1, min_limit: 0, **options)
|
13
|
+
super(*args, min_limit: min_limit, **options)
|
14
|
+
|
15
|
+
@window = window
|
16
|
+
@last_acquired_time = -1
|
17
|
+
end
|
18
|
+
|
19
|
+
def blocking?
|
20
|
+
super && current_delay.positive?
|
21
|
+
end
|
22
|
+
|
23
|
+
def acquire
|
24
|
+
super
|
25
|
+
@last_acquired_time = now
|
26
|
+
end
|
27
|
+
|
28
|
+
def delay
|
29
|
+
@window.to_f / @limit
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def now
|
35
|
+
Clock.now
|
36
|
+
end
|
37
|
+
|
38
|
+
def current_delay
|
39
|
+
[delay - elapsed_time, 0].max
|
40
|
+
end
|
41
|
+
|
42
|
+
def elapsed_time
|
43
|
+
now - @last_acquired_time
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require "async/clock"
|
2
|
+
require_relative "../limiter"
|
3
|
+
|
4
|
+
module Async
|
5
|
+
class Limiter
|
6
|
+
# Ensures units are acquired during the time window.
|
7
|
+
# Example: You can perform N operations at 10:10:10.999, and then can
|
8
|
+
# perform another N operations at 10:10:11.000.
|
9
|
+
class FixedWindow < Limiter
|
10
|
+
attr_reader :window
|
11
|
+
|
12
|
+
def initialize(*args, window: 1, **options)
|
13
|
+
super(*args, **options)
|
14
|
+
|
15
|
+
@window = window
|
16
|
+
@acquired_window_indexes = []
|
17
|
+
end
|
18
|
+
|
19
|
+
def blocking?
|
20
|
+
super && window_limited?
|
21
|
+
end
|
22
|
+
|
23
|
+
def acquire
|
24
|
+
super
|
25
|
+
@acquired_window_indexes.unshift(window_index)
|
26
|
+
# keep more entries in case a limit is increased
|
27
|
+
@acquired_window_indexes = @acquired_window_indexes.first(keep_limit)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def window_limited?
|
33
|
+
first_index_in_limit_scope == window_index
|
34
|
+
end
|
35
|
+
|
36
|
+
def first_index_in_limit_scope
|
37
|
+
@acquired_window_indexes.fetch(@limit - 1) { -1 }
|
38
|
+
end
|
39
|
+
|
40
|
+
def window_index
|
41
|
+
(now / @window).floor
|
42
|
+
end
|
43
|
+
|
44
|
+
def keep_limit
|
45
|
+
@max_limit.infinite? ? @limit * 10 : @max_limit
|
46
|
+
end
|
47
|
+
|
48
|
+
def now
|
49
|
+
Clock.now
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require "async/clock"
|
2
|
+
require_relative "../limiter"
|
3
|
+
|
4
|
+
module Async
|
5
|
+
class Limiter
|
6
|
+
# Ensures units are acquired during the sliding time window.
|
7
|
+
# Example: You can perform N operations at 10:10:10.999 but can't perform
|
8
|
+
# another N operations until 10:10:11.999.
|
9
|
+
class SlidingWindow < Limiter
|
10
|
+
attr_reader :window
|
11
|
+
|
12
|
+
def initialize(*args, window: 1, min_limit: 0, **options)
|
13
|
+
super(*args, min_limit: min_limit, **options)
|
14
|
+
|
15
|
+
@window = window
|
16
|
+
@acquired_times = []
|
17
|
+
end
|
18
|
+
|
19
|
+
def blocking?
|
20
|
+
super && window_limited?
|
21
|
+
end
|
22
|
+
|
23
|
+
def acquire
|
24
|
+
super
|
25
|
+
@acquired_times.unshift(now)
|
26
|
+
# keep more entries in case a limit is increased
|
27
|
+
@acquired_times = @acquired_times.first(keep_limit)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def window_limited?
|
33
|
+
first_time_in_limit_scope >= window_start_time
|
34
|
+
end
|
35
|
+
|
36
|
+
def first_time_in_limit_scope
|
37
|
+
@acquired_times.fetch(@limit - 1) { -1 }
|
38
|
+
end
|
39
|
+
|
40
|
+
def window_start_time
|
41
|
+
now - @window
|
42
|
+
end
|
43
|
+
|
44
|
+
def keep_limit
|
45
|
+
@max_limit.infinite? ? @limit * 10 : @max_limit
|
46
|
+
end
|
47
|
+
|
48
|
+
def now
|
49
|
+
Clock.now
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
metadata
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: async-limiter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Bruno Sutic
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-10-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: async
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.26'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.26'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: standard
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.7'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.7'
|
41
|
+
description:
|
42
|
+
email: code@brunosutic.com
|
43
|
+
executables: []
|
44
|
+
extensions: []
|
45
|
+
extra_rdoc_files: []
|
46
|
+
files:
|
47
|
+
- lib/async/limiter.rb
|
48
|
+
- lib/async/limiter/concurrent.rb
|
49
|
+
- lib/async/limiter/delay.rb
|
50
|
+
- lib/async/limiter/fixed_window.rb
|
51
|
+
- lib/async/limiter/sliding_window.rb
|
52
|
+
- lib/async/limiter/version.rb
|
53
|
+
homepage: https://github.com/bruno-/async-limiter
|
54
|
+
licenses:
|
55
|
+
- MIT
|
56
|
+
metadata: {}
|
57
|
+
post_install_message:
|
58
|
+
rdoc_options: []
|
59
|
+
require_paths:
|
60
|
+
- lib
|
61
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: 2.7.0
|
66
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
requirements: []
|
72
|
+
rubygems_version: 3.1.2
|
73
|
+
signing_key:
|
74
|
+
specification_version: 4
|
75
|
+
summary: Async limiters
|
76
|
+
test_files: []
|