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.
@@ -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: []