falcon-limiter 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6f3dddcc61c0937bb6126b7a27298982a6159438479753b717fff59491ab9048
4
+ data.tar.gz: 6430a166f1ffb09e7dbdc6b0e4897abdcfc82c4b299af550aebe3a0ace1deba5
5
+ SHA512:
6
+ metadata.gz: 4313993a4aa621ed7be5449759e8e874224c1d7ad88f1708e54f8fc2b070064b8ec43cce7b43775d98295c90eb46bd273592bd592331d20cb0d3f7990cef0a4a
7
+ data.tar.gz: a4b12e91879182a082573553293615843d07f08c8aa67d4f9a2aa13af1121dda6b13cff654887e7da585d21eb10f1d7a6c06d009818e03fbcb2f6a363b9b6a87
checksums.yaml.gz.sig ADDED
Binary file
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Released under the MIT License.
5
+ # Copyright, 2025, by Samuel Williams.
6
+
7
+ require_relative "../lib/falcon/limiter"
8
+
9
+ # Create a semaphore with a limit of 3 concurrent resources
10
+ semaphore = Falcon::Limiter::Semaphore.new(3)
11
+
12
+ puts "Available resources: #{semaphore.available_count}"
13
+ puts "Acquired resources: #{semaphore.acquired_count}"
14
+
15
+ # Simulate acquiring and releasing resources
16
+ tasks = []
17
+
18
+ 10.times do |i|
19
+ tasks << Thread.new do
20
+ puts "Task #{i}: Waiting for resource..."
21
+
22
+ # Acquire a resource token
23
+ token = semaphore.acquire
24
+
25
+ puts "Task #{i}: Acquired resource (#{semaphore.available_count} remaining)"
26
+
27
+ # Simulate work
28
+ sleep(rand * 2)
29
+
30
+ # Release the resource
31
+ token.release
32
+
33
+ puts "Task #{i}: Released resource (#{semaphore.available_count} available)"
34
+ end
35
+ end
36
+
37
+ # Wait for all tasks to complete
38
+ tasks.each(&:join)
39
+
40
+ puts "Final state - Available: #{semaphore.available_count}, Acquired: #{semaphore.acquired_count}"
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Released under the MIT License.
5
+ # Copyright, 2025, by Samuel Williams.
6
+
7
+ require_relative "../lib/falcon/limiter"
8
+
9
+ # Example service module that includes the limiter environment
10
+ module MyService
11
+ include Falcon::Limiter::Environment
12
+
13
+ # Customize limiter configuration by overriding methods
14
+ def limiter_maximum_long_tasks
15
+ 8 # Override default of 4
16
+ end
17
+
18
+ def limiter_maximum_accepts
19
+ 3 # Override default of 1
20
+ end
21
+
22
+ def limiter_start_delay
23
+ 0.8 # Override default of 0.1
24
+ end
25
+
26
+ # Mock middleware method
27
+ def middleware
28
+ proc {|_env| [200, {}, ["Base middleware"]]}
29
+ end
30
+
31
+ # Mock endpoint method
32
+ def endpoint
33
+ Object.new.tap do |endpoint|
34
+ endpoint.define_singleton_method(:to_s) {"MockEndpoint"}
35
+ end
36
+ end
37
+ end
38
+
39
+ # Demonstrate the environment with proper async-service pattern
40
+ require "async/service"
41
+ environment = Async::Service::Environment.build(MyService)
42
+ service = environment.evaluator
43
+
44
+ puts "=== Falcon Limiter Environment Demo ==="
45
+ puts
46
+ puts "Configuration:"
47
+ options = service.limiter_semaphore_options
48
+ puts " Max Long Tasks: #{options[:maximum_long_tasks]}"
49
+ puts " Max Accepts: #{options[:maximum_accepts]}"
50
+ puts " Start Delay: #{options[:start_delay]}s"
51
+ puts
52
+
53
+ puts "Semaphore:"
54
+ semaphore = service.limiter_semaphore
55
+ puts " Limiter Semaphore: #{semaphore.available_count}/#{semaphore.limit} available"
56
+ puts " Limited: #{semaphore.limited?}"
57
+ puts " Waiting: #{semaphore.waiting_count}"
58
+ puts
59
+
60
+ puts "Middleware Integration:"
61
+ base_middleware = service.middleware
62
+ wrapped_middleware = service.limiter_middleware(base_middleware)
63
+ puts " Base middleware: #{base_middleware.class}"
64
+ puts " Wrapped middleware: #{wrapped_middleware.class}"
65
+ puts
66
+
67
+ puts "Endpoint Integration:"
68
+ endpoint = service.endpoint
69
+ puts " Final Endpoint: #{endpoint.class}"
70
+
71
+ # Check if it's actually wrapped (only if it's a Wrapper)
72
+ if endpoint.respond_to?(:semaphore)
73
+ puts " Semaphore: #{endpoint.semaphore.class}"
74
+ puts " Same semaphore instance: #{endpoint.semaphore == service.limiter_semaphore}"
75
+ else
76
+ puts " Note: Endpoint is not wrapped (likely mock object)"
77
+ end
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env falcon-host
2
+ # frozen_string_literal: true
3
+
4
+ # Released under the MIT License.
5
+ # Copyright, 2025, by Samuel Williams.
6
+
7
+ require_relative "../lib/falcon/limiter"
8
+
9
+ service "limiter-example.localhost" do
10
+ include Falcon::Limiter::Environment
11
+
12
+ # Configure concurrency limits by overriding methods
13
+ def limiter_max_long_tasks = 4
14
+ def limiter_max_accepts = 2
15
+ def limiter_start_delay = 0.5
16
+
17
+ scheme "http"
18
+ url "http://localhost:9292"
19
+
20
+ rack_app do
21
+ run lambda {|env|
22
+ request = env["protocol.http.request"]
23
+
24
+ case env["PATH_INFO"]
25
+ when "/fast"
26
+ # Fast response - no need for long task
27
+ [200, { "Content-Type" => "text/plain" }, ["Fast response"]]
28
+
29
+ when "/slow"
30
+ # Slow response - use long task management
31
+ Falcon::Limiter::LongTask.current&.start
32
+
33
+ # Simulate I/O operation (database query, API call, etc.)
34
+ sleep(2)
35
+
36
+ Falcon::Limiter::LongTask.current&.stop
37
+
38
+ [200, { "Content-Type" => "text/plain" }, ["Slow response completed"]]
39
+
40
+ when "/stats"
41
+ # Show limiter statistics
42
+ stats = if respond_to?(:statistics)
43
+ statistics
44
+ else
45
+ { message: "Statistics not available" }
46
+ end
47
+
48
+ [200, { "Content-Type" => "application/json" }, [stats.to_json]]
49
+
50
+ else
51
+ [404, { "Content-Type" => "text/plain" }, ["Not found"]]
52
+ end
53
+ }
54
+ end
55
+ end
@@ -0,0 +1,95 @@
1
+ # Falcon Limiter Examples
2
+
3
+ This directory contains examples demonstrating the Falcon::Limiter functionality for managing concurrent workloads.
4
+
5
+ ## Overview
6
+
7
+ The Falcon::Limiter system helps distinguish between I/O bound and CPU bound workloads:
8
+
9
+ - **I/O bound work**: Long tasks that benefit from releasing connection tokens to improve concurrency
10
+ - **CPU bound work**: Tasks that should keep connection tokens to prevent GVL contention
11
+
12
+ ## Examples
13
+
14
+ ### 1. Falcon Environment (`falcon.rb`)
15
+
16
+ Uses `Falcon::Environment::Limiter` for turn-key setup:
17
+
18
+ ```bash
19
+ falcon-host ./falcon.rb
20
+ ```
21
+
22
+ **Endpoints:**
23
+ - `/fast` - Quick response without long task
24
+ - `/slow` - I/O bound task using long task management
25
+ - `/cpu` - CPU bound task without long task
26
+ - `/stats` - Show limiter statistics
27
+
28
+ ### 2. Rack Application (`config.ru`)
29
+
30
+ Basic Rack app with manual limiter middleware:
31
+
32
+ ```bash
33
+ falcon serve -c config.ru
34
+ ```
35
+
36
+ **Endpoints:**
37
+ - `/long-io` - Long I/O operation with long task
38
+ - `/short` - Short operation (long task delay avoids overhead)
39
+ - `/token-info` - Shows connection token information
40
+ - `/` - Simple hello response
41
+
42
+ ## Key Concepts
43
+
44
+ ### Long Task Management
45
+
46
+ ```ruby
47
+ # For I/O bound operations
48
+ request.long_task&.start
49
+ external_api_call() # Long I/O operation
50
+ request.long_task&.stop # Optional - auto cleanup on response end
51
+ ```
52
+
53
+ ### Connection Token Release
54
+
55
+ Long tasks automatically:
56
+ 1. Extract and release connection tokens during I/O operations
57
+ 2. Acquire long task tokens from a separate semaphore
58
+ 3. Allow more connections to be accepted while I/O is pending
59
+ 4. Clean up automatically when response finishes
60
+
61
+ ### Configuration
62
+
63
+ ```ruby
64
+ # Environment-based
65
+ ENV["FALCON_LIMITER_MAX_LONG_TASKS"] = "8"
66
+ ENV["FALCON_LIMITER_MAX_ACCEPTS"] = "2"
67
+
68
+ # Or programmatic
69
+ Falcon::Limiter.configure do |config|
70
+ config.max_long_tasks = 8
71
+ config.max_accepts = 2
72
+ config.start_delay = 0.6
73
+ end
74
+ ```
75
+
76
+ ## Testing Load Scenarios
77
+
78
+ Test with multiple concurrent requests:
79
+
80
+ ```bash
81
+ # Test slow endpoint concurrency
82
+ curl -s "http://localhost:9292/slow" &
83
+ curl -s "http://localhost:9292/slow" &
84
+ curl -s "http://localhost:9292/slow" &
85
+
86
+ # Should still be responsive for fast requests
87
+ curl -s "http://localhost:9292/fast"
88
+ ```
89
+
90
+ ## Benefits
91
+
92
+ 1. **Better Concurrency**: I/O operations don't block connection acceptance
93
+ 2. **Graceful Degradation**: System remains responsive under high load
94
+ 3. **Resource Management**: Prevents GVL contention for CPU work
95
+ 4. **Automatic Cleanup**: Long tasks clean up automatically on response completion
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env falcon --verbose serve -c
2
+ # frozen_string_literal: true
3
+
4
+ require "falcon/limiter"
5
+
6
+ # Configure limiter globally
7
+ Falcon::Limiter.configure do |config|
8
+ config.max_long_tasks = 4
9
+ config.max_accepts = 2
10
+ config.start_delay = 0.1
11
+ end
12
+
13
+ # Basic Rack app demonstrating limiter usage
14
+ run lambda {|env|
15
+ request = env["protocol.http.request"]
16
+ path = env["PATH_INFO"]
17
+
18
+ case path
19
+ when "/long-io"
20
+ # Start long task for I/O bound work
21
+ request.long_task&.start
22
+
23
+ # Simulate database query or external API call
24
+ sleep(1.5)
25
+
26
+ [200, { "content-type" => "text/plain" }, ["Long I/O operation completed at #{Time.now}"]]
27
+
28
+ when "/short"
29
+ # Short operation - long task overhead avoided by delay
30
+ sleep(0.05)
31
+ [200, { "content-type" => "text/plain" }, ["Short operation at #{Time.now}"]]
32
+
33
+ when "/token-info"
34
+ # Show connection token information
35
+ token_info = "No connection token"
36
+
37
+ if request.respond_to?(:connection)
38
+ io = request.connection.stream.io
39
+ if io.respond_to?(:token)
40
+ token = io.token
41
+ token_info = "Token: #{token.inspect}, Released: #{token.released?}"
42
+ end
43
+ end
44
+
45
+ [200, { "content-type" => "text/plain" }, [token_info]]
46
+
47
+ else
48
+ [200, { "content-type" => "text/plain" }, ["Hello from Falcon Limiter!"]]
49
+ end
50
+ }
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env falcon-host
2
+ # frozen_string_literal: true
3
+
4
+ # Released under the MIT License.
5
+ # Copyright, 2025, by Samuel Williams.
6
+
7
+ require "falcon/limiter"
8
+
9
+ service "limiter-example.localhost" do
10
+ include Falcon::Environment::Limiter
11
+
12
+ # Configure limiter settings
13
+ limiter_configuration.max_long_tasks = 4
14
+ limiter_configuration.max_accepts = 2
15
+ limiter_configuration.start_delay = 0.1 # Shorter delay for demo
16
+
17
+ scheme "http"
18
+ url "http://localhost:9292"
19
+
20
+ rack_app do
21
+ run lambda {|env|
22
+ # Access HTTP request directly
23
+ request = env["protocol.http.request"]
24
+ path = env["PATH_INFO"]
25
+
26
+ case path
27
+ when "/fast"
28
+ # Fast request - no long task needed
29
+ [200, { "content-type" => "text/plain" }, ["Fast response: #{Time.now}"]]
30
+
31
+ when "/slow"
32
+ # Slow I/O bound request - use long task
33
+ request.long_task&.start
34
+
35
+ # Simulate I/O operation
36
+ sleep(2.0)
37
+
38
+ # Optional manual stop (auto-cleanup on response end)
39
+ request.long_task&.stop
40
+
41
+ [200, { "content-type" => "text/plain" }, ["Slow response: #{Time.now}"]]
42
+
43
+ when "/cpu"
44
+ # CPU bound request - don't use long task to prevent GVL contention
45
+ # Simulate CPU work
46
+ result = (1..1_000_000).sum
47
+
48
+ [200, { "content-type" => "text/plain" }, ["CPU result: #{result}"]]
49
+
50
+ when "/stats"
51
+ # Show limiter statistics
52
+ stats = statistics
53
+ [200, { "content-type" => "application/json" }, [stats.to_json]]
54
+
55
+ else
56
+ [404, { "content-type" => "text/plain" }, ["Not found"]]
57
+ end
58
+ }
59
+ end
60
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "falcon/limiter/long_task"
4
+
5
+ run do |env|
6
+ path = env["PATH_INFO"]
7
+
8
+ case path
9
+ when "/io"
10
+ Console.info(self, "Starting \"I/O intensive\" task...")
11
+ Falcon::Limiter::LongTask.current.start
12
+ sleep(10)
13
+ when "/cpu"
14
+ Console.info(self, "Starting \"CPU intensive\" task...")
15
+ sleep(10)
16
+ end
17
+
18
+ [200, {"content-type" => "text/plain"}, ["Hello from Falcon Limiter!"]]
19
+ end
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env falcon-host
2
+ # frozen_string_literal: true
3
+
4
+ # Released under the MIT License.
5
+ # Copyright, 2025, by Samuel Williams.
6
+
7
+ require "falcon/limiter/environment"
8
+ require "falcon/environment/rack"
9
+
10
+ service "hello.localhost" do
11
+ include Falcon::Environment::Rack
12
+ include Falcon::Limiter::Environment
13
+
14
+ endpoint do
15
+ Async::HTTP::Endpoint.parse("http://localhost:9292").with(wrapper: limiter_wrapper)
16
+ end
17
+
18
+ count {1}
19
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Josh Teeter.
5
+ # Copyright, 2025, by Samuel Williams.
6
+
7
+ require_relative "middleware"
8
+ require_relative "semaphore"
9
+ require_relative "wrapper"
10
+
11
+ module Falcon
12
+ module Limiter
13
+ # A flat environment module for falcon-limiter services.
14
+ #
15
+ # Provides simple, declarative configuration for concurrency limiting.
16
+ # Override these methods in your service to customize behavior.
17
+ module Environment
18
+ # Maximum number of concurrent long tasks (default: 4).
19
+ # If this is nil or non-positive, long task support will be disabled.
20
+ # @returns [Integer] The maximum number of concurrent long tasks.
21
+ def limiter_maximum_long_tasks
22
+ 4
23
+ end
24
+
25
+ # @returns [Integer] The maximum number of concurrent connection accepts.
26
+ def limiter_maximum_connections
27
+ 1
28
+ end
29
+
30
+ # @returns [Float] The delay before starting long task in seconds.
31
+ def limiter_start_delay
32
+ 0.1
33
+ end
34
+
35
+ # @returns [Async::Limiter::Queued] The limiter for coordinating long tasks and connection accepts.
36
+ def connection_limiter
37
+ # Create priority queue and pre-populate with tokens:
38
+ queue = Async::PriorityQueue.new
39
+ limiter_maximum_connections.times{queue.push(true)}
40
+
41
+ Async::Limiter::Queued.new(queue)
42
+ end
43
+
44
+ # @returns [Class] The middleware class to use for long task support.
45
+ def limiter_middleware_class
46
+ Middleware
47
+ end
48
+
49
+ # @returns [Protocol::HTTP::Middleware] The middleware with long task support, if enabled.
50
+ def limiter_middleware(middleware)
51
+ # Create middleware with long task support if enabled:
52
+ if limiter_maximum_long_tasks&.positive?
53
+ limiter_middleware_class.new(
54
+ middleware,
55
+ connection_limiter: connection_limiter,
56
+ maximum_long_tasks: limiter_maximum_long_tasks,
57
+ start_delay: limiter_start_delay
58
+ )
59
+ else
60
+ middleware
61
+ end
62
+ end
63
+
64
+ # @returns [Protocol::HTTP::Middleware] The middleware with long task support, if enabled.
65
+ def middleware
66
+ limiter_middleware(super)
67
+ end
68
+
69
+ def limiter_wrapper
70
+ Wrapper.new(connection_limiter)
71
+ end
72
+
73
+ # @returns [IO::Endpoint::Wrapper] The endpoint with connection limiting.
74
+ def endpoint
75
+ super.with(wrapper: limiter_wrapper)
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Marc-André Cournoyer.
5
+ # Copyright, 2025, by Francisco Mejia.
6
+ # Copyright, 2025, by Samuel Williams.
7
+
8
+ require "async/task"
9
+ require_relative "semaphore"
10
+
11
+ Fiber.attr_accessor :falcon_limiter_long_task
12
+
13
+ module Falcon
14
+ module Limiter
15
+ # Manages long-running tasks by releasing connection tokens during I/O operations to prevent contention and maintain server responsiveness.
16
+ #
17
+ # A long task is any long (1+ sec) operation that isn't CPU-bound (usually long I/O). Starting a long task lets the server accept one more (potentially CPU-bound) request. This allows us to handle many concurrent I/O bound requests, without adding contention (which impacts latency).
18
+ class LongTask
19
+ # The priority to use when stopping a long task to re-acquire the connection token.
20
+ STOP_PRIORITY = 1000
21
+
22
+ # @returns [LongTask] The current long task.
23
+ def self.current
24
+ Fiber.current.falcon_limiter_long_task
25
+ end
26
+
27
+ # Assign the current long task.
28
+ def self.current=(long_task)
29
+ Fiber.current.falcon_limiter_long_task = long_task
30
+ end
31
+
32
+ # Execute the block with the current long task.
33
+ def with
34
+ previous = self.class.current
35
+ self.class.current = self
36
+ yield
37
+ ensure
38
+ self.class.current = previous
39
+ end
40
+
41
+ # Create a long task for the given request.
42
+ # Extracts connection token from the request if available for proper token management.
43
+ # @parameter limiter [Async::Limiter] The limiter instance for managing concurrent long tasks.
44
+ # @parameter request [Object] The HTTP request object to extract connection information from.
45
+ # @parameter options [Hash] Additional options passed to the constructor.
46
+ # @returns [LongTask] A new long task instance ready for use.
47
+ def self.for(request, limiter, **options)
48
+ # Get connection token from request if possible:
49
+ connection_token = request&.connection&.stream&.io&.token rescue nil
50
+
51
+ return new(request, limiter, connection_token, **options)
52
+ end
53
+
54
+ # Initialize a new long task with the specified configuration.
55
+ # @parameter limiter [Async::Limiter] The limiter instance for controlling concurrency.
56
+ # @parameter connection_token [Async::Limiter::Token, nil] Optional connection token to manage.
57
+ # @parameter start_delay [Float] Delay in seconds before starting the long task (default: 0.1).
58
+ def initialize(request, limiter, connection_token = nil, start_delay: 0.1)
59
+ @request = request
60
+ @limiter = limiter
61
+ @connection_token = connection_token
62
+ @start_delay = start_delay
63
+
64
+ @token = Async::Limiter::Token.new(@limiter)
65
+ @delayed_start_task = nil
66
+ end
67
+
68
+ # Check if the long task has been started.
69
+ # @returns [Boolean] True if the long task token has been acquired, false otherwise.
70
+ def started?
71
+ @token.acquired? || @delayed_start_task
72
+ end
73
+
74
+ # Start the long task, optionally with a delay to avoid overhead for short operations
75
+ def start(delay: @start_delay)
76
+ # If already started, nothing to do:
77
+ if started?
78
+ if block_given?
79
+ return yield self
80
+ else
81
+ return self
82
+ end
83
+ end
84
+
85
+ if delay == true
86
+ delay = @start_delay
87
+ elsif delay == false
88
+ delay = nil
89
+ end
90
+
91
+ def acquired?
92
+ @token.acquired?
93
+ end
94
+
95
+ # Otherwise, start the long task:
96
+ if delay&.positive?
97
+ # Wait specified delay before starting the long task:
98
+ @delayed_start_task = Async do
99
+ sleep(delay)
100
+ self.acquire
101
+ rescue Async::Stop
102
+ # Gracefully exit on stop.
103
+ ensure
104
+ @delayed_start_task = nil
105
+ end
106
+ else
107
+ # Start the long task immediately:
108
+ self.acquire
109
+ end
110
+
111
+ return self unless block_given?
112
+
113
+ begin
114
+ yield self
115
+ ensure
116
+ self.stop
117
+ end
118
+ end
119
+
120
+ # Stop the long task and restore connection token
121
+ def stop(force: false, **options)
122
+ if delayed_start_task = @delayed_start_task
123
+ @delayed_start_task = nil
124
+ delayed_start_task.stop
125
+ end
126
+
127
+ # Re-acquire the connection token with high priority than inbound requests:
128
+ options[:priority] ||= STOP_PRIORITY
129
+
130
+ # Release the long task token:
131
+ release(force, **options)
132
+ end
133
+
134
+ private
135
+
136
+ # This acquires the long task token and releases the connection token if it exists.
137
+ # This marks the beginning of a long task.
138
+ # @parameter options [Hash] The options to pass to the long task token acquisition.
139
+ def acquire(**options)
140
+ return if @token.acquired?
141
+
142
+ # Wait if we've reached our limit of ongoing long tasks.
143
+ if @token.acquire(**options)
144
+ # Release the socket accept token.
145
+ @connection_token&.release
146
+
147
+ # Mark connection as non-persistent since we released the token.
148
+ make_non_persistent!
149
+ end
150
+ end
151
+
152
+ # This releases the long task token and re-acquires the connection token if it exists.
153
+ # This marks the end of a long task.
154
+ # @parameter force [Boolean] Whether to force the release of the long task token without re-acquiring the connection token.
155
+ # @parameter options [Hash] The options to pass to the connection token re-acquisition.
156
+ def release(force = false, **options)
157
+ return if @token.released?
158
+
159
+ @token.release
160
+
161
+ return if force
162
+
163
+ # Re-acquire the connection token to prevent overloading the connection limiter:
164
+ @connection_token&.acquire(**options)
165
+ end
166
+
167
+ def make_non_persistent!
168
+ # Keeping the connection alive here is problematic because if the next request is slow,
169
+ # it will "block the server" since we have relinquished the token already.
170
+ @request&.connection&.persistent = false
171
+ rescue NoMethodError
172
+ # Connection may not support persistent flag
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Josh Teeter.
5
+ # Copyright, 2025, by Samuel Williams.
6
+
7
+ require "protocol/http/middleware"
8
+ require_relative "long_task"
9
+ require_relative "semaphore"
10
+
11
+ module Falcon
12
+ module Limiter
13
+ # Protocol::HTTP middleware that provides long task management for requests.
14
+ # This allows applications to manage I/O vs CPU bound workloads effectively.
15
+ class Middleware < Protocol::HTTP::Middleware
16
+ # Initialize the middleware with limiting configuration.
17
+ # @parameter delegate [Object] The next middleware in the chain to call.
18
+ # @parameter connection_limiter [Async::Limiter] Connection limiter instance for managing accepts.
19
+ # @parameter maximum_long_tasks [Integer] Maximum number of concurrent long tasks (default: 4).
20
+ # @parameter start_delay [Float] Delay in seconds before starting long tasks (default: 0.1).
21
+ def initialize(delegate, connection_limiter:, maximum_long_tasks: 4, start_delay: 0.1)
22
+ super(delegate)
23
+
24
+ @maximum_long_tasks = maximum_long_tasks
25
+ @start_delay = start_delay
26
+ @connection_limiter = connection_limiter
27
+ @long_task_limiter = Semaphore.new(maximum_long_tasks)
28
+ end
29
+
30
+ attr_reader :maximum_long_tasks, :start_delay, :long_task_limiter, :connection_limiter
31
+
32
+ # Process an HTTP request with long task management support.
33
+ # Creates a long task context that applications can use to manage I/O operations.
34
+ # @parameter request [Object] The HTTP request to process.
35
+ # @returns [Object] The HTTP response from the downstream middleware.
36
+ def call(request)
37
+ # Create LongTask instance for this request if enabled
38
+ long_task = LongTask.for(request, @long_task_limiter, start_delay: @start_delay)
39
+
40
+ # Use scoped context for clean access
41
+ long_task.with do
42
+ response = super(request)
43
+
44
+ if long_task.started?
45
+ Protocol::HTTP::Body::Completable.wrap(response) do
46
+ long_task.stop(force: true)
47
+ end
48
+ end
49
+
50
+ response
51
+ end
52
+ end
53
+
54
+ # Get semaphore statistics
55
+ def statistics
56
+ {
57
+ long_task_limiter: @long_task_limiter.statistics,
58
+ connection_limiter: @connection_limiter.statistics
59
+ }
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Josh Teeter.
5
+ # Copyright, 2025, by Samuel Williams.
6
+
7
+ require "async/limiter"
8
+ require "async/limiter/token"
9
+ require "async/priority_queue"
10
+
11
+ module Falcon
12
+ module Limiter
13
+ # Simple wrapper around Async::Limiter::Queued that provides the interface
14
+ # expected by Falcon while leveraging async-limiter's implementation.
15
+ module Semaphore
16
+ # Create a new limiter with the specified capacity.
17
+ # @parameter limit [Integer] The maximum number of concurrent operations allowed (default: 1).
18
+ # @returns [Async::Limiter::Queued] A new limiter instance with pre-allocated tokens.
19
+ def self.new(limit = 1)
20
+ # Create priority queue and pre-populate with tokens
21
+ queue = Async::PriorityQueue.new
22
+ limit.times{queue.push(true)}
23
+
24
+ return Async::Limiter::Queued.new(queue)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ module Falcon
7
+ module Limiter
8
+ # A transparent socket wrapper that automatically manages token release.
9
+ # Behaves exactly like the wrapped socket but releases the limiter token on close.
10
+ class Socket < BasicObject
11
+ # Initialize the socket wrapper with delegation and token management.
12
+ # @parameter delegate [Object] The socket object to wrap and delegate to.
13
+ # @parameter token [Async::Limiter::Token] The limiter token to release when socket closes.
14
+ def initialize(delegate, token)
15
+ @delegate = delegate
16
+ @token = token
17
+ end
18
+
19
+ # Provide access to the token for manual management if needed.
20
+ attr_reader :token
21
+
22
+ # Override close to release the token.
23
+ def close
24
+ @delegate.close
25
+ ensure
26
+ if token = @token
27
+ @token = nil
28
+ token.release
29
+ end
30
+ end
31
+
32
+ # Transparent delegation to the wrapped delegate.
33
+ def method_missing(...)
34
+ @delegate.public_send(...)
35
+ end
36
+
37
+ # Check if this wrapper or the delegate responds to a method.
38
+ # @parameter method [Symbol] The method name to check.
39
+ # @parameter include_private [Boolean] Whether to include private methods (default: false).
40
+ # @returns [Boolean] True if either wrapper or delegate responds to the method.
41
+ def respond_to?(method, include_private = false)
42
+ # Check our own methods first (token, close, inspect, to_s, etc.)
43
+ case method.to_sym
44
+ when :token
45
+ true
46
+ else
47
+ # Check delegate for other methods
48
+ @delegate.respond_to?(method, include_private)
49
+ end
50
+ end
51
+
52
+ # Support for method_missing delegation by checking delegate's methods.
53
+ # @parameter method [Symbol] The method name to check.
54
+ # @parameter include_private [Boolean] Whether to include private methods (default: false).
55
+ # @returns [Boolean] True if the delegate responds to the method.
56
+ def respond_to_missing?(method, include_private = false)
57
+ @delegate.respond_to?(method, include_private) || super
58
+ end
59
+
60
+ # Forward common inspection methods
61
+ def inspect
62
+ "#<#{self.class}(#{@delegate.class}) #{@delegate.inspect}>"
63
+ end
64
+
65
+ # String representation of the wrapped socket.
66
+ # @returns [String] The string representation of the delegate socket.
67
+ def to_s
68
+ @delegate.to_s
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ module Falcon
7
+ module Limiter
8
+ VERSION = "0.1.0"
9
+ end
10
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require "io/endpoint/wrapper"
7
+ require "async/limiter/token"
8
+ require_relative "socket"
9
+
10
+ module Falcon
11
+ module Limiter
12
+ # An endpoint wrapper that limits concurrent connections using a semaphore.
13
+ # This provides backpressure by limiting how many connections can be accepted simultaneously.
14
+ class Wrapper < IO::Endpoint::Wrapper
15
+ # Initialize the wrapper with a connection limiter.
16
+ # @parameter limiter [Async::Limiter] The limiter instance for controlling concurrent connections.
17
+ def initialize(limiter)
18
+ super()
19
+ @limiter = limiter
20
+ end
21
+
22
+ attr_reader :limiter
23
+
24
+ # Wait for an inbound connection to be ready to be accepted.
25
+ def wait_for_inbound_connection(server)
26
+ loop do
27
+ # Wait until there is a connection ready to be accepted:
28
+ server.wait_readable
29
+
30
+ # Acquire the limiter:
31
+ if token = Async::Limiter::Token.acquire(@limiter)
32
+ return token
33
+ end
34
+ end
35
+ end
36
+
37
+ # Once the server is readable and we've acquired the token, we can accept the connection (if it's still there).
38
+ def socket_accept_nonblock(server, token)
39
+ socket = server.accept_nonblock
40
+ rescue IO::WaitReadable
41
+ nil
42
+ ensure
43
+ token.release unless socket
44
+ end
45
+
46
+ def wrap_socket(socket, token)
47
+ Socket.new(socket, token)
48
+ end
49
+
50
+ # Accept a connection from the server, limited by the per-worker (thread or process) semaphore.
51
+ def socket_accept(server)
52
+ socket = nil
53
+ address = nil
54
+ token = nil
55
+
56
+ loop do
57
+ next unless token = wait_for_inbound_connection(server)
58
+
59
+ # In principle, there is a connection ready to be accepted:
60
+ socket, address = socket_accept_nonblock(server, token)
61
+
62
+ break if socket
63
+ end
64
+
65
+ # Wrap socket with transparent token management
66
+ return wrap_socket(socket, token), address
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require_relative "limiter/semaphore"
7
+ require_relative "limiter/socket"
8
+ require_relative "limiter/wrapper"
9
+ require_relative "limiter/long_task"
10
+ require_relative "limiter/middleware"
11
+ require_relative "limiter/environment"
12
+
13
+ # @namespace
14
+ module Falcon
15
+ # @namespace
16
+ module Limiter
17
+ end
18
+ end
data/license.md ADDED
@@ -0,0 +1,24 @@
1
+ # MIT License
2
+
3
+ Copyright, 2025, by Marc-André Cournoyer.
4
+ Copyright, 2025, by Francisco Mejia.
5
+ Copyright, 2025, by Josh Teeter.
6
+ Copyright, 2025, by Samuel Williams.
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
data/readme.md ADDED
@@ -0,0 +1,160 @@
1
+ # Falcon Limiter
2
+
3
+ Advanced concurrency control and resource limiting for Falcon web server, built on top of [async-limiter](https://github.com/socketry/async-limiter).
4
+
5
+ [![Development Status](https://github.com/socketry/falcon-limiter/workflows/Test/badge.svg)](https://github.com/socketry/falcon-limiter/actions?workflow=Test)
6
+
7
+ ## Features
8
+
9
+ This gem provides sophisticated concurrency management for Falcon applications by:
10
+
11
+ - **Connection Limiting**: Control the number of concurrent connections to prevent server overload
12
+ - **Long Task Management**: Handle I/O vs CPU bound workloads effectively by releasing resources during long operations
13
+ - **Priority-based Resource Allocation**: Higher priority tasks get preferential access to limited resources
14
+ - **Automatic Resource Cleanup**: Ensures proper resource release even when exceptions occur
15
+ - **Built-in Statistics**: Monitor resource utilization and contention
16
+
17
+ ## Installation
18
+
19
+ Add this line to your application's Gemfile:
20
+
21
+ ``` ruby
22
+ gem 'falcon-limiter'
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ Please see the [project documentation](https://socketry.github.io/falcon-limiter/) for more details.
28
+
29
+ ### Basic Falcon Environment Integration
30
+
31
+ ``` ruby
32
+ #!/usr/bin/env falcon-host
33
+
34
+ require "falcon-limiter"
35
+
36
+ service "myapp.localhost" do
37
+ include Falcon::Environment::Limiter
38
+
39
+ # Configure concurrency limits
40
+ limiter_configuration.max_long_tasks = 8
41
+ limiter_configuration.max_accepts = 2
42
+
43
+ scheme "http"
44
+ url "http://localhost:9292"
45
+
46
+ rack_app do
47
+ run lambda { |env|
48
+ # Start long task for I/O bound work
49
+ Falcon::Limiter::LongTask.current&.start
50
+
51
+ # Long I/O operation (database query, external API call, etc.)
52
+ external_api_call
53
+
54
+ # Optional manual stop (auto-cleanup on response end)
55
+ Falcon::Limiter::LongTask.current&.stop
56
+
57
+ [200, {}, ["OK"]]
58
+ }
59
+ end
60
+ end
61
+ ```
62
+
63
+ ### Manual Middleware Setup
64
+
65
+ ``` ruby
66
+ require "falcon-limiter"
67
+ require "protocol/http/middleware"
68
+
69
+ # Configure middleware stack
70
+ middleware = Protocol::HTTP::Middleware.build do
71
+ use Falcon::Limiter::Middleware,
72
+ max_long_tasks: 4,
73
+ max_accepts: 2
74
+ use Protocol::Rack::Adapter
75
+ run rack_app
76
+ end
77
+ ```
78
+
79
+ ### Direct Semaphore Usage
80
+
81
+ ``` ruby
82
+ require "falcon-limiter"
83
+
84
+ # Create a semaphore for database connections
85
+ db_semaphore = Falcon::Limiter::Semaphore.new(5)
86
+
87
+ # Acquire a connection
88
+ token = db_semaphore.acquire
89
+
90
+ begin
91
+ # Use database connection
92
+ database_operation
93
+ ensure
94
+ # Release the connection
95
+ token.release
96
+ end
97
+ ```
98
+
99
+ ## Configuration
100
+
101
+ Configure limits using environment variables or programmatically:
102
+
103
+ ``` bash
104
+ export FALCON_LIMITER_MAX_LONG_TASKS=8
105
+ export FALCON_LIMITER_MAX_ACCEPTS=2
106
+ export FALCON_LIMITER_START_DELAY=0.6
107
+ ```
108
+
109
+ Or in code:
110
+
111
+ ``` ruby
112
+ Falcon::Limiter.configure do |config|
113
+ config.max_long_tasks = 8
114
+ config.max_accepts = 2
115
+ config.start_delay = 0.6
116
+ end
117
+ ```
118
+
119
+ ## Architecture
120
+
121
+ Falcon Limiter is built on top of [async-limiter](https://github.com/socketry/async-limiter), providing:
122
+
123
+ - **Thread-safe resource management** using priority queues
124
+ - **Integration with Falcon's HTTP pipeline** through Protocol::HTTP::Middleware
125
+ - **Automatic connection token management** for optimal resource utilization
126
+ - **Priority-based task scheduling** to prevent resource starvation
127
+
128
+ ## Development
129
+
130
+ After checking out the repo, run `bundle install` to install dependencies. Then, run `bundle exec sus` to run the tests.
131
+
132
+ ## Contributing
133
+
134
+ We welcome contributions to this project.
135
+
136
+ 1. Fork it.
137
+ 2. Create your feature branch (`git checkout -b my-new-feature`).
138
+ 3. Commit your changes (`git commit -am 'Add some feature'`).
139
+ 4. Push to the branch (`git push origin my-new-feature`).
140
+ 5. Create new Pull Request.
141
+
142
+ ### Developer Certificate of Origin
143
+
144
+ In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
145
+
146
+ ### Community Guidelines
147
+
148
+ This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers.
149
+
150
+ ## Releases
151
+
152
+ Please see the [project releases](https://socketry.github.io/falcon-limiter/releases/index) for all releases.
153
+
154
+ ### v0.1.0
155
+
156
+ ## See Also
157
+
158
+ - [falcon](https://github.com/socketry/falcon) - A fast, asynchronous, rack-compatible web server.
159
+ - [async-limiter](https://github.com/socketry/async-limiter) - Execution rate limiting for Async.
160
+ - [async](https://github.com/socketry/async) - A concurrency framework for Ruby.
data/releases.md ADDED
@@ -0,0 +1,3 @@
1
+ # Releases
2
+
3
+ ## v0.1.0
data.tar.gz.sig ADDED
Binary file
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: falcon-limiter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Josh Teeter
8
+ - Samuel Williams
9
+ - Francisco Mejia
10
+ - Marc-André Cournoyer
11
+ bindir: bin
12
+ cert_chain:
13
+ - |
14
+ -----BEGIN CERTIFICATE-----
15
+ MIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11
16
+ ZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK
17
+ CZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz
18
+ MjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd
19
+ MBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj
20
+ bzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB
21
+ igKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2
22
+ 9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW
23
+ sGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE
24
+ e5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN
25
+ XibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss
26
+ RZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn
27
+ tUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM
28
+ zp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW
29
+ xm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O
30
+ BBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs
31
+ aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs
32
+ aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE
33
+ cBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl
34
+ xCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/
35
+ c1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp
36
+ 8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws
37
+ JkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP
38
+ eX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt
39
+ Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
40
+ voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
41
+ -----END CERTIFICATE-----
42
+ date: 1980-01-02 00:00:00.000000000 Z
43
+ dependencies:
44
+ - !ruby/object:Gem::Dependency
45
+ name: async-limiter
46
+ requirement: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - "~>"
49
+ - !ruby/object:Gem::Version
50
+ version: '2.0'
51
+ type: :runtime
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - "~>"
56
+ - !ruby/object:Gem::Version
57
+ version: '2.0'
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - examples/basic_limiting.rb
63
+ - examples/environment_usage.rb
64
+ - examples/falcon_environment.rb
65
+ - examples/limiter/README.md
66
+ - examples/limiter/config.ru
67
+ - examples/limiter/falcon.rb
68
+ - examples/load/config.ru
69
+ - examples/load/falcon.rb
70
+ - lib/falcon/limiter.rb
71
+ - lib/falcon/limiter/environment.rb
72
+ - lib/falcon/limiter/long_task.rb
73
+ - lib/falcon/limiter/middleware.rb
74
+ - lib/falcon/limiter/semaphore.rb
75
+ - lib/falcon/limiter/socket.rb
76
+ - lib/falcon/limiter/version.rb
77
+ - lib/falcon/limiter/wrapper.rb
78
+ - license.md
79
+ - readme.md
80
+ - releases.md
81
+ homepage: https://github.com/socketry/falcon-limiter
82
+ licenses:
83
+ - MIT
84
+ metadata:
85
+ documentation_uri: https://socketry.github.io/falcon-limiter/
86
+ source_code_uri: https://github.com/socketry/falcon-limiter.git
87
+ rdoc_options: []
88
+ require_paths:
89
+ - lib
90
+ required_ruby_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '3.2'
95
+ required_rubygems_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ requirements: []
101
+ rubygems_version: 3.6.9
102
+ specification_version: 4
103
+ summary: Advanced concurrency control and resource limiting for Falcon web server.
104
+ test_files: []
metadata.gz.sig ADDED
@@ -0,0 +1,3 @@
1
+ ^Ù/�Sg� Pلz�ɺ��G���oĺ��{N�m}|0���<�O�
2
+ ��˾G�ϙDL��O����
3
+ +�T���Q:��7X��qrYnB���