falcon-limiter 0.1.0 → 0.1.2
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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/context/getting-started.md +116 -0
- data/context/index.yaml +16 -0
- data/context/long-tasks.md +160 -0
- data/lib/falcon/limiter/environment.rb +3 -2
- data/lib/falcon/limiter/long_task.rb +8 -6
- data/lib/falcon/limiter/middleware.rb +2 -2
- data/lib/falcon/limiter/socket.rb +1 -9
- data/lib/falcon/limiter/version.rb +1 -1
- data/lib/falcon/limiter/wrapper.rb +4 -0
- data/readme.md +21 -126
- data/releases.md +2 -0
- data.tar.gz.sig +0 -0
- metadata +4 -9
- metadata.gz.sig +0 -0
- data/examples/basic_limiting.rb +0 -40
- data/examples/environment_usage.rb +0 -77
- data/examples/falcon_environment.rb +0 -55
- data/examples/limiter/README.md +0 -95
- data/examples/limiter/config.ru +0 -50
- data/examples/limiter/falcon.rb +0 -60
- data/examples/load/config.ru +0 -19
- data/examples/load/falcon.rb +0 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 19aed9c67b7fe7b68716eb8b4db0ffaa0424733a2e3e18abda65e367293fda17
|
4
|
+
data.tar.gz: 1eabdea4a1110e92a86a5d30bc1dba115cc10f7d33fb8ef545d3f5f6806eaac0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: be434c0ba702339891fbf77bf78ce76610e5b833abf304ac7c65913aceac28a04a0ac4560d75e29c5866d8fa13598482b62aa949d9bef6d326eb62b2ccfcd1f7
|
7
|
+
data.tar.gz: 51344952a67ae841d4f3284fe084178ba6af764e482c7f16d730c5d22a3ffc64560af6010602c416427c8f42706e822b3228073ee84714f7508c07b787f2da18
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# Getting Started
|
2
|
+
|
3
|
+
This guide explains how to get started with `falcon-limiter` for advanced concurrency control and resource limiting in Falcon web applications.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add the gem to your project:
|
8
|
+
|
9
|
+
```bash
|
10
|
+
$ bundle add falcon-limiter
|
11
|
+
```
|
12
|
+
|
13
|
+
## Core Concepts
|
14
|
+
|
15
|
+
`falcon-limiter` has one main concept:
|
16
|
+
|
17
|
+
- {ruby Falcon::Limiter::LongTask} represents operations that take significant time but aren't CPU-intensive (like database queries or API calls).
|
18
|
+
|
19
|
+
When you start a Long Task, the server:
|
20
|
+
|
21
|
+
- Releases the connection token so new requests can be accepted.
|
22
|
+
- Continues processing your I/O operation in the background.
|
23
|
+
- Prevents too many I/O operations from running simultaneously.
|
24
|
+
- Maintains responsiveness for CPU-bound requests.
|
25
|
+
|
26
|
+
This means your server can handle many concurrent I/O operations without blocking quick CPU-bound requests.
|
27
|
+
|
28
|
+
## Usage
|
29
|
+
|
30
|
+
The easiest way to get started is using the {ruby Falcon::Limiter::Environment} module:
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
#!/usr/bin/env falcon-host
|
34
|
+
# frozen_string_literal: true
|
35
|
+
|
36
|
+
require "falcon/limiter/environment"
|
37
|
+
require "falcon/environment/rack"
|
38
|
+
|
39
|
+
service "myapp.localhost" do
|
40
|
+
include Falcon::Environment::Rack
|
41
|
+
include Falcon::Limiter::Environment
|
42
|
+
|
43
|
+
# If you use a custom endpoint, you need to use it with the limiter wrapper:
|
44
|
+
endpoint do
|
45
|
+
Async::HTTP::Endpoint.parse("http://localhost:9292").with(wrapper: limiter_wrapper)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
```
|
49
|
+
|
50
|
+
Then in your Rack application:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
# config.ru
|
54
|
+
require "falcon/limiter/long_task"
|
55
|
+
|
56
|
+
run do |env|
|
57
|
+
path = env["PATH_INFO"]
|
58
|
+
|
59
|
+
case path
|
60
|
+
when "/io"
|
61
|
+
# For I/O bound work, start a long task to release the connection token:
|
62
|
+
Falcon::Limiter::LongTask.current.start
|
63
|
+
|
64
|
+
# Long I/O operation (database query, external API call, etc.).
|
65
|
+
sleep(5) # Simulating I/O.
|
66
|
+
|
67
|
+
when "/cpu"
|
68
|
+
# For CPU bound work, keep the connection token.
|
69
|
+
# This ensures only limited CPU work happens concurrently.
|
70
|
+
sleep(5) # Simulating CPU work.
|
71
|
+
end
|
72
|
+
|
73
|
+
[200, {"content-type" => "text/plain"}, ["Request completed"]]
|
74
|
+
end
|
75
|
+
```
|
76
|
+
|
77
|
+
### Understanding the Behavior
|
78
|
+
|
79
|
+
With the default configuration:
|
80
|
+
|
81
|
+
- **Connection limit**: 1 (only 1 connection accepted at a time)
|
82
|
+
- **Long task limit**: 10 (up to 10 concurrent I/O operations)
|
83
|
+
|
84
|
+
This means:
|
85
|
+
|
86
|
+
1. **CPU-bound requests** (`/cpu`) will be processed sequentially - only one at a time
|
87
|
+
2. **I/O-bound requests** (`/io`) can run concurrently (up to 10) because they release their connection token
|
88
|
+
3. **Mixed workloads** work optimally - I/O requests don't block CPU requests from being accepted
|
89
|
+
|
90
|
+
### Configuration
|
91
|
+
|
92
|
+
You can customize the limits by overriding the environment methods:
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
service "myapp.localhost" do
|
96
|
+
include Falcon::Environment::Rack
|
97
|
+
include Falcon::Limiter::Environment
|
98
|
+
|
99
|
+
# Override default limits
|
100
|
+
def limiter_maximum_connections
|
101
|
+
2 # Allow 2 concurrent connections
|
102
|
+
end
|
103
|
+
|
104
|
+
def limiter_maximum_long_tasks
|
105
|
+
8 # Allow 8 concurrent I/O operations
|
106
|
+
end
|
107
|
+
|
108
|
+
def limiter_start_delay
|
109
|
+
0.05 # Reduce delay before starting long tasks
|
110
|
+
end
|
111
|
+
|
112
|
+
endpoint do
|
113
|
+
Async::HTTP::Endpoint.parse("http://localhost:9292").with(wrapper: limiter_wrapper)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
```
|
data/context/index.yaml
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# Automatically generated context index for Utopia::Project guides.
|
2
|
+
# Do not edit then files in this directory directly, instead edit the guides and then run `bake utopia:project:agent:context:update`.
|
3
|
+
---
|
4
|
+
description: Advanced concurrency control and resource limiting for Falcon web server.
|
5
|
+
metadata:
|
6
|
+
documentation_uri: https://socketry.github.io/falcon-limiter/
|
7
|
+
source_code_uri: https://github.com/socketry/falcon-limiter.git
|
8
|
+
files:
|
9
|
+
- path: getting-started.md
|
10
|
+
title: Getting Started
|
11
|
+
description: This guide explains how to get started with `falcon-limiter` for advanced
|
12
|
+
concurrency control and resource limiting in Falcon web applications.
|
13
|
+
- path: long-tasks.md
|
14
|
+
title: Long Tasks
|
15
|
+
description: This guide explains how to use <code class="language-ruby">Falcon::Limiter::LongTask</code>
|
16
|
+
to effectively manage I/O vs CPU bound workloads in your Falcon applications.
|
@@ -0,0 +1,160 @@
|
|
1
|
+
# Long Tasks
|
2
|
+
|
3
|
+
This guide explains how to use {ruby Falcon::Limiter::LongTask} to effectively manage I/O vs CPU bound workloads in your Falcon applications.
|
4
|
+
|
5
|
+
## Understanding Long Tasks
|
6
|
+
|
7
|
+
A **long task** in `falcon-limiter` is any operation that takes significant time (1+ seconds) and isn't CPU-bound, typically I/O operations like:
|
8
|
+
|
9
|
+
- Database queries.
|
10
|
+
- External API calls.
|
11
|
+
- File system operations.
|
12
|
+
- Network requests.
|
13
|
+
- Message queue operations.
|
14
|
+
|
15
|
+
The key insight is that during I/O operations, your application is waiting rather than consuming CPU resources. Long tasks allow the server to:
|
16
|
+
|
17
|
+
1. **Release the connection token** during I/O, allowing other requests to be accepted.
|
18
|
+
2. **Maintain responsiveness** by not blocking CPU-bound requests.
|
19
|
+
3. **Optimize resource utilization** by running multiple I/O operations concurrently.
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
When using {ruby Falcon::Limiter::Environment}, a long task is automatically created for each request:
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
# In your Rack application
|
27
|
+
run do |env|
|
28
|
+
# Long task is available via Falcon::Limiter::LongTask.current
|
29
|
+
long_task = Falcon::Limiter::LongTask.current
|
30
|
+
|
31
|
+
if long_task
|
32
|
+
# Start the long task for I/O operations
|
33
|
+
long_task.start
|
34
|
+
|
35
|
+
# Perform I/O operation
|
36
|
+
database_query
|
37
|
+
|
38
|
+
# Long task automatically stops when request completes
|
39
|
+
end
|
40
|
+
|
41
|
+
[200, {}, ["Response"]]
|
42
|
+
end
|
43
|
+
```
|
44
|
+
|
45
|
+
### Custom Delays
|
46
|
+
|
47
|
+
A delay is used to avoid starting a long task until we know that it's likely to be slow.
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
run do |env|
|
51
|
+
path = env["PATH_INFO"]
|
52
|
+
|
53
|
+
case path
|
54
|
+
when "/io"
|
55
|
+
# Start long task with default delay (0.1 seconds):
|
56
|
+
Falcon::Limiter::LongTask.current.start
|
57
|
+
|
58
|
+
# Perform I/O operation:
|
59
|
+
external_api_call
|
60
|
+
|
61
|
+
when "/io-immediate"
|
62
|
+
# Start immediately without delay:
|
63
|
+
Falcon::Limiter::LongTask.current.start(delay: false)
|
64
|
+
|
65
|
+
# Perform I/O operation:
|
66
|
+
database_query
|
67
|
+
|
68
|
+
when "/io-custom-delay"
|
69
|
+
# Start with custom delay:
|
70
|
+
Falcon::Limiter::LongTask.current.start(delay: 0.5)
|
71
|
+
|
72
|
+
# Perform I/O operation:
|
73
|
+
slow_file_operation
|
74
|
+
|
75
|
+
when "/cpu"
|
76
|
+
# Don't start long task for CPU-bound work:
|
77
|
+
cpu_intensive_calculation
|
78
|
+
end
|
79
|
+
|
80
|
+
[200, {}, ["Completed"]]
|
81
|
+
end
|
82
|
+
```
|
83
|
+
|
84
|
+
### Block-based Long Tasks
|
85
|
+
|
86
|
+
You can use long tasks with blocks for automatic cleanup:
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
require "net/http"
|
90
|
+
|
91
|
+
run do |env|
|
92
|
+
path = env["PATH_INFO"]
|
93
|
+
|
94
|
+
case path
|
95
|
+
when "/api/weather"
|
96
|
+
# Use block-based long task for automatic cleanup:
|
97
|
+
response = Falcon::Limiter::LongTask.current.start do |long_task|
|
98
|
+
# Make external API call:
|
99
|
+
uri = URI("https://api.openweathermap.org/data/2.5/weather?q=London")
|
100
|
+
Net::HTTP.get_response(uri)
|
101
|
+
# Automatic LongTask#stop when block exits.
|
102
|
+
end
|
103
|
+
|
104
|
+
# CPU work happens outside the long task:
|
105
|
+
result = JSON.parse(response.body)
|
106
|
+
[200, {"content-type" => "application/json"}, [result.to_json]]
|
107
|
+
|
108
|
+
when "/api/database"
|
109
|
+
# Multiple I/O operations in one long task:
|
110
|
+
users, enriched_data = Falcon::Limiter::LongTask.current.start do
|
111
|
+
# Database query
|
112
|
+
users = database.query("SELECT * FROM users WHERE active = true")
|
113
|
+
|
114
|
+
# External service call:
|
115
|
+
uri = URI("https://api.example.com/enrich")
|
116
|
+
enriched_data = Net::HTTP.post_form(uri, {user_ids: users.map(&:id)})
|
117
|
+
|
118
|
+
[users, enriched_data]
|
119
|
+
end
|
120
|
+
|
121
|
+
# CPU work happens outside the long task:
|
122
|
+
parsed_data = JSON.parse(enriched_data.body)
|
123
|
+
result = users.zip(parsed_data)
|
124
|
+
[200, {"content-type" => "application/json"}, [result.to_json]]
|
125
|
+
end
|
126
|
+
end
|
127
|
+
```
|
128
|
+
|
129
|
+
The block form ensures the long task is properly stopped even if an exception occurs. They can also be nested.
|
130
|
+
|
131
|
+
## Long Task Lifecycle
|
132
|
+
|
133
|
+
### 1. Creation
|
134
|
+
|
135
|
+
Long tasks are created automatically by {ruby Falcon::Limiter::Middleware} for each request when long task support is enabled.
|
136
|
+
|
137
|
+
### 2. Starting
|
138
|
+
|
139
|
+
When you call `start()`, the long task:
|
140
|
+
|
141
|
+
- Waits for the configured delay (default: 0.1 seconds).
|
142
|
+
- Acquires a long task token from the limiter.
|
143
|
+
- Releases the connection token, allowing new connections.
|
144
|
+
- Marks the connection as non-persistent to prevent token leakage
|
145
|
+
|
146
|
+
### 3. Execution
|
147
|
+
|
148
|
+
During long task execution:
|
149
|
+
|
150
|
+
- The connection token is released, so new requests can be accepted.
|
151
|
+
- The long task token prevents too many I/O operations from running concurrently.
|
152
|
+
- Your I/O operation runs normally.
|
153
|
+
|
154
|
+
### 4. Completion
|
155
|
+
|
156
|
+
When the request completes, the long task automatically:
|
157
|
+
|
158
|
+
- Releases the long task token.
|
159
|
+
- Re-acquires the connection token with high priority.
|
160
|
+
- Cleans up resources.
|
@@ -15,11 +15,11 @@ module Falcon
|
|
15
15
|
# Provides simple, declarative configuration for concurrency limiting.
|
16
16
|
# Override these methods in your service to customize behavior.
|
17
17
|
module Environment
|
18
|
-
# Maximum number of concurrent long tasks (default:
|
18
|
+
# Maximum number of concurrent long tasks (default: 10).
|
19
19
|
# If this is nil or non-positive, long task support will be disabled.
|
20
20
|
# @returns [Integer] The maximum number of concurrent long tasks.
|
21
21
|
def limiter_maximum_long_tasks
|
22
|
-
|
22
|
+
10
|
23
23
|
end
|
24
24
|
|
25
25
|
# @returns [Integer] The maximum number of concurrent connection accepts.
|
@@ -66,6 +66,7 @@ module Falcon
|
|
66
66
|
limiter_middleware(super)
|
67
67
|
end
|
68
68
|
|
69
|
+
# @returns [Falcon::Limiter::Wrapper] The wrapper for the connection limiter.
|
69
70
|
def limiter_wrapper
|
70
71
|
Wrapper.new(connection_limiter)
|
71
72
|
end
|
@@ -65,12 +65,18 @@ module Falcon
|
|
65
65
|
@delayed_start_task = nil
|
66
66
|
end
|
67
67
|
|
68
|
-
# Check if the long task has been started.
|
69
|
-
# @returns [Boolean]
|
68
|
+
# Check if the long task has been started, but not necessarily acquired (e.g. if there was a delay).
|
69
|
+
# @returns [Boolean] If the long task has been started.
|
70
70
|
def started?
|
71
71
|
@token.acquired? || @delayed_start_task
|
72
72
|
end
|
73
73
|
|
74
|
+
# Check if the long task has been acquired.
|
75
|
+
# @returns [Boolean] If the long task token has been acquired.
|
76
|
+
def acquired?
|
77
|
+
@token.acquired?
|
78
|
+
end
|
79
|
+
|
74
80
|
# Start the long task, optionally with a delay to avoid overhead for short operations
|
75
81
|
def start(delay: @start_delay)
|
76
82
|
# If already started, nothing to do:
|
@@ -88,10 +94,6 @@ module Falcon
|
|
88
94
|
delay = nil
|
89
95
|
end
|
90
96
|
|
91
|
-
def acquired?
|
92
|
-
@token.acquired?
|
93
|
-
end
|
94
|
-
|
95
97
|
# Otherwise, start the long task:
|
96
98
|
if delay&.positive?
|
97
99
|
# Wait specified delay before starting the long task:
|
@@ -16,9 +16,9 @@ module Falcon
|
|
16
16
|
# Initialize the middleware with limiting configuration.
|
17
17
|
# @parameter delegate [Object] The next middleware in the chain to call.
|
18
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:
|
19
|
+
# @parameter maximum_long_tasks [Integer] Maximum number of concurrent long tasks (default: 10).
|
20
20
|
# @parameter start_delay [Float] Delay in seconds before starting long tasks (default: 0.1).
|
21
|
-
def initialize(delegate, connection_limiter:, maximum_long_tasks:
|
21
|
+
def initialize(delegate, connection_limiter:, maximum_long_tasks: 10, start_delay: 0.1)
|
22
22
|
super(delegate)
|
23
23
|
|
24
24
|
@maximum_long_tasks = maximum_long_tasks
|
@@ -49,17 +49,9 @@ module Falcon
|
|
49
49
|
end
|
50
50
|
end
|
51
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
52
|
# Forward common inspection methods
|
61
53
|
def inspect
|
62
|
-
"#<#{
|
54
|
+
"#<#{Socket} #{@delegate.inspect}>"
|
63
55
|
end
|
64
56
|
|
65
57
|
# String representation of the wrapped socket.
|
@@ -43,6 +43,10 @@ module Falcon
|
|
43
43
|
token.release unless socket
|
44
44
|
end
|
45
45
|
|
46
|
+
# Wrap the socket with a transparent token management.
|
47
|
+
# @parameter socket [Object] The socket to wrap.
|
48
|
+
# @parameter token [Async::Limiter::Token] The limiter token to release when socket closes.
|
49
|
+
# @returns [Falcon::Limiter::Socket] The wrapped socket.
|
46
50
|
def wrap_socket(socket, token)
|
47
51
|
Socket.new(socket, token)
|
48
52
|
end
|
data/readme.md
CHANGED
@@ -8,126 +8,33 @@ Advanced concurrency control and resource limiting for Falcon web server, built
|
|
8
8
|
|
9
9
|
This gem provides sophisticated concurrency management for Falcon applications by:
|
10
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
|
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
16
|
|
17
|
-
##
|
17
|
+
## Usage
|
18
18
|
|
19
|
-
|
19
|
+
Please see the [project documentation](https://socketry.github.io/falcon-limiter/) for more details.
|
20
20
|
|
21
|
-
|
22
|
-
gem 'falcon-limiter'
|
23
|
-
```
|
21
|
+
- [Getting Started](https://socketry.github.io/falcon-limiter/guides/getting-started/index) - This guide explains how to get started with `falcon-limiter` for advanced concurrency control and resource limiting in Falcon web applications.
|
24
22
|
|
25
|
-
|
23
|
+
- [Long Tasks](https://socketry.github.io/falcon-limiter/guides/long-tasks/index) - This guide explains how to use <code class="language-ruby">Falcon::Limiter::LongTask</code> to effectively manage I/O vs CPU bound workloads in your Falcon applications.
|
26
24
|
|
27
|
-
|
25
|
+
## Releases
|
26
|
+
|
27
|
+
Please see the [project releases](https://socketry.github.io/falcon-limiter/releases/index) for all releases.
|
28
28
|
|
29
|
-
###
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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.
|
29
|
+
### v0.1.0
|
30
|
+
|
31
|
+
- Initial implementation.
|
32
|
+
|
33
|
+
## See Also
|
34
|
+
|
35
|
+
- [falcon](https://github.com/socketry/falcon) - A fast, asynchronous, rack-compatible web server.
|
36
|
+
- [async-limiter](https://github.com/socketry/async-limiter) - Execution rate limiting for Async.
|
37
|
+
- [async](https://github.com/socketry/async) - A concurrency framework for Ruby.
|
131
38
|
|
132
39
|
## Contributing
|
133
40
|
|
@@ -146,15 +53,3 @@ In order to protect users of this project, we require all contributors to comply
|
|
146
53
|
### Community Guidelines
|
147
54
|
|
148
55
|
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
CHANGED
data.tar.gz.sig
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: falcon-limiter
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Josh Teeter
|
@@ -59,14 +59,9 @@ executables: []
|
|
59
59
|
extensions: []
|
60
60
|
extra_rdoc_files: []
|
61
61
|
files:
|
62
|
-
-
|
63
|
-
-
|
64
|
-
-
|
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
|
62
|
+
- context/getting-started.md
|
63
|
+
- context/index.yaml
|
64
|
+
- context/long-tasks.md
|
70
65
|
- lib/falcon/limiter.rb
|
71
66
|
- lib/falcon/limiter/environment.rb
|
72
67
|
- lib/falcon/limiter/long_task.rb
|
metadata.gz.sig
CHANGED
Binary file
|
data/examples/basic_limiting.rb
DELETED
@@ -1,40 +0,0 @@
|
|
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}"
|
@@ -1,77 +0,0 @@
|
|
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
|
@@ -1,55 +0,0 @@
|
|
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
|
data/examples/limiter/README.md
DELETED
@@ -1,95 +0,0 @@
|
|
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
|
data/examples/limiter/config.ru
DELETED
@@ -1,50 +0,0 @@
|
|
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
|
-
}
|
data/examples/limiter/falcon.rb
DELETED
@@ -1,60 +0,0 @@
|
|
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
|
data/examples/load/config.ru
DELETED
@@ -1,19 +0,0 @@
|
|
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
|
data/examples/load/falcon.rb
DELETED
@@ -1,19 +0,0 @@
|
|
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
|