Skip to content

Commit f8f89f7

Browse files
authored
Merge pull request #38 from taskiq-python/chore-add-benchmarks
chore: add benchmarks wth codspeed
2 parents 2b606bb + 8e12d57 commit f8f89f7

8 files changed

Lines changed: 524 additions & 220 deletions

File tree

‎.github/workflows/codspeed.yml‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: CodSpeed
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
paths-ignore:
7+
- 'docs/**'
8+
- '**/*.md'
9+
pull_request:
10+
branches: [ main ]
11+
paths-ignore:
12+
- 'docs/**'
13+
- '**/*.md'
14+
15+
permissions:
16+
contents: read
17+
id-token: write # required for OIDC authentication with CodSpeed
18+
19+
jobs:
20+
benchmarks:
21+
name: Benchmarks
22+
runs-on: ubuntu-latest
23+
services:
24+
ministack:
25+
image: ministackorg/ministack:1.3.53
26+
ports:
27+
- 4566:4566
28+
env:
29+
AWS_DEFAULT_REGION: us-east-1
30+
GATEWAY_PORT: 4566
31+
MINISTACK_ACCOUNT_ID: "000000000000"
32+
MINISTACK_REGION: us-east-1
33+
LOG_LEVEL: INFO
34+
steps:
35+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
36+
with:
37+
persist-credentials: false
38+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
39+
with:
40+
python-version: "3.13"
41+
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
42+
with:
43+
enable-cache: true
44+
version: "latest"
45+
- name: Install dependencies
46+
run: uv sync --all-extras
47+
- name: Run the benchmarks
48+
uses: CodSpeedHQ/action@4296e51e7041e24dadb86d1d6e8b9320d223dbe8 # v5.0.3
49+
with:
50+
mode: simulation
51+
run: uv run pytest tests/ --codspeed

‎docker-compose.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ services:
1212
LOG_LEVEL: INFO
1313
PERSIST_STATE: "1"
1414
healthcheck:
15-
test: ["CMD", "curl", "-f", "http://localhost:4566/_ministack/health"]
15+
test: ["CMD", "busybox", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:4566/_ministack/health"]
1616
interval: 2s
1717
timeout: 5s
1818
retries: 30

‎pyproject.toml‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,20 @@ dev = [
4444
{include-group = "lint"},
4545
{include-group = "test"},
4646
{include-group = "types"},
47-
"prek>=0.4.3",
47+
"prek>=0.5.0",
4848
]
4949
test = [
5050
"pytest>=9.0.3",
5151
"pytest-asyncio>=0.23.8",
52+
"pytest-codspeed>=5.0.3",
5253
]
5354
lint = [
5455
"bandit>=1.9.4",
55-
"ruff>=0.15.15",
56-
"zizmor>=1.25.2",
56+
"ruff>=0.16.5",
57+
"zizmor>=1.29.0",
5758
]
5859
types = [
59-
"mypy>=2.1.0",
60+
"mypy>=2.3.1",
6061
"types-aiobotocore[essential]>=3.7.0",
6162
]
6263
examples = [
@@ -82,6 +83,7 @@ asyncio_mode = "auto"
8283
markers = [
8384
"unit: marks unit tests",
8485
"integration: marks tests with real infrastructure env",
86+
"benchmark: marks performance benchmarks (run with --codspeed)",
8587
]
8688

8789
[tool.coverage.report]
@@ -129,6 +131,7 @@ ignore = [
129131

130132
"EM101",
131133
"TRY003",
134+
"CPY001",
132135
]
133136

134137
[tool.ruff.lint.per-file-ignores]

‎tests/benchmarks/__init__.py‎

Whitespace-only changes.

‎tests/benchmarks/conftest.py‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from collections.abc import AsyncGenerator
2+
3+
import pytest
4+
from taskiq.acks import AckableMessage
5+
from taskiq.result import TaskiqResult
6+
7+
from tests.conftest import AWSCredentials, _queue_name_from_url
8+
9+
from taskiq_sqs import S3ResultBackend, SQSBroker
10+
11+
12+
BATCH_SIZE = 10
13+
RESULT_TASK_ID = "benchmark-task"
14+
15+
16+
async def ack(message: AckableMessage) -> None:
17+
result = message.ack()
18+
if result is not None:
19+
await result
20+
21+
22+
@pytest.fixture
23+
async def bench_broker(
24+
aws_credentials: AWSCredentials,
25+
sqs_queue: str,
26+
) -> AsyncGenerator[SQSBroker, None]:
27+
"""The shared `sqs_broker`, but tuned for measuring a round-trip.
28+
29+
Two settings differ from the default broker: long polling, because with `wait_time_seconds=0` a receive returns
30+
immediately and usually empty, so a round-trip would measure idle polls; and a batch size that lets a single
31+
`ReceiveMessage` call serve the whole batch.
32+
"""
33+
broker = SQSBroker(
34+
queue_name=_queue_name_from_url(sqs_queue),
35+
wait_time_seconds=1,
36+
max_number_of_messages=BATCH_SIZE,
37+
**aws_credentials,
38+
)
39+
await broker.startup()
40+
yield broker
41+
await broker.shutdown()
42+
43+
44+
@pytest.fixture
45+
async def listener(bench_broker: SQSBroker) -> AsyncGenerator[AsyncGenerator[AckableMessage, None], None]:
46+
"""A single `listen()` generator, created outside the measured test body."""
47+
messages = bench_broker.listen()
48+
yield messages
49+
await messages.aclose()
50+
51+
52+
@pytest.fixture
53+
async def stored_result(s3_backend: S3ResultBackend[str]) -> TaskiqResult[str]:
54+
"""A result already written to the bucket, so read benchmarks only measure reads."""
55+
result: TaskiqResult[str] = TaskiqResult(
56+
is_err=False,
57+
return_value="benchmark",
58+
execution_time=0.1,
59+
log=None,
60+
)
61+
await s3_backend.set_result(RESULT_TASK_ID, result)
62+
return result

‎tests/benchmarks/test_broker.py‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from collections.abc import AsyncGenerator
2+
3+
import pytest
4+
from taskiq import BrokerMessage
5+
from taskiq.acks import AckableMessage
6+
7+
from .conftest import BATCH_SIZE, ack
8+
from taskiq_sqs import SQSBroker
9+
10+
11+
@pytest.mark.benchmark
12+
async def test_build_kick_kwargs(bench_broker: SQSBroker, broker_message: BrokerMessage) -> None:
13+
await bench_broker._build_kick_kwargs(broker_message)
14+
15+
16+
@pytest.mark.benchmark
17+
async def test_kick(bench_broker: SQSBroker, broker_message: BrokerMessage) -> None:
18+
await bench_broker.kick(broker_message)
19+
20+
21+
@pytest.mark.benchmark
22+
async def test_kick_and_listen_roundtrip(
23+
bench_broker: SQSBroker,
24+
broker_message: BrokerMessage,
25+
listener: AsyncGenerator[AckableMessage, None],
26+
) -> None:
27+
await bench_broker.kick(broker_message)
28+
message = await anext(listener)
29+
await ack(message)
30+
31+
32+
@pytest.mark.benchmark
33+
async def test_kick_and_listen_roundtrip_batch(
34+
bench_broker: SQSBroker,
35+
broker_message: BrokerMessage,
36+
listener: AsyncGenerator[AckableMessage, None],
37+
) -> None:
38+
for _ in range(BATCH_SIZE):
39+
await bench_broker.kick(broker_message)
40+
# SQS may return fewer messages than requested per call, so keep pulling until the whole batch is back.
41+
for _ in range(BATCH_SIZE):
42+
message = await anext(listener)
43+
await ack(message)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import pytest
2+
from taskiq.result import TaskiqResult
3+
4+
from tests.benchmarks.conftest import RESULT_TASK_ID
5+
6+
from taskiq_sqs import S3ResultBackend
7+
8+
9+
@pytest.mark.benchmark
10+
async def test_set_result(s3_backend: S3ResultBackend[str], stored_result: TaskiqResult[str]) -> None:
11+
await s3_backend.set_result(RESULT_TASK_ID, stored_result)
12+
13+
14+
@pytest.mark.benchmark
15+
async def test_get_result(
16+
s3_backend: S3ResultBackend[str],
17+
stored_result: TaskiqResult[str], # noqa: ARG001
18+
) -> None:
19+
await s3_backend.get_result(RESULT_TASK_ID)
20+
21+
22+
@pytest.mark.benchmark
23+
async def test_is_result_ready(
24+
s3_backend: S3ResultBackend[str],
25+
stored_result: TaskiqResult[str], # noqa: ARG001
26+
) -> None:
27+
await s3_backend.is_result_ready(RESULT_TASK_ID)

0 commit comments

Comments
 (0)