Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
f17638d
feat(testing): one IntegrationDsn helper that reads both DSN forms an…
Oct 13, 2026
74c005e
test(integration): parse every FIREFLY_* DSN through IntegrationDsn, …
Oct 13, 2026
f1f9be3
ci(integration): run the integration group per database server and ag…
Oct 13, 2026
57609c5
feat(data): DatabaseCapabilities reads SKIP LOCKED and READPAST suppo…
Oct 13, 2026
2ae472f
feat(eda-database,scheduling-database): scaffold the two database ada…
Oct 13, 2026
b80f617
feat(eda-database): own the outbox schema, with lease columns and an …
Oct 13, 2026
ad7a3c3
feat(eda-database): an OutboxClaimStrategy port with SKIP LOCKED, REA…
Oct 13, 2026
cf0f7f5
feat(eda-database): DatabaseEventPublisher writes the outbox row on a…
Oct 13, 2026
4286453
feat(eda-database): DatabaseEventConsumer leases each row before deli…
Oct 13, 2026
98a9a8a
feat(eda-database): the relay leases its batch, so any number of rela…
Oct 13, 2026
cb4c8aa
fix(eda-database): a database outbox key that is present but null fal…
Oct 13, 2026
3278c86
feat(eda-database): firefly.eda.provider=database wires the portable …
Oct 13, 2026
706b92f
refactor(eda-postgres): provider=postgres is the database outbox plus…
Oct 13, 2026
9f99ba8
test(eda-database): two racing consumers or relays deliver every outb…
Oct 13, 2026
a8a1b0e
feat(eda): a durable, replayable database dead-letter store behind fi…
Oct 13, 2026
e4a1084
feat(messaging): a durable, replayable database dead-letter store beh…
Oct 13, 2026
fc26d4a
fix(messaging): dead letters keep an opaque message key as base64 tex…
Oct 13, 2026
c656387
test(eda,messaging): the database dead-letter stores keep payloads, r…
Oct 13, 2026
c7c569d
feat(scheduling-database): DatabaseLeaseLock, a portable lease-table …
Oct 13, 2026
cacedcc
feat(scheduling-database): MySqlNamedLock over GET_LOCK for MySQL and…
Oct 13, 2026
5fd696a
test(scheduling): lease and named locks on every server, and CacheLoc…
Oct 13, 2026
959089a
feat(data): transaction timeouts set SQL Server's LOCK_TIMEOUT and re…
Oct 13, 2026
e18067f
test(data): exception translation, deadlock victims and repository lo…
Oct 13, 2026
5ebeb2c
docs: the outbox, dead letters and locks on every database, the suppo…
Oct 13, 2026
b8ad4f0
fix(eda,messaging): dead letters written in the same millisecond repl…
Oct 13, 2026
d00ce8b
docs(cqrs): the CQRS/EDA bridge figure credits the database outbox, n…
Oct 13, 2026
d66ccf4
fix(testing): keep DSN secrets out of IntegrationDsn errors, validate…
Oct 13, 2026
70d42d7
fix(feature-flags): list stored flags in byte order on every server, …
Oct 13, 2026
ad8d92d
docs(book): record the structure of the two diagrams whose outbox cap…
Oct 13, 2026
ff753b8
ci(integration): install the Microsoft ODBC driver the sqlsrv lane's …
Oct 13, 2026
a5b725a
ci(integration): import the Microsoft key non-interactively, since th…
Oct 13, 2026
1565904
fix(data): set SQL Server's LOCK_TIMEOUT as a direct batch, since a p…
Oct 13, 2026
5437522
ci(integration): bound the integration and broker jobs to 25 minutes
Oct 13, 2026
bb0bdf4
test(eda-database): read past the writer's uncommitted row on SQL Ser…
Oct 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@
/package.json export-ignore
/package-lock.json export-ignore
/testbench.yaml export-ignore
/docker-compose.integration.yml export-ignore
155 changes: 155 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,161 @@ jobs:
retention-days: 14
if-no-files-found: warn

integration:
name: Integration (${{ matrix.lane }})
runs-on: ubuntu-latest
# A lock wait that never times out must fail the lane, not hold a runner for six hours.
timeout-minutes: 25
strategy:
fail-fast: false
matrix:
include:
- lane: pgsql
image: postgres:16
port: 5432
extensions: pdo_pgsql
dsn-variable: FIREFLY_PG_DSN
dsn: host=127.0.0.1;port=5432;dbname=firefly;user=firefly;password=firefly
health: pg_isready -U firefly -d firefly
- lane: mysql
image: mysql:8.4
port: 3306
extensions: pdo_mysql
dsn-variable: FIREFLY_MYSQL_DSN
dsn: host=127.0.0.1;port=3306;dbname=firefly;user=firefly;password=firefly
health: mysqladmin ping -h 127.0.0.1 -ufirefly -pfirefly
- lane: mariadb
image: mariadb:11
port: 3306
extensions: pdo_mysql
dsn-variable: FIREFLY_MARIADB_DSN
dsn: host=127.0.0.1;port=3306;dbname=firefly;user=firefly;password=firefly
health: healthcheck.sh --connect --innodb_initialized
- lane: sqlsrv
image: mcr.microsoft.com/mssql/server:2022-latest
port: 1433
extensions: pdo_sqlsrv, sqlsrv
dsn-variable: FIREFLY_SQLSRV_DSN
dsn: host=127.0.0.1;port=1433;dbname=firefly;user=sa;password=Firefly-Integration-1;encrypt=yes;trust_server_certificate=true
health: /opt/mssql-tools18/bin/sqlcmd -C -S localhost -U sa -P Firefly-Integration-1 -Q 'SELECT 1'
services:
database:
image: ${{ matrix.image }}
# One env block serves every image: each reads only its own variables.
env:
POSTGRES_DB: firefly
POSTGRES_USER: firefly
POSTGRES_PASSWORD: firefly
MYSQL_DATABASE: firefly
MYSQL_USER: firefly
MYSQL_PASSWORD: firefly
MYSQL_ROOT_PASSWORD: firefly-root
MARIADB_DATABASE: firefly
MARIADB_USER: firefly
MARIADB_PASSWORD: firefly
MARIADB_ROOT_PASSWORD: firefly-root
ACCEPT_EULA: 'Y'
MSSQL_SA_PASSWORD: Firefly-Integration-1
ports:
- ${{ matrix.port }}:${{ matrix.port }}
options: >-
--health-cmd "${{ matrix.health }}"
--health-interval 10s
--health-timeout 5s
--health-retries 30
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
coverage: none
tools: composer:v2
extensions: ${{ matrix.extensions }}
- name: Install dependencies
run: composer install --no-interaction --prefer-dist
- name: Install the Microsoft ODBC driver (pdo_sqlsrv needs it)
if: matrix.lane == 'sqlsrv'
run: |
curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | sudo gpg --batch --yes --dearmor -o /usr/share/keyrings/microsoft-prod.gpg
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/microsoft-prod.gpg] https://packages.microsoft.com/ubuntu/$(lsb_release -rs)/prod $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/mssql-release.list
sudo apt-get update
sudo ACCEPT_EULA=Y apt-get install -y msodbcsql18
- name: Create the sqlsrv database
if: matrix.lane == 'sqlsrv'
run: docker exec ${{ job.services.database.id }} /opt/mssql-tools18/bin/sqlcmd -C -S localhost -U sa -P Firefly-Integration-1 -Q "CREATE DATABASE firefly"
- name: Pest (integration, ${{ matrix.lane }})
env:
FIREFLY_INTEGRATION_LANE: ${{ matrix.lane }}
run: |
export ${{ matrix.dsn-variable }}='${{ matrix.dsn }}'
composer test:integration

brokers:
name: Integration (brokers)
runs-on: ubuntu-latest
timeout-minutes: 25
services:
rabbitmq:
image: rabbitmq:3.13-alpine
ports:
- 5672:5672
options: >-
--health-cmd "rabbitmq-diagnostics -q ping"
--health-interval 10s
--health-timeout 10s
--health-retries 20
kafka:
image: apache/kafka:3.8.0
env:
KAFKA_NODE_ID: 1
KAFKA_PROCESS_ROLES: broker,controller
KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://127.0.0.1:9092
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@localhost:9093
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
ports:
- 9092:9092
options: >-
--health-cmd "/opt/kafka/bin/kafka-broker-api-versions.sh --bootstrap-server 127.0.0.1:9092"
--health-interval 10s
--health-timeout 10s
--health-retries 20
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 5s
--health-timeout 5s
--health-retries 20
steps:
- uses: actions/checkout@v4
- name: Install librdkafka
run: sudo apt-get update && sudo apt-get install -y librdkafka-dev
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
coverage: none
tools: composer:v2
extensions: rdkafka
- name: Install dependencies
run: composer install --no-interaction --prefer-dist
- name: Pest (integration, brokers)
env:
FIREFLY_INTEGRATION_LANE: brokers
FIREFLY_RABBITMQ_DSN: host=127.0.0.1;port=5672;user=guest;password=guest
FIREFLY_KAFKA_BROKERS: 127.0.0.1:9092
FIREFLY_REDIS_URL: redis://127.0.0.1:6379/0
run: composer test:integration

docs:
name: Docs (site + bilingual books)
runs-on: ubuntu-latest
Expand Down
53 changes: 53 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,59 @@ All notable changes to LaraFly are documented here. This project uses CalVer (`Y

## [Unreleased]

### Added

- **`firefly/eda-database` — the same-transaction outbox on every database.** `firefly.eda.provider=database`
writes the outbox row inside the aggregate's transaction on PostgreSQL, MySQL, MariaDB, SQL Server or SQLite, and
claims committed rows by leases through an `OutboxClaimStrategy` (SKIP LOCKED on PostgreSQL, MySQL 8.0.1+ and
MariaDB 10.6+, READPAST on SQL Server, a portable optimistic lease elsewhere — chosen from the server's version,
overridable by `firefly.eda.database.claim-strategy`). Any number of `firefly:eda:consume` workers and
`firefly:outbox:relay` processes may run against one outbox; two racing processes are proven to deliver every
row exactly once on each server. An `outbox` health component reports the driver, the strategy and the backlog.
- **Durable dead letters.** `firefly.eda.dead-letter.store=database` and `firefly.messaging.dead-letter.store=database`
keep exhausted envelopes and messages in `firefly_eda_dead_letters` / `firefly_messaging_dead_letters`, and
`firefly:eda:dead-letters:replay` / `firefly:messaging:dead-letters:replay` republish them (`--dry-run`, `--keep`,
selectors by id, destination, event type or topic), oldest first: the ids are time-ordered UUIDs (version 7),
monotonic within a process, so dead letters written in the same millisecond replay in the order they were written.
- **`firefly/scheduling-database`.** `firefly.scheduling.lock.provider=database` (`DatabaseLeaseLock`, a lease table
honouring the TTL on every driver) and `firefly.scheduling.lock.provider=mysql` (`MySqlNamedLock`, `GET_LOCK` on
MySQL and MariaDB).
- **Integration lanes.** `composer test:integration`; CI jobs `integration` (PostgreSQL 16, MySQL 8.4, MariaDB 11,
SQL Server 2022) and `brokers` (RabbitMQ, Kafka, Redis), each health-checked and exporting only its own DSN, with a
lane-sanity test that fails a misconfigured lane; `docker-compose.integration.yml` mirrors them locally.
`Firefly\Testing\Integration\IntegrationDsn` is the one parser every integration test uses (both DSN forms).
- **Data layer on real servers.** Exception translation (including deadlock victims), statement timeouts and
pessimistic/optimistic repository locking are integration-tested on every server; `DatabaseCapabilities` (in
`firefly/data`) reads SKIP LOCKED and READPAST support from the server version; the [database support
matrix](docs/database-support.md) documents the result.

### Changed

- **`firefly/eda-postgres` is the database outbox plus `pg_notify`/`LISTEN`.** `PostgresEventPublisher`,
`PostgresEventConsumer` and its `OutboxPreCommitHook` now extend the `firefly/eda-database` classes;
`provider=postgres` reads the `firefly.eda.database.*` keys, with every `firefly.eda.postgres.*` key kept as an
alias. The outbox table gains `claimed_by` / `claimed_until` (run `php artisan migrate`).
- **Transaction timeouts on SQL Server** set the session's `LOCK_TIMEOUT` and restore it afterwards.

### Fixed

- **Concurrent consumers could deliver an outbox row twice.** The Postgres consumer's `FOR UPDATE SKIP LOCKED` ran
outside any transaction, so its lock ended with the statement; on other drivers the claim was an unlocked read.
Every claim is now a lease committed before delivery.
- **`RabbitMqRoundTripTest` failed against a real broker when `FIREFLY_RABBITMQ_DSN` was a URL**, which the old
parser read as one key and silently replaced with `127.0.0.1:5672`.
- **The lumen sample's Postgres round trip called `firefly:eda:consume` without registering it.**
- **The CQRS/EDA bridge figure still credited only the Postgres adapter with same-transaction atomicity.** Its
caption now names the database outbox (`provider=database` or `postgres`), and its adapter row draws the
`database` provider; a test derives both providers from their outbox auto-configurations.

### Deprecated

- `Firefly\Eda\Postgres\Outbox\OutboxSchema`, `OutboxRow`, `OutboxRelay` (its `$useSkipLocked` argument now picks
`auto` or the optimistic lease),
`RelayDownstream` and `Firefly\Eda\Postgres\Console\OutboxRelayCommand` — use their `Firefly\Eda\Database\…`
namesakes; the old names remain as subclasses for one release.

## [26.10.1] - 2026-10-06

### Added
Expand Down
Loading
Loading