async-limiter 0.0.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: 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,10 @@
1
+ require_relative "../limiter"
2
+
3
+ module Async
4
+ class Limiter
5
+ # Allows running x units of work concurrently.
6
+ # Has the same logic as Async::Semaphore.
7
+ class Concurrent < Limiter
8
+ end
9
+ end
10
+ 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
@@ -0,0 +1,5 @@
1
+ module Async
2
+ class Limiter
3
+ VERSION = "0.0.1"
4
+ end
5
+ 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: []