Skip to content

Commit eb6218f

Browse files
Testing consumer directly
1 parent 8088013 commit eb6218f

10 files changed

Lines changed: 160 additions & 9 deletions

File tree

app/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class Settings(BaseSettings):
3737

3838
# rabbit mq queue/exchange settings end
3939
app_rabbit_inbound_queue: str = "in-bound-queue"
40+
app_rabbit_inbound_concurrency: int = 10
4041
app_rabbit_inbound_exchange: str = "in-bound-ex"
4142
app_rabbit_inbound_routing_key: str = "in-bound-rk"
4243

app/consumer/__init__.py

Whitespace-only changes.

app/consumer/inbound_consumer.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import asyncio
2+
from _asyncio import Task
3+
4+
import structlog
5+
from aio_pika.abc import AbstractIncomingMessage
6+
from app.rabbit_mq.rabbit_mq_client import get_connection
7+
from app.config import settings
8+
9+
10+
async def process_inbound_message_task(message: AbstractIncomingMessage):
11+
logger = structlog.get_logger()
12+
async with message.process(): # Automatically ACKs if no exception occurs
13+
payload_string: str = message.body.decode()
14+
logger.info(
15+
f"Processed inbound message {payload_string}", payload=payload_string
16+
)
17+
18+
19+
async def start_inbound_consumer() -> Task[str]:
20+
logger = structlog.get_logger()
21+
# Create a dedicated channel for this consumer group
22+
channel = await get_connection().channel()
23+
# MAX CONCURRENCY SETTING: Limit unacknowledged messages on this channel
24+
await channel.set_qos(prefetch_count=settings.app_rabbit_inbound_concurrency)
25+
queue = await channel.get_queue(settings.app_rabbit_inbound_queue)
26+
# Fire-and-forget the consumer loop into the background loop
27+
task: Task[str] = asyncio.create_task(queue.consume(process_inbound_message_task))
28+
logger.info("Started inbound message consumer")
29+
return task

app/main.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import asyncio
2+
from asyncio import Task
3+
14
import structlog
25
import uvicorn
36
from fastapi import FastAPI
@@ -34,6 +37,7 @@
3437
from app.routers.dependency import router as dependency_router
3538
from app.routers.external_faq import router as external_faq_router
3639
from app.routers.sample_form import router as sample_form_router
40+
from app.consumer.inbound_consumer import start_inbound_consumer
3741

3842
# Initialize logging at application startup
3943
setup_logging(json_logs=settings.app_json_logs, db_logs=settings.app_db_log_sql)
@@ -70,12 +74,21 @@ async def lifespan(_app: FastAPI):
7074
apply_db_migrations()
7175
if settings.app_db_enabled:
7276
await database.connect()
77+
consumer_tasks: list[Task[str]] = []
7378
if settings.app_rabbit_mq_connect:
7479
await open_rabbit_connection()
7580
if not settings.app_rabbit_mq_passive:
7681
await configure_rabbitmq()
82+
consumer_tasks.append(await start_inbound_consumer())
7783
yield
7884
if settings.app_rabbit_mq_connect:
85+
# Cancel any long-running active consumer loop tasks
86+
for task in consumer_tasks:
87+
task.cancel()
88+
try:
89+
await task
90+
except asyncio.CancelledError:
91+
pass
7992
await close_rabbit_connection()
8093
if engine is not None:
8194
engine.dispose()

app/producer/__init__.py

Whitespace-only changes.

app/producer/inbound_producer.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import json
2+
3+
import aio_pika
4+
import structlog
5+
from fastapi.encoders import jsonable_encoder
6+
7+
from app.rabbit_mq.rabbit_mq_client import get_connection
8+
from app.model.response.sample import Sample
9+
from app.config import settings
10+
11+
12+
async def publish_to_inbound(samples: list[Sample]):
13+
logger = structlog.get_logger()
14+
async with get_connection().channel() as channel:
15+
exchange = await channel.get_exchange(settings.app_rabbit_inbound_exchange)
16+
payload_string: str = json.dumps(jsonable_encoder(samples))
17+
message = aio_pika.Message(
18+
body=payload_string.encode(),
19+
delivery_mode=aio_pika.DeliveryMode.PERSISTENT, # Survives broker restart
20+
)
21+
await exchange.publish(
22+
message, routing_key=settings.app_rabbit_inbound_routing_key
23+
)
24+
logger.info(
25+
f"Published to inbound queue {payload_string}", payload=payload_string
26+
)
27+
return

app/rabbit_mq/rabbit_mq_client.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import aio_pika
44
import structlog
5-
from aio_pika.abc import AbstractRobustConnection, AbstractChannel
5+
from aio_pika.abc import AbstractRobustConnection
66

77
from app.config import settings
88

@@ -65,9 +65,3 @@ def get_connection() -> AbstractRobustConnection:
6565
"Ensure 'set_connection' was called during app startup."
6666
)
6767
return _connection
68-
69-
70-
async def get_channel() -> AbstractChannel:
71-
"""Utility helper to quickly grab an isolated channel off the global connection."""
72-
conn = get_connection()
73-
return await conn.channel()

app/rabbit_mq/rabbit_mq_initialisation.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
import structlog
33

44
from app.config import settings
5-
from app.rabbit_mq.rabbit_mq_client import get_channel
5+
from app.rabbit_mq.rabbit_mq_client import get_connection
66

77

88
async def configure_rabbitmq():
99
logger = structlog.get_logger()
1010

11-
async with await get_channel() as channel:
11+
async with await get_connection().channel() as channel:
1212
logger.info("Configuring queues and exchanges")
1313

1414
inbound_queue = await channel.declare_queue(

tests_integration/consumer/__init__.py

Whitespace-only changes.
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import datetime
2+
import json
3+
import uuid
4+
from decimal import Decimal
5+
from typing import MutableMapping, Any
6+
7+
import aio_pika
8+
import pytest
9+
import structlog
10+
from assertpy import assert_that
11+
from fastapi.encoders import jsonable_encoder
12+
from fastapi.testclient import TestClient
13+
from tenacity import Retrying, stop_after_delay, wait_fixed
14+
15+
16+
from app.model.response.sample import Sample
17+
18+
19+
@pytest.mark.asyncio
20+
async def test_inbound_consumer_handles_message_successfully(
21+
test_client: TestClient,
22+
captured_logs: list[MutableMapping[str, Any]],
23+
):
24+
sample1: Sample = Sample(
25+
id=uuid.uuid4(),
26+
username=f"user-{uuid.uuid4()}",
27+
bool_field=True,
28+
float_field=2.0,
29+
decimal_field=Decimal("3.1"),
30+
created_datetime=datetime.datetime.now(datetime.UTC),
31+
updated_datetime=datetime.datetime.now(datetime.UTC),
32+
version=1,
33+
)
34+
sample2: Sample = Sample(
35+
id=uuid.uuid4(),
36+
username=f"user-{uuid.uuid4()}",
37+
bool_field=None,
38+
float_field=None,
39+
decimal_field=None,
40+
created_datetime=datetime.datetime.now(datetime.UTC),
41+
updated_datetime=datetime.datetime.now(datetime.UTC),
42+
version=1,
43+
)
44+
samples: list[Sample] = [sample1, sample2]
45+
46+
await publish_to_inbound(samples)
47+
48+
for attempt in Retrying(
49+
stop=stop_after_delay(5), wait=wait_fixed(0.5), reraise=True
50+
):
51+
with attempt:
52+
assert_that(len(captured_logs)).is_greater_than(0)
53+
inbound_consumer_logs = list(
54+
filter(
55+
lambda entry: str(entry["event"]).startswith(
56+
"Processed inbound message"
57+
),
58+
captured_logs,
59+
)
60+
)
61+
assert_that(inbound_consumer_logs).is_length(1)
62+
63+
64+
async def publish_to_inbound(samples: list[Sample]):
65+
logger = structlog.get_logger()
66+
from app.config import settings
67+
68+
rabbit_mq_url = (
69+
f"amqp://{settings.app_rabbit_mq_username}:{settings.app_rabbit_mq_password}"
70+
f"@{settings.app_rabbit_mq_host}:{settings.app_rabbit_mq_port}/{settings.app_rabbit_mq_vhost}"
71+
)
72+
73+
async with await aio_pika.connect_robust(rabbit_mq_url) as connection:
74+
async with connection.channel() as channel:
75+
exchange = await channel.get_exchange(settings.app_rabbit_inbound_exchange)
76+
payload_string: str = json.dumps(jsonable_encoder(samples))
77+
message = aio_pika.Message(
78+
body=payload_string.encode(),
79+
delivery_mode=aio_pika.DeliveryMode.PERSISTENT,
80+
)
81+
await exchange.publish(
82+
message, routing_key=settings.app_rabbit_inbound_routing_key
83+
)
84+
logger.info(
85+
f"Published to inbound queue {payload_string}", payload=payload_string
86+
)
87+
return

0 commit comments

Comments
 (0)