Skip to content

Commit e6f9617

Browse files
ammodevclaude
andcommitted
Merge branch 'feat/surf-event-bus' into feat/topology-redesign
Three conflicts, all where both branches had been working on the same RabbitMQ consumer paths. `RabbitConnectionProvider`: both sides added a different import. Kept both. `RabbitConsumer.getChannel`: topology-redesign added the channel shutdown listener and consumer recovery, and `createChannel` now takes the connection generation; surf-event-bus had moved the blocking open onto `Dispatchers.IO`. Kept the recovery machinery and wrapped its `createChannel(connectionGeneration)` in `withContext(Dispatchers.IO)`, so the blocking AMQP round trip still leaves the caller's dispatcher — the same reason `awaitOpen` hops to IO. `RabbitListenerHandlerManager`: both branches fixed the same double-settle bug in different ways. surf-event-bus guarded `settleWithRetry` with a `settled` CAS; topology-redesign removed the race by construction, completing a `CompletableDeferred` from `invokeOnCompletion` and racing it against the response in a `select`, and added the `SurfRabbitConnectionException` requeue and `CancellationException` branches. Took topology-redesign's structure and routed its settlements through the CAS-guarded `settleWithRetry`, which the already-merged tail of the same function calls too. Verified: ./gradlew check -PrequireIntegration — 280 passed, 0 failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2 parents ad843bd + 809f16f commit e6f9617

61 files changed

Lines changed: 2824 additions & 379 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- '**'
8+
- '!version/*'
9+
workflow_dispatch:
10+
11+
concurrency:
12+
group: ci-${{ github.ref }}
13+
cancel-in-progress: true
14+
15+
jobs:
16+
check:
17+
name: Build and test
18+
runs-on: ubuntu-latest
19+
timeout-minutes: 45
20+
21+
steps:
22+
- uses: actions/checkout@v4
23+
24+
- name: Set up JDK 25
25+
uses: actions/setup-java@v4
26+
with:
27+
distribution: temurin
28+
java-version: '25'
29+
30+
- name: Set up Gradle
31+
uses: gradle/actions/setup-gradle@v4
32+
with:
33+
cache-read-only: ${{ github.ref != 'refs/heads/version/1.21.11' }}
34+
35+
# ubuntu-latest ships a running Docker daemon, so the integration tag must not be
36+
# skipped here. -PrequireIntegration turns DockerAvailableCondition from "skip" into
37+
# "fail", which is the whole point of running these on CI: a silent skip reports green
38+
# while verifying nothing.
39+
- name: Verify Docker is available
40+
run: docker info
41+
42+
- name: Check
43+
run: ./gradlew check -PrequireIntegration --stacktrace --no-daemon
44+
45+
- name: Upload test reports
46+
if: always()
47+
uses: actions/upload-artifact@v4
48+
with:
49+
name: test-reports
50+
path: |
51+
**/build/reports/tests/
52+
**/build/test-results/
53+
retention-days: 7
54+
if-no-files-found: ignore

docker-compose.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Local development brokers for surf-eventbus.
2+
#
3+
# The integration test suite does NOT use this file — it starts its own throwaway containers
4+
# through Testcontainers. This is for running a real application against the bus by hand, and
5+
# for poking at the broker while debugging a failing suite.
6+
#
7+
# docker compose up -d
8+
# RabbitMQ management UI: http://localhost:15672 (surf / surf)
9+
#
10+
# The credentials below match the defaults in docs/development.md. They are deliberately weak;
11+
# nothing here should ever be exposed outside localhost.
12+
13+
services:
14+
rabbitmq:
15+
image: rabbitmq:4-management
16+
container_name: surf-eventbus-rabbitmq
17+
ports:
18+
- "5672:5672"
19+
- "15672:15672"
20+
environment:
21+
RABBITMQ_DEFAULT_USER: surf
22+
RABBITMQ_DEFAULT_PASS: surf
23+
RABBITMQ_DEFAULT_VHOST: /
24+
healthcheck:
25+
test: [ "CMD", "rabbitmq-diagnostics", "-q", "ping" ]
26+
interval: 10s
27+
timeout: 5s
28+
retries: 10
29+
volumes:
30+
- rabbitmq-data:/var/lib/rabbitmq
31+
32+
redis:
33+
image: redis:7-alpine
34+
container_name: surf-eventbus-redis
35+
ports:
36+
- "6379:6379"
37+
command: [ "redis-server", "--requirepass", "surf", "--appendonly", "no" ]
38+
healthcheck:
39+
test: [ "CMD", "redis-cli", "-a", "surf", "ping" ]
40+
interval: 10s
41+
timeout: 5s
42+
retries: 10
43+
44+
volumes:
45+
rabbitmq-data:

docs/audit-2026-08-01-repository.md

Lines changed: 1392 additions & 0 deletions
Large diffs are not rendered by default.

docs/development.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Developing surf-eventbus
2+
3+
Everything you need to build, test and run the bus locally.
4+
5+
## Prerequisites
6+
7+
| Tool | Version | Notes |
8+
|---|---|---|
9+
| JDK | 25 | The toolchain targets class-file major 69. CI uses Temurin 25. |
10+
| Docker | any recent | Required for the integration suite. Without it those tests **skip**. |
11+
| Gradle || Use the wrapper (`./gradlew`); it pins 9.6.1. |
12+
13+
## Building
14+
15+
```bash
16+
./gradlew build # compile + test + ABI check
17+
./gradlew check # everything CI runs
18+
./gradlew assemble # jars only, no tests
19+
```
20+
21+
## Running the tests
22+
23+
```bash
24+
./gradlew check # the full suite, including integration tests
25+
./gradlew check -PskipIntegration # skip everything that needs Docker
26+
./gradlew check -PrequireIntegration # fail (don't skip) if Docker is missing — what CI does
27+
./gradlew lincheckTest # the concurrency tests, excluded from `test` by default
28+
```
29+
30+
### About `-PrequireIntegration`
31+
32+
Integration tests are tagged `integration` and guarded by `DockerAvailableCondition`. Without a
33+
Docker daemon they report *"integration test skipped, NOT verified"* and the build still goes
34+
green. That is a reasonable default on a laptop and a dangerous one on CI, so
35+
`.github/workflows/ci.yml` passes `-PrequireIntegration`, which turns the skip into a failure.
36+
37+
Pass at most one of `-PskipIntegration` / `-PrequireIntegration`; the build rejects both together.
38+
39+
The integration suite starts its own throwaway containers via Testcontainers. It does **not** use
40+
`docker-compose.yml` and does not need the brokers below to be running.
41+
42+
## Running brokers locally
43+
44+
For driving a real application against the bus by hand:
45+
46+
```bash
47+
docker compose up -d # rabbitmq:4-management on 5672/15672, redis:7 on 6379
48+
docker compose down -v # tear down, including the rabbit volume
49+
```
50+
51+
| Service | Address | Credentials |
52+
|---|---|---|
53+
| RabbitMQ | `localhost:5672` | `surf` / `surf`, vhost `/` |
54+
| RabbitMQ management UI | http://localhost:15672 | `surf` / `surf` |
55+
| Redis | `localhost:6379` | password `surf` |
56+
57+
These credentials are deliberately trivial. Do not expose the compose stack beyond localhost.
58+
59+
## Pointing the bus at them
60+
61+
Configuration resolves per field as `env > plugin yaml > global yaml > built-in default`
62+
(`EventBusConfigResolver`). The quickest local route is the environment:
63+
64+
```bash
65+
export SURF_EVENTBUS_RABBITMQ_HOST=localhost
66+
export SURF_EVENTBUS_RABBITMQ_PORT=5672
67+
export SURF_EVENTBUS_RABBITMQ_USERNAME=surf
68+
export SURF_EVENTBUS_RABBITMQ_PASSWORD=surf
69+
export SURF_EVENTBUS_RABBITMQ_VHOST=/
70+
71+
export SURF_EVENTBUS_REDIS_HOST=localhost
72+
export SURF_EVENTBUS_REDIS_PORT=6379
73+
export SURF_EVENTBUS_REDIS_PASSWORD=surf
74+
```
75+
76+
The yaml layers are `eventbus.yml` (global, next to the platform plugin's data folder) and
77+
`eventbus-plugin.yml` (per consuming plugin, in that plugin's own folder). Both are generated
78+
full of sentinels on first run and override nothing until edited.
79+
80+
Two chunking flags additionally honour JVM system properties as a last resort before the built-in
81+
default — `surf.rabbitmq.outgoingRequestChunkingEnabled` and
82+
`surf.rabbitmq.outgoingResponseChunkingEnabled`. They are the only fields with this extra layer.
83+
84+
## Module layout
85+
86+
| Module | Contains |
87+
|---|---|
88+
| `surf-eventbus-api` | Public surface: `SurfEventBus`, annotations, config, codecs, ABI dump |
89+
| `surf-eventbus-core` | Implementations: dispatchers, RabbitMQ and Redis transports, retry, audit |
90+
| `surf-eventbus-ksp` | The `@RpcService` / `@QueryService` descriptor generator |
91+
| `surf-eventbus-platform` | Paper, Velocity and standalone bootstrap |
92+
93+
## The ABI check
94+
95+
`surf-eventbus-api` has a frozen ABI dump at `surf-eventbus-api/api/surf-eventbus-api.api`. Any
96+
change to the public surface fails `check` until the dump is regenerated:
97+
98+
```bash
99+
./gradlew :surf-eventbus-api:checkKotlinAbi # what `check` runs
100+
./gradlew :surf-eventbus-api:updateKotlinAbi # regenerate, then review the diff before committing
101+
```
102+
103+
Review that diff. It is the only automated guard against accidentally publishing an
104+
implementation type.
105+
106+
## Benchmarks
107+
108+
```bash
109+
./gradlew :surf-eventbus-core:jmh
110+
```

docs/rollout-2.0.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,23 @@ Wire format and API both changed. A 1.6.x service and a 2.0 service cannot talk
111111

112112
Stop everything, upgrade, start everything. There is no rolling upgrade path.
113113

114+
Two identifiers that are *not* obvious from the API also changed, and both fail silently rather
115+
than loudly if a straggler is left running:
116+
117+
| What | 1.6.x | 2.0 | If a 1.6.x process is still running |
118+
|---|---|---|---|
119+
| AMQP version header | `x-surf-rabbitmq-version` | `x-surf-eventbus-version` | Peers read each other as an unknown version, so version-gated behaviour is off. Degrades, does not crash. |
120+
| Redis sync key prefix | `surf-redis:sync:` | `surf.eventbus.sync:` | The two run **disjoint copies** of every `SyncMap`/`SyncSet`/`SyncList`/`SyncValue`. No error; the data simply diverges. |
121+
122+
The sync prefix is the dangerous one: nothing anywhere reports that two halves of the fleet are
123+
maintaining separate state. There is no migration for existing keys — a sync structure is a
124+
cache of authoritative state elsewhere, so the intended procedure is to let the old keys expire
125+
rather than rewrite them. If some structure in your deployment is *not* reconstructible, copy
126+
those keys across before starting 2.0.
127+
128+
Both constants are pinned by `WireConstantsTest`, so a later accidental rename fails the build
129+
rather than the fleet.
130+
114131
---
115132

116133
## Checklist
@@ -121,4 +138,6 @@ Stop everything, upgrade, start everything. There is no rolling upgrade path.
121138
- [ ] every `surf.service.*` queue deleted, or the vhost recreated
122139
- [ ] old `surf-rabbitmq-*` and surf-redis plugins removed from every server
123140
- [ ] all services stopped, upgraded, and started together
141+
- [ ] no 1.6.x process left running against the same Redis (silent sync-structure split)
142+
- [ ] any non-reconstructible `surf-redis:sync:*` keys copied to `surf.eventbus.sync:*`
124143
- [ ] someone knows the audit trail is best effort until its writer ships

0 commit comments

Comments
 (0)