refactor: adapter 구현중..
This commit is contained in:
@@ -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)의 "템플릿 재사용 체크리스트"에 있습니다.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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 에서는 피하세요.**
|
||||
|
||||
+20
-4
@@ -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(
|
||||
|
||||
+1
-1
@@ -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");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+35
-2
@@ -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
|
||||
|
||||
+152
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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`.
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
-6
@@ -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() {
|
||||
|
||||
+40
@@ -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
-6
@@ -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() {
|
||||
|
||||
+2
-6
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
+36
@@ -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;
|
||||
}
|
||||
}
|
||||
+96
@@ -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();
|
||||
}
|
||||
}
|
||||
+55
@@ -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);
|
||||
}
|
||||
}
|
||||
+55
@@ -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();
|
||||
}
|
||||
}
|
||||
+68
@@ -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);
|
||||
}
|
||||
}
|
||||
+41
@@ -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;
|
||||
}
|
||||
}
|
||||
+11
@@ -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 {
|
||||
|
||||
|
||||
+12
-4
@@ -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());
|
||||
}
|
||||
|
||||
+94
@@ -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 {}
|
||||
}
|
||||
+226
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
+40
-34
@@ -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
|
||||
+ " JDBC V1 store(s), "
|
||||
+ expectedJdbcExecutors
|
||||
+ " JDBC V1 executor(s) and "
|
||||
+ expectedOwnerSafeStores
|
||||
+ " owner-safe V2 store(s), but found "
|
||||
+ jdbcStores
|
||||
+ ", "
|
||||
+ jdbcExecutors
|
||||
+ " and "
|
||||
+ ownerSafeStores);
|
||||
+ 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), "
|
||||
+ jdbcExecutors
|
||||
+ " JDBC V1 executor(s), "
|
||||
+ ownerSafeStores
|
||||
+ " owner-safe V2 store(s) and "
|
||||
+ ownerSafeExecutors
|
||||
+ " owner-safe V2 executor(s)";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+29
@@ -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.
|
||||
*
|
||||
|
||||
+8
-3
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+89
@@ -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;
|
||||
}
|
||||
}
|
||||
+6
@@ -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.
|
||||
|
||||
+19
@@ -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()
|
||||
|
||||
+37
@@ -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);
|
||||
|
||||
+282
@@ -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());
|
||||
}
|
||||
}
|
||||
+171
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+101
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
-1
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+18
-3
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+328
@@ -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);
|
||||
}
|
||||
}
|
||||
+343
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user