Skip to content

Commit e1a0882

Browse files
authored
Merge pull request #51 from Sage/connection-pooling
FOX-3833 - Connection pooling
2 parents 1d98c86 + bb9e5f7 commit e1a0882

15 files changed

Lines changed: 1039 additions & 279 deletions

.github/workflows/rspec.yml

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ jobs:
77

88
services:
99
mysql:
10-
image: mysql:5.7
10+
image: mysql:8.0
1111
ports:
1212
- 3306:3306
1313
env:
@@ -19,19 +19,11 @@ jobs:
1919
- 6379:6379
2020

2121
steps:
22-
- uses: actions/checkout@v2
22+
- uses: actions/checkout@v6
2323
- uses: ruby/setup-ruby@v1
2424
with:
25-
ruby-version: 2.4
25+
ruby-version: 4.0
2626
bundler-cache: true
2727

2828
- name: Run tests
2929
run: bundle exec rspec
30-
31-
- name: Code Coverage
32-
uses: paambaati/codeclimate-action@v2.7.5
33-
env:
34-
CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
35-
with:
36-
coverageLocations: |
37-
${{github.workspace}}/coverage/.resultset.json:simplecov

Gemfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ group :test, :development do
77
end
88

99
group :test do
10-
gem 'simplecov', '0.17.1', require: false
10+
gem 'simplecov', require: false
11+
gem 'debug', require: false
1112
end

README.md

Lines changed: 88 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,11 @@ gem 'mysql_framework'
3434

3535
#### MySQL Connection Pooling Variables
3636

37-
* `MYSQL_START_POOL_SIZE` - how many connections should be created by default (default: `1`)
37+
* `MYSQL_CONNECTION_POOL_ENABLED` - enables/disables pooling (default: `true`)
3838
* `MYSQL_MAX_POOL_SIZE` - how many connections should the pool be allowed to grow to (default: `5`)
39+
* `MYSQL_POOL_TIMEOUT` - how long to wait for a pooled connection before timing out (default: `5` seconds)
40+
* `MYSQL_POOL_IDLE_TIMEOUT` - how long a pooled connection can remain idle before being reaped (default: `300` seconds)
41+
* `MYSQL_POOL_IDLE_REAP_TIME` - time interval between background thread checking for idle connections to reap (default: `60` seconds)
3942

4043
#### MySQL Migration Variables
4144

@@ -161,31 +164,33 @@ MysqlFramework::Connector.new(options)
161164

162165
#### #setup
163166

164-
Sets up the connection pooling. Creates `ENV['MYSQL_START_POOL_SIZE']` `Mysql2::Client` instances up front. This is provided as a separate method to allow for use within process forking where connections would need to be created after forking the process.
167+
Sets up connection pooling using `connection_pool` with `ENV['MYSQL_MAX_POOL_SIZE']` and `ENV['MYSQL_POOL_TIMEOUT']`. Connections are created lazily by the pool when first needed.
165168

166169
```ruby
167170
connector.setup
168171
```
169172

170173
#### #dispose
171174

172-
Closes all the `Mysql2::Client` connections and removes the connection pool. Intended as a clean-up method to be used on process fork shutdown.
175+
Closes pooled `Mysql2::Client` connections and removes the pool. Intended as a clean-up method to be used on process fork shutdown.
173176

174177
```ruby
175178
connector.dispose
176179
```
177180

178181
#### #check_out
179182

180-
Check out a client from the connection pool. Will create new `Mysql2::Client` instances up-to `ENV['MYSQL_MAX_POOL_SIZE']` times if no idle connections are available.
183+
Checks out a `Mysql2::Client` instance from the pool, sanitizes it, and returns it.
184+
When pooling is disabled, it returns a newly created client.
181185

182186
```ruby
183187
client = connector.check_out
184188
```
185189

186190
#### #check_in
187191

188-
Check in a client to the connection pool
192+
Checks a client back in to the pool.
193+
When pooling is disabled, it closes the provided client.
189194

190195
```ruby
191196
client = connector.check_out
@@ -195,7 +200,17 @@ connector.check_in(client)
195200

196201
#### #with_client
197202

198-
Called with a block. The method checks out a client from the pool and yields it to the block. Finally it ensures that the client is always checked back into the pool.
203+
Called with a block. The method obtains a client (from the pool when enabled), yields it to the block, and guarantees cleanup.
204+
205+
When pooling is enabled, it uses the pool lifecycle (`ConnectionPool#with`) and supports optional discarding of the current pooled connection:
206+
207+
```ruby
208+
connector.with_client(discard_current_pool_connection: true) do |client|
209+
# use client
210+
end
211+
```
212+
213+
When pooling is disabled, it creates a fresh client for the block and closes it afterwards.
199214

200215
```ruby
201216
connector.with_client do |client|
@@ -205,6 +220,17 @@ connector.with_client do |client|
205220
end
206221
```
207222

223+
**Warning: re-entrant connections within the same thread**
224+
225+
The `connection_pool` gem implements thread-local connection tracking. When a thread already holds a connection via `with_client` (or `check_out`), any nested call to `with_client` or `check_out` on the **same thread** returns the **same connection** — it does not check out a second one from the pool.
226+
227+
This means that if you fire an async query on a connection and then attempt to run a second query (e.g. via `run_query` or `connector.query`) from within the same `with_client` block, the nested call will receive the already-checked-out connection. Sanitization will then fail with:
228+
229+
```
230+
Connection sanitization failed: This connection is still waiting for a result,
231+
try again once you have the result
232+
```
233+
208234
It can optionally accept an existing client to avoid starting new connections in the middle of a transaction. This can be used to ensure that a series of queries are wrapped by the same transaction.
209235

210236
```ruby
@@ -280,10 +306,65 @@ The default options used to initialise MySQL2::Client instances:
280306
database: ENV.fetch('MYSQL_DATABASE'),
281307
username: ENV.fetch('MYSQL_USERNAME'),
282308
password: ENV.fetch('MYSQL_PASSWORD'),
283-
reconnect: true
309+
reconnect: true,
310+
read_timeout: Integer(ENV.fetch('MYSQL_READ_TIMEOUT', 30)),
311+
write_timeout: Integer(ENV.fetch('MYSQL_WRITE_TIMEOUT', 10))
284312
}
285313
```
286314

315+
### MysqlFramework::Stats::AwsMetricPublisher
316+
317+
Publishes connection-pool metrics (`size`, `available`, `idle`) to AWS CloudWatch on a configurable interval via a background thread.
318+
319+
**Setup sequence** — the connector must be set up before the publisher is started:
320+
321+
```ruby
322+
connector = MysqlFramework::Connector.new
323+
connector.setup # must come first
324+
325+
publisher = MysqlFramework::Stats::AwsMetricPublisher.new(
326+
connector: connector,
327+
publish_interval: 300 # seconds, default
328+
)
329+
publisher.start
330+
```
331+
332+
On shutdown, stop the publisher before disposing the connector:
333+
334+
```ruby
335+
publisher.stop
336+
connector.dispose
337+
```
338+
339+
#### Customising CloudWatch dimensions and namespace
340+
341+
Use `MysqlFramework::Stats::DimensionMap` to configure the CloudWatch namespace and dimensions. Each attribute falls back to the corresponding environment variable when not set explicitly:
342+
343+
| Attribute | ENV fallback | CloudWatch dimension |
344+
|---|---|---|
345+
| `service_name` | `SERVICE_NAME` | `ServiceName` |
346+
| `application` | `APPLICATION` | `Application` |
347+
| `environment` | `ENVIRONMENT` | `Environment` |
348+
| `landscape` | `LANDSCAPE` | `Landscape` |
349+
| `namespace` | `AWS_METRICS_NAMESPACE` | (namespace, default: `MysqlFramework`) |
350+
351+
```ruby
352+
dimension_map = MysqlFramework::Stats::DimensionMap.new(
353+
service_name: 'my-service',
354+
application: 'my-app',
355+
environment: 'production',
356+
landscape: 'us-east',
357+
namespace: 'MyCompany/MySQL'
358+
)
359+
360+
publisher = MysqlFramework::Stats::AwsMetricPublisher.new(
361+
connector: connector,
362+
dimension_map: dimension_map,
363+
publish_interval: 60
364+
)
365+
publisher.start
366+
```
367+
287368
### MysqlFramework::SqlCondition
288369

289370
A representation of a MySQL Condition for a column. Created automatically by SqlColumn

docker-compose.yml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
version: '2.1'
2-
31
services:
42
test-runner:
5-
image: ruby:2.4
3+
image: ruby:4.0
64
working_dir: /usr/src/app
75
container_name: test-runner
86
command: sh -c "while true; do echo 'Container is running..'; sleep 5; done"
@@ -21,7 +19,7 @@ services:
2119

2220
test-mysql:
2321
container_name: test-mysql
24-
image: mysql:5.7
22+
image: mysql:8
2523
restart: always
2624
environment:
2725
MYSQL_ROOT_PASSWORD: admin

0 commit comments

Comments
 (0)