Skip to main content

hexastack-events

hexastack-events

CNCF CloudEvents 1.0 serialization, Transactional Outbox pattern, NATS JetStream distributed event bus, and janus async-sync thread bridge for Hexastack.

Part of the Hexastack Framework.

PyPI: hexastack-events Python 3.13+ Coverage License: Apache 2.0


1. Architectural Overview

hexastack-events extends Hexastack's in-process CQRS buses with enterprise distributed event streaming, reliability patterns, and async-sync thread bridging:

  1. CNCF CloudEvents 1.0 Protocol: Automatic serialization and deserialization of domain Event models into standardized CloudEvents JSON envelopes with W3C correlation ID and multi-tenant partitioning.
  2. Transactional Outbox Engine: Guarantees at-least-once delivery by staging uncommitted domain events in an OutboxStoragePort within the same transaction as business state mutations.
  3. Partitioned Stream Engine (StreamPort, InMemoryStreamAdapter): High-throughput partitioned event stream ingestion with deterministic partition key routing, consumer group offset tracking, and async/sync read slices.
  4. Distributed Task Queue & Worker Leasing (TaskQueuePort, InMemoryTaskQueueAdapter): Background task execution with active worker leasing, priority-ordered execution, lease renewals/heartbeats, failure retries, and dead-letter queue routing.
  5. Dual Relay Engines with Multi-Process Locking:
    • Native Asyncio (AsyncioOutboxRelay): In-process background task with zero external dependencies, supporting optional LockPort / filelock coordination.
    • Huey Worker (HueyOutboxRelay): Multi-process worker executing outbox polling in separate worker nodes (pip install hexastack-events[huey]), supporting optional LockPort / filelock mutual exclusion.
  6. Relational Database Outbox Storage (SqlAlchemyOutboxStorage): Pluggable storage adapter supporting SQLAlchemy tables and transactions (pip install hexastack-events[sql]).
  7. NATS JetStream Distributed Event Bus (NatsJetStreamEventBusAdapter): Production-grade at-least-once delivery via NATS JetStream — durable push consumers, WorkQueue stream retention, dead-letter routing, and msgspec zero-copy encoding (pip install hexastack-events[nats]).
  8. Janus Async-Sync Thread Bridge (JanusEventChannel, JanusCommandQueue[T]): Thread-safe ↔ asyncio-safe queue bridges enabling synchronous OS threads (gRPC servicers, CLI handlers) to enqueue events and commands for dispatch by async event-loop consumers (pip install hexastack-events[janus]).
  9. Multi-Process Concurrency Protection (filelock): Inter-process file locking prevents competing workers or pollers from creating lock contention or duplicate event dispatches on SQLite and filesystem backends (pip install hexastack-events[filelock]).
graph TD
    CMD["CQRS Command Handler"]
    UOW["Unit of Work Transaction"]
    OUTBOX_MW["OutboxCaptureMiddleware"]
    OUTBOX_STORE["OutboxStoragePort\n(SQLAlchemy / DB Outbox Table)"]
    RELAY["OutboxRelayPort\n(Asyncio / Huey Worker)"]
    BUS["DistributedEventBusPort"]
    STREAM["StreamPort / AsyncStreamPort\n(Partitioned Ingestion)"]
    TASKS["TaskQueuePort / AsyncTaskQueuePort\n(Worker Leased Tasks)"]

    subgraph Brokers["Broker & Stream Adapters"]
        INMEM["InMemoryDistributedEventBus\n(Testing / Local Dev)"]
        NATS["NatsJetStreamEventBusAdapter\n(NATS JetStream — pip install hexastack-events[nats])"]
        STREAM_INMEM["InMemoryStreamAdapter\n(Partitioned FIFO Streams)"]
    end

    subgraph TaskQueues["Task Queue Adapters"]
        TASK_INMEM["InMemoryTaskQueueAdapter\n(Priority Leases & DLQ)"]
    end

    subgraph ThreadBridge["Async-Sync Thread Bridge"]
        JANUS_EV["JanusEventChannel\n(sync_put / drain)"]
        JANUS_CMD["JanusCommandQueue[T]\n(sync_put / async_get)"]
    end

    CMD --> UOW
    CMD --> OUTBOX_MW
    OUTBOX_MW -->|Stages Record| OUTBOX_STORE
    UOW -->|Atomic Commit| OUTBOX_STORE
    RELAY -->|Polls Pending| OUTBOX_STORE
    RELAY -->|Publishes CloudEvent| BUS
    BUS --> INMEM
    BUS --> NATS

    STREAM --> STREAM_INMEM
    TASKS --> TASK_INMEM

    GRPC["gRPC Sync Thread"] -->|sync_put| JANUS_EV
    JANUS_EV -->|drain → handler| BUS
    GRPC -->|sync_put| JANUS_CMD
    JANUS_CMD -->|async_get| CMD

2. Key Exports & Package Structure

hexastack_events/
├── domain/
│   ├── models.py        # CloudEventEnvelope, OutboxRecord, OutboxStatus
│   ├── streams.py       # StreamMessage, StreamPartitionOffset
│   ├── tasks.py         # TaskRecord, TaskState
│   ├── serialization.py # MsgspecEnvelopeSerializer, encode/decode helpers
│   ├── exceptions.py    # EventError, EventDeliveryError, EventSerializationError, …
│   └── context.py       # EventContext (correlation, tenant)
├── ports/
│   ├── buses.py         # DistributedEventBusPort
│   ├── streams.py       # StreamPort, AsyncStreamPort
│   ├── tasks.py         # TaskQueuePort, AsyncTaskQueuePort
│   └── outbox.py        # OutboxStoragePort, OutboxRelayPort
├── adapters/
│   ├── cloudevents/     # to_cloudevent, from_cloudevent, cloudevent_to_json/dict
│   ├── outbox/          # AsyncioOutboxRelay, HueyOutboxRelay, InMemoryOutboxStorage, SqlAlchemyOutboxStorage
│   ├── streams/         # InMemoryStreamAdapter
│   ├── tasks/           # InMemoryTaskQueueAdapter
│   └── buses/
│       ├── in_memory.py     # InMemoryDistributedEventBus (testing)
│       ├── nats.py          # NatsJetStreamEventBusAdapter [nats]
│       └── janus_bridge.py  # JanusEventChannel, JanusCommandQueue[T] [janus]
└── infra/
    ├── bootstrap.py     # EventsBootstrapper (order=22)
    ├── config.py        # HexastackEventsConfig
    └── middleware.py    # OutboxCaptureMiddleware

High-Throughput msgspec Serialization

hexastack-events includes zero-copy serialization engines powered by msgspec:

  • encode_cloudevent_bytes(envelope) / decode_cloudevent_bytes(data): Optimized UTF-8 JSON encoding.
  • encode_cloudevent_msgpack(envelope) / decode_cloudevent_msgpack(data): Binary MessagePack encoding.
  • MsgspecEnvelopeSerializer: Reusable wire serialization adapter (used internally by NatsJetStreamEventBusAdapter).

3. Quickstart & Installation

# Core CloudEvents & Asyncio Outbox Relay (zero extra dependencies)
pip install hexastack-events

# With SQLAlchemy relational database outbox storage
pip install "hexastack-events[sql]"

# With Huey multi-process worker support
pip install "hexastack-events[huey]"

# With NATS JetStream distributed event bus
pip install "hexastack-events[nats]"

# With janus async-sync thread bridge (usable with any bus adapter)
pip install "hexastack-events[janus]"

# Full stack: NATS + janus bridge
pip install "hexastack-events[nats,janus]"

Configuration (hexastack.toml or pyproject.toml)

[hexastack.events]
source = "billing-service"
relay_mode = "asyncio" # "asyncio", "huey", "manual", "disabled"
poll_interval_seconds = 1.0
batch_size = 50
max_retries = 5
enabled = true

Transactional Outbox Example with SQLAlchemy

from hexastack_core.domain import Event
from hexastack_events.adapters.outbox.sqlalchemy import OutboxEventMixin
from sqlalchemy.orm import DeclarativeBase


class Base(DeclarativeBase):
    pass


class OrderOutboxEvent(Base, OutboxEventMixin):
    __tablename__ = "outbox_events"


class OrderPlacedEvent(Event):
    order_id: str
    total_amount: float
    customer_id: str

4. NATS JetStream Adapter

NatsJetStreamEventBusAdapter implements DistributedEventBusPort backed by NATS JetStream for production at-least-once distributed event delivery.

Design

Property Detail
Lazy import nats-py imported only at call-time; package loads cleanly without [nats] installed
Lazy connect No network I/O until await adapter.connect()
Stream provisioning WorkQueue retention, File storage; calls update_stream if stream already exists
Subject routing {prefix}.{envelope.type} — one NATS subject per CloudEvent type
Wire encoding msgspec JSON (zero-copy, sub-millisecond)
Consumer resilience Durable push consumers; configurable max_deliver + ack_wait; messages nak'd on handler failure
Sync bridge Background asyncio thread loop + run_coroutine_threadsafe — sync callers never block the event loop
Lifecycle async with adapter: context manager for connect/disconnect

Usage

import asyncio
from hexastack_events.adapters.buses.nats import NatsJetStreamEventBusAdapter
from hexastack_events.domain.models import CloudEventEnvelope


async def main():
    adapter = NatsJetStreamEventBusAdapter(
        servers=["nats://localhost:4222"],
        stream_name="my-service",
        subject_prefix="my-service.events",
        max_deliver=5,
        ack_wait_seconds=30.0,
    )

    async with adapter:
        # Publish a CloudEvent envelope
        envelope = CloudEventEnvelope(
            id="evt-001",
            source="order-service",
            type="OrderPlaced",
            time="2026-08-31T00:00:00Z",
            data={"order_id": "o-42", "total": 199.0},
        )
        adapter.publish_envelope(envelope)

        # Subscribe a handler
        def on_order_placed(env: CloudEventEnvelope) -> None:
            print(f"Received: {env.type}{env.data}")

        adapter.subscribe("OrderPlaced", on_order_placed)
        await asyncio.sleep(1)  # let messages arrive


asyncio.run(main())

DI Bootstrap Integration

from hexastack_events.adapters.buses.nats import NatsJetStreamEventBusAdapter
from hexastack_events.ports.buses import DistributedEventBusPort

# In your application bootstrap / DI wiring:
nats_bus = NatsJetStreamEventBusAdapter(servers=["nats://nats:4222"])
container.add_instance(nats_bus, declared_class=DistributedEventBusPort)

5. Janus Async-Sync Thread Bridge

JanusEventChannel and JanusCommandQueue[T] solve the hard problem of getting synchronous OS threads (gRPC servicers, CLI processes, background OS threads) to publish events or dispatch commands into an async event-loop — without nesting event loops or blocking I/O.

sequenceDiagram
    participant GrpcThread as gRPC Sync Thread
    participant Janus as JanusEventChannel
    participant Loop as asyncio Event Loop
    participant Bus as DistributedEventBusPort

    GrpcThread->>Janus: sync_put(envelope)
    Note right of Janus: Thread-safe enqueue
    Loop->>Janus: await async_get() [drain]
    Janus-->>Loop: envelope
    Loop->>Bus: await publish_envelope(envelope)

JanusEventChannel — CloudEvent envelope bridge

import asyncio
from hexastack_events.adapters.buses.janus_bridge import JanusEventChannel
from hexastack_events.domain.models import CloudEventEnvelope


async def start_drain(channel: JanusEventChannel, bus) -> None:
    async def handler(env: CloudEventEnvelope) -> None:
        await bus.async_publish_envelope(env)

    await channel.drain(handler)


async def main():
    channel = JanusEventChannel(maxsize=1000)
    bus = ...  # your DistributedEventBusPort

    # Start the async drainer
    asyncio.create_task(start_drain(channel, bus))

    # In a gRPC sync thread:
    import threading

    def grpc_handler():
        envelope = CloudEventEnvelope(
            id="evt-grpc-1",
            source="grpc-service",
            type="PaymentReceived",
            time="2026-08-31T00:00:00Z",
            data={"amount": 50.0},
        )
        channel.sync_put(envelope)  # non-blocking, thread-safe

    threading.Thread(target=grpc_handler).start()
    await asyncio.sleep(0.1)

JanusCommandQueue[T] — Generic CQRS command bridge

import asyncio
import threading
from hexastack_events.adapters.buses.janus_bridge import JanusCommandQueue
from my_app.commands import CreateOrderCommand


queue: JanusCommandQueue[CreateOrderCommand] = JanusCommandQueue()


async def dispatcher(bus) -> None:
    """Drain commands from sync threads into the async command bus."""

    async def dispatch(cmd: CreateOrderCommand) -> None:
        await bus.dispatch(cmd)

    await queue.drain(dispatch)


# In a sync gRPC servicer:
def grpc_create_order(request, context):
    queue.sync_put(CreateOrderCommand(order_id=request.order_id))

6. Adapter Comparison

Adapter Install Use Case
InMemoryDistributedEventBus (core) Unit tests, local development
InMemoryStreamAdapter (core) Partitioned FIFO event stream buffer, deterministic hashing, offset tracking
InMemoryTaskQueueAdapter (core) Worker-leased background task execution, priorities, and DLQ
NatsJetStreamEventBusAdapter [nats] Production at-least-once delivery, consumer groups, DLQ
JanusEventChannel [janus] Sync thread → async event loop bridge (gRPC, CLI)
JanusCommandQueue[T] [janus] Sync thread → async CQRS command dispatch

7. Optional Extras Summary

Extra Installs Unlocks
(none) cloudevents, msgspec, pydantic CloudEvents serialization, Asyncio outbox relay
[sql] sqlalchemy Relational database outbox storage
[huey] huey Multi-process Huey outbox relay worker
[apprise] apprise Multi-channel notification adapter
[nats] nats-py NATS JetStream distributed event bus
[janus] janus Async-sync thread bridge for events and commands

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

hexastack_events-0.3.0.tar.gz (29.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

hexastack_events-0.3.0-py3-none-any.whl (46.1 kB view details)

Uploaded Python 3

File details

Details for the file hexastack_events-0.3.0.tar.gz.

File metadata

  • Download URL: hexastack_events-0.3.0.tar.gz
  • Upload date:
  • Size: 29.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for hexastack_events-0.3.0.tar.gz
Algorithm Hash digest
SHA256 5a2f284496fdecea56dab31d52ce511f879349159c478ffbb89f61633d21f558
MD5 151e6696c523d01585d8b4f4c95bb49c
BLAKE2b-256 59b439b944dae66d715b2c773c04467b34d5d45f4bb0ae444879683139e5b995

See more details on using hashes here.

File details

Details for the file hexastack_events-0.3.0-py3-none-any.whl.

File metadata

File hashes

Hashes for hexastack_events-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 eb0124b5ae5a26847d4d938335d285e4acb1e20851b46ac9556c6c2d11ced3ce
MD5 42c2c14a34c738968f18ae061e9d6fe1
BLAKE2b-256 274ba101a3b723a88f70ac8f26a7adc3af6da6e5b7b3500d8fda7814294a7f00

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 files

0.2.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page