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 +7 -0
- checksums.yaml.gz.sig +0 -0
- data/examples/basic_limiting.rb +40 -0
- data/examples/environment_usage.rb +77 -0
- data/examples/falcon_environment.rb +55 -0
- data/examples/limiter/README.md +95 -0
- data/examples/limiter/config.ru +50 -0
- data/examples/limiter/falcon.rb +60 -0
- data/examples/load/config.ru +19 -0
- data/examples/load/falcon.rb +19 -0
- data/lib/falcon/limiter/environment.rb +79 -0
- data/lib/falcon/limiter/long_task.rb +176 -0
- data/lib/falcon/limiter/middleware.rb +63 -0
- data/lib/falcon/limiter/semaphore.rb +28 -0
- data/lib/falcon/limiter/socket.rb +72 -0
- data/lib/falcon/limiter/version.rb +10 -0
- data/lib/falcon/limiter/wrapper.rb +70 -0
- data/lib/falcon/limiter.rb +18 -0
- data/license.md +24 -0
- data/readme.md +160 -0
- data/releases.md +3 -0
- data.tar.gz.sig +0 -0
- metadata +104 -0
- metadata.gz.sig +3 -0
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,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
|
+
[](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
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