Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
25 changes: 25 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Two things to know when the next LTS arrives:
- Admin UI: http://localhost:8080/admin/test/events (credentials: `alice` / `alice`)
- Mailpit UI: http://localhost:8080/q/dev-ui/quarkus-mailpit/mailpit-ui
- Keycloak dev server: http://localhost:8081
- Health: http://localhost:8080/q/health

## Architecture

Expand Down Expand Up @@ -91,6 +92,30 @@ creates the client role of the same name by hand.
`Accept` header gets the page -- without it the runtime picks whichever method it discovered first, which
is not stable across JVM runs and failed the menu test in roughly one build in eight.

### Health Endpoints

`quarkus-smallrye-health` exposes `/q/health`, `/q/health/live`, `/q/health/ready` and
`/q/health/started` (issue #61). There is **no hand-written `HealthCheck` in this code base**: the
single check that shows up is the datasource readiness check `quarkus-agroal` contributes by itself.

- **The external events feed is deliberately not a health check.** `EventService` loads each tenant's
events JSON from a third-party URL. A readiness probe that went red when someone else's server
hiccups would pull a working app out of rotation, while the registration pages keep serving from the
5-minute Caffeine cache. Feed failures belong in the log.
- **The endpoints are anonymous and sit on the main port**, which is what lets an external uptime
monitor reach them. They therefore have to stay out of `quarkus.http.auth.permission`: with
`quarkus.oidc.application-type=web-app` a secured probe path answers **302 to Keycloak**, and a
monitor that only asks "did I get a response" never notices. `HealthCheckTest` pins that down
(`redirects().follow(false)`), plus the fact that readiness carries at least one check -- without
that, `quarkus.datasource.health.enabled=false` would leave a probe reporting UP while testing
nothing.

`docker-compose.yml` consumes them: `pg_isready` on the database, `curl -f .../q/health/ready` on the
app (the `ubi9/openjdk-25-runtime` image has `curl`, but no `wget`), and the app now waits for
`condition: service_healthy` instead of a bare `depends_on: database` -- which used to let Flyway race
the database's first accepting connection. Compose does not restart an unhealthy container on its own;
the healthcheck buys ordering and visibility, not self-healing.

### Key Components

- **`RegistrationResource`** — handles registration form display and submission; decides between `registration`, `closed`, and `not_yet_open` templates based on deadline / `opensBeforeInMonths`.
Expand Down
12 changes: 12 additions & 0 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ TIP: You can now change classes, configurations and other resources on the fly,
* Registration: http://localhost:8080/registration/test?eventId=2026-12-31&opensBeforeInMonths=8
* Admin: http://localhost:8080/admin/test/events (User: alice/alice)
* Mailpit: http://localhost:8080/q/dev-ui/quarkus-mailpit/mailpit-ui
* Health: http://localhost:8080/q/health

For runnint http requests manually, see link:misc/run-http-requests-manually.adoc[here].

Expand All @@ -58,3 +59,14 @@ include::docker-compose.yml[]
----

TIP: The production app should be available directly under https://registration.ijug.eu

== Monitoring

The app exposes the standard MicroProfile Health endpoints, unauthenticated, on the regular port:

* `/q/health/live` -- the process is alive
* `/q/health/ready` -- alive *and* the database is reachable
* `/q/health/started` -- startup has finished
* `/q/health` -- all of the above in one response

An uptime monitor should poll `/q/health/ready`; that is also what the Docker Compose healthcheck above uses.
16 changes: 15 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,20 @@ services:
POSTGRES_PASSWORD: Rg1str4t10n
ports:
- "5432:5432"
healthcheck:
# POSTGRES_DB is unset, so the database is named after POSTGRES_USER.
test: ["CMD-SHELL", "pg_isready -U registration -d registration"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
restart: always

application:
image: ghcr.io/ijug-ev/registration:latest
depends_on:
- database
database:
condition: service_healthy
environment:
QUARKUS_DATASOURCE_JDBC_URL: jdbc:postgresql://database:5432/registration
QUARKUS_DATASOURCE_USERNAME: registration
Expand All @@ -24,4 +32,10 @@ services:
QUARKUS_OIDC_CREDENTIALS_SECRET: changeme
ports:
- "8080:8080"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/q/health/ready"]
interval: 30s
timeout: 5s
retries: 3
start_period: 60s
restart: always
4 changes: 4 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-scheduler</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-health</artifactId>
</dependency>
<dependency>
<groupId>io.quarkiverse.mailpit</groupId>
<artifactId>quarkus-mailpit</artifactId>
Expand Down
50 changes: 50 additions & 0 deletions src/test/java/de/jugda/registration/HealthCheckTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package de.jugda.registration;

import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.hasSize;

/**
* The health endpoints are the one part of the app that is polled by machines instead of people,
* so the two ways they break silently are pinned down here: a redirect instead of a status (OIDC
* runs in web-app mode, where an accidentally secured path answers a probe with a 302 to Keycloak
* that a naive monitor may even count as "reachable"), and a readiness check that reports UP
* without checking anything.
*
* @author Niko Köbler, https://www.n-k.de, @dasniko
*/
@QuarkusTest
public class HealthCheckTest {

@Test
void theHealthEndpointsAnswerAnonymouslyAndWithoutARedirect() {
for (String path : new String[]{"/q/health", "/q/health/live", "/q/health/ready", "/q/health/started"}) {
given()
.redirects().follow(false)
.when()
.get(path)
.then()
.statusCode(200)
.body("status", equalTo("UP"));
}
}

@Test
void readinessActuallyChecksTheDatabase() {
given()
.when()
.get("/q/health/ready")
.then()
.statusCode(200)
// The check is contributed by quarkus-agroal, not by this code base, and disabling it
// (quarkus.datasource.health.enabled=false) would leave a readiness probe that reports
// UP while testing nothing. Its name is a SmallRye-internal string, so only its
// presence is asserted, not its wording.
.body("checks", hasSize(greaterThanOrEqualTo(1)))
.body("checks[0].status", equalTo("UP"));
}
}
Loading