@@ -25,10 +25,9 @@ module Invidious::ConnectionPool
25
25
# Streaming API for {{method.id.upcase}} request.
26
26
# The response will have its body as an `IO` accessed via `HTTP::Client::Response#body_io`.
27
27
def {{method.id}} (* args , ** kwargs, & )
28
- self .checkout do | client |
28
+ self .checkout_with_retry do | client |
29
29
client.{{method.id}}(* args, ** kwargs) do | response |
30
- result = yield response
31
- return result
30
+ return yield response
32
31
ensure
33
32
response.body_io?.try & .skip_to_end
34
33
end
@@ -38,45 +37,82 @@ module Invidious::ConnectionPool
38
37
# Executes a {{method.id.upcase}} request.
39
38
# The response will have its body as a `String`, accessed via `HTTP::Client::Response#body`.
40
39
def {{method.id}} (* args , ** kwargs)
41
- self .checkout do | client |
40
+ self .checkout_with_retry do | client |
42
41
return client.{{method.id}}(* args, ** kwargs)
43
42
end
44
43
end
45
44
{% end % }
46
45
47
46
# Checks out a client in the pool
47
+ #
48
+ # This method will NOT delete a client that has errored from the pool.
49
+ # Use `#checkout_with_retry` to ensure that the pool does not get poisoned.
48
50
def checkout (& )
49
- # If a client has been deleted from the pool
50
- # we won't try to release it
51
- client_exists_in_pool = true
52
-
53
- http_client = pool.checkout
54
-
55
- # When the HTTP::Client connection is closed, the automatic reconnection
56
- # feature will create a new IO to connect to the server with
57
- #
58
- # This new TCP IO will be a direct connection to the server and will not go
59
- # through the proxy. As such we'll need to reinitialize the proxy connection
60
-
61
- http_client.proxy = make_configured_http_proxy_client() if @reinitialize_proxy && CONFIG .http_proxy
62
-
63
- response = yield http_client
64
- rescue ex : DB ::PoolTimeout
65
- # Failed to checkout a client
66
- raise ConnectionPool ::PoolCheckoutError .new(ex.message)
67
- rescue ex
68
- # An error occurred with the client itself.
69
- # Delete the client from the pool and close the connection
70
- if http_client
71
- client_exists_in_pool = false
72
- @pool .delete(http_client)
73
- http_client.close
51
+ pool.checkout do |client |
52
+ # When the HTTP::Client connection is closed, the automatic reconnection
53
+ # feature will create a new IO to connect to the server with
54
+ #
55
+ # This new TCP IO will be a direct connection to the server and will not go
56
+ # through the proxy. As such we'll need to reinitialize the proxy connection
57
+ client.proxy = make_configured_http_proxy_client() if @reinitialize_proxy && CONFIG .http_proxy
58
+
59
+ response = yield client
60
+
61
+ return response
62
+ rescue ex : DB ::PoolTimeout
63
+ # Failed to checkout a client
64
+ raise ConnectionPool ::PoolCheckoutError .new(ex.message)
74
65
end
66
+ end
75
67
76
- # Raise exception for outer methods to handle
77
- raise ConnectionPool ::Error .new(ex.message, cause: ex)
78
- ensure
79
- pool.release(http_client) if http_client && client_exists_in_pool
68
+ # Checks out a client from the pool; retries only if a connection is lost or refused
69
+ #
70
+ # Will cycle through all of the existing connections at no delay, but any new connections
71
+ # that is created will be subject to a delay.
72
+ #
73
+ # The first attempt to make a new connection will not have the delay, but all subsequent
74
+ # attempts will.
75
+ #
76
+ # To `DB::Pool#retry`:
77
+ # - `DB::PoolResourceLost` means that the connection has been lost
78
+ # and should be deleted from the pool.
79
+ #
80
+ # - `DB::PoolResourceRefused` means a new connection was intended to be created but failed
81
+ # but the client can be safely released back into the pool to try again later with
82
+ #
83
+ # See the following code of `crystal-db` for more information
84
+ #
85
+ # https://github.com/crystal-lang/crystal-db/blob/023dc5de90c11927656fc747601c5f08ea3c906f/src/db/pool.cr#L191
86
+ # https://github.com/crystal-lang/crystal-db/blob/023dc5de90c11927656fc747601c5f08ea3c906f/src/db/pool_statement.cr#L41
87
+ # https://github.com/crystal-lang/crystal-db/blob/023dc5de90c11927656fc747601c5f08ea3c906f/src/db/pool_prepared_statement.cr#L13
88
+ #
89
+ def checkout_with_retry (& )
90
+ @pool .retry do
91
+ self .checkout do |client |
92
+ begin
93
+ return yield client
94
+ rescue ex : IO ::TimeoutError
95
+ LOGGER .trace(" Client: #{ client } has failed to complete the request. Retrying with a new client" )
96
+ raise DB ::PoolResourceRefused .new
97
+ rescue ex : InfoException
98
+ raise ex
99
+ rescue ex : Exception
100
+ # Any other errors should cause the client to be deleted from the pool
101
+
102
+ # This means that the client is closed and needs to be deleted from the pool
103
+ # due its inability to reconnect
104
+ if ex.message == " This HTTP::Client cannot be reconnected"
105
+ LOGGER .trace(" Checked out client is closed and cannot be reconnected. Trying the next retry attempt..." )
106
+ else
107
+ LOGGER .error(" Client: #{ client } has encountered an error: #{ ex } #{ ex.message } and will be removed from the pool" )
108
+ end
109
+
110
+ raise DB ::PoolResourceLost (HTTP ::Client ).new(client)
111
+ end
112
+ end
113
+ rescue ex : DB ::PoolRetryAttemptsExceeded
114
+ raise PoolRetryAttemptsExceeded .new
115
+ end
80
116
end
81
117
end
82
118
@@ -87,6 +123,10 @@ module Invidious::ConnectionPool
87
123
class PoolCheckoutError < Error
88
124
end
89
125
126
+ # Raised when too many retries
127
+ class PoolRetryAttemptsExceeded < Error
128
+ end
129
+
90
130
# Mapping of subdomain => Invidious::ConnectionPool::Pool
91
131
# This is needed as we may need to access arbitrary subdomains of ytimg
92
132
private YTIMG_POOLS = {} of String => ConnectionPool ::Pool
0 commit comments