spakky-outbox
Spakky Framework를 위한 트랜잭셔널 Outbox 패턴 패키지입니다.
IEventBus를@Primaryoutbox bus로 교체해 이벤트 저장과 외부 전송을 분리합니다.
설치
pip install spakky-outbox "spakky-sqlalchemy[outbox]"
참고:
spakky-outbox는 core abstraction을 제공하고,spakky-sqlalchemy는spakky.contributions.spakky.outboxcontribution으로 Outbox storage 구현체를 제공합니다.spakky-sqlalchemy단독 설치는spakky-outbox를 끌어오지 않습니다.
주요 기능
- 트랜잭셔널 Outbox: 이벤트를 비즈니스 데이터와 원자적으로 저장
- 자동 relay: background relay가 이벤트를 외부 transport(Kafka, RabbitMQ)로 발행
- 재시도 지원: transport가 영구적이고 특정 레코드에 귀속되는 거부를 확정한 경우에만 설정 가능한 한도 내에서 재시도하며, connection·timeout·queue 등 transport 자체 장애는 메시지별 retry 예산을 소모하지 않음
- 다중 인스턴스 안전성: 원자적 claim으로 중복 발행 방지
- 파티션 키 단위 순서 보전: 파티션 키를 통째로 claim하고, 영구 거부되어 재시도 대기 중인 키의 후속 메시지를 보류
- 발행 포기 상태: 재시도를 소진한 메시지를
abandoned처리하여 키가 무기한 막히지 않게 함
사용법
1. 애플리케이션에서 플러그인 로드
from spakky.core.application.application import SpakkyApplication
from spakky.core.application.application_context import ApplicationContext
app = (
SpakkyApplication(ApplicationContext())
.load_plugins() # outbox와 sqlalchemy plugin을 자동 로드
.scan()
.start()
)
2. Use case에서 이벤트 발행
IAsyncEventPublisher로 발행된 이벤트는 자동으로 라우팅됩니다.
AbstractDomainEvent→ in-process dispatchAbstractIntegrationEvent→IEventBus(Outbox가@Primary로 가로챔)
from spakky.core.common.mutability import immutable
from spakky.core.stereotype.usecase import UseCase
from spakky.data.aspects.transactional import Transactional
from spakky.domain.models.event import AbstractIntegrationEvent
from spakky.event.event_publisher import IAsyncEventPublisher
@immutable
class OrderCreatedEvent(AbstractIntegrationEvent):
order_id: int
customer_id: int
@UseCase()
class CreateOrderUseCase:
def __init__(self, event_publisher: IAsyncEventPublisher) -> None:
self._event_publisher = event_publisher
@Transactional()
async def execute(self, command: CreateOrderCommand) -> Order:
order = Order.create(...)
# 이벤트는 order와 같은 transaction에 저장됨
await self._event_publisher.publish(
OrderCreatedEvent(order_id=order.id, customer_id=command.customer_id)
)
return order
3. 환경변수로 설정
| 변수 | 기본값 | 설명 |
|---|---|---|
SPAKKY_OUTBOX__POLLING_INTERVAL_SECONDS |
1.0 |
Relay polling interval |
SPAKKY_OUTBOX__BATCH_SIZE |
100 |
batch당 message 수 |
SPAKKY_OUTBOX__MAX_RETRY_COUNT |
5 |
포기 전 최대 retry 횟수 |
SPAKKY_OUTBOX__CLAIM_TIMEOUT_SECONDS |
300.0 |
crash recovery용 claim 만료 |
구성 요소
| 구성 요소 | 설명 |
|---|---|
IOutboxStorage / IAsyncOutboxStorage |
Outbox message storage port |
OutboxEventBus / AsyncOutboxEventBus |
Outbox pattern용 event bus 경계(@Primary가 DirectEventBus 대체) |
OutboxRelayBackgroundService / AsyncOutboxRelayBackgroundService |
background relay service(polling 및 send) |
OutboxConfig |
환경변수 기반 설정 |
OutboxMessage |
Outbox message model |
OutboxEventBus와 AsyncOutboxEventBus는 메시지를 저장할 때 DirectEventBus와 같은 outbound metadata 정책을 사용합니다. 기존 tracing header는 보존하고, auth snapshot 전파가 활성화된 request-scope에서는 spakky.auth.context_snapshot에 signed AuthContextSnapshot envelope를 저장합니다. Raw bearer token은 Outbox headers에 저장하지 않습니다.
커스텀 Storage 구현
custom storage backend를 구현하려면 다음 interface를 구현합니다.
from spakky.outbox.ports.storage import IAsyncOutboxStorage
from spakky.outbox.common.message import OutboxMessage
class MyCustomStorage(IAsyncOutboxStorage):
async def save(self, message: OutboxMessage) -> None:
# 현재 transaction 안에서 저장
...
async def fetch_pending(self, limit: int, max_retry: int) -> list[OutboxMessage]:
# pending message를 atomic claim 후 반환.
# 파티션 키가 있는 메시지는 그 키에서 아직 발행되지도 포기되지도 않은
# 가장 오래된 메시지를 함께 claim할 때만 넘긴다 — 그러지 않으면 릴레이
# 인스턴스 둘이 같은 키를 병렬 발행하여 키가 보장하려던 순서를 잃는다.
...
async def mark_published(self, message_id: UUID) -> None:
...
async def increment_retry(self, message_id: UUID) -> None:
...
async def mark_abandoned(self, message_id: UUID) -> None:
# 재시도를 소진한 메시지를 발행 대기열에서 빼되 레코드는 남긴다.
# 이 상태가 없으면 그 메시지가 같은 키의 후속 메시지를 영구히 막는다.
...
개발 검증
패키지 단위 검증은 해당 패키지 디렉토리에서 실행합니다.
uv run ruff format .
uv run ruff check .
uv run pyrefly check
uv run pytest
pytest는 각 패키지 pyproject.toml의 coverage 설정을 사용합니다.
라이선스
MIT License
Metadata
Release files for spakky-outbox 8.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| spakky_outbox-8.1.0.tar.gz | 8.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| spakky_outbox-8.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 21.3 kB
Release files / spakky_outbox-8.1.0.tar.gz
| Download URL | spakky_outbox-8.1.0.tar.gz |
|---|---|
| Size | 8.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
15153882c1dd2470e8e23af0a15fdd952428e6f270f7b68cc268a14117df1479
|
|
BLAKE2b-256 checksum How to use checksums |
72b98f095ea9e75c80678ef6021844d2c5013fdd4611efd2f36547500e5c7085
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 22, 2026.
Transparency logRelease files / spakky_outbox-8.1.0-py3-none-any.whl
| Download URL | spakky_outbox-8.1.0-py3-none-any.whl |
|---|---|
| Size | 12.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b97dfe3d1a6dd4f36fce9d6d8e8d1a22fbbdba75fe13a7aec24513fe51e1aa82
|
|
BLAKE2b-256 checksum How to use checksums |
57b2623316ed09e38e071ae61fb6f4c7010fe3e5cd279b65160e72e3f063f604
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 22, 2026.
Transparency log