refactor: adapter 구현중..

This commit is contained in:
DongHyeonka
2026-08-13 02:21:34 +09:00
parent 0cd959a494
commit 0a6dd0e419
86 changed files with 3088 additions and 302 deletions
+17
View File
@@ -56,6 +56,23 @@ docker compose -f docker-compose.yml -f docker-compose.local.yml down
`src/.env`는 커밋된 안전 기본값이라 별도 `.env.example`을 만들지 않습니다. 전체 환경 변수 목록과 조정 시점은 [src/README.md](src/README.md)와 [docs/registries/env-keys.yaml](docs/registries/env-keys.yaml)에 있습니다.
### 프로파일별 데이터스토어
`bootstrap`은 컨테이너 경로(PostgreSQL)를 검증하는 첫 실행 진입점입니다. 일상 개발은 Docker 없이 돌리는 `local` 프로파일이며, 이때 데이터스토어는 H2 in-memory입니다.
```bash
cd src
./gradlew :app-bootstrap:bootRun
```
| 프로파일 | 데이터스토어 | 스키마 소유자 |
| --- | --- | --- |
| `local` (bootRun 기본) | H2 in-memory | Hibernate `create-drop` |
| `dev` | PostgreSQL | Flyway |
| `prod` | PostgreSQL | Flyway |
`local`은 wiring과 애플리케이션 동작을 검증하고, migration과 vendor 동작은 검증하지 않습니다. 프로파일별 설정은 [src/app-bootstrap/src/main/resources/](src/app-bootstrap/src/main/resources/)의 `application-{local,dev,prod}.yml`이, 상세 설명은 [src/README.md](src/README.md)가 소유합니다.
## 새 프로젝트로 시작하기
이 저장소를 새 서비스의 출발점으로 쓸 때 핵심 단계는 다음과 같습니다. 전체 체크리스트는 [AGENTS.md](AGENTS.md)의 "템플릿 재사용 체크리스트"에 있습니다.
+14 -3
View File
@@ -8,8 +8,9 @@
# - Starts a local PostgreSQL database for integration testing without Testcontainers.
# - Wires the app environment to point at the local DB.
# - Keeps read-only filesystem and memory limits from the base compose.
# - Does NOT expose the DB port publicly; app and db communicate on the
# internal `caskeleton-local` network only.
# - Publishes the DB on the loopback interface only, so a host-side run
# (`./gradlew :app-bootstrap:bootRun`, IDE) reaches the same database the
# containerised app reaches over the internal `caskeleton-local` network.
# =============================================================================
services:
@@ -59,7 +60,17 @@ services:
- type: volume
source: caskeleton-db-data
target: /var/lib/postgresql/data
# No host port: startup Flyway runs in the app container over the internal network.
# The containerised app reaches this over the internal network and needs no host port. A
# host-side run does: src/.env is the dotenv source bootRun reads, and its committed
# APP_DATASOURCE_URL is jdbc:postgresql://localhost:5433/ca_skeleton. With the port unpublished
# that default named an address nothing in the repository provisioned, so every bootRun died in
# the startup migration phase with a connection refusal.
#
# Bound to 127.0.0.1, never 0.0.0.0: the database is reachable from this machine and from
# nowhere else on the network. Host 5433 (not 5432) so a PostgreSQL already installed on the
# host keeps its conventional port.
ports:
- "127.0.0.1:5433:5432"
networks:
- caskeleton-local
healthcheck:
+2 -2
View File
@@ -27,7 +27,7 @@ Therefore the design's 19 library modules become **package boundaries inside the
| Design module | Repository home | Reason |
|---|---|---|
| `httpclient-spring-boot-starter` | `:app-bootstrap` (`dev.caskeleton.bootstrap.autoconfigure.httpclient`) | This repository's composition root owns wiring and canonical activation; an adapter leaf must not auto-configure itself. |
| `httpclient-testkit` | `:adapter:outbound:httpclient` `src/test/java/**/testkit` | The design forbids production modules depending on the testkit; a test source set gives the same guarantee without a new Gradle project. |
| `httpclient-testkit` | `:adapter:outbound:httpclient` `src/testkit/java/**/testkit` | The design forbids production modules depending on the testkit; a source set whose dependencies are declared only on the test configurations gives the same guarantee without a new Gradle project. It is its own source set rather than part of `test` because three lanes consume it — `test`, `httpClientPerformanceTest` and `jmh` — and reaching into `sourceSets.test.output` from `jmh` compiled under Gradle but could not be modelled by an IDE, which classifies a source set as test source only when a `Test` task runs its output and forbids main source from reading test source. `PlatformClasses` excludes the source set's output so the boundary rules keep meaning production classes. |
The package boundary is enforced by ArchUnit rules (`PublicApiArchitectureTest`,
`HttpClientModuleBoundaryTest`) that reproduce the design's module dependency table.
@@ -56,7 +56,7 @@ Root package: `io.backend.skeleton.httpclient` → `dev.caskeleton.adapter.outbo
| `httpclient-spring7-service-groups` | `…httpclient.spring7` | `…outbound.httpclient.spring7` |
| `httpclient-jetty-http3-experimental` | `…httpclient.http3` | `…outbound.httpclient.http3` |
| `httpclient-spring-boot-starter` | `…httpclient.autoconfigure` | `dev.caskeleton.bootstrap.autoconfigure.httpclient` |
| `httpclient-testkit` | `…httpclient.testkit` | `…outbound.httpclient.testkit` (test source set) |
| `httpclient-testkit` | `…httpclient.testkit` | `…outbound.httpclient.testkit` (`testkit` source set) |
## 3. Other deliberate substitutions
+19 -6
View File
@@ -2698,7 +2698,11 @@ env_keys:
allowed_values: null
classification: sensitive-config
required: false
required_when: app.redis.mode=sentinel
# Applied only when present: RedisTopologyClientFactory sets the sentinel credentials provider
# through ifPresent, so a Sentinel deployment whose sentinels accept unauthenticated discovery
# starts without it. The unconditional "app.redis.mode=sentinel" this used to declare was a
# requirement the runtime never enforced.
required_when: app.redis.mode=sentinel and the sentinels require authentication
reload_policy: restart-only
owner_branch: redis-optionality-and-composition
validation: nonblank_when_required
@@ -2724,11 +2728,13 @@ env_keys:
property: app.redis.sentinel.nodes
owner_module: adapter-outbound-cache-redis
type: csv
default: null
default: app.redis.nodes
allowed_values: null
classification: public-config
required: false
required_when: app.redis.mode=sentinel
# Falls back to app.redis.nodes by design, so a deployment that points nodes at its sentinels
# and says nothing else is the common case rather than a misconfiguration.
required_when: app.redis.mode=sentinel and app.redis.nodes does not list the sentinels
reload_policy: restart-only
owner_branch: redis-optionality-and-composition
validation: csv_nonempty
@@ -2743,7 +2749,9 @@ env_keys:
allowed_values: null
classification: public-config
required: false
required_when: app.redis.tls.enabled=true
# A key manager is configured only when this is present. Ordinary one-way TLS needs no client
# certificate, so requiring one whenever TLS is on was a claim the runtime never made.
required_when: app.redis.tls.enabled=true and the server requires mutual TLS
reload_policy: restart-only
owner_branch: redis-optionality-and-composition
validation: nonblank_when_required
@@ -2758,7 +2766,10 @@ env_keys:
allowed_values: null
classification: sensitive-config
required: false
required_when: app.redis.tls.enabled=true
# A relationship between two settings rather than a switch: a client certificate without its
# key cannot build a key manager. Enforced in RedisSdkSettings.validate and covered by
# RedisSdkSettingsTest, which is where the registry's prose conditions are proven.
required_when: app.redis.tls.client-certificate-resource is configured
reload_policy: restart-only
owner_branch: redis-optionality-and-composition
validation: nonblank_when_required
@@ -2804,7 +2815,9 @@ env_keys:
allowed_values: null
classification: public-config
required: false
required_when: app.redis.tls.enabled=true
# A trust manager is installed only when this is present; otherwise the JDK default trust
# anchors apply, which is enough for a server certificate from a public CA.
required_when: app.redis.tls.enabled=true and the server certificate is not publicly trusted
reload_policy: restart-only
owner_branch: redis-optionality-and-composition
validation: nonblank_when_required
@@ -0,0 +1,4 @@
user default off
user ca-skeleton-sentinel-client on >fixture-sentinel-client ~* &* -@all +auth +hello +ping +client|setname +client|id +subscribe +psubscribe +unsubscribe +punsubscribe +info +sentinel|get-master-addr-by-name +sentinel|master +sentinel|masters +sentinel|replicas +sentinel|slaves +sentinel|sentinels +sentinel|is-master-down-by-addr
user ca-skeleton-sentinel-peer on >fixture-sentinel-peer ~* &* +@all
user ca-skeleton-sentinel-operator on >fixture-sentinel-operator ~* &* -@all +auth +hello +ping +client|setname +info +subscribe +psubscribe +sentinel|get-master-addr-by-name +sentinel|master +sentinel|masters +sentinel|replicas +sentinel|slaves +sentinel|sentinels +sentinel|failover +sentinel|reset
+40 -2
View File
@@ -431,13 +431,51 @@ Boot 기본값이 바뀌어도 wire 계약이 조용히 깨지지 않게 합니
- **`APP_SECURITY_CORS_ALLOW_CREDENTIALS`** — `true` | `false`.
- **`APP_SECURITY_CORS_MAX_AGE`** — preflight 캐시 TTL(초).
### 프로파일과 데이터베이스
프로파일마다 데이터스토어가 다르고, 그 차이는 `app-bootstrap/src/main/resources/application-*.yml`
가 소유합니다.
| 프로파일 | 데이터스토어 | 스키마 소유자 | 외부 인프라 |
| --- | --- | --- | --- |
| `local` (기본) | H2 in-memory | Hibernate `ddl-auto: create-drop` | 없음 |
| `dev` | PostgreSQL | Flyway `db/migration/postgresql` | 필요 |
| `prod` | PostgreSQL | Flyway `db/migration/postgresql` | 필요 |
`local``./gradlew :app-bootstrap:bootRun` 의 기본값(`src/.env``SPRING_PROFILES_ACTIVE=local`)
이라 Docker 없이 바로 뜹니다. 아래 `APP_DATASOURCE_*` 값은 `local` 에서는 쓰이지 않고
`application-local.yml` 이 덮어씁니다.
`local` 이 검증하는 것은 wiring·요청/응답·애플리케이션 로직이고, **검증하지 않는 것은 migration 과
vendor 동작**입니다. H2 에는 migration tree 가 없어 migration 에만 존재하는 테이블(capability schema
registry, polling-delivery·inbox stream, Spring Integration lock)이 만들어지지 않습니다. 해당
capability 는 `local` 기본값에서 꺼져 있고, 켜면 테이블 없음으로 실패합니다.
`dev` 를 호스트에서 띄우려면(`SPRING_PROFILES_ACTIVE=dev`) PostgreSQL 이 필요합니다.
`docker-compose.local.yml``db` 서비스가 루프백(`127.0.0.1:5433`)에만 게시하며, 이 주소가
아래 `APP_DATASOURCE_URL` 의 커밋된 기본값입니다. 저장소 루트에서 실행합니다.
```bash
docker compose -f docker-compose.yml -f docker-compose.local.yml up -d --wait db
```
컨테이너로 띄우는 `./gradlew bootstrap` 경로는 이 호스트 포트를 쓰지 않습니다. compose 가 app
컨테이너의 `APP_DATASOURCE_URL` 을 내부 네트워크 주소 `jdbc:postgresql://db:5432/...` 로 덮어씁니다.
`ca-skeleton.persistence.vendor`(`postgresql` | `h2`)가 어느 vendor 구성을 조립할지 고르는 단일
스위치입니다. 값이 둘 중 하나가 아니면 기동이 실패하고, prod 에서 `h2` 이거나 datasource URL 이
`jdbc:h2:` 이면 `PersistenceVendorProdSafetyValidator` 가 기동을 거부합니다(env 로 덮어써도 동일).
### Database (Postgres)
- **`APP_DATASOURCE_URL`** — JDBC URL(예: `jdbc:postgresql://host:5432/dbname`).
- **`APP_DATASOURCE_URL`** — JDBC URL(예: `jdbc:postgresql://host:5432/dbname`). `dev`·`prod` 에서
쓰이며, 커밋된 기본값 `jdbc:postgresql://localhost:5433/ca_skeleton` 은 위 compose `db` 서비스의
호스트 주소입니다.
- **`APP_DATASOURCE_USERNAME`** / **`APP_DATASOURCE_PASSWORD`** — DB 접속 계정.
- **`APP_DATASOURCE_DRIVER`** — Hibernate dialect 에 맞는 드라이버(예: `org.postgresql.Driver`).
- **`APP_DATASOURCE_DDL_AUTO`** — `none` | `validate` | `update` | `create` | `create-drop`. **prod
`validate` 또는 `none`**, local 은 `update`편리합니다.
`validate` 또는 `none`**(`JpaSchemaSafetyValidator`기동 시 강제). `local` 은 이 값을 쓰지
않습니다 — `application-local.yml``create-drop` 으로 고정합니다.
- **`APP_DATASOURCE_SHOW_SQL`** — `true` 면 SQL 을 로그로 echo.
- **`APP_DATASOURCE_FORMAT_SQL`** — SQL pretty-print(`SHOW_SQL=true` 일 때만 의미 있음).
- **`APP_DATASOURCE_OPEN_IN_VIEW`** — Hibernate OSIV. **prod 에서는 피하세요.**
@@ -82,8 +82,20 @@ public class RedisSdkSettings {
if (mode == RedisDeploymentMode.SENTINEL
&& (sentinel.getMasterName() == null || sentinel.getMasterName().isBlank())) {
throw new IllegalStateException(
"a Sentinel deployment must name the monitored primary; without it the client cannot"
+ " resolve a primary at all, let alone follow a promotion");
"app.redis.sentinel.master-name is required when app.redis.mode=sentinel: a Sentinel"
+ " deployment must name the monitored primary; without it the client cannot resolve"
+ " a primary at all, let alone follow a promotion");
}
// A certificate without its key cannot build a key manager. Refused here rather than at the
// first connection, where it surfaced as a NullPointerException from inside the SSL options.
if (tls.isEnabled()
&& tls.getClientCertificateResource() != null
&& !tls.getClientCertificateResource().isBlank()
&& (tls.getClientKeyReference() == null || tls.getClientKeyReference().isBlank())) {
throw new IllegalStateException(
"app.redis.tls.client-key-reference is required when"
+ " app.redis.tls.client-certificate-resource is configured: mutual TLS presents a"
+ " certificate, and a certificate without its private key cannot be presented");
}
if (tls.isEnabled() && !tls.isHostnameVerification()) {
warnings.add(
@@ -99,11 +111,15 @@ public class RedisSdkSettings {
}
if (raw.isEnabled()
&& (raw.getCredentialReference() == null || raw.getCredentialReference().isBlank())) {
throw new IllegalStateException("the raw gateway requires its own credential reference");
throw new IllegalStateException(
"app.redis.raw.credential-reference is required when app.redis.raw.enabled=true: the raw"
+ " gateway authenticates as its own account");
}
if (admin.isEnabled()
&& (admin.getCredentialReference() == null || admin.getCredentialReference().isBlank())) {
throw new IllegalStateException("the admin plane requires its own credential reference");
throw new IllegalStateException(
"app.redis.admin.credential-reference is required when app.redis.admin.enabled=true: the"
+ " admin plane authenticates as its own account");
}
if (!advanced.isEnabled() && !advanced.getPolicies().isEmpty()) {
throw new IllegalStateException(
@@ -441,7 +441,7 @@ class RedisSdkAutoConfigurationTest {
context -> {
assertThat(context).hasFailed();
assertThat(context.getStartupFailure())
.hasStackTraceContaining("the admin plane requires its own credential reference");
.hasStackTraceContaining("app.redis.admin.credential-reference");
});
}
}
@@ -71,7 +71,10 @@ class RedisSdkSettingsTest {
RedisSdkSettings missingCredential = validProperties();
missingCredential.getRaw().setEnabled(true);
assertThatThrownBy(missingCredential::validate).hasMessageContaining("credential reference");
// The setting by name. An operator reading "requires its own credential reference" has to work
// out which of the five credential references the registry declares is the missing one.
assertThatThrownBy(missingCredential::validate)
.hasMessageContaining("app.redis.raw.credential-reference");
}
@Test
@@ -79,7 +82,37 @@ class RedisSdkSettingsTest {
RedisSdkSettings properties = validProperties();
properties.getAdmin().setEnabled(true);
assertThatThrownBy(properties::validate).hasMessageContaining("credential reference");
assertThatThrownBy(properties::validate)
.hasMessageContaining("app.redis.admin.credential-reference");
}
@Test
void aClientCertificateWithoutItsKeyIsRefusedAtStartup() {
// The registry declares this one as a relationship between two settings rather than a switch,
// so RequiredWhenIsEnforcedTest skips it and this is where the claim is kept. Before it was
// checked here, the missing key surfaced as a NullPointerException while the SSL options were
// being built — at the first connection, not at startup.
RedisSdkSettings properties = validProperties();
properties.getTls().setEnabled(true);
properties.getTls().setClientCertificateResource("classpath:redis/client.crt");
assertThatThrownBy(properties::validate)
.hasMessageContaining("app.redis.tls.client-key-reference");
properties.getTls().setClientKeyReference("secret://environment/APP_REDIS_TLS_CLIENT_KEY");
assertThatCode(properties::validate).doesNotThrowAnyException();
}
@Test
void oneWayTlsNeedsNoClientCertificateAndNoTrustMaterial() {
// What the registry used to claim was required whenever TLS was on. A server certificate from
// a public CA verifies against the JDK trust anchors, and a server that does not ask for a
// client certificate is not given one, so neither setting is a startup requirement.
RedisSdkSettings properties = validProperties();
properties.getTls().setEnabled(true);
assertThatCode(properties::validate).doesNotThrowAnyException();
}
@Test
@@ -0,0 +1,152 @@
package dev.caskeleton.adapter.outbound.cache.redis.sdk.config;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.yaml.snakeyaml.Yaml;
/**
* Makes the registry's {@code required_when} keep its promise.
*
* <p>{@code verifyEnvKeys} checks that every bindable property has a row and every row names a
* property, which leaves the most load-bearing field on the row unchecked: {@code required_when}
* declares that a setting <em>must</em> be present once some condition holds. Nothing verified
* that, so several rows claimed a requirement the runtime did not enforce — a deployment could
* satisfy the documentation and still start with the setting missing, which is worse than an
* undocumented setting because it reads as covered.
*
* <p>Each machine-readable condition — {@code <property>=<value>} — becomes a case: enable the
* condition, omit the property, and require the context to fail. Prose conditions ("… is
* configured") are deliberately excluded and are exactly the rows whose rule is a relationship
* between two settings rather than a switch; {@code RedisSdkSettingsTest} covers those.
*
* <p>A row that cannot pass this test has two honest fixes and one dishonest one. Enforce the
* requirement, or weaken the declaration to what is true. Deleting the case is the third.
*/
class RequiredWhenIsEnforcedTest {
private static final Path REGISTRY =
Path.of("..", "..", "..", "..", "docs", "registries", "env-keys.yaml");
/** Conditions this test can drive: a property, an equals sign, and a literal. */
private record Condition(String property, String value) {
static Optional<Condition> parse(String declared) {
if (declared == null || !declared.contains("=") || declared.contains(" ")) {
return Optional.empty();
}
int equals = declared.indexOf('=');
return Optional.of(
new Condition(
declared.substring(0, equals).strip(), declared.substring(equals + 1).strip()));
}
}
@TestFactory
@DisplayName("every declared required_when condition is refused at startup when unmet")
List<DynamicTest> everyRequiredWhenIsEnforced() throws IOException {
List<DynamicTest> cases = new ArrayList<>();
for (Map<String, Object> row : rows()) {
Object property = row.get("property");
Object declared = row.get("required_when");
if (!(property instanceof String bound) || !bound.startsWith("app.redis.")) {
continue;
}
Optional<Condition> condition =
Condition.parse(declared instanceof String text ? text : null);
if (condition.isEmpty() || "app.redis.enabled".equals(condition.get().property())) {
// `app.redis.enabled=true` scopes a setting to Redis being on; it does not claim the
// setting must be present. Those rows are the SDK's defaults and have them.
continue;
}
cases.add(
DynamicTest.dynamicTest(
row.get("name") + " is required when " + declared,
() -> assertRefused(bound, condition.get(), String.valueOf(row.get("name")))));
}
assertThat(cases)
.as("the registry declares conditional requirements; a run with none is a parse failure")
.isNotEmpty();
return cases;
}
private void assertRefused(String property, Condition condition, String envName) {
List<String> properties = new ArrayList<>();
properties.add("app.redis.enabled=true");
properties.add("app.redis.nodes=redis-a:6379");
properties.add(
"app.redis.authentication.credential-reference=secret://u@environment/APP_REDIS_PASSWORD");
properties.add(condition.property() + "=" + condition.value());
// Everything the condition itself needs in order to be reachable, minus the property under
// test — otherwise an unrelated earlier rule would fail the context and this case would pass
// for the wrong reason.
prerequisites(condition).forEach((key, value) -> properties.add(key + "=" + value));
properties.removeIf(entry -> entry.startsWith(property + "="));
new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(RedisSdkAutoConfiguration.class))
.withBean(
RedisSdkAutoConfiguration.RedisSecretSource.class,
() -> name -> Optional.of("resolved-" + name))
.withPropertyValues(properties.toArray(String[]::new))
.run(
context -> {
assertThat(context)
.as(
"%s declares it is required when %s=%s, so a context without it must not"
+ " start",
envName, condition.property(), condition.value())
.hasFailed();
assertThat(context.getStartupFailure())
.as("the failure must name the setting, not something downstream of it")
.hasStackTraceContaining(shortName(property));
});
}
/** The other settings a condition needs before the property under test can be the cause. */
private static Map<String, String> prerequisites(Condition condition) {
Map<String, String> extra = new LinkedHashMap<>();
if ("app.redis.mode".equals(condition.property())
&& "sentinel".equals(condition.value().toLowerCase(Locale.ROOT))) {
extra.put("app.redis.sentinel.master-name", "skeleton");
extra.put(
"app.redis.sentinel.credential-reference", "secret://s@environment/APP_REDIS_SENTINEL");
}
if ("app.redis.raw.enabled".equals(condition.property())) {
extra.put("app.redis.raw.credential-reference", "secret://r@environment/APP_REDIS_RAW");
extra.put("app.redis.raw.policy-resource", "classpath:redis-sdk/redis-command-policy.yml");
}
if ("app.redis.admin.enabled".equals(condition.property())) {
extra.put("app.redis.admin.credential-reference", "secret://a@environment/APP_REDIS_ADMIN");
}
return extra;
}
/** The last segment of the property, which is what a failure message can be expected to name. */
private static String shortName(String property) {
int dot = property.lastIndexOf('.');
return dot < 0 ? property : property.substring(dot + 1);
}
@SuppressWarnings("unchecked")
private static List<Map<String, Object>> rows() throws IOException {
try (InputStream registry = Files.newInputStream(REGISTRY)) {
Map<String, Object> parsed = new Yaml().load(registry);
return (List<Map<String, Object>>) parsed.get("env_keys");
}
}
}
+4 -1
View File
@@ -37,7 +37,10 @@ moving a type between packages.
- `profile` depends publicly only on `api`.
- transport packages never reach back into the gateways.
- `resilience` never depends on a transport — retry eligibility is transport-neutral.
- no production package depends on `testkit`.
- no production package depends on `testkit`. The testkit lives in its own `testkit` source set
(`src/testkit/java`), consumed by the `test`, `httpClientPerformanceTest` and `jmh` lanes; its
dependencies are declared only on the test configurations, so production still cannot reach it.
`PlatformClasses` is the single definition of "production classes" the boundary rules import.
- Stable code never references `http3`.
- `org.springframework.web.service.registry` appears only in `spring7`.
- `RestTemplate` appears only in `migration`.
+46 -7
View File
@@ -50,7 +50,8 @@ dependencies {
// Testkit dependencies (design §28.1 test topology). They are test-scoped so no production
// module can depend on the testkit.
// module can depend on the testkit; the testkit source set inherits them by extending
// testImplementation, and the test lanes use them directly.
testImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0'
testImplementation 'com.squareup.okhttp3:okhttp-tls:4.12.0'
testImplementation 'org.testcontainers:testcontainers'
@@ -61,34 +62,69 @@ dependencies {
testImplementation 'io.projectreactor.tools:blockhound:1.0.17.RELEASE'
}
// Performance certification and JMH benchmarks are separate source sets: they are slow, they assert
// on resource bounds rather than behaviour, and they must never be part of the default unit lane.
// The testkit is its own source set, not part of `test`, because three lanes consume it and only
// one of them is a test lane. Reaching into `sourceSets.test.output` from `jmh` compiled under
// Gradle and could not be modelled by the IDE at all: a source set is test source there only when
// a Test task runs its output, `jmh` is driven by JavaExec, and IDE main source may not read IDE
// test source — so every testkit reference in the benchmarks was an unresolved type in the editor
// while the build was green. A source set nobody runs tests from is main source for all three
// consumers, which is what it always was.
//
// Performance certification and JMH benchmarks are separate source sets for their own reason: they
// are slow, they assert on resource bounds rather than behaviour, and they must never be part of
// the default unit lane.
sourceSets {
testkit {
java.srcDir 'src/testkit/java'
compileClasspath += sourceSets.main.output
runtimeClasspath += output + compileClasspath
}
httpClientPerformanceTest {
java.srcDir 'src/httpClientPerformanceTest/java'
compileClasspath += sourceSets.main.output + sourceSets.test.output
compileClasspath += sourceSets.main.output + sourceSets.testkit.output
runtimeClasspath += output + compileClasspath
}
jmh {
java.srcDir 'src/jmh/java'
compileClasspath += sourceSets.main.output + sourceSets.test.output
compileClasspath += sourceSets.main.output + sourceSets.testkit.output
runtimeClasspath += output + compileClasspath
}
}
configurations {
// The testkit compiles against exactly what a test does: testImplementation already extends
// implementation, so this is the module's own dependencies plus the test libraries.
testkitImplementation.extendsFrom testImplementation
testkitRuntimeOnly.extendsFrom testRuntimeOnly
httpClientPerformanceTestImplementation.extendsFrom testImplementation
httpClientPerformanceTestRuntimeOnly.extendsFrom testRuntimeOnly
jmhImplementation.extendsFrom testImplementation
jmhRuntimeOnly.extendsFrom testRuntimeOnly
}
// Every test lane compiles and runs against the testkit.
sourceSets.test {
compileClasspath += sourceSets.testkit.output
runtimeClasspath += sourceSets.testkit.output
}
dependencies {
// Testkit dependencies (design §28.1 test topology). They stay off the production
// configurations, so no production module can depend on the testkit.
testkitImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0'
testkitImplementation 'com.squareup.okhttp3:okhttp-tls:4.12.0'
testkitImplementation 'org.testcontainers:testcontainers'
testkitImplementation 'org.testcontainers:testcontainers-junit-jupiter'
testkitImplementation 'org.testcontainers:testcontainers-toxiproxy'
testkitImplementation 'io.projectreactor:reactor-test'
testkitImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0'
testkitImplementation 'io.projectreactor.tools:blockhound:1.0.17.RELEASE'
jmhImplementation 'org.openjdk.jmh:jmh-core:1.37'
jmhAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.37'
}
tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' }
// UTF-8 is pinned for every JavaCompile task in the root build; this leaf no longer repeats it.
// JMH generates its harness classes at compile time. They are not our source, so the
// compile-time checker and -Werror are switched off for that source set only; applying them
@@ -103,7 +139,10 @@ tasks.named('spotbugsJmh') {
enabled = false
}
Closure<Void> applyContractSelection = { Test task ->
// Takes a Test task. The parameter is left untyped because the IDE's Gradle parser has no Gradle
// API on its classpath and reports the annotation as an unresolved type; Groovy dispatches the
// calls below dynamically either way.
Closure<Void> applyContractSelection = { task ->
// Cross-transport contract lane. The same semantic contract runs against every Stable transport;
// the transport under test is selected explicitly so a missing transport is an error, not a skip.
task.systemProperty 'httpclient.contract.transports',
+204 -204
View File
@@ -1,251 +1,251 @@
# This is a Gradle generated file for dependency locking.
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
biz.aQute.bnd:biz.aQute.bnd.annotation:7.1.0=httpClientPerformanceTestCompileClasspath,jmhCompileClasspath,testCompileClasspath
ch.qos.logback:logback-classic:1.5.21=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
ch.qos.logback:logback-core:1.5.21=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.fasterxml.jackson.core:jackson-annotations:2.20=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.fasterxml.jackson.core:jackson-core:2.20.1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.fasterxml.jackson.core:jackson-databind:2.20.1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.fasterxml.jackson:jackson-bom:2.20.1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.github.ben-manes.caffeine:caffeine:3.2.3=annotationProcessor,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor
com.github.docker-java:docker-java-api:3.7.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.github.docker-java:docker-java-transport-zerodep:3.7.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.github.docker-java:docker-java-transport:3.7.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.github.kevinstern:software-and-algorithms:1.0=annotationProcessor,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor
biz.aQute.bnd:biz.aQute.bnd.annotation:7.1.0=httpClientPerformanceTestCompileClasspath,jmhCompileClasspath,testCompileClasspath,testkitCompileClasspath
ch.qos.logback:logback-classic:1.5.21=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
ch.qos.logback:logback-core:1.5.21=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.fasterxml.jackson.core:jackson-annotations:2.20=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.fasterxml.jackson.core:jackson-core:2.20.1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.fasterxml.jackson.core:jackson-databind:2.20.1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.fasterxml.jackson:jackson-bom:2.20.1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.github.ben-manes.caffeine:caffeine:3.2.3=annotationProcessor,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor
com.github.docker-java:docker-java-api:3.7.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.github.docker-java:docker-java-transport-zerodep:3.7.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.github.docker-java:docker-java-transport:3.7.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.github.kevinstern:software-and-algorithms:1.0=annotationProcessor,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor
com.github.spotbugs:spotbugs-annotations:4.10.2=spotbugs
com.github.spotbugs:spotbugs-annotations:4.8.6=httpClientPerformanceTestCompileClasspath,jmhCompileClasspath,testCompileClasspath
com.github.spotbugs:spotbugs-annotations:4.8.6=httpClientPerformanceTestCompileClasspath,jmhCompileClasspath,testCompileClasspath,testkitCompileClasspath
com.github.spotbugs:spotbugs:4.10.2=spotbugs
com.github.stephenc.jcip:jcip-annotations:1.0-1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath
com.google.auto.service:auto-service-annotations:1.0.1=annotationProcessor,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor
com.google.auto.value:auto-value-annotations:1.9=annotationProcessor,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor
com.google.auto:auto-common:1.2.2=annotationProcessor,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor
com.google.code.findbugs:jsr305:3.0.2=checkstyle,httpClientPerformanceTestCompileClasspath,jmhCompileClasspath,spotbugs,testCompileClasspath
com.google.code.gson:gson:2.13.2=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath
com.google.errorprone:error_prone_annotation:2.49.0=annotationProcessor,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor
com.google.errorprone:error_prone_annotations:2.41.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath
com.github.stephenc.jcip:jcip-annotations:1.0-1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.google.auto.service:auto-service-annotations:1.0.1=annotationProcessor,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor
com.google.auto.value:auto-value-annotations:1.9=annotationProcessor,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor
com.google.auto:auto-common:1.2.2=annotationProcessor,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor
com.google.code.findbugs:jsr305:3.0.2=checkstyle,httpClientPerformanceTestCompileClasspath,jmhCompileClasspath,spotbugs,testCompileClasspath,testkitCompileClasspath
com.google.code.gson:gson:2.13.2=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.google.errorprone:error_prone_annotation:2.49.0=annotationProcessor,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor
com.google.errorprone:error_prone_annotations:2.41.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.google.errorprone:error_prone_annotations:2.47.0=checkstyle
com.google.errorprone:error_prone_annotations:2.49.0=annotationProcessor,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor
com.google.errorprone:error_prone_check_api:2.49.0=annotationProcessor,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor
com.google.errorprone:error_prone_core:2.49.0=annotationProcessor,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor
com.google.googlejavaformat:google-java-format:1.35.0=annotationProcessor,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor
com.google.guava:failureaccess:1.0.3=annotationProcessor,checkstyle,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor
com.google.guava:guava:33.5.0-jre=annotationProcessor,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor
com.google.errorprone:error_prone_annotations:2.49.0=annotationProcessor,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor
com.google.errorprone:error_prone_check_api:2.49.0=annotationProcessor,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor
com.google.errorprone:error_prone_core:2.49.0=annotationProcessor,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor
com.google.googlejavaformat:google-java-format:1.35.0=annotationProcessor,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor
com.google.guava:failureaccess:1.0.3=annotationProcessor,checkstyle,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor
com.google.guava:guava:33.5.0-jre=annotationProcessor,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor
com.google.guava:guava:33.6.0-jre=checkstyle
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,checkstyle,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor
com.google.j2objc:j2objc-annotations:3.1=annotationProcessor,checkstyle,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor
com.google.protobuf:protobuf-java:4.33.2=annotationProcessor,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,checkstyle,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor
com.google.j2objc:j2objc-annotations:3.1=annotationProcessor,checkstyle,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor
com.google.protobuf:protobuf-java:4.33.2=annotationProcessor,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor
com.h3xstream.findsecbugs:findsecbugs-plugin:1.14.0=spotbugsPlugins
com.jayway.jsonpath:json-path:2.9.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.nimbusds:content-type:2.3=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.nimbusds:lang-tag:1.7=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.nimbusds:nimbus-jose-jwt:10.4=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.nimbusds:oauth2-oidc-sdk:11.26.1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.jayway.jsonpath:json-path:2.9.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.nimbusds:content-type:2.3=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.nimbusds:lang-tag:1.7=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.nimbusds:nimbus-jose-jwt:10.4=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.nimbusds:oauth2-oidc-sdk:11.26.1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.puppycrawl.tools:checkstyle:13.5.0=checkstyle
com.squareup.okhttp3:mockwebserver:4.12.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.squareup.okhttp3:okhttp-tls:4.12.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.squareup.okhttp3:okhttp:4.12.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.squareup.okio:okio-jvm:3.6.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.squareup.okio:okio:3.6.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.tngtech.archunit:archunit-junit5-api:1.3.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=httpClientPerformanceTestRuntimeClasspath,jmhRuntimeClasspath,testRuntimeClasspath
com.tngtech.archunit:archunit-junit5-engine:1.3.0=httpClientPerformanceTestRuntimeClasspath,jmhRuntimeClasspath,testRuntimeClasspath
com.tngtech.archunit:archunit-junit5:1.3.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.tngtech.archunit:archunit:1.3.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.vaadin.external.google:android-json:0.0.20131108.vaadin1=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.squareup.okhttp3:mockwebserver:4.12.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.squareup.okhttp3:okhttp-tls:4.12.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.squareup.okhttp3:okhttp:4.12.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.squareup.okio:okio-jvm:3.6.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.squareup.okio:okio:3.6.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.tngtech.archunit:archunit-junit5-api:1.3.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=httpClientPerformanceTestRuntimeClasspath,jmhRuntimeClasspath,testRuntimeClasspath,testkitRuntimeClasspath
com.tngtech.archunit:archunit-junit5-engine:1.3.0=httpClientPerformanceTestRuntimeClasspath,jmhRuntimeClasspath,testRuntimeClasspath,testkitRuntimeClasspath
com.tngtech.archunit:archunit-junit5:1.3.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.tngtech.archunit:archunit:1.3.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.vaadin.external.google:android-json:0.0.20131108.vaadin1=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
commons-beanutils:commons-beanutils:1.11.0=checkstyle
commons-codec:commons-codec:1.19.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
commons-codec:commons-codec:1.19.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
commons-collections:commons-collections:3.2.2=checkstyle
commons-io:commons-io:2.20.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
commons-io:commons-io:2.20.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
commons-io:commons-io:2.21.0=spotbugs
commons-logging:commons-logging:1.3.5=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
eu.rekawek.toxiproxy:toxiproxy-java:2.1.11=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
commons-logging:commons-logging:1.3.5=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
eu.rekawek.toxiproxy:toxiproxy-java:2.1.11=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
info.picocli:picocli:4.7.7=checkstyle
io.github.eisop:dataflow-errorprone:3.41.0-eisop1=annotationProcessor,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor
io.github.java-diff-utils:java-diff-utils:4.12=annotationProcessor,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor
io.github.resilience4j:resilience4j-bulkhead:2.2.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.github.resilience4j:resilience4j-circuitbreaker:2.2.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.github.resilience4j:resilience4j-core:2.2.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.github.resilience4j:resilience4j-micrometer:2.2.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.github.resilience4j:resilience4j-ratelimiter:2.2.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.github.resilience4j:resilience4j-retry:2.2.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.github.resilience4j:resilience4j-timelimiter:2.2.0=httpClientPerformanceTestRuntimeClasspath,jmhRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.micrometer:micrometer-commons:1.16.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.micrometer:micrometer-core:1.16.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.micrometer:micrometer-observation:1.16.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-buffer:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-codec-base:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-codec-classes-quic:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-codec-compression:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-codec-dns:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-codec-http2:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-codec-http3:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-codec-http:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-codec-native-quic:4.2.17.Final=httpClientPerformanceTestRuntimeClasspath,jmhRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.netty:netty-codec-socks:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-common:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-handler-proxy:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-handler:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-resolver-dns-classes-macos:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-resolver-dns-native-macos:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-resolver-dns:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-resolver:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-transport-classes-epoll:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-transport-native-epoll:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-transport-native-unix-common:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-transport:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.projectreactor.netty:reactor-netty-core:1.3.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.projectreactor.netty:reactor-netty-http:1.3.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.projectreactor.tools:blockhound:1.0.17.RELEASE=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.projectreactor:reactor-core:3.8.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.projectreactor:reactor-test:3.8.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
jakarta.activation:jakarta.activation-api:2.1.4=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
jakarta.annotation:jakarta.annotation-api:3.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
jakarta.xml.bind:jakarta.xml.bind-api:4.0.4=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
javax.inject:javax.inject:1=annotationProcessor,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor
io.github.eisop:dataflow-errorprone:3.41.0-eisop1=annotationProcessor,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor
io.github.java-diff-utils:java-diff-utils:4.12=annotationProcessor,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor
io.github.resilience4j:resilience4j-bulkhead:2.2.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.github.resilience4j:resilience4j-circuitbreaker:2.2.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.github.resilience4j:resilience4j-core:2.2.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.github.resilience4j:resilience4j-micrometer:2.2.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.github.resilience4j:resilience4j-ratelimiter:2.2.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.github.resilience4j:resilience4j-retry:2.2.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.github.resilience4j:resilience4j-timelimiter:2.2.0=httpClientPerformanceTestRuntimeClasspath,jmhRuntimeClasspath,runtimeClasspath,testRuntimeClasspath,testkitRuntimeClasspath
io.micrometer:micrometer-commons:1.16.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.micrometer:micrometer-core:1.16.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.micrometer:micrometer-observation:1.16.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.netty:netty-buffer:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.netty:netty-codec-base:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.netty:netty-codec-classes-quic:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.netty:netty-codec-compression:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.netty:netty-codec-dns:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.netty:netty-codec-http2:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.netty:netty-codec-http3:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.netty:netty-codec-http:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.netty:netty-codec-native-quic:4.2.17.Final=httpClientPerformanceTestRuntimeClasspath,jmhRuntimeClasspath,runtimeClasspath,testRuntimeClasspath,testkitRuntimeClasspath
io.netty:netty-codec-socks:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.netty:netty-common:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.netty:netty-handler-proxy:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.netty:netty-handler:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.netty:netty-resolver-dns-classes-macos:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.netty:netty-resolver-dns-native-macos:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.netty:netty-resolver-dns:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.netty:netty-resolver:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.netty:netty-transport-classes-epoll:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.netty:netty-transport-native-epoll:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.netty:netty-transport-native-unix-common:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.netty:netty-transport:4.2.17.Final=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.projectreactor.netty:reactor-netty-core:1.3.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.projectreactor.netty:reactor-netty-http:1.3.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.projectreactor.tools:blockhound:1.0.17.RELEASE=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.projectreactor:reactor-core:3.8.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.projectreactor:reactor-test:3.8.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
jakarta.activation:jakarta.activation-api:2.1.4=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
jakarta.annotation:jakarta.annotation-api:3.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
jakarta.xml.bind:jakarta.xml.bind-api:4.0.4=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
javax.inject:javax.inject:1=annotationProcessor,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor
jaxen:jaxen:2.0.0=spotbugs
junit:junit:4.13.2=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.bytebuddy:byte-buddy-agent:1.17.8=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.bytebuddy:byte-buddy:1.17.8=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.java.dev.jna:jna:5.18.1=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.minidev:accessors-smart:2.6.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
net.minidev:json-smart:2.6.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
junit:junit:4.13.2=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
net.bytebuddy:byte-buddy-agent:1.17.8=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
net.bytebuddy:byte-buddy:1.17.8=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
net.java.dev.jna:jna:5.18.1=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
net.minidev:accessors-smart:2.6.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
net.minidev:json-smart:2.6.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
net.sf.jopt-simple:jopt-simple:5.0.4=jmhAnnotationProcessor,jmhCompileClasspath,jmhRuntimeClasspath
net.sf.saxon:Saxon-HE:12.9=checkstyle,spotbugs
org.antlr:antlr4-runtime:4.13.2=checkstyle
org.apache.bcel:bcel:6.12.0=spotbugs
org.apache.commons:commons-compress:1.28.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apache.commons:commons-lang3:3.20.0=checkstyle,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath
org.apache.commons:commons-compress:1.28.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.apache.commons:commons-lang3:3.20.0=checkstyle,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.apache.commons:commons-math3:3.6.1=jmhAnnotationProcessor,jmhCompileClasspath,jmhRuntimeClasspath
org.apache.commons:commons-text:1.15.0=spotbugs
org.apache.commons:commons-text:1.3=checkstyle
org.apache.httpcomponents.client5:httpclient5:5.5.1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apache.httpcomponents.core5:httpcore5-h2:5.3.6=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apache.httpcomponents.core5:httpcore5:5.3.6=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apache.httpcomponents.client5:httpclient5:5.5.1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.apache.httpcomponents.core5:httpcore5-h2:5.3.6=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.apache.httpcomponents.core5:httpcore5:5.3.6=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.apache.httpcomponents:httpclient:4.5.13=checkstyle
org.apache.httpcomponents:httpcore:4.4.16=checkstyle
org.apache.logging.log4j:log4j-api:2.25.2=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath
org.apache.logging.log4j:log4j-api:2.25.2=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.apache.logging.log4j:log4j-core:2.25.2=spotbugs
org.apache.logging.log4j:log4j-to-slf4j:2.25.2=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apache.logging.log4j:log4j-to-slf4j:2.25.2=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.apache.maven.doxia:doxia-core:1.12.0=checkstyle
org.apache.maven.doxia:doxia-logging-api:1.12.0=checkstyle
org.apache.maven.doxia:doxia-module-xdoc:1.12.0=checkstyle
org.apache.maven.doxia:doxia-sink-api:1.12.0=checkstyle
org.apache.tomcat.embed:tomcat-embed-core:11.0.14=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apache.tomcat.embed:tomcat-embed-el:11.0.14=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apache.tomcat.embed:tomcat-embed-websocket:11.0.14=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apache.tomcat.embed:tomcat-embed-core:11.0.14=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.apache.tomcat.embed:tomcat-embed-el:11.0.14=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.apache.tomcat.embed:tomcat-embed-websocket:11.0.14=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.apache.xbean:xbean-reflect:3.7=checkstyle
org.apiguardian:apiguardian-api:1.1.2=compileClasspath,httpClientPerformanceTestCompileClasspath,jmhCompileClasspath,testCompileClasspath
org.assertj:assertj-core:3.27.6=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.awaitility:awaitility:4.3.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apiguardian:apiguardian-api:1.1.2=compileClasspath,httpClientPerformanceTestCompileClasspath,jmhCompileClasspath,testCompileClasspath,testkitCompileClasspath
org.assertj:assertj-core:3.27.6=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.awaitility:awaitility:4.3.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.codehaus.plexus:plexus-classworlds:2.6.0=checkstyle
org.codehaus.plexus:plexus-component-annotations:2.1.0=checkstyle
org.codehaus.plexus:plexus-container-default:2.1.0=checkstyle
org.codehaus.plexus:plexus-utils:3.3.0=checkstyle
org.dom4j:dom4j:2.2.0=spotbugs
org.eclipse.jetty.compression:jetty-compression-common:12.1.4=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty.compression:jetty-compression-gzip:12.1.4=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty.http3:jetty-http3-client-transport:12.1.4=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty.http3:jetty-http3-client:12.1.4=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty.http3:jetty-http3-common:12.1.4=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty.http3:jetty-http3-qpack:12.1.4=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty.quic:jetty-quic-api:12.1.4=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty.quic:jetty-quic-client:12.1.4=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty.quic:jetty-quic-common:12.1.4=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty.quic:jetty-quic-util:12.1.4=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-alpn-client:12.1.4=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-client:12.1.4=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-http:12.1.4=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-io:12.1.4=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-util:12.1.4=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.hamcrest:hamcrest-core:3.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.hamcrest:hamcrest:3.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.hdrhistogram:HdrHistogram:2.2.2=httpClientPerformanceTestRuntimeClasspath,jmhRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.eclipse.jetty.compression:jetty-compression-common:12.1.4=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.eclipse.jetty.compression:jetty-compression-gzip:12.1.4=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.eclipse.jetty.http3:jetty-http3-client-transport:12.1.4=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.eclipse.jetty.http3:jetty-http3-client:12.1.4=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.eclipse.jetty.http3:jetty-http3-common:12.1.4=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.eclipse.jetty.http3:jetty-http3-qpack:12.1.4=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.eclipse.jetty.quic:jetty-quic-api:12.1.4=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.eclipse.jetty.quic:jetty-quic-client:12.1.4=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.eclipse.jetty.quic:jetty-quic-common:12.1.4=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.eclipse.jetty.quic:jetty-quic-util:12.1.4=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.eclipse.jetty:jetty-alpn-client:12.1.4=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.eclipse.jetty:jetty-client:12.1.4=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.eclipse.jetty:jetty-http:12.1.4=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.eclipse.jetty:jetty-io:12.1.4=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.eclipse.jetty:jetty-util:12.1.4=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.hamcrest:hamcrest-core:3.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.hamcrest:hamcrest:3.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.hdrhistogram:HdrHistogram:2.2.2=httpClientPerformanceTestRuntimeClasspath,jmhRuntimeClasspath,runtimeClasspath,testRuntimeClasspath,testkitRuntimeClasspath
org.javassist:javassist:3.28.0-GA=checkstyle
org.jetbrains.kotlin:kotlin-stdlib-common:2.2.21=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.2.21=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.2.21=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-stdlib:2.2.21=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.jetbrains:annotations:17.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.jspecify:jspecify:1.0.0=annotationProcessor,checkstyle,compileClasspath,httpClientPerformanceTestAnnotationProcessor,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhAnnotationProcessor,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-api:6.0.1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-engine:6.0.1=httpClientPerformanceTestRuntimeClasspath,jmhRuntimeClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-params:6.0.1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter:6.0.1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-commons:6.0.1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-engine:6.0.1=httpClientPerformanceTestRuntimeClasspath,jmhRuntimeClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-launcher:6.0.1=httpClientPerformanceTestRuntimeClasspath,jmhRuntimeClasspath,testRuntimeClasspath
org.junit:junit-bom:6.0.1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-stdlib-common:2.2.21=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.2.21=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.2.21=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.jetbrains.kotlin:kotlin-stdlib:2.2.21=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.jetbrains:annotations:17.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.jspecify:jspecify:1.0.0=annotationProcessor,checkstyle,compileClasspath,httpClientPerformanceTestAnnotationProcessor,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhAnnotationProcessor,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath,testkitAnnotationProcessor,testkitCompileClasspath,testkitRuntimeClasspath
org.junit.jupiter:junit-jupiter-api:6.0.1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.junit.jupiter:junit-jupiter-engine:6.0.1=httpClientPerformanceTestRuntimeClasspath,jmhRuntimeClasspath,testRuntimeClasspath,testkitRuntimeClasspath
org.junit.jupiter:junit-jupiter-params:6.0.1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.junit.jupiter:junit-jupiter:6.0.1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.junit.platform:junit-platform-commons:6.0.1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.junit.platform:junit-platform-engine:6.0.1=httpClientPerformanceTestRuntimeClasspath,jmhRuntimeClasspath,testRuntimeClasspath,testkitRuntimeClasspath
org.junit.platform:junit-platform-launcher:6.0.1=httpClientPerformanceTestRuntimeClasspath,jmhRuntimeClasspath,testRuntimeClasspath,testkitRuntimeClasspath
org.junit:junit-bom:6.0.1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.junit:junit-bom:6.1.0=spotbugs
org.latencyutils:LatencyUtils:2.0.3=httpClientPerformanceTestRuntimeClasspath,jmhRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.mockito:mockito-core:5.20.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,mockitoAgent,testCompileClasspath,testRuntimeClasspath
org.mockito:mockito-junit-jupiter:5.20.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.objenesis:objenesis:3.3=httpClientPerformanceTestRuntimeClasspath,jmhRuntimeClasspath,testRuntimeClasspath
org.latencyutils:LatencyUtils:2.0.3=httpClientPerformanceTestRuntimeClasspath,jmhRuntimeClasspath,runtimeClasspath,testRuntimeClasspath,testkitRuntimeClasspath
org.mockito:mockito-core:5.20.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,mockitoAgent,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.mockito:mockito-junit-jupiter:5.20.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.objenesis:objenesis:3.3=httpClientPerformanceTestRuntimeClasspath,jmhRuntimeClasspath,testRuntimeClasspath,testkitRuntimeClasspath
org.openjdk.jmh:jmh-core:1.37=jmhAnnotationProcessor,jmhCompileClasspath,jmhRuntimeClasspath
org.openjdk.jmh:jmh-generator-annprocess:1.37=jmhAnnotationProcessor
org.opentest4j:opentest4j:1.3.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.osgi:org.osgi.annotation.bundle:2.0.0=httpClientPerformanceTestCompileClasspath,jmhCompileClasspath,testCompileClasspath
org.osgi:org.osgi.annotation.versioning:1.1.2=httpClientPerformanceTestCompileClasspath,jmhCompileClasspath,testCompileClasspath
org.osgi:org.osgi.resource:1.0.0=httpClientPerformanceTestCompileClasspath,jmhCompileClasspath,testCompileClasspath
org.osgi:org.osgi.service.serviceloader:1.0.0=httpClientPerformanceTestCompileClasspath,jmhCompileClasspath,testCompileClasspath
org.opentest4j:opentest4j:1.3.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.osgi:org.osgi.annotation.bundle:2.0.0=httpClientPerformanceTestCompileClasspath,jmhCompileClasspath,testCompileClasspath,testkitCompileClasspath
org.osgi:org.osgi.annotation.versioning:1.1.2=httpClientPerformanceTestCompileClasspath,jmhCompileClasspath,testCompileClasspath,testkitCompileClasspath
org.osgi:org.osgi.resource:1.0.0=httpClientPerformanceTestCompileClasspath,jmhCompileClasspath,testCompileClasspath,testkitCompileClasspath
org.osgi:org.osgi.service.serviceloader:1.0.0=httpClientPerformanceTestCompileClasspath,jmhCompileClasspath,testCompileClasspath,testkitCompileClasspath
org.ow2.asm:asm-analysis:9.10.1=spotbugs
org.ow2.asm:asm-commons:9.10.1=spotbugs
org.ow2.asm:asm-tree:9.10.1=spotbugs
org.ow2.asm:asm-util:9.10.1=spotbugs
org.ow2.asm:asm:9.10.1=spotbugs
org.ow2.asm:asm:9.7.1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.pcollections:pcollections:4.0.1=annotationProcessor,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor
org.reactivestreams:reactive-streams:1.0.4=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.ow2.asm:asm:9.7.1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.pcollections:pcollections:4.0.1=annotationProcessor,httpClientPerformanceTestAnnotationProcessor,jmhAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor
org.reactivestreams:reactive-streams:1.0.4=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.reflections:reflections:0.10.2=checkstyle
org.rnorth.duct-tape:duct-tape:1.0.8=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.skyscreamer:jsonassert:1.5.3=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.slf4j:jul-to-slf4j:2.0.17=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.slf4j:slf4j-api:2.0.17=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,spotbugs,spotbugsSlf4j,testCompileClasspath,testRuntimeClasspath
org.rnorth.duct-tape:duct-tape:1.0.8=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.skyscreamer:jsonassert:1.5.3=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.slf4j:jul-to-slf4j:2.0.17=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.slf4j:slf4j-api:2.0.17=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,spotbugs,spotbugsSlf4j,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.slf4j:slf4j-simple:2.0.17=checkstyle,spotbugsSlf4j
org.springframework.boot:spring-boot-autoconfigure:4.0.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-http-client:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-http-converter:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-jackson:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-restclient:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-resttestclient:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-servlet:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-jackson-test:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-jackson:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-logging:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-test:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-tomcat-runtime:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-tomcat:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-webmvc-test:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-webmvc:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-test-autoconfigure:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-test:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-tomcat:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-web-server:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-webmvc-test:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-webmvc:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot:4.0.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.security:spring-security-core:7.0.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.security:spring-security-crypto:7.0.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.security:spring-security-oauth2-client:7.0.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.security:spring-security-oauth2-core:7.0.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.security:spring-security-web:7.0.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-aop:7.0.1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-beans:7.0.1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-context:7.0.1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-core:7.0.1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-expression:7.0.1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-test:7.0.1=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-web:7.0.1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-webflux:7.0.1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-webmvc:7.0.1=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.testcontainers:testcontainers-junit-jupiter:2.0.2=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.testcontainers:testcontainers-toxiproxy:2.0.2=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.testcontainers:testcontainers:2.0.2=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-autoconfigure:4.0.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-http-client:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-http-converter:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-jackson:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-restclient:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-resttestclient:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-servlet:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-starter-jackson-test:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-starter-jackson:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-starter-logging:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-starter-test:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-starter-tomcat-runtime:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-starter-tomcat:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-starter-webmvc-test:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-starter-webmvc:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-starter:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-test-autoconfigure:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-test:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-tomcat:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-web-server:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-webmvc-test:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-webmvc:4.0.0=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot:4.0.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.security:spring-security-core:7.0.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.security:spring-security-crypto:7.0.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.security:spring-security-oauth2-client:7.0.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.security:spring-security-oauth2-core:7.0.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.security:spring-security-web:7.0.0=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework:spring-aop:7.0.1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework:spring-beans:7.0.1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework:spring-context:7.0.1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework:spring-core:7.0.1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework:spring-expression:7.0.1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework:spring-test:7.0.1=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework:spring-web:7.0.1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework:spring-webflux:7.0.1=compileClasspath,httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework:spring-webmvc:7.0.1=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.testcontainers:testcontainers-junit-jupiter:2.0.2=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.testcontainers:testcontainers-toxiproxy:2.0.2=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.testcontainers:testcontainers:2.0.2=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.xmlresolver:xmlresolver:5.3.3=checkstyle,spotbugs
org.xmlunit:xmlunit-core:2.10.4=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.yaml:snakeyaml:2.5=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
tools.jackson.core:jackson-core:3.0.2=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
tools.jackson.core:jackson-databind:3.0.2=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
tools.jackson:jackson-bom:3.0.2=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.xmlunit:xmlunit-core:2.10.4=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.yaml:snakeyaml:2.5=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
tools.jackson.core:jackson-core:3.0.2=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
tools.jackson.core:jackson-databind:3.0.2=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
tools.jackson:jackson-bom:3.0.2=httpClientPerformanceTestCompileClasspath,httpClientPerformanceTestRuntimeClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
empty=
@@ -1,8 +1,6 @@
package dev.caskeleton.adapter.outbound.httpclient.architecture;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition;
import org.junit.jupiter.api.Test;
@@ -15,10 +13,7 @@ import org.junit.jupiter.api.Test;
*/
class HttpClientModuleBoundaryTest {
private static final JavaClasses PLATFORM =
new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.importPackages("dev.caskeleton.adapter.outbound.httpclient");
private static final JavaClasses PLATFORM = PlatformClasses.production();
@Test
void coreApiDependsOnNothingInsideThePlatform() {
@@ -0,0 +1,40 @@
package dev.caskeleton.adapter.outbound.httpclient.architecture;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
/**
* The platform's production classes, as every boundary rule in this module sees them.
*
* <p>Shared rather than repeated because "which classes are production" is one decision, and three
* rule classes silently disagreeing about it is how a boundary stops being enforced.
*
* <p>ArchUnit's {@code DO_NOT_INCLUDE_TESTS} recognises the conventional test output locations, and
* the testkit is no longer in one: it is its own source set, so it compiles to {@code
* build/classes/java/testkit}. Left in, the fixtures would be imported as production code and every
* rule here would be asserted against them — starting with the one that says production code never
* depends on the testkit, which the testkit itself trivially does.
*/
public final class PlatformClasses {
private static final ImportOption NOT_THE_TESTKIT_SOURCE_SET =
location -> !location.contains("/classes/java/testkit/");
private static final JavaClasses PRODUCTION =
new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.withImportOption(NOT_THE_TESTKIT_SOURCE_SET)
.importPackages("dev.caskeleton.adapter.outbound.httpclient");
private PlatformClasses() {}
/**
* Returns the platform's production classes.
*
* @return every compiled class of this module that is neither a test nor a testkit fixture
*/
public static JavaClasses production() {
return PRODUCTION;
}
}
@@ -1,8 +1,6 @@
package dev.caskeleton.adapter.outbound.httpclient.architecture;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@@ -16,10 +14,7 @@ import org.junit.jupiter.api.Test;
@Tag("httpclient-spring62-surface")
class PublicApiArchitectureTest {
private static final JavaClasses PLATFORM =
new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.importPackages("dev.caskeleton.adapter.outbound.httpclient");
private static final JavaClasses PLATFORM = PlatformClasses.production();
@Test
void publicApiDoesNotExposeNativeEnginesOrUnsafeBuilders() {
@@ -1,10 +1,9 @@
package dev.caskeleton.adapter.outbound.httpclient.migration;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition;
import dev.caskeleton.adapter.outbound.httpclient.architecture.PlatformClasses;
import org.junit.jupiter.api.Test;
/**
@@ -15,10 +14,7 @@ import org.junit.jupiter.api.Test;
*/
class RestTemplateBoundaryTest {
private static final JavaClasses PLATFORM_CLASSES =
new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.importPackages("dev.caskeleton.adapter.outbound.httpclient");
private static final JavaClasses PLATFORM_CLASSES = PlatformClasses.production();
static ArchRule restTemplateIsConfinedToMigration() {
return ArchRuleDefinition.noClasses()
+37 -4
View File
@@ -30,7 +30,37 @@ adapters implement application/domain ports directly and must not depend on this
`audit/DomainContextAuditContextPort`) — see "Persistence auditing contract" below.
- Vendor SPI extension points shared by all RDBMS vendors:
- `outbox/OutboxClaimRepository` — vendor module implements claim strategy (e.g. FOR UPDATE SKIP LOCKED).
- `idempotency/IdempotencyClaimRepository` — vendor implements insert-or-expired-reclaim.
- `failure/SqlStateErrorMapping` — vendor module contributes vendor-specific SQLState rows.
- `transaction/TransactionLocalTimeoutConfigurer` — vendor applies statement/lock guards.
## Vendor selection
Two vendor compositions live in this module, each in its own subpackage, each registering the same
four SPI beans:
| Vendor | Package | Selected by | Schema owner |
| --- | --- | --- | --- |
| PostgreSQL | `.postgresql` | `ca-skeleton.persistence.vendor=postgresql` (also the default) | Flyway, `db/migration/postgresql` |
| H2 | `.h2` | `ca-skeleton.persistence.vendor=h2` | Hibernate `ddl-auto`, entities only |
`config/PersistenceVendorSettings` binds the selector to an enum, so an unknown value fails at
startup instead of loading neither composition and surfacing as a missing `OutboxClaimRepository`.
The profiles state the choice: `application-local.yml` selects H2, `application-dev.yml` and
`application-prod.yml` select PostgreSQL, and `PersistenceVendorProdSafetyValidator` (app-bootstrap)
refuses H2 under prod whatever property source supplies it.
H2 is the local-development datastore, not a second production target. It has no migration tree, so
tables that exist only in migrations — capability schema registry, polling-delivery and inbox
streams, the Spring Integration lock table — do not exist under it. Vendor concurrency and migration
fidelity stay with `postgresqlIntegrationTest`.
Two H2 statements diverge from PostgreSQL and the reasons are measured, not assumed (H2 2.4.240):
- the outbox claim is identical — H2 accepts `FOR UPDATE SKIP LOCKED` and genuinely skips locked
rows, so the claim keeps its meaning;
- the idempotency claim is not — H2 has no `INSERT ... ON CONFLICT ... RETURNING`, so it is a
`MERGE ... USING` with the same three outcomes. `H2ClaimSqlTest` executes both against a real H2.
### Capability-gated stores
@@ -59,11 +89,12 @@ should not exist.
- Repository adapters owning `@Transactional` boundaries — the application use case owns
the transaction via `TransactionPort` (see
[application-core/CLAUDE.md](../../../application-core/CLAUDE.md)).
- **DB drivers** (`org.postgresql..`) or **`org.flywaydb.database.postgresql..`** — those are
vendor-specific and belong only in this module's `.postgresql` package; NoSQL-specific dependencies
belong only in their own future modules
- **DB drivers** (`org.postgresql..`, `org.h2..`) or **`org.flywaydb.database.postgresql..`** —
those are vendor-specific and belong only in this module's matching vendor package (`.postgresql`,
`.h2`); NoSQL-specific dependencies belong only in their own future modules
(persistence-multi-db-extensibility D3). This is enforced by ArchUnit
`persistence_rdbms_stays_vendor_neutral` in `CleanArchitectureTest`.
`PERSISTENCE_RDBMS_STAYS_VENDOR_NEUTRAL` and `PERSISTENCE_RDBMS_STAYS_NEUTRAL_OF_H2` in
`CleanArchitectureTest`.
- NoSQL adapter code. MongoDB/Redis/DynamoDB adapters are sibling modules, not children of this module.
- Any sibling persistence or inbound/outbound adapter not allowed by the registry.
@@ -128,6 +159,8 @@ framework-neutral `shared.error.PersistenceFailureException` carrying one of the
| `40P01` | `DB_DEADLOCK` | PostgreSQL (`.postgresql` package) |
| `25P03` | `DB_IDLE_IN_TX_TIMEOUT` | PostgreSQL |
| `57014` | `DB_QUERY_CANCELED` | PostgreSQL |
| `23513` | `DB_CHECK_VIOLATION` | H2 (`.h2` package) — H2 reports CHECK as 23513, not the standard 23514 the core table maps |
| `HYT00` | `DB_QUERY_CANCELED` | H2 — H2 collapses statement and lock timeout into one state |
- A repository adapter that catches a `DataAccessException` calls
`translator.translate(ex)` and rethrows the carrier (`ifPresent(e -> { throw e; })`);
@@ -35,6 +35,12 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-flyway'
runtimeOnly 'org.postgresql:postgresql'
runtimeOnly 'org.flywaydb:flyway-database-postgresql'
// Vendor (H2): the local-profile driver. Used only by the .h2 subpackage, which reaches it
// through JDBC/JPA rather than by importing org.h2 types — the same shape as the PostgreSQL
// driver above. Not `developmentOnly`: local is a deployable profile of this artifact, and the
// vendor selector, not the packaging, decides which driver a deployment loads.
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
postgresqlIntegrationTestImplementation 'org.testcontainers:testcontainers-postgresql'
@@ -37,6 +37,7 @@ com.google.guava:guava:33.6.0-jre=checkstyle
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,checkstyle,postgresqlIntegrationTestAnnotationProcessor,testAnnotationProcessor
com.google.j2objc:j2objc-annotations:3.1=annotationProcessor,checkstyle,postgresqlIntegrationTestAnnotationProcessor,testAnnotationProcessor
com.google.protobuf:protobuf-java:4.33.2=annotationProcessor,postgresqlIntegrationTestAnnotationProcessor,testAnnotationProcessor
com.h2database:h2:2.4.240=postgresqlIntegrationTestRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
com.h3xstream.findsecbugs:findsecbugs-plugin:1.14.0=spotbugsPlugins
com.jayway.jsonpath:json-path:2.9.0=postgresqlIntegrationTestCompileClasspath,postgresqlIntegrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.puppycrawl.tools:checkstyle:13.5.0=checkstyle
@@ -0,0 +1,36 @@
package dev.caskeleton.adapter.outbound.persistence.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Selects which RDBMS vendor composition this deployment runs.
*
* <p>The selector is a property rather than a profile name because the vendor is a property of the
* datastore, not of the environment that happens to use it. A fork that runs PostgreSQL under a
* profile named something other than {@code dev}/{@code prod}, or that wants H2 in a throwaway
* demo, sets this key; it does not have to rename its profiles or edit a condition.
*
* <p>Binding to an enum is what makes an unknown vendor a startup failure. With a raw string the
* two {@code @ConditionalOnProperty} vendor configurations would both stay off, and the first
* missing SPI bean would surface as a {@code NoSuchBeanDefinitionException} naming
* {@code OutboxClaimRepository} — a symptom several layers away from the misspelled value that
* caused it.
*/
@ConfigurationProperties(prefix = PersistenceVendorSettings.PREFIX)
public record PersistenceVendorSettings(Vendor vendor) {
public static final String PREFIX = "ca-skeleton.persistence";
public static final String VENDOR_PROPERTY = PREFIX + ".vendor";
/** The RDBMS vendors this repository composes a persistence adapter for. */
public enum Vendor {
POSTGRESQL,
H2
}
public PersistenceVendorSettings {
// Absent means PostgreSQL: the vendor every deployment before this selector existed ran, so an
// upgrade that does not set the key keeps its datastore.
vendor = vendor == null ? Vendor.POSTGRESQL : vendor;
}
}
@@ -0,0 +1,96 @@
package dev.caskeleton.adapter.outbound.persistence.h2;
import dev.caskeleton.adapter.outbound.persistence.idempotency.IdempotencyClaimRepository;
import dev.caskeleton.adapter.outbound.persistence.idempotency.entity.IdempotencyRecordEntity;
import jakarta.persistence.EntityManager;
import java.time.Instant;
import java.util.Optional;
import java.util.UUID;
import org.jspecify.annotations.Nullable;
/**
* H2 atomic scope claim.
*
* <p>H2 has no {@code INSERT ... ON CONFLICT ... DO UPDATE ... RETURNING}, so the PostgreSQL
* statement does not port. The standard {@code MERGE ... USING} does, and carries the same
* meaning in one statement:
*
* <ul>
* <li>no row for the scope → {@code WHEN NOT MATCHED} inserts the claim (1 row);
* <li>a live row → neither branch fires (0 rows), so the caller lost to a live winner;
* <li>an expired row → {@code WHEN MATCHED AND expires_at <= now} takes it over (1 row).
* </ul>
*
* <p>One statement rather than select-then-insert is what keeps the SPI's promise not to poison the
* caller transaction: a losing claim returns zero updated rows, never a constraint violation the
* surrounding transaction would have to absorb.
*
* <p>No {@code RETURNING} is needed. The PostgreSQL statement returns {@code EXCLUDED.id}, which is
* the proposed id on both branches, so a claimed row is always this caller's proposed id.
*/
public final class H2IdempotencyClaimRepository implements IdempotencyClaimRepository {
private static final String CLAIM_SQL =
"""
MERGE INTO idempotency_record t
USING (VALUES (
CAST(:id AS uuid), CAST(:tenant AS varchar(128)), CAST(:principal AS varchar(256)),
CAST(:idempotencyKey AS varchar(256)), CAST(:useCaseName AS varchar(256)),
CAST(:requestHash AS varchar(64)),
CAST(:createdAt AS timestamp(6) with time zone),
CAST(:expiresAt AS timestamp(6) with time zone)
)) AS s (id, tenant, principal, idempotency_key, use_case_name,
request_hash, created_at, expires_at)
ON t.tenant = s.tenant
AND t.principal = s.principal
AND t.idempotency_key = s.idempotency_key
AND t.use_case_name = s.use_case_name
WHEN MATCHED AND t.expires_at <= :now THEN UPDATE SET
id = s.id,
request_hash = s.request_hash,
status = 'IN_FLIGHT',
response_payload = NULL,
response_ref = NULL,
created_at = s.created_at,
expires_at = s.expires_at
WHEN NOT MATCHED THEN INSERT (
id, tenant, principal, idempotency_key, use_case_name,
request_hash, status, response_payload, response_ref, created_at, expires_at
) VALUES (
s.id, s.tenant, s.principal, s.idempotency_key, s.use_case_name,
s.request_hash, 'IN_FLIGHT', NULL, NULL, s.created_at, s.expires_at
)
""";
private final EntityManager entityManager;
public H2IdempotencyClaimRepository(EntityManager entityManager) {
this.entityManager = entityManager;
}
@Override
public Optional<UUID> tryClaim(
IdempotencyRecordEntity proposed,
Instant now,
@Nullable IdempotencyRecordEntity exactExpiredEntity) {
// Same detach as the PostgreSQL path: a managed copy of the row this statement is about to
// overwrite would be flushed back over the claim at commit.
if (exactExpiredEntity != null && entityManager.contains(exactExpiredEntity)) {
entityManager.detach(exactExpiredEntity);
}
int claimed =
entityManager
.createNativeQuery(CLAIM_SQL)
.setParameter("id", proposed.getId())
.setParameter("tenant", proposed.getTenant())
.setParameter("principal", proposed.getPrincipal())
.setParameter("idempotencyKey", proposed.getIdempotencyKey())
.setParameter("useCaseName", proposed.getUseCaseName())
.setParameter("requestHash", proposed.getRequestHash())
.setParameter("createdAt", proposed.getCreatedAt())
.setParameter("expiresAt", proposed.getExpiresAt())
.setParameter("now", now)
.executeUpdate();
return claimed == 1 ? Optional.of(proposed.getId()) : Optional.empty();
}
}
@@ -0,0 +1,55 @@
package dev.caskeleton.adapter.outbound.persistence.h2;
import dev.caskeleton.adapter.outbound.persistence.transaction.EffectiveTransactionTimeouts;
import dev.caskeleton.adapter.outbound.persistence.transaction.TransactionLocalTimeoutConfigurer;
import java.time.Duration;
import java.util.Objects;
import org.springframework.jdbc.core.JdbcOperations;
/**
* Applies H2's timeout guards to the connection bound to the current transaction.
*
* <p>Two differences from the PostgreSQL configurer, both inherent to H2 rather than choices:
*
* <ul>
* <li><b>Session scope, not transaction scope.</b> PostgreSQL takes {@code set_config(..., true)}
* — a value that reverts at transaction end. H2's {@code SET} is session-wide and outlives
* the transaction on a pooled connection. It is not left stale in practice because the
* transaction port applies these before every transaction, so each one overwrites the last;
* a connection borrowed outside that path keeps the previous transaction's guard.
* <li><b>No idle-in-transaction guard.</b> H2 has no counterpart to
* {@code idle_in_transaction_session_timeout}, so that budget cannot be pushed into the
* database here. It is left to the caller-side deadline the transaction port already
* enforces, rather than silently reported as applied.
* </ul>
*
* <p>The millisecond values are inlined because H2's {@code SET} takes no bind parameter. They
* arrive as {@link Duration}s from validated settings, never from request input, and a negative one
* is rejected below rather than concatenated.
*/
public final class H2LocalTimeoutConfigurer implements TransactionLocalTimeoutConfigurer {
private static final String STATEMENT_TIMEOUT_SQL = "SET QUERY_TIMEOUT ";
private static final String LOCK_TIMEOUT_SQL = "SET LOCK_TIMEOUT ";
private final JdbcOperations jdbcOperations;
public H2LocalTimeoutConfigurer(JdbcOperations jdbcOperations) {
this.jdbcOperations = Objects.requireNonNull(jdbcOperations, "jdbcOperations must be non-null");
}
@Override
public void apply(EffectiveTransactionTimeouts timeouts) {
Objects.requireNonNull(timeouts, "timeouts must be non-null");
apply(STATEMENT_TIMEOUT_SQL, "statementTimeout", timeouts.statementTimeout());
apply(LOCK_TIMEOUT_SQL, "lockTimeout", timeouts.lockTimeout());
}
private void apply(String command, String name, Duration timeout) {
long milliseconds = timeout.toMillis();
if (milliseconds < 0) {
throw new IllegalArgumentException(name + " must not be negative, but was " + timeout);
}
jdbcOperations.execute(command + milliseconds);
}
}
@@ -0,0 +1,55 @@
package dev.caskeleton.adapter.outbound.persistence.h2;
import dev.caskeleton.adapter.outbound.persistence.outbox.OutboxClaimRepository;
import dev.caskeleton.adapter.outbound.persistence.outbox.entity.OutboxEventEntity;
import jakarta.persistence.EntityManager;
import java.time.Instant;
import java.util.List;
/**
* H2 {@link OutboxClaimRepository}.
*
* <p>The statement is character-for-character the PostgreSQL one, because H2 2.4 accepts {@code FOR
* UPDATE SKIP LOCKED} and honours it: a probe holding a row lock on one connection saw a concurrent
* {@code SKIP LOCKED} claim return zero rows rather than block or read through the lock. The claim
* therefore keeps its meaning here — competing relay workers take disjoint rows — instead of
* degrading to a serialised scan.
*
* <p>Kept as its own class rather than shared with the PostgreSQL implementation: the SPI exists so
* a vendor can diverge, and the packages are the boundary ArchUnit enforces. A shared "portable
* SQL" base would make the next H2-only fix a change to PostgreSQL's claim path.
*/
public final class H2OutboxClaimRepository implements OutboxClaimRepository {
private static final String CLAIM_SQL =
"""
SELECT * FROM outbox_event o
WHERE o.next_attempt_at <= :now
AND o.status IN ('PENDING', 'FAILED', 'IN_FLIGHT')
AND NOT EXISTS (
SELECT 1 FROM outbox_event p
WHERE p.aggregate_id = o.aggregate_id
AND p.occurred_at < o.occurred_at
AND p.status <> 'PUBLISHED'
)
ORDER BY o.occurred_at ASC
LIMIT :limit
FOR UPDATE SKIP LOCKED
""";
private final EntityManager entityManager;
public H2OutboxClaimRepository(EntityManager entityManager) {
this.entityManager = entityManager;
}
@Override
@SuppressWarnings("unchecked")
public List<OutboxEventEntity> claimEligible(Instant now, int limit) {
return entityManager
.createNativeQuery(CLAIM_SQL, OutboxEventEntity.class)
.setParameter("now", now)
.setParameter("limit", limit)
.getResultList();
}
}
@@ -0,0 +1,68 @@
package dev.caskeleton.adapter.outbound.persistence.h2;
import dev.caskeleton.adapter.outbound.persistence.config.PersistenceJpaConfig;
import dev.caskeleton.adapter.outbound.persistence.config.PersistenceVendorSettings;
import dev.caskeleton.adapter.outbound.persistence.failure.SqlStateErrorMapping;
import dev.caskeleton.adapter.outbound.persistence.idempotency.IdempotencyClaimRepository;
import dev.caskeleton.adapter.outbound.persistence.outbox.OutboxClaimRepository;
import dev.caskeleton.adapter.outbound.persistence.transaction.TransactionLocalTimeoutConfigurer;
import jakarta.persistence.EntityManager;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.core.JdbcOperations;
/**
* H2 vendor persistence configuration — the same four SPI beans the PostgreSQL vendor registers,
* implemented against H2. Selected by {@code ca-skeleton.persistence.vendor=h2}, which the
* {@code local} profile sets.
*
* <p><b>No Flyway location customizer, deliberately.</b> The PostgreSQL vendor points Flyway at
* {@code classpath:db/migration/postgresql}; there is no H2 equivalent tree, because the local
* profile turns Flyway off and lets Hibernate derive the schema from the entities. Two
* consequences worth stating out loud:
*
* <ul>
* <li>Tables that exist only in migrations — the capability schema registry, the polling-delivery
* and inbox streams, the Spring Integration lock table — are not created under H2. The
* capabilities that own them are off by default in the local profile, and turning one on
* there will fail on a missing table rather than silently misbehave.
* <li>A fork that enables Flyway while this vendor is selected gets no location override, so
* Flyway falls back to {@code classpath:db/migration} and walks the whole tree — including
* PostgreSQL DDL H2 cannot parse. Such a fork should register its own
* {@code FlywayConfigurationCustomizer} naming an H2 location.
* </ul>
*
* <p>Local therefore verifies wiring and behaviour, not migrations. Migration and vendor-concurrency
* fidelity stay with the real-PostgreSQL integration suites.
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(
prefix = PersistenceVendorSettings.PREFIX,
name = "vendor",
havingValue = "h2")
@Import(PersistenceJpaConfig.class)
public class H2PersistenceConfig {
@Bean
public OutboxClaimRepository outboxClaimRepository(EntityManager entityManager) {
return new H2OutboxClaimRepository(entityManager);
}
@Bean
public SqlStateErrorMapping h2SqlStateErrorMapping() {
return new H2SqlStateErrorMapping();
}
@Bean
public TransactionLocalTimeoutConfigurer transactionLocalTimeoutConfigurer(
JdbcOperations jdbcOperations) {
return new H2LocalTimeoutConfigurer(jdbcOperations);
}
@Bean
public IdempotencyClaimRepository idempotencyClaimRepository(EntityManager entityManager) {
return new H2IdempotencyClaimRepository(entityManager);
}
}
@@ -0,0 +1,41 @@
package dev.caskeleton.adapter.outbound.persistence.h2;
import dev.caskeleton.adapter.outbound.persistence.failure.SqlStateErrorMapping;
import dev.caskeleton.shared.error.OperationalError;
import java.util.Map;
/**
* H2-specific {@link SqlStateErrorMapping} rows.
*
* <p>H2 emits the standard SQLStates for unique ({@code 23505}) and not-null ({@code 23502})
* violations, which the vendor-neutral matrix already covers. Two states it does not share are
* below; both were read off a running H2 2.4.240 rather than inferred from the standard.
*
* <table>
* <caption>H2 vendor rows</caption>
* <tr><th>SQLState</th><th>code</th><th>why</th></tr>
* <tr>
* <td>{@code 23513}</td><td>{@code DB_CHECK_VIOLATION}</td>
* <td>H2 reports a failed CHECK constraint as 23513, not the 23514 the neutral matrix maps.
* Without this row a check violation falls through as an unmapped INTERNAL.</td>
* </tr>
* <tr>
* <td>{@code HYT00}</td><td>{@code DB_QUERY_CANCELED}</td>
* <td>H2 collapses every timeout-guard expiry into one state. PostgreSQL splits the same
* ground across 57014 (statement) and 55P03 (lock) and this repository maps only 57014,
* so DB_QUERY_CANCELED is the existing code for "a guard stopped the statement".</td>
* </tr>
* </table>
*/
public final class H2SqlStateErrorMapping implements SqlStateErrorMapping {
private static final Map<String, OperationalError> MAPPINGS =
Map.of(
"23513", OperationalError.DB_CHECK_VIOLATION,
"HYT00", OperationalError.DB_QUERY_CANCELED);
@Override
public Map<String, OperationalError> exactMappings() {
return MAPPINGS;
}
}
@@ -1,11 +1,13 @@
package dev.caskeleton.adapter.outbound.persistence.postgresql;
import dev.caskeleton.adapter.outbound.persistence.config.PersistenceJpaConfig;
import dev.caskeleton.adapter.outbound.persistence.config.PersistenceVendorSettings;
import dev.caskeleton.adapter.outbound.persistence.failure.SqlStateErrorMapping;
import dev.caskeleton.adapter.outbound.persistence.idempotency.IdempotencyClaimRepository;
import dev.caskeleton.adapter.outbound.persistence.outbox.OutboxClaimRepository;
import dev.caskeleton.adapter.outbound.persistence.transaction.TransactionLocalTimeoutConfigurer;
import jakarta.persistence.EntityManager;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.flyway.autoconfigure.FlywayConfigurationCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -15,8 +17,17 @@ import org.springframework.jdbc.core.JdbcOperations;
/**
* PostgreSQL vendor persistence configuration: imports the core JPA config and registers the vendor
* {@code @Bean}s. See the module README.
*
* <p>{@code matchIfMissing = true} keeps PostgreSQL the default: this configuration was
* unconditional before {@link PersistenceVendorSettings} existed, and a deployment that never sets
* the selector must keep the vendor it already runs.
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(
prefix = PersistenceVendorSettings.PREFIX,
name = "vendor",
havingValue = "postgresql",
matchIfMissing = true)
@Import(PersistenceJpaConfig.class)
public class PostgreSqlPersistenceConfig {
@@ -32,9 +32,7 @@ import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.support.TransactionSynchronizationManager;
/**
@@ -43,8 +41,19 @@ import org.springframework.transaction.support.TransactionSynchronizationManager
* <p>Mutations require an application-owned primary read-write transaction. The row is locked
* before {@code clock_timestamp()} is evaluated, and every state change repeats the complete owner
* CAS tuple in SQL. Raw client idempotency keys never reach this adapter.
*
* <p>Deliberately carries no Spring stereotype. Both composition roots component-scan {@code
* dev.caskeleton.adapter}, so a {@code @Repository} here was registered in every deployment
* regardless of which idempotency provider was selected: {@code provider=jdbc} acquired an
* owner-safe V2 store it never asked for, and {@code provider=redis} acquired a second one beside
* its own. Both counts are what {@code IdempotencyProviderSelectionConfig} refuses, so a scan-
* registered store meant neither selection could start.
*
* <p>{@code ca-skeleton.capabilities.idempotency.provider} is {@code disabled | jdbc | redis} and
* has no value that selects this store, so nothing composes it today; the integration test
* constructs it directly. Giving it a selector is outstanding work, and it belongs with the
* registry entry for that property rather than with a stereotype that composes it everywhere.
*/
@Repository
public class PostgreSqlOwnerSafeIdempotencyStore implements IdempotencyStorePortV2 {
static final int INLINE_RESPONSE_MAX_BYTES = 8 * 1024;
@@ -258,7 +267,6 @@ public class PostgreSqlOwnerSafeIdempotencyStore implements IdempotencyStorePort
private final JdbcOperations jdbc;
private final SecureRandom secureRandom;
@Autowired
public PostgreSqlOwnerSafeIdempotencyStore(JdbcOperations jdbc) {
this(jdbc, new SecureRandom());
}
@@ -0,0 +1,94 @@
package dev.caskeleton.adapter.outbound.persistence.config;
import static org.assertj.core.api.Assertions.assertThat;
import dev.caskeleton.adapter.outbound.persistence.config.PersistenceVendorSettings.Vendor;
import dev.caskeleton.adapter.outbound.persistence.h2.H2PersistenceConfig;
import dev.caskeleton.adapter.outbound.persistence.postgresql.PostgreSqlPersistenceConfig;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Configuration;
/**
* Guards the vendor selector itself: the value binding, and the two conditions that turn a value
* into a composition.
*
* <p>The conditions are asserted on their declared metadata rather than by loading the two
* configurations, because both import the JPA entity/repository registration and would drag a live
* {@code EntityManagerFactory} into a test about a string. What the loaded composition then does
* against a real database is {@code H2ClaimSqlTest}'s job.
*/
class PersistenceVendorSelectionTest {
private final ApplicationContextRunner runner =
new ApplicationContextRunner().withUserConfiguration(VendorSettings.class);
@ParameterizedTest
@ValueSource(strings = {"postgresql", "POSTGRESQL", "h2", "H2"})
void bindsTheSupportedVendorsCaseInsensitively(String value) {
runner
.withPropertyValues(PersistenceVendorSettings.VENDOR_PROPERTY + "=" + value)
.run(
context -> {
assertThat(context).hasNotFailed();
assertThat(context.getBean(PersistenceVendorSettings.class).vendor())
.isEqualTo(Vendor.valueOf(value.toUpperCase(java.util.Locale.ROOT)));
});
}
@Test
void defaultsToPostgreSqlWhenTheSelectorIsAbsent() {
runner.run(
context ->
assertThat(context.getBean(PersistenceVendorSettings.class).vendor())
.isEqualTo(Vendor.POSTGRESQL));
}
@Test
void rejectsAnUnknownVendorAtStartupRatherThanComposingNothing() {
runner
.withPropertyValues(PersistenceVendorSettings.VENDOR_PROPERTY + "=mysql")
.run(
context -> {
assertThat(context).hasFailed();
assertThat(context.getStartupFailure())
.hasStackTraceContaining(PersistenceVendorSettings.VENDOR_PROPERTY);
});
}
@Test
void postgreSqlIsTheCompositionAKeylessDeploymentGets() {
ConditionalOnProperty condition =
PostgreSqlPersistenceConfig.class.getAnnotation(ConditionalOnProperty.class);
assertThat(condition).isNotNull();
assertThat(condition.prefix()).isEqualTo(PersistenceVendorSettings.PREFIX);
assertThat(condition.name()).containsExactly("vendor");
assertThat(condition.havingValue()).isEqualTo("postgresql");
assertThat(condition.matchIfMissing())
.as("an upgrade that never sets the selector must keep the vendor it already runs")
.isTrue();
}
@Test
void h2IsOnlyEverSelectedExplicitly() {
ConditionalOnProperty condition =
H2PersistenceConfig.class.getAnnotation(ConditionalOnProperty.class);
assertThat(condition).isNotNull();
assertThat(condition.prefix()).isEqualTo(PersistenceVendorSettings.PREFIX);
assertThat(condition.name()).containsExactly("vendor");
assertThat(condition.havingValue()).isEqualTo("h2");
assertThat(condition.matchIfMissing())
.as("an in-memory datastore must never be what a deployment gets by saying nothing")
.isFalse();
}
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(PersistenceVendorSettings.class)
static class VendorSettings {}
}
@@ -0,0 +1,226 @@
package dev.caskeleton.adapter.outbound.persistence.h2;
import static org.assertj.core.api.Assertions.assertThat;
import dev.caskeleton.adapter.outbound.persistence.idempotency.entity.IdempotencyRecordEntity;
import dev.caskeleton.adapter.outbound.persistence.outbox.entity.OutboxEventEntity;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.Properties;
import java.util.UUID;
import java.util.function.Function;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.orm.jpa.EntityManagerFactoryUtils;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.support.TransactionTemplate;
/**
* Runs the H2 vendor claim statements against a real H2, because their risk is dialect acceptance
* rather than branch logic. The idempotency claim in particular is not a translation of the
* PostgreSQL statement H2 has no {@code ON CONFLICT ... RETURNING}, so it is a {@code MERGE ...
* USING}, and only an execution proves that the substitution kept the three outcomes intact.
*
* <p>In-memory and process-local, so this stays an ordinary unit test: no container, no network,
* nothing to skip when Docker is absent. Real-PostgreSQL fidelity remains the job of the
* {@code postgresqlIntegrationTest} source set.
*/
class H2ClaimSqlTest {
private static final String TENANT = "tenant-a";
private static final String PRINCIPAL = "principal-a";
private static final String USE_CASE = "PlaceOrder";
private static LocalContainerEntityManagerFactoryBean factoryBean;
private static EntityManagerFactory entityManagerFactory;
private static TransactionTemplate transactionTemplate;
private Instant now;
@BeforeAll
static void startDatabase() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.h2.Driver");
// Same URL shape as application-local.yml, so the test exercises the dialect and identifier
// folding the local profile actually runs.
dataSource.setUrl(
"jdbc:h2:mem:h2claimsql;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1");
dataSource.setUsername("sa");
dataSource.setPassword("");
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
vendorAdapter.setGenerateDdl(true);
Properties jpaProperties = new Properties();
jpaProperties.setProperty("hibernate.hbm2ddl.auto", "create-drop");
factoryBean = new LocalContainerEntityManagerFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setPersistenceUnitName("h2-claim-sql");
factoryBean.setPackagesToScan(
"dev.caskeleton.adapter.outbound.persistence.idempotency.entity",
"dev.caskeleton.adapter.outbound.persistence.outbox.entity");
factoryBean.setJpaVendorAdapter(vendorAdapter);
factoryBean.setJpaProperties(jpaProperties);
factoryBean.afterPropertiesSet();
entityManagerFactory = factoryBean.getObject();
JpaTransactionManager transactionManager = new JpaTransactionManager(entityManagerFactory);
transactionManager.afterPropertiesSet();
transactionTemplate = new TransactionTemplate(transactionManager);
}
@AfterAll
static void stopDatabase() {
factoryBean.destroy();
}
@BeforeEach
void clearTables() {
now = Instant.parse("2026-08-12T00:00:00Z");
inTransaction(
entityManager -> {
entityManager.createQuery("DELETE FROM IdempotencyRecordEntity").executeUpdate();
entityManager.createQuery("DELETE FROM OutboxEventEntity").executeUpdate();
return null;
});
}
@Test
void claimsAFreeScope() {
UUID proposed = UUID.randomUUID();
Optional<UUID> claimed = tryClaim(proposed, "key-1", now.plus(Duration.ofHours(1)), now);
assertThat(claimed).contains(proposed);
assertThat(storedStatus("key-1")).isEqualTo("IN_FLIGHT");
}
@Test
void losesToALiveClaimOnTheSameScope() {
UUID winner = UUID.randomUUID();
tryClaim(winner, "key-2", now.plus(Duration.ofHours(1)), now);
Optional<UUID> second =
tryClaim(UUID.randomUUID(), "key-2", now.plus(Duration.ofHours(1)), now);
assertThat(second).as("a live winner must not be displaced").isEmpty();
assertThat(storedId("key-2")).isEqualTo(winner);
}
@Test
void takesOverAnExpiredClaimOnTheSameScope() {
UUID abandoned = UUID.randomUUID();
Instant expiry = now.plus(Duration.ofHours(1));
tryClaim(abandoned, "key-3", expiry, now);
UUID reclaimer = UUID.randomUUID();
Optional<UUID> retaken = tryClaim(reclaimer, "key-3", expiry.plus(Duration.ofHours(2)), expiry);
assertThat(retaken).contains(reclaimer);
assertThat(storedId("key-3")).isEqualTo(reclaimer);
}
@Test
void claimsOnlyTheOldestUnpublishedEventPerAggregate() {
inTransaction(
entityManager -> {
entityManager.persist(event("evt-old", "agg-1", now.minusSeconds(60), now));
entityManager.persist(event("evt-new", "agg-1", now.minusSeconds(30), now));
entityManager.persist(event("evt-other", "agg-2", now.minusSeconds(10), now));
return null;
});
List<OutboxEventEntity> claimed = claimEligible(now, 10);
assertThat(claimed).extracting(OutboxEventEntity::getEventId).containsExactly("evt-old",
"evt-other");
}
@Test
void skipsEventsWhoseNextAttemptIsInTheFuture() {
inTransaction(
entityManager -> {
entityManager.persist(
event("evt-backoff", "agg-3", now.minusSeconds(60), now.plusSeconds(300)));
return null;
});
assertThat(claimEligible(now, 10)).isEmpty();
}
private Optional<UUID> tryClaim(UUID id, String key, Instant expiresAt, Instant asOf) {
return inTransaction(
entityManager ->
new H2IdempotencyClaimRepository(entityManager)
.tryClaim(
new IdempotencyRecordEntity(
id,
TENANT,
PRINCIPAL,
key,
USE_CASE,
"request-hash",
"IN_FLIGHT",
null,
null,
asOf,
expiresAt),
asOf,
null));
}
private List<OutboxEventEntity> claimEligible(Instant asOf, int limit) {
return inTransaction(
entityManager -> new H2OutboxClaimRepository(entityManager).claimEligible(asOf, limit));
}
private String storedStatus(String key) {
return inTransaction(entityManager -> stored(entityManager, key).getStatus());
}
private UUID storedId(String key) {
return inTransaction(entityManager -> stored(entityManager, key).getId());
}
private static IdempotencyRecordEntity stored(EntityManager entityManager, String key) {
return entityManager
.createQuery(
"SELECT r FROM IdempotencyRecordEntity r WHERE r.idempotencyKey = :key",
IdempotencyRecordEntity.class)
.setParameter("key", key)
.getSingleResult();
}
private static OutboxEventEntity event(
String eventId, String aggregateId, Instant occurredAt, Instant nextAttemptAt) {
OutboxEventEntity entity = new OutboxEventEntity();
entity.setEventId(eventId);
entity.setAggregateId(aggregateId);
entity.setEventType("OrderPlaced");
entity.setPayload("{}");
entity.setOccurredAt(occurredAt);
entity.setStatus("PENDING");
entity.setAttemptCount(0);
entity.setNextAttemptAt(nextAttemptAt);
entity.setCorrelationId("corr-1");
entity.setIdempotencyKey("idem-" + eventId);
return entity;
}
private <T> T inTransaction(Function<EntityManager, T> work) {
return transactionTemplate.execute(
status ->
work.apply(
EntityManagerFactoryUtils.getTransactionalEntityManager(entityManagerFactory)));
}
}
+1
View File
@@ -65,6 +65,7 @@ com.google.protobuf:protobuf-java:3.25.5=conditionalTransportTestRuntimeClasspat
com.google.protobuf:protobuf-java:4.33.2=annotationProcessor,conditionalTransportTestAnnotationProcessor,functionalTestAnnotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
com.graphql-java:graphql-java:25.0=conditionalTransportTestRuntimeClasspath
com.graphql-java:java-dataloader:6.0.0=conditionalTransportTestRuntimeClasspath
com.h2database:h2:2.4.240=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
com.h3xstream.findsecbugs:findsecbugs-plugin:1.14.0=spotbugsPlugins
com.jayway.jsonpath:json-path:2.9.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.networknt:json-schema-validator:3.0.2=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
@@ -2,6 +2,7 @@ package dev.caskeleton.bootstrap.idempotency;
import dev.caskeleton.application.idempotency.IdempotencyExecutor;
import dev.caskeleton.application.idempotency.IdempotencyStorePort;
import dev.caskeleton.application.idempotency.v2.IdempotencyExecutorV2;
import dev.caskeleton.application.idempotency.v2.IdempotencyStorePortV2;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.SmartInitializingSingleton;
@@ -18,10 +19,13 @@ import org.springframework.context.annotation.Configuration;
* therefore required a bean no provider could supply, and the deployment failed with "ambiguous or
* incomplete" while every configured provider was present and correct.
*
* <p>It also no longer requires a V2 executor. {@code IdempotencyExecutorV2} is written against
* that same unimplemented parent-package contract, so demanding one made every Redis deployment
* unstartable rather than proving anything. Driving the V2 store from an executor is real
* outstanding work; pretending the guard covers it is not the way to track it.
* <p>It requires a V2 executor alongside the V2 store. A store with nothing to drive it is a
* selection that cannot serve a request, and the executor is the only thing that turns the store's
* claim/start/complete transitions into a request-replay lifecycle. The requirement was dropped
* once, while the only {@code IdempotencyExecutorV2} was written against the unimplemented
* parent-package contract and so could never be composed; that executor now exists in {@code
* application.idempotency.v2} over the contract the stores implement, so the requirement is real
* again rather than a demand nothing could satisfy.
*/
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(IdempotencyProviderSettings.class)
@@ -32,17 +36,19 @@ public class IdempotencyProviderSelectionConfig {
IdempotencyProviderSettings settings,
ObjectProvider<IdempotencyStorePort> jdbcStores,
ObjectProvider<IdempotencyExecutor> jdbcExecutors,
ObjectProvider<IdempotencyStorePortV2> ownerSafeStores) {
ObjectProvider<IdempotencyStorePortV2> ownerSafeStores,
ObjectProvider<IdempotencyExecutorV2> ownerSafeExecutors) {
return () -> {
int jdbcStoreCount = count(jdbcStores);
int jdbcExecutorCount = count(jdbcExecutors);
int ownerSafeStoreCount = count(ownerSafeStores);
Counts found =
new Counts(
count(jdbcStores),
count(jdbcExecutors),
count(ownerSafeStores),
count(ownerSafeExecutors));
switch (settings.provider()) {
case DISABLED ->
requireCounts(jdbcStoreCount, jdbcExecutorCount, ownerSafeStoreCount, 0, 0, 0);
case JDBC -> requireCounts(jdbcStoreCount, jdbcExecutorCount, ownerSafeStoreCount, 1, 1, 0);
case REDIS ->
requireCounts(jdbcStoreCount, jdbcExecutorCount, ownerSafeStoreCount, 0, 0, 1);
case DISABLED -> require(found, new Counts(0, 0, 0, 0));
case JDBC -> require(found, new Counts(1, 1, 0, 0));
case REDIS -> require(found, new Counts(0, 0, 1, 1));
default ->
throw new IllegalStateException(
"Unsupported idempotency provider: " + settings.provider());
@@ -54,29 +60,29 @@ public class IdempotencyProviderSelectionConfig {
return Math.toIntExact(beans.stream().count());
}
private static void requireCounts(
int jdbcStores,
int jdbcExecutors,
int ownerSafeStores,
int expectedJdbcStores,
int expectedJdbcExecutors,
int expectedOwnerSafeStores) {
if (jdbcStores != expectedJdbcStores
|| jdbcExecutors != expectedJdbcExecutors
|| ownerSafeStores != expectedOwnerSafeStores) {
private static void require(Counts found, Counts expected) {
if (!found.equals(expected)) {
throw new IllegalStateException(
"Idempotency provider selection is ambiguous or incomplete: expected "
+ expectedJdbcStores
+ expected.describe()
+ ", but found "
+ found.describe());
}
}
/** The four bean counts the selection is exact about, in one comparable value. */
private record Counts(
int jdbcStores, int jdbcExecutors, int ownerSafeStores, int ownerSafeExecutors) {
String describe() {
return jdbcStores
+ " JDBC V1 store(s), "
+ expectedJdbcExecutors
+ " JDBC V1 executor(s) and "
+ expectedOwnerSafeStores
+ " owner-safe V2 store(s), but found "
+ jdbcStores
+ ", "
+ jdbcExecutors
+ " and "
+ ownerSafeStores);
+ " JDBC V1 executor(s), "
+ ownerSafeStores
+ " owner-safe V2 store(s) and "
+ ownerSafeExecutors
+ " owner-safe V2 executor(s)";
}
}
}
@@ -13,6 +13,7 @@ import dev.caskeleton.adapter.outbound.cache.redis.sdk.config.RedisSdkAutoConfig
import dev.caskeleton.adapter.outbound.cache.redis.sdk.config.RedisSdkSettings;
import dev.caskeleton.adapter.outbound.cache.redis.sdk.lettuce.connection.RedisRuntimeOwner;
import dev.caskeleton.application.cache.CacheRegionPort;
import dev.caskeleton.application.idempotency.v2.IdempotencyExecutorV2;
import dev.caskeleton.application.idempotency.v2.IdempotencyStorePortV2;
import dev.caskeleton.application.lease.DistributedLeasePort;
import dev.caskeleton.bootstrap.runtime.SecretSource;
@@ -270,6 +271,34 @@ public class RedisCapabilityConfig {
idempotency.getCommandTimeout());
}
/**
* The request-replay lifecycle driven over the selected owner-safe store.
*
* <p>Provider-neutral by type it depends on {@link IdempotencyStorePortV2}, not on Redis and
* composed here because this is where the settings behind it are bound. A selection that produced
* the store and no executor produced a store nothing could drive, which is what {@code
* IdempotencyProviderSelectionConfig} refuses.
*
* @param store the selected owner-safe store
* @param capabilities the capability settings
* @return the owner-safe request-replay executor
*/
@Bean
@ConditionalOnProperty(
name = "ca-skeleton.capabilities.idempotency.provider",
havingValue = "redis")
IdempotencyExecutorV2 idempotencyExecutorV2(
IdempotencyStorePortV2 store, RedisCapabilitySettings capabilities) {
RedisCapabilitySettings.Idempotency idempotency = capabilities.getIdempotency();
return new IdempotencyExecutorV2(
store,
idempotency.getProcessingLease(),
idempotency.getReplayTtl(),
idempotency.getFailureRetention(),
idempotency.getResponseCodecId(),
idempotency.getPolicyRevision());
}
/**
* Digests a semantic key before it reaches Redis.
*
@@ -395,7 +395,12 @@ public class RedisCapabilitySettings {
private Duration replayTtl = Duration.ofHours(24);
private Duration failureRetention = Duration.ofHours(24);
private String responseCodecId = "json-v2";
private String policyRevision = "request-replay-v2";
// The owner-safe claim carries this as a number, not a name: the store writes it into the
// record and compares it there, and `IdempotencyClaimRequest` rejects anything below 1. It
// used to be the string "request-replay-v2", which nothing read, because the only executor
// that could have read it was written against a contract nothing implements.
private int policyRevision = 2;
public boolean redisSelected() {
return "redis".equalsIgnoreCase(provider);
@@ -457,11 +462,11 @@ public class RedisCapabilitySettings {
this.responseCodecId = responseCodecId;
}
public String getPolicyRevision() {
public int getPolicyRevision() {
return policyRevision;
}
public void setPolicyRevision(String policyRevision) {
public void setPolicyRevision(int policyRevision) {
this.policyRevision = policyRevision;
}
}
@@ -0,0 +1,89 @@
package dev.caskeleton.bootstrap.runtime;
import dev.caskeleton.bootstrap.runtime.startup.StartupFailures;
import java.util.Locale;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.core.env.Environment;
/**
* Keeps the in-memory development datastore out of production.
*
* <p>{@code application-prod.yml} already pins {@code ca-skeleton.persistence.vendor=postgresql},
* but a YAML default only wins against the property sources quieter than it: an environment
* variable or a container override outranks every committed file. The two ways prod could end up on
* H2 are therefore checked at startup, where the source of the value no longer matters.
*
* <p>Both checks are worth having separately, because either one alone leaves a hole:
*
* <ul>
* <li>the <b>vendor</b> selector alone can be right while the URL points at H2 the adapter
* would issue PostgreSQL SQL against an in-memory database;
* <li>the <b>URL</b> alone can be PostgreSQL while the vendor selector says H2 the adapter
* would issue H2's MERGE claim against PostgreSQL.
* </ul>
*
* <p>An H2 URL also slips silently past {@link PostgreSqlTransportSecurityValidator}, which only
* inspects {@code jdbc:postgresql:} URLs: prod would lose its TLS requirement and its durability in
* the same move, and report neither.
*
* <p>Scoped to H2 by name rather than asserting "must be PostgreSQL", so a fork that adds a third
* production-grade vendor is not blocked by this validator.
*/
public final class PersistenceVendorProdSafetyValidator implements SmartInitializingSingleton {
static final String VENDOR_KEY = "ca-skeleton.persistence.vendor";
static final String JDBC_URL_KEY = "spring.datasource.url";
static final String JDBC_URL_ENV_KEY = "APP_DATASOURCE_URL";
private static final String PROD_PROFILE = "prod";
private static final String H2_VENDOR = "h2";
private static final String H2_URL_PREFIX = "jdbc:h2:";
private final Environment environment;
public PersistenceVendorProdSafetyValidator(Environment environment) {
this.environment = environment;
}
@Override
public void afterSingletonsInstantiated() {
if (!isProdActive()) {
return;
}
String vendor = normalized(environment.getProperty(VENDOR_KEY));
if (H2_VENDOR.equals(vendor)) {
throw StartupFailures.profileMismatch(
"prod profile forbids "
+ VENDOR_KEY
+ "=h2: the H2 composition is the in-memory local datastore and loses every write"
+ " when the process restarts");
}
String jdbcUrl = normalized(environment.getProperty(JDBC_URL_KEY));
// The URL is never echoed back it can carry credentials, endpoints and database names
// (same reason as PostgreSqlTransportSecurityValidator).
if (jdbcUrl != null && jdbcUrl.startsWith(H2_URL_PREFIX)) {
throw StartupFailures.profileMismatch(
"prod profile forbids an H2 datasource: "
+ JDBC_URL_ENV_KEY
+ " ("
+ JDBC_URL_KEY
+ ") resolves to a jdbc:h2: URL, which is the in-memory local datastore and also"
+ " bypasses the prod PostgreSQL TLS check");
}
}
private static String normalized(String value) {
return value == null ? null : value.trim().toLowerCase(Locale.ROOT);
}
private boolean isProdActive() {
for (String profile : environment.getActiveProfiles()) {
if (PROD_PROFILE.equalsIgnoreCase(profile)) {
return true;
}
}
return false;
}
}
@@ -40,6 +40,12 @@ public class RuntimeSafetyConfig {
return new PostgreSqlTransportSecurityValidator(environment);
}
@Bean
PersistenceVendorProdSafetyValidator persistenceVendorProdSafetyValidator(
Environment environment) {
return new PersistenceVendorProdSafetyValidator(environment);
}
@Bean
HikariPoolConstraintValidator hikariPoolConstraintValidator(Environment environment) {
return new HikariPoolConstraintValidator(environment);
@@ -0,0 +1,22 @@
# =============================================================================
# dev profile — PostgreSQL, Flyway-migrated.
#
# Deliberately thin. The datasource, pool, and every other operational value stay env-driven in
# application.yml and src/.env, because a shared dev database is an operator-supplied address, not
# a value this file can know. What belongs here is the shape dev must have whatever the operator
# sets: the vendor and the schema owner.
#
# Both keys below restate the repository default rather than change it, so adding this file moves
# no behaviour. That is the point — the moment dev and prod diverge from local, the difference has
# a declared home instead of being implied by whatever the environment happened to inject.
# =============================================================================
spring:
flyway:
# Flyway owns the schema from dev onward. Stated rather than inherited so switching a profile
# to H2 cannot silently carry a migration expectation with it.
enabled: true
ca-skeleton:
persistence:
vendor: postgresql
@@ -0,0 +1,183 @@
# =============================================================================
# local profile — in-memory H2, no external infrastructure, no environment.
#
# This file is a complete standalone configuration, not a patch. application.yml resolves ~70
# values from ${APP_*} placeholders that carry no inline default, so a launcher that does not
# inject src/.env dies during property binding — and because logback-spring.xml reads the same
# unresolved ca-skeleton.logging.* values, the logging system fails first and swallows the console
# output that would have said so. The observable symptom is a process that exits 1 having printed
# nothing.
#
# Only `./gradlew :app-bootstrap:bootRun` injects src/.env (see app-bootstrap/build.gradle). An IDE
# Run/Debug on CaSkeletonApplication, `java -jar`, and a bare container do not. Restating the
# required values here is what makes "local needs nothing" true for every launcher rather than for
# one Gradle task.
#
# The placeholders stay in application.yml untouched: dev and prod must keep failing loudly when an
# operator forgets a value. Local is the profile allowed to have answers of its own.
# ProfileSeparationContractTest#localProfileAnswersEveryRequiredPlaceholder fails the build when
# application.yml grows a required placeholder this file does not cover.
#
# What local verifies: wiring, request/response behaviour, application logic.
# What it does not: migrations and vendor behaviour — the schema here comes from the JPA entities,
# and the dev/prod path is the one Flyway proves.
# =============================================================================
spring:
application:
name: ca-skeleton
web:
error:
include-stacktrace: never
include-message: never
datasource:
# MODE=PostgreSQL keeps H2's dialect and semantics as close to the deployed vendor as H2 gets;
# DATABASE_TO_LOWER matches PostgreSQL's unquoted-identifier folding so the native SQL in the
# vendor adapters resolves the same table and column names in both.
#
# DB_CLOSE_DELAY=-1 is load-bearing, not decoration: an in-memory database is dropped when its
# last connection closes, and HikariCP closes idle connections. Without it the schema
# disappears mid-run the first time the pool goes idle.
url: jdbc:h2:mem:ca_skeleton;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1
username: sa
password: ""
driver-class-name: org.h2.Driver
hikari:
maximum-pool-size: 10
minimum-idle: 2
# Milliseconds. Must stay above the 3000 ms validation-timeout pinned in application.yml
# (HIKARI-CFG-C6), which HikariPoolConstraintValidator enforces at startup.
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
flyway:
# The migration tree is PostgreSQL DDL — DO $$ blocks and all — so there is nothing here for
# Flyway to apply. Off rather than pointed at an empty location: with Flyway on and no vendor
# location override it falls back to classpath:db/migration and walks the PostgreSQL tree.
enabled: false
jpa:
hibernate:
# Hibernate owns the local schema, which is only safe because the database is thrown away
# with the process. JpaSchemaSafetyValidator rejects this mode under prod.
ddl-auto: create-drop
show-sql: false
open-in-view: false
properties:
hibernate:
format_sql: false
security:
oauth2:
resourceserver:
jwt:
# The local Keycloak realm. Startup does not contact it — Spring defers JWKS resolution to
# first use — so an unauthenticated call such as GET /api/healthcheck works with no IdP
# running at all. Authenticated calls need it up.
issuer-uri: http://localhost:8081/realms/ca-skeleton
audiences: ca-skeleton-api
main:
banner-mode: console
lazy-initialization: false
log-startup-info: true
threads:
virtual:
enabled: true
jackson:
deserialization:
fail-on-unknown-properties: true
fail-on-null-for-primitives: true
fail-on-ignored-properties: true
datatype:
enum:
read-unknown-enum-values-as-null: false
datetime:
write-dates-as-timestamps: false
lifecycle:
timeout-per-shutdown-phase: 30s
server:
port: 8080
shutdown: graceful
# No proxy in front of a local run, so trusting X-Forwarded-* would let a caller forge its own
# client IP. dev/prod sit behind a load balancer and set this to framework via env.
forward-headers-strategy: none
tomcat:
threads:
max: 200
min-spare: 10
accept-count: 100
max-connections: 8192
connection-timeout: 20s
compression:
enabled: true
min-response-size: 1024
logging:
level:
root: INFO
dev.caskeleton: DEBUG
org.springframework: INFO
org.springframework.web: INFO
org.hibernate.SQL: WARN
ca-skeleton:
persistence:
# Selects H2PersistenceConfig instead of PostgreSqlPersistenceConfig: the claim SQL, SQLState
# rows and timeout guards differ per vendor even though the ports do not.
vendor: h2
bootstrap:
app-name: ca-skeleton
presentation:
# Pinned even though application.yml has an inline default, because the default is /v1 and
# src/.env says /api. Everything that names a URL in this repository — SECURITY_PUBLIC_PATHS,
# the compose healthchecks, bootstrapSmoke, the README curl — assumes /api, so a launcher that
# missed src/.env served /v1/healthcheck and answered 401 on the address the docs give.
api-base-path: /api
privacy:
# A blank salt makes PrivacySettings warn and fall back to a dev sentinel. Naming the dev value
# keeps the local pseudonymization stable across runs and keeps the warning for deployments
# that really did forget to supply one.
pseudonymization-salt: __LOCAL_DEV_pseudonymization_salt
idempotency:
ttl: 24h
security:
issuer-uri: http://localhost:8081/realms/ca-skeleton
audience: ca-skeleton-api
public-paths: /api/healthcheck
cors:
enabled: true
allowed-origins: http://localhost:3000
# Empty = the default method set (GET, POST, PATCH, PUT, DELETE, OPTIONS).
allowed-methods: ""
allowed-headers: "*"
allow-credentials: true
max-age-seconds: 3600
logging:
file:
# Console only. A local run should not quietly grow a logs/ directory in the working
# directory, which differs between a Gradle run and an IDE run.
enabled: false
path: logs/ca-skeleton.json
max-size: 100MB
max-history: 14
total-size-cap: 3GB
async:
enabled: true
queue-size: 512
discarding-threshold: 20
json:
timezone: UTC
timestamp-pattern: "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"
include-caller-data: false
logger-name-length: 0
# Keep every log line locally; prod samples <=INFO down to 10%.
sampling-rate: 1.0
app:
messaging:
# Blank = messaging disabled; the broker binds a fail-fast sentinel rather than a real client.
broker: ""
notification:
slack:
provider: ""
email:
provider: ""
@@ -0,0 +1,28 @@
# =============================================================================
# prod profile — PostgreSQL, Flyway-migrated.
#
# As thin as the dev profile, and for the same reason: production values are operator-supplied and
# live in the environment, not in a committed file. Only the shape is pinned here.
#
# The prod safety rules are NOT restated in this file. They are startup validators, because a
# forbidden value has to fail whether it arrives from this file, an env var, or a container
# override — a YAML default only wins against the sources that are quieter than it:
#
# JpaSchemaSafetyValidator ddl-auto must be none|validate (exit 71)
# FlywayProdSafetyValidator baseline-on-migrate / out-of-order / clean stay disarmed
# StartupSafetyValidator error-detail exposure and body-capture logging stay off
# PostgreSqlTransportSecurityValidator pgJDBC sslmode=verify-full
# PersistenceVendorProdSafetyValidator neither the vendor selector nor the URL may be H2
#
# The vendor pin below is therefore the readable statement of intent, not the enforcement: an env
# var outranks this file, so overriding it to h2 fails at startup rather than booting an in-memory
# database that loses every write on restart.
# =============================================================================
spring:
flyway:
enabled: true
ca-skeleton:
persistence:
vendor: postgresql
@@ -356,7 +356,8 @@ ca-skeleton:
replay-ttl: ${APP_IDEMPOTENCY_TTL:24h}
failure-retention: ${APP_IDEMPOTENCY_FAILURE_RETENTION:24h}
response-codec-id: json-v2
policy-revision: request-replay-v2
# Numeric: the owner-safe claim writes this into the record and compares it there.
policy-revision: 2
lease:
# disabled | redis. This is EFFICIENCY_ONLY and never supplies fencing.
provider: ${APP_LEASE_PROVIDER:disabled}
@@ -411,6 +412,18 @@ ca-skeleton:
lock:
wait-time: 3s
lease-ttl: 30s
# Which RDBMS vendor composition the persistence adapter registers: postgresql | h2. Bound to
# PersistenceVendorSettings (adapter-persistence), which rejects any other value at startup.
#
# A property rather than a profile check, because the vendor belongs to the datastore and not to
# the environment that happens to use it. The profiles then state their choice:
# application-local.yml selects h2, application-dev.yml and application-prod.yml select
# postgresql, and prod additionally refuses h2 through PersistenceVendorProdSafetyValidator.
#
# The value here is the default for a deployment that activates no profile at all — PostgreSQL,
# the vendor every deployment ran before this selector existed.
persistence:
vendor: postgresql
# JPA named-policy deadline envelope. JpaTransactionSettings validates the hierarchy;
# SpringPolicyTransactionPort intersects these limits with the caller's absolute CallBudget
# and the actual Hikari connection timeout before acquiring a transaction.
@@ -873,6 +873,25 @@ class CleanArchitectureTest {
+ ".postgresql subpackage of adapter:outbound:persistence-jpa.")
.allowEmptyShould(true);
// The same fence around the second vendor. H2 arrives as a runtimeOnly driver, so today nothing
// imports org.h2.. and this rule holds vacuously which is the point of writing it now: the
// first class that reaches for an H2 type has to do it inside the vendor package, before the
// "vendor-neutral base" claim quietly stops being true.
@ArchTest
static final ArchRule PERSISTENCE_RDBMS_STAYS_NEUTRAL_OF_H2 =
noClasses()
.that()
.resideInAPackage("dev.caskeleton.adapter.outbound.persistence..")
.and()
.resideOutsideOfPackage("dev.caskeleton.adapter.outbound.persistence.h2..")
.should()
.dependOnClassesThat()
.resideInAnyPackage("org.h2..")
.as(
"adapter:outbound:persistence-jpa (non-h2) must stay vendor-neutral: H2 driver types "
+ "live only in the .h2 subpackage of adapter:outbound:persistence-jpa.")
.allowEmptyShould(true);
@ArchTest
static final ArchRule PERSISTENCE_RDBMS_ENTITIES_DO_NOT_PIN_VENDOR_COLUMN_DEFINITIONS =
fields()
@@ -111,6 +111,32 @@ class DeveloperExperienceContractTest {
.contains("http://localhost:8080/api/healthcheck");
}
/**
* src/.env is the host-side source bootRun reads, so its datasource URL names a host port. A
* database the compose stack keeps on its internal network only is unreachable from there, and
* every host-side run dies in the startup migration phase with a connection refusal.
*/
@Test
void localComposePublishesTheHostPortTheCommittedDatasourceUrlTargets() throws IOException {
String datasourceUrl = envValue("APP_DATASOURCE_URL");
Matcher target = Pattern.compile("^jdbc:postgresql://([^:/]+):(\\d+)/").matcher(datasourceUrl);
assertThat(target.find())
.as("APP_DATASOURCE_URL must name an explicit host and port: %s", datasourceUrl)
.isTrue();
String host = target.group(1);
String hostPort = target.group(2);
assertThat(host)
.as("the committed datasource default is host-side, so it must resolve to the loopback")
.isIn("localhost", "127.0.0.1");
Map<?, ?> local = parseYamlMap(read("docker-compose.local.yml"));
Map<?, ?> database = requireMap(requireMap(local, "services"), "db");
assertThat(requireStringList(database, "ports"))
.as("a host-side run cannot reach a database the compose stack never publishes")
.contains("127.0.0.1:" + hostPort + ":5432");
}
@Test
void readmeCommandsAreVerifiedAndBootstrapIsTheFirstRunEntrypoint() throws IOException {
String build = read("src/build.gradle");
@@ -1022,6 +1048,17 @@ class DeveloperExperienceContractTest {
return Files.readString(REPOSITORY_ROOT.resolve(relative));
}
private static String envValue(String key) throws IOException {
String prefix = key + "=";
return read("src/.env")
.lines()
.map(String::trim)
.filter(line -> line.startsWith(prefix))
.map(line -> line.substring(prefix.length()).trim())
.findFirst()
.orElseThrow(() -> new AssertionError("src/.env does not declare " + key));
}
private static Map<?, ?> parseYamlMap(String source) {
LoaderOptions options = new LoaderOptions();
options.setAllowDuplicateKeys(false);
@@ -0,0 +1,282 @@
package dev.caskeleton.bootstrap.contract;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;
/**
* Pins the environment split: local runs on an in-memory database, every deployed profile runs on
* PostgreSQL with Flyway owning the schema.
*
* <p>These are file assertions rather than a booted context on purpose. Booting the real
* composition root inside this source set is not currently possible the component scan that makes
* {@code CaSkeletonApplication} the composition root also finds the nested {@code @Configuration}
* classes that dozens of tests here declare, and they collide. The behaviour behind these files is
* covered where it can be: {@code H2ClaimSqlTest} runs the H2 statements against a real H2,
* {@code PersistenceVendorSelectionTest} covers the selector, and
* {@code PersistenceVendorProdSafetyValidatorTest} covers the prod refusal. What is left, and what
* this test guards, is the wiring between them drifting a profile quietly changing vendor, or
* local regaining a migration expectation it cannot satisfy.
*/
class ProfileSeparationContractTest {
private static final Path REPOSITORY_ROOT = repositoryRoot();
private static final String VENDOR_KEY = "vendor";
@Test
void localRunsAnInMemoryDatabaseWithNoMigrations() throws IOException {
Map<?, ?> local = profile("local");
assertThat(vendorOf(local)).isEqualTo("h2");
assertThat(datasourceUrlOf(local))
.as("local must need no external database")
.startsWith("jdbc:h2:mem:");
assertThat(flywayEnabledOf(local))
.as("db/migration/postgresql is PostgreSQL DDL and cannot run on H2")
.isEqualTo(false);
}
@Test
void localKeepsTheInMemoryDatabaseAliveAcrossPoolIdleness() throws IOException {
// An in-memory database is dropped when its last connection closes, and HikariCP closes idle
// connections: without this the schema disappears mid-run rather than at shutdown.
assertThat(datasourceUrlOf(profile("local"))).contains("DB_CLOSE_DELAY=-1");
}
@ParameterizedTest
@ValueSource(strings = {"dev", "prod"})
void deployedProfilesRunPostgreSqlWithFlyway(String profile) throws IOException {
Map<?, ?> configuration = profile(profile);
assertThat(vendorOf(configuration)).isEqualTo("postgresql");
assertThat(flywayEnabledOf(configuration)).isEqualTo(true);
}
@ParameterizedTest
@ValueSource(strings = {"dev", "prod"})
void deployedProfilesLeaveTheDatasourceToTheEnvironment(String profile) throws IOException {
// A committed database address is either wrong or a leak; the ${APP_DATASOURCE_*} placeholders
// in application.yml stay the single entry point for it.
assertThat(datasourceUrlOf(profile(profile))).isNull();
}
@Test
void theProfilelessDefaultIsPostgreSqlRatherThanTheInMemoryDatabase() throws IOException {
Map<?, ?> base = yaml("application.yml");
assertThat(vendorOf(base))
.as("a deployment that activates no profile must not silently get H2")
.isEqualTo("postgresql");
}
private static Map<?, ?> profile(String profile) throws IOException {
return yaml("application-" + profile + ".yml");
}
private static String vendorOf(Map<?, ?> configuration) {
Map<?, ?> persistence = child(child(configuration, "ca-skeleton"), "persistence");
Object vendor = persistence == null ? null : persistence.get(VENDOR_KEY);
return vendor == null ? null : vendor.toString();
}
private static String datasourceUrlOf(Map<?, ?> configuration) {
Map<?, ?> datasource = child(child(configuration, "spring"), "datasource");
Object url = datasource == null ? null : datasource.get("url");
return url == null ? null : url.toString();
}
private static Boolean flywayEnabledOf(Map<?, ?> configuration) {
Map<?, ?> flyway = child(child(configuration, "spring"), "flyway");
Object enabled = flyway == null ? null : flyway.get("enabled");
return enabled instanceof Boolean value ? value : null;
}
private static Map<?, ?> child(Map<?, ?> owner, String key) {
if (owner == null) {
return null;
}
Object value = owner.get(key);
return value instanceof Map<?, ?> map ? map : null;
}
private static Map<?, ?> yaml(String resource) throws IOException {
String source =
Files.readString(
REPOSITORY_ROOT.resolve("src/app-bootstrap/src/main/resources").resolve(resource));
LoaderOptions options = new LoaderOptions();
options.setAllowDuplicateKeys(false);
// The base file is placeholder-driven; resolving is Spring's job, parsing is all this needs.
Object loaded = new Yaml(new SafeConstructor(options)).load(source);
assertThat(loaded).as("%s must parse as a YAML mapping", resource).isInstanceOf(Map.class);
return (Map<?, ?>) loaded;
}
private static Path repositoryRoot() {
for (Path path = Paths.get("").toAbsolutePath(); path != null; path = path.getParent()) {
if (Files.isRegularFile(path.resolve("AGENTS.md"))
&& Files.isRegularFile(path.resolve("src/settings.gradle"))) {
return path;
}
}
throw new IllegalStateException("repository root not found from " + Paths.get("").toAbsolutePath());
}
/**
* The local profile must not need src/.env. Only {@code ./gradlew :app-bootstrap:bootRun} injects
* that file; an IDE Run/Debug, {@code java -jar} and a bare container do not, and an unresolved
* {@code ${APP_*}} placeholder fails property binding. The failure is worse than it sounds:
* logback-spring.xml reads the same unresolved {@code ca-skeleton.logging.*} values, so the
* logging system dies first and the process exits having printed nothing at all.
*/
@Test
void localProfileAnswersEveryRequiredPlaceholder() throws IOException {
Map<String, String> localValues = flatten(profile("local"));
List<String> unanswered =
placeholders().entrySet().stream()
.filter(entry -> entry.getValue().inlineDefault() == null)
.map(Map.Entry::getKey)
.filter(path -> !localValues.containsKey(path))
.sorted()
.toList();
assertThat(unanswered)
.as("application-local.yml must restate every value application.yml demands from src/.env")
.isEmpty();
}
/**
* And where a placeholder does have an inline default, local must not silently disagree with the
* repository's own local values. {@code PRESENTATION_API_BASE_PATH} is why: application.yml
* defaults it to {@code /v1} while src/.env says {@code /api}, so a launcher without src/.env
* served {@code /v1/healthcheck} and answered 401 on the {@code /api/healthcheck} address the
* README, the compose healthchecks and SECURITY_PUBLIC_PATHS all name.
*/
@Test
void localProfilePinsEveryValueWhoseInlineDefaultDisagreesWithTheCommittedEnvironment()
throws IOException {
Map<String, String> environment = committedEnvironment();
Map<String, String> localValues = flatten(profile("local"));
List<String> divergent =
placeholders().entrySet().stream()
.filter(entry -> entry.getValue().inlineDefault() != null)
.filter(entry -> environment.containsKey(entry.getValue().variable()))
.filter(
entry ->
!environment
.get(entry.getValue().variable())
.equals(entry.getValue().inlineDefault()))
.map(Map.Entry::getKey)
.filter(path -> !localValues.containsKey(path))
.sorted()
.toList();
assertThat(divergent)
.as("local must behave the same whether or not the launcher injected src/.env")
.isEmpty();
}
private record Placeholder(String variable, String inlineDefault) {}
/** Property path → the {@code ${VAR}} or {@code ${VAR:default}} application.yml resolves it from. */
private static Map<String, Placeholder> placeholders() throws IOException {
Pattern syntax = Pattern.compile("^\\$\\{([A-Z0-9_]+)(?::(.*))?}$");
Map<String, Placeholder> found = new LinkedHashMap<>();
Deque<int[]> indents = new ArrayDeque<>();
Deque<String> names = new ArrayDeque<>();
for (String raw :
Files.readAllLines(
REPOSITORY_ROOT.resolve("src/app-bootstrap/src/main/resources/application.yml"))) {
String line = raw.strip();
if (line.isEmpty() || line.startsWith("#") || !line.contains(":")) {
continue;
}
int indent = raw.length() - raw.stripLeading().length();
int separator = line.indexOf(':');
String key = line.substring(0, separator).strip();
String value = line.substring(separator + 1).strip();
while (!indents.isEmpty() && indents.peek()[0] >= indent) {
indents.pop();
names.pop();
}
if (value.isEmpty()) {
indents.push(new int[] {indent});
names.push(key);
continue;
}
Matcher matcher = syntax.matcher(value);
if (!matcher.matches()) {
continue;
}
List<String> path = new ArrayList<>(names);
Collections.reverse(path);
path.add(key);
found.put(String.join(".", path), new Placeholder(matcher.group(1), matcher.group(2)));
}
return found;
}
private static Map<String, String> committedEnvironment() throws IOException {
Map<String, String> values = new LinkedHashMap<>();
for (String raw : Files.readAllLines(REPOSITORY_ROOT.resolve("src/.env"))) {
String line = raw.strip();
if (line.isEmpty() || line.startsWith("#") || !line.contains("=")) {
continue;
}
int separator = line.indexOf('=');
values.put(line.substring(0, separator).strip(), line.substring(separator + 1).strip());
}
return values;
}
private static Map<String, String> flatten(Map<?, ?> configuration) {
Map<String, String> flat = new LinkedHashMap<>();
flatten("", configuration, flat);
return flat;
}
private static void flatten(String prefix, Map<?, ?> node, Map<String, String> into) {
node.forEach(
(key, value) -> {
String path = prefix.isEmpty() ? String.valueOf(key) : prefix + "." + key;
if (value instanceof Map<?, ?> child) {
flatten(path, child, into);
} else {
into.put(path, String.valueOf(value));
}
});
}
@Test
void everyDeclaredProfileHasAConfigurationFile() {
List<String> profiles = List.of("local", "dev", "prod");
assertThat(profiles)
.allSatisfy(
profile ->
assertThat(
REPOSITORY_ROOT.resolve(
"src/app-bootstrap/src/main/resources/application-" + profile + ".yml"))
.exists());
}
}
@@ -0,0 +1,171 @@
package dev.caskeleton.bootstrap.idempotency;
import static org.assertj.core.api.Assertions.assertThat;
import dev.caskeleton.application.idempotency.IdempotencyExecutor;
import dev.caskeleton.application.idempotency.IdempotencyStorePort;
import dev.caskeleton.application.idempotency.v2.IdempotencyStorePortV2;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcOperations;
/**
* The provider guard, against the beans the component scan actually produces.
*
* <p>Every other test of this guard hands it the beans it expects. That is precisely the shape of
* test that cannot see the defect it is guarding against, because the question is not "does the
* guard count correctly" but "what is in the context to count". {@code CaSkeletonApplication} scans
* {@code dev.caskeleton.adapter}, and {@code PostgreSqlOwnerSafeIdempotencyStore} is a
* {@code @Repository} implementing the owner-safe V2 contract so it is registered in every
* deployment, whichever provider was selected, and no test in the repository booted a context that
* would notice.
*
* <p>Both selections are asserted, because the collision breaks both: {@code jdbc} acquires a V2
* store it never asked for, and {@code redis} acquires a second one beside its own.
*/
class IdempotencyProviderScanCollisionTest {
private final ApplicationContextRunner runner =
new ApplicationContextRunner()
.withUserConfiguration(ScannedPersistence.class, IdempotencyProviderSelectionConfig.class)
.withBean(JdbcOperations.class, () -> org.mockito.Mockito.mock(JdbcOperations.class));
@Test
@DisplayName("the default JDBC provider composes exactly its own V1 pair and no V2 store")
void theJdbcProviderComposesOnlyV1() {
runner
.withPropertyValues("ca-skeleton.capabilities.idempotency.provider=jdbc")
.withBean(
IdempotencyStorePort.class, () -> org.mockito.Mockito.mock(IdempotencyStorePort.class))
.withBean(
IdempotencyExecutor.class, () -> org.mockito.Mockito.mock(IdempotencyExecutor.class))
.run(
context -> {
assertThat(context)
.as("the default provider must not acquire an owner-safe V2 store by scan")
.hasNotFailed();
assertThat(context.getBeansOfType(IdempotencyStorePortV2.class)).isEmpty();
});
}
@Test
@DisplayName("the Redis provider composes exactly one owner-safe V2 store and its executor")
void theRedisProviderComposesOneV2StoreAndItsExecutor() {
runner
.withPropertyValues("ca-skeleton.capabilities.idempotency.provider=redis")
.withBean(IdempotencyStorePortV2.class, RedisLikeStore::new)
.withBean(
dev.caskeleton.application.idempotency.v2.IdempotencyExecutorV2.class,
() ->
new dev.caskeleton.application.idempotency.v2.IdempotencyExecutorV2(
new RedisLikeStore(),
java.time.Duration.ofSeconds(30),
java.time.Duration.ofHours(1),
java.time.Duration.ofHours(1),
"json-v2",
1))
.run(
context -> {
assertThat(context)
.as("a second V2 store from the scan makes the selection ambiguous")
.hasNotFailed();
assertThat(context.getBeansOfType(IdempotencyStorePortV2.class)).hasSize(1);
});
}
@Test
@DisplayName("a V2 store with no executor is refused: nothing could drive it")
void aStoreWithoutAnExecutorIsRefused() {
runner
.withPropertyValues("ca-skeleton.capabilities.idempotency.provider=redis")
.withBean(IdempotencyStorePortV2.class, RedisLikeStore::new)
.run(
context -> {
assertThat(context).hasFailed();
assertThat(context.getStartupFailure())
.hasStackTraceContaining("owner-safe V2 executor");
});
}
/** Exactly what the composition root scans, narrowed to the package under test. */
@Configuration(proxyBeanMethods = false)
@ComponentScan("dev.caskeleton.adapter.outbound.persistence.postgresql.idempotency")
static class ScannedPersistence {
@Bean
IdempotencySettings idempotencySettings() {
return new IdempotencySettings(null, null);
}
}
/** Stands in for the Redis V2 store, whose own composition is covered elsewhere. */
private static final class RedisLikeStore implements IdempotencyStorePortV2 {
@Override
public dev.caskeleton.application.idempotency.v2.IdempotencyClaimAttempt newClaimAttempt(
dev.caskeleton.application.transaction.OperationId operationId) {
throw new UnsupportedOperationException();
}
@Override
public dev.caskeleton.application.idempotency.v2.IdempotencyClaimOutcome claim(
dev.caskeleton.application.idempotency.v2.IdempotencyClaimRequest request) {
throw new UnsupportedOperationException();
}
@Override
public dev.caskeleton.application.idempotency.v2.IdempotencyMutationResult<
dev.caskeleton.application.idempotency.v2.IdempotencyStartOutcome>
markExecutionStarted(
dev.caskeleton.application.idempotency.v2.IdempotencyOwner owner,
dev.caskeleton.application.transaction.OperationId operationId) {
throw new UnsupportedOperationException();
}
@Override
public dev.caskeleton.application.idempotency.v2.IdempotencyMutationResult<
dev.caskeleton.application.idempotency.v2.IdempotencyRenewOutcome>
renew(
dev.caskeleton.application.idempotency.v2.IdempotencyOwner owner,
java.time.Duration processingLeaseTtl,
dev.caskeleton.application.transaction.OperationId operationId) {
throw new UnsupportedOperationException();
}
@Override
public dev.caskeleton.application.idempotency.v2.IdempotencyCompleteOutcome complete(
dev.caskeleton.application.idempotency.v2.IdempotencyOwner owner,
dev.caskeleton.application.idempotency.StoredResponse response,
java.time.Duration replayTtl,
dev.caskeleton.application.transaction.OperationId operationId) {
throw new UnsupportedOperationException();
}
@Override
public dev.caskeleton.application.idempotency.v2.IdempotencyFailOutcome markFailed(
dev.caskeleton.application.idempotency.v2.IdempotencyOwner owner,
dev.caskeleton.application.idempotency.v2.IdempotencyFailureDisposition disposition,
java.time.Duration retention,
dev.caskeleton.application.transaction.OperationId operationId) {
throw new UnsupportedOperationException();
}
@Override
public dev.caskeleton.application.idempotency.v2.IdempotencyReleaseOutcome
releaseBeforeExecution(
dev.caskeleton.application.idempotency.v2.IdempotencyOwner owner,
dev.caskeleton.application.transaction.OperationId operationId) {
throw new UnsupportedOperationException();
}
@Override
public dev.caskeleton.application.idempotency.v2.IdempotencyInspection inspect(
dev.caskeleton.application.idempotency.v2.IdempotencyInspectionRequest request) {
throw new UnsupportedOperationException();
}
}
}
@@ -0,0 +1,101 @@
package dev.caskeleton.bootstrap.runtime;
import static org.assertj.core.api.Assertions.assertThat;
import dev.caskeleton.bootstrap.runtime.startup.ProfileMismatchException;
import java.io.PrintWriter;
import java.io.StringWriter;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
class PersistenceVendorProdSafetyValidatorTest {
private static final String POSTGRESQL_URL = "jdbc:postgresql://db:5432/ca_skeleton";
private static final String H2_URL = "jdbc:h2:mem:ca_skeleton;MODE=PostgreSQL";
private final ApplicationContextRunner runner =
new ApplicationContextRunner().withUserConfiguration(ValidatorConfig.class);
@ParameterizedTest
@ValueSource(strings = {"h2", "H2", " h2 "})
void prodRejectsTheH2VendorSelector(String vendor) {
runner
.withInitializer(context -> context.getEnvironment().setActiveProfiles("prod"))
.withPropertyValues(
PersistenceVendorProdSafetyValidator.VENDOR_KEY + "=" + vendor,
PersistenceVendorProdSafetyValidator.JDBC_URL_KEY + "=" + POSTGRESQL_URL)
.run(
context -> {
assertThat(context).hasFailed();
assertThat(context.getStartupFailure())
.isInstanceOf(ProfileMismatchException.class)
.hasStackTraceContaining(PersistenceVendorProdSafetyValidator.VENDOR_KEY);
});
}
@Test
void prodRejectsAnH2UrlEvenWhenTheSelectorSaysPostgreSql() {
runner
.withInitializer(context -> context.getEnvironment().setActiveProfiles("prod"))
.withPropertyValues(
PersistenceVendorProdSafetyValidator.VENDOR_KEY + "=postgresql",
PersistenceVendorProdSafetyValidator.JDBC_URL_KEY + "=" + H2_URL)
.run(
context -> {
assertThat(context).hasFailed();
assertThat(context.getStartupFailure())
.isInstanceOf(ProfileMismatchException.class)
.hasStackTraceContaining(PersistenceVendorProdSafetyValidator.JDBC_URL_ENV_KEY);
});
}
@Test
void prodFailureNeverEchoesTheJdbcUrl() {
runner
.withInitializer(context -> context.getEnvironment().setActiveProfiles("prod"))
.withPropertyValues(PersistenceVendorProdSafetyValidator.JDBC_URL_KEY + "=" + H2_URL)
.run(
context -> {
StringWriter trace = new StringWriter();
context.getStartupFailure().printStackTrace(new PrintWriter(trace));
assertThat(trace.toString())
.as("a JDBC URL can carry credentials, endpoints and database names")
.doesNotContain(H2_URL);
});
}
@Test
void prodAcceptsPostgreSql() {
runner
.withInitializer(context -> context.getEnvironment().setActiveProfiles("prod"))
.withPropertyValues(
PersistenceVendorProdSafetyValidator.VENDOR_KEY + "=postgresql",
PersistenceVendorProdSafetyValidator.JDBC_URL_KEY + "=" + POSTGRESQL_URL)
.run(context -> assertThat(context).hasNotFailed());
}
@ParameterizedTest
@ValueSource(strings = {"local", "dev", "stage"})
void nonProdProfilesMayRunH2(String profile) {
runner
.withInitializer(context -> context.getEnvironment().setActiveProfiles(profile))
.withPropertyValues(
PersistenceVendorProdSafetyValidator.VENDOR_KEY + "=h2",
PersistenceVendorProdSafetyValidator.JDBC_URL_KEY + "=" + H2_URL)
.run(context -> assertThat(context).hasNotFailed());
}
@Configuration
static class ValidatorConfig {
@Bean
PersistenceVendorProdSafetyValidator validator(Environment environment) {
return new PersistenceVendorProdSafetyValidator(environment);
}
}
}
@@ -13,7 +13,24 @@ public class IdempotencyInFlightException extends RuntimeException {
private final transient IdempotencyScope scope;
public IdempotencyInFlightException(IdempotencyScope scope) {
super("idempotent request still in flight: " + (scope == null ? "<null>" : scope.storageKey()));
this(scope == null ? "<null>" : scope.storageKey(), scope);
}
/**
* Creates the exception for the owner-safe lifecycle, which never holds the raw scope: the
* principal and the client key are digested before they reach the store, so the opaque digest
* diagnostic is the only identity available. {@link #scope()} is {@code null} on this path, as it
* already is for any deserialized instance the field is {@code transient}.
*
* @param scopeDigestDiagnostic opaque scope-digest diagnostic, for logs only
* @return the exception
*/
public static IdempotencyInFlightException forScopeDigest(String scopeDigestDiagnostic) {
return new IdempotencyInFlightException(scopeDigestDiagnostic, null);
}
private IdempotencyInFlightException(String scopeDiagnostic, IdempotencyScope scope) {
super("idempotent request still in flight: " + scopeDiagnostic);
this.scope = scope;
}
@@ -13,9 +13,24 @@ public class IdempotencyRequestMismatchException extends RuntimeException {
private final transient IdempotencyScope scope;
public IdempotencyRequestMismatchException(IdempotencyScope scope) {
super(
"idempotency key reused with a different request body: "
+ (scope == null ? "<null>" : scope.storageKey()));
this(scope == null ? "<null>" : scope.storageKey(), scope);
}
/**
* Creates the exception for the owner-safe lifecycle, which never holds the raw scope: the
* principal and the client key are digested before they reach the store, so the opaque digest
* diagnostic is the only identity available. {@link #scope()} is {@code null} on this path, as it
* already is for any deserialized instance the field is {@code transient}.
*
* @param scopeDigestDiagnostic opaque scope-digest diagnostic, for logs only
* @return the exception
*/
public static IdempotencyRequestMismatchException forScopeDigest(String scopeDigestDiagnostic) {
return new IdempotencyRequestMismatchException(scopeDigestDiagnostic, null);
}
private IdempotencyRequestMismatchException(String scopeDiagnostic, IdempotencyScope scope) {
super("idempotency key reused with a different request body: " + scopeDiagnostic);
this.scope = scope;
}
@@ -0,0 +1,328 @@
package dev.caskeleton.application.idempotency.v2;
import dev.caskeleton.application.idempotency.IdempotencyInFlightException;
import dev.caskeleton.application.idempotency.IdempotencyRecoveryRequiredException;
import dev.caskeleton.application.idempotency.IdempotencyRequestMismatchException;
import dev.caskeleton.application.idempotency.IdempotencyUnavailableException;
import dev.caskeleton.application.idempotency.IdempotentAction;
import dev.caskeleton.application.idempotency.IdempotentResponseCodec;
import dev.caskeleton.application.idempotency.RequestFingerprint;
import dev.caskeleton.application.idempotency.StoredResponse;
import dev.caskeleton.application.transaction.OperationId;
import java.time.Duration;
import java.util.Objects;
import java.util.Optional;
/**
* Owner-safe request-replay lifecycle, over the contract the providers actually implement.
*
* <p>There were two V2 store contracts with the same name in neighbouring packages, and this
* orchestration was written against the one nothing implements. Both owner-safe stores PostgreSQL
* and Redis implement {@link IdempotencyStorePortV2} here, so selecting a V2 provider required a
* bean that could not exist and the executor could never be composed at all.
*
* <p>Porting it was not a rename. This contract threads the owner handle through every mutation: a
* confirmed transition returns the owner with its state revision advanced, and the next call must
* present <em>that</em> handle. Re-using the claim's original owner would present a stale revision,
* which the store is built to refuse that refusal is the whole point of the compare-and-set, so a
* caller that defeats it has an owner-safe store and no owner safety.
*
* <p>The action runs only after a confirmed start. This preserves request-replay evidence; it does
* not create a cross-store exactly-once boundary, and nothing here should be read as claiming one.
*/
public final class IdempotencyExecutorV2 {
private final IdempotencyStorePortV2 store;
private final Duration processingLeaseTtl;
private final Duration replayTtl;
private final Duration failureRetention;
private final String responseCodecId;
private final int policyRevision;
/**
* Creates the executor.
*
* @param store the owner-safe store
* @param processingLeaseTtl how long a claim holds the record before another caller may take over
* @param replayTtl how long a completed response is replayable
* @param failureRetention how long a failed attempt's evidence is kept
* @param responseCodecId the identifier of the codec the stored payload was written with
* @param policyRevision the replay policy revision this deployment enforces
*/
public IdempotencyExecutorV2(
IdempotencyStorePortV2 store,
Duration processingLeaseTtl,
Duration replayTtl,
Duration failureRetention,
String responseCodecId,
int policyRevision) {
this.store = Objects.requireNonNull(store, "store must be non-null");
this.processingLeaseTtl =
Objects.requireNonNull(processingLeaseTtl, "processing lease TTL must be non-null");
this.replayTtl = Objects.requireNonNull(replayTtl, "replay TTL must be non-null");
this.failureRetention =
Objects.requireNonNull(failureRetention, "failure retention must be non-null");
this.responseCodecId =
Objects.requireNonNull(responseCodecId, "response codec identifier must be non-null");
this.policyRevision = policyRevision;
if (failureRetention.isZero() || failureRetention.isNegative()) {
throw new IllegalArgumentException("the failure retention must be positive");
}
}
/**
* Mints the attempt a caller retains across a lost response.
*
* @param operationId the caller's operation identifier
* @return the claim attempt
*/
public IdempotencyClaimAttempt newAttempt(OperationId operationId) {
return store.newClaimAttempt(operationId);
}
/**
* Claims the scope, runs the action once, and stores its response for replay.
*
* @param scope the digested scope; the raw client key never reaches this class
* @param fingerprint the request fingerprint a replay is checked against
* @param attempt the retained claim attempt
* @param action the action to run at most once
* @param codec serialises and deserialises the action's result
* @param <R> the action's result type
* @return the action's result, or the replayed one
*/
public <R> R execute(
IdempotencyScopeDigest scope,
RequestFingerprint fingerprint,
IdempotencyClaimAttempt attempt,
IdempotentAction<R> action,
IdempotentResponseCodec<R> codec) {
Objects.requireNonNull(action, "action must be non-null");
Objects.requireNonNull(codec, "codec must be non-null");
IdempotencyClaimRequest request =
new IdempotencyClaimRequest(
scope,
fingerprint,
attempt,
processingLeaseTtl,
replayTtl,
responseCodecId,
policyRevision);
return switch (store.claim(request)) {
case IdempotencyClaimOutcome.CompletedReplay replay ->
codec.deserialize(replay.response().payload());
case IdempotencyClaimOutcome.FingerprintMismatch ignored ->
throw IdempotencyRequestMismatchException.forScopeDigest(diagnostic(scope));
case IdempotencyClaimOutcome.InProgress ignored ->
throw IdempotencyInFlightException.forScopeDigest(diagnostic(scope));
case IdempotencyClaimOutcome.RecoveryRequired ignored ->
throw recovery("claim requires reconciliation");
case IdempotencyClaimOutcome.OwnerOperationConflict ignored ->
throw recovery("claim requires reconciliation");
case IdempotencyClaimOutcome.Unavailable ignored ->
throw new IdempotencyUnavailableException();
// The response was lost, not the attempt. The retained attempt is what makes the record
// findable, so the only safe move is to ask the store what actually happened.
case IdempotencyClaimOutcome.Indeterminate ignored -> reconcile(request, action, codec);
case IdempotencyClaimOutcome.Acquired acquired ->
startAndRun(request, acquired.owner(), action, codec, false);
case IdempotencyClaimOutcome.ReplayedAcquire replayed ->
startAndRun(request, replayed.owner(), action, codec, false);
case IdempotencyClaimOutcome.TakenOverClaimed takenOver ->
startAndRun(request, takenOver.owner(), action, codec, false);
};
}
private <R> R reconcile(
IdempotencyClaimRequest request,
IdempotentAction<R> action,
IdempotentResponseCodec<R> codec) {
IdempotencyInspection inspection = inspect(request);
return switch (inspection.outcome()) {
case CLAIMED_SAME_OPERATION ->
startAndRun(request, requireOwner(inspection), action, codec, false);
case EXECUTING_SAME_OPERATION -> runStarted(request, requireOwner(inspection), action, codec);
case COMPLETED_REPLAY -> codec.deserialize(requireResponse(inspection).payload());
case FINGERPRINT_MISMATCH ->
throw IdempotencyRequestMismatchException.forScopeDigest(diagnostic(request.scope()));
case IN_PROGRESS_OTHER ->
throw IdempotencyInFlightException.forScopeDigest(diagnostic(request.scope()));
case UNAVAILABLE -> throw new IdempotencyUnavailableException();
// ABSENT, FAILED_RETRYABLE, ABANDONED and OPERATION_CONFLICT all mean the same thing here:
// this caller cannot prove what happened to its own attempt, and guessing is the one thing an
// owner-safe store exists to prevent.
default -> throw recovery("an indeterminate claim could not be resumed safely");
};
}
private <R> R startAndRun(
IdempotencyClaimRequest request,
IdempotencyOwner owner,
IdempotentAction<R> action,
IdempotentResponseCodec<R> codec,
boolean alreadyRetried) {
IdempotencyMutationResult<IdempotencyStartOutcome> started =
store.markExecutionStarted(owner, request.claimAttempt().operationId());
return switch (started.outcome()) {
// The owner handle from the transition, not the one from the claim: the state revision has
// advanced and the next compare-and-set is against the new one.
case STARTED, ALREADY_STARTED_SAME_OPERATION ->
runStarted(request, advanced(started, owner), action, codec);
case UNAVAILABLE -> throw new IdempotencyUnavailableException();
case INDETERMINATE -> {
if (alreadyRetried) {
throw recovery("execution start stayed indeterminate after reconciliation");
}
yield resumeAfterIndeterminateStart(request, action, codec);
}
default -> throw recovery("execution start was not confirmed for this exact operation");
};
}
private <R> R resumeAfterIndeterminateStart(
IdempotencyClaimRequest request,
IdempotentAction<R> action,
IdempotentResponseCodec<R> codec) {
IdempotencyInspection inspection = inspect(request);
return switch (inspection.outcome()) {
case CLAIMED_SAME_OPERATION ->
startAndRun(request, requireOwner(inspection), action, codec, true);
case EXECUTING_SAME_OPERATION -> runStarted(request, requireOwner(inspection), action, codec);
case COMPLETED_REPLAY -> codec.deserialize(requireResponse(inspection).payload());
case UNAVAILABLE -> throw new IdempotencyUnavailableException();
default -> throw recovery("execution start is indeterminate");
};
}
private <R> R runStarted(
IdempotencyClaimRequest request,
IdempotencyOwner owner,
IdempotentAction<R> action,
IdempotentResponseCodec<R> codec) {
OperationId operationId = request.claimAttempt().operationId();
IdempotentAction.Outcome<R> outcome;
try {
outcome = Objects.requireNonNull(action.run(), "action outcome must be non-null");
} catch (RuntimeException failure) {
// The action threw without saying whether it had an effect, so the record must not say
// "retryable": that would invite a second execution of something that may already have run.
preserveUnknown(owner, operationId);
throw failure;
}
return switch (outcome) {
case IdempotentAction.Outcome.Success<R> success ->
complete(request, owner, success.result(), codec, false);
case IdempotentAction.Outcome.RetryableNoEffect<R> retryable -> {
store.markFailed(
owner,
IdempotencyFailureDisposition.NO_EFFECT_RETRYABLE,
failureRetention,
operationId);
throw retryable.failure();
}
case IdempotentAction.Outcome.EffectUnknown<R> unknown -> {
preserveUnknown(owner, operationId);
throw unknown.failure();
}
};
}
private <R> R complete(
IdempotencyClaimRequest request,
IdempotencyOwner owner,
R result,
IdempotentResponseCodec<R> codec,
boolean alreadyRetried) {
StoredResponse response = new StoredResponse(codec.serialize(result));
return switch (store.complete(
owner, response, replayTtl, request.claimAttempt().operationId())) {
case COMPLETED, ALREADY_COMPLETED_SAME_RESULT -> result;
case UNAVAILABLE -> throw new IdempotencyUnavailableException();
case INDETERMINATE -> reconcileCompletion(request, result, codec, alreadyRetried);
default -> throw recovery("completion was not confirmed");
};
}
/**
* Resolves a completion whose response was lost.
*
* <p>Deliberately does not carry the caller's owner handle forward: after an indeterminate
* completion the caller's handle may already be stale, and the only handle worth presenting is
* the one the store reports now.
*/
private <R> R reconcileCompletion(
IdempotencyClaimRequest request,
R result,
IdempotentResponseCodec<R> codec,
boolean alreadyRetried) {
IdempotencyInspection inspection = inspect(request);
return switch (inspection.outcome()) {
case COMPLETED_REPLAY -> {
StoredResponse stored = requireResponse(inspection);
if (stored.payload().equals(codec.serialize(result))) {
yield result;
}
// The stored response is somebody else's answer to this scope. Returning either one would
// be asserting a fact this caller cannot establish.
throw recovery("the completed response conflicts with the one this caller produced");
}
case EXECUTING_SAME_OPERATION -> {
if (alreadyRetried) {
throw recovery("completion stayed indeterminate after reconciliation");
}
yield complete(request, requireOwner(inspection), result, codec, true);
}
case UNAVAILABLE -> throw new IdempotencyUnavailableException();
default -> throw recovery("the completion response is indeterminate and was not reconciled");
};
}
private void preserveUnknown(IdempotencyOwner owner, OperationId operationId) {
store.markFailed(
owner,
IdempotencyFailureDisposition.EFFECT_UNKNOWN_ABANDONED,
failureRetention,
operationId);
}
private IdempotencyInspection inspect(IdempotencyClaimRequest request) {
return store.inspect(
new IdempotencyInspectionRequest(
request.scope(), request.requestFingerprint(), request.claimAttempt()));
}
private static IdempotencyOwner advanced(
IdempotencyMutationResult<IdempotencyStartOutcome> transition, IdempotencyOwner fallback) {
return transition.owner().orElse(fallback);
}
private static IdempotencyOwner requireOwner(IdempotencyInspection inspection) {
Optional<IdempotencyOwner> owner = inspection.owner();
return owner.orElseThrow(
() ->
recovery(
"the store reported "
+ inspection.outcome()
+ " without the owner handle it needs"));
}
private static StoredResponse requireResponse(IdempotencyInspection inspection) {
return inspection
.response()
.orElseThrow(() -> recovery("the store reported a completed replay with no response"));
}
/**
* Renders the scope for a diagnostic. Every component is already opaque the raw client key and
* principal were digested before the scope was constructed so this is safe to log, and it is
* the only scope identity this lifecycle ever holds.
*/
private static String diagnostic(IdempotencyScopeDigest scope) {
return scope.operationCode() + "::v" + scope.keyDigestVersion() + "::" + scope.digest();
}
private static IdempotencyRecoveryRequiredException recovery(String message) {
return new IdempotencyRecoveryRequiredException(message);
}
}
@@ -0,0 +1,343 @@
package dev.caskeleton.application.idempotency.v2;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import dev.caskeleton.application.idempotency.IdempotentAction;
import dev.caskeleton.application.idempotency.IdempotentResponseCodec;
import dev.caskeleton.application.idempotency.RequestFingerprint;
import dev.caskeleton.application.idempotency.StoredResponse;
import dev.caskeleton.application.transaction.OperationId;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
/**
* The request-replay lifecycle, over the contract the stores implement.
*
* <p>Carried over from a test of the same name that exercised the duplicate V2 contract in the
* parent package the one nothing implements. The behaviours below are the ones worth keeping: the
* action runs at most once, a lost response is reconciled rather than re-run, and an outcome the
* caller cannot classify is never recorded as "no effect".
*
* <p>One behaviour is new, because the contract is: every confirmed transition hands back the owner
* with its state revision advanced, and the next call must present that handle rather than the one
* the claim returned.
*/
class IdempotencyExecutorV2Test {
private static final String OWNER_TOKEN = "b".repeat(64);
private static final OperationId OPERATION = new OperationId("operation-aaaaaaaaaa");
private static final IdempotencyScopeDigest SCOPE =
new IdempotencyScopeDigest("a".repeat(64), 1, "CREATE_WORKLOG");
private static final RequestFingerprint FINGERPRINT = new RequestFingerprint("c".repeat(64));
private static final IdempotencyClaimAttempt ATTEMPT =
new IdempotencyClaimAttempt(OWNER_TOKEN, OPERATION);
private static final Instant LEASE_UNTIL = Instant.parse("2026-07-29T12:00:30Z");
private static IdempotencyOwner owner(long stateRevision) {
return new IdempotencyOwner(SCOPE, OWNER_TOKEN, 1, stateRevision, OPERATION);
}
@Test
@DisplayName("the action runs only after a confirmed start, and then completes")
void actionRunsOnlyAfterAConfirmedStart() {
FakeStore store = new FakeStore();
store.claim = new IdempotencyClaimOutcome.Acquired(owner(0), LEASE_UNTIL);
store.start = IdempotencyStartOutcome.STARTED;
AtomicBoolean ran = new AtomicBoolean();
String result =
executor(store)
.execute(
SCOPE,
FINGERPRINT,
ATTEMPT,
() -> {
ran.set(true);
return new IdempotentAction.Outcome.Success<>("created");
},
codec());
assertThat(result).isEqualTo("created");
assertThat(ran).isTrue();
assertThat(store.completeCalls).isEqualTo(1);
assertThat(store.failedCalls).isZero();
}
@Test
@DisplayName("each mutation presents the owner handle the previous one returned")
void theAdvancedOwnerHandleIsCarriedForward() {
// The contract's whole point. The store advances the state revision on a confirmed transition
// and refuses a stale one; a caller that kept presenting the claim's original handle would be
// refused by its own store, so an owner-safe store would buy nothing.
FakeStore store = new FakeStore();
store.claim = new IdempotencyClaimOutcome.Acquired(owner(0), LEASE_UNTIL);
store.start = IdempotencyStartOutcome.STARTED;
store.startOwner = owner(1);
executor(store)
.execute(
SCOPE,
FINGERPRINT,
ATTEMPT,
() -> new IdempotentAction.Outcome.Success<>("created"),
codec());
assertThat(store.completeOwner.stateRevision())
.as("complete must present the revision the start returned, not the claim's")
.isEqualTo(1);
}
@Test
@DisplayName("an indeterminate claim is reconciled rather than re-run")
void anIndeterminateClaimIsReconciled() {
FakeStore store = new FakeStore();
store.claim = new IdempotencyClaimOutcome.Indeterminate(OPERATION);
store.inspection =
new IdempotencyInspection(
IdempotencyInspectionOutcome.EXECUTING_SAME_OPERATION,
Optional.of(owner(1)),
Optional.of(LEASE_UNTIL),
Optional.empty(),
Optional.empty());
AtomicBoolean ran = new AtomicBoolean();
assertThat(
executor(store)
.execute(
SCOPE,
FINGERPRINT,
ATTEMPT,
() -> {
ran.set(true);
return new IdempotentAction.Outcome.Success<>("created");
},
codec()))
.isEqualTo("created");
assertThat(ran).isTrue();
assertThat(store.startCalls)
.as("the record already says EXECUTING; starting it again would be a second transition")
.isZero();
}
@Test
@DisplayName("a lost completion is inspected without running the action a second time")
void aLostCompletionIsInspectedNotRepeated() {
FakeStore store = startedStore();
store.complete = IdempotencyCompleteOutcome.INDETERMINATE;
store.inspectionAfterComplete =
new IdempotencyInspection(
IdempotencyInspectionOutcome.COMPLETED_REPLAY,
Optional.empty(),
Optional.empty(),
Optional.of(new StoredResponse("created")),
Optional.of(Instant.parse("2026-07-29T13:00:00Z")));
int[] actionCalls = {0};
assertThat(
executor(store)
.execute(
SCOPE,
FINGERPRINT,
ATTEMPT,
() -> {
actionCalls[0]++;
return new IdempotentAction.Outcome.Success<>("created");
},
codec()))
.isEqualTo("created");
assertThat(actionCalls[0]).isEqualTo(1);
assertThat(store.completeCalls).isEqualTo(1);
}
@Test
@DisplayName("a replayed response that disagrees with this caller's is a recovery, not a result")
void aConflictingReplayIsNotReturned() {
FakeStore store = startedStore();
store.complete = IdempotencyCompleteOutcome.INDETERMINATE;
store.inspectionAfterComplete =
new IdempotencyInspection(
IdempotencyInspectionOutcome.COMPLETED_REPLAY,
Optional.empty(),
Optional.empty(),
Optional.of(new StoredResponse("somebody-elses-answer")),
Optional.of(Instant.parse("2026-07-29T13:00:00Z")));
assertThatThrownBy(
() ->
executor(store)
.execute(
SCOPE,
FINGERPRINT,
ATTEMPT,
() -> new IdempotentAction.Outcome.Success<>("created"),
codec()))
.hasMessageContaining("conflicts");
}
@Test
@DisplayName("an unclassified throw and an unknown effect are never recorded as no-effect")
void unknownEffectsAreNeverDiscarded() {
FakeStore thrown = startedStore();
RuntimeException unclassified = new IllegalStateException("unknown effect");
assertThatThrownBy(
() ->
executor(thrown)
.execute(
SCOPE,
FINGERPRINT,
ATTEMPT,
() -> {
throw unclassified;
},
codec()))
.isSameAs(unclassified);
assertThat(thrown.lastDisposition)
.isEqualTo(IdempotencyFailureDisposition.EFFECT_UNKNOWN_ABANDONED);
FakeStore declared = startedStore();
RuntimeException classified = new IllegalArgumentException("provider response unknown");
assertThatThrownBy(
() ->
executor(declared)
.execute(
SCOPE,
FINGERPRINT,
ATTEMPT,
() -> new IdempotentAction.Outcome.EffectUnknown<>(classified),
codec()))
.isSameAs(classified);
assertThat(declared.lastDisposition)
.isEqualTo(IdempotencyFailureDisposition.EFFECT_UNKNOWN_ABANDONED);
}
@Test
@DisplayName("only an explicitly effect-free failure is recorded as retryable")
void onlyDeclaredNoEffectIsRetryable() {
FakeStore store = startedStore();
RuntimeException failure = new IllegalArgumentException("validation");
assertThatThrownBy(
() ->
executor(store)
.execute(
SCOPE,
FINGERPRINT,
ATTEMPT,
() -> new IdempotentAction.Outcome.RetryableNoEffect<>(failure),
codec()))
.isSameAs(failure);
assertThat(store.lastDisposition).isEqualTo(IdempotencyFailureDisposition.NO_EFFECT_RETRYABLE);
}
private static FakeStore startedStore() {
FakeStore store = new FakeStore();
store.claim = new IdempotencyClaimOutcome.Acquired(owner(0), LEASE_UNTIL);
store.start = IdempotencyStartOutcome.STARTED;
return store;
}
private static IdempotencyExecutorV2 executor(FakeStore store) {
return new IdempotencyExecutorV2(
store, Duration.ofSeconds(30), Duration.ofHours(1), Duration.ofHours(1), "json-v2", 1);
}
private static IdempotentResponseCodec<String> codec() {
return new IdempotentResponseCodec<>() {
@Override
public String serialize(String result) {
return result;
}
@Override
public String deserialize(String payload) {
return payload;
}
};
}
private static final class FakeStore implements IdempotencyStorePortV2 {
private IdempotencyClaimOutcome claim;
private IdempotencyStartOutcome start;
private IdempotencyOwner startOwner;
private IdempotencyCompleteOutcome complete = IdempotencyCompleteOutcome.COMPLETED;
private IdempotencyInspection inspection =
IdempotencyInspection.outcome(IdempotencyInspectionOutcome.UNAVAILABLE);
private IdempotencyInspection inspectionAfterComplete;
private int startCalls;
private int completeCalls;
private int failedCalls;
private IdempotencyOwner completeOwner;
private IdempotencyFailureDisposition lastDisposition;
@Override
public IdempotencyClaimAttempt newClaimAttempt(OperationId operationId) {
return new IdempotencyClaimAttempt(OWNER_TOKEN, operationId);
}
@Override
public IdempotencyClaimOutcome claim(IdempotencyClaimRequest request) {
return claim;
}
@Override
public IdempotencyMutationResult<IdempotencyStartOutcome> markExecutionStarted(
IdempotencyOwner owner, OperationId operationId) {
startCalls++;
IdempotencyOwner advanced =
start.carriesOwner() ? (startOwner == null ? owner : startOwner) : null;
return new IdempotencyMutationResult<>(
start, advanced, IdempotencyStartOutcome::carriesOwner);
}
@Override
public IdempotencyMutationResult<IdempotencyRenewOutcome> renew(
IdempotencyOwner owner, Duration processingLeaseTtl, OperationId operationId) {
throw new UnsupportedOperationException("the executor does not renew");
}
@Override
public IdempotencyCompleteOutcome complete(
IdempotencyOwner owner,
StoredResponse response,
Duration replayTtl,
OperationId operationId) {
completeCalls++;
completeOwner = owner;
if (inspectionAfterComplete != null) {
inspection = inspectionAfterComplete;
}
return complete;
}
@Override
public IdempotencyFailOutcome markFailed(
IdempotencyOwner owner,
IdempotencyFailureDisposition disposition,
Duration retention,
OperationId operationId) {
failedCalls++;
lastDisposition = disposition;
return disposition == IdempotencyFailureDisposition.NO_EFFECT_RETRYABLE
? IdempotencyFailOutcome.MARKED_RETRYABLE
: IdempotencyFailOutcome.MARKED_ABANDONED;
}
@Override
public IdempotencyReleaseOutcome releaseBeforeExecution(
IdempotencyOwner owner, OperationId operationId) {
throw new UnsupportedOperationException("the executor never releases before execution");
}
@Override
public IdempotencyInspection inspect(IdempotencyInspectionRequest request) {
return inspection;
}
}
}
+10 -2
View File
@@ -232,8 +232,10 @@ def verifySpotBugsAnalysisFailureContract =
throw new GradleException(
'verifySpotBugsAnalysisFailureContract: analysis report verifier is not configured')
}
Closure<List<String>> analysisFailures =
rootProject.ext.spotBugsAnalysisFailures as Closure<List<String>>
// Raw Closure on purpose. The parameterised form, wrapped across two lines, is
// valid Groovy and Gradle runs it, but the IDE's Gradle parser reads the trailing
// `>>` as the end of a block and reports a syntax error for the rest of the file.
def analysisFailures = rootProject.ext.spotBugsAnalysisFailures as Closure
Map<String, String> fixtures = [
clean : '<BugCollection><Errors errors="0" missingClasses="0"/></BugCollection>',
missing : '<BugCollection><Errors errors="0" missingClasses="1"><MissingClass>fixture.MissingType</MissingClass></Errors></BugCollection>',
@@ -339,6 +341,12 @@ configure(subprojects.findAll { it.childProjects.isEmpty() }) {
// binding (rationale in README.md). ErrorProne (D5) hooks the same compile tasks: it
// auto-injects the JDK 16+ --add-exports/--add-opens forking args, so none are added here.
tasks.withType(JavaCompile).configureEach {
// Pinned, not inherited from the platform. Sources carry non-ASCII Korean comments and
// em dashes inside string literals so a builder whose default charset is not UTF-8
// compiles different bytes than this one does. It is also what the Gradle model hands the
// IDE as the project encoding; without it every imported project reports "no explicit
// encoding set".
options.encoding = 'UTF-8'
['-parameters', '-Werror', '-Xlint:deprecation', '-Xlint:unchecked'].each { String compilerArg ->
if (!options.compilerArgs.contains(compilerArg)) {
options.compilerArgs.add(compilerArg)
+1
View File
@@ -41,6 +41,7 @@ com.google.guava:guava:33.6.0-jre=checkstyle
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,checkstyle,posterImageMigrationTestAnnotationProcessor,testAnnotationProcessor
com.google.j2objc:j2objc-annotations:3.1=annotationProcessor,checkstyle,posterImageMigrationTestAnnotationProcessor,testAnnotationProcessor
com.google.protobuf:protobuf-java:4.33.2=annotationProcessor,posterImageMigrationTestAnnotationProcessor,testAnnotationProcessor
com.h2database:h2:2.4.240=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
com.h3xstream.findsecbugs:findsecbugs-plugin:1.14.0=spotbugsPlugins
com.jayway.jsonpath:json-path:2.9.0=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.nimbusds:nimbus-jose-jwt:10.4=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath