Skip to content

Commit a4dcf9e

Browse files
authored
Merge pull request #7 from bluedynamics/fix/s3-max-pool-connections
fix: set explicit max_pool_connections on boto3 S3 client (closes #6)
2 parents b920080 + ea29efc commit a4dcf9e

3 files changed

Lines changed: 57 additions & 1 deletion

File tree

CHANGES.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# Changelog
22

3+
## 0.4.3 (unreleased)
4+
5+
- Fix: boto3 S3 client is now created with an explicit
6+
`max_pool_connections` via `botocore.Config` (default 50, overridable
7+
via `PGTHUMBOR_S3_MAX_POOL_CONNECTIONS`). The boto3 default of 10
8+
caused urllib3 pool-full warnings and connection churn under normal
9+
Thumbor load (30 thumbnails per listing page times active visitors),
10+
which in turn correlated with intermittent Thumbor 400s on aaf-6
11+
prod. 50 covers `asyncio.to_thread`'s default executor
12+
(`min(32, cpu+4)`) plus headroom.
13+
Fixes [#6](https://github.com/bluedynamics/zodb-pgjsonb-thumborblobloader/issues/6).
14+
315
## 0.4.2 (2026-04-02)
416

517
- Fix: S3 loader now reads `PGTHUMBOR_S3_ACCESS_KEY` and `PGTHUMBOR_S3_SECRET_KEY`

src/zodb_pgjsonb_thumborblobloader/s3.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from __future__ import annotations
88

9+
from botocore.config import Config
910
from botocore.exceptions import ClientError
1011

1112
import asyncio
@@ -15,6 +16,13 @@
1516

1617
logger = logging.getLogger(__name__)
1718

19+
# boto3's default urllib3 max_pool_connections is 10 — too low for
20+
# concurrent Thumbor image loads (30 thumbnails per listing page times
21+
# active visitors easily exceeds it, causing connection discard/reopen
22+
# churn and handshake-failure-induced 400s). 50 covers
23+
# asyncio.to_thread's default executor (min(32, cpu+4)) plus headroom.
24+
DEFAULT_MAX_POOL_CONNECTIONS = 50
25+
1826
_s3_client = None
1927
_s3_config: tuple[str, str, str] | None = None
2028

@@ -28,7 +36,15 @@ def _get_s3_client(bucket: str, region: str, endpoint: str = ""):
2836

2937
import boto3
3038

31-
kwargs: dict = {"region_name": region}
39+
max_pool = int(
40+
os.environ.get(
41+
"PGTHUMBOR_S3_MAX_POOL_CONNECTIONS", str(DEFAULT_MAX_POOL_CONNECTIONS)
42+
)
43+
)
44+
kwargs: dict = {
45+
"region_name": region,
46+
"config": Config(max_pool_connections=max_pool),
47+
}
3248
if endpoint:
3349
kwargs["endpoint_url"] = endpoint
3450
access_key = os.environ.get("PGTHUMBOR_S3_ACCESS_KEY", "")

tests/test_loader_s3.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,34 @@ def _reset_s3_client():
3030
s3_mod._s3_config = None
3131

3232

33+
class TestS3ClientConfig:
34+
"""Verify the boto3 client is created with an explicit max_pool_connections."""
35+
36+
def test_default_max_pool_connections(self, monkeypatch):
37+
from unittest.mock import patch
38+
from zodb_pgjsonb_thumborblobloader import s3 as s3_mod
39+
40+
monkeypatch.delenv("PGTHUMBOR_S3_MAX_POOL_CONNECTIONS", raising=False)
41+
42+
with patch("boto3.client") as mock_client:
43+
s3_mod._get_s3_client("bucket", "us-east-1")
44+
45+
kwargs = mock_client.call_args.kwargs
46+
assert kwargs["config"].max_pool_connections == 50
47+
48+
def test_env_override_max_pool_connections(self, monkeypatch):
49+
from unittest.mock import patch
50+
from zodb_pgjsonb_thumborblobloader import s3 as s3_mod
51+
52+
monkeypatch.setenv("PGTHUMBOR_S3_MAX_POOL_CONNECTIONS", "128")
53+
54+
with patch("boto3.client") as mock_client:
55+
s3_mod._get_s3_client("bucket", "us-east-1")
56+
57+
kwargs = mock_client.call_args.kwargs
58+
assert kwargs["config"].max_pool_connections == 128
59+
60+
3361
class TestLoadS3Blob:
3462
"""Test loading blobs from S3 via s3_key."""
3563

0 commit comments

Comments
 (0)