fantail 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b1f870f878fe0737149ef9b28549ceeba613a51fc39c70a243a84667713b73d4
4
+ data.tar.gz: 114fd5d72a6f53af337967440fdabd26c8f726a0654edaf80835a2b96faa4666
5
+ SHA512:
6
+ metadata.gz: 734e0a70510bfdd3ee9e9bf602d0ecb557f8202cde970c7b4aeb4e4900f23ff7fc6bc8f2a3c8a675b1366bb3d99ad6772618b6043ca737edf50ef37e7482e3e4
7
+ data.tar.gz: 9e5cff59c2dd10739f677a102c12ef83cb3e007b175be6158e239e59abdf7ffe0b3460258149862564ebf6c09575467aaa92b2168581cdcba2cdf28bc77ba942
checksums.yaml.gz.sig ADDED
Binary file
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ module Fantail
7
+ # Represents a live backend client and its admission state.
8
+ class Backend
9
+ # Initialize a backend.
10
+ # @parameter endpoint [Endpoint] The endpoint served by this backend.
11
+ # @parameter client [Interface(:call, :close)] The HTTP client for the endpoint.
12
+ # @parameter exchange_limit [Integer] The maximum number of outstanding response exchanges.
13
+ # @yields {|backend| ...} Invoked when the backend can accept another request.
14
+ def initialize(endpoint, client, exchange_limit:, &available)
15
+ raise ArgumentError, "Exchange limit must be positive!" unless exchange_limit.positive?
16
+
17
+ @endpoint = endpoint
18
+ @client = client
19
+ @exchange_limit = exchange_limit
20
+ @available = available
21
+
22
+ @guard = Thread::Mutex.new
23
+ @active = true
24
+ @processing = false
25
+ @exchanges = 0
26
+ @queued = false
27
+ @closed = false
28
+ end
29
+
30
+ # @attribute [Endpoint] The endpoint served by this backend.
31
+ attr :endpoint
32
+
33
+ # @attribute [String] The stable backend name.
34
+ def name
35
+ @endpoint.name
36
+ end
37
+
38
+ # @attribute [Integer] The maximum number of outstanding response exchanges.
39
+ attr :exchange_limit
40
+
41
+ # Start advertising this backend as available.
42
+ def start
43
+ notify_available
44
+ end
45
+
46
+ # Reserve the processing slot and one response exchange.
47
+ # @returns [Boolean] Whether the backend was successfully reserved.
48
+ def reserve
49
+ @guard.synchronize do
50
+ @queued = false
51
+
52
+ if @active && !@processing && @exchanges < @exchange_limit
53
+ @processing = true
54
+ @exchanges += 1
55
+ return true
56
+ end
57
+ end
58
+
59
+ return false
60
+ end
61
+
62
+ # Send a request to the backend.
63
+ # @parameter request [Protocol::HTTP::Request] The upstream request.
64
+ # @returns [Protocol::HTTP::Response] The upstream response.
65
+ def call(request)
66
+ @client.call(request)
67
+ end
68
+
69
+ # Release the request-processing slot after response headers arrive.
70
+ def processed
71
+ @guard.synchronize do
72
+ raise RuntimeError, "Backend is not processing a request!" unless @processing
73
+ @processing = false
74
+ end
75
+
76
+ notify_available
77
+ end
78
+
79
+ # Release both reservations when a request fails before response headers.
80
+ def failed
81
+ close = @guard.synchronize do
82
+ raise RuntimeError, "Backend is not processing a request!" unless @processing
83
+ @processing = false
84
+ @exchanges -= 1
85
+ should_close?
86
+ end
87
+
88
+ notify_available
89
+ close_client if close
90
+ end
91
+
92
+ # Release an outstanding response exchange.
93
+ def release
94
+ close = @guard.synchronize do
95
+ raise RuntimeError, "Backend has no outstanding exchange!" unless @exchanges.positive?
96
+ @exchanges -= 1
97
+ should_close?
98
+ end
99
+
100
+ notify_available
101
+ close_client if close
102
+ end
103
+
104
+ # Retire this backend without interrupting outstanding responses.
105
+ def retire
106
+ close = @guard.synchronize do
107
+ @active = false
108
+ should_close?
109
+ end
110
+
111
+ close_client if close
112
+ end
113
+
114
+ # @returns [Boolean] Whether this backend still accepts new requests.
115
+ def active?
116
+ @guard.synchronize{@active}
117
+ end
118
+
119
+ # @returns [Integer] The number of outstanding response exchanges.
120
+ def exchanges
121
+ @guard.synchronize{@exchanges}
122
+ end
123
+
124
+ # @returns [Boolean] Whether a request is waiting for response headers.
125
+ def processing?
126
+ @guard.synchronize{@processing}
127
+ end
128
+
129
+ protected
130
+
131
+ def notify_available
132
+ notify = @guard.synchronize do
133
+ if @active && !@processing && @exchanges < @exchange_limit && !@queued
134
+ @queued = true
135
+ true
136
+ else
137
+ false
138
+ end
139
+ end
140
+
141
+ @available.call(self) if notify
142
+ end
143
+
144
+ def should_close?
145
+ !@active && !@processing && @exchanges.zero? && !@closed
146
+ end
147
+
148
+ def close_client
149
+ close = @guard.synchronize do
150
+ unless @closed
151
+ @closed = true
152
+ true
153
+ end
154
+ end
155
+
156
+ @client.close if close
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require "async/bus/server"
7
+
8
+ module Fantail
9
+ # Exposes a registry over async-bus for endpoint publication.
10
+ class Control < Async::Bus::Server
11
+ # Initialize a control server.
12
+ # @parameter endpoint [IO::Endpoint] The endpoint used for registry updates.
13
+ # @parameter registry [Registry] The registry to expose.
14
+ def initialize(endpoint, registry)
15
+ super(endpoint)
16
+ @registry = registry
17
+ end
18
+
19
+ protected def connected!(connection)
20
+ connection.bind(:registry, @registry)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require "async/http/client"
7
+ require "async/http/endpoint"
8
+ require "async/http/protocol/http1"
9
+ require "async/http/protocol/http2"
10
+
11
+ module Fantail
12
+ # Represents a named HTTP backend endpoint.
13
+ class Endpoint
14
+ # Coerce an endpoint or endpoint description into an endpoint.
15
+ # @parameter description [Endpoint | Hash] The endpoint or serialized endpoint description.
16
+ # @returns [Endpoint] The coerced endpoint.
17
+ def self.coerce(description)
18
+ return description if description.is_a?(self)
19
+
20
+ name = description[:name] || description["name"]
21
+ url = description[:url] || description["url"]
22
+ protocol = description[:protocol] || description["protocol"]
23
+
24
+ self.new(name, url, protocol: protocol)
25
+ end
26
+
27
+ # Initialize a named backend endpoint.
28
+ # @parameter name [String] The stable endpoint identity.
29
+ # @parameter url [String] The absolute backend URL.
30
+ # @parameter protocol [String | Nil] The upstream HTTP protocol name.
31
+ def initialize(name, url, protocol: nil)
32
+ raise ArgumentError, "Endpoint name is required!" unless name
33
+ raise ArgumentError, "Endpoint URL is required!" unless url
34
+
35
+ @name = name.to_s
36
+ @url = url.to_s
37
+ @protocol = protocol&.to_s
38
+
39
+ freeze
40
+ end
41
+
42
+ # @attribute [String] The stable endpoint identity.
43
+ attr :name
44
+
45
+ # @attribute [String] The absolute backend URL.
46
+ attr :url
47
+
48
+ # @attribute [String | Nil] The upstream HTTP protocol name.
49
+ attr :protocol
50
+
51
+ # Build an HTTP client for this endpoint.
52
+ # @parameter exchange_limit [Integer] The maximum number of outstanding response exchanges.
53
+ # @returns [Async::HTTP::Client] A client connected to this endpoint.
54
+ def make_client(exchange_limit:)
55
+ endpoint = Async::HTTP::Endpoint.parse(@url, protocol: protocol_module)
56
+
57
+ Async::HTTP::Client.new(endpoint, limit: exchange_limit, retries: 0)
58
+ end
59
+
60
+ # Convert this endpoint into a transport-safe description.
61
+ # @returns [Hash] A serialized endpoint description.
62
+ def to_h
63
+ {
64
+ "name" => @name,
65
+ "url" => @url,
66
+ "protocol" => @protocol,
67
+ }.compact
68
+ end
69
+
70
+ # Compare endpoints by their serialized configuration.
71
+ def ==(other)
72
+ other.is_a?(Endpoint) && other.to_h == self.to_h
73
+ end
74
+
75
+ protected
76
+
77
+ def protocol_module
78
+ case @protocol
79
+ when nil, "http/1.0", "http/1.1"
80
+ Async::HTTP::Protocol::HTTP1
81
+ when "h2", "http/2"
82
+ Async::HTTP::Protocol::HTTP2
83
+ else
84
+ raise ArgumentError, "Unsupported HTTP protocol: #{@protocol.inspect}"
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require "async/bus/client"
7
+ require "async/queue"
8
+
9
+ require_relative "endpoint"
10
+
11
+ module Fantail
12
+ # Publishes a desired endpoint set and subsequent deltas to a Fantail registry.
13
+ class Monitor < Async::Bus::Client
14
+ # Initialize an endpoint monitor.
15
+ # @parameter endpoint [IO::Endpoint] The Fantail control endpoint.
16
+ def initialize(endpoint)
17
+ super(endpoint)
18
+
19
+ @guard = Thread::Mutex.new
20
+ @endpoints = {}
21
+ @revision = 0
22
+ @changes = Async::Queue.new
23
+ end
24
+
25
+ # Replace the desired endpoint set without blocking on the control connection.
26
+ # @parameter descriptions [Array(Endpoint | Hash)] The complete desired endpoint set.
27
+ # @returns [Integer] The local revision number.
28
+ def replace(descriptions)
29
+ endpoints = descriptions.map{|description| Endpoint.coerce(description).to_h}
30
+
31
+ revision = @guard.synchronize do
32
+ @endpoints = endpoints.to_h{|description| [description.fetch("name"), description]}
33
+ @revision += 1
34
+ end
35
+
36
+ @changes.enqueue([revision, :replace, endpoints])
37
+ return revision
38
+ end
39
+
40
+ # Add or replace a desired endpoint without blocking on the control connection.
41
+ # @parameter description [Endpoint | Hash] The endpoint to publish.
42
+ # @returns [Integer] The local revision number.
43
+ def upsert(description)
44
+ endpoint = Endpoint.coerce(description).to_h
45
+
46
+ revision = @guard.synchronize do
47
+ @endpoints[endpoint.fetch("name")] = endpoint
48
+ @revision += 1
49
+ end
50
+
51
+ @changes.enqueue([revision, :update, [endpoint], []])
52
+ return revision
53
+ end
54
+
55
+ # Remove a desired endpoint without blocking on the control connection.
56
+ # @parameter name [String] The endpoint identity to remove.
57
+ # @returns [Integer] The local revision number.
58
+ def remove(name)
59
+ name = name.to_s
60
+
61
+ revision = @guard.synchronize do
62
+ @endpoints.delete(name)
63
+ @revision += 1
64
+ end
65
+
66
+ @changes.enqueue([revision, :update, [], [name]])
67
+ return revision
68
+ end
69
+
70
+ # @returns [Array(Hash)] A snapshot of the desired endpoints.
71
+ def endpoints
72
+ @guard.synchronize{@endpoints.values.map(&:dup)}
73
+ end
74
+
75
+ # Run the persistent publisher, resynchronizing completely after each reconnect.
76
+ # @parameter options [Hash] Options forwarded to Async::Bus::Client#run.
77
+ # @returns [Async::Task] The publisher task.
78
+ def run(**options)
79
+ super(**options) do |connection|
80
+ synchronize(connection[:registry])
81
+ end
82
+ end
83
+
84
+ protected
85
+
86
+ def synchronize(registry)
87
+ revision, endpoints = snapshot
88
+ registry.replace(endpoints)
89
+
90
+ publish_changes(registry, revision)
91
+ end
92
+
93
+ def snapshot
94
+ @guard.synchronize{[@revision, @endpoints.values.map(&:dup)]}
95
+ end
96
+
97
+ def publish_changes(registry, revision)
98
+ while change = @changes.dequeue
99
+ change_revision, operation, *arguments = change
100
+ next if change_revision <= revision
101
+
102
+ registry.public_send(operation, *arguments)
103
+ revision = change_revision
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require "protocol/http/request"
7
+ require "protocol/http/response"
8
+
9
+ require_relative "response_body"
10
+
11
+ module Fantail
12
+ # Routes HTTP requests through the registry's global admission queue.
13
+ class Proxy
14
+ # Initialize an HTTP proxy.
15
+ # @parameter registry [Registry] The backend registry.
16
+ def initialize(registry)
17
+ @registry = registry
18
+ end
19
+
20
+ # Route a request to the next available backend.
21
+ # @parameter request [Protocol::HTTP::Request] The downstream request.
22
+ # @returns [Protocol::HTTP::Response] The upstream or generated response.
23
+ def call(request)
24
+ unless backend = @registry.acquire
25
+ return Protocol::HTTP::Response[503, {"content-type" => "text/plain"}, ["No backends available.\n"]]
26
+ end
27
+
28
+ reservation = :processing
29
+ upstream_request = build_request(request)
30
+ response = backend.call(upstream_request)
31
+ backend.processed
32
+ reservation = :exchange
33
+
34
+ if body = response.body
35
+ response.body = ResponseBody.new(body){backend.release}
36
+ else
37
+ backend.release
38
+ end
39
+ reservation = nil
40
+
41
+ return response
42
+ rescue => error
43
+ case reservation
44
+ when :processing
45
+ backend.failed
46
+ when :exchange
47
+ backend.release
48
+ end
49
+
50
+ return Protocol::HTTP::Response[502, {"content-type" => "text/plain"}, ["Bad Gateway: #{error.class}\n"]]
51
+ end
52
+
53
+ protected
54
+
55
+ def build_request(request)
56
+ upstream_request = Protocol::HTTP::Request.new(
57
+ nil,
58
+ nil,
59
+ request.method,
60
+ request.path,
61
+ nil,
62
+ request.headers,
63
+ request.body,
64
+ request.protocol,
65
+ request.interim_response,
66
+ )
67
+
68
+ # Transfer ownership of the request body to the upstream request:
69
+ request.body = nil
70
+
71
+ return upstream_request
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require "async/bus/controller"
7
+ require "async/queue"
8
+
9
+ require_relative "endpoint"
10
+ require_relative "backend"
11
+
12
+ module Fantail
13
+ # Maintains live backends and a global queue of available processing slots.
14
+ class Registry < Async::Bus::Controller
15
+ WAKE = Object.new.freeze
16
+
17
+ # Initialize an endpoint registry.
18
+ # @parameter exchange_limit [Integer] The maximum outstanding responses per backend.
19
+ # @parameter backend_factory [Proc | Nil] An optional backend construction strategy.
20
+ def initialize(exchange_limit: 8, backend_factory: nil)
21
+ @exchange_limit = exchange_limit
22
+ @backend_factory = backend_factory || self.method(:make_backend)
23
+
24
+ @guard = Thread::Mutex.new
25
+ @backends = {}
26
+ @available = Async::Queue.new
27
+ @closed = false
28
+ end
29
+
30
+ # Replace the complete endpoint set.
31
+ # @parameter descriptions [Array(Endpoint | Hash)] The desired endpoints.
32
+ # @returns [Integer] The resulting endpoint count.
33
+ def replace(descriptions)
34
+ endpoints = descriptions.map{|description| Endpoint.coerce(description)}
35
+ names = endpoints.map(&:name)
36
+
37
+ current_names = @guard.synchronize{@backends.keys}
38
+ self.update(endpoints, current_names - names)
39
+ end
40
+
41
+ # Apply endpoint additions, replacements, and removals.
42
+ # @parameter upserted [Array(Endpoint | Hash)] Endpoints to add or replace.
43
+ # @parameter removed [Array(String)] Endpoint names to remove.
44
+ # @returns [Integer] The resulting endpoint count.
45
+ def update(upserted, removed)
46
+ retired = []
47
+ started = []
48
+
49
+ @guard.synchronize do
50
+ raise IOError, "Registry is closed!" if @closed
51
+
52
+ removed.each do |name|
53
+ if backend = @backends.delete(name.to_s)
54
+ retired << backend
55
+ end
56
+ end
57
+
58
+ upserted.each do |description|
59
+ endpoint = Endpoint.coerce(description)
60
+ current = @backends[endpoint.name]
61
+
62
+ next if current&.endpoint == endpoint
63
+
64
+ retired << current if current
65
+ backend = @backend_factory.call(endpoint, @exchange_limit, self.method(:offer))
66
+ @backends[endpoint.name] = backend
67
+ started << backend
68
+ end
69
+ end
70
+
71
+ retired.each(&:retire)
72
+ started.each(&:start)
73
+ @available.enqueue(WAKE) unless retired.empty?
74
+
75
+ self.size
76
+ end
77
+
78
+ # Acquire the next backend with processing capacity.
79
+ # @returns [Backend | Nil] An admitted backend, or nil if no endpoints exist.
80
+ def acquire
81
+ loop do
82
+ return nil if self.empty?
83
+
84
+ candidate = @available.dequeue
85
+ return nil unless candidate
86
+ next if candidate.equal?(WAKE)
87
+
88
+ return candidate if candidate.reserve
89
+ end
90
+ end
91
+
92
+ # @returns [Integer] The number of active endpoints.
93
+ def size
94
+ @guard.synchronize{@backends.size}
95
+ end
96
+
97
+ # @returns [Boolean] Whether no active endpoints are registered.
98
+ def empty?
99
+ self.size.zero?
100
+ end
101
+
102
+ # @returns [Array(String)] The active endpoint names.
103
+ def names
104
+ @guard.synchronize{@backends.keys.sort}
105
+ end
106
+
107
+ # Find an active backend by name.
108
+ # @parameter name [String] The endpoint name.
109
+ # @returns [Backend | Nil] The active backend.
110
+ def [](name)
111
+ @guard.synchronize{@backends[name.to_s]}
112
+ end
113
+
114
+ # Close the registry and retire all backends.
115
+ def close
116
+ backends = @guard.synchronize do
117
+ next [] if @closed
118
+
119
+ @closed = true
120
+ @backends.values.tap{@backends = {}}
121
+ end
122
+
123
+ @available.close
124
+ backends.each(&:retire)
125
+ end
126
+
127
+ protected
128
+
129
+ def offer(backend)
130
+ @available.enqueue(backend)
131
+ rescue Async::Queue::ClosedError
132
+ # The registry is already shutting down:
133
+ end
134
+
135
+ def make_backend(endpoint, exchange_limit, available)
136
+ client = endpoint.make_client(exchange_limit: exchange_limit)
137
+ Backend.new(endpoint, client, exchange_limit: exchange_limit, &available)
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require "protocol/http/body/wrapper"
7
+
8
+ module Fantail
9
+ # Wraps an upstream response body and releases its backend exchange when closed.
10
+ class ResponseBody < Protocol::HTTP::Body::Wrapper
11
+ # Initialize a response body wrapper.
12
+ # @parameter body [Protocol::HTTP::Body::Readable] The upstream response body.
13
+ # @yields Invoked exactly once when the response body closes.
14
+ def initialize(body, &release)
15
+ super(body)
16
+ @release = release
17
+ end
18
+
19
+ # Close the upstream body and release its backend exchange.
20
+ # @parameter error [Exception | Nil] The error which caused the body to close.
21
+ def close(error = nil)
22
+ super
23
+ ensure
24
+ if release = @release
25
+ @release = nil
26
+ release.call
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require "async/http/server"
7
+
8
+ require_relative "registry"
9
+ require_relative "proxy"
10
+ require_relative "control"
11
+
12
+ module Fantail
13
+ # Runs the HTTP load balancer and endpoint-control server together.
14
+ class Server
15
+ # Initialize a Fantail server.
16
+ # @parameter endpoint [Async::HTTP::Endpoint] The downstream HTTP endpoint.
17
+ # @parameter control_endpoint [IO::Endpoint] The async-bus control endpoint.
18
+ # @parameter exchange_limit [Integer] The maximum outstanding responses per backend.
19
+ def initialize(endpoint, control_endpoint, exchange_limit: 8)
20
+ @registry = Registry.new(exchange_limit: exchange_limit)
21
+ @proxy = Proxy.new(@registry)
22
+ @http_server = Async::HTTP::Server.new(@proxy, endpoint)
23
+ @control_server = Control.new(control_endpoint, @registry)
24
+ end
25
+
26
+ # @attribute [Registry] The server's endpoint registry.
27
+ attr :registry
28
+
29
+ # Run the HTTP and control servers.
30
+ # @parameter parent [Interface(:async)] The parent task.
31
+ # @returns [Async::Task] The server task.
32
+ def run(parent: Async::Task.current)
33
+ parent.async do |task|
34
+ task.async do
35
+ @control_server.accept
36
+ end
37
+
38
+ @http_server.run.wait
39
+ end
40
+ end
41
+
42
+ # Close the endpoint registry.
43
+ def close
44
+ @registry.close
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ module Fantail
7
+ VERSION = "0.0.1"
8
+ end
data/lib/fantail.rb ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require_relative "fantail/version"
7
+ require_relative "fantail/endpoint"
8
+ require_relative "fantail/backend"
9
+ require_relative "fantail/response_body"
10
+ require_relative "fantail/registry"
11
+ require_relative "fantail/proxy"
12
+ require_relative "fantail/control"
13
+ require_relative "fantail/monitor"
14
+ require_relative "fantail/server"
15
+
16
+ # Provides worker-aware HTTP load balancing with a global admission queue.
17
+ module Fantail
18
+ end
data/license.md ADDED
@@ -0,0 +1,21 @@
1
+ # MIT License
2
+
3
+ Copyright, 2026, by Samuel Williams.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/readme.md ADDED
@@ -0,0 +1,55 @@
1
+ # Fantail
2
+
3
+ Worker-aware HTTP load balancing with a global admission queue.
4
+
5
+ [![Development Status](https://github.com/socketry/fantail/workflows/Test/badge.svg)](https://github.com/socketry/fantail/actions?workflow=Test)
6
+
7
+ Fantail routes each request to a worker which is ready to process it. It separates the short-lived request-processing reservation from the potentially longer response exchange, so another request can begin after response headers arrive while the previous response body is still streaming.
8
+
9
+ ## Usage
10
+
11
+ Please see the [project documentation](https://socketry.github.io/fantail/) for more details.
12
+
13
+ - [Getting Started](https://socketry.github.io/fantail/guides/getting-started/index) - This guide explains how to run Fantail and publish HTTP worker endpoints.
14
+
15
+ ## Releases
16
+
17
+ Please see the [project releases](https://socketry.github.io/fantail/releases/index) for all releases.
18
+
19
+ ### v0.0.1
20
+
21
+ - Initial implementation.
22
+
23
+ ## Contributing
24
+
25
+ We welcome contributions to this project.
26
+
27
+ 1. Fork the repository.
28
+ 2. Create your feature branch (`git checkout -b my-new-feature`).
29
+ 3. Commit your changes (`git commit -am 'Add some feature.'`).
30
+ 4. Push to the branch (`git push origin my-new-feature`).
31
+ 5. Create a new pull request.
32
+
33
+ ### Running Tests
34
+
35
+ To run the test suite:
36
+
37
+ ``` bash
38
+ $ bundle exec sus
39
+ ```
40
+
41
+ ### Making Releases
42
+
43
+ To make a new release:
44
+
45
+ ``` bash
46
+ $ bundle exec bake gem:release:patch # or minor or major
47
+ ```
48
+
49
+ ### Developer Certificate of Origin
50
+
51
+ 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.
52
+
53
+ ### Community Guidelines
54
+
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.
data/releases.md ADDED
@@ -0,0 +1,5 @@
1
+ # Releases
2
+
3
+ ## v0.0.1
4
+
5
+ - Initial implementation.
data.tar.gz.sig ADDED
Binary file
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fantail
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Samuel Williams
8
+ bindir: bin
9
+ cert_chain:
10
+ - |
11
+ -----BEGIN CERTIFICATE-----
12
+ MIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11
13
+ ZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK
14
+ CZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz
15
+ MjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd
16
+ MBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj
17
+ bzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB
18
+ igKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2
19
+ 9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW
20
+ sGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE
21
+ e5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN
22
+ XibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss
23
+ RZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn
24
+ tUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM
25
+ zp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW
26
+ xm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O
27
+ BBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs
28
+ aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs
29
+ aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE
30
+ cBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl
31
+ xCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/
32
+ c1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp
33
+ 8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws
34
+ JkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP
35
+ eX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt
36
+ Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
37
+ voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
38
+ -----END CERTIFICATE-----
39
+ date: 1980-01-02 00:00:00.000000000 Z
40
+ dependencies:
41
+ - !ruby/object:Gem::Dependency
42
+ name: async-bus
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.3'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.3'
55
+ - !ruby/object:Gem::Dependency
56
+ name: async-http
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.99'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.99'
69
+ executables: []
70
+ extensions: []
71
+ extra_rdoc_files: []
72
+ files:
73
+ - lib/fantail.rb
74
+ - lib/fantail/backend.rb
75
+ - lib/fantail/control.rb
76
+ - lib/fantail/endpoint.rb
77
+ - lib/fantail/monitor.rb
78
+ - lib/fantail/proxy.rb
79
+ - lib/fantail/registry.rb
80
+ - lib/fantail/response_body.rb
81
+ - lib/fantail/server.rb
82
+ - lib/fantail/version.rb
83
+ - license.md
84
+ - readme.md
85
+ - releases.md
86
+ homepage: https://github.com/socketry/fantail
87
+ licenses:
88
+ - MIT
89
+ metadata:
90
+ bug_tracker_uri: https://github.com/socketry/fantail/issues
91
+ changelog_uri: https://github.com/socketry/fantail/blob/main/releases.md
92
+ documentation_uri: https://socketry.github.io/fantail/
93
+ funding_uri: https://github.com/sponsors/ioquatix/
94
+ source_code_uri: https://github.com/socketry/fantail.git
95
+ rdoc_options: []
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '3.3'
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ requirements: []
109
+ rubygems_version: 4.0.10
110
+ specification_version: 4
111
+ summary: Worker-aware HTTP load balancing with a global admission queue.
112
+ test_files: []
metadata.gz.sig ADDED
@@ -0,0 +1,5 @@
1
+ 8�Nċ����*�Pb��\��y,�� ��<���_��I�g8<4v7rz˅z�dX@\kK�4��$��P��NBi�J�^ľ������*lD���|8fI��g��83�؉o�5a�f���9�3�9p���[ĜP ?<�g2])��m��Ă�����ϫj$�!e6/|Gq�C��d���K��{C�����]Z������{�[�n�T
2
+ +k/�3z[
3
+ ���/��P��
4
+ I�y������1-�a�oo�UC�l�������A���C���T�O\FhL���VP����xo��{��׆��K�ie����D��L� �%�;���6\
5
+ ���Ku�'�A��w�Y�k[�����Pʻl�n�3�X����41�p����l