7055 lines
283 KiB
Markdown
7055 lines
283 KiB
Markdown
# HTTP Client Production Capability Deep Design
|
||
|
||
- 작성일: 2026-07-27
|
||
- 상태: 상세 설계 완료, Phase 0/1 기반·legacy deadline R1·canonical zero-binding 구현, R2 미구현
|
||
- 기준: Java 21, Spring Boot 4.0.0, Gradle 멀티모듈 Clean Architecture
|
||
- 대상 leaf: `adapter-outbound-httpclient`
|
||
- 구현 추적: typed operation/target foundation, legacy JDK 안전 결함과 active logical deadline
|
||
단면까지 적용되었다.
|
||
- 상위 문서:
|
||
[Production Capability Platform Design](2026-07-26-production-capability-platform-design.md)
|
||
|
||
## 0. 구현 상태
|
||
|
||
2026-07-28 기준 구현된 범위:
|
||
|
||
- `application-core`의 framework-free, monotonic, parent-capped `CallBudget`;
|
||
- bounded `HttpDestinationId`, versioned `HttpOperationId`;
|
||
- method/route/semantics/request-response mode/success status/retry/physical-attempt/body 상한을
|
||
고정하는 immutable `HttpOperationDescriptor`와 closed catalog;
|
||
- fixed base authority와 registered relative route/path-segment만 조합하는 target builder;
|
||
- absolute/scheme-relative/traversal/pre-encoded target, base URI user-info/query/fragment 거부;
|
||
- legacy JDK client redirect `NEVER` 명시;
|
||
- legacy streaming의 status-first 검증과 error body callback 차단;
|
||
- retry 바깥에 physical-attempt circuit breaker를 두어 각 wire attempt를 독립 집계하는 수정.
|
||
- caller/configured monotonic `CallBudget` intersection을 buffered/streaming 실행 경로에 연결;
|
||
- blocking logical call과 retry/backoff를 cancellable virtual thread에서 실행하고 cutoff 시
|
||
interrupt/`DEPENDENCY_TIMEOUT` 처리;
|
||
- client별 bounded live-worker admission, non-cooperative worker의 slot retention, shutdown 시
|
||
active task cancellation과 신규 admission 차단;
|
||
- worker MDC 복사/정리와 retry ThreadLocal lifecycle 정렬.
|
||
- strict canonical expected-state/binding/provider map binder와 exact provider/destination/catalog
|
||
resolver;
|
||
- `DISABLED_VERIFIED` descriptor와 zero-binding HTTP runtime resource 0 composition;
|
||
- `httpclient-static-buffered=NOT_IMPLEMENTED` fail-closed ACTIVE admission;
|
||
- legacy settings/configuration의 global Spring scan 분리와 explicit migration binder.
|
||
|
||
아직 구현되지 않은 범위:
|
||
|
||
- application feature-specific production port와 실제 upstream anti-corruption adapter;
|
||
- full compatibility profile tuple/scenario registry와 release-eligible readiness evidence;
|
||
- Apache HC5 pool/acquire/lifetime/idle provider;
|
||
- Apache engine phase별 deadline 전달, wire hard cancellation과 connection quarantine;
|
||
- DNS/address/SSRF/TLS/mTLS/proxy/auth/secret lifecycle;
|
||
- wire/decoded/error/streaming body 전체 제한과 codec/media contract;
|
||
- OTel 단독 propagation과 manual trace interceptor 제거;
|
||
- real-network/TLS/pool/failure qualification 및 R2 readiness card.
|
||
|
||
따라서 현재 `OutboundHttpClient`는 migration용 JDK R1 이하 facade이며 HTTP capability R2가 아니다.
|
||
legacy 실행 경로는 active logical deadline을 사용하지만 operation catalog와 engine phase
|
||
deadline을 아직 사용하지 않는다. Canonical ACTIVE도 현재 `NOT_IMPLEMENTED` card에서 실패한다.
|
||
이 단면만으로 hard cancellation이나 R2를 주장하지 않는다.
|
||
|
||
## 1. 설계 판정
|
||
|
||
현재 HTTP Client 모듈은 timeout, response-size cap, retry, circuit breaker, shutdown guard,
|
||
진단 로깅을 가진 유용한 골격이다. 그러나 운영에서 필요한 보장을 실제로 제공하는
|
||
production capability는 아니다.
|
||
|
||
현재 가장 큰 문제는 기능의 수가 아니라 보장의 정확성이다.
|
||
|
||
1. `globalCallTimeout`은 실제 실행을 중단시키는 total deadline이 아니다.
|
||
2. README가 설명한 retry/circuit-breaker 순서와 코드의 실제 합성 순서가 반대다.
|
||
3. streaming 경로는 HTTP status를 검사하지 않고 4xx/5xx body도 reader에 전달한다.
|
||
4. method만으로 retry 안전성을 판단하며 body replayability와 upstream idempotency 계약이 없다.
|
||
5. JDK client의 pool, DNS, TLS, redirect, proxy, cancellation과 lifecycle이 운영 계약에 없다.
|
||
6. 모든 dependency가 하나의 global settings를 공유하며 실제 destination registry가 없다.
|
||
7. sample의 feature-specific port와 fixture consumer는 있지만, production semantic port 구현과
|
||
실제 HTTP operation/binding이 없다.
|
||
8. manual trace header가 실제 tracer의 sampling 결정을 훼손하고 tenant baggage를 모든 목적지로
|
||
보낼 수 있다.
|
||
9. arbitrary URI, header, credential, redirect와 DNS rebinding에 대한 egress 보안 경계가 없다.
|
||
10. unknown mutation outcome, partial body, decode failure, pool timeout과 cancellation이
|
||
`4xx/5xx/connect/timeout` 몇 개로 합쳐진다.
|
||
|
||
이번 설계의 목표는 범용 `execute(method, url, body)` SDK가 아니다.
|
||
|
||
> feature-specific application port 뒤에서, 등록된 destination과 operation만 호출하고,
|
||
> 하나의 monotonic deadline 안에서 admission·pool·DNS·connect·TLS·write·response·body·retry를
|
||
> 제한하며, retry safety·unknown outcome·보안·관측성·취소를 실행 가능한 계약으로 증명하는
|
||
> outbound HTTP capability
|
||
|
||
선택한 핵심 구조는 다음과 같다.
|
||
|
||
1. Application은 `OutboundHttpClient`가 아니라 `FraudScreeningPort`,
|
||
`PartnerCatalogPort` 같은 feature-specific anti-corruption port에 의존한다.
|
||
2. HTTP method, path template, success status, media type, retry safety와 body replayability는
|
||
adapter의 typed operation catalog에 등록한다.
|
||
3. 호출자는 arbitrary absolute URL이나 raw credential header를 전달하지 않는다.
|
||
4. destination binding과 provider activation은 canonical configuration 하나로 결정한다.
|
||
5. 하나의 total deadline은 모든 대기와 physical attempt를 포함하며 timeout 때 engine call과
|
||
response stream을 적극 취소한다.
|
||
6. retry는 method 이름만이 아니라 operation semantics, request identity, replayable body,
|
||
failure phase, remaining budget를 모두 만족할 때만 가능하다.
|
||
7. mutation이 전송된 뒤 응답을 잃으면 일반 transient failure가 아니라
|
||
`INDETERMINATE`로 반환하고 provider-specific inspect/reconcile을 요구한다.
|
||
8. circuit breaker는 기본적으로 physical attempt를 집계하고, 논리 호출 집계는 별도 이름의
|
||
선택 정책으로만 허용한다.
|
||
9. Apache HttpComponents 5 classic + blocking facade를 초기 R2 reference candidate로 삼고,
|
||
hard-cancel evidence를 통과할 때만 baseline provider로 승격한다. JDK/HTTP2/reactive engine도
|
||
같은 보장을 통과할 때만 R2로 승격한다.
|
||
10. OTel instrumentation이 trace propagation을 단독 소유하며 manual MDC `traceparent`
|
||
writer는 제거한다.
|
||
11. no binding이면 client, pool, evictor, credential refresh, DNS probe가 하나도 생성되지 않는다.
|
||
12. capability별 readiness card가 실제 선택된 operation profile과 engine evidence를 검증한다.
|
||
|
||
## 2. 상위 통합 설계와 이번 심화 범위
|
||
|
||
상위 설계의 HTTP client 절은 다음 방향을 이미 정했다.
|
||
|
||
- named client registry와 dependency별 설정;
|
||
- pool total/per-route/acquisition/idle/DNS/lifecycle;
|
||
- bulkhead와 optional outbound rate limit;
|
||
- declared-safe operation만 retry;
|
||
- total deadline과 active cancellation;
|
||
- redirect와 SSRF 방어;
|
||
- TLS, mTLS, proxy, certificate rotation;
|
||
- request/response bounds;
|
||
- upload/download streaming;
|
||
- OTel trace ownership;
|
||
- failure injection과 pool exhaustion test;
|
||
- application의 feature-specific anti-corruption port.
|
||
|
||
이번 문서는 위 방향을 구현 계획으로 변환할 수 있도록 다음을 확정한다.
|
||
|
||
- application port와 adapter-internal kernel의 정확한 경계;
|
||
- destination, operation, attempt, logical call의 identity;
|
||
- request/response/body 타입과 lifecycle;
|
||
- absolute deadline과 phase budget;
|
||
- retry, circuit breaker, bulkhead, rate limiter의 정확한 실행 순서;
|
||
- safe/idempotent/replayable/unknown outcome의 차이;
|
||
- status별 default와 operation override;
|
||
- pool과 HTTP/1.1·HTTP/2 capacity model;
|
||
- DNS refresh, IP allowlist, rebinding, redirect와 proxy 검증;
|
||
- TLS trust, hostname verification, mTLS와 credential rotation;
|
||
- buffered, streaming upload/download, compression과 decode limit;
|
||
- error taxonomy와 application mapping;
|
||
- metrics, span, log, health와 cardinality;
|
||
- typed activation/configuration과 zero-side-effect 비활성 상태;
|
||
- provider engine 선택과 Gradle dependency ownership;
|
||
- real-network, TLS, DNS, proxy, failure-injection test;
|
||
- capability별 R2 readiness card와 CI aggregate;
|
||
- 기존 `OutboundHttpClient`에서의 단계적 migration.
|
||
|
||
## 3. 증거 기반 현재 상태
|
||
|
||
### 3.1 실제 HTTP capability binding이 없다
|
||
|
||
현재 `adapter-outbound-httpclient`는 `shared-contract`와
|
||
`adapter-outbound-support`에만 project dependency를 둔다. Registry는 application/domain edge를
|
||
허용하지만 production code는 application의 어떤 semantic port도 구현하지 않는다.
|
||
|
||
`OutboundHttpClient`는 adapter package의 기술 타입이며 기본 bean도 없다. README는 fork가
|
||
dependency별 configuration에서 `baseline(...)`을 직접 호출하도록 안내한다. repository 전체에서
|
||
이를 호출하는 production consumer는 없다.
|
||
|
||
초기 조사 시점에는 component scan이 `OutboundHttpSettings`, shutdown guard,
|
||
`RestClient`/builder 차단 BeanPostProcessor, error mapper, logger와 retry policy를 생성해
|
||
“binding 0개”와 “HTTP resource 0개”가 일치하지 않았다. Phase 1 구현에서 이 결함은 폐쇄됐다.
|
||
현재 settings와 두 legacy configuration은 global scan 대상이 아니며 canonical composition은
|
||
immutable configuration, registry, resolver와 sanitized `DISABLED_VERIFIED` descriptor만 만든다.
|
||
기본 `application.yml`과 `application-test.yml`도 legacy `app.outbound.http.*`를 선언하지 않는다.
|
||
|
||
다만 sample에는 이미 다음 seam이 있다.
|
||
|
||
- `RepoStatsPort`;
|
||
- `GetRepoStatsUseCase`;
|
||
- `RepoStatsPortClient` stub;
|
||
- `/worklogs/repoStats` endpoint.
|
||
|
||
이는 application port와 consumer skeleton이 없다는 뜻이 아니다. `RepoStatsPortClient`의
|
||
`fetchRaw()`가 고정 fixture를 반환하므로 실제 HTTP capability를 소비하지 않는다는 뜻이다.
|
||
현재 sample registry edge에도 `sample-portfolio -> adapter-outbound-httpclient`가 없다.
|
||
|
||
따라서 정확한 현재 상태는 다음과 같다.
|
||
|
||
```text
|
||
HTTP leaf:
|
||
settings + generic technical wrapper + tests
|
||
|
||
sample:
|
||
feature-specific port + use case + fixture adapter
|
||
|
||
but:
|
||
no real HTTP operation/binding between them
|
||
!=
|
||
feature-specific application port + upstream adapter + runtime binding
|
||
```
|
||
|
||
현재 capability level은 R0 seam과 일부 R1 local kernel 사이이며 R2가 아니다.
|
||
|
||
### 3.2 total deadline이 실행을 제한하지 않는다
|
||
|
||
`OutboundHttpClient.exchange()`는 다음 deadline을 계산한다.
|
||
|
||
```java
|
||
Instant deadline = Instant.now().plus(settings.globalCallTimeout());
|
||
retryPolicy.beginCall(method, deadline);
|
||
```
|
||
|
||
그러나 이 값은 `OutboundRetryPolicy.shouldRetry()`가 다음 retry를 시작할지 확인할 때만 사용한다.
|
||
현재 attempt를 중단하지 않고 다음도 포함하지 않는다.
|
||
|
||
- connection/pool wait;
|
||
- DNS;
|
||
- connect와 TLS handshake;
|
||
- request body write;
|
||
- response header wait;
|
||
- response body read;
|
||
- streaming reader callback;
|
||
- retry backoff 자체;
|
||
- circuit-breaker/bulkhead wait.
|
||
|
||
첫 attempt가 `globalCallTimeout`보다 오래 걸려도 read timeout 전까지 계속 실행된다. Retry
|
||
predicate가 deadline 직전 true를 반환하면 남은 시간보다 긴 새 attempt도 시작할 수 있다.
|
||
`stream()`은 이 deadline을 검사만 하는 수준도 아니며 absolute deadline/retry context/cancel
|
||
handle을 아예 만들거나 전달하지 않는다. 그러므로 application.yml의 “whole call including
|
||
retries” 주석과 runtime behavior가 일치하지 않는다.
|
||
|
||
### 3.3 retry와 circuit breaker 합성 설명이 코드와 반대다
|
||
|
||
현재 코드는 다음 순서로 decorator를 만든다.
|
||
|
||
```java
|
||
Supplier<T> decorated = countingSupplier;
|
||
decorated = Retry.decorateSupplier(retry, decorated);
|
||
decorated = CircuitBreaker.decorateSupplier(cb, decorated);
|
||
```
|
||
|
||
실행 구조는 다음과 같다.
|
||
|
||
```text
|
||
CircuitBreaker(
|
||
Retry(
|
||
physical attempt
|
||
)
|
||
)
|
||
```
|
||
|
||
따라서 circuit breaker는 retry가 끝난 논리 호출 하나를 집계한다. README와 코드 주석은
|
||
“retry가 circuit breaker 바깥이고 각 attempt가 독립 집계된다”고 설명한다. 원하는 physical
|
||
attempt 집계는 다음 구조여야 한다.
|
||
|
||
```text
|
||
Retry loop(
|
||
CircuitBreaker(
|
||
one physical attempt
|
||
)
|
||
)
|
||
```
|
||
|
||
현재 test는 retry hit count와 meter 존재를 확인하지만 circuit-breaker sliding window가 physical
|
||
attempt를 몇 건 집계했는지는 검증하지 않는다.
|
||
|
||
### 3.4 streaming은 status error를 성공 body처럼 전달한다
|
||
|
||
buffered 경로는 `retrieve()`를 사용해 default 4xx/5xx handler가 예외를 발생시킨다. Streaming
|
||
경로는 `exchange(...)` callback을 사용하면서 status를 확인하지 않는다.
|
||
|
||
Spring Framework는 `RestClient.exchange()`에서 status handler가 자동 적용되지 않는다고
|
||
명시한다. Callback이 status를 직접 처리해야 한다.
|
||
|
||
현재 구현:
|
||
|
||
```java
|
||
streamingClient
|
||
.method(method)
|
||
.uri(uri)
|
||
.exchange((req, res) -> reader.apply(res.getBody()));
|
||
```
|
||
|
||
그 결과 401, 429, 500도 body reader가 정상 결과로 만들 수 있고
|
||
`observer.recordSuccess(...)`가 호출된다.
|
||
|
||
### 3.5 streaming이 size limit의 무제한 우회 경로다
|
||
|
||
현재 buffered response는 `Content-Length`와 counting stream으로 10 MB 기본 한계를 확인한다.
|
||
그러나 streaming 경로에는 다음 제한이 없다.
|
||
|
||
- maximum wire bytes;
|
||
- maximum decoded bytes;
|
||
- logical total deadline across retries/backoff;
|
||
- 별도의 no-progress body idle deadline;
|
||
- content type와 content encoding;
|
||
- record/item count;
|
||
- reader output size;
|
||
- compression expansion ratio.
|
||
|
||
현재 `JdkClientHttpRequestFactory#setReadTimeout`은 selected Spring Framework 7 구현에서
|
||
`sendAsync` completion에 timer를 걸고 timeout 때 future/response body를 cancel/close하는
|
||
attempt elapsed timeout에 가깝다. 이를 socket byte-idle timeout이나 logical-call total
|
||
deadline이라고 부르지 않는다. Non-cooperative callback의 CPU loop도 제한하지 못한다.
|
||
|
||
README와 test는 buffered limit을 넘으면 streaming API를 사용하면 된다고 설명하고, test는
|
||
oversized response 전체를 성공적으로 읽는 것을 기대한다. Streaming은 heap materialization을
|
||
피하는 방법이지 무제한 데이터 허용 권한이 아니다.
|
||
|
||
### 3.6 request 계약이 너무 넓고 동시에 부족하다
|
||
|
||
현재 API:
|
||
|
||
```java
|
||
<T> T exchange(HttpMethod method, String uri, Object requestBody, Class<T> responseType)
|
||
<T> T stream(HttpMethod method, String uri, Function<InputStream, T> reader)
|
||
```
|
||
|
||
문제:
|
||
|
||
- raw Spring `HttpMethod`와 transport `InputStream`이 public adapter API에 노출된다.
|
||
- `String uri`가 relative path인지 absolute URI인지 강제하지 않는다.
|
||
- path/query encoding과 template variable allowlist가 없다.
|
||
- caller가 operation ID나 low-cardinality route template을 제공하지 않는다.
|
||
- request header, `Accept`, `Content-Type`, API version, conditional header를 표현하지 못한다.
|
||
- authentication profile이나 credential ownership이 없다.
|
||
- status별 success/domain outcome mapping이 없다.
|
||
- `Class<T>`는 generic collection과 versioned envelope를 충분히 표현하지 못한다.
|
||
- request body byte limit과 replayability가 없다.
|
||
- operation-specific deadline과 resilience policy가 없다.
|
||
|
||
이 API를 application에 그대로 노출하면 Clean Architecture의 anti-corruption boundary가 사라진다.
|
||
|
||
### 3.7 retry safety가 method set 하나에 묶여 있다
|
||
|
||
현재 retry allowlist는 `GET`, `HEAD`, `PUT`, `DELETE`다. `POST`, `PATCH`는 항상 거부한다.
|
||
|
||
HTTP method의 idempotency는 중요한 출발점이지만 충분하지 않다.
|
||
|
||
- upstream이 PUT/DELETE를 문서와 다르게 구현할 수 있다.
|
||
- request body stream이 다시 열리지 않을 수 있다.
|
||
- idempotent effect여도 각 response는 다를 수 있다.
|
||
- conditional request가 precondition 없이 재실행될 수 있다.
|
||
- POST/PATCH도 upstream이 durable idempotency-key와 replay contract를 제공하면 안전할 수 있다.
|
||
- connect reset이 request 전송 전인지 후인지 모르면 mutation outcome은 불명확하다.
|
||
|
||
RFC 9110도 non-idempotent request 자동 retry는 operation semantics를 실제로 알거나 최초 요청이
|
||
적용되지 않았음을 아는 경우가 아니면 하지 말라고 요구한다.
|
||
|
||
### 3.8 모든 4xx와 많은 transport error가 뭉친다
|
||
|
||
현재 error mapper는:
|
||
|
||
- 400~499 전체를 `DEPENDENCY_4XX_CLIENT`;
|
||
- 500 이상 전체를 `DEPENDENCY_5XX_SERVER`;
|
||
- 알 수 없는 failure를 `DEPENDENCY_CONNECT_FAILED`;
|
||
- shutdown reject를 `DEPENDENCY_CIRCUIT_OPEN`;
|
||
- connect timeout을 `DEPENDENCY_CONNECT_FAILED`;
|
||
- 나머지 timeout을 `DEPENDENCY_TIMEOUT`;
|
||
|
||
으로 분류한다.
|
||
|
||
이 분류로는 다음을 결정할 수 없다.
|
||
|
||
- 401 credential refresh 후 정확히 한 번 replay 가능한가;
|
||
- 404가 정상적인 absence인가 API drift인가;
|
||
- 408/425/429의 retry 조건은 무엇인가;
|
||
- 409/412가 domain conflict/precondition outcome인가;
|
||
- 413이 request contract 위반인가;
|
||
- 415/406이 media negotiation drift인가;
|
||
- 422가 permanent validation인가 idempotency fingerprint mismatch인가;
|
||
- 502/503/504와 일반 500의 retry 차이는 무엇인가;
|
||
- TLS trust, hostname, certificate expiry, proxy auth failure는 무엇인가;
|
||
- pool acquire와 local admission reject를 구분할 수 있는가;
|
||
- response가 일부 도착한 뒤 잘렸는가;
|
||
- mutation이 적용됐는지 모르는가.
|
||
|
||
### 3.9 JDK client의 운영 자원 정책이 없다
|
||
|
||
현재 factory는 destination마다 다음 client를 만든다.
|
||
|
||
```java
|
||
HttpClient.newBuilder()
|
||
.connectTimeout(settings.connectTimeout())
|
||
.build();
|
||
```
|
||
|
||
명시하지 않은 항목:
|
||
|
||
- executor;
|
||
- HTTP version;
|
||
- redirect;
|
||
- proxy;
|
||
- authenticator/cookie handler;
|
||
- SSL context/parameters;
|
||
- connection capacity와 per-route isolation;
|
||
- acquisition timeout;
|
||
- connection TTL/idle validation;
|
||
- DNS resolver/TTL/address selection;
|
||
- shutdown/close ownership;
|
||
- pool metrics.
|
||
|
||
Java 21 JDK client에는 builder-level total/per-route pool과 connection lease timeout을 구성하는
|
||
API가 없다. `jdk.httpclient.connectionPoolSize`는 HTTP/1.1 keep-alive cache의 구현 property이며
|
||
application-level admission이나 per-destination active-call bound를 대체하지 않는다.
|
||
|
||
### 3.10 global settings가 destination 정책을 표현하지 못한다
|
||
|
||
`app.outbound.http.*` 하나가 모든 dependency에 적용된다.
|
||
|
||
실제 운영에서는 다음이 destination마다 다르다.
|
||
|
||
- base URI와 allowed address;
|
||
- API version;
|
||
- required/optional readiness impact;
|
||
- timeout/SLO;
|
||
- pool capacity;
|
||
- protocol version;
|
||
- TLS trust and mTLS identity;
|
||
- authentication;
|
||
- proxy;
|
||
- request/response size;
|
||
- retryable statuses와 idempotency contract;
|
||
- circuit-breaker threshold;
|
||
- rate quota;
|
||
- trace/baggage/privacy policy.
|
||
|
||
현재 required global timeout placeholder는 실제 client binding이 0개여도 bootstrap 설정을
|
||
요구한다. 반대로 client가 여러 개여도 서로 다른 정책을 지정할 수 없다.
|
||
|
||
### 3.11 manual trace propagation은 fork landmine이 아니라 현재 결함이다
|
||
|
||
`TraceContextPropagationInterceptor`는 MDC의 `trace_id`와 `span_id`로 `traceparent`를 만들고
|
||
sampled flag를 항상 `00`으로 기록한다. 실제 tracer가 있어도 먼저 설정된 header를 보존하도록
|
||
구성되면 downstream sampling decision을 끊을 수 있다.
|
||
|
||
추가로 `request_id`와 `tenant_id`를 `baggage`로 모든 destination에 전파한다. Tenant ID가 내부
|
||
service boundary에서는 필요할 수 있어도 외부 partner에 보내도 된다는 뜻은 아니다. Destination별
|
||
data-sharing policy 없는 global allowlist는 privacy boundary가 아니다.
|
||
|
||
W3C `traceparent`/`tracestate` mutation과 sampling flag는 tracer propagator가 소유해야 한다.
|
||
MDC reconstruction은 fallback으로도 기본 활성화하지 않는다.
|
||
|
||
### 3.12 observability가 logical call과 attempt를 구분하지 않는다
|
||
|
||
현재 logger는 dependency, outcome, total duration, retry count를 남기지만:
|
||
|
||
- operation ID가 없다.
|
||
- HTTP status class가 없다.
|
||
- pool/DNS/connect/TLS/write/header/body phase가 없다.
|
||
- physical attempt별 span과 duration이 없다.
|
||
- cancellation과 indeterminate outcome이 없다.
|
||
- request/response bytes가 없다.
|
||
- actual HTTP client duration meter가 없다.
|
||
- pool leased/available/pending meter가 없다.
|
||
|
||
Resilience4j metric filter는 shared `MeterRegistry`에 전역으로 설치되고
|
||
`resilience4j.*` 중 세 meter 외 전부 deny한다. 동일 registry를 사용하는 다른 capability의
|
||
Resilience4j meter까지 차단할 수 있다. 현재 singleton configuration에서는 한 번 설치되지만,
|
||
다중 ApplicationContext 또는 fork가 config를 수동 구성하면 같은 global filter가 반복 설치될 수
|
||
있다.
|
||
|
||
### 3.13 lifecycle guard가 drain/cancel/close를 수행하지 않는다
|
||
|
||
`OutboundHttpShutdownGuard.stop()`은 boolean을 변경할 뿐:
|
||
|
||
- in-flight call count;
|
||
- graceful drain deadline;
|
||
- pending retry/backoff cancellation;
|
||
- response stream close;
|
||
- HTTP engine close;
|
||
- idle evictor stop;
|
||
- credential refresh scheduler stop;
|
||
- forced cancellation;
|
||
|
||
을 수행하지 않는다.
|
||
|
||
새 호출을 reject하는 것은 필요하지만 resource lifecycle 전체는 아니다. Shutdown을
|
||
`DEPENDENCY_CIRCUIT_OPEN`으로 재사용하는 것도 잘못된 operational diagnosis다.
|
||
|
||
### 3.14 현재 test가 증명하는 범위
|
||
|
||
2026-07-27 baseline:
|
||
|
||
```text
|
||
./gradlew :adapter:outbound:httpclient:check --rerun-tasks --console=plain
|
||
BUILD SUCCESSFUL
|
||
```
|
||
|
||
현재 unit/local test는 다음을 증명한다.
|
||
|
||
- settings의 일부 numeric validation;
|
||
- local JDK HttpServer에 대한 200/4xx/5xx/read-timeout mapping;
|
||
- GET retry hit count와 POST no-retry;
|
||
- circuit-open short-circuit;
|
||
- response `Content-Length`/chunked counting;
|
||
- manual MDC header propagation;
|
||
- basic logs와 Resilience4j meters;
|
||
- shutdown boolean gating.
|
||
|
||
증명하지 않는 항목:
|
||
|
||
- real total deadline와 hard cancellation;
|
||
- CB physical-attempt count;
|
||
- streaming 4xx/5xx rejection;
|
||
- connection pool saturation/acquisition/idle/TTL;
|
||
- TLS/mTLS/hostname/certificate rotation;
|
||
- DNS TTL/rebinding/address failover;
|
||
- redirect/proxy/SSRF;
|
||
- compressed body expansion;
|
||
- streaming cancellation/partial output;
|
||
- mutation unknown outcome와 idempotency replay;
|
||
- OAuth token refresh;
|
||
- HTTP/2 multiplexing/GOAWAY;
|
||
- process shutdown drain;
|
||
- multi-destination isolation;
|
||
- real observability instrumentation.
|
||
|
||
현재 compile에는 `ThreadLocalUsage`, locale 없는 `toUpperCase`, test default charset 등 기존
|
||
ErrorProne warning이 있지만 focused check는 성공한다.
|
||
|
||
### 3.15 Retry wiring이 object identity에 의존한다
|
||
|
||
`OutboundHttpClient.baseline(...)`은 `OutboundHttpResilience`와 `OutboundRetryPolicy`를 별도
|
||
parameter로 받는다. Client는 전달받은 policy에 ThreadLocal call context를 기록하지만,
|
||
Resilience4j retry predicate는 `OutboundHttpResilience`를 만들 때 캡처한 policy를 호출한다.
|
||
|
||
두 policy instance가 다르면 predicate는 context가 없다고 판단해 retry를 조용히 비활성화한다.
|
||
현재 test도 이를 critical wiring constraint로 설명한다. Type system, constructor, composition
|
||
validation은 동일 instance를 강제하지 않는다.
|
||
|
||
R2 state machine은 ThreadLocal/object-identity coupling을 제거한다.
|
||
|
||
- immutable attempt context를 retry decision에 명시적으로 전달;
|
||
- retry loop와 decision policy를 한 aggregate가 소유;
|
||
- 동일 instance를 수동으로 맞춰야 하는 factory signature 제거;
|
||
- composition mismatch characterization test;
|
||
- virtual-thread/async boundary에서도 context loss 없음.
|
||
|
||
### 3.16 Response-size violation이 retry와 diagnosis를 깨뜨린다
|
||
|
||
`ResponseSizeBoundingInterceptor`가 size exception을 발생시키지만 현재 error mapper는 이를
|
||
stable contract failure로 알지 못한다. Unknown runtime failure fallback이
|
||
`DEPENDENCY_CONNECT_FAILED`이며 retryable이므로 retry-enabled safe method는 같은 oversized
|
||
response를 반복 다운로드할 수 있다.
|
||
|
||
추가 영향:
|
||
|
||
- 최종 size exception이 observer failure path 밖에서 발생해 structured failure log가 빠질 수 있음;
|
||
- 큰 4xx/5xx body가 status mapping 전에 size exception으로 바뀌어 원래 status를 잃을 수 있음;
|
||
- 현재 size tests는 retry disabled 상태만 검증.
|
||
|
||
R2는 status/header를 먼저 분기하고 wire/decoded bound failure를
|
||
`RESPONSE_TOO_LARGE`/`DECOMPRESSION_LIMIT_EXCEEDED`로 매핑한다. 둘은 default no-retry,
|
||
circuit-breaker ignore이며 logical failure observation을 정확히 한 번 남긴다.
|
||
|
||
## 4. 범위와 명시적 비범위
|
||
|
||
### 4.1 전체 production capability 설계 범위
|
||
|
||
- synchronous/virtual-thread-friendly request-response capability;
|
||
- JSON과 bounded binary buffered response;
|
||
- bounded streaming download callback;
|
||
- bounded/reopenable streaming upload callback;
|
||
- named destination와 typed operation catalog;
|
||
- relative path template와 typed query/header mapping;
|
||
- per-destination Apache HttpComponents classic engine;
|
||
- explicit HTTP/1.1 connection pool;
|
||
- monotonic total deadline와 active cancellation;
|
||
- retry, retry budget, physical-attempt circuit breaker;
|
||
- logical admission과 attempt bulkhead;
|
||
- optional local outbound quota limiter;
|
||
- safe/idempotent/idempotency-key/non-retryable operation;
|
||
- unknown mutation outcome와 reconciliation hook;
|
||
- redirect disabled default와 future-card용 exact allowlisted state-machine;
|
||
- DNS/address/SSRF validation;
|
||
- HTTPS/TLS hostname verification, custom trust와 mTLS;
|
||
- static bearer/API key/OAuth2 client credentials seams; HTTP Basic은 future-card-only;
|
||
- proxy allowlist와 proxy-auth profile;
|
||
- content type/encoding/header/body bounds;
|
||
- OTel/Micrometer observation ownership;
|
||
- typed configuration, activation, lifecycle, health;
|
||
- real TCP/TLS/DNS/proxy/failure integration tests;
|
||
- readiness card와 runbook.
|
||
|
||
위 목록은 이 문서가 설계하는 전체 surface이지 한 번에 R2로 승인되는 minimum profile이 아니다.
|
||
R2는 derived selected card와 exact full effective profile tuple 범위에서만 주장한다.
|
||
|
||
### 4.2 Minimum R2 static baseline
|
||
|
||
최소 ACTIVE R2 profile은 `httpclient-static-buffered` 하나와 그 exact compatibility profile만
|
||
선택하고 다음으로 제한한다.
|
||
|
||
- fixed registered destination + relative route;
|
||
- synchronous H1;
|
||
- bodiless 또는 bounded JSON buffered request/response;
|
||
- `SAFE_READ`;
|
||
- server TLS/hostname verification;
|
||
- auth `none`;
|
||
- direct-only, redirect/cookie/engine-hidden-retry disabled;
|
||
- finite admission/pool/body/deadline와 hard cancellation;
|
||
- safe-read retry budget와 physical-attempt circuit breaker;
|
||
- OTel sanitizer, lifecycle, readiness와 runbook.
|
||
|
||
다음은 minimum profile 밖이다.
|
||
|
||
- `IDEMPOTENT_MUTATION`/`KEYED_MUTATION` -> `httpclient-idempotent-mutation`;
|
||
- `NON_RETRYABLE_MUTATION` -> `httpclient-non-retryable-mutation`;
|
||
- streaming download/upload -> 각각의 streaming card;
|
||
- mTLS, OAuth2 client credentials, required proxy, HTTP/2, untrusted fetch -> 각각의 card.
|
||
|
||
Bounded binary, custom server trust, API key와 static bearer는 새 card ID를 만들지 않고
|
||
`httpclient-static-buffered`의 conditional effective mode로 지원할 수 있다. 단 profile
|
||
compatibility registry에 provider/protocol/request·response body/TLS/auth/proxy/redirect/
|
||
operation-semantics 전체 tuple이 exact entry로 존재하고 mode-specific 및 cross-mode
|
||
HSEC/HRES/HOBS/HCMP scenario가 모두 포함된 경우에만 활성화한다. 기본 card 시나리오나 축별
|
||
지원 합집합만으로 이를 지원했다고 간주하지 않는다.
|
||
|
||
Redirect-follow, request-signature와 HTTP Basic은 현재 canonical card set에서 지원하지 않는다.
|
||
관련 state-machine 설계는 future card를 위한 것이며, mode가 설정되면 card 등록 전에는
|
||
startup/readiness가 실패한다. “disabled by default”는 켤 수 있다는 의미가 아니다.
|
||
|
||
### 4.3 별도 optional capability로 열어둘 항목
|
||
|
||
- HTTP Service Interface proxy;
|
||
- conditional GET/ETag;
|
||
- RFC-compliant HTTP cache;
|
||
- HTTP/2 multiplexed engine;
|
||
- reactive `WebClient` engine;
|
||
- OAuth2 token exchange/JWT bearer;
|
||
- HTTP message signatures;
|
||
- resumable/range download;
|
||
- multipart/form-data upload;
|
||
- webhook callback registration;
|
||
- controlled dynamic public egress through an egress proxy;
|
||
- service discovery/load-balancer integration;
|
||
- client-side hedging for safe reads;
|
||
- response streaming to file/objectstorage workflow;
|
||
- provider-specific rate-limit header interpretation.
|
||
|
||
Optional 항목은 같은 raw API에 boolean을 추가하지 않는다. 요구 guarantee, dependency,
|
||
concurrency model이 다르면 별도 readiness card와 provider profile을 갖는다.
|
||
|
||
### 4.4 이번 범위에서 제외
|
||
|
||
- controller/inbound DTO와 HTTP server behavior;
|
||
- WebSocket, SSE long-lived subscription, gRPC;
|
||
- business saga/compensation;
|
||
- business-level batch orchestration;
|
||
- external API별 domain DTO와 mapping;
|
||
- arbitrary user URL fetcher;
|
||
- browser cookie/session emulation;
|
||
- transparent application-wide retry annotation/AOP;
|
||
- distributed transaction;
|
||
- service mesh/egress gateway 자체의 설치;
|
||
- synthetic benchmark 수치;
|
||
- HTTP/3 production baseline.
|
||
|
||
사용자가 제공한 URL을 fetch해야 하는 feature는 일반 destination client가 아니다. Egress proxy,
|
||
quarantine, content scan, strict public-address policy와 별도 threat model을 가진 capability로
|
||
설계한다.
|
||
|
||
## 5. HARD invariants
|
||
|
||
다음은 구현 편의를 위해 낮출 수 없다.
|
||
|
||
1. `domain-core`와 `application-core`에 Spring HTTP, Apache/JDK client, Resilience4j,
|
||
Micrometer/OTel, URI transport 타입을 노출하지 않는다.
|
||
2. Application use case는 generic `OutboundHttpClient`나
|
||
`execute(method, url, body)`에 의존하지 않는다.
|
||
3. Destination ID와 operation ID는 bounded registry 값이며 caller 입력으로 동적 생성하지 않는다.
|
||
4. Base URI, host, port, proxy, credential, TLS bundle은 application command에 포함하지 않는다.
|
||
5. Normal operation은 relative path template만 사용하며 absolute URI override를 거부한다.
|
||
6. Caller input을 raw path/query/header 문자열 연결에 사용하지 않는다.
|
||
7. `Authorization`, `Cookie`, proxy credential, API key를 inbound request에서 자동 전달하지 않는다.
|
||
8. Redirect는 default disabled다. Enabled hop마다 destination/IP/credential policy를 다시
|
||
검증한다.
|
||
9. HTTPS production destination에서 trust-all, hostname verification disable,
|
||
`NoopHostnameVerifier`를 허용하지 않는다.
|
||
10. Plain HTTP는 explicit local/test 또는 승인된 private-network exception 없이는 prod에서
|
||
거부한다.
|
||
11. 하나의 total deadline이 admission, pool wait, attempt, backoff와 body consumption 전체를
|
||
포함한다.
|
||
12. Deadline expiry는 flag만 기록하지 않고 transport task와 response body를 적극 취소/close한다.
|
||
13. Wall clock을 elapsed deadline 계산에 사용하지 않는다.
|
||
14. Method만으로 retry를 허용하지 않는다.
|
||
15. Request body를 재생할 수 없으면 automatic retry를 허용하지 않는다.
|
||
16. Non-idempotent mutation은 transmission 가능성이 생긴 뒤 upstream이 문서화한
|
||
idempotency/reconciliation 계약 없이 replay하지 않는다. Exact `NOT_SENT`가 증명된
|
||
pre-send restart는 replay가 아니며 별도 bounded restart policy/budget에서만 허용한다.
|
||
17. Mutation request가 전송된 뒤 response를 잃은 경우 ordinary timeout으로 축소하지 않는다.
|
||
18. `INDETERMINATE` mutation을 새 operation ID/key로 blind retry하지 않는다.
|
||
19. Underlying engine의 hidden automatic retry와 redirect는 끈다.
|
||
20. Circuit breaker metric이 physical attempt 정책이라고 문서화되면 실제 physical attempt를
|
||
개별 집계한다.
|
||
21. Retry backoff 중 physical-attempt bulkhead permit과 connection을 보유하지 않는다.
|
||
22. Virtual thread는 bulkhead나 connection pool을 무한대로 만들어도 된다는 근거가 아니다.
|
||
23. Pool wait와 network connect timeout을 같은 error로 합치지 않는다.
|
||
24. Streaming은 무제한 경로가 아니다.
|
||
25. Wire bytes와 decoded bytes의 limit을 구분한다.
|
||
26. Status와 headers를 검증하기 전에 response body를 consumer에 넘기지 않는다.
|
||
27. Response body/stream은 모든 success, failure, cancellation 경로에서 정확히 한 번 close한다.
|
||
28. Error response body도 작은 별도 cap 안에서만 선택적으로 decode하며 log에 남기지 않는다.
|
||
29. Content type, charset, content encoding과 JSON constraint mismatch를 ordinary 5xx로 숨기지
|
||
않는다.
|
||
30. Metric tag와 span name에 raw path, query, user ID, tenant ID, idempotency key, token,
|
||
payload를 사용하지 않는다.
|
||
31. Trace propagation은 실제 tracer/instrumentation 하나가 소유한다.
|
||
32. Baggage는 destination별 explicit allowlist가 없으면 전파하지 않는다.
|
||
33. Liveness가 외부 dependency 상태에 의존하지 않는다.
|
||
34. Readiness가 모든 pod를 동시에 eject해 outage를 증폭하지 않도록 dependency impact를
|
||
명시한다.
|
||
35. No binding이면 pool, thread/executor, evictor, DNS lookup, token refresh와 health probe가
|
||
생성되지 않는다.
|
||
36. Legacy global settings와 canonical binding이 함께 있거나 충돌하면 startup을 실패시킨다.
|
||
37. Engine/provider가 요구 보장을 증명하지 못하면 더 약한 guarantee로 조용히 downgrade하지
|
||
않는다.
|
||
38. Application-level fallback/compensation은 HTTP kernel 안에 넣지 않는다.
|
||
39. Adapter는 application/domain DTO를 wire format으로 그대로 serialize하지 않는다.
|
||
40. R2 label은 real-network, TLS, pool, cancellation, security failure evidence 없이 부여하지
|
||
않는다.
|
||
|
||
## 6. 대안 검토
|
||
|
||
### A. 현재 `OutboundHttpClient`에 옵션만 계속 추가
|
||
|
||
장점:
|
||
|
||
- 변경량이 작다.
|
||
- 기존 tests를 재사용하기 쉽다.
|
||
|
||
문제:
|
||
|
||
- raw method/URI/body/class API가 더 커진다.
|
||
- application semantic port와 transport kernel이 분리되지 않는다.
|
||
- destination/operation policy를 runtime argument로 넘기게 된다.
|
||
- replayability, unknown outcome, body lifecycle을 표현하기 어렵다.
|
||
- boolean 조합이 잘못된 상태를 만들 수 있다.
|
||
|
||
선택하지 않는다. 현재 API는 migration facade로만 유지한다.
|
||
|
||
### B. Application에 하나의 범용 `HttpPort`를 둔다
|
||
|
||
예:
|
||
|
||
```java
|
||
HttpResponse call(HttpRequest request);
|
||
```
|
||
|
||
이는 HTTP method, status, header, URI, JSON과 transport failure를 application에 유출한다.
|
||
Use case가 upstream protocol을 직접 알고 anti-corruption adapter가 사라진다. 선택하지 않는다.
|
||
|
||
### C. OpenFeign/HTTP Interface annotation을 application port에 직접 붙인다
|
||
|
||
선언적 API는 편리하지만 annotation과 wire DTO가 application boundary를 오염시킨다. Retry,
|
||
auth, status mapping이 proxy magic으로 숨을 수도 있다.
|
||
|
||
Spring HTTP Service Interface는 허용하되 adapter package의 upstream wire client로만 둔다.
|
||
Application port를 별도로 구현한다.
|
||
|
||
### D. JDK `HttpClient`를 R2 default로 계속 사용
|
||
|
||
장점:
|
||
|
||
- JDK 21 기본 제공;
|
||
- dependency가 적다;
|
||
- HTTP/2와 async cancellation API;
|
||
- immutable/thread-safe client.
|
||
|
||
문제:
|
||
|
||
- builder API로 per-destination total/per-route pool과 lease timeout을 구성하기 어렵다.
|
||
- pool resource와 pending lease observability가 제한된다.
|
||
- custom DNS/address admission과 connection lifecycle evidence가 어렵다.
|
||
- keep-alive pool 일부가 implementation property에 의존한다.
|
||
|
||
JDK engine은 R1 compatibility 또는 제한된 profile로 열어두되 초기 R2 default로 선택하지 않는다.
|
||
|
||
### E. Apache HttpComponents 5 classic + Spring `RestClient`
|
||
|
||
장점:
|
||
|
||
- Spring MVC/imperative baseline과 맞는다.
|
||
- total/per-route pool, lease timeout, TTL, idle validation, DNS resolver, TLS, proxy를 제어할 수
|
||
있다.
|
||
- hard cancellation과 connection eviction을 검증할 수 있다.
|
||
- broad reactive stack 없이 virtual-thread-friendly blocking facade를 제공할 수 있다.
|
||
|
||
단점:
|
||
|
||
- total deadline을 `RestClient` timeout 하나로 얻을 수 없다.
|
||
- active cancellation을 위한 execution wrapper와 engine-specific evidence가 필요하다.
|
||
- HTTP/2는 별도 provider 판단이 필요하다.
|
||
|
||
초기 minimum-R2 reference candidate로 선택하되 hard-cancel/card evidence 전에는 R2로 승격하지
|
||
않는다.
|
||
|
||
### F. `WebClient` + Reactor Netty를 모든 호출의 default로 사용
|
||
|
||
장점:
|
||
|
||
- cancellation/backpressure/HTTP2/pool 설정이 풍부하다.
|
||
- streaming에 강하다.
|
||
|
||
문제:
|
||
|
||
- 현재 imperative/virtual-thread template에 reactive runtime과 context semantics를 강제한다.
|
||
- `.block()` facade는 cancellation/context/metrics를 잘못 연결하기 쉽다.
|
||
- 모든 use case가 reactive일 필요는 없다.
|
||
|
||
Reactive application이나 HTTP/2 streaming 요구에 대한 별도 provider로 열어둔다.
|
||
|
||
### G. Apache async engine을 직접 감싼 blocking facade
|
||
|
||
가장 강한 cancellation과 HTTP/2 확장 경로를 제공하지만 codec, Spring HTTP Service integration,
|
||
error mapping을 더 많이 직접 소유한다. 초기 Phase 1은 Apache classic으로 시작하고, classic
|
||
hard-cancel evidence가 요구를 만족하지 못하면 이 provider로 승격한다.
|
||
|
||
### H. Resilience를 Spring annotation/AOP로 적용
|
||
|
||
Method annotation은 operation descriptor, body replayability, failure phase와 remaining deadline을
|
||
알기 어렵다. Decorator order도 proxy order에 숨는다. 선택하지 않는다.
|
||
|
||
### I. Explicit call state machine에서 Resilience4j primitive를 사용
|
||
|
||
선택한 방식이다.
|
||
|
||
- Resilience4j registry/state machine은 재사용한다.
|
||
- Retry loop와 ordering은 adapter가 명시적으로 소유한다.
|
||
- AOP/annotation은 쓰지 않는다.
|
||
- Engine의 automatic retry는 끈다.
|
||
- 각 physical attempt의 permission/metric/span을 코드와 test가 검증한다.
|
||
|
||
## 7. 목표 아키텍처
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
UC[Application use case]
|
||
PORT[Feature-specific outbound port]
|
||
ACL[Upstream anti-corruption adapter]
|
||
CAT[Typed operation catalog]
|
||
REG[Destination registry]
|
||
KERNEL[HTTP execution kernel]
|
||
ADMIT[Logical admission]
|
||
RETRY[Retry budget and loop]
|
||
CB[Physical-attempt circuit breaker]
|
||
LIMIT[Attempt bulkhead and local quota]
|
||
ENGINE[Apache classic engine]
|
||
POOL[Destination-isolated pool]
|
||
DNS[Validated DNS/address resolver]
|
||
TLS[TLS/mTLS/auth/proxy]
|
||
UP[External dependency]
|
||
OBS[Observation/error/lifecycle]
|
||
|
||
UC --> PORT
|
||
ACL --> PORT
|
||
ACL --> CAT
|
||
ACL --> KERNEL
|
||
KERNEL --> REG
|
||
KERNEL --> ADMIT
|
||
ADMIT --> RETRY
|
||
RETRY --> LIMIT
|
||
LIMIT --> CB
|
||
CB --> ENGINE
|
||
ENGINE --> POOL
|
||
POOL --> DNS
|
||
ENGINE --> TLS
|
||
DNS --> UP
|
||
TLS --> UP
|
||
KERNEL --> OBS
|
||
```
|
||
|
||
Dependency direction:
|
||
|
||
```text
|
||
production fork:
|
||
application-core
|
||
owns feature-specific port and application outcome
|
||
|
||
adapter-outbound-httpclient
|
||
implements that production port
|
||
owns external wire DTO, HTTP interface, operation descriptor,
|
||
execution kernel, engine, resilience, auth and mapping
|
||
|
||
skeleton sample:
|
||
sample-portfolio
|
||
owns RepoStatsPort, use case, wire mapper and sample-local RepoStatsHttpAdapter
|
||
consumes the HTTP leaf's adapter-consumer SPI after an explicit registry edge
|
||
|
||
adapter-outbound-httpclient
|
||
never depends on sample-portfolio
|
||
|
||
adapter-outbound-support
|
||
may provide framework-neutral outbound correlation/observation helpers
|
||
|
||
app-bootstrap
|
||
binds selected destinations/providers and validates descriptors
|
||
```
|
||
|
||
`adapter-outbound-httpclient`가 다른 outbound adapter를 직접 호출하지 않는다. OAuth token,
|
||
distributed rate quota, secret manager 등이 별도 capability여도 adapter-to-adapter edge를
|
||
추가하지 않는다. 필요한 framework-neutral port나 bootstrap composition을 설계하고 registry
|
||
승인 뒤 연결한다.
|
||
|
||
## 8. 모듈과 계층 소유권
|
||
|
||
### 8.1 `domain-core`
|
||
|
||
허용:
|
||
|
||
- 외부 서비스 결과가 실제 domain concept이면 domain value;
|
||
- upstream과 무관한 invariant.
|
||
|
||
금지:
|
||
|
||
- HTTP status/method/header;
|
||
- URI;
|
||
- timeout/retry;
|
||
- JSON/wire DTO;
|
||
- provider ID.
|
||
|
||
### 8.2 `application-core`
|
||
|
||
소유:
|
||
|
||
- `FraudScreeningPort`, `TaxQuotePort`, `PartnerCatalogPort` 같은 semantic port;
|
||
- application request/result;
|
||
- domain/application-level unavailable/indeterminate outcome;
|
||
- use-case deadline 전달을 위한 framework-neutral `CallBudget`가 필요하다면 그 contract;
|
||
- compensation/reconciliation use case.
|
||
|
||
금지:
|
||
|
||
- `OutboundHttpClient`;
|
||
- Spring `HttpMethod`, `HttpHeaders`, `ResponseEntity`;
|
||
- Apache/JDK client exception;
|
||
- OAuth/SSL bundle;
|
||
- raw URL.
|
||
|
||
예:
|
||
|
||
```java
|
||
public interface PartnerCatalogPort {
|
||
CatalogLookupResult find(ProductReference reference, CallBudget budget);
|
||
}
|
||
```
|
||
|
||
`CatalogLookupResult`의 `NotFound`는 upstream 404 자체가 아니라 application이 정의한 absence다.
|
||
|
||
### 8.3 `adapter-outbound-httpclient`
|
||
|
||
소유:
|
||
|
||
- production fork의 upstream별 `application-core` port implementation;
|
||
- wire request/response DTO와 mapper;
|
||
- operation catalog;
|
||
- destination binding;
|
||
- path/query/header encoding;
|
||
- engine SPI와 Apache provider;
|
||
- timeout/cancellation;
|
||
- resilience;
|
||
- TLS/auth/proxy/DNS;
|
||
- status/error mapping;
|
||
- observability/lifecycle;
|
||
- provider tests.
|
||
|
||
HTTP Service annotation interface를 쓴다면 이 leaf 안에 둔다.
|
||
|
||
Template sample처럼 feature port가 `sample-portfolio`에 격리된 경우 HTTP leaf가 sample port를
|
||
구현하지 않는다. 대신 §10.5의 bounded adapter-consumer SPI를 제공하고 sample-local outbound
|
||
adapter가 이를 사용한다.
|
||
|
||
### 8.4 `shared-contract`
|
||
|
||
정말 여러 runtime leaf가 동일하게 소비할 때만 다음과 같은 value-only operational contract를
|
||
둘 수 있다.
|
||
|
||
- bounded capability descriptor;
|
||
- generic readiness level;
|
||
- low-cardinality dependency outcome vocabulary;
|
||
- framework-neutral deadline carrier.
|
||
|
||
HTTP operation, URL, method, credential을 skeleton-wide contract로 올리지 않는다.
|
||
|
||
### 8.5 `adapter-outbound-support`
|
||
|
||
유지 가능한 책임:
|
||
|
||
- correlation value 추출;
|
||
- 공통 clock abstraction;
|
||
- generic bounded metric naming helper;
|
||
- secret-safe diagnostic formatter.
|
||
|
||
HTTP-specific retry/status/pool/DNS/TLS 정책은 httpclient leaf가 소유한다.
|
||
|
||
### 8.6 `app-bootstrap`
|
||
|
||
소유:
|
||
|
||
- canonical activation;
|
||
- typed configuration binding;
|
||
- selected provider/destination validation;
|
||
- SSL bundle와 secret reference resolution;
|
||
- capability descriptor aggregation;
|
||
- required readiness policy;
|
||
- lifecycle ordering.
|
||
|
||
Business API operation이나 mapping을 bootstrap에 넣지 않는다.
|
||
|
||
### 8.7 `sample-portfolio`
|
||
|
||
Production leaf가 sample을 의존하지 않는다. 현재 sample은 `RepoStatsPort`,
|
||
`GetRepoStatsUseCase`, fixture `RepoStatsPortClient`를 이미 가진다.
|
||
|
||
실제 HTTP reference consumer로 전환할 때:
|
||
|
||
- `sample-portfolio -> adapter-outbound-httpclient` edge를
|
||
`src/config/architecture/modules.json`에 명시적으로 추가;
|
||
- sample-local `RepoStatsHttpAdapter`가 `RepoStatsPort`를 구현;
|
||
- adapter-consumer SPI는 sample adapter package에서만 사용;
|
||
- `repoUrl` raw string을 `RepositoryCoordinates(hostProfile, owner, repository)` 같은 validated
|
||
application value로 변경;
|
||
- inbound가 허용된 Git provider URL을 parse하되 application/adaptor로 absolute URL을 전달하지
|
||
않음;
|
||
- registered fixed destination + relative route로 재구성;
|
||
- sample-off production build에서는 이 consumer/binding이 없어도 됨;
|
||
- sample contract는 provider/card implementation evidence와 deployment ACTIVE readiness를
|
||
대신하지 않음.
|
||
|
||
Registry edge 변경 전에는 sample에서 HTTP leaf type을 import하지 않는다.
|
||
|
||
### 8.8 두 consumer shape의 선택
|
||
|
||
```text
|
||
실제 fork production:
|
||
application-core feature port
|
||
<- adapter-outbound-httpclient의 feature adapter
|
||
|
||
template sample fixture:
|
||
sample-local feature port
|
||
<- sample-local outbound adapter
|
||
-> adapter-outbound-httpclient adapter-consumer SPI
|
||
```
|
||
|
||
새 business concept를 production `application-core`에 demo 목적으로 추가하지 않는다. 반대로
|
||
HTTP leaf가 sample business type을 import하지 않는다. 어느 shape든 controller/use case가 generic
|
||
HTTP kernel을 직접 호출하는 것은 금지한다.
|
||
|
||
## 9. Identity와 vocabulary
|
||
|
||
### 9.1 Destination ID
|
||
|
||
`HttpDestinationId`는 bounded configuration/registry key다.
|
||
|
||
예:
|
||
|
||
```text
|
||
partner-catalog
|
||
fraud-screening
|
||
tax-service
|
||
github-api
|
||
```
|
||
|
||
규칙:
|
||
|
||
- lowercase kebab case;
|
||
- committed registry와 typed binding에 존재;
|
||
- metric/span/log tag로 사용 가능;
|
||
- tenant/user/request에서 동적 생성 금지;
|
||
- host name과 동일할 필요 없음;
|
||
- credential/TLS/pool isolation 단위.
|
||
|
||
### 9.2 Operation ID
|
||
|
||
`HttpOperationId`는 한 upstream API operation의 안정적인 low-cardinality ID다.
|
||
|
||
```text
|
||
partner-catalog.get-product.v1
|
||
fraud-screening.evaluate.v2
|
||
tax-service.create-quote.v1
|
||
```
|
||
|
||
Operation ID는 다음 정책의 join key다.
|
||
|
||
- method/path template;
|
||
- success/error status;
|
||
- body codec;
|
||
- retry safety;
|
||
- idempotency-key;
|
||
- deadline;
|
||
- size;
|
||
- observability;
|
||
- readiness test.
|
||
|
||
Endpoint path나 Java method name에서 runtime 추론하지 않는다.
|
||
|
||
### 9.3 Logical call ID
|
||
|
||
한 application port invocation 안에서 retry/redirect/auth-refresh를 묶는 opaque attempt-group
|
||
identity다.
|
||
|
||
- log/trace correlation용;
|
||
- metric tag로 사용 금지;
|
||
- idempotency key와 다름;
|
||
- application business ID와 다름;
|
||
- 한 logical call의 모든 physical attempt에서 동일.
|
||
|
||
### 9.4 HTTP request attempt와 공통 amplification budget
|
||
|
||
`HttpRequestAttempt`는 origin으로 wire request 하나를 시작하려는 단위다. 0부터 증가하는
|
||
`physicalAttemptOrdinal`은 다음을 모두 같은 연속 번호로 센다.
|
||
|
||
- initial request;
|
||
- ordinary retry;
|
||
- confirmed-`NOT_SENT` pre-send restart;
|
||
- same-intent/reconciliation 뒤 replay;
|
||
- 안전하게 허용한 redirect hop;
|
||
- 401 credential refresh 뒤 auth replay;
|
||
- provider가 processing evidence로 증명하고 kernel이 새 요청으로 승인한 protocol restart.
|
||
|
||
사유별 counter는 별도로 유지한다.
|
||
|
||
```text
|
||
ordinaryRetryCount
|
||
preSendRestartCount
|
||
authReplayCount
|
||
redirectHopCount
|
||
sameIntentReplayCount
|
||
confirmedNotProcessedRestartCount
|
||
```
|
||
|
||
그러나 각 counter/budget의 합이 전체 증폭 상한을 우회해서는 안 된다.
|
||
|
||
```text
|
||
maxProtectedPhysicalAttemptsPerLogicalCall
|
||
maxNestedCredentialRequestsPerLogicalCall
|
||
maxReconciliationRequestsPerLogicalCall
|
||
maxProxyConnectRequestsPerRootCall
|
||
maxRevocationHttpRequestsPerRootCall
|
||
maxTotalHttpRequestAttemptsPerRootCall
|
||
```
|
||
|
||
Protected destination의 모든 origin request start는 첫 번째와 마지막 shared token을 원자적으로
|
||
소비한다. Token endpoint, in-call reconciliation, HTTP CONNECT proxy와 named HTTP OCSP/CRL lookup의
|
||
각 실제 HTTP request도 자신의 bounded counter와 같은 root-call total token을 소비한다.
|
||
Redirect/auth refresh/retry가 중첩돼도 곱셈 증폭되지 않는다. DNS query와 A/AAAA별 TCP connect는
|
||
HTTP request가 아니므로 이 token 대신 resolver/per-address connect cap과 같은 total deadline을
|
||
사용한다. Deferred reconciliation use case가 나중에 독립 호출로 시작되면 새 root-call budget을
|
||
갖지만 같은 operation identity와 별도 scheduler quota를 유지한다.
|
||
|
||
`maxAttempts`라는 모호한 이름은 canonical model에서 사용하지 않는다. Operation별 ordinary retry
|
||
횟수와 위 physical/root ceiling을 분리한다. Runtime config가 catalog 상한을 낮출 수는 있지만
|
||
증가시킬 수 없다. Protected/root 상한은 finite positive이고 비활성일 수 있는 child 상한은 finite
|
||
non-negative이며 다음 cross-field invariant를 만족한다.
|
||
|
||
```text
|
||
1 <= maxProtectedPhysicalAttemptsPerLogicalCall
|
||
maxNestedCredentialRequestsPerLogicalCall >= 0
|
||
maxReconciliationRequestsPerLogicalCall >= 0
|
||
maxProxyConnectRequestsPerRootCall >= 0
|
||
maxRevocationHttpRequestsPerRootCall >= 0
|
||
maxTotalHttpRequestAttemptsPerRootCall >= minimumRequiredRootHttpAttempts(effective profile)
|
||
```
|
||
|
||
`minimumRequiredRootHttpAttempts`는 exact profile의 cold-path scenario마다 계산한다. 최소 initial
|
||
protected request, OAuth cache miss/401 refresh, required proxy CONNECT, hard-fail revocation lookup와
|
||
advertised auth replay에 필요한 protected request를 포함한다. 예를 들어 one-401 OAuth replay를
|
||
지원하면 protected ceiling은 최소 2이고 해당 scenario의 token refresh + 두 protected requests가
|
||
root ceiling 안에 들어야 한다. Root ceiling이 reason-specific maxima의 합보다 작을 수는 있지만,
|
||
그 때문에 required scenario 하나라도 구조적으로 실행 불가능하면 startup/qualification이 실패한다.
|
||
|
||
Protected origin 밖에서 실제 HTTP request를 만드는 child path는 공통 authorization protocol을
|
||
사용한다.
|
||
|
||
```text
|
||
NestedHttpRequestKind = OAUTH_TOKEN | RECONCILIATION | PROXY_CONNECT | REVOCATION
|
||
NestedHttpAuthorizationLease(
|
||
kind,
|
||
rootCallId,
|
||
childProfileFingerprint,
|
||
childAttemptOrdinal,
|
||
state = AUTHORIZED | BOUND_TO_ENGINE | ABORTED
|
||
)
|
||
```
|
||
|
||
`NestedHttpAuthorizationBroker`는 parent root context가 이 lease만 발급하는 최소 capability이며
|
||
provider/child adapter에 mutable counter나 refill API를 노출하지 않는다. Broker 생성 시 exact
|
||
dependency DAG에서 다음 immutable edge set을 캡처한다.
|
||
|
||
```text
|
||
AllowedChildEdge(
|
||
parentProfileFingerprint,
|
||
kind,
|
||
childProfileFingerprint,
|
||
childOperationId,
|
||
childAuthorityPolicyFingerprint
|
||
)
|
||
```
|
||
|
||
Caller가 전달한 kind/profile/operation edge가 이 set과 exact match하지 않거나 다른 root의 broker/
|
||
lease를 재사용하면 child/root token 소비 전에 거절한다. Lease bind 시 provider가 제출한 resolved
|
||
child profile, operation, scheme/authority-policy fingerprint도 lease edge와 byte-for-byte 일치해야
|
||
한다. 따라서 broker를 generic registered-child/authority oracle로 사용할 수 없다.
|
||
`tryAuthorizeNestedHttpRequest`는 deadline/cancellation과 해당 child cap의 순수 availability를 먼저
|
||
검사하고, exact child counter token을 원자적으로 한 번 소비해 lease를 만든다. 실제 wire start
|
||
직전에는 같은 root-call total HTTP token 하나를 uncommitted reserve하고 cancellation과 engine
|
||
handoff를 race한다. Handoff가 이기면 lease/root token을 bind/commit하고, 그 전 실패면 root token은
|
||
반납하되 child reason token은 amplification churn을 막기 위해 되살리지 않고 lease를 `ABORTED`로
|
||
exactly once 닫는다. Cache hit, 기존 CONNECT tunnel 재사용처럼 HTTP request가 발생하지 않는
|
||
경로는 lease/token을 소비하지 않는다. Provider가 이 broker를 호출하지 않고 child HTTP request를
|
||
시작할 수 있으면 해당 profile은 release-eligible이 아니다.
|
||
|
||
`TransportConnectAttempt`는 A/AAAA address candidate 하나에 대한 connect 시도이며
|
||
`HttpRequestAttempt`와 다르다. Request byte 전 address failover는 connect event/metric일 뿐
|
||
`http.request.resend_count`나 physical attempt ordinal을 증가시키지 않는다. First request byte 뒤
|
||
재연결은 새 `HttpRequestAttempt`이며 operation state machine 승인이 필요하다.
|
||
|
||
Engine autonomous resend가 start 전 shared token/deadline/body/CB/bulkhead/span gate를 호출할 수
|
||
없으면 hidden resend를 비활성화해야 하며 해당 provider/profile은 release-eligible이 아니다.
|
||
사후 관측만으로 공통 상한을 지켰다고 주장하지 않는다.
|
||
|
||
### 9.5 Operation attempt ID와 idempotency key
|
||
|
||
Mutation은 application workflow가 생성해 재시도/재시작에도 보존하는 안정적인
|
||
`OperationAttemptId`를 가질 수 있다. Adapter는 이를 upstream 형식의 idempotency key로 encode한다.
|
||
|
||
규칙:
|
||
|
||
- 첫 network call 전에 생성;
|
||
- response loss 뒤에도 caller가 보유;
|
||
- payload fingerprint와 결합;
|
||
- 다른 intent에 재사용 금지;
|
||
- log/metric에 raw value 금지;
|
||
- provider가 expiry와 replay semantics를 문서화한 경우에만 retry 근거.
|
||
|
||
### 9.6 Policy revision
|
||
|
||
Destination와 operation policy에는 stable ID와 revision/digest가 있다.
|
||
|
||
```text
|
||
destination profile: partner-catalog-r2 / revision 4
|
||
operation profile: partner-catalog.get-product.v1 / revision 2
|
||
```
|
||
|
||
같은 revision의 canonical digest가 배포 사이에서 달라지면 startup을 실패시킨다. Rolling
|
||
deployment에서 retry semantics가 조용히 변하는 것을 막는다.
|
||
|
||
## 10. Application semantic port와 adapter boundary
|
||
|
||
### 10.1 Feature-specific port 원칙
|
||
|
||
Application port는 upstream HTTP API를 복제하지 않고 use case가 필요한 의미만 노출한다.
|
||
|
||
좋은 예:
|
||
|
||
```java
|
||
public interface FraudScreeningPort {
|
||
ScreeningDecision evaluate(ScreeningSubject subject, CallBudget budget);
|
||
}
|
||
```
|
||
|
||
나쁜 예:
|
||
|
||
```java
|
||
public interface HttpPort {
|
||
HttpResponse execute(String method, String url, Map<String, String> headers, Object body);
|
||
}
|
||
```
|
||
|
||
### 10.2 Wire DTO 격리
|
||
|
||
Adapter 흐름:
|
||
|
||
```text
|
||
application request
|
||
-> upstream request mapper
|
||
-> wire DTO
|
||
-> registered HTTP operation
|
||
-> wire response DTO
|
||
-> schema/semantic validation
|
||
-> application result
|
||
```
|
||
|
||
Wire DTO는:
|
||
|
||
- upstream field name/version/null semantics를 소유;
|
||
- Jackson annotation을 가질 수 있음;
|
||
- application/domain package로 반환하지 않음;
|
||
- tolerant read와 required semantic validation을 분리;
|
||
- raw problem detail/error body를 application에 노출하지 않음.
|
||
|
||
### 10.3 Application failure shape
|
||
|
||
Application port마다 의미 있는 결과를 선택한다.
|
||
|
||
예:
|
||
|
||
```text
|
||
Found
|
||
NotFound
|
||
RejectedByPartner
|
||
TemporarilyUnavailable
|
||
Indeterminate(operationAttemptId)
|
||
```
|
||
|
||
모든 port가 같은 generic exception을 강제로 사용하지 않는다. Kernel failure를 adapter가
|
||
operation 의미에 맞게 매핑한다.
|
||
|
||
### 10.4 Call budget
|
||
|
||
Use case 전체에 이미 deadline이 있으면 application은 framework-neutral absolute budget을
|
||
전달할 수 있다.
|
||
|
||
```java
|
||
public record CallBudget(long monotonicDeadlineNanos) {
|
||
}
|
||
```
|
||
|
||
실제 contract는 다음을 만족해야 한다.
|
||
|
||
- `System.nanoTime()`과 같은 monotonic time domain;
|
||
- wall-clock timestamp로 serialize하지 않음;
|
||
- destination policy cap과 `min`으로 결합;
|
||
- 이미 만료되면 network side effect 없이 reject;
|
||
- child call이 parent보다 긴 budget을 만들 수 없음.
|
||
|
||
`Duration timeout`만 매번 전달하면 nested call이 호출 시점마다 새 budget을 받아 상위 deadline을
|
||
넘길 수 있으므로 absolute budget을 선호한다.
|
||
|
||
### 10.5 Adapter-consumer SPI와 internal kernel
|
||
|
||
Kernel은 application/controller에 공개하지 않는다. 다만 sample-local outbound adapter나 future
|
||
domain-specific outbound leaf가 capability를 재사용할 수 있도록 최소 adapter-consumer SPI를
|
||
공개한다.
|
||
|
||
개념 예:
|
||
|
||
```java
|
||
interface RegisteredHttpOperationInvoker {
|
||
<Req, Res> HttpCallResult<Res> execute(
|
||
HttpOperation<Req, Res> operation,
|
||
Req request,
|
||
CallContext context);
|
||
}
|
||
```
|
||
|
||
`HttpOperation`은 registry와 fingerprint가 일치하는 immutable descriptor다. Caller는 method,
|
||
absolute URL, raw header, credential, retry boolean을 runtime에 제공하지 않는다.
|
||
|
||
Boundary:
|
||
|
||
- application/domain/inbound package의 SPI import는 ArchUnit으로 금지;
|
||
- adapter/sample outbound package만 사용;
|
||
- operation 등록은 startup 전 완료하고 runtime dynamic registration 금지;
|
||
- descriptor/codec/wire DTO는 consumer adapter 소유 가능;
|
||
- kernel provider/Apache/Spring type은 SPI에 노출하지 않음;
|
||
- public-path snapshot으로 accidental generic SDK surface 확장을 검출.
|
||
|
||
HTTP leaf 안에서 provider와 resilience를 다루는 `HttpTransportEngine`은 계속 internal이다.
|
||
|
||
### 10.6 HTTP Service Interface 사용
|
||
|
||
Spring HTTP Service Interface는 다음 조건에서 adapter-internal wire client로 사용할 수 있다.
|
||
|
||
- interface와 annotation이 adapter package에 위치;
|
||
- feature-specific application port를 별도로 구현;
|
||
- group/destination이 canonical registry와 1:1로 검증;
|
||
- underlying `RestClient`가 동일 kernel의 engine, auth, bounds, observation을 사용;
|
||
- proxy가 kernel의 retry/deadline을 우회하지 않음;
|
||
- method metadata가 operation catalog와 build-time 대조됨;
|
||
- return type이 wire DTO이며 domain/application DTO가 아님.
|
||
|
||
단순 `@ImportHttpServices` classpath scan으로 새 client를 자동 활성화하지 않는다.
|
||
|
||
## 11. Typed operation catalog
|
||
|
||
### 11.1 Catalog가 필요한 이유
|
||
|
||
동일 destination 안에서도 operation마다 안전성이 다르다.
|
||
|
||
```text
|
||
GET /products/{id}
|
||
POST /quotes
|
||
POST /payments/{id}/capture
|
||
GET /exports/{id}/content
|
||
DELETE /sessions/{id}
|
||
```
|
||
|
||
Destination global retry boolean로 이 차이를 표현할 수 없다. Operation catalog는 runtime
|
||
request가 정책을 선택하는 것을 막고 code review 가능한 안전 계약을 제공한다.
|
||
|
||
### 11.2 Operation descriptor
|
||
|
||
개념적 `HttpOperation<Req, Res>` 필드:
|
||
|
||
| 필드 | 의미 |
|
||
| --- | --- |
|
||
| `operationId` | stable low-cardinality ID |
|
||
| `destinationId` | exact destination binding |
|
||
| `policyRevision` | rolling compatibility revision |
|
||
| `method` | fixed HTTP method |
|
||
| `routeTemplate` | low-cardinality relative template |
|
||
| `semantics` | `SAFE_READ`, `IDEMPOTENT_MUTATION`, `KEYED_MUTATION`, `NON_RETRYABLE_MUTATION` |
|
||
| `requestMode` | `NONE`, `BUFFERED`, `REOPENABLE_STREAM`, `SINGLE_USE_STREAM` |
|
||
| `responseMode` | `BODILESS`, `BUFFERED`, `STREAM_CALLBACK` |
|
||
| `requestCodecId` | wire encoder |
|
||
| `responseCodecId` | wire decoder |
|
||
| `successContractId` | accepted status/media/schema |
|
||
| `errorContractId` | operation-specific status mapping |
|
||
| `deadlineProfileId` | phase/total budget profile |
|
||
| `retryProfileId` | retry decision/backoff/budget |
|
||
| `resilienceGroupId` | CB/bulkhead state-sharing group |
|
||
| `authProfileId` | adapter-owned credential profile |
|
||
| `egressPolicyId` | URI/address/redirect/proxy policy |
|
||
| `observabilityProfileId` | route template and privacy policy |
|
||
| `readinessCardIds` | evidence required before R2 |
|
||
|
||
Runtime caller가 이 필드를 override하지 않는다.
|
||
|
||
### 11.3 Executable definition과 review registry
|
||
|
||
다음 이중 구조를 사용한다.
|
||
|
||
1. Adapter Java code의 immutable descriptor가 executable behavior를 소유한다.
|
||
2. `docs/registries/http-operations.yaml`은 operation identity, revision, safety class,
|
||
readiness evidence의 review registry다.
|
||
3. Build test가 registry와 runtime descriptor를 one-to-one 대조하고 canonical fingerprint를
|
||
비교한다.
|
||
4. Java code에만 존재하거나 registry에만 존재하는 operation은 실패한다.
|
||
5. YAML로 class name을 reflection load하거나 arbitrary expression을 실행하지 않는다.
|
||
|
||
Template 자체에는 domain operation을 강제하지 않는다. Schema와 fixture operation만 제공하고
|
||
fork가 실제 operation을 등록한다.
|
||
|
||
Illustrative registry:
|
||
|
||
```yaml
|
||
schema_version: 1
|
||
operations:
|
||
- id: partner-catalog.get-product.v1
|
||
destination: partner-catalog
|
||
policy_revision: 2
|
||
method: GET
|
||
route_template: /v1/products/{productRef}
|
||
semantics: SAFE_READ
|
||
request_mode: NONE
|
||
response_mode: BUFFERED
|
||
retry_profile: safe-read
|
||
resilience_group: partner-catalog-read
|
||
readiness_cards:
|
||
- httpclient-static-buffered
|
||
```
|
||
|
||
### 11.4 Descriptor validation
|
||
|
||
Startup/build-time invariant:
|
||
|
||
- operation/destination ID grammar와 uniqueness;
|
||
- route가 relative이며 scheme/authority/userinfo/fragment 없음;
|
||
- path variable declaration과 mapper가 정확히 일치;
|
||
- query/header key가 allowlist에 존재;
|
||
- `KEYED_MUTATION`이면 idempotency key encoder, stable attempt ID, payload fingerprint,
|
||
provider replay window와 reconciliation operation이 모두 존재;
|
||
- `SINGLE_USE_SOURCE`이면 `maxProtectedPhysicalAttemptsPerLogicalCall=1`;
|
||
- `NON_RETRYABLE_MUTATION`이면 default `maxProtectedPhysicalAttemptsPerLogicalCall=1`, redirect/auth replay 금지, transmission
|
||
evidence-to-receipt mapping 필수;
|
||
- non-retryable pre-send restart를 opt-in하면 `maxProtectedPhysicalAttemptsPerLogicalCall<=2`, buffered/reopenable body,
|
||
exact `NOT_SENT` evidence와 별도 restart policy/budget/scenario 필수;
|
||
- retryable operation이면 body가 absent/buffered/reopenable;
|
||
- streaming response에는 status validator, body cap, cancellation-safe callback contract가 존재;
|
||
- success/error status set이 겹치지 않음;
|
||
- response codec이 accepted media type마다 존재;
|
||
- auth profile과 redirect cross-origin policy가 충돌하지 않음;
|
||
- deadline phase 합이 total을 강제로 결정한다는 잘못된 계산을 하지 않음;
|
||
- resilience group/card가 registry에 존재;
|
||
- readiness card가 operation 요구 feature를 모두 덮음.
|
||
|
||
### 11.5 Configuration override 제한
|
||
|
||
Deployment configuration은 다음을 더 보수적으로 만들 수 있다.
|
||
|
||
- 더 짧은 timeout;
|
||
- 더 작은 body/header limit;
|
||
- 더 적은 retry;
|
||
- 더 낮은 concurrency;
|
||
- redirect disable;
|
||
- HTTP/2에서 HTTP/1.1로 제한;
|
||
- optional dependency를 disabled.
|
||
|
||
다음은 configuration만으로 넓힐 수 없다.
|
||
|
||
- method 또는 route;
|
||
- safe/idempotent classification;
|
||
- accepted destination/redirect host;
|
||
- retryable status/exception;
|
||
- body replayability;
|
||
- auth header 종류;
|
||
- media type;
|
||
- private CIDR access;
|
||
- unknown outcome을 success로 변경.
|
||
|
||
안전성을 넓히는 변경은 code, registry revision, tests와 review가 필요하다.
|
||
|
||
## 12. Request target와 URI construction
|
||
|
||
### 12.1 Base URI
|
||
|
||
Destination base URI는 startup에 parse/normalize한다.
|
||
|
||
Required:
|
||
|
||
- absolute URI;
|
||
- default `https`;
|
||
- exact lowercase ASCII/IDNA host;
|
||
- explicit 또는 scheme default port;
|
||
- optional fixed base path;
|
||
- empty userinfo;
|
||
- empty query/fragment;
|
||
- no ambiguous backslash/control/whitespace;
|
||
- normalized dot segment 없음;
|
||
- allowed scheme/host/port policy와 일치.
|
||
|
||
Host 비교는 display Unicode가 아니라 canonical ASCII form을 사용한다. IP literal은 canonical
|
||
binary address로 비교한다. IPv4-in-IPv6 mapped address와 zone ID도 명시적으로 처리한다.
|
||
|
||
### 12.2 Relative route only
|
||
|
||
Normal operation은 catalog의 relative route template만 사용한다.
|
||
|
||
거부:
|
||
|
||
```text
|
||
https://other.example/path
|
||
//other.example/path
|
||
file:///etc/passwd
|
||
gopher://...
|
||
data:...
|
||
../admin
|
||
%2e%2e/admin
|
||
\other
|
||
```
|
||
|
||
Base URI resolution 전에 raw/decoded 두 표현의 ambiguity를 검사한다. Decode 후 다시 decode하는
|
||
double-encoding을 허용하지 않는다.
|
||
|
||
### 12.3 Path variable
|
||
|
||
각 variable은 한 segment value다.
|
||
|
||
- URI component encoder로 정확히 한 번 encode;
|
||
- `/`, `\`, NUL, control, dot segment 거부;
|
||
- 길이와 character profile 제한;
|
||
- Unicode normalization policy 고정;
|
||
- pre-encoded input 금지;
|
||
- empty 허용 여부를 operation이 선언;
|
||
- path remainder/wildcard는 별도 typed value와 stricter test 없이는 금지.
|
||
|
||
String concatenation으로 URI를 만들지 않는다.
|
||
|
||
### 12.4 Query
|
||
|
||
Operation이 key와 multiplicity를 선언한다.
|
||
|
||
- key는 caller가 선택하지 않음;
|
||
- value는 component encoding;
|
||
- list ordering/canonicalization 명시;
|
||
- duplicate 허용 여부;
|
||
- blank/null/absent 차이;
|
||
- page size/range/total query length bound;
|
||
- secret/token/PII query parameter 금지;
|
||
- signature provider가 요구하면 canonical order와 exact encoding golden test.
|
||
|
||
Query는 log, metric tag, span name에 기록하지 않는다.
|
||
|
||
### 12.5 Dynamic target exception
|
||
|
||
다음은 일반 URI seam으로 허용하지 않는다.
|
||
|
||
- user supplied webhook URL;
|
||
- arbitrary avatar/document fetch;
|
||
- upstream이 응답한 presigned URL;
|
||
- pagination `next` absolute link.
|
||
|
||
필요하면 별도 operation type을 만든다.
|
||
|
||
- trusted issuer/source 검증;
|
||
- allowed scheme/host suffix가 아닌 exact policy;
|
||
- resolved IP validation;
|
||
- redirect 재검증;
|
||
- credential 제거;
|
||
- size/type/scan;
|
||
- egress proxy;
|
||
- bounded lifetime;
|
||
- no internal address.
|
||
|
||
`next` link는 가능한 경우 opaque cursor만 추출해 known route를 재구성한다.
|
||
|
||
## 13. Header, cookie와 metadata policy
|
||
|
||
### 13.1 Header ownership
|
||
|
||
세 그룹으로 나눈다.
|
||
|
||
1. Engine-owned:
|
||
- `Host`/`:authority`;
|
||
- `Content-Length`, `Transfer-Encoding`;
|
||
- connection/protocol headers;
|
||
- proxy authorization.
|
||
2. Infrastructure-owned:
|
||
- `Authorization`/API key;
|
||
- trace context;
|
||
- sanitized `User-Agent`;
|
||
- `Idempotency-Key`;
|
||
- conditional/version headers declared by operation.
|
||
3. Operation-owned typed business metadata:
|
||
- fixed `Accept`, `Content-Type`;
|
||
- provider-documented correlation/reference;
|
||
- bounded locale or version enum.
|
||
|
||
Application caller에게 arbitrary `Map<String,String>`을 주지 않는다.
|
||
|
||
### 13.2 Forbidden forwarding
|
||
|
||
Inbound에서 자동 forward 금지:
|
||
|
||
```text
|
||
Authorization
|
||
Proxy-Authorization
|
||
Cookie
|
||
Set-Cookie
|
||
X-Forwarded-*
|
||
Forwarded
|
||
Host
|
||
Content-Length
|
||
Transfer-Encoding
|
||
Connection
|
||
Upgrade
|
||
TE
|
||
Trailer
|
||
Keep-Alive
|
||
```
|
||
|
||
사용자 bearer token의 on-behalf-of 전달이 비즈니스 요구라면 별도 credential exchange/delegation
|
||
profile을 사용한다. Raw inbound token pass-through를 default로 하지 않는다.
|
||
|
||
### 13.3 Header limits
|
||
|
||
Request와 response 모두:
|
||
|
||
- max field count;
|
||
- max single name/value bytes;
|
||
- max aggregate bytes;
|
||
- duplicate singleton rejection;
|
||
- invalid control/obs-fold rejection;
|
||
- allowlisted captured response headers;
|
||
- trailer allowlist와 aggregate cap;
|
||
- header casing에 의미를 두지 않음.
|
||
|
||
Underlying engine/JVM global header limit만 믿지 않고 destination policy와 test를 둔다.
|
||
|
||
### 13.4 Cookies
|
||
|
||
Machine-to-machine default:
|
||
|
||
- cookie store disabled;
|
||
- `Set-Cookie` 저장/재전송 안 함;
|
||
- caller cookie 금지.
|
||
|
||
Cookie-required partner가 있으면:
|
||
|
||
- destination-exclusive bounded store;
|
||
- domain/path/Secure/SameSite policy;
|
||
- max cookies/bytes/TTL;
|
||
- tenant 간 공유 금지;
|
||
- restart persistence 여부;
|
||
- secret classification;
|
||
- 별도 readiness card.
|
||
|
||
Browser session emulation은 baseline이 아니다.
|
||
|
||
### 13.5 Baggage와 correlation
|
||
|
||
- OTel propagator가 `traceparent`와 `tracestate` 소유;
|
||
- `baggage`는 default empty;
|
||
- destination별 key allowlist;
|
||
- external partner에는 tenant/user/business ID default 금지;
|
||
- `X-Request-Id`/`X-Correlation-Id`도 partner contract에 있을 때만 전파;
|
||
- inbound value는 syntax/length 검증 후 사용;
|
||
- caller-provided trace header override 금지.
|
||
|
||
## 14. Response contract와 failure taxonomy
|
||
|
||
### 14.1 Kernel result
|
||
|
||
Kernel은 raw exception 대신 내부적으로 다음 결과를 만든다.
|
||
|
||
```text
|
||
Completed<T>
|
||
Rejected(HttpFailure)
|
||
Indeterminate(HttpMutationUncertainty)
|
||
Cancelled(HttpCancellation)
|
||
```
|
||
|
||
Adapter가 이를 application-specific result/exception으로 변환한다.
|
||
|
||
### 14.2 Completed
|
||
|
||
`Completed<T>`는 단순 2xx가 아니다.
|
||
|
||
모두 만족해야 한다.
|
||
|
||
- operation이 success로 선언한 status;
|
||
- framing complete;
|
||
- body size 안;
|
||
- accepted content type/encoding/charset;
|
||
- decode 성공;
|
||
- required wire field와 semantic invariant 성공;
|
||
- stream callback 정상 종료;
|
||
- response close 성공 또는 close failure가 결과 신뢰도에 영향 없음을 증명.
|
||
|
||
Status 204/304/HEAD처럼 body가 없어야 하는 response에 unexpected body가 있으면 engine/framing
|
||
policy에 따라 discard cap 안에서 닫고 protocol drift를 기록한다.
|
||
|
||
### 14.3 Failure stages
|
||
|
||
가능하면 engine은 다음 stage evidence를 제공한다.
|
||
|
||
```text
|
||
ADMISSION
|
||
POOL_ACQUIRE
|
||
DNS_RESOLUTION
|
||
CONNECT
|
||
TLS_HANDSHAKE
|
||
REQUEST_HEADERS
|
||
REQUEST_BODY
|
||
RESPONSE_HEADERS
|
||
RESPONSE_BODY
|
||
DECODE
|
||
CALLBACK
|
||
```
|
||
|
||
Transmission progress와 observation confidence를 분리한다.
|
||
|
||
```text
|
||
TransmissionProgress =
|
||
NOT_SENT | MAYBE_SENT | SENT | RESPONSE_STARTED | RESPONSE_COMPLETE
|
||
ObservationConfidence =
|
||
OBSERVED | UNKNOWN_AFTER_HANDOFF
|
||
ProjectedTransmissionEvidence =
|
||
progress when sufficient OBSERVED evidence exists, otherwise UNKNOWN
|
||
```
|
||
|
||
정상 provider의 tracker는 progress에 다음 단조 전이만 허용한다.
|
||
|
||
```text
|
||
NOT_SENT
|
||
-- immediately before the first possible origin-request byte/frame write --> MAYBE_SENT
|
||
-- local request headers/body write completed ----------------------------> SENT
|
||
-- first valid response headers observed ---------------------------------> RESPONSE_STARTED
|
||
-- required response framing/body contract completed ---------------------> RESPONSE_COMPLETE
|
||
```
|
||
|
||
첫 write는 HTTP/1.1 request line/header byte, HTTP/2 HEADERS frame 제출처럼 upstream이 request를
|
||
관측할 수 있는 가장 이른 지점이다. Provider는 그 지점 전에 synchronous tracker callback을
|
||
호출해야 한다. Engine ownership transfer 뒤 이 callback을 보장하지 못하거나 start/cancel race의
|
||
증거를 회수하지 못하면 progress의 마지막 observed lower bound는 유지하고 confidence만
|
||
`UNKNOWN_AFTER_HANDOFF`로 단조 전이한다. Progress는 어느 상태도 downgrade하지 않고 cleanup/cancel이
|
||
이를 `NOT_SENT`로 되돌리지 않는다.
|
||
|
||
한 physical attempt의 authoritative response/failure event와 cancellation은 atomic terminal CAS로
|
||
정확히 하나만 승리한다. Response winner가 retry/auth/redirect control disposition을 만들 수 있으므로
|
||
항상 logical call terminal을 뜻하지는 않는다. Valid completed/rejection response가 먼저 확정되면
|
||
뒤늦은 cancel이 결과를 덮지 않는다. 반대로 mutation이 cancel/deadline 시점에 `MAYBE_SENT` 이상
|
||
또는 `UNKNOWN`이고 authoritative terminal result가 없으면 반드시 `INDETERMINATE`다. Exact
|
||
`NOT_SENT`일 때만 `Cancelled` 또는 reviewed pre-send restart가 가능하다.
|
||
|
||
`UNKNOWN_AFTER_HANDOFF`는 progress 순서의 마지막 값이 아니다. 이후 authoritative response
|
||
headers/body completion을 직접 관측하면 progress를 `RESPONSE_STARTED|RESPONSE_COMPLETE`로 전진시키고
|
||
그 이후 사실의 confidence를 `OBSERVED`로 기록할 수 있다. 다만 response가 없다는 이유로 unknown
|
||
request transmission을 `NOT_SENT|SENT`로 추론하지 않는다. Valid terminal response가 있으면 §16의
|
||
response semantic outcome이 우선하고, 없으면 projected evidence는 계속 `UNKNOWN`이다.
|
||
|
||
Provider가 정확히 관측하지 못하면 더 강한 값으로 추측하지 않고 `UNKNOWN`을 사용한다. 이
|
||
linearization callback과 race test를 제공하지 못하는 engine은 non-retryable mutation card를
|
||
통과하지 못한다.
|
||
|
||
### 14.4 Stable failure classes
|
||
|
||
| Failure | 의미 | Generic retry default |
|
||
| --- | --- | --- |
|
||
| `CALL_BUDGET_EXHAUSTED` | network 전 또는 중 total deadline 만료 | operation policy |
|
||
| `ADMISSION_REJECTED` | logical call queue/budget 초과 | false |
|
||
| `POOL_ACQUIRE_TIMEOUT` | connection/stream lease 대기 만료 | safe/replayable만 |
|
||
| `LOCAL_RATE_LIMITED` | local egress quota 거부 | Retry-After와 budget에 따름 |
|
||
| `CIRCUIT_OPEN` | resilience group open | false inside same call |
|
||
| `DNS_FAILED` | name resolution 실패 | safe/replayable + bounded |
|
||
| `DESTINATION_ADDRESS_REJECTED` | IP/SSRF policy 위반 | false, security alert |
|
||
| `CONNECT_TIMEOUT` | socket connect timeout | safe/replayable |
|
||
| `CONNECT_REFUSED` | connection 거부 | safe/replayable |
|
||
| `TLS_TRUST_FAILED` | trust/chain/revocation | false |
|
||
| `TLS_HOSTNAME_FAILED` | hostname mismatch | false |
|
||
| `TLS_HANDSHAKE_TIMEOUT` | handshake deadline | safe/replayable if not sent |
|
||
| `PROXY_FAILED` | proxy connect/auth/protocol | policy-specific |
|
||
| `REQUEST_WRITE_TIMEOUT` | request body write 제한 | mutation may be indeterminate |
|
||
| `RESPONSE_HEADER_TIMEOUT` | headers 대기 제한 | operation/transmission-specific |
|
||
| `RESPONSE_IDLE_TIMEOUT` | body progress 없음 | safe read may retry from start |
|
||
| `RESPONSE_TRUNCATED` | framing/body incomplete | safe/replayable; mutation usually indeterminate |
|
||
| `RESPONSE_TOO_LARGE` | wire/decoded cap 초과 | false |
|
||
| `UNSUPPORTED_MEDIA_TYPE` | content contract drift | false |
|
||
| `UNSUPPORTED_CONTENT_ENCODING` | encoding contract drift | false |
|
||
| `DECODE_FAILED` | malformed/schema mismatch | false |
|
||
| `UPSTREAM_STATUS` | operation-mapped status | operation-specific |
|
||
| `PROTOCOL_VIOLATION` | malformed framing/header/version | false by default |
|
||
| `AUTH_MATERIAL_UNAVAILABLE` | local secret/token failure | bounded refresh policy |
|
||
| `CANCELLED_BY_CALLER` | caller cancellation | false |
|
||
| `SHUTTING_DOWN` | lifecycle reject/cancel | false |
|
||
| `CALLBACK_FAILED` | application adapter stream consumer failure | false |
|
||
| `INTERNAL_CLIENT_DEFECT` | invariant/programming failure | false, alert |
|
||
|
||
### 14.5 HTTP status default matrix
|
||
|
||
Operation mapping이 우선하며 generic default는 보수적이다.
|
||
|
||
| Status | Default |
|
||
| --- | --- |
|
||
| 200~299 | declared success set에 있을 때만 success |
|
||
| 300~399 | redirect disabled면 explicit upstream status failure |
|
||
| 400 | permanent request contract failure |
|
||
| 401 | credential profile이 허용하면 one refresh path, 일반 retry 아님 |
|
||
| 403 | permanent authz/config failure |
|
||
| 404 | operation이 absence로 선언한 경우만 domain absence |
|
||
| 408 | safe/replayable operation에서 bounded retry candidate |
|
||
| 409 | operation-specific conflict/in-flight/idempotency mapping |
|
||
| 410 | operation-specific terminal absence |
|
||
| 412 | precondition/domain concurrency outcome |
|
||
| 413 | request size/contract failure |
|
||
| 415/406 | media negotiation/config drift |
|
||
| 422 | application rejection 또는 idempotency fingerprint mismatch |
|
||
| 425 | replay-safe operation만 retry candidate |
|
||
| 429 | provider quota outcome; valid `Retry-After`와 budget 필요 |
|
||
| 500 | default no retry, operation opt-in 가능 |
|
||
| 501/505 | permanent capability/protocol mismatch |
|
||
| 502/503/504 | safe/replayable bounded retry candidate |
|
||
|
||
Status code만으로 `retryable=true`를 application 외부 error envelope에 그대로 전달하지 않는다.
|
||
Retry가 안전한지는 현재 operation과 body/attempt evidence에 따라 달라진다.
|
||
|
||
### 14.6 Error body
|
||
|
||
Default는 discard-and-close다. Operation이 structured error mapping을 요구할 때만:
|
||
|
||
- 별도 작은 `maxErrorBodyBytes`;
|
||
- accepted content type;
|
||
- bounded decoder;
|
||
- field allowlist;
|
||
- message/log 미노출;
|
||
- application-safe code로 mapping;
|
||
- malformed error body는 original status를 보존한 `ERROR_BODY_DECODE_FAILED` evidence.
|
||
|
||
### 14.7 `Throwable` 금지
|
||
|
||
Infrastructure 경계가 모든 `Throwable`을 dependency failure로 바꾸지 않는다.
|
||
|
||
- `VirtualMachineError`, `LinkageError`, `ThreadDeath` 등은 통과;
|
||
- `InterruptedException`은 interrupt flag 복구 후 cancellation/shutdown으로 분류;
|
||
- `CancellationException`은 별도;
|
||
- adapter programmer exception은 `INTERNAL_CLIENT_DEFECT`;
|
||
- engine/network exception만 taxonomy mapping;
|
||
- Error mapper 자체 failure가 original failure를 덮지 않음.
|
||
|
||
## 15. Total deadline와 active cancellation
|
||
|
||
### 15.1 Deadline 정의
|
||
|
||
Effective deadline:
|
||
|
||
```text
|
||
min(
|
||
inherited application deadline,
|
||
operation total-deadline cap,
|
||
destination maximum call duration
|
||
)
|
||
```
|
||
|
||
Elapsed time은 monotonic clock으로 계산한다.
|
||
|
||
```text
|
||
remaining = deadlineNanos - monotonicNowNanos
|
||
```
|
||
|
||
`Instant.now()`는 NTP/clock adjustment 영향을 받으므로 elapsed control에 사용하지 않는다.
|
||
`Retry-After` HTTP date 해석에만 wall clock을 사용하고 최종 sleep은 monotonic remaining으로
|
||
제한한다.
|
||
|
||
### 15.2 포함 범위
|
||
|
||
Total deadline에는 모두 포함된다.
|
||
|
||
```text
|
||
logical admission wait
|
||
+ credential acquisition/refresh wait
|
||
+ request body encode/open/spool wait
|
||
+ local outbound quota wait
|
||
+ physical-attempt bulkhead wait
|
||
+ circuit permission acquisition
|
||
+ DNS
|
||
+ connection/HTTP2 stream lease
|
||
+ connect
|
||
+ TLS
|
||
+ request write
|
||
+ response headers
|
||
+ response body/callback
|
||
+ retry decision
|
||
+ Retry-After/backoff
|
||
+ every physical attempt
|
||
```
|
||
|
||
### 15.3 Phase cap
|
||
|
||
Caller-visible absolute deadline을 `D`라 하고 positive finite `cleanupReserve`를 둔다.
|
||
|
||
```text
|
||
executionCutoff = D - cleanupReserve
|
||
remainingExecution = executionCutoff - monotonicNow
|
||
remainingReturn = D - monotonicNow
|
||
```
|
||
|
||
Normal admission, backoff와 새 attempt는 `executionCutoff`까지만 허용한다. 각 실행 phase는:
|
||
|
||
```text
|
||
effectivePhaseTimeout = min(configuredPhaseCap, remainingExecution)
|
||
```
|
||
|
||
을 사용한다.
|
||
|
||
필수 cap:
|
||
|
||
- admission acquire;
|
||
- credential acquire/refresh;
|
||
- body encode/open/spool;
|
||
- local quota acquire;
|
||
- physical bulkhead acquire;
|
||
- circuit permission acquire;
|
||
- pool acquire;
|
||
- DNS;
|
||
- connect;
|
||
- TLS handshake;
|
||
- request write/idle;
|
||
- response header;
|
||
- response body idle;
|
||
- total body/callback;
|
||
|
||
Phase cap의 합을 total deadline으로 오해하지 않는다. 실제 phase는 순차/중첩되고 retry가 있으므로
|
||
total은 별도 상한이다.
|
||
|
||
Cleanup은 일반 phase가 아니라 reserve를 사용한다.
|
||
|
||
```text
|
||
synchronousCleanupBudget = max(0, min(cleanupReserve, remainingReturn))
|
||
```
|
||
|
||
`D <= now + cleanupReserve`이면 새 network side effect를 시작하지 않는다. Scheduler tolerance를
|
||
cleanup budget으로 사용하지 않는다.
|
||
|
||
### 15.4 Attempt 시작 조건
|
||
|
||
모든 physical attempt의 공통 조건:
|
||
|
||
- remaining > `minimumAttemptBudget`;
|
||
- execution cutoff 전이며 cleanup reserve가 보존됨;
|
||
- 필요한 admission/connection acquisition 뒤에도 meaningful budget;
|
||
- logical call not cancelled;
|
||
- protected physical ceiling과 shared root-call total capacity가 남음;
|
||
- shutdown state가 이 call lease를 허용.
|
||
|
||
최초 `attempt=0`은 retry가 아니다.
|
||
|
||
- retry budget token을 요구하거나 소비하지 않음;
|
||
- operation/body가 replayable일 필요 없음;
|
||
- `SINGLE_USE_SOURCE`와 `NON_RETRYABLE_MUTATION`도 실행 가능;
|
||
- validation/encoding에서 network side effect 전 거절될 수 있음.
|
||
|
||
`attempt>0`은 공통 조건에 더해 다음을 모두 만족한다.
|
||
|
||
- previous `AttemptDisposition`이 정확히 후속 attempt를 허용;
|
||
- body가 동일 bytes/intent로 reopen 가능;
|
||
- applicable retry/pre-send-restart/auth-replay/operation-specific replay budget의 순수 availability 확인;
|
||
- backoff/`Retry-After` 뒤에도 meaningful budget;
|
||
- `physicalAttemptOrdinal < maxProtectedPhysicalAttemptsPerLogicalCall`이고 shared root-call total
|
||
token이 남음;
|
||
- identity-preserving replay라면 key/fingerprint/scope 동일.
|
||
|
||
`NON_RETRYABLE_MUTATION`은 기본 `maxProtectedPhysicalAttemptsPerLogicalCall=1`이다. 별도
|
||
reviewed pre-send restart policy가
|
||
`RESTART_CONFIRMED_NOT_SENT`만 허용하고 body가 buffered/reopenable일 때 한 번의 attempt를
|
||
추가로 허용할 수 있다. 이는 mutation replay/retry가 아니며 별도 restart budget/metric을
|
||
사용한다. `MAYBE_SENT` 이상은 후속 mutation attempt를 절대 시작하지 않는다.
|
||
|
||
Deadline 직전에 성공 가능성이 없는 attempt를 시작하지 않는다.
|
||
|
||
### 15.5 Active cancellation 구현
|
||
|
||
Apache classic R2 provider는 blocking `RestClient` call을 adapter-owned virtual-thread task로
|
||
실행할 수 있다.
|
||
|
||
```text
|
||
caller
|
||
-> submit virtual-thread attempt task
|
||
-> wait only until execution cutoff
|
||
-> cutoff/cancel:
|
||
Future.cancel(true)
|
||
cancel request execution
|
||
close response/entity stream
|
||
hard-cancel connection when required
|
||
await cleanup only until caller-visible deadline D
|
||
if still active at D:
|
||
return to caller
|
||
quarantine connection/generation
|
||
hand off to bounded orphan reaper
|
||
```
|
||
|
||
구현 시 검증할 사항:
|
||
|
||
- interrupt가 Apache request cancellation으로 실제 연결;
|
||
- `hardCancellationEnabled`의 connection 폐기 semantics;
|
||
- cancelled connection이 pool로 정상 반환되지 않음;
|
||
- stream callback 중 network read가 해제됨;
|
||
- executor shutdown이 in-flight task를 유실하지 않음;
|
||
- virtual-thread task 수가 logical admission으로 bounded;
|
||
- cancellation race에서 response가 정확히 한 번 close.
|
||
|
||
Thread interrupt flag만 세우고 active cancellation이라 주장하지 않는다.
|
||
|
||
### 15.6 Callback 한계
|
||
|
||
Streaming callback이 무한 CPU loop를 돌거나 interrupt를 무시하면 transport close만으로 callback
|
||
종료를 보장할 수 없다.
|
||
|
||
계약:
|
||
|
||
- callback은 blocking read/write interrupt와 cancellation token을 존중;
|
||
- adapter는 network stream을 close;
|
||
- callback에 checkpoint/cancellation view 제공 가능;
|
||
- non-cooperative callback hard kill은 보장하지 않음;
|
||
- test가 cooperative/non-cooperative behavior와 shutdown impact를 구분.
|
||
|
||
### 15.7 Cancellation outcome
|
||
|
||
Cancellation 원인을 구분한다.
|
||
|
||
```text
|
||
PARENT_DEADLINE
|
||
OPERATION_DEADLINE
|
||
CALLER_CANCELLED
|
||
SHUTDOWN_DRAIN_EXPIRED
|
||
HEDGE_LOSER
|
||
```
|
||
|
||
OTel convention상 의도된 caller cancellation은 자동으로 dependency error로 기록하지 않는다.
|
||
그러나 mutation이 이미 전송됐으면 application outcome은 cancellation보다
|
||
`INDETERMINATE`가 우선할 수 있다.
|
||
|
||
### 15.8 Cleanup reserve와 orphan reaper
|
||
|
||
Normal path는 execution cutoff에서 cancellation을 시작해 `D` 전에 response close, connection
|
||
discard, circuit/bulkhead permit 정리를 끝내는 것을 목표로 한다.
|
||
|
||
`D`까지 cleanup이 끝나지 않으면 caller latency를 더 늘리지 않는다.
|
||
|
||
- incomplete connection/stream은 reusable pool로 반환 금지;
|
||
- engine generation을 quarantine;
|
||
- physical-attempt permit는 실제 task termination까지 reaper가 보유해 capacity를 과다 판매하지
|
||
않음;
|
||
- logical call은 caller 반환 뒤 release하되 orphan count가 새 admission capacity에 반영됨;
|
||
- orphan registry와 reaper worker/queue/deadline은 finite;
|
||
- `orphanCleanupTimeout` 뒤에도 종료되지 않으면 generation을 `DEGRADED/NOT_READY`로 만들고 새
|
||
call을 받지 않음;
|
||
- Java task를 강제 kill했다고 주장하지 않음;
|
||
- provider close/rollover/runbook escalation;
|
||
- synchronous logical latency SLO와 asynchronous cleanup SLO를 별도 metric으로 기록.
|
||
|
||
따라서 보장은 다음처럼 구분한다.
|
||
|
||
```text
|
||
caller return <= D + scheduler tolerance
|
||
normal cleanup target <= D
|
||
quarantined cleanup <= orphanCleanupTimeout
|
||
no quarantined resource reuse at any time
|
||
```
|
||
|
||
Parent cancellation이 이미 `D`를 지나 도착하면 synchronous cleanup budget은 0이며 즉시
|
||
quarantine/reaper 경로를 사용한다.
|
||
|
||
## 16. Replayability, idempotency와 unknown outcome
|
||
|
||
### 16.1 세 개념을 분리한다
|
||
|
||
- HTTP method idempotency: 같은 intended effect를 반복해도 추가 effect가 없음.
|
||
- Body replayability: client가 동일 bytes/semantics를 다시 전송할 수 있음.
|
||
- Operation deduplication: upstream이 stable key/fingerprint로 같은 mutation을 식별하고 이전
|
||
결과를 replay함.
|
||
|
||
하나가 다른 둘을 암시하지 않는다.
|
||
|
||
### 16.2 Request body mode
|
||
|
||
```text
|
||
NONE
|
||
BUFFERED_IMMUTABLE
|
||
REOPENABLE_SOURCE
|
||
SINGLE_USE_SOURCE
|
||
```
|
||
|
||
`BUFFERED_IMMUTABLE`:
|
||
|
||
- size cap 안;
|
||
- canonical encoding 후 bytes/digest 고정;
|
||
- attempt마다 새 publisher/stream.
|
||
|
||
`REOPENABLE_SOURCE`:
|
||
|
||
- `open()`마다 처음부터 같은 content;
|
||
- stable length/checksum 또는 canonical fingerprint;
|
||
- concurrent open 허용 여부;
|
||
- 실패 시 close;
|
||
- source revision drift 검증.
|
||
|
||
`SINGLE_USE_SOURCE`:
|
||
|
||
- `maxProtectedPhysicalAttemptsPerLogicalCall=1`;
|
||
- redirect/auth replay 금지;
|
||
- response loss는 transmission evidence에 따라 indeterminate.
|
||
|
||
### 16.3 Operation semantics
|
||
|
||
```text
|
||
SAFE_READ
|
||
IDEMPOTENT_MUTATION
|
||
KEYED_MUTATION
|
||
NON_RETRYABLE_MUTATION
|
||
```
|
||
|
||
`GET`이라고 자동으로 `SAFE_READ`가 되지 않고 catalog review가 필요하다. `PUT`/`DELETE`도
|
||
provider 문서와 precondition을 검증한다.
|
||
|
||
### 16.4 Keyed mutation
|
||
|
||
`KEYED_MUTATION` 요구:
|
||
|
||
- upstream 문서화된 key contract;
|
||
- key format/entropy/length;
|
||
- uniqueness scope;
|
||
- retention/replay window;
|
||
- same key + different payload 처리;
|
||
- concurrent same-key 처리;
|
||
- success/error replay semantics;
|
||
- inspection/reconciliation endpoint;
|
||
- client-side stable operation attempt ID;
|
||
- canonical payload fingerprint;
|
||
- credential/tenant scope;
|
||
- real failure tests.
|
||
|
||
2026-07 기준 IETF Idempotency-Key 문서는 만료된 Internet-Draft이며 표준 RFC로 취급하지 않는다.
|
||
Provider가 실제 지원하는 header와 semantics를 contract로 검증한다.
|
||
|
||
### 16.5 Unknown mutation state
|
||
|
||
다음 상황은 operation이 적용됐을 가능성이 있다.
|
||
|
||
- request body 일부/전체 전송 후 connection reset;
|
||
- response header timeout;
|
||
- response body truncate;
|
||
- caller/shutdown cancellation after send;
|
||
- proxy/gateway가 upstream response를 잃음;
|
||
- success response decode 실패.
|
||
|
||
결과:
|
||
|
||
```text
|
||
Indeterminate(
|
||
operationAttemptId,
|
||
destinationId,
|
||
operationId,
|
||
requestFingerprint,
|
||
lastAttempt,
|
||
transmissionEvidence,
|
||
reconciliationHint
|
||
)
|
||
```
|
||
|
||
Raw URL, credential, payload는 receipt에 넣지 않는다.
|
||
|
||
### 16.6 Exhaustive attempt disposition
|
||
|
||
Kernel은 `mutation outcome unresolved` 같은 loose boolean을 사용하지 않는다.
|
||
|
||
입력:
|
||
|
||
```text
|
||
operationSemantics
|
||
transmissionEvidence
|
||
processingEvidence
|
||
cancellationOutcome
|
||
responseIntegrity
|
||
responseSemanticClass
|
||
bodyReplayability
|
||
reconciliationContract
|
||
credentialGeneration
|
||
authChallengeDecision
|
||
authReplayCount
|
||
remainingBudget
|
||
```
|
||
|
||
출력은 정확히 하나다.
|
||
|
||
```text
|
||
RETURN_COMPLETED
|
||
RETURN_DECLARED_REJECTION
|
||
RETURN_CANCELLED(reason)
|
||
RESTART_CONFIRMED_NOT_SENT
|
||
RESTART_CONFIRMED_NOT_PROCESSED
|
||
RETRY_SAFE_READ
|
||
REPLAY_SAME_INTENT
|
||
REFRESH_CREDENTIAL_AND_REPLAY
|
||
FOLLOW_DECLARED_REDIRECT
|
||
RECONCILE_SAME_OPERATION
|
||
RETURN_INDETERMINATE
|
||
RETURN_PERMANENT_FAILURE
|
||
```
|
||
|
||
응답의 byte/framing 무결성과 operation 의미를 한 enum에 섞지 않는다.
|
||
|
||
```text
|
||
ResponseIntegrity =
|
||
VALID_COMPLETE
|
||
| VALID_HEADERS_ONLY
|
||
| NONE_OR_INVALID
|
||
|
||
ResponseSemanticClass =
|
||
COMPLETED
|
||
| DOMAIN_REJECTION
|
||
| RETRY_CONTROL
|
||
| STALE_CREDENTIAL_CHALLENGE
|
||
| DECLARED_REDIRECT
|
||
| RECONCILIATION_SIGNAL
|
||
| UNKNOWN
|
||
```
|
||
|
||
`VALID_HEADERS_ONLY`는 catalog가 status/header만으로 해당 control outcome을 authoritative하게
|
||
판단할 수 있을 때만 사용한다. Success가 required body를 요구하는데 truncate/decode/schema
|
||
failure가 발생하면 `NONE_OR_INVALID`이며 status가 2xx라는 이유로 `COMPLETED`가 되지 않는다.
|
||
반대로 bounded error body decode가 실패해도 catalog가 429 status와 valid `Retry-After`만으로
|
||
retry control을 선언했다면 원래 status를 보존할 수 있다.
|
||
|
||
Terminal-result/cancellation CAS에서 cancellation이 먼저 이겼다면 response table보다 먼저
|
||
분기한다. `SAFE_READ` 또는 exact `NOT_SENT` operation은 `RETURN_CANCELLED(reason)`이며 reason은
|
||
caller/deadline/shutdown을 보존한다. Mutation이 `MAYBE_SENT` 이상 또는 `UNKNOWN`이면 cancellation을
|
||
permanent failure로 축소하지 않고 `RETURN_INDETERMINATE`다. Authoritative response가 먼저 CAS를
|
||
이겼다면 뒤늦은 cancellation은 아래 semantic result를 덮지 않는다.
|
||
|
||
CAS 시점은 required integrity에 따라 다르다. Exact `VALID_HEADERS_ONLY` response outcome은
|
||
status/header/framing 검증 직후 response winner를 시도하고, body-required outcome은 bounded
|
||
body/decode/semantic 검증이 끝난 직후 시도한다. Header-authoritative winner 뒤의 body
|
||
drain/discard는 cleanup일 뿐 outcome 확정을 늦추지 않는다. Response body task, provider failure와
|
||
cancellation callback은 §20의 단일 `AttemptTerminalCoordinator`에 event를 제출하며 winner와 cleanup
|
||
owner를 각각 정확히 하나만 선출한다.
|
||
|
||
Authoritative completed/rejection response가 없을 때는 `processingEvidence`를 semantic
|
||
`UNKNOWN`/transmission fallback보다 먼저 평가한다.
|
||
|
||
| Provider processing evidence | Required contract | Disposition |
|
||
| --- | --- | --- |
|
||
| `CONFIRMED_NOT_PROCESSED` | exact RFC/provider evidence + operation H2 opt-in + replayable body + same identity + finite protocol-restart/protected/root budget | `RESTART_CONFIRMED_NOT_PROCESSED` |
|
||
| `MAYBE_PROCESSED` 또는 `UNKNOWN` | mutation | processing evidence로 restart 금지; semantic/fallback matrix 계속 평가 |
|
||
|
||
따라서 request progress가 `SENT`인 mutation이라도 authoritative `CONFIRMED_NOT_PROCESSED`가 있으면
|
||
아래 generic transmission fallback이 먼저 `RETURN_INDETERMINATE`로 종결하지 않는다. 반대로 valid
|
||
completed/domain-rejection response가 있으면 processing hint가 그 authoritative result를 덮지 않는다.
|
||
|
||
그 다음 response semantic disposition precedence는 다음과 같다.
|
||
|
||
| Semantic class | Required integrity/contract | Disposition |
|
||
| --- | --- | --- |
|
||
| `COMPLETED` | operation success contract의 required integrity 충족 | `RETURN_COMPLETED` |
|
||
| `DOMAIN_REJECTION` | operation이 status/header/body 중 요구한 rejection evidence 충족 | `RETURN_DECLARED_REJECTION` |
|
||
| `RETRY_CONTROL` | exact status profile + operation/body/transmission/budget gate | safe read는 `RETRY_SAFE_READ`; mutation은 authoritative same-intent/reconciliation 계약이 있을 때만 해당 state, 그 외 indeterminate/permanent |
|
||
| `STALE_CREDENTIAL_CHALLENGE` | exact configured challenge + §17.5 gate | `REFRESH_CREDENTIAL_AND_REPLAY`, 아니면 terminal auth rejection |
|
||
| `DECLARED_REDIRECT` | future redirect card + §24.3 hop/body/origin gate | `FOLLOW_DECLARED_REDIRECT`; current cards에서는 terminal reject |
|
||
| `RECONCILIATION_SIGNAL` | provider-specific inspect/reconcile contract | `RECONCILE_SAME_OPERATION` 또는 authoritative terminal result |
|
||
| `UNKNOWN` 또는 required integrity 미충족 | 아래 fallback matrix | transmission evidence에 따른 disposition |
|
||
|
||
임의의 401, 403, malformed challenge 또는 provider가 선언하지 않은 error body를 refresh 신호로
|
||
추측하지 않는다. 408/425/429/5xx가 framing-complete response라는 이유만으로 terminal rejection이
|
||
되는 것도 아니며, operation catalog의 exact `RETRY_CONTROL` mapping을 통과해야 한다.
|
||
|
||
`UNKNOWN`/required-integrity-failure fallback 결정표:
|
||
|
||
| Semantics | `NOT_SENT` | `MAYBE_SENT` / `SENT` / `RESPONSE_STARTED` / `RESPONSE_COMPLETE` / `UNKNOWN` |
|
||
| --- | --- | --- |
|
||
| `SAFE_READ` | failure/status profile이 허용하면 `RESTART_CONFIRMED_NOT_SENT` | failure/status profile + replayable body + budget이 모두 허용하면 `RETRY_SAFE_READ`, 아니면 permanent failure |
|
||
| `IDEMPOTENT_MUTATION` | policy가 허용하면 `RESTART_CONFIRMED_NOT_SENT` | catalog가 same-intent replay 결과도 authoritative라고 증명한 경우만 `REPLAY_SAME_INTENT`; 그 외 `RETURN_INDETERMINATE` |
|
||
| `KEYED_MUTATION` | policy가 허용하면 `RESTART_CONFIRMED_NOT_SENT` | generic retry 금지, contract가 있으면 `RECONCILE_SAME_OPERATION`, 없으면 `RETURN_INDETERMINATE` |
|
||
| `NON_RETRYABLE_MUTATION` | protected physical ceiling 1이 기본. buffered/reopenable body + explicit ceiling 2 restart policy가 exact `NOT_SENT`에만 opt-in한 경우 `RESTART_CONFIRMED_NOT_SENT`, 아니면 permanent before-send failure | 항상 `RETURN_INDETERMINATE` |
|
||
|
||
`RESPONSE_COMPLETE`라도 terminal semantic result가 invalid하면 mutation effect를 부정하지 못한다.
|
||
`MAYBE_SENT`와 `UNKNOWN`을 `NOT_SENT`로 낮추지 않는다.
|
||
|
||
`RESTART_CONFIRMED_NOT_SENT`는 upstream mutation transmission이 없었다는 engine evidence 뒤의
|
||
pre-send restart다. Retry/replay metric과 budget에 섞지 않지만 새 physical attempt, deadline,
|
||
quota/bulkhead/CB/span에는 그대로 집계한다.
|
||
|
||
HTTP/2 provider가 §30.2의 exact `CONFIRMED_NOT_PROCESSED`를 반환하면
|
||
`RESTART_CONFIRMED_NOT_PROCESSED` 후보가 된다. Operation catalog의 explicit H2 restart opt-in,
|
||
replayable body, same identity, finite protocol-restart budget, shared physical/root ceiling과 deadline을
|
||
모두 요구한다. 이는 engine autonomous resend가 아니라 provider가 현재 attempt를 종료하고 kernel이
|
||
새 physical request를 승인하는 state다. `MAYBE_PROCESSED|UNKNOWN`은 이 disposition을 만들지 않는다.
|
||
Non-retryable mutation도 provider/RFC evidence가 authoritative `NOT_PROCESSED`이고 operation이
|
||
opt-in한 경우에만 허용하며 one-shot body에는 허용하지 않는다.
|
||
|
||
`FOLLOW_DECLARED_REDIRECT`도 engine auto-follow가 아니다. Prior 3xx response/resource를 닫고 future
|
||
redirect card의 hop token과 attempt authorization을 얻은 뒤, target origin/DNS/credential/body를
|
||
다시 검증해 같은 kernel loop로 들어간다. Current canonical card set에서는 항상 terminal reject다.
|
||
|
||
`REFRESH_CREDENTIAL_AND_REPLAY`는 다음 조건을 모두 만족할 때만 선택한다.
|
||
|
||
```text
|
||
configured stale/expired-token challenge exactly matched
|
||
AND challenged credential generation is older than the currently usable generation
|
||
OR a bounded single-flight refresh for that generation is required
|
||
AND authReplayCount == 0
|
||
AND operation semantics is SAFE_READ
|
||
OR operation contract states that this exact 401 authoritatively means NOT_APPLIED
|
||
AND operation semantics is not NON_RETRYABLE_MUTATION
|
||
AND body is ABSENT, BUFFERED_REPLAYABLE or REOPENABLE_SOURCE
|
||
AND body mode is not SINGLE_USE_SOURCE
|
||
AND same operation-attempt identity and credential scope can be preserved
|
||
AND auth replay/amplification budget is available
|
||
AND caller is not cancelled and execution cutoff leaves the minimum attempt budget
|
||
```
|
||
|
||
401을 받았다는 사실만으로 mutation 미적용을 추론하지 않는다. `IDEMPOTENT_MUTATION` 또는
|
||
`KEYED_MUTATION`은 provider contract가 그 challenge를 authoritative `NOT_APPLIED`로 선언하고
|
||
같은 operation ID/key/fingerprint를 유지할 때만 auth replay할 수 있다.
|
||
`NON_RETRYABLE_MUTATION`과 `SINGLE_USE_SOURCE`는 auth replay를 항상 금지한다.
|
||
|
||
Auth replay는 ordinary retry나 pre-send restart가 아니지만 새 physical attempt다. 따라서
|
||
attempt/resend count, total deadline, local quota, physical bulkhead, circuit permission과 attempt
|
||
span을 다시 거치며 total amplification dashboard에 포함한다. Ordinary retry token을 소비하지
|
||
않고 별도의 one-token auth-replay budget을 원자적으로 소비한다.
|
||
|
||
`REPLAY_SAME_INTENT`와 `RECONCILE_SAME_OPERATION`은 다음 identity를 그대로 유지한다.
|
||
|
||
```text
|
||
operationAttemptId
|
||
idempotencyKey
|
||
requestFingerprint
|
||
operationId
|
||
tenant/credential scope
|
||
```
|
||
|
||
새 key/fingerprint로 mutation을 재시작하지 않는다. Authoritative reconciliation이
|
||
`CONFIRMED_NOT_APPLIED`를 반환한 뒤에만 같은 logical identity로 새 physical mutation attempt를
|
||
시작할 수 있다.
|
||
|
||
### 16.7 Reconciliation
|
||
|
||
Operation이 다음 중 하나를 제공해야 automatic mutation retry를 허용할 수 있다.
|
||
|
||
- same idempotency key replay가 authoritative result를 반환;
|
||
- operation ID로 status inspection;
|
||
- provider resource ID + conditional lookup;
|
||
- externally visible durable receipt;
|
||
- verified `NOT_APPLIED` evidence.
|
||
|
||
Reconciliation outcome:
|
||
|
||
```text
|
||
CONFIRMED_APPLIED(result/reference)
|
||
CONFIRMED_NOT_APPLIED
|
||
STILL_IN_PROGRESS
|
||
UNKNOWN
|
||
KEY_EXPIRED
|
||
PAYLOAD_MISMATCH
|
||
```
|
||
|
||
`CONFIRMED_NOT_APPLIED`일 때만 새 attempt 여부를 policy가 결정한다. `UNKNOWN`을 success/failure로
|
||
추측하지 않는다.
|
||
|
||
Mapping:
|
||
|
||
| Reconciliation result | Kernel disposition |
|
||
| --- | --- |
|
||
| `CONFIRMED_APPLIED` | `RETURN_COMPLETED` with authoritative result/reference |
|
||
| `CONFIRMED_NOT_APPLIED` | budget/replay policy가 허용하면 same-identity new physical attempt |
|
||
| `STILL_IN_PROGRESS` | bounded later reconciliation, caller에는 indeterminate receipt |
|
||
| `UNKNOWN` | `RETURN_INDETERMINATE` |
|
||
| `KEY_EXPIRED` | `RETURN_INDETERMINATE`, blind new key 금지 |
|
||
| `PAYLOAD_MISMATCH` | permanent local/provider contract failure, security alert |
|
||
|
||
In-call reconciliation은 다음 exact state를 따른다.
|
||
|
||
1. prior mutation response/handle과 circuit permission을 완료하고 bulkhead를 반납한다. Committed
|
||
local quota handle은 닫되 소비한 token을 환불하지 않는다.
|
||
2. catalog에 등록된 `SAFE_READ` reconciliation operation만 선택한다. Raw URL/임의 operation은
|
||
허용하지 않고 별도 resilience group과 pool/admission 상한을 사용한다.
|
||
3. 같은 parent execution cutoff, cancellation token과 root-call HTTP-request-attempt budget을
|
||
전달한다. Poll 횟수/간격과 `maxReconciliationRequestsPerLogicalCall`은 finite하다.
|
||
4. 각 poll wait 직후 common gate와 registered child operation의 pure eligibility를 재검사하고
|
||
`tryAuthorizeNestedHttpRequest(RECONCILIATION, childProfileFingerprint)`를 정확히 한 번 호출한다.
|
||
반환된 `NestedHttpAuthorizationLease`는 child physical attempt까지 carrying하며 같은 poll에서
|
||
다시 acquire하지 않는다. Child-level safe-read retry를 허용하면 새 wire request마다 새 poll
|
||
ordinal/lease가 필요하고 child retry budget availability도 같은 atomic authorization에 포함한다.
|
||
5. Child request의 body/pool/quota/bulkhead/CB acquisition 뒤 wire start 직전에 shared root HTTP token을
|
||
reserve한다. Cancellation/start race에서 start가 이길 때만 lease/root token을 bind/commit한다.
|
||
Pre-bind exit는 lease를 `ABORTED`로 exactly once 닫고 uncommitted root/quota를 반납한다. 현재
|
||
logical call에서 detached background task를 만들지 않는다.
|
||
6. `CONFIRMED_NOT_APPLIED`만 same operation identity의 `REPLAY_SAME_INTENT`로 돌아가며 별도 replay
|
||
budget과 protected/root physical token을 소비한다. 나머지는 위 mapping대로 terminal/receipt다.
|
||
|
||
Deadline 안에 확인되지 않으면 kernel은 `Indeterminate` receipt를 반환한다. 이후 scheduler/use
|
||
case가 수행하는 reconciliation은 새 root-call budget과 별도 scheduler quota를 갖고 원래
|
||
operation-attempt identity를 이어받는다.
|
||
|
||
### 16.8 Application orchestration
|
||
|
||
HTTP kernel이 business compensation을 수행하지 않는다.
|
||
|
||
```text
|
||
use case
|
||
-> stable operation attempt ID 저장/보유
|
||
-> outbound port mutation
|
||
-> Indeterminate
|
||
-> reconciliation use case/scheduler
|
||
-> confirmed state
|
||
-> next business transition/compensation
|
||
```
|
||
|
||
DB transaction 안에서 remote HTTP mutation을 호출하고 rollback이 remote effect도 취소한다고
|
||
가정하지 않는다.
|
||
|
||
## 17. Retry policy와 retry budget
|
||
|
||
### 17.1 Pure eligibility와 exactly-once attempt authorization
|
||
|
||
후속 attempt 판정은 state를 바꾸지 않는 pure eligibility와 token을 한 번만 소비하는 authorization을
|
||
분리한다. 최초 request는 ordinary reason token을 요구하지 않지만 shared protected/root physical
|
||
token은 engine handoff 직전에 소비한다.
|
||
|
||
Pure eligibility:
|
||
|
||
```text
|
||
operation policy allows this exact AttemptDisposition
|
||
AND request body replayable when the disposition needs replay
|
||
AND failure/status/processing-evidence profile allows this disposition
|
||
AND call not cancelled/shutting down
|
||
AND absolute deadline has minimum attempt budget
|
||
AND reason-specific counter, protected physical ceiling and root-call total capacity are available
|
||
AND exact reason budget currently has a token
|
||
```
|
||
|
||
Pure 함수는 budget/CB/quota/ordinal을 획득하거나 증가시키지 않는다. Eligibility가 true인 뒤
|
||
state machine이 backoff 등 disposition별 wait를 마치고 gate를 재검사한 다음 단 한 번
|
||
`tryAuthorizeNextAttempt`를 호출한다.
|
||
|
||
```text
|
||
AttemptAuthorizationLease(
|
||
disposition,
|
||
reasonBudgetId,
|
||
logicalCallId,
|
||
operationIdentityDigest,
|
||
state = AUTHORIZED | BOUND_TO_PHYSICAL_ATTEMPT | ABORTED
|
||
)
|
||
```
|
||
|
||
`tryAuthorizeNextAttempt`는 exact reason token과 counter를 원자적으로 한 번 소비한다. Lease가 다음
|
||
loop로 carrying되므로 auth/retry/replay 분기가 다시 token을 소비하지 않는다. Initial request에는
|
||
`INITIAL` authorization marker만 있고 reason token은 없다. Authorized 뒤 deadline, refresh 또는
|
||
final protected/root reservation이 실패하면 lease는 `ABORTED`로 exactly once 닫고 default로 token을
|
||
환불하지 않아 churn이 amplification budget을 되살리지 못하게 한다. Circuit permission, local
|
||
quota와 protected/root physical token은 이 lease와 별도로 §20의 실제 attempt 경계에서 획득한다.
|
||
|
||
Disposition/semantics gate:
|
||
|
||
| Semantics | Ordinary subsequent attempt |
|
||
| --- | --- |
|
||
| `SAFE_READ` | `RESTART_CONFIRMED_NOT_SENT` 또는 `RETRY_SAFE_READ` |
|
||
| `IDEMPOTENT_MUTATION` | `RESTART_CONFIRMED_NOT_SENT`; same-intent/H2-not-processed는 별도 state |
|
||
| `KEYED_MUTATION` | `RESTART_CONFIRMED_NOT_SENT`; reconciliation/same-intent/H2-not-processed는 별도 state |
|
||
| `NON_RETRYABLE_MUTATION` | default 금지; explicit protected ceiling 2 + reopenable body + restart budget의 `RESTART_CONFIRMED_NOT_SENT`, 또는 H2 exact opt-in의 `RESTART_CONFIRMED_NOT_PROCESSED`만 |
|
||
|
||
`REPLAY_SAME_INTENT`, `RESTART_CONFIRMED_NOT_PROCESSED`, `REFRESH_CREDENTIAL_AND_REPLAY`와
|
||
`FOLLOW_DECLARED_REDIRECT`는 ordinary retry predicate를 재사용하지 않고 동일 pure-eligibility +
|
||
reason-specific `AttemptAuthorizationLease` protocol을 사용한다. `RECONCILE_SAME_OPERATION`은 prior
|
||
protected attempt를 반복하지 않고 §16.7의 registered child state로 들어가며, 각 실제 reconciliation
|
||
HTTP request는 별도 `NestedHttpAuthorizationLease`에 bind된다. 모든 state는 동일
|
||
key/fingerprint/scope와 shared root ceiling을 유지한다.
|
||
|
||
### 17.2 Backoff
|
||
|
||
Default:
|
||
|
||
- exponential backoff;
|
||
- full jitter 또는 decorrelated jitter를 명시;
|
||
- zero busy-loop 금지;
|
||
- upper cap;
|
||
- absolute deadline cap;
|
||
- retry budget cap.
|
||
|
||
예시 full jitter:
|
||
|
||
```text
|
||
cap_n = min(maxBackoff, initialBackoff * 2^n)
|
||
sleep_n = random(0, cap_n)
|
||
sleep_n = min(sleep_n, remaining - minimumAttemptBudget)
|
||
```
|
||
|
||
Random source는 test에서 deterministic injection 가능해야 한다.
|
||
|
||
### 17.3 `Retry-After`
|
||
|
||
408/429/503 등 operation이 허용한 status에서만 해석한다.
|
||
|
||
- delta-seconds와 HTTP-date만 허용하고 multiple/mixed 값은 invalid;
|
||
- malformed, negative 또는 overflow delta는 server hint를 버리고 bounded client jitter로 fallback;
|
||
- 문법상 valid하지만 과거인 HTTP-date는 `serverMinimumDelay=0`으로 두되 client jitter는 유지;
|
||
- maximum cap;
|
||
- wall-clock skew tolerance;
|
||
- remaining deadline보다 길면 현재 logical call에서는 retry하지 않음;
|
||
- provider-specific rate-reset header는 별도 parser;
|
||
- invalid header는 bounded metric, raw value log 금지.
|
||
|
||
Server hint와 client jitter 조합은:
|
||
|
||
```text
|
||
delay = max(serverMinimumDelay, clientBackoffWithJitter)
|
||
```
|
||
|
||
를 기본으로 하되 operation contract가 다르면 명시한다.
|
||
|
||
### 17.4 Retry budget
|
||
|
||
대규모 장애 때 모든 최초 호출이 N번 retry하면 outage를 증폭한다. Destination/resilience group별
|
||
retry budget을 둔다.
|
||
|
||
가능한 정책:
|
||
|
||
- token bucket;
|
||
- 성공 call 비율 기반 token replenish;
|
||
- rolling retry/original ratio cap;
|
||
- minimum reserved original-call capacity.
|
||
|
||
Metric:
|
||
|
||
```text
|
||
http.client.retry.attempts
|
||
http.client.retry.exhausted
|
||
http.client.retry.budget.rejected
|
||
http.client.presend.restarts
|
||
http.client.presend.restart.exhausted
|
||
```
|
||
|
||
Retry budget은 provider quota의 정확한 cluster-wide enforcement를 주장하지 않는다. Pod별 local
|
||
보호 장치다. Pre-send restart budget/metric은 retry budget과 분리하되 둘 다 total amplification
|
||
dashboard에 포함한다.
|
||
|
||
### 17.5 Credential refresh는 retry와 분리
|
||
|
||
401 처리 순서는 다음과 같다.
|
||
|
||
1. operation/auth profile에 등록된 invalid/expired-token status, challenge와 bounded error
|
||
contract가 정확히 일치하는지 확인한다.
|
||
2. 401 response body/connection을 drain-or-discard policy로 닫고 기존 attempt의 circuit
|
||
permission을 완료한다. Bulkhead는 반납하고 committed quota handle은 exactly once 닫되 token은
|
||
환불하지 않는다. Protected destination의 해당
|
||
401은 breaker-ignore이며 token endpoint failure와 섞지 않는다.
|
||
3. §16.6의 `REFRESH_CREDENTIAL_AND_REPLAY` pure eligibility만 평가한다. 여기서는 token을
|
||
소비하지 않는다. 조건이 하나라도 거짓이면 401을 terminal auth failure로 반환한다.
|
||
4. §17.1의 `tryAuthorizeNextAttempt`가 one-token auth-replay
|
||
`AttemptAuthorizationLease`를 정확히 한 번 만든 뒤 common deadline/cancellation/shutdown gate를
|
||
다시 확인하고 bounded single-flight refresh에 참여한다.
|
||
5. 각 waiter는 자신의 absolute execution cutoff와 cancellation token을 사용한다. Waiter가
|
||
timeout/cancel되면 shared refresh에서 detach하고 즉시 반환하며, shared refresh는 active
|
||
waiter/owner가 0일 때만 취소한다. Waiter bound를 넘으면 local admission failure다.
|
||
6. 실제 token fetch는 별도 named destination, pool, quota/bulkhead, circuit breaker와 token
|
||
operation policy를 사용한다. Protected destination의 attempt permit/connection을 보유한 채
|
||
refresh하지 않으며 자기 자신을 재귀 호출하지 않는다.
|
||
7. refresh wait 직후 common gate와 minimum attempt budget을 다시 확인한다. 성공한 새
|
||
generation의 scope/audience/tenant binding을 검증하고 원래 operation-attempt identity,
|
||
idempotency key와 payload fingerprint를 유지한다.
|
||
8. 다음 요청은 ordinary physical-attempt loop의 body-open, quota, bulkhead, circuit permission,
|
||
attempt span과 engine-start gate를 모두 새로 거친다. 정확히 한 번만 auth replay하고 두 번째
|
||
401은 refresh 없이 terminal이다.
|
||
|
||
Single-flight key는 `TokenCacheKey + challengedCredentialGeneration`이다. 여러 root call이 같은
|
||
flight에 합류할 때 실제 token network call의 owner와 deadline을 고정한다.
|
||
|
||
```text
|
||
RefreshFlight(
|
||
key,
|
||
immutableFlightDeadline,
|
||
creatorRootCallId,
|
||
creatorRefreshBudgetLease,
|
||
activeWaiters,
|
||
state
|
||
)
|
||
```
|
||
|
||
- creator election winner만 자신의 remaining nested-credential/root HTTP capacity에서 finite
|
||
token-operation attempt slice를 고정해 `creatorRefreshBudgetLease`로 flight에 이전한다. 이 slice는
|
||
상한 소유권이며 아직 wire token을 소비하지 않는다;
|
||
- `immutableFlightDeadline = min(creatorExecutionCutoff, now + tokenCallCap)`이며 joiner가 연장하지
|
||
못한다;
|
||
- 실제 child attempt마다 §9.4의 `NestedHttpAuthorizationLease(OAUTH_TOKEN)`를 하나 만들고 token
|
||
write 직전에 creator root의 shared HTTP token과 bind/commit한다. 사용하지 않은 slice capacity는
|
||
flight terminal close 때 해제하지만 이미 authorize/실행한 attempt token은 환불하지 않는다;
|
||
- joiner는 존재하지 않는 shared network attempt를 자기 root count에 중복 기록하지 않는다. 대신
|
||
자신의 auth `AttemptAuthorizationLease`, waiter admission과 후속 protected/root replay capacity를
|
||
보유해야 한다;
|
||
- creator가 cancel/detach돼도 다른 waiter가 있으면 이미 이전된 lease/deadline으로 flight가
|
||
계속된다. active waiter가 0이면 cancel하고, deadline owner를 다른 root로 바꾸거나 늘리지 않는다;
|
||
- 짧은 creator deadline으로 flight가 실패하면 joiner는 terminal refresh failure를 받고 같은 auth
|
||
lease로 새 flight를 반복 생성하지 않는다.
|
||
|
||
Refresh call의 자체 retry는 token operation이 선언한 semantics와 budget만 사용하며 protected
|
||
operation의 retry count에 합산하지 않는다. 반면 protected request의 auth replay는
|
||
`http.request.resend_count`, logical/physical call count와 total amplification에 포함한다.
|
||
|
||
최소 metric:
|
||
|
||
```text
|
||
http.client.auth.refresh.calls
|
||
http.client.auth.refresh.waiters
|
||
http.client.auth.refresh.failures
|
||
http.client.auth.replays
|
||
http.client.auth.replay.rejected
|
||
```
|
||
|
||
Raw token, client ID, tenant/user, scope의 unbounded 값과 credential generation은 metric tag에
|
||
넣지 않는다. Invalid request를 401마다 무한 재전송하지 않는다.
|
||
|
||
### 17.6 Engine hidden retry 금지
|
||
|
||
Apache:
|
||
|
||
- automatic retry strategy disabled;
|
||
- automatic redirect disabled;
|
||
- automatic auth challenge replay가 credential policy를 우회하지 않음;
|
||
- stale connection retry semantics를 검증;
|
||
- protocol upgrade/resend 관측.
|
||
|
||
JDK/reactive provider를 포함해 사후 문서화/count만으로는 충분하지 않다. Protocol event가 새
|
||
request를 요구하면 provider는 현재 attempt를 종료하고 transmission/processing evidence를 kernel에
|
||
반환한다. Kernel이 `RESTART_CONFIRMED_NOT_PROCESSED` 등 exact disposition을 만들고 새
|
||
`AttemptAuthorizationLease`, deadline/body/protected-root budget/quota/bulkhead/CB/span gate를 모두
|
||
통과한 뒤에만 다음 request를 시작한다.
|
||
|
||
Engine autonomous retry/auth/redirect/protocol resend가 이 pre-resend authorization 경계로 제어되지
|
||
않으면 반드시 disable한다. Disable할 수 없거나 callback이 실제 wire start보다 늦으면 해당
|
||
provider/profile은 release-eligible이 아니다. 보이지 않는 resend를 사후 span/count로 보정하지
|
||
않는다.
|
||
|
||
### 17.7 Hedging
|
||
|
||
R2 default는 disabled.
|
||
|
||
허용 조건:
|
||
|
||
- safe read;
|
||
- duplicate load 허용;
|
||
- distinct endpoint/connection;
|
||
- shared total deadline;
|
||
- retry/hedge combined amplification budget;
|
||
- loser active cancellation;
|
||
- provider quota 반영;
|
||
- attempt별 span;
|
||
- no mutation.
|
||
|
||
Hedging은 retry와 같은 config boolean이 아니라 R3 candidate card다.
|
||
|
||
## 18. Circuit breaker semantics
|
||
|
||
### 18.1 Default unit
|
||
|
||
Default circuit breaker는 `resilienceGroupId`별 physical attempt를 집계한다.
|
||
|
||
```text
|
||
retry loop
|
||
-> physical bulkhead
|
||
-> circuit permission lease
|
||
-> one physical attempt
|
||
-> exactly-once record/ignore/release
|
||
```
|
||
|
||
`destinationId` 하나에 모든 operation을 무조건 합치면 cheap health read와 expensive mutation이
|
||
서로 circuit을 오염시킨다. 반대로 operation마다 breaker를 만들면 state와 metric cardinality가
|
||
폭증한다. Committed bounded resilience group을 사용한다.
|
||
|
||
Circuit permission은 boolean이 아니라 `CircuitPermissionLease`로 관리한다.
|
||
|
||
```text
|
||
ACQUIRED
|
||
-> local preflight failure: RELEASED exactly once
|
||
-> immediately before provider ownership: STARTING (CAS)
|
||
STARTING
|
||
-> provider accepted handle: STARTED
|
||
-> synchronous start throw: tracker evidence로 RECORDED_SUCCESS/RECORDED_ERROR/RELEASED exactly once
|
||
STARTED
|
||
-> recordable success: RECORDED_SUCCESS exactly once
|
||
-> recordable failure: RECORDED_ERROR exactly once
|
||
-> ignored outcome: RELEASED exactly once
|
||
```
|
||
|
||
`engine.start()` 뒤에 started flag를 쓰는 순서를 금지한다. 그러면 synchronous send/failure가 flag
|
||
보다 먼저 발생해 permission을 잘못 release할 수 있다. `STARTING` 전이, transmission tracker와
|
||
provider ownership handoff를 하나의 protocol로 묶고 어느 경로든 atomic terminal state 하나만
|
||
허용한다.
|
||
|
||
Bulkhead/body/span/local preflight failure, cancellation, deadline, synchronous start failure,
|
||
half-open race도 finally 경로에서 permission을 회수한다. Resilience4j API의 ignore predicate가
|
||
permission을 어떻게 반환하는지 추측하지 않고 adapter wrapper test로 고정한다.
|
||
|
||
### 18.2 Record matrix
|
||
|
||
Default record:
|
||
|
||
- connect/DNS transient;
|
||
- response header/read timeout;
|
||
- selected 5xx;
|
||
- response truncated;
|
||
- provider overload;
|
||
- slow call threshold 초과.
|
||
|
||
Default ignore:
|
||
|
||
- application cancellation;
|
||
- shutdown;
|
||
- local admission/bulkhead/rate reject;
|
||
- caller/codec/programming defect;
|
||
- response size/media/schema contract violation;
|
||
- expected domain 404/409/412;
|
||
- 4xx config/auth error;
|
||
- SSRF/TLS policy rejection;
|
||
- circuit-open rejection 자체.
|
||
|
||
Operation-specific status mapping 이후 breaker outcome을 결정한다. Raw exception class만으로
|
||
breaker를 기록하지 않는다.
|
||
|
||
### 18.3 Slow-call policy
|
||
|
||
Failure rate와 별도로 slow-call rate를 구성할 수 있다.
|
||
|
||
- slow threshold < operation total deadline;
|
||
- body mode별 threshold 분리;
|
||
- streaming 전체 duration을 일반 JSON call과 같은 group에 넣지 않음;
|
||
- callback CPU time 포함 여부 명시;
|
||
- slow success도 capacity risk로 기록 가능.
|
||
|
||
### 18.4 Half-open
|
||
|
||
- bounded concurrent probes;
|
||
- retry disabled, ordinaryRetryCount=0, protected physical ceiling=1;
|
||
- provider quota 존중;
|
||
- representative safe operation만 probe;
|
||
- mutation을 half-open probe로 사용 금지;
|
||
- shutdown 중 probe 금지.
|
||
|
||
Automatic transition scheduler를 켜면 no-binding/disabled 상태에서 thread가 생기지 않아야 한다.
|
||
|
||
### 18.5 Logical-call breaker
|
||
|
||
일부 조직은 사용자에게 보인 논리 호출 성공률을 breaker에 반영할 수 있다. 이 경우 별도 정책 ID:
|
||
|
||
```text
|
||
physical-attempt-breaker
|
||
logical-call-breaker
|
||
```
|
||
|
||
를 사용하고 meter/span/test도 분리한다. Wrapper order의 우연한 side effect로 선택하지 않는다.
|
||
|
||
### 18.6 State와 deployment
|
||
|
||
Resilience4j breaker state는 process-local이다.
|
||
|
||
- pod마다 state가 다를 수 있음;
|
||
- rolling restart에서 reset;
|
||
- cluster-wide exact breaker 아님;
|
||
- 이 특성이 R2 availability protection에는 허용됨;
|
||
- shared distributed breaker를 위해 Redis/DB adapter에 직접 의존하지 않음.
|
||
|
||
## 19. Admission, bulkhead와 outbound quota
|
||
|
||
### 19.1 두 단계 bound
|
||
|
||
1. Logical admission:
|
||
- in-flight logical calls + backoff waiters 전체를 제한;
|
||
- bounded queue 또는 immediate reject;
|
||
- parent deadline 포함.
|
||
2. Physical attempt bulkhead:
|
||
- 실제 pool/network attempt만 제한;
|
||
- backoff 중 permit 미보유;
|
||
- per resilience group/destination.
|
||
|
||
Connection pool만으로 logical retry storm을 막을 수 없고, logical semaphore 하나를 backoff 동안
|
||
보유하면 healthy work가 starvation될 수 있다. 두 목적을 분리한다.
|
||
|
||
### 19.2 Queue
|
||
|
||
Default:
|
||
|
||
- unbounded queue 금지;
|
||
- queue capacity 명시;
|
||
- FIFO/fairness policy 명시;
|
||
- acquire timeout은 remaining deadline 이하;
|
||
- queue full과 deadline expiry 구분;
|
||
- request body를 queue 전에 대용량 materialize하지 않음;
|
||
- cancelled waiter 즉시 제거.
|
||
|
||
### 19.3 Virtual threads
|
||
|
||
Virtual thread는 blocking 비용을 낮추지만 downstream capacity를 늘리지 않는다.
|
||
|
||
필요:
|
||
|
||
- max logical concurrent;
|
||
- max protected physical attempts와 root-call total attempts;
|
||
- pool capacity;
|
||
- response-body memory budget;
|
||
- streaming connection budget;
|
||
- credential refresh bound.
|
||
|
||
예시 capacity constraint:
|
||
|
||
```text
|
||
maxBufferedInFlight * maxBufferedResponseBytes
|
||
+ maxBufferedRequestBytes
|
||
+ decoder overhead
|
||
<= allocated HTTP heap budget
|
||
```
|
||
|
||
정확한 수치는 배포 workload로 산정하고 템플릿이 임의 숫자를 성능 보장으로 제시하지 않는다.
|
||
|
||
### 19.4 Local outbound quota
|
||
|
||
Provider API quota를 보호하기 위한 local token bucket/leaky bucket을 optional로 제공할 수 있다.
|
||
|
||
- destination/operation group key만 사용;
|
||
- user/tenant high-cardinality limiter 아님;
|
||
- request cost weight 지원;
|
||
- monotonic refill;
|
||
- bounded wait 또는 reject;
|
||
- `Retry-After` local result;
|
||
- pod 수 증가 시 aggregate quota가 증가함을 명시.
|
||
|
||
정확한 조직 전체 quota가 필요하면 API gateway/provider-side quota 또는 별도 distributed
|
||
coordination capability를 사용한다. HTTP adapter가 Redis에 직접 의존하지 않는다.
|
||
|
||
### 19.5 Bulkhead와 pool 관계
|
||
|
||
권장:
|
||
|
||
```text
|
||
attemptBulkhead.maxConcurrent <= usablePoolCapacity
|
||
```
|
||
|
||
HTTP/1.1에서는 active request당 대체로 connection 하나가 필요하다. HTTP/2에서는 connection
|
||
수와 concurrent stream 수를 별도 계산한다. Pool pending queue와 adapter queue를 둘 다 크게
|
||
두어 이중 queue를 만들지 않는다.
|
||
|
||
## 20. 정확한 실행 순서와 state machine
|
||
|
||
### 20.1 Default logical call
|
||
|
||
```text
|
||
1. resolve registered operation/destination and dependency DAG
|
||
2. validate application request, auth profile and stable operation-attempt identity
|
||
3. compute absolute effective deadline and finite root-call amplification budget
|
||
4. acquire logical admission
|
||
5. encode/freeze buffered body or prepare reopenable source
|
||
6. enter explicit attempt state machine; do not freeze a credential generation for the whole call
|
||
7. release logical admission
|
||
8. map kernel result to application result
|
||
```
|
||
|
||
Logical preflight validates the auth profile/reference only. Current credential generation is selected for
|
||
each attempt after backoff and before protected attempt resources are held. Network credential refresh uses the
|
||
separate nested dependency path. Final credential injection/signing happens only after final URI/header/body
|
||
bytes are fixed and immediately before engine ownership transfer.
|
||
|
||
### 20.2 Physical attempt loop
|
||
|
||
```text
|
||
nextDisposition = INITIAL_ATTEMPT
|
||
nextAuthorization = INITIAL
|
||
physicalAttemptOrdinal = 0
|
||
while a disposition can start another protected request:
|
||
check common cancellation/shutdown/deadline gate
|
||
require protected physical ceiling and root-call HTTP budget have capacity
|
||
|
||
if nextDisposition is not INITIAL_ATTEMPT:
|
||
evaluate pure eligibility against semantics/body/identity/evidence/reason capacity
|
||
if ineligible -> return mapped terminal result without consuming a reason token
|
||
if nextDisposition is RESTART_CONFIRMED_NOT_SENT or RETRY_SAFE_READ:
|
||
wait bounded backoff or valid Retry-After without holding attempt permit/connection
|
||
recheck common gate and pure eligibility
|
||
nextAuthorization = tryAuthorizeNextAttempt(nextDisposition) exactly once
|
||
if authorization failed -> return reason-budget rejection
|
||
if nextDisposition is REFRESH_CREDENTIAL_AND_REPLAY:
|
||
join/create bounded RefreshFlight using the authorization lease
|
||
validate refreshed generation scope and detach waiter exactly once
|
||
if nextDisposition is FOLLOW_DECLARED_REDIRECT:
|
||
resolve hop target and revalidate method/body/origin/DNS/credential policy
|
||
if nextDisposition is RESTART_CONFIRMED_NOT_PROCESSED:
|
||
revalidate exact H2 processing evidence and protocol-restart opt-in
|
||
recheck common gate; carry the same nextAuthorization lease forward
|
||
|
||
select current credential generation; perform any network refresh only through nested dependency
|
||
recheck common gate
|
||
open initial body handle; on ordinal > 0 open a fresh identical buffered/reopenable body
|
||
recheck common gate
|
||
acquire uncommitted local-quota reservation with absolute execution cutoff
|
||
recheck common gate
|
||
acquire physical-attempt bulkhead with absolute execution cutoff
|
||
recheck common gate
|
||
acquire circuit-breaker permission lease without blocking past cutoff
|
||
recheck common gate
|
||
finalize URI/header/body metadata; validate generation expiry/scope and inject/sign credential
|
||
recheck common gate immediately before engine ownership transfer
|
||
|
||
on any refresh/hop/protocol revalidation, body-open, quota, bulkhead, CB, credential-signing,
|
||
local preflight or common-gate exit after authorization but before physical reservation:
|
||
abort nextAuthorization exactly once if its state is AUTHORIZED
|
||
release every acquired body/CB/bulkhead/uncommitted-quota resource in reverse order
|
||
engine start count = 0; return the exact mapped local/cancellation/deadline result
|
||
|
||
physicalReservation = atomically tryReserve one protected slot + one root HTTP-request slot
|
||
if reservation failed:
|
||
abort nextAuthorization if AUTHORIZED; release CB/bulkhead/uncommitted quota/body in reverse order
|
||
engine start count = 0; return amplification-budget rejection
|
||
|
||
prepare CLIENT span context, ordinal and one AttemptTerminalCoordinator before provider callback is possible
|
||
coordinator winner = OPEN -> RESPONSE_WON | FAILURE_WON | CANCELLATION_WON
|
||
coordinator cleanup = UNCLAIMED -> RUNNING -> DONE
|
||
atomically race pre-start cancellation gate against CB ACQUIRED -> STARTING handoff
|
||
if cancellation won before STARTING:
|
||
release uncommitted physicalReservation and quota reservation
|
||
abort nextAuthorization if AUTHORIZED; release CB/bulkhead/body; engine start count = 0
|
||
return RETURN_CANCELLED(reason) because transmission is exact NOT_SENT
|
||
if STARTING won:
|
||
atomically transfer cancellation token/handle + transmission tracker to provider
|
||
bind nextAuthorization to the ordinal when it is an authorization lease; record INITIAL marker otherwise
|
||
commit physical/root reservation and increment physicalAttemptOrdinal
|
||
commit local-quota token; it is consumed and never refunded
|
||
start one CLIENT span and invoke cancel-aware engine start with remaining phase budgets
|
||
if cancellation was already signaled, provider observes pre-cancelled token before first write
|
||
provider marks STARTED or reports synchronous start failure through the same tracker
|
||
|
||
all provider response/body/failure callbacks and cancellation submit events to the same coordinator
|
||
on response headers while winner is OPEN:
|
||
classify status/header/framing and choose bounded success/error decoder before body consumer
|
||
if exact VALID_HEADERS_ONLY outcome is authoritative:
|
||
tryWin RESPONSE_WON immediately with immutable header semantic evidence
|
||
run bounded drain/discard only as coordinator-owned cleanup; it cannot change the winner
|
||
else:
|
||
register exactly one coordinator-owned bounded body consumer
|
||
if required body/decode/semantic validation completes:
|
||
finalize ResponseIntegrity and ResponseSemanticClass; tryWin RESPONSE_WON
|
||
if body/framing/decode fails before an authoritative outcome:
|
||
preserve NONE_OR_INVALID + failure evidence; tryWin FAILURE_WON
|
||
on provider transport failure:
|
||
preserve absent/partial response, processing and transmission evidence; tryWin FAILURE_WON
|
||
on caller/deadline/shutdown cancellation:
|
||
preserve exact cancellation outcome and current evidence; tryWin CANCELLATION_WON
|
||
|
||
after one winner is visible:
|
||
coordinator claims exactly one cleanup finalizer
|
||
losing callbacks/body tasks relinquish body and cleanup ownership and observe the cleanup cancel token
|
||
preserve winner semantic class, processing evidence and monotonic transmission evidence
|
||
close/cancel exact resources
|
||
complete STARTING/STARTED circuit permission once from tracker + mapped outcome
|
||
release bulkhead; release only uncommitted quota or close committed handle without refund
|
||
finish span before any between-attempt wait
|
||
mark coordinator cleanup DONE; resolve exhaustive AttemptDisposition from the immutable winner evidence
|
||
if disposition is RETURN_COMPLETED/RETURN_DECLARED_REJECTION -> return mapped outcome
|
||
if disposition is RETURN_CANCELLED -> return cancellation reason
|
||
if disposition is RETURN_INDETERMINATE -> return receipt
|
||
if disposition is RETURN_PERMANENT_FAILURE -> return mapped permanent failure
|
||
if disposition is RECONCILE_SAME_OPERATION:
|
||
enter §16.7 bounded reconciliation child state; acquire no protected-attempt lease here
|
||
each child wire request obtains its own NestedHttpAuthorizationLease and root token
|
||
if child state returns terminal/receipt -> return it
|
||
if child state returns REPLAY_SAME_INTENT -> set nextDisposition; continue
|
||
if disposition is REPLAY_SAME_INTENT
|
||
or REFRESH_CREDENTIAL_AND_REPLAY
|
||
or FOLLOW_DECLARED_REDIRECT
|
||
or RESTART_CONFIRMED_NOT_PROCESSED:
|
||
require previous response/body, breaker permission, bulkhead and quota handle already closed
|
||
preserve exact identity/evidence required by disposition
|
||
set nextDisposition; set nextAuthorization = NONE; continue
|
||
evaluate ordinary pure eligibility without acquiring a token
|
||
if ineligible -> return mapped rejection/failure
|
||
set nextDisposition; set nextAuthorization = NONE; continue
|
||
```
|
||
|
||
|
||
|
||
`INITIAL_ATTEMPT`는 retry disposition이 아니며 ordinary reason token을 소비하지 않는다. 다만
|
||
실제 initial engine handoff도 protected/root shared physical token을 하나 소비한다. 후속 state는
|
||
`AttemptAuthorizationLease`를 정확히 하나 carrying하며 같은 disposition에서 다시 acquire하지
|
||
않는다.
|
||
`SINGLE_USE_SOURCE`는 ordinal 0에서만 열 수 있다.
|
||
|
||
모든 wait API는 복사된 상대 timeout이 아니라 같은 absolute `executionCutoff`와 cancellation
|
||
token을 받는다. Wait가 끝난 뒤 gate를 다시 확인하지 않고 다음 resource/network side effect를
|
||
시작하지 않는다. Gate failure면 body handle, circuit permission, bulkhead와 quota reservation handle을 획득
|
||
역순으로 exactly-once 정리하고 아직 bind되지 않은 `AttemptAuthorizationLease`를 `ABORTED`로 닫는다.
|
||
Uncommitted quota만 반환하고 committed provider-quota token은 어떤 결과에서도 환불하지 않는다.
|
||
|
||
Credential refresh는 previous protected attempt의 resource를 모두 반납한 뒤에만 기다린다.
|
||
Refresh waiter cancellation은 shared refresh ownership과 분리하고, refresh 성공 직후에도 cutoff가
|
||
지났으면 새 protected request를 시작하지 않는다. Token endpoint call은 별도 logical call이며
|
||
그 failure를 protected destination breaker에 기록하지 않는다.
|
||
|
||
Engine 내부도 pool/stream lease, DNS, connect, TLS와 request-write phase 사이에서 remaining
|
||
budget/cancellation을 다시 확인한다. 특히 pool/DNS wait가 cutoff 전에 시작됐다는 이유로
|
||
cutoff 뒤 새 connection, TLS handshake나 request transmission을 시작하지 않는다.
|
||
|
||
Engine handoff에는 raw mutable counter가 아니라 parent-scoped `NestedHttpAuthorizationBroker`를
|
||
함께 전달한다. Cold connection에서 HTTP CONNECT가 필요하거나 TLS validation이 named HTTP
|
||
OCSP/CRL lookup을 요구하면 provider는 각 wire request 전에 broker에서 exact child lease를 얻고,
|
||
별도 child cap + shared root token + cancellation/start handoff를 통과해야 한다. Authorization
|
||
failure는 origin request를 보내지 않고 해당 proxy/TLS failure로 종료한다. Provider/JVM이 broker
|
||
밖에서 implicit CONNECT 또는 revocation HTTP request를 만들 수 있는 profile은 금지한다.
|
||
|
||
### 20.3 Mermaid sequence
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant U as Use case
|
||
participant A as Upstream adapter
|
||
participant K as HTTP kernel
|
||
participant R as Retry loop
|
||
participant C as Circuit breaker
|
||
participant B as Attempt bulkhead
|
||
participant E as Engine/pool
|
||
participant D as Dependency
|
||
|
||
U->>A: feature-specific request + budget
|
||
A->>K: registered operation
|
||
K->>K: validate, deadline, logical admission
|
||
loop bounded physical attempts
|
||
K->>R: next attempt decision
|
||
R->>B: acquire
|
||
B->>C: permission lease
|
||
C->>E: execute with remaining budget
|
||
E->>D: physical HTTP request
|
||
D-->>E: response/failure
|
||
E-->>C: typed attempt result
|
||
C-->>B: exact-once record/ignore/release
|
||
C-->>R: attempt outcome
|
||
end
|
||
R-->>K: completed/rejected/indeterminate
|
||
K-->>A: kernel result
|
||
A-->>U: application result
|
||
```
|
||
|
||
### 20.4 Resource-release order
|
||
|
||
Attempt 종료:
|
||
|
||
1. stop body producer/consumer;
|
||
2. close response/entity stream;
|
||
3. cancel request execution if incomplete;
|
||
4. mark connection reusable 또는 discard according to framing/cancel evidence;
|
||
5. release pool lease;
|
||
6. release uncommitted local-quota reservation; already committed wire-attempt token은 환불하지 않음;
|
||
7. derive final mapped attempt outcome;
|
||
8. complete/release `CircuitPermissionLease` exactly once;
|
||
9. release attempt bulkhead;
|
||
10. finish attempt observation;
|
||
11. resolve exhaustive attempt disposition;
|
||
12. finally release logical admission.
|
||
|
||
Retry decision 전에 prior response와 connection lifecycle을 정리한다.
|
||
|
||
### 20.5 Failure during cleanup
|
||
|
||
- cleanup failure가 primary failure를 덮지 않음;
|
||
- suppressed diagnostic은 secret-safe class/code만;
|
||
- incomplete entity는 connection discard;
|
||
- double-close idempotent;
|
||
- normal cleanup은 caller deadline reserve로 bounded;
|
||
- caller deadline을 넘으면 quarantine + bounded orphan reaper;
|
||
- engine 미시작 lease는 `releasePermission`, started lease는 mapped terminal outcome으로 exactly-once
|
||
완료;
|
||
- resource leak metric/alert;
|
||
- unknown cleanup state에서 pool reuse 금지.
|
||
|
||
## 21. Transport engine와 provider design
|
||
|
||
### 21.1 Engine SPI
|
||
|
||
Spring `RestClient` 자체를 provider로 부르지 않는다. 이는 synchronous API/codec facade이고 실제
|
||
network semantics는 request factory/engine이 결정한다.
|
||
|
||
개념적 SPI:
|
||
|
||
```java
|
||
interface HttpTransportEngine extends AutoCloseable {
|
||
HttpAttemptHandle start(HttpAttemptRequest request, HttpAttemptObserver observer);
|
||
HttpEngineDescriptor descriptor();
|
||
}
|
||
```
|
||
|
||
`HttpAttemptHandle`:
|
||
|
||
```text
|
||
awaitHeaders(deadline)
|
||
response()
|
||
cancel(reason)
|
||
transmissionEvidence()
|
||
completion()
|
||
```
|
||
|
||
`HttpAttemptResponse`:
|
||
|
||
```text
|
||
status
|
||
validated bounded header view
|
||
protocol
|
||
remote address evidence
|
||
wire body stream
|
||
trailers completion
|
||
close/discard
|
||
```
|
||
|
||
Engine type은 adapter 내부에만 존재한다.
|
||
|
||
### 21.2 Provider matrix
|
||
|
||
| Provider ID | 용도 | 초기 level | R2 제약 |
|
||
| --- | --- | --- | --- |
|
||
| `apache-hc5-classic` | imperative JSON/H1/stream callback | R2 candidate/default | hard cancel, pool, DNS, TLS, close evidence 통과 |
|
||
| `apache-hc5-async` | stronger cancellation, HTTP/2, async streaming | R2 candidate | codec bridge와 callback lifecycle evidence |
|
||
| `jdk-httpclient` | dependency-minimal H1/H2 | R1 | explicit pool lease/capacity/DNS evidence 부족 |
|
||
| `reactor-netty` | reactive pipeline/streaming | R1 candidate | reactive contract와 cancellation/backpressure suite 분리 |
|
||
| `http3-quic` | future HTTP/3 | R0 | 별도 R3 topology/security card |
|
||
|
||
Provider ID가 classpath detection 결과로 바뀌지 않는다.
|
||
|
||
### 21.3 초기 reference engine
|
||
|
||
초기 구현은 `apache-hc5-classic` + Spring `RestClient`를 reference candidate로 선택한다.
|
||
|
||
이유:
|
||
|
||
- 현재 imperative/virtual-thread template과 정렬;
|
||
- Spring message converter와 HTTP Service Interface 활용 가능;
|
||
- pool total/per-route/lease timeout;
|
||
- custom DNS resolver;
|
||
- TLS/mTLS/proxy;
|
||
- idle/expired eviction;
|
||
- engine lifecycle;
|
||
- request hard cancellation을 검증할 seam.
|
||
|
||
단, classic provider가 total deadline 때 실제 I/O와 connection을 확실히 취소하지 못하면
|
||
`httpclient-static-buffered`의 `HRES-STATIC-HARD-CANCEL` evidence를 통과하지 못하며 R2로
|
||
표기하지 않는다. 이 경우
|
||
`apache-hc5-async`를 reference provider로 승격한다. 문서 선택이 evidence를 대신하지 않는다.
|
||
|
||
### 21.4 Exact engine selection
|
||
|
||
Spring Boot `ClientHttpRequestFactoryBuilder.detect()`는 classpath에 따라 HttpComponents, Jetty,
|
||
Reactor, JDK, Simple 순으로 선택할 수 있다. Template R2는 classpath 변화가 runtime engine을
|
||
바꾸게 두지 않는다.
|
||
|
||
```text
|
||
provider=apache-hc5-classic
|
||
-> exact HttpComponents builder
|
||
|
||
provider=jdk-httpclient
|
||
-> exact JDK builder
|
||
```
|
||
|
||
Selected provider dependency가 없으면 startup failure다. 다른 SDK가 transitive로 들어와도
|
||
provider가 바뀌지 않는다.
|
||
|
||
### 21.5 Boot-configured `RestClient.Builder`
|
||
|
||
Spring Boot 4는 preconfigured prototype `RestClient.Builder`에 message converters,
|
||
appropriate request factory와 observation customization을 제공한다.
|
||
|
||
구현 원칙:
|
||
|
||
- injected prototype builder를 destination마다 clone;
|
||
- exact request factory/engine을 명시적으로 교체;
|
||
- Boot observation registry/customizer를 보존;
|
||
- destination-specific base URI와 status/codec policy 적용;
|
||
- mutable builder를 destination 간 재사용하지 않음;
|
||
- `RestClient.create()`/raw builder로 auto-configuration을 우회하지 않음;
|
||
- configuration test가 expected interceptor/observation/codec set을 snapshot.
|
||
|
||
현재 모든 `RestClient.Builder` bean을 차단하는 `OutboundHttpTimeoutEnforcer`는 이 구조와
|
||
양립하지 않는다. 이를 제거하고 architecture/build gate와 registered factory 검증으로 대체한다.
|
||
|
||
### 21.6 Apache client hardening
|
||
|
||
Reference configuration은 최소 다음을 명시한다.
|
||
|
||
- `PoolingHttpClientConnectionManager`;
|
||
- `maxConnTotal`, `maxConnPerRoute`;
|
||
- connection lease timeout;
|
||
- connect timeout;
|
||
- response header timeout;
|
||
- socket/response idle semantics;
|
||
- connection TTL;
|
||
- validate-after-inactivity;
|
||
- idle/expired eviction;
|
||
- DNS resolver;
|
||
- TLS socket strategy;
|
||
- proxy route planner;
|
||
- user-token/connection state policy;
|
||
- automatic retry disabled;
|
||
- automatic redirect disabled;
|
||
- cookie management disabled;
|
||
- automatic content compression disabled unless bounded layer가 소유;
|
||
- default credentials/auth caching disabled unless named profile owns;
|
||
- hard cancellation policy;
|
||
- `Expect: 100-continue` operation-specific;
|
||
- finite max redirects even though default disabled;
|
||
- close ownership.
|
||
|
||
Apache default는 안전 계약이 아니다. 예를 들어 connection request timeout과 redirect default가
|
||
library release에서 바뀌거나 매우 클 수 있으므로 모두 typed setting으로 고정한다.
|
||
|
||
### 21.7 JDK provider limits
|
||
|
||
JDK Java 21 provider를 유지할 경우:
|
||
|
||
- `HttpClient` handle을 destination runtime이 보유하고 close;
|
||
- explicit executor와 lifecycle;
|
||
- redirect `NEVER`;
|
||
- version fixed;
|
||
- `ProxySelector` fixed/none;
|
||
- `CookieHandler` none;
|
||
- `Authenticator` none unless profile;
|
||
- SSL context/parameters explicit;
|
||
- request timeout과 outer total deadline distinction;
|
||
- `sendAsync` future cancellation evidence;
|
||
- `jdk.httpclient.*` implementation property 사용 여부를 descriptor에 표시;
|
||
- implementation property를 per-destination pool guarantee로 과장하지 않음.
|
||
|
||
JVM-wide property는 여러 destination의 isolation을 제공하지 않는다.
|
||
|
||
### 21.8 Reactive provider
|
||
|
||
`reactor-netty` 또는 WebClient provider는 다음 조건에서만 활성화한다.
|
||
|
||
- `spring-webflux`/Reactor 타입은 adapter 내부;
|
||
- application port가 synchronous이면 blocking bridge의 cancellation/context evidence;
|
||
- event-loop에서 `.block()` 금지;
|
||
- connection provider와 loop resource lifecycle;
|
||
- max connections/pending acquire/idle/lifetime;
|
||
- response release on cancel/error;
|
||
- context propagation;
|
||
- backpressure;
|
||
- H2 stream capacity;
|
||
- 별도 readiness card.
|
||
|
||
Classpath에 WebFlux가 있다는 이유만으로 default를 바꾸지 않는다.
|
||
|
||
## 22. Connection pool와 capacity
|
||
|
||
### 22.1 Pool isolation key
|
||
|
||
기본은 destination별 pool이다. 다음이 모두 같을 때만 explicit pool group으로 공유할 수 있다.
|
||
|
||
- scheme/authority;
|
||
- proxy route;
|
||
- TLS trust;
|
||
- mTLS client identity;
|
||
- DNS/address policy;
|
||
- protocol policy;
|
||
- credential connection-affinity requirement;
|
||
- lifecycle/rotation generation.
|
||
|
||
Bearer token만 다른 요청은 같은 TLS pool을 공유할 수 있지만 mTLS identity가 다르면 절대 공유하지
|
||
않는다. HTTP/2 origin coalescing도 default disabled다.
|
||
|
||
### 22.2 Required settings
|
||
|
||
```text
|
||
maxConnectionsTotal
|
||
maxConnectionsPerRoute
|
||
maxPendingAcquires
|
||
poolAcquireTimeout
|
||
connectionTtl
|
||
idleTimeout
|
||
validateAfterInactivity
|
||
defaultKeepAliveCap
|
||
evictionInterval
|
||
gracefulCloseTimeout
|
||
```
|
||
|
||
모든 값:
|
||
|
||
- finite;
|
||
- positive/zero semantics 명시;
|
||
- cross-field validation;
|
||
- selected protocol과 consistency;
|
||
- inactive destination에서는 무시가 아니라 dead-setting detection.
|
||
|
||
### 22.3 HTTP/1.1 capacity
|
||
|
||
대체로 한 active request가 connection 하나를 점유한다.
|
||
|
||
```text
|
||
usableConcurrentAttempts
|
||
<= min(maxConnectionsPerRoute, attemptBulkheadMax)
|
||
```
|
||
|
||
Streaming download는 callback 전체 동안 lease를 보유한다. 일반 JSON pool과 장시간 streaming
|
||
pool을 분리하지 않으면 작은 호출이 starvation될 수 있다.
|
||
|
||
### 22.4 HTTP/2 capacity
|
||
|
||
다음 축이 별도다.
|
||
|
||
```text
|
||
connections
|
||
max concurrent streams per connection
|
||
locally configured stream cap
|
||
server SETTINGS limit
|
||
pending stream acquires
|
||
```
|
||
|
||
`connections * streams`를 무조건 usable capacity로 계산하지 않는다. Flow control, large body,
|
||
server SETTINGS, GOAWAY와 head-of-line at application layer를 고려한다.
|
||
|
||
### 22.5 Pool acquire
|
||
|
||
- adapter logical queue 이후 attempt permit을 얻고 pool lease 요청;
|
||
- acquire timeout = min(profile cap, remaining deadline);
|
||
- pending acquire count bound;
|
||
- timeout이면 `POOL_ACQUIRE_TIMEOUT`;
|
||
- cancelled waiter 제거;
|
||
- mutation request는 아직 전송되지 않았으므로 `NOT_SENT`;
|
||
- same logical call에서 즉시 다시 pool queue에 들어가는 retry는 default 금지;
|
||
- saturation metric/alert.
|
||
|
||
### 22.6 Lifetime, idle와 DNS
|
||
|
||
권장 관계:
|
||
|
||
```text
|
||
connectionTtl <= approvedDnsPinLifetime
|
||
idleTimeout <= upstream/load-balancer idle timeout safety margin
|
||
```
|
||
|
||
DNS TTL이 짧아도 이미 열린 keep-alive connection은 자동으로 새 IP로 이동하지 않는다.
|
||
Connection TTL과 generation drain이 DNS rollout policy에 포함되어야 한다.
|
||
|
||
Lifetime jitter를 사용해 모든 pod/connection이 동시에 reconnect하지 않도록 할 수 있다. Jitter
|
||
range도 bounded/deterministic test 대상이다.
|
||
|
||
### 22.7 Keep-Alive
|
||
|
||
Server `Keep-Alive` hint를 무제한 신뢰하지 않는다.
|
||
|
||
```text
|
||
effectiveKeepAlive = min(serverHintIfValid, clientKeepAliveCap, connectionRemainingTtl)
|
||
```
|
||
|
||
Invalid/huge hint는 cap하고 low-cardinality metric을 남긴다.
|
||
|
||
### 22.8 Validation과 stale connection
|
||
|
||
- inactivity 후 validate;
|
||
- stale connection first-use failure;
|
||
- engine automatic retry disabled 상태의 동작;
|
||
- safe read의 retry decision은 kernel 소유;
|
||
- mutation은 stale connection이라는 추측만으로 blind retry하지 않음;
|
||
- repeated stale failures는 pool/DNS/load-balancer runbook 신호.
|
||
|
||
### 22.9 Resource/memory budget
|
||
|
||
Per destination capacity input:
|
||
|
||
```text
|
||
max logical calls
|
||
max protected physical attempts/root-call total attempts
|
||
max buffered request/response/error bytes
|
||
max streaming calls
|
||
max pending acquires
|
||
max auth refresh waiters
|
||
connection buffers/TLS overhead
|
||
```
|
||
|
||
Template는 arbitrary high default를 주지 않는다. Deployment overlay가 workload/SLO/downstream
|
||
quota에서 산정하고 readiness validator가 machine/container bounds와 모순을 검사한다.
|
||
|
||
### 22.10 Pool observability
|
||
|
||
필수:
|
||
|
||
```text
|
||
leased
|
||
available
|
||
pending
|
||
max
|
||
acquire duration
|
||
acquire timeout/reject
|
||
created/closed/expired/idle-evicted
|
||
reuse
|
||
cancel-discard
|
||
generation
|
||
```
|
||
|
||
Pool route나 remote IP를 unbounded tag로 쓰지 않는다. Destination/pool-profile ID만 tag한다.
|
||
|
||
## 23. DNS, address selection와 service discovery
|
||
|
||
### 23.1 DNS policy profile
|
||
|
||
```text
|
||
FIXED_PUBLIC
|
||
FIXED_PRIVATE
|
||
KUBERNETES_SERVICE
|
||
SERVICE_MESH_LOOPBACK
|
||
EGRESS_PROXY_ENFORCED
|
||
DYNAMIC_PUBLIC_FETCH (separate capability)
|
||
```
|
||
|
||
각 profile은 allowed/forbidden address, resolver, cache, stale policy, connection TTL과 readiness
|
||
evidence가 다르다.
|
||
|
||
### 23.2 Resolve-validate-connect binding
|
||
|
||
SSRF 방어에서 다음 TOCTOU를 금지한다.
|
||
|
||
```text
|
||
validate hostname resolution A
|
||
then engine independently resolves B
|
||
then connect B
|
||
```
|
||
|
||
필수 흐름:
|
||
|
||
```text
|
||
canonical hostname
|
||
-> designated resolver
|
||
-> all A/AAAA answers
|
||
-> canonical binary address classification
|
||
-> policy filter
|
||
-> only approved addresses handed to connection operator
|
||
-> TLS SNI/hostname verification uses original hostname
|
||
```
|
||
|
||
Engine이 filter 뒤 다시 resolve하면 해당 provider는 address-pinning card를 통과하지 못한다.
|
||
|
||
### 23.3 Address classification
|
||
|
||
Profile에 따라 최소 다음을 명시적으로 분류한다.
|
||
|
||
- unspecified;
|
||
- loopback;
|
||
- link-local;
|
||
- private;
|
||
- carrier-grade NAT;
|
||
- multicast;
|
||
- documentation/benchmark;
|
||
- IPv4-mapped IPv6;
|
||
- configured NAT64 well-known/network-specific prefix 안의 IPv4-embedded IPv6;
|
||
- 6to4, Teredo와 IANA special-purpose IPv6 transition address;
|
||
- IPv6 unique-local/link-local;
|
||
- cloud metadata ranges;
|
||
- organization internal CIDR;
|
||
- exact approved public/private ranges.
|
||
|
||
String prefix/regex가 아니라 parsed binary address와 prefix math를 사용한다. Configured
|
||
translation prefix에서는 embedded IPv4를 추출해 동일 IPv4 CIDR/metadata/private 정책을 다시
|
||
적용한다. Dynamic public fetch에서 translation prefix를 신뢰성 있게 확정하지 못하면 NAT64를
|
||
fail-closed한다.
|
||
|
||
`localhost`, decimal/octal/hex-like IPv4 ambiguity, shortened IPv4, trailing dot, mixed-case IDN,
|
||
IPv6 zone ID, DNS CNAME chain을 security test에 포함한다.
|
||
|
||
### 23.4 Mixed answer policy
|
||
|
||
Public profile에서 하나의 hostname이 public과 forbidden address를 함께 반환하면:
|
||
|
||
- default: 전체 resolution reject;
|
||
- 일부 safe address만 선택하는 mode는 DNS poisoning/partial outage 의미를 명시한 별도 profile;
|
||
- alert와 sanitized evidence;
|
||
- raw tenant/request hostname tag 금지.
|
||
|
||
### 23.5 TTL와 cache
|
||
|
||
Java name resolution은 positive/negative/stale cache를 가질 수 있고 일부 default가
|
||
implementation-specific다. R2는 JVM default를 암묵적으로 사용하지 않는다.
|
||
|
||
정의:
|
||
|
||
- positive TTL min/max;
|
||
- negative TTL;
|
||
- stale-if-DNS-error 허용 여부와 max stale;
|
||
- CNAME chain expiry;
|
||
- refresh jitter;
|
||
- resolution timeout;
|
||
- max concurrent resolutions;
|
||
- cache entry bound;
|
||
- config/DNS change generation.
|
||
|
||
Fixed internal service에서 previously validated stale address를 잠깐 쓰는 availability policy는
|
||
가능하지만 public/dynamic egress에서 stale을 허용하면 address ownership 변화 위험이 있다.
|
||
|
||
### 23.6 DNS timeout
|
||
|
||
Platform resolver call이 interruption/deadline을 보장하지 않으면 별도 bounded resolver executor,
|
||
resolver library 또는 proxy/service-discovery provider가 필요하다.
|
||
|
||
- executor queue bound;
|
||
- resolution deadline;
|
||
- abandoned task bound;
|
||
- resolver shutdown;
|
||
- DNS storm coalescing;
|
||
- negative cache;
|
||
- no platform thread leak.
|
||
|
||
Timeout wrapper만 반환하고 blocking resolver task를 계속 누적시키지 않는다.
|
||
|
||
### 23.7 Address selection
|
||
|
||
- approved A/AAAA 전체를 순환/정책에 따라 사용;
|
||
- one bad address가 전체 deadline을 소모하지 않도록 per-address connect cap;
|
||
- IPv4/IPv6 preference와 Happy Eyeballs 지원 여부;
|
||
- address failover도 physical connect attempt로 관측;
|
||
- mutation request 전 connect failure는 `NOT_SENT`;
|
||
- connection establishment 뒤 resend는 operation retry policy 소유.
|
||
|
||
### 23.8 Kubernetes/service discovery
|
||
|
||
Kubernetes Service profile:
|
||
|
||
- expected private CIDR;
|
||
- service FQDN;
|
||
- headless/ClusterIP 구분;
|
||
- readiness/pod churn과 connection TTL;
|
||
- DNS TTL;
|
||
- multi-address load spread;
|
||
- zone/locality awareness optional;
|
||
- mesh sidecar interception 여부.
|
||
|
||
Short service name/search suffix에 의존하지 않고 canonical name을 사용한다.
|
||
|
||
### 23.9 Service mesh
|
||
|
||
Sidecar가 traffic을 loopback으로 redirect하면 application이 본 peer address만으로 origin
|
||
security를 증명할 수 없다.
|
||
|
||
Descriptor:
|
||
|
||
```text
|
||
egress_enforcement=SERVICE_MESH
|
||
mesh_identity_policy_revision
|
||
direct_egress_blocked=true
|
||
proxy_tls_mode
|
||
```
|
||
|
||
Mesh policy evidence와 application destination registry를 둘 다 요구한다. Mesh가 있으니
|
||
application URI/credential/redirect 검증을 제거하지 않는다.
|
||
|
||
## 24. SSRF, redirect와 proxy security
|
||
|
||
### 24.1 Threat boundary
|
||
|
||
공격 입력:
|
||
|
||
- path/query value;
|
||
- upstream redirect;
|
||
- pagination link;
|
||
- configured DNS;
|
||
- compromised external response;
|
||
- proxy environment;
|
||
- IDN/encoding ambiguity;
|
||
- user-controlled webhook/fetch URL;
|
||
- poisoned service discovery.
|
||
|
||
보호 대상:
|
||
|
||
- cloud metadata;
|
||
- loopback/admin endpoints;
|
||
- cluster control plane;
|
||
- internal service;
|
||
- Unix/file/local schemes;
|
||
- credentials in cross-origin redirect;
|
||
- proxy credentials;
|
||
- network topology.
|
||
|
||
### 24.2 Fixed destination baseline
|
||
|
||
- exact scheme/host/port;
|
||
- relative route only;
|
||
- no userinfo;
|
||
- no fragment;
|
||
- no raw Host override;
|
||
- resolved IP policy;
|
||
- network policy/security group;
|
||
- redirect disabled;
|
||
- system proxy disabled;
|
||
- no direct fallback from proxy.
|
||
|
||
Application validation과 network egress policy를 함께 사용한다.
|
||
|
||
### 24.3 Redirect manual state machine
|
||
|
||
이 절은 future redirect card의 실행 계약이다. 현재 canonical card set에는 redirect-follow가
|
||
없으므로 fixed-destination R2에서는 engine과 kernel redirect를 모두 disable하고, follow mode를
|
||
설정하면 startup에서 거절한다.
|
||
|
||
Engine auto redirect를 끄고 kernel이 처리한다.
|
||
|
||
Hop마다:
|
||
|
||
1. 3xx가 operation에 허용되는지 확인;
|
||
2. `Location` size/syntax/relative resolution;
|
||
3. hop/loop bound;
|
||
4. method/body rewrite rule;
|
||
5. body replayability;
|
||
6. scheme downgrade 금지;
|
||
7. target origin allowlist;
|
||
8. fresh DNS/address validation;
|
||
9. cross-origin credential/header stripping;
|
||
10. remaining deadline/retry amplification budget;
|
||
11. new physical attempt span.
|
||
|
||
301/302가 POST를 GET으로 바꾸는 engine default에 business mutation을 맡기지 않는다.
|
||
307/308이 method를 보존해도 one-shot body는 replay하지 않는다.
|
||
|
||
Declared 3xx는 response/connection, CB permission, bulkhead와 quota handle을 먼저 닫은 뒤 §16의
|
||
`FOLLOW_DECLARED_REDIRECT`를 만든다. 다음 loop에서 redirect-hop
|
||
`AttemptAuthorizationLease`를 한 번 획득하고 protected/root physical ceiling, fresh DNS/SSRF,
|
||
credential/body와 deadline gate를 다시 통과한다. Redirect hop도 새 ordinal/CLIENT span과
|
||
`http.request.resend_count`를 가진다. Current card set은 이 disposition을 생성하지 않고 startup과
|
||
call에서 fail-closed한다.
|
||
|
||
### 24.4 Credential stripping
|
||
|
||
Origin이 달라지면 default 제거:
|
||
|
||
```text
|
||
Authorization
|
||
Cookie
|
||
API key header
|
||
Idempotency-Key
|
||
signature headers
|
||
client correlation containing protected data
|
||
```
|
||
|
||
Same-origin이라도 operation redirect contract가 없는 credential replay는 금지한다.
|
||
|
||
### 24.5 Proxy
|
||
|
||
Proxy profile:
|
||
|
||
- exact proxy scheme/host/port;
|
||
- proxy TLS 여부;
|
||
- proxy auth secret ref;
|
||
- CONNECT allowed target;
|
||
- local DNS 또는 remote DNS;
|
||
- target SNI/hostname verification;
|
||
- no `HTTP_PROXY`/`HTTPS_PROXY`/`NO_PROXY` ambient inheritance unless explicit;
|
||
- direct fallback disabled;
|
||
- proxy readiness;
|
||
- proxy audit/egress policy revision.
|
||
|
||
Proxy가 remote DNS를 수행하면 application의 A/AAAA 검증을 R2 evidence로 주장하지 않는다.
|
||
`EGRESS_PROXY_ENFORCED` DNS profile을 선택하면 `httpclient-egress-proxy` card가 proxy-side
|
||
address filtering을 증명해야 한다.
|
||
|
||
Proxy 자체도 숨은 transport 설정이 아니라 first-class named dependency다. Proxy
|
||
host/DNS/address/TLS/auth generation은 exact profile tuple과 readiness evidence를 가지며 parent
|
||
origin profile이 그 fingerprint를 참조한다. Proxy dependency cycle, ambient fallback과 direct
|
||
fallback을 startup에서 거절하고 shared proxy client/pool은 owner/reference count로 닫는다.
|
||
Raw-capture contract test는 `Proxy-Authorization`이 proxy hop에만 존재하고 CONNECT tunnel 내부
|
||
origin request, redirect와 telemetry에 절대 나타나지 않음을 검증한다.
|
||
|
||
새 tunnel에 HTTP CONNECT wire request가 필요하면 provider는 parent broker에
|
||
`tryAuthorizeNestedHttpRequest(PROXY_CONNECT, proxyProfileFingerprint)`를 호출한다. Lease는
|
||
`maxProxyConnectRequestsPerRootCall` token을 한 번 소비하고 CONNECT write 직전 shared root HTTP
|
||
token과 함께 bind한다. Proxy DNS/TCP/TLS만 실패했거나 기존 tunnel을 재사용해 CONNECT를 보내지
|
||
않으면 CONNECT lease를 만들지 않는다. Child lease/root reservation/cancellation race를 통제할 수
|
||
없는 engine proxy mode와 ambient proxy는 release-eligible이 아니다.
|
||
|
||
### 24.6 Dynamic public fetch
|
||
|
||
별도 high-risk capability다.
|
||
|
||
- no credentials;
|
||
- GET/HEAD only;
|
||
- public addresses only;
|
||
- redirect exact revalidation;
|
||
- content type/size/compression cap;
|
||
- malware/content scan;
|
||
- no cookies;
|
||
- no auth challenge;
|
||
- separate pool/quota;
|
||
- egress proxy recommended;
|
||
- audit;
|
||
- downloaded content를 trusted data로 바로 사용하지 않음.
|
||
|
||
Target authority 자체가 attacker-controlled이므로 이 card는 standard HTTP auto-instrumentation을
|
||
억제한다. Default telemetry는 fixed `destination_id=untrusted-fetch`, bounded operation/outcome을
|
||
사용하는 INTERNAL span/project metric뿐이며 `server.address`, `url.full`, `url.path/query`와
|
||
`network.peer.address`를 SDK에 넣지 않는다. Target-level telemetry가 반드시 필요하면 별도
|
||
confidential pipeline, retention/access policy와 cardinality cap을 exact profile evidence로
|
||
승격해야 하며 없으면 fail-closed한다.
|
||
|
||
일반 outbound HTTP card 선택만으로 dynamic fetch가 활성화되지 않는다.
|
||
|
||
## 25. TLS, mTLS와 certificate lifecycle
|
||
|
||
### 25.1 TLS profiles
|
||
|
||
```text
|
||
public-system-trust
|
||
private-ca
|
||
mutual-tls
|
||
service-mesh-plaintext-to-sidecar
|
||
local-test
|
||
```
|
||
|
||
Profile ID는 destination에 고정되며 caller가 선택하지 않는다.
|
||
|
||
### 25.2 Required TLS controls
|
||
|
||
- HTTPS required in prod except approved profile;
|
||
- endpoint identification/hostname verification;
|
||
- original hostname SNI;
|
||
- supported TLS protocol allowlist;
|
||
- algorithm/cipher constraints;
|
||
- trust source;
|
||
- client key source if mTLS;
|
||
- certificate validity;
|
||
- handshake timeout;
|
||
- ALPN policy;
|
||
- session resumption policy;
|
||
- trust/key material version;
|
||
- secret-safe errors.
|
||
|
||
Trust-all manager, accept-all hostname verifier, expired cert ignore는 hard startup/test failure다.
|
||
|
||
### 25.3 Spring SSL Bundles
|
||
|
||
Spring Boot named `SslBundle`을 trust/key material source로 재사용한다.
|
||
|
||
- destination config에는 bundle ID만;
|
||
- secret literal/password를 repository YAML에 넣지 않음;
|
||
- exact bundle existence/type validation;
|
||
- engine-specific SSL context/socket strategy 생성;
|
||
- bundle ID low-cardinality metadata;
|
||
- key/trust fingerprint raw value log 금지.
|
||
|
||
Spring Boot의 bundle file reload가 모든 client consumer를 자동 재구성한다는 보장은 없다.
|
||
공식 문서는 reload-compatible component를 제한적으로 열거한다. HTTP client는 별도 generation
|
||
rotation을 구현하고 검증한다.
|
||
|
||
### 25.4 Rotation과 emergency revocation
|
||
|
||
정상 변경과 compromise/revocation을 같은 drain 정책으로 처리하지 않는다.
|
||
|
||
`NORMAL_ROLLOVER`:
|
||
|
||
```text
|
||
detect/receive new material version
|
||
-> validate chain/key match/expiry/hostname policy
|
||
-> build new engine + pool generation
|
||
-> optional safe probe
|
||
-> atomic registry swap
|
||
-> new calls use new generation
|
||
-> old in-flight calls drain
|
||
-> bounded timeout
|
||
-> cancel/close old pool
|
||
```
|
||
|
||
`EMERGENCY_REVOKE`:
|
||
|
||
```text
|
||
verified compromise/revocation signal
|
||
-> atomically block old-generation admission
|
||
-> invalidate matching token cache, TLS sessions and pool generation
|
||
-> cancel in-flight calls when the registered emergency policy requires it
|
||
-> forbid old-generation fallback and rollback
|
||
-> close old resources with bounded quarantine/reaper
|
||
-> remain NOT_READY until a validated non-revoked generation is published
|
||
```
|
||
|
||
Emergency event source/authenticity, affected generation/scope, in-flight cancellation policy와 audit
|
||
receipt를 registry/runbook가 소유한다. Availability를 위해 revoked credential/trust anchor를 계속
|
||
사용하지 않는다. In-place mutable SSLContext가 기존 connection을 새 certificate로 바꾼다고
|
||
가정하지 않는다.
|
||
|
||
### 25.5 mTLS
|
||
|
||
- client certificate/key pair validation;
|
||
- expected key alias;
|
||
- permitted subject/SAN policy if required;
|
||
- distinct identity => distinct pool;
|
||
- private key file permission;
|
||
- secret rotation;
|
||
- server request가 client cert를 실제 요구하는 integration test;
|
||
- expired/not-yet-valid/wrong CA/wrong key;
|
||
- dual-certificate overlap rollout;
|
||
- certificate expiry alert.
|
||
|
||
### 25.6 Revocation과 certificate-directed egress
|
||
|
||
OCSP/CRL은 environment policy에 따라 optional이지만 mode를 명시한다.
|
||
|
||
- off/soft-fail/hard-fail;
|
||
- stapling support;
|
||
- cache/concurrency/response count와 byte cap;
|
||
- privacy;
|
||
- outage failure mode.
|
||
|
||
R2 baseline은 certificate AIA/CRLDP URI, implicit OCSP responder discovery와 LDAP/FTP retrieval을
|
||
자동으로 따라가지 않도록 비활성화한다. 인증서가 지시한 URI는 신뢰된 outbound destination이
|
||
아니며 JVM default network timeout은 capability total deadline이 아니다.
|
||
|
||
Network revocation lookup을 활성화하려면 responder를 first-class named revocation dependency로
|
||
등록한다.
|
||
|
||
- exact HTTPS scheme/host/port allowlist와 pinned DNS/SSRF policy;
|
||
- redirect, cookie, ambient proxy와 origin credential 전파 금지;
|
||
- responder TLS/auth가 필요하면 별도 exact profile;
|
||
- handshake parent cutoff 안의 finite phase deadline;
|
||
- bounded response bytes, certificate/CRL count, cache TTL와 concurrent lookups;
|
||
- no direct fallback, no certificate-provided dynamic host;
|
||
- startup에서 JVM-global PKI/security property effective value 검증;
|
||
- lifecycle/readiness/evidence fingerprint에 responder profile 포함.
|
||
|
||
Cache miss로 named HTTP OCSP/CRL wire request가 필요하면 TLS/provider integration은 parent broker에
|
||
`tryAuthorizeNestedHttpRequest(REVOCATION, responderProfileFingerprint)`를 호출한다. 각 request는
|
||
`maxRevocationHttpRequestsPerRootCall` token과 shared root HTTP token을 write 직전에 bind하며,
|
||
pre-bind deadline/cancel/validation failure에서는 network side effect 없이 lease를 abort한다. Valid
|
||
cache hit/stapled evidence에는 token을 쓰지 않는다. Trust manager/JVM이 이 hook 밖에서 responder를
|
||
호출할 수 있으면 network revocation mode를 활성화하지 않는다.
|
||
|
||
HTTP 200은 revocation success evidence가 아니다. OCSP는 response signature와 authorized responder
|
||
chain, exact CertID/issuer/serial, status `GOOD|REVOKED|UNKNOWN`, `producedAt`/`thisUpdate`/
|
||
`nextUpdate`, configured max-age/clock skew와 nonce/replay policy를 검증한다. `UNKNOWN`, stale,
|
||
wrong-responder와 replayed old `GOOD`을 success로 승격하지 않는다. CRL은 issuer/signature,
|
||
distribution-point와 issuing-distribution-point scope, base/delta CRL number, `thisUpdate`/
|
||
`nextUpdate`, indirect-CRL support policy와 freshness를 검증한다.
|
||
|
||
Internal cache/single-flight key는 raw telemetry가 아닌 다음 bounded identity로 구성한다.
|
||
|
||
```text
|
||
revocationProfileFingerprint
|
||
+ responderProfileFingerprint
|
||
+ issuerNameHash/issuerKeyHash
|
||
+ certificateSerialOrCrlScope
|
||
+ base/delta revision
|
||
```
|
||
|
||
악성 certificate가 loopback, metadata, private CIDR 또는 oversized CRL/AIA URI를 가리켜도 network
|
||
side effect가 0인지 검증한다. Soft-fail은 policy가 허용한 responder outage만 의미하며 SSRF reject,
|
||
malformed/revoked evidence를 soft success로 바꾸지 않는다. “JVM default”라고만 두고 revocation
|
||
보장을 주장하지 않는다.
|
||
|
||
### 25.7 TLS failure semantics
|
||
|
||
- hostname/trust/expired/revoked => permanent security/config failure, retry 금지;
|
||
- handshake timeout/reset => replay-safe operation에서 bounded retry candidate;
|
||
- repeated handshake failure가 breaker를 열어도 security alert는 별도;
|
||
- certificate 내용/subject 전체 log 금지;
|
||
- peer cert hash도 metric tag 금지.
|
||
|
||
## 26. Authentication와 secret lifecycle
|
||
|
||
### 26.1 Auth profile
|
||
|
||
```text
|
||
none
|
||
api-key-header
|
||
static-bearer
|
||
oauth2-client-credentials
|
||
mutual-tls
|
||
request-signature
|
||
legacy-basic
|
||
```
|
||
|
||
Operation은 하나의 profile 또는 declared composition을 참조한다.
|
||
|
||
현재 canonical card set에서 `none`, conditional `api-key-header`/`static-bearer`,
|
||
`oauth2-client-credentials`, `mutual-tls`만 각각 정해진 card/evidence로 활성화할 수 있다.
|
||
`request-signature`와 `legacy-basic`은 future card가 생길 때까지 vocabulary reservation일 뿐이며
|
||
설정하면 startup/readiness가 실패한다.
|
||
|
||
### 26.2 Secret reference
|
||
|
||
Config:
|
||
|
||
```text
|
||
secretRef
|
||
version/generation
|
||
headerName if allowlisted
|
||
scope/audience
|
||
rotation overlap
|
||
refresh skew
|
||
```
|
||
|
||
금지:
|
||
|
||
- repository literal;
|
||
- env registry의 example secret;
|
||
- `toString()`에 material;
|
||
- exception/log/span/metric;
|
||
- query parameter token;
|
||
- application command에 raw secret.
|
||
|
||
### 26.3 API key/static bearer
|
||
|
||
- fixed allowlisted header;
|
||
- CR/LF/length validation;
|
||
- `Authorization` scheme fixed;
|
||
- attempt 직전에 inject;
|
||
- redirect 전에 strip/re-evaluate;
|
||
- generation rotation with overlap if provider permits;
|
||
- health probe가 token을 노출하지 않음.
|
||
|
||
### 26.4 OAuth2 client credentials
|
||
|
||
Spring Security OAuth2 client integration을 사용할 수 있지만 다음은 HTTP capability가 검증한다.
|
||
|
||
- registration/profile ID;
|
||
- token endpoint destination;
|
||
- client authentication method;
|
||
- scope/audience allowlist;
|
||
- finite token call deadline;
|
||
- token response size/media/JSON bounds;
|
||
- child token-endpoint auth-purpose/mode exact profile fingerprint와 acyclic dependency DAG;
|
||
- exact token cache/single-flight key;
|
||
- expiry skew + refresh jitter;
|
||
- single-flight refresh;
|
||
- refresh waiter bound;
|
||
- failure/backoff;
|
||
- secret rotation;
|
||
- one replay on stale-token 401;
|
||
- no recursive use of protected destination client to fetch its own token.
|
||
|
||
Token cache와 single-flight는 정확히 같은 key를 사용한다.
|
||
|
||
```text
|
||
TokenCacheKey(
|
||
childProfileFingerprint,
|
||
registrationOrAuthProfileId,
|
||
credentialGeneration,
|
||
normalizedCaseSensitiveScopeSet,
|
||
audienceOrResource,
|
||
tenantOrDelegationScope
|
||
)
|
||
```
|
||
|
||
Scope는 provider contract에 따라 case를 바꾸지 않고 정렬/deduplicate하며 audience/resource와
|
||
opaque tenant/delegation scope도 typed canonicalization을 거친다. 어느 축 하나라도 다르면 token,
|
||
refresh future와 failure backoff를 공유하지 않는다. Key 원문은 telemetry에 넣지 않으며 credential
|
||
normal rotation/emergency revoke 시 matching entries와 waiter를 정확히 invalidate한다.
|
||
|
||
Token endpoint 자체도 first-class named outbound dependency이며 separate pool/resilience
|
||
group을 사용한다. 일반 origin auth와 별도의 purpose/mode를 exact tuple에 넣는다.
|
||
|
||
```text
|
||
AuthPurpose = ORIGIN | OAUTH_TOKEN_ENDPOINT | PROXY_HOP | REVOCATION_RESPONDER
|
||
TokenEndpointAuthMode =
|
||
CLIENT_SECRET_BASIC
|
||
| CLIENT_SECRET_POST
|
||
| PRIVATE_KEY_JWT
|
||
| MTLS_CLIENT_AUTH
|
||
| NONE_WHEN_PROVIDER_EXPLICITLY_ALLOWS
|
||
```
|
||
|
||
OAuth token child는 `AuthPurpose=OAUTH_TOKEN_ENDPOINT`이고 exact client-auth mode, header/body
|
||
ownership, redaction, signing/mTLS와 secret generation scenario를 증명한다. OAuth의
|
||
`CLIENT_SECRET_BASIC`은 generic origin `legacy-basic` card와 다른 protocol-owned mode다. Child가
|
||
다시 `authMode=oauth2-client-credentials`를 선택하는 재귀만 금지하며 client authentication 자체를
|
||
`none`으로 숨기지 않는다.
|
||
|
||
Parent OAuth profile은 child compatibility-profile fingerprint, provider/version와 policy digest를
|
||
참조한다. Token endpoint가 자신 또는 보호 destination의 OAuth profile로 되돌아가는 cycle과
|
||
self-reference는 startup failure다.
|
||
|
||
각 token network attempt는 `maxNestedCredentialRequestsPerLogicalCall`과 shared root-call total
|
||
budget을 소비한다. Shared token client/cache/pool은 owner/reference count를 가지며 마지막 parent
|
||
binding이 제거되면 refresh waiter, cache와 resource를 닫는다. No OAuth binding이면 token endpoint
|
||
resource/evidence도 0개다. Child tuple maturity/scenario가 release-eligible이 아니면 parent OAuth
|
||
profile도 release-eligible이 될 수 없다.
|
||
|
||
### 26.5 Multi-tenant/on-behalf-of
|
||
|
||
Per-user token을 global static client profile로 넣지 않는다.
|
||
|
||
필요 시:
|
||
|
||
- application/security가 opaque delegated credential reference 제공;
|
||
- adapter credential exchange;
|
||
- raw inbound token pass-through 금지;
|
||
- bounded credential cache;
|
||
- tenant/user를 metric tag로 사용 금지;
|
||
- connection pool은 bearer token별로 생성하지 않음;
|
||
- mTLS tenant identity라면 bounded separate pool/card;
|
||
- revocation/logout semantics.
|
||
|
||
### 26.6 Request signing
|
||
|
||
SigV4/HTTP Message Signature 등 provider-specific signer:
|
||
|
||
- final method/URI/header/body digest 확정 후 attempt마다 sign;
|
||
- redirect 후 기존 signature 재사용 금지;
|
||
- clock skew/nonce policy;
|
||
- stable payload bytes;
|
||
- idempotency key는 attempts 간 유지;
|
||
- signing key secret ref;
|
||
- signed header canonicalization golden vector;
|
||
- proxy가 signed fields를 변형하는지 test.
|
||
|
||
### 26.7 Basic auth
|
||
|
||
Legacy opt-in:
|
||
|
||
- HTTPS only;
|
||
- fixed destination;
|
||
- no preemptive cross-origin forwarding;
|
||
- credential rotation;
|
||
- log redaction;
|
||
- 별도 security waiver/readiness evidence.
|
||
|
||
### 26.8 Auth failure
|
||
|
||
```text
|
||
local material missing/expired
|
||
token endpoint unavailable
|
||
401 invalid token
|
||
403 insufficient scope
|
||
mTLS rejection
|
||
signature clock skew/mismatch
|
||
```
|
||
|
||
을 분리한다. 모든 401/403을 dependency 4xx 하나로 축소하지 않는다.
|
||
|
||
## 27. Request body, serialization와 upload
|
||
|
||
### 27.1 Body modes
|
||
|
||
```text
|
||
NONE
|
||
BUFFERED_JSON
|
||
BUFFERED_BINARY
|
||
REOPENABLE_STREAM
|
||
SINGLE_USE_STREAM
|
||
MULTIPART_MANAGED
|
||
```
|
||
|
||
Mode는 operation catalog에 고정한다.
|
||
|
||
### 27.2 Buffered encoding
|
||
|
||
- adapter wire DTO만 serialize;
|
||
- canonical ObjectMapper/profile;
|
||
- max encoded bytes;
|
||
- bounded output stream으로 encode 중 limit;
|
||
- content length 계산;
|
||
- immutable byte snapshot;
|
||
- digest if required;
|
||
- retries마다 동일 bytes;
|
||
- heap budget;
|
||
- encoder exception은 internal/request contract failure, network retry 금지.
|
||
|
||
Object를 먼저 거대한 byte array로 만든 뒤 limit을 검사하지 않는다.
|
||
|
||
### 27.3 JSON constraints
|
||
|
||
- maximum nesting depth;
|
||
- maximum string/name/number length;
|
||
- maximum token/document length;
|
||
- duplicate key policy;
|
||
- numeric overflow;
|
||
- polymorphic typing disabled;
|
||
- unknown field policy per API version;
|
||
- null/absent distinction;
|
||
- date/time/locale;
|
||
- UTF-8 default;
|
||
- non-finite number policy;
|
||
- golden wire snapshots.
|
||
|
||
Inbound API ObjectMapper의 관대한 설정을 outbound wire contract에 우연히 공유하지 않는다.
|
||
|
||
### 27.4 Reopenable stream
|
||
|
||
개념:
|
||
|
||
```java
|
||
interface ReopenableBodySource {
|
||
BodyHandle open(AttemptContext context);
|
||
BodyIdentity identity();
|
||
}
|
||
```
|
||
|
||
`BodyHandle`:
|
||
|
||
- content type;
|
||
- known/unknown length;
|
||
- bounded readable source/callback;
|
||
- close;
|
||
- optional checksum;
|
||
- attempt generation.
|
||
|
||
`open()`마다 같은 semantic content를 제공해야 하며 identity/digest drift면 retry를 중단한다.
|
||
|
||
### 27.5 One-shot
|
||
|
||
- `maxProtectedPhysicalAttemptsPerLogicalCall=1`;
|
||
- redirect/auth challenge replay 금지;
|
||
- pool/acquire/DNS failure처럼 `NOT_SENT`가 확실한 경우에 새 open 가능 여부도 source contract가
|
||
명시해야 함;
|
||
- body write 시작 뒤 실패는 indeterminate mutation 가능;
|
||
- caller가 stream close를 소유하지 않도록 scoped callback 선호.
|
||
|
||
### 27.6 Request compression
|
||
|
||
Default disabled.
|
||
|
||
Opt-in:
|
||
|
||
- provider accepted encoding;
|
||
- minimum threshold;
|
||
- original/encoded size caps;
|
||
- CPU budget;
|
||
- deterministic/replayable bytes;
|
||
- signature/digest order;
|
||
- compressed size observability;
|
||
- CRIME/BREACH 같은 secret/reflection context 해당 여부.
|
||
|
||
### 27.7 `Expect: 100-continue`
|
||
|
||
큰 authenticated upload에서 operation opt-in.
|
||
|
||
- interim response timeout;
|
||
- proxy/server compatibility;
|
||
- 401/413를 body 전송 전에 받을 수 있음;
|
||
- 100을 받았다는 것이 mutation 미적용 보장은 아님;
|
||
- unsupported server fallback 여부;
|
||
- test matrix.
|
||
|
||
### 27.8 Multipart
|
||
|
||
Optional card:
|
||
|
||
- library-generated boundary;
|
||
- fixed part names;
|
||
- sanitized filename;
|
||
- per-part/aggregate size;
|
||
- part media type;
|
||
- header injection 방지;
|
||
- streaming/replayability;
|
||
- checksum;
|
||
- temporary spool policy;
|
||
- provider contract.
|
||
|
||
Arbitrary caller part/header map 금지.
|
||
|
||
### 27.9 Spooling
|
||
|
||
Memory limit을 넘는 replayable upload에 bounded disk spool을 선택할 수 있다.
|
||
|
||
- encrypted 또는 data classification에 맞는 persistent/temp storage;
|
||
- restrictive permission;
|
||
- quota;
|
||
- checksum;
|
||
- crash cleanup;
|
||
- symlink/path defense;
|
||
- no shared predictable filename;
|
||
- lifecycle/retention;
|
||
- fileserver/objectstorage adapter 직접 의존 금지.
|
||
|
||
Spooling은 별도 provider-internal resource이며 기본 활성화하지 않는다.
|
||
|
||
## 28. Response body, decoding와 download
|
||
|
||
### 28.1 Header/status first
|
||
|
||
Response body consumer 전에:
|
||
|
||
1. status read;
|
||
2. header aggregate validation;
|
||
3. status mapping;
|
||
4. expected body presence;
|
||
5. content type/encoding/declared length;
|
||
6. success/error decoder selection;
|
||
7. body bound 설치;
|
||
8. consumer 호출.
|
||
|
||
Streaming `exchange()` callback이 이 순서를 직접 구현한다.
|
||
|
||
### 28.2 Wire와 decoded bound
|
||
|
||
```text
|
||
maxWireBytes
|
||
maxDecodedBytes
|
||
maxExpansionRatio
|
||
```
|
||
|
||
를 별도로 둔다.
|
||
|
||
`Content-Length`:
|
||
|
||
- fast reject hint;
|
||
- missing 가능;
|
||
- 거짓/압축 표현 가능;
|
||
- actual counting을 대체하지 않음.
|
||
|
||
Transparent engine decompression을 꺼서 counting layer가 compressed/decoded 경계를 소유하거나,
|
||
provider가 두 값을 확실히 관측하는 별도 구현을 제공한다.
|
||
|
||
### 28.3 Content encoding
|
||
|
||
Default accepted:
|
||
|
||
```text
|
||
identity
|
||
gzip (explicit operation/profile)
|
||
```
|
||
|
||
Unknown/multiple encoding:
|
||
|
||
- operation allowlist;
|
||
- decode chain depth;
|
||
- decoded cap;
|
||
- CPU/time budget;
|
||
- malformed/truncated handling;
|
||
- connection discard.
|
||
|
||
Brotli/Zstd native/runtime dependency는 별도 optional card다.
|
||
|
||
### 28.4 Buffered response
|
||
|
||
- bounded byte load;
|
||
- media/charset validation;
|
||
- decode;
|
||
- required semantic field validation;
|
||
- no raw body retention;
|
||
- generic type support through typed decoder, not caller `Class<T>`;
|
||
- success with empty/null body contract;
|
||
- response close in finally.
|
||
|
||
### 28.5 Streaming callback
|
||
|
||
개념:
|
||
|
||
```java
|
||
interface BoundedResponseConsumer<R> {
|
||
R consume(SafeResponseMetadata metadata, BoundedBody body, CancellationView cancellation);
|
||
}
|
||
```
|
||
|
||
불변식:
|
||
|
||
- body callback scope 밖 탈출 금지;
|
||
- limit은 streaming에도 적용;
|
||
- idle/total deadline;
|
||
- early return/exception에서 close/cancel;
|
||
- `readAllBytes()`를 streaming sample로 제시하지 않음;
|
||
- callback이 application/domain `InputStream`을 반환하지 않음;
|
||
- partial output publication은 consumer workflow가 staging/commit으로 처리;
|
||
- callback result가 body bytes에 비례해 무한히 커지지 않도록 별도 contract.
|
||
|
||
### 28.6 Partial/truncated response
|
||
|
||
- Content-Length 미충족;
|
||
- chunk terminator 없음;
|
||
- connection reset;
|
||
- checksum mismatch;
|
||
- decompressor EOF;
|
||
- HTTP/2 reset;
|
||
|
||
은 `RESPONSE_TRUNCATED`/`CHECKSUM_MISMATCH`다. 이미 consumer가 partial side effect를 만들었다면
|
||
adapter/use case가 abort/cleanup할 수 있어야 한다.
|
||
|
||
### 28.7 Range/resume
|
||
|
||
Optional:
|
||
|
||
- stable ETag/version required;
|
||
- `Range` + `If-Match`;
|
||
- 206 required;
|
||
- exact `Content-Range`;
|
||
- total size consistency;
|
||
- checksum;
|
||
- 200 fallback은 새 target으로 restart, 기존 partial에 append 금지;
|
||
- 416 mapping;
|
||
- provider change detection;
|
||
- same deadline/retry budget.
|
||
|
||
### 28.8 Error response
|
||
|
||
- separate small cap;
|
||
- status received 사실 보존;
|
||
- operation-specific error codec;
|
||
- raw provider message default 미노출;
|
||
- problem type/code allowlist;
|
||
- error decode failure가 status를 잃게 하지 않음;
|
||
- connection reuse를 위해 cap 안에서 consume하거나 discard/close.
|
||
|
||
### 28.9 Memory budget
|
||
|
||
Buffered mode의 limit은 개별 body만이 아니라 concurrent aggregate와 연결된다.
|
||
|
||
```text
|
||
maxConcurrentBufferedAttempts
|
||
* (request cap + response cap + error cap + codec overhead)
|
||
```
|
||
|
||
Startup validator가 configured concurrency와 container/JVM budget의 명백한 모순을 거부한다.
|
||
정확한 heap overhead는 load/soak evidence로 조정한다.
|
||
|
||
## 29. Protocol, media와 API compatibility
|
||
|
||
### 29.1 Success status is operation-specific
|
||
|
||
다음은 모두 가능한 정상 계약이다.
|
||
|
||
- create: 201;
|
||
- accepted async: 202 + status reference;
|
||
- delete: 204;
|
||
- conditional read: 200/304;
|
||
- range: 206;
|
||
- empty lookup: 404 mapped absence.
|
||
|
||
`is2xxSuccessful()` 하나로 완료를 정의하지 않는다.
|
||
|
||
### 29.2 Media negotiation
|
||
|
||
Operation이 고정:
|
||
|
||
- `Accept`;
|
||
- request `Content-Type`;
|
||
- response accepted type/parameters;
|
||
- charset;
|
||
- content encoding;
|
||
- API vendor media version.
|
||
|
||
Unexpected HTML error page를 JSON으로 decode하다 connect failure로 분류하지 않는다.
|
||
|
||
### 29.3 API version
|
||
|
||
Header/query/path version strategy를 operation catalog가 소유한다. Spring Boot/Framework client API
|
||
version support를 사용할 수 있지만:
|
||
|
||
- server-side version config 자동 공유를 가정하지 않음;
|
||
- version이 operation revision과 연결;
|
||
- N/N-1 rolling compatibility;
|
||
- sunset/deprecation header monitoring;
|
||
- version 값 caller override 금지.
|
||
|
||
### 29.4 Tolerant reader, strict semantic validation
|
||
|
||
- unknown additive field는 configured tolerant read 가능;
|
||
- required business field missing은 failure;
|
||
- enum unknown은 explicit `UNKNOWN`/failure policy;
|
||
- numeric unit/currency/version 검증;
|
||
- null/absent/default;
|
||
- provider timestamp/clock;
|
||
- schema validation과 business mapping 분리.
|
||
|
||
Wire DTO를 domain entity로 직접 deserialize하지 않는다.
|
||
|
||
### 29.5 Contract tests
|
||
|
||
가능한 evidence:
|
||
|
||
- provider OpenAPI schema;
|
||
- captured sanitized golden fixtures;
|
||
- consumer-driven contract;
|
||
- provider sandbox;
|
||
- backward/forward fixture matrix;
|
||
- unknown field/enum;
|
||
- deprecation header.
|
||
|
||
Mock fixture가 provider 실제 behavior와 같다는 보장은 없으므로 sandbox/nightly evidence를
|
||
구분한다.
|
||
|
||
### 29.6 Pagination
|
||
|
||
Generic kernel이 자동으로 모든 `next` link를 따라가지 않는다.
|
||
|
||
Upstream adapter가:
|
||
|
||
- bounded max pages/items/bytes;
|
||
- total deadline;
|
||
- cursor loop detection;
|
||
- known route reconstruction;
|
||
- per-page retry;
|
||
- partial-result policy;
|
||
- consistency/snapshot token;
|
||
|
||
을 operation 의미에 맞게 소유한다.
|
||
|
||
### 29.7 Conditional request
|
||
|
||
ETag/If-Match/If-None-Match:
|
||
|
||
- opaque validator;
|
||
- weak/strong semantics;
|
||
- operation-specific storage;
|
||
- 304/412 mapping;
|
||
- credential/tenant scope;
|
||
- cache interaction;
|
||
- raw ETag metric tag 금지.
|
||
|
||
Mutation retry safety를 위해 `If-Match`를 사용할 수 있지만 idempotency-key와 동일하지 않다.
|
||
|
||
### 29.8 HTTP caching
|
||
|
||
Default no transparent client cache.
|
||
|
||
필요하면:
|
||
|
||
- RFC caching semantics;
|
||
- auth/private response;
|
||
- `Vary`;
|
||
- freshness/revalidation;
|
||
- storage/eviction;
|
||
- tenant isolation;
|
||
- invalidation;
|
||
- cache observability;
|
||
|
||
를 별도 cache integration card로 설계한다. HTTP engine 내부 cache가 Redis/application cache
|
||
정책을 우회하지 않는다.
|
||
|
||
### 29.9 Informational responses와 trailers
|
||
|
||
- 100 Continue는 upload state machine;
|
||
- 103 Early Hints는 baseline에서 application outcome 아님;
|
||
- trailers는 declared allowlist와 size cap;
|
||
- checksum trailer가 있으면 full-body completion 전 success 금지;
|
||
- unsupported interim/trailer behavior는 provider matrix에 기록.
|
||
|
||
## 30. HTTP version policy
|
||
|
||
### 30.1 HTTP/1.1 baseline
|
||
|
||
초기 `apache-hc5-classic` R2 profile은 HTTP/1.1을 기본으로 한다.
|
||
|
||
- pipelining disabled;
|
||
- finite header limits;
|
||
- request/response framing strict;
|
||
- `Content-Length` + `Transfer-Encoding` ambiguity reject;
|
||
- connection-close delimited response는 truncation ambiguity를 명시;
|
||
- stale keep-alive test;
|
||
- proxy compatibility.
|
||
|
||
RFC 9112 framing violation과 smuggling 위험을 engine strict mode/test로 검증한다.
|
||
|
||
### 30.2 HTTP/2 opt-in
|
||
|
||
Provider/card requirements:
|
||
|
||
- ALPN negotiation;
|
||
- `H2_REQUIRED` 또는 fallback policy;
|
||
- stream concurrency/pending bounds;
|
||
- connection flow control;
|
||
- server SETTINGS;
|
||
- GOAWAY last-stream handling;
|
||
- `REFUSED_STREAM` retry safety;
|
||
- RST_STREAM;
|
||
- header list limit/HPACK abuse bound;
|
||
- server push disabled;
|
||
- origin coalescing disabled/verified;
|
||
- proxy CONNECT/H2 support;
|
||
- attempt observation.
|
||
|
||
GOAWAY/RST_STREAM을 무조건 safe retry로 보지 않는다. HTTP/2 card는 transmission evidence와
|
||
별도로 다음 processing evidence를 제공한다.
|
||
|
||
```text
|
||
ProcessingEvidence = CONFIRMED_NOT_PROCESSED | MAYBE_PROCESSED | UNKNOWN
|
||
```
|
||
|
||
`REFUSED_STREAM`과 수신한 GOAWAY의 last-stream-ID보다 큰 client stream만 RFC 9113/provider
|
||
contract가 정확히 뒷받침할 때 `CONFIRMED_NOT_PROCESSED`로 올릴 수 있다. 여러 GOAWAY를 받으면
|
||
last-stream-ID가 증가하지 않는지 검증하고 최신 evidence를 단조롭게 반영한다. 그 밖의
|
||
RST_STREAM/GOAWAY, connection loss와 provider가 stream ID를 노출하지 않는 경우는
|
||
`MAYBE_PROCESSED|UNKNOWN`이다. `CONFIRMED_NOT_PROCESSED`만 §16의 `RESTART_CONFIRMED_NOT_PROCESSED` 후보를 만들며,
|
||
protocol-restart `AttemptAuthorizationLease`와 body replayability, shared physical ceiling,
|
||
root-call budget, deadline, operation opt-in을 모두 통과한다. `MAYBE_PROCESSED|UNKNOWN`은 mutation
|
||
reattempt를 만들지 않는다. HTTP/2 provider가 이 evidence를 제공하지 못하면 mutation replay를
|
||
허용하지 않는다.
|
||
|
||
### 30.3 Downgrade
|
||
|
||
```text
|
||
H1_ONLY
|
||
NEGOTIATE_H2_H1
|
||
H2_REQUIRED
|
||
```
|
||
|
||
를 구분한다.
|
||
|
||
- `H2_REQUIRED`가 H1로 떨어지면 startup/readiness 또는 call failure;
|
||
- negotiated protocol metric;
|
||
- proxy/LB별 compatibility;
|
||
- silent downgrade 금지.
|
||
|
||
### 30.4 Connection coalescing
|
||
|
||
HTTP/2가 certificate와 DNS 조건상 여러 origin을 한 connection으로 합칠 수 있어도 default
|
||
금지한다.
|
||
|
||
- auth/tenant/mTLS isolation;
|
||
- DNS/address policy;
|
||
- destination metrics;
|
||
- circuit breaker;
|
||
- certificate SAN;
|
||
|
||
이 섞일 수 있기 때문이다. Opt-in은 별도 security/performance evidence가 필요하다.
|
||
|
||
### 30.5 HTTP/3
|
||
|
||
R2 baseline 제외, R3 candidate.
|
||
|
||
- QUIC/UDP egress;
|
||
- certificate/ALPN;
|
||
- connection migration;
|
||
- stream capacity;
|
||
- retry/token;
|
||
- load balancer;
|
||
- observability;
|
||
- 0-RTT replay.
|
||
|
||
Mutation과 credential-bearing request에 0-RTT를 허용하지 않는다. HTTP/3를 boolean 하나로
|
||
HTTP/2 profile에 추가하지 않는다.
|
||
|
||
## 31. Observability와 privacy
|
||
|
||
### 31.1 관측 단위
|
||
|
||
HTTP client는 logical call과 physical attempt를 분리한다.
|
||
|
||
| 단위 | 의미 | 기본 telemetry |
|
||
| --- | --- | --- |
|
||
| logical call | application port 호출 한 번 | application/internal span 또는 logical timer |
|
||
| physical attempt | 실제 wire request 한 번 | HTTP CLIENT span, 표준 HTTP client metric |
|
||
| admission wait | logical/physical bulkhead 진입 대기 | queue timer/gauge |
|
||
| pool lease | connection/stream capacity 대기 | pool acquire timer/gauge |
|
||
| retry delay | backoff와 `Retry-After` 대기 | retry delay timer |
|
||
| reconciliation | `INDETERMINATE` 후 상태 확인 | 별도 operation/span |
|
||
|
||
한 physical attempt를 두 개의 CLIENT span으로 감싸지 않는다. Spring/engine instrumentation이
|
||
CLIENT span을 만들면 adapter는 같은 attempt에 다른 CLIENT span을 추가하지 않는다. Logical call
|
||
span이 필요하면 kind를 `INTERNAL`로 두고 이름과 속성에서 attempt span과 구분한다.
|
||
|
||
예외는 attacker-controlled authority 때문에 §24.6이 표준 HTTP instrumentation 억제를 요구하는
|
||
`httpclient-untrusted-url-fetch`다. 이 card는 fixed bounded INTERNAL/project telemetry만 사용하며
|
||
confidential exact profile 없이는 CLIENT `server.address|url.full`을 만들지 않는다.
|
||
|
||
권장 span 구조:
|
||
|
||
```text
|
||
feature use-case span
|
||
└─ http.logical <destination>/<operation> // optional INTERNAL
|
||
├─ HTTP <method> physical attempt 0 // CLIENT
|
||
├─ retry wait
|
||
├─ HTTP <method> physical attempt 1 // CLIENT
|
||
└─ reconcile <destination>/<operation> // separate call when required
|
||
```
|
||
|
||
Manual retry, redirect, challenge replay가 새 wire request를 만들면 OTel HTTP semantic convention의
|
||
`http.request.resend_count`를 실제 재전송 횟수로 기록한다. Logical call ID나 idempotency key를
|
||
span attribute로 원문 기록하지 않는다.
|
||
|
||
### 31.2 Trace propagation 단일 소유자
|
||
|
||
Propagation은 application MDC가 아니라 OTel instrumentation이 단독 소유한다.
|
||
|
||
- `traceparent`와 `tracestate`는 current Observation/Context에서 생성한다.
|
||
- 기존 MDC `trace_id`, `span_id` 문자열로 새 `traceparent`를 조립하지 않는다.
|
||
- sampling flag를 하드코딩하지 않는다.
|
||
- B3와 W3C를 동시에 보내지 않는다.
|
||
- destination별 propagation policy가 `deny`면 trace header도 보내지 않는다.
|
||
- untrusted public fetch에는 baggage와 tenant/correlation header를 보내지 않는다.
|
||
|
||
`docs/registries/headers.yaml`의 `traceparent`/`tracestate` 선언은 “허용된 표준 header”를
|
||
뜻하며 custom writer 소유권을 뜻하지 않도록 registry 설명을 수정한다. 현재
|
||
`TraceContextPropagationInterceptor`는 구현 migration에서 제거한다.
|
||
|
||
### 31.3 Baggage와 correlation
|
||
|
||
기본 정책:
|
||
|
||
| Metadata | 내부 allowlisted destination | 외부 partner | untrusted URL |
|
||
| --- | --- | --- | --- |
|
||
| W3C trace context | opt-in/default allow | destination opt-in | deny |
|
||
| correlation ID | explicit allowlist | explicit contract일 때만 | deny |
|
||
| request ID | explicit allowlist | default deny | deny |
|
||
| tenant ID | destination + operation allowlist | default deny | deny |
|
||
| user principal | 금지 | 금지 | 금지 |
|
||
| auth credential | auth policy가 생성 | auth policy가 생성 | 금지 |
|
||
|
||
`docs/registries/mdc-keys.yaml`의 `tenant_id.propagation=[http,...]`는 모든 HTTP 요청으로의
|
||
무조건 전파가 아니다. HTTP client policy가 destination/operation allowlist를 적용한다는 설명과
|
||
tenant leakage contract test를 registry migration에 함께 반영한다.
|
||
|
||
### 31.4 Metrics
|
||
|
||
표준 physical-attempt metric과 project logical-call metric을 분리한다.
|
||
|
||
| Metric | 단위 | 의미 |
|
||
| --- | --- | --- |
|
||
| `http.client.request.duration` | seconds | 표준 physical attempt duration |
|
||
| `dependency.client.requests` | seconds | 기존 registry 호환 logical call timer |
|
||
| proposed `http.client.logical.attempts` | count | logical call당 wire attempt 수 |
|
||
| proposed `http.client.admission.duration` | seconds | logical/physical admission wait |
|
||
| proposed `http.client.pool.acquire.duration` | seconds | lease wait |
|
||
| proposed `http.client.pool.connections` | connections | leased/idle/pending/max 상태 |
|
||
| proposed `http.client.retry.delay` | seconds | 실제 backoff/Retry-After |
|
||
| proposed `http.client.failures` | count | stable failure code/stage |
|
||
| proposed `http.client.body.size` | bytes | direction + wire/decoded class별 bounded distribution |
|
||
| proposed `http.client.generations` | count | active/draining client generation |
|
||
|
||
`proposed` metric은 먼저 `docs/registries/metrics.yaml`에 schema, tag cardinality, owner,
|
||
required test를 등록한 뒤 구현한다. 문서에 이름만 있고 meter가 없는 phantom metric을 만들지
|
||
않는다.
|
||
|
||
현재 `dependency.client.requests`는 다음 의미로 유지한다.
|
||
|
||
- 한 logical call당 정확히 한 번;
|
||
- retry가 성공해도 attempt 수만큼 중복 기록하지 않음;
|
||
- `dependency_name=<destination-id>`;
|
||
- `dependency_type=http`;
|
||
- `outcome=SUCCESS|FAILURE|CIRCUIT_OPEN|TIMEOUT|REJECTED`;
|
||
- 세부 failure stage/code는 별도 bounded metric 또는 log/span;
|
||
- duration에는 admission부터 body close/release까지 포함.
|
||
|
||
OTel HTTP metric과 compatibility metric이 같은 현상을 다른 단위로 세는 것을 dashboard에서
|
||
명시한다. 둘을 합산하지 않는다.
|
||
|
||
### 31.5 허용 tag와 금지 tag
|
||
|
||
허용되는 low-cardinality dimension:
|
||
|
||
- `destination_id`;
|
||
- `operation_id` 또는 bounded operation group;
|
||
- normalized method;
|
||
- status class, not arbitrary status text;
|
||
- stable outcome/failure code/stage;
|
||
- negotiated protocol;
|
||
- provider ID;
|
||
- `generation_role=active|draining|quarantined`와
|
||
`rotation_outcome=success|failed|revoked` 같은 bounded enum;
|
||
- checked-in stable pool isolation ID/role; host, fingerprint, credential/TLS generation에서 동적
|
||
파생 금지;
|
||
- logical/physical scope.
|
||
|
||
표준 OTel metric은 semantic convention이 요구하는 `http.request.method`, registered
|
||
`server.address`, `server.port`, bounded `error.type/status`를 사용한다. `server.address`는
|
||
runtime Host header나 user input이 아니라 validated fixed destination registry에서 나온 값이어야
|
||
한다. Project-specific metric은 destination/operation ID를 사용한다.
|
||
|
||
금지:
|
||
|
||
- raw URI, resolved IP, path variable, query;
|
||
- arbitrary host or redirect location;
|
||
- request/response header value;
|
||
- body, error body, exception message;
|
||
- token, API key, cookie, certificate subject 전체;
|
||
- tenant/user/customer/order/file ID;
|
||
- idempotency key, logical call ID;
|
||
- unbounded exception class/package.
|
||
|
||
Operation ID와 destination ID는 checked-in registry에 존재하는 값만 meter tag로 사용할 수 있다.
|
||
Unknown 값은 호출 전에 거절하므로 `unknown-<raw>` 같은 동적 tag를 만들지 않는다.
|
||
|
||
위 금지 목록은 metric tag와 application log에 대한 규칙이다. OTel CLIENT span은 standard가
|
||
요구하는 `url.full`, `server.address`, `server.port`를 다룰 수 있어 별도 sanitizer contract를
|
||
둔다.
|
||
|
||
- user-info는 URI validation 단계에서 금지;
|
||
- query value는 모두 제거하거나 allowlisted non-sensitive key만 값 없이 남김;
|
||
- path variable은 operation template에 따라 `REDACTED`로 치환;
|
||
- fixed scheme/registered authority는 보존;
|
||
- `url.template`이 selected instrumentation에서 지원되면 low-cardinality template을 추가;
|
||
- raw header capture는 off;
|
||
- untrusted URL fetch는 별도 privacy profile/egress trace policy;
|
||
- actual credential/TLS generation ID는 metric에 넣지 않고 sanitized descriptor/rotation audit에만
|
||
기록;
|
||
- `network.peer.address`는 trust-zone별 explicit opt-in과 secure telemetry pipeline이 있을 때만 span에
|
||
기록하며 metric/log tag와 external/untrusted profile에서는 금지;
|
||
- explicit caller cancellation은 dependency error로 세지 않고 OTel status를 unset으로 유지하며
|
||
bounded cancellation outcome만 logical telemetry에 기록.
|
||
|
||
Sanitizer는 exporter 후처리가 아니라 span attribute 생성 경계에 위치한다. Raw path/query/header와
|
||
unsanitized absolute URL을 OTel SDK, processor, sampler 또는 exporter에 한 번도 전달하지 않는다.
|
||
In-memory SDK/exporter test는 forbidden fixture 값이 생성된 attribute/event/link 전체에 0회 존재함을
|
||
검증한다.
|
||
|
||
Semantic convention 준수와 privacy가 충돌하는 selected instrumentation/version이면 조용히 raw
|
||
URL을 내보내지 않는다. Sanitized absolute URL contract를 구현하거나 해당 provider/card를
|
||
release-eligible로 승격하지 않고 deviation을 descriptor에 명시한다.
|
||
|
||
### 31.6 Resilience4j metric ownership
|
||
|
||
현재 adapter가 설치하는 global `MeterFilter.DENY`는 제거 대상이다. 한 capability가 application
|
||
전체의 `resilience4j.*` meter를 차단하면 Redis, messaging 등 다른 capability의 관측 계약을
|
||
깨뜨릴 수 있다.
|
||
|
||
두 선택지 중 하나를 composition root에서 명시한다.
|
||
|
||
1. registry가 허용한 Resilience4j meter를 bounded name/tag로 등록한다.
|
||
2. native binder를 사용하지 않고 HTTP adapter가 semantic logical/physical metric만 직접
|
||
기록한다.
|
||
|
||
초기 구현은 2를 권장한다. Circuit breaker instance name에는
|
||
`<destination-id>/<policy-id>`만 사용하고 operation/cardinality를 무제한 확장하지 않는다.
|
||
|
||
### 31.7 Log policy
|
||
|
||
한 logical call의 최종 failure는 한 번만 structured log로 남긴다. 개별 attempt는 기본적으로
|
||
span/metric이고, debug sampling이나 security audit 사유가 있을 때만 log한다.
|
||
|
||
필수 필드:
|
||
|
||
```text
|
||
event=http_client_call_completed
|
||
destination_id
|
||
operation_id
|
||
outcome
|
||
failure_code?
|
||
failure_stage?
|
||
attempt_count
|
||
duration_ms
|
||
http_status_class?
|
||
protocol?
|
||
policy_revision
|
||
```
|
||
|
||
금지:
|
||
|
||
- `Throwable#getMessage()` 직접 출력;
|
||
- URL/query/header/body dump;
|
||
- Authorization/cookie/idempotency key;
|
||
- redirect location 원문;
|
||
- resolved IP 원문을 일반 application log에 기록;
|
||
- TLS certificate 원문;
|
||
- OAuth token endpoint response.
|
||
|
||
예외는 stable class category와 sanitized code로 변환한다. Stack trace는 unexpected internal
|
||
defect에만 보안 필터를 거쳐 제한적으로 남기며, partner response body를 exception message에
|
||
포함하지 않는다.
|
||
|
||
### 31.8 Sampling
|
||
|
||
- failure와 `INDETERMINATE` span은 tail-sampling 후보;
|
||
- 성공 request의 high-volume span은 deployment sampling policy 적용;
|
||
- credential/PII가 들어갈 수 있는 event/body는 sampling 여부와 무관하게 기록 금지;
|
||
- attempt 수와 retry delay metric은 trace sampling과 무관하게 집계;
|
||
- debug wire logging은 production에서 불허;
|
||
- provider library의 header/body logger도 startup validation으로 비활성 확인.
|
||
|
||
### 31.9 Dashboard와 alert
|
||
|
||
Universal 임계값을 설계 문서에 하드코딩하지 않는다. Destination별 checked-in SLO가 다음
|
||
signal과 연결되어야 한다.
|
||
|
||
- logical success/availability;
|
||
- physical attempt amplification;
|
||
- timeout stage distribution;
|
||
- p50/p95/p99 logical/attempt latency;
|
||
- admission/pool wait와 saturation;
|
||
- circuit state/half-open result;
|
||
- DNS/TLS/auth failure;
|
||
- decoded-size/truncation reject;
|
||
- `INDETERMINATE` mutation count와 reconciliation age;
|
||
- readiness state와 client generation drain;
|
||
- retry budget exhaustion.
|
||
|
||
Alert는 traffic이 없는 상태와 100% 성공을 구분하고, optional dependency 장애를 application
|
||
liveness failure로 바꾸지 않는다.
|
||
|
||
## 32. Health, readiness와 SLO
|
||
|
||
### 32.1 Liveness
|
||
|
||
Liveness는 외부 destination을 호출하지 않는다.
|
||
|
||
- engine thread/executor 자체 deadlock을 외부 probe로 고치지 않는다;
|
||
- partner outage 때문에 pod를 반복 재시작하지 않는다;
|
||
- pool saturation도 liveness failure가 아니라 dependency/resource alert다;
|
||
- process가 자체 health endpoint에 응답 가능한지와 fatal internal state만 본다.
|
||
|
||
### 32.2 Startup validation
|
||
|
||
Startup 단계에서 network business call 없이 다음을 검증한다.
|
||
|
||
- binding이 존재하는 destination/provider/card가 registry에 존재;
|
||
- operation catalog의 destination과 binding이 일치;
|
||
- URI scheme/host/port와 SSRF policy;
|
||
- timeout/pool/body/retry cross-field invariant;
|
||
- auth/TLS/proxy/DNS profile reference;
|
||
- secret reference 해석 가능성과 최소 metadata;
|
||
- ACTIVE에서 derived selected card와 exact compatibility profile이 각 registry상 모두
|
||
`release-eligible`;
|
||
- OTel propagation owner가 하나;
|
||
- hidden redirect/retry/cookie가 disabled;
|
||
- no binding이면 resource/bean 생성이 없음.
|
||
|
||
TLS key material parsing이나 local trust-store load는 startup에 포함할 수 있다. 실제 remote
|
||
handshake는 readiness/evidence probe다.
|
||
|
||
### 32.3 Readiness impact
|
||
|
||
Destination마다 다음 impact 중 하나를 선언한다.
|
||
|
||
```text
|
||
REQUIRED_FOR_ALL_TRAFFIC
|
||
REQUIRED_FOR_CAPABILITY
|
||
OPTIONAL
|
||
```
|
||
|
||
의미:
|
||
|
||
| Impact | Remote failure 시 |
|
||
| --- | --- |
|
||
| `REQUIRED_FOR_ALL_TRAFFIC` | 충분한 debounce와 rollout 보호 후 global readiness에 반영 가능 |
|
||
| `REQUIRED_FOR_CAPABILITY` | 해당 use-case routing만 unavailable/degraded, global readiness는 정책에 따름 |
|
||
| `OPTIONAL` | global readiness 유지, descriptor/alert는 degraded |
|
||
|
||
`REQUIRED_FOR_ALL_TRAFFIC`은 아주 드물게 사용한다. External dependency 한 곳의 장애가 모든
|
||
pod를 동시에 NotReady로 만들어 트래픽 재분배와 재시작 폭주를 유발하지 않도록 최소 failure
|
||
window, success recovery window, stale-result TTL, rollout grace를 둔다.
|
||
|
||
### 32.4 Probe operation
|
||
|
||
Readiness probe는 business mutation을 호출하지 않는다.
|
||
|
||
허용:
|
||
|
||
- documented health endpoint;
|
||
- bounded `HEAD`/`GET` metadata endpoint;
|
||
- TLS/auth handshake까지 포함하는 provider-specific safe probe;
|
||
- service mesh/passive telemetry와 조합한 cached result.
|
||
|
||
금지:
|
||
|
||
- order/payment/notification 생성;
|
||
- unbounded payload download;
|
||
- normal retry budget을 소모하는 aggressive probe;
|
||
- circuit breaker의 normal call 통계를 왜곡하는 probe;
|
||
- pod마다 동기화된 고주기 polling.
|
||
|
||
Probe는 별도 operation ID, bulkhead, rate, breaker를 사용한다. Jitter와 single-flight를 적용하고
|
||
마지막 성공/실패 시각, stale age, failure class를 descriptor로 공개한다.
|
||
|
||
### 32.5 Capability descriptor state
|
||
|
||
```text
|
||
DISABLED
|
||
STARTING
|
||
READY
|
||
DEGRADED
|
||
NOT_READY
|
||
DRAINING
|
||
CLOSED
|
||
```
|
||
|
||
Descriptor는 최소 다음을 포함한다.
|
||
|
||
```text
|
||
capability=http-client
|
||
destination_id
|
||
binding/provider_id
|
||
selected_cards
|
||
compatibility_profile_ids
|
||
provider_maturity
|
||
card_maturities
|
||
compatibility_profile_maturities
|
||
effective_protocol
|
||
policy_revision
|
||
active_generation
|
||
dependency_profile_fingerprints
|
||
readiness_impact
|
||
state
|
||
last_probe_result/age
|
||
```
|
||
|
||
Secret, URI user-info, raw host for dynamic targets, IP, credential generation material은 포함하지
|
||
않는다.
|
||
|
||
### 32.6 SLO와 timeout budget
|
||
|
||
Timeout 값은 “connect 1초가 흔하다” 같은 template 상수로 정하지 않는다.
|
||
|
||
Destination별:
|
||
|
||
```text
|
||
inbound/use-case budget
|
||
> application processing reserve
|
||
+ http logical-call total deadline
|
||
+ response/compensation reserve
|
||
```
|
||
|
||
를 checked-in SLO/profile로 검토한다. Retry p99 amplification과 pool queue까지 포함해 capacity를
|
||
계산한다. Required dependency의 alert threshold는 해당 dependency SLO와 error budget에
|
||
연결한다.
|
||
|
||
## 33. Configuration design
|
||
|
||
2026-07-28 구현 단면은 canonical expected-state/binding/provider map의 strict binding, exact
|
||
provider/destination/code-owned catalog resolution과 `httpclient-static-buffered` card derivation까지
|
||
포함한다. 아래 full provider tuple의 pool/security/TLS/auth 필드는 아직 bind/runtime model로
|
||
구현되지 않았다.
|
||
|
||
### 33.1 Canonical activation shape
|
||
|
||
상위 capability platform과 같은 canonical prefix를 사용한다.
|
||
|
||
```yaml
|
||
ca-skeleton:
|
||
capabilities:
|
||
http-client:
|
||
expected-state: ACTIVE
|
||
bindings:
|
||
partner-catalog: apache-hc5-classic
|
||
|
||
providers:
|
||
http-client:
|
||
apache-hc5-classic:
|
||
destinations:
|
||
partner-catalog:
|
||
base-uri: ${PARTNER_CATALOG_BASE_URI}
|
||
readiness-impact: REQUIRED_FOR_CAPABILITY
|
||
protocol: H1_ONLY
|
||
operation-catalog: partner-catalog-v1
|
||
|
||
timeout:
|
||
total: 2s
|
||
cleanup-reserve: 100ms
|
||
minimum-attempt-budget: 200ms
|
||
pool-acquire: 100ms
|
||
dns: 250ms
|
||
connect: 300ms
|
||
tls-handshake: 500ms
|
||
request-write: 500ms
|
||
response-headers: 1s
|
||
response-idle: 500ms
|
||
|
||
orphan-reaper:
|
||
max-workers: 2
|
||
max-orphans: 8
|
||
max-queued-cleanups: 8
|
||
cleanup-timeout: 10s
|
||
|
||
pool:
|
||
max-total: 64
|
||
max-per-route: 32
|
||
max-pending-acquires: 64
|
||
connection-max-lifetime: 5m
|
||
idle-eviction: 30s
|
||
validate-after-inactivity: 5s
|
||
|
||
admission:
|
||
max-logical-in-flight: 96
|
||
max-physical-in-flight: 48
|
||
max-queued-logical-calls: 32
|
||
|
||
amplification:
|
||
max-protected-physical-attempts-per-logical-call: 2
|
||
max-nested-credential-requests-per-logical-call: 0
|
||
max-reconciliation-requests-per-logical-call: 0
|
||
max-proxy-connect-requests-per-root-call: 0
|
||
max-revocation-http-requests-per-root-call: 0
|
||
max-total-http-request-attempts-per-root-call: 2
|
||
|
||
retry:
|
||
ordinary-max-retries: 1
|
||
initial-backoff: 50ms
|
||
maximum-backoff: 250ms
|
||
jitter-ratio: 0.30
|
||
honor-retry-after: true
|
||
maximum-retry-after: 1s
|
||
retry-budget-ratio: 0.05
|
||
|
||
circuit-breaker:
|
||
policy: partner-read-v1
|
||
|
||
dns:
|
||
policy: fixed-internal-v1
|
||
egress-policy: partner-catalog-fixed-v1
|
||
|
||
tls:
|
||
ssl-bundle: partner-catalog-client
|
||
require-https: true
|
||
|
||
authentication:
|
||
type: none
|
||
|
||
proxy:
|
||
mode: DIRECT_ONLY
|
||
|
||
bounds:
|
||
request-header-bytes: 16KiB
|
||
response-header-bytes: 32KiB
|
||
buffered-request-bytes: 1MiB
|
||
buffered-response-wire-bytes: 2MiB
|
||
buffered-response-decoded-bytes: 4MiB
|
||
```
|
||
|
||
숫자는 schema 예시이며 production universal default가 아니다. 실제 값은 destination SLO,
|
||
provider limit, pod memory/CPU, replica 수, upstream quota를 근거로 승인한다. Secret 값은 직접
|
||
YAML에 쓰지 않는다. 이 예제는 static buffered/safe-read baseline만 표현한다. OAuth2를 선택하는
|
||
예제는 `httpclient-oauth2-client-credentials` maturity가 release-eligible이고 파생 selected set에
|
||
포함된 뒤에만 유효하다. 현재 모든 card/profile이 `not-implemented`이므로 위 ACTIVE 예제는
|
||
target configuration shape일 뿐이며 그대로는 startup/readiness를 통과하지 않는다.
|
||
|
||
### 33.2 Activation SSOT
|
||
|
||
- `capabilities.http-client.bindings`에 destination이 없으면 disabled다.
|
||
- provider destination 정의가 존재하는 것만으로 client를 만들지 않는다.
|
||
- 별도 `enabled` boolean을 두지 않는다.
|
||
- binding은 정확히 하나의 provider를 선택한다.
|
||
- runtime classpath가 provider를 자동 선택하지 않는다.
|
||
- binding 대상 provider/destination이 없으면 startup failure다.
|
||
- destination binding이 하나라도 있으면 resolver가 `httpclient-static-buffered` card를
|
||
자동 요구한다.
|
||
- binding이 0개면 derived selected card도 0개이고 descriptor는 `DISABLED`; R2를 표시하지 않는다.
|
||
- `expected-state=ACTIVE`인데 binding이 0개면 startup/readiness failure다.
|
||
- `expected-state=DISABLED`인데 binding/provider resource/derived card가 하나라도 있으면
|
||
startup/readiness failure다.
|
||
- `DISABLED` 성공은 `DISABLED_VERIFIED`이지 R2 HTTP 성공이 아니다.
|
||
|
||
Selected card는 사람이 여러 위치에서 중복 입력하지 않고 deterministic resolver가 계산한다.
|
||
|
||
```text
|
||
derivedSelectedCards =
|
||
baselineCards(required by active bindings)
|
||
union operationCatalog.requiredCards
|
||
union providerMode.requiredCards
|
||
```
|
||
|
||
Provider-mode derivation 예:
|
||
|
||
| Effective mode | Required card |
|
||
| --- | --- |
|
||
| any active fixed destination | `httpclient-static-buffered` |
|
||
| bounded binary body | `httpclient-static-buffered` + binary conditional scenarios |
|
||
| API key/static bearer | `httpclient-static-buffered` + exact auth conditional scenarios |
|
||
| custom server trust | `httpclient-static-buffered` + exact TLS conditional scenarios |
|
||
| idempotent/keyed mutation operation | `httpclient-idempotent-mutation` |
|
||
| non-retryable mutation operation | `httpclient-non-retryable-mutation` |
|
||
| response stream callback | `httpclient-streaming-download` |
|
||
| reopenable/one-shot upload | `httpclient-streaming-upload` |
|
||
| client certificate | `httpclient-mtls` |
|
||
| OAuth2 client credentials | `httpclient-oauth2-client-credentials` |
|
||
| required egress proxy | `httpclient-egress-proxy` |
|
||
| H2 negotiated/required | `httpclient-http2` |
|
||
| dynamic public fetch | `httpclient-untrusted-url-fetch` |
|
||
| redirect-follow, request-signature 또는 HTTP Basic | canonical card 없음 -> startup/readiness failure |
|
||
|
||
Resolver는 card ID나 mode 축별 합집합만 검사하지 않는다. Active operation마다 전체 tuple을
|
||
만든다.
|
||
|
||
```text
|
||
EffectiveHttpProfileTuple(
|
||
providerId,
|
||
resolvedProviderArtifactVersion,
|
||
jdkMajor,
|
||
protocol,
|
||
requestBodyMode,
|
||
responseBodyMode,
|
||
requestCodecMediaMode,
|
||
responseCodecMediaEncodingMode,
|
||
tlsMode,
|
||
authPurpose,
|
||
authMode,
|
||
proxyMode,
|
||
redirectMode,
|
||
dnsAddressMode,
|
||
egressTrustZone,
|
||
resilienceMode,
|
||
operationSemantics,
|
||
operationPolicyRevision,
|
||
dependencyProfileFingerprints,
|
||
effectiveBehaviorDigest
|
||
)
|
||
```
|
||
|
||
```text
|
||
for each active operation:
|
||
tuple = resolveExactEffectiveTuple(binding, provider, operation)
|
||
compatibility = profileCompatibilityRegistry.exactMatch(tuple)
|
||
require compatibility.requiredCards == cardsRequiredByRules(tuple)
|
||
requiredScenarioSet(tuple) =
|
||
union(compatibility.requiredCards.baseScenarioIds)
|
||
union compatibility.requiredScenarioIds
|
||
union compatibility.interactionScenarioIds
|
||
require resolvedScenarioIds(tuple) == requiredScenarioSet(tuple)
|
||
|
||
derivedSelectedCards = union(compatibility.requiredCards)
|
||
```
|
||
|
||
`exactMatch`는 wildcard, 축별 union, “각 축에서 하나씩 지원됨”을 허용하지 않는다. 예를 들어
|
||
API-key + buffered를 증명한 card와 auth-none + streaming을 증명한 card가 각각 있어도
|
||
API-key + streaming 전체 tuple entry와 interaction scenario가 없으면 실패한다. 동일 destination의
|
||
여러 operation은 각자 tuple을 통과하고 selected card는 그 결과의 union이다.
|
||
|
||
`effectiveBehaviorDigest`는 secret/raw endpoint를 제외한 validated immutable settings와 operation
|
||
descriptor의 canonical serialization을 SHA-256한 값이다. Timeout/pool/bounds/compression,
|
||
DNS/egress, codec/media, retry/resilience처럼 tuple의 readable dimension 밖에서 behavior를 바꾸는
|
||
필드도 digest를 바꾼다. OAuth token endpoint, proxy와 named revocation responder 같은 child
|
||
outbound dependency의 exact compatibility-profile fingerprint도 정렬된
|
||
`dependencyProfileFingerprints`와 digest 입력에 포함한다. Behavior schema revision이나 필드가
|
||
추가되면 canonical serializer와 compatibility entry를 함께 갱신하며 unknown field는 실패한다.
|
||
Dependency DAG cycle/self-reference와 child maturity gap은 resource 생성 전에 실패한다.
|
||
|
||
Release assertion registry는 expected selected card와 compatibility-profile ID를 assertion으로
|
||
둘 수 있으나 selector가 아니다. Assertion과 derived set이 byte-for-byte 다르면 실패한다.
|
||
Destination provider definition에
|
||
`readiness-cards`를 반복하지 않는다.
|
||
|
||
### 33.3 Typed settings
|
||
|
||
Spring binding model과 validated immutable runtime model을 분리한다.
|
||
|
||
```text
|
||
@ConfigurationProperties(
|
||
prefix = "ca-skeleton.capabilities.http-client",
|
||
ignoreUnknownFields = false)
|
||
HttpClientCapabilitySelectionProperties
|
||
ExpectedCapabilityState expectedState
|
||
Map<DestinationId, ProviderId> bindings
|
||
|
||
@ConfigurationProperties(
|
||
prefix = "ca-skeleton.providers.http-client",
|
||
ignoreUnknownFields = false)
|
||
HttpClientProviderProperties
|
||
Map<ProviderId, HttpProviderDefinitionProperties> providers
|
||
|
||
HttpDestinationSettingsFactory
|
||
binding + provider properties + operation catalog + card/compatibility registries
|
||
-> ValidatedHttpDestinationSettings
|
||
```
|
||
|
||
Runtime settings에는 raw mutable map을 남기지 않는다. `URI`, `Duration`, byte-size, enum,
|
||
validated ID, sealed auth/proxy/DNS/TLS policy로 변환한 뒤 client를 생성한다.
|
||
|
||
### 33.4 Cross-field validation
|
||
|
||
최소 startup failure 조건:
|
||
|
||
1. unknown destination/provider/card/operation catalog;
|
||
2. duplicate normalized ID;
|
||
3. binding과 provider destination 불일치;
|
||
4. absolute/relative URI invariant 위반;
|
||
5. production profile의 plain HTTP;
|
||
6. URI user-info, fragment, unsafe port;
|
||
7. timeout이 zero/negative/infinite, `cleanup-reserve >= total`, 또는 execution cutoff보다 큰
|
||
phase cap;
|
||
8. pool per-route가 total보다 큼;
|
||
9. pending acquire/admission queue가 unbounded;
|
||
10. response decoded cap이 hard process cap보다 큼;
|
||
11. compression enabled인데 wire/decoded cap 또는 ratio cap 없음;
|
||
12. ordinary retry가 1회 이상인데 retry budget/backoff upper bound 없음;
|
||
13. operation catalog보다 느슨한 config retry/redirect/header/body policy;
|
||
14. mutation semantics cross-field invariant 위반:
|
||
- `KEYED_MUTATION`: idempotency key/fingerprint/replay window/reconciliation 없음;
|
||
- `IDEMPOTENT_MUTATION`: authoritative same-intent semantics 또는 indeterminate fallback 없음;
|
||
- `NON_RETRYABLE_MUTATION`: protected physical ceiling=1/no auth·redirect replay/transmission
|
||
evidence-to-receipt 없음;
|
||
- non-retryable `NOT_SENT` restart opt-in인데 protected physical ceiling>2, body non-reopenable,
|
||
exact evidence/restart-policy/budget/scenario 없음;
|
||
15. streaming card인데 callback close/cancel evidence 없음;
|
||
16. mTLS card인데 client key/trust bundle 없음;
|
||
17. OAuth2 card인데 registration/ref와 token-call isolation 없음;
|
||
18. proxy required인데 direct fallback 허용;
|
||
19. HTTP/2 card인데 provider/proxy/TLS profile가 미지원;
|
||
20. multiple auth subtype 또는 auth type과 nested fields 불일치;
|
||
21. TLS hostname verification disabled;
|
||
22. provider hidden redirect/retry/auth/protocol resend/cookie가 enabled 또는 pre-start kernel
|
||
authorization을 우회;
|
||
23. derived selected card 또는 exact compatibility profile maturity가 `release-eligible`이 아님;
|
||
24. same canonical + legacy key 동시 사용;
|
||
25. secret literal처럼 보이는 credential 값;
|
||
26. orphan reaper worker/queue/count/timeout이 finite positive가 아니거나 physical capacity보다
|
||
큰 orphan을 허용;
|
||
27. `minimumAttemptBudget + cleanupReserve > totalDeadline`;
|
||
28. active operation의 full effective profile tuple이 compatibility registry에 exact match되지
|
||
않거나 required-card set이 requirement rules와 다르거나
|
||
`card base ∪ compatibility required ∪ compatibility interaction` scenario set이 완전
|
||
일치하지 않음;
|
||
29. redirect-follow, request-signature 또는 HTTP Basic처럼 canonical card가 없는 mode가 활성화됨;
|
||
30. protected/nested-credential/reconciliation/proxy/revocation/root-call HTTP amplification
|
||
상한이 finite가 아니거나 child HTTP request를 root budget에 포함하지 않음, 또는 exact profile의
|
||
cold OAuth/401/proxy/revocation `minimumRequiredRootHttpAttempts`보다 root ceiling이 작음;
|
||
31. OAuth token endpoint가 exact `OAUTH_TOKEN_ENDPOINT` purpose/client-auth mode child tuple이
|
||
아니거나 OAuth 재귀/cycle/self-reference, child maturity/fingerprint/owner lifecycle 누락;
|
||
32. proxy가 exact named child profile이 아니거나 ambient/direct fallback, proxy credential tunnel
|
||
leakage policy 누락;
|
||
33. network revocation lookup이 named responder 없이 certificate-directed URI/JVM implicit discovery를
|
||
활성화하거나 SSRF/deadline/size/concurrency/effective-property 검증 누락;
|
||
34. emergency revocation profile이 revoked-generation admission/fallback을 허용하거나 replacement 전
|
||
`NOT_READY`를 보장하지 않음.
|
||
|
||
### 33.5 Operation policy와 configuration의 관계
|
||
|
||
Operation safety는 code-reviewed catalog가 상한이다.
|
||
|
||
Configuration이 가능한 것:
|
||
|
||
- ordinary retry와 protected/root physical-attempt 상한 감소;
|
||
- total/phase timeout 감소;
|
||
- body/header cap 감소;
|
||
- allowed status/media type 축소;
|
||
- HTTP/2를 H1로 축소;
|
||
- optional propagation 제거;
|
||
- readiness impact를 더 보수적으로 변경.
|
||
|
||
Configuration이 불가능한 것:
|
||
|
||
- non-idempotent operation을 retry-safe로 승격;
|
||
- one-shot body를 replayable로 선언;
|
||
- absolute dynamic URL 허용;
|
||
- new header/credential propagation 추가;
|
||
- redirect host 확대;
|
||
- response cap 확대해 hard limit 우회;
|
||
- card가 증명하지 않은 protocol/auth/body mode 활성화.
|
||
|
||
확장이 필요하면 catalog와 readiness evidence를 같이 변경한다.
|
||
|
||
HTTP/2 축소는 operation/provider가 `NEGOTIATE_H2_H1`을 허용할 때만 가능하다.
|
||
`H2_REQUIRED`를 H1로 낮추는 configuration은 startup failure다.
|
||
|
||
### 33.6 Environment key
|
||
|
||
Flattened environment key는 canonical properties에서 기계적으로 파생한다. Destination ID를
|
||
환경 변수 key에 직접 넣어 동적 key 폭증을 만들기보다 environment-specific checked-in YAML과
|
||
secret reference를 사용한다. 꼭 필요한 scalar override만 `verifyEnvKeys` registry에 등록한다.
|
||
|
||
Base URI, proxy endpoint, SSL bundle/secret reference 변경은 운영 영향이 있으므로:
|
||
|
||
- old/new sanitized descriptor diff;
|
||
- rollout strategy;
|
||
- readiness probe;
|
||
- rollback generation;
|
||
|
||
을 요구한다.
|
||
|
||
### 33.7 Legacy migration
|
||
|
||
`app.outbound.http.*`는 canonical application configuration에 포함되지 않는 migration-only
|
||
입력이다.
|
||
|
||
현재 구현은 global `@ConfigurationPropertiesScan`을 제거하고
|
||
`OutboundHttpSettings.bindLegacy(Binder)`/직접 생성자만 남겼다. Canonical composition은
|
||
expected state가 DISABLED여도 legacy property가 하나라도 보이면 silent no-op 대신
|
||
fail-closed한다. Legacy fork는 canonical composition 밖에서 migration binder와 configuration을
|
||
명시적으로 import해야 한다. 아래 deprecation warning, one-destination conversion,
|
||
release-window removal은 후속 migration 단계다.
|
||
|
||
1. legacy만 있으면 deprecation warning과 함께 immutable legacy settings로 변환;
|
||
2. canonical과 legacy가 동시에 있으면 값이 같아도 startup failure;
|
||
3. legacy global settings는 한 destination 외에는 사용할 수 없음;
|
||
4. retry/total-deadline 등 보장하지 못하는 legacy field를 canonical 보장으로 과장하지 않음;
|
||
5. 한 release window 뒤 alias 제거;
|
||
6. 제거 전 configuration migration test와 release note 제공.
|
||
|
||
## 34. Composition, activation과 lifecycle
|
||
|
||
### 34.1 Composition root
|
||
|
||
`app-bootstrap`만 다음을 수행한다.
|
||
|
||
1. canonical binding resolve;
|
||
2. selected destination settings validation;
|
||
3. provider factory 선택;
|
||
4. credential/TLS/DNS/proxy collaborator 주입;
|
||
5. client generation 생성;
|
||
6. feature-specific application port adapter wiring;
|
||
7. descriptor/health/lifecycle 등록.
|
||
|
||
HTTP adapter가 component scan만으로 모든 provider와 client를 자가 활성화하지 않는다.
|
||
Application은 adapter type, `RestClient`, Apache type을 알지 못한다.
|
||
|
||
### 34.2 Zero-binding contract
|
||
|
||
이 절의 resource 0 계약은 `HttpClientCompositionConfigTest`와
|
||
`OptionalAdapterBeanGatingTest`로 구현됐다. 기본 composition은 inert registry/resolver/descriptor
|
||
외에 HTTP runtime bean을 만들지 않으며 `DISABLED_VERIFIED`만 게시한다.
|
||
|
||
Binding이 없으면 다음이 모두 0개여야 한다.
|
||
|
||
- engine client와 connection manager;
|
||
- pool evictor/reaper;
|
||
- DNS resolver thread/cache;
|
||
- scheduler/executor/virtual-thread owner;
|
||
- TLS/secret file watcher;
|
||
- OAuth token refresh/cache;
|
||
- readiness probe;
|
||
- circuit breaker/retry registry entry;
|
||
- HTTP capability health contributor;
|
||
- generic default HTTP bean;
|
||
- background task.
|
||
|
||
Classpath presence만으로 `RestClient`, provider connection manager 또는 health probe가 생성되면
|
||
composition contract failure다.
|
||
|
||
### 34.3 Start order
|
||
|
||
```text
|
||
bind + validate settings
|
||
-> load local TLS/auth metadata
|
||
-> create engine generation
|
||
-> verify effective engine options
|
||
-> register bounded telemetry
|
||
-> run selected safe startup/readiness evidence
|
||
-> publish destination adapters
|
||
-> allow ingress readiness
|
||
```
|
||
|
||
Optional destination가 unavailable이면 policy에 따라 `DEGRADED`로 시작할 수 있다. Required
|
||
destination의 initial probe failure 처리에는 rollout grace와 cached state가 적용된다.
|
||
|
||
### 34.4 Drain order
|
||
|
||
현재 shutdown guard처럼 “outbound를 제일 먼저 닫는” 방식은 in-flight inbound request를 깨뜨린다.
|
||
종료 coordination은 상대적 순서를 명시한다.
|
||
|
||
```text
|
||
ACTIVE
|
||
-> DRAIN_REQUESTED
|
||
stop accepting new ingress
|
||
stop scheduled/background producers
|
||
already accepted request token may still start outbound calls
|
||
-> INGRESS_DRAINED_OR_GRACE_EXPIRED
|
||
-> OUTBOUND_ADMISSION_CLOSED
|
||
reject all new logical calls
|
||
wait active logical calls/streams
|
||
-> CANCEL_REMAINING
|
||
cancel request/response handles
|
||
-> CLOSE_RESOURCES
|
||
close pool/client/executor/resolver/watchers
|
||
-> CLOSED
|
||
```
|
||
|
||
Spring `SmartLifecycle` phase 숫자를 이 문서에서 임의로 고정하지 않는다. Bootstrap의 ingress/
|
||
background/outbound drain coordinator가 위 partial order를 contract test로 증명한 뒤 숫자를
|
||
배정한다. 현재 `MAX_VALUE` guard는 이 순서를 증명하지 못하므로 교체 대상이다.
|
||
|
||
### 34.5 Accepted-request lease
|
||
|
||
Drain 중 허용 대상을 thread name/MDC로 추정하지 않는다. Ingress가 request lease/token을 발급하고
|
||
그 request의 application call chain에 명시적으로 전달한다.
|
||
|
||
- drain 전에 발급된 lease는 grace 안에서 outbound 시작 가능;
|
||
- background job은 별도 producer lease;
|
||
- grace 이후 모든 lease 만료;
|
||
- child async task가 lease lifetime을 무한 연장하지 못함;
|
||
- active lease/call/stream 수가 drain metric에 나타남.
|
||
|
||
### 34.6 Generation swap
|
||
|
||
다음 변경은 in-place mutation이 아니라 immutable generation 교체를 기본으로 한다.
|
||
|
||
- certificate/trust material;
|
||
- credential/token client configuration;
|
||
- base endpoint/proxy;
|
||
- DNS policy;
|
||
- pool/protocol settings;
|
||
- operation policy revision.
|
||
|
||
절차:
|
||
|
||
```text
|
||
load new material
|
||
-> build generation N+1
|
||
-> local validation + safe probe
|
||
-> atomic new-call routing swap
|
||
-> generation N drain
|
||
-> cancel on generation deadline
|
||
-> close N resources
|
||
```
|
||
|
||
`NORMAL_ROLLOVER`에서만 new generation 검증 실패 시 아직 유효하고 non-revoked인 old
|
||
generation을 유지하고 alert할 수 있다. `EMERGENCY_REVOKE`는 별도 transition이다.
|
||
|
||
```text
|
||
ACTIVE(old)
|
||
-> REVOKE_REQUESTED
|
||
-> old admission blocked + matching sessions/tokens/pool invalidated
|
||
-> optional policy-driven in-flight cancellation
|
||
-> NOT_READY until validated replacement
|
||
-> ACTIVE(new) or CLOSED
|
||
```
|
||
|
||
Revoked generation으로의 fallback/rollback과 old-new overlap은 금지한다. Close가 지연되면 기존
|
||
quarantine/reaper 계약으로 추적하되 new admission을 열지 않는다. “reload supported”를 Spring SSL
|
||
bundle 존재만으로 가정하지 않는다. 실제 selected engine의 live-reload 증거가 없으면 항상
|
||
generation swap을 사용한다.
|
||
|
||
### 34.7 Runtime reconfiguration
|
||
|
||
Arbitrary hot reload는 R2 baseline이 아니다. 지원할 변경마다:
|
||
|
||
- source authenticity;
|
||
- version monotonicity;
|
||
- full validation;
|
||
- generation atomicity;
|
||
- rollback;
|
||
- audit;
|
||
- concurrent call behavior;
|
||
|
||
를 증명한다. 그렇지 않으면 deployment rollout로 변경한다.
|
||
|
||
### 34.8 Engine resource ownership
|
||
|
||
Provider factory는 다음 close handle을 반환한다.
|
||
|
||
```text
|
||
HttpEngineGeneration implements AutoCloseable
|
||
engine
|
||
connection manager
|
||
executor/scheduler
|
||
DNS resolver/cache
|
||
credential/token collaborator
|
||
TLS material handle
|
||
active call/stream registry
|
||
```
|
||
|
||
공유 가능한 executor도 소유자와 reference counting/close order가 명확할 때만 공유한다.
|
||
JDK `HttpClient` handle을 버리고 GC에 lifecycle을 맡기지 않는다.
|
||
|
||
## 35. Security threat model
|
||
|
||
### 35.1 위협과 통제
|
||
|
||
| Threat | Preventive control | Detective/CI evidence |
|
||
| --- | --- | --- |
|
||
| User-controlled SSRF | fixed destination + relative route, scheme/host/port/CIDR policy | URI property tests, internal IP deny tests |
|
||
| DNS rebinding | resolve-validate-connect binding, every answer validation, TTL policy | scripted DNS rebind test |
|
||
| Redirect credential leak | redirect default deny, hop-by-hop state machine, cross-origin credential strip | 30x chain test |
|
||
| Proxy bypass/credential leak | named exact proxy profile, no ambient/direct fallback, hop-only auth | proxy-down/CONNECT raw-capture test |
|
||
| Header injection | typed header values, CR/LF/NUL reject, forbidden header ownership | raw request capture |
|
||
| Request smuggling | strict framing, no conflicting length/transfer encoding, engine hardening | raw byte server tests |
|
||
| Response splitting | strict header parser and count/byte limits | malformed response tests |
|
||
| Decompression bomb | wire + decoded + expansion ratio + time bound | compressed bomb test |
|
||
| Oversized/truncated body | bounded reader/stream, declared length validation, EOF state | lying length/chunk truncation test |
|
||
| TLS downgrade/MITM | HTTPS required, hostname verification, trust policy, protocol floor | wrong host/untrusted CA/old TLS tests |
|
||
| Client-key leakage | secret reference, non-exportable/file permission policy, redacted telemetry | secret scan and log capture |
|
||
| OAuth/API-key leakage | auth owner creates header, no raw caller credential, redirect stripping | capture server and log tests |
|
||
| Duplicate mutation | typed idempotency contract, same key/digest, unknown outcome reconciliation | lost-response replay test |
|
||
| Retry/auth/redirect amplification | reason budgets + protected/nested/root shared physical ceilings, `Retry-After` cap | combined resend property/concurrent outage test |
|
||
| Pool/queue exhaustion | finite admission/pending/pool bounds, active cancellation | saturation/resource test |
|
||
| Slowloris response | response-header and idle/body deadline | drip-feed test |
|
||
| Slow upload sink | write progress/deadline and cancellable producer | no-read server test |
|
||
| Unsafe deserialization | per-operation codec, media type/schema/size/depth limits | malicious payload corpus |
|
||
| Cross-tenant metadata leak | destination/operation propagation allowlist | tenant leakage matrix |
|
||
| Cookie/session bleed | cookie store disabled baseline, destination isolation | sequential identity test |
|
||
| Metric cardinality attack | registry IDs/bounded generation role only, no raw URI/host/status text | cardinality test |
|
||
| Telemetry pre-export leak | sanitize at attribute construction, peer address trust-zone gate | SDK/processor/exporter forbidden-value test |
|
||
| Log injection/secret leak | structured sanitized fields, no throwable message/body | hostile header/body log test |
|
||
| Dependency compromise | lock/checksum/SBOM/vulnerability/license/KEV gate | supply-chain CI |
|
||
| Stale certificate/secret | generation metadata, expiry alert, normal rotation drill | rotation test/runbook |
|
||
| Revoked/compromised material fallback | emergency admission block, session/token/pool invalidate, NOT_READY | emergency revoke/no-fallback test |
|
||
| Certificate-directed SSRF | automatic AIA/CRLDP/implicit OCSP off or named responder egress | malicious certificate zero-side-effect test |
|
||
| HTTP/2 coalescing leak | coalescing disabled/verified, auth/pool isolation | multi-origin H2 test |
|
||
| 0-RTT replay | HTTP/3 excluded; mutations/credentials never 0-RTT | provider policy test |
|
||
|
||
### 35.2 Trust zones
|
||
|
||
Destination registry가 trust zone을 선언한다.
|
||
|
||
```text
|
||
INTERNAL_SERVICE
|
||
TRUSTED_PARTNER
|
||
PUBLIC_FIXED_ORIGIN
|
||
UNTRUSTED_FETCH
|
||
```
|
||
|
||
Zone은 기본 policy bundle을 고르지만 operation catalog보다 권한을 넓히지 않는다.
|
||
`UNTRUSTED_FETCH`는 baseline provider의 mode가 아니라 별도 readiness card/capability다.
|
||
|
||
### 35.3 Dynamic URL fetch 분리
|
||
|
||
Image/PDF preview처럼 user-provided URL이 정말 필요하면 별도 adapter port로 둔다.
|
||
|
||
- public IP만 허용하는 dedicated resolver/egress proxy;
|
||
- redirect hop마다 재검증;
|
||
- credential/cookie/trace/baggage zero;
|
||
- port/scheme allowlist;
|
||
- network policy로 metadata/control-plane/private CIDR 차단;
|
||
- content type sniffing과 decoded cap;
|
||
- sandbox/antivirus/timeout;
|
||
- audit와 abuse rate limit.
|
||
|
||
초기 card `httpclient-untrusted-url-fetch`는 `not-implemented`다. Fixed-destination client에 boolean
|
||
하나로 열 수 없다.
|
||
|
||
### 35.4 Security defaults
|
||
|
||
- TLS/hostname verification on;
|
||
- redirect off;
|
||
- cookies off;
|
||
- engine automatic retry off;
|
||
- raw absolute URL off;
|
||
- arbitrary caller header off;
|
||
- proxy direct fallback off when proxy selected;
|
||
- wire/body logging off;
|
||
- trust-all/hostname-ignore API absent;
|
||
- unbounded buffer/queue absent;
|
||
- hidden auth challenge replay off unless carded;
|
||
- certificate pinning은 일반 default가 아니라 운영 가능한 rotation design이 있을 때만 opt-in.
|
||
|
||
### 35.5 Network policy와 application policy
|
||
|
||
Application SSRF 방어만으로 충분하지 않다. Deployment에는:
|
||
|
||
- destination/proxy egress allowlist;
|
||
- cloud metadata/control-plane deny;
|
||
- DNS egress 제한;
|
||
- service account 최소 권한;
|
||
- proxy access log와 alert;
|
||
- secret volume permission;
|
||
|
||
을 적용한다. 반대로 network policy만 믿고 raw URL을 application에서 허용하지 않는다. 두 층이
|
||
독립적으로 실패를 막는다.
|
||
|
||
## 36. Test strategy
|
||
|
||
### 36.1 Test pyramid
|
||
|
||
| Layer | 목적 | 외부 자원 |
|
||
| --- | --- | --- |
|
||
| pure unit/property | policy, parser, deadline, classification | 없음 |
|
||
| engine contract | 실제 provider wire behavior | loopback fake/raw server |
|
||
| fault integration | TCP/DNS/TLS/proxy/resource fault | pinned local containers |
|
||
| composition | binding/zero-resource/wiring/lifecycle | Spring context |
|
||
| compatibility | upstream schema/protocol fixtures | recorded/generated fixtures, no real partner |
|
||
| load/soak | pool, leak, retry amplification | isolated CI/nightly |
|
||
|
||
실제 인터넷 partner endpoint를 CI에서 호출하지 않는다. Fake server/container는 loopback 또는
|
||
CI private network에만 둔다.
|
||
|
||
### 36.2 Pure unit/property tests
|
||
|
||
최소:
|
||
|
||
- destination/operation ID normalization;
|
||
- relative path encoding과 dot-segment/double-encoding;
|
||
- query multi-value/order/null policy;
|
||
- URI scheme/host/port/CIDR validation;
|
||
- IPv4/IPv6 mapped/obfuscated, NAT64/6to4/Teredo/special-purpose address classification;
|
||
- header CR/LF/NUL, count, byte limit;
|
||
- forbidden header ownership;
|
||
- media type and charset selection;
|
||
- status success/error mapping;
|
||
- `Retry-After` delta/date parsing, past/overflow/cap;
|
||
- retry decision predicate truth table;
|
||
- body replayability/idempotency/reconciliation matrix;
|
||
- `operationSemantics × transmissionEvidence × processingEvidence × cancellationOutcome ×
|
||
responseIntegrity × responseSemanticClass × bodyReplayability` exhaustive disposition matrix;
|
||
- monotonic remaining budget and phase cap;
|
||
- jitter range with deterministic random source;
|
||
- CB record/ignore matrix;
|
||
- decoded/wire byte and ratio accounting;
|
||
- failure taxonomy exhaustive mapping;
|
||
- config cross-field validation;
|
||
- immutable `AllowedChildEdge` exact-match와 wrong-kind/wrong-child/unknown-fingerprint/cross-root lease
|
||
replay가 child/root token 소비와 wire side effect 0으로 거절되는 property;
|
||
- full effective profile tuple exact matching, card-set equality와 interaction scenario derivation;
|
||
- profile별 `minimumRequiredRootHttpAttempts`와 protected/token/reconcile/proxy/revocation cap
|
||
cross-field 계산;
|
||
- log/metric sanitizer/cardinality.
|
||
|
||
Deadline test는 fake monotonic clock와 deterministic scheduler로 정확히 검증하고 wall clock에
|
||
의존하지 않는다.
|
||
|
||
Profile resolver property test는 각 축이 개별 card에서 지원되더라도 full tuple entry가 없으면
|
||
항상 거절한다. Exact entry가 있을 때만 그 entry의 required card set과
|
||
base/conditional/interaction scenario set을 반환하며, 등록되지 않은 cartesian product를
|
||
생성하지 않는다. Validated behavior field 하나를 바꾸면 canonical behavior digest가 바뀌고,
|
||
새 exact compatibility entry/evidence 없이는 실패함을 mutation/property test로 검증한다.
|
||
|
||
### 36.3 HTTP semantic contract
|
||
|
||
`MockWebServer` 같은 programmable loopback server와 필요한 경우 raw socket fixture로:
|
||
|
||
- all declared success status;
|
||
- 3xx default reject;
|
||
- every 4xx/5xx mapping;
|
||
- 204/HEAD no-body behavior;
|
||
- error-body drain/close cap;
|
||
- duplicate headers/trailers;
|
||
- interim 100/103;
|
||
- chunked/fixed/close-delimited framing;
|
||
- malformed status/header/framing;
|
||
- connection reuse after success/error/partial close;
|
||
- keep-alive expiry/stale connection;
|
||
- media type/charset mismatch;
|
||
- gzip/other declared encoding;
|
||
- pagination/ETag/conditional request;
|
||
- provider hidden retry/redirect/cookie disabled.
|
||
|
||
401 stale-credential challenge, 429/503 retry control, declared 3xx와 reconciliation signal은 contract가
|
||
요구한 header/body validation 뒤 공통 cleanup/disposition resolver를 반드시 통과하며
|
||
completed/domain outcome으로 조기 반환되지 않는지 검증한다. Deterministic barrier로
|
||
`VALID_HEADERS_ONLY`는 header 검증 직후 cancellation보다 먼저 response CAS를 이길 수 있고 body
|
||
drain failure가 결과를 덮지 않으며, body-required outcome은 decode/semantic 완료 전 response CAS를
|
||
이기지 못함을 고정한다. Cancellation/failure/response-body callback 경합에서 winner와 cleanup
|
||
finalizer가 각각 정확히 하나이고 loser가 body/connection을 다시 닫지 않는지도 검증한다.
|
||
|
||
`RestClient.exchange()` 경로에는 status handler가 자동 적용된다고 가정하지 않고 callback이 status를
|
||
먼저 분기하는 contract test를 둔다.
|
||
|
||
### 36.4 Deadline와 cancellation
|
||
|
||
Server fault:
|
||
|
||
- accept하지 않음;
|
||
- connect 후 TLS bytes 정지;
|
||
- request body를 읽지 않음;
|
||
- response header를 보내지 않음;
|
||
- body byte를 천천히 drip;
|
||
- retry response 후 긴 `Retry-After`;
|
||
- pool slot을 점유한 채 정지;
|
||
- streaming callback이 정지/예외/조기 반환.
|
||
|
||
모든 경우:
|
||
|
||
1. logical call이 total deadline upper bound + 작은 scheduler tolerance 안에 반환;
|
||
2. execution cutoff에서 active engine request/stream cancel/close를 시작;
|
||
3. cooperative/normal cleanup은 caller deadline `D` 안에 끝나고 connection/permit를 정확히 한
|
||
번 회수 또는 폐기;
|
||
4. uncooperative task는 caller를 붙잡지 않고 `D`에 quarantine되며 reusable pool로 한 번도
|
||
반환되지 않음;
|
||
5. quarantined physical-attempt permit는 실제 task 종료까지 유지되고 orphan count가 새
|
||
admission capacity에 반영됨;
|
||
6. bounded orphan registry/queue/reaper가 `orphanCleanupTimeout` 안에 정리하고, 초과 시
|
||
generation을 `DEGRADED/NOT_READY`로 전환해 새 호출을 거부;
|
||
7. retry/backoff/새 network side effect가 execution cutoff 뒤 시작되지 않음;
|
||
8. 정상 경로에는 background task/thread가 남지 않고 quarantine 경로에는 registry가 추적하지
|
||
않는 task/thread가 남지 않음;
|
||
9. mutation은 transmission phase에 따라 `INDETERMINATE`를 보존.
|
||
|
||
Fake monotonic clock으로 `executionCutoff = D - cleanupReserve`와 caller return upper bound를
|
||
분리 검증한다. Parent deadline이 이미 지난 경우 synchronous cleanup budget 0과 즉시 quarantine
|
||
경로를 검증한다. Thread interrupt, caller cancellation, shutdown cancellation도 별도 테스트한다.
|
||
|
||
Deterministic latch로 backoff, credential/body open, local quota, physical bulkhead, circuit
|
||
permission, pool lease와 DNS wait 각각의 직후 deadline/cancellation을 발생시킨다. 각 경계에서
|
||
후속 resource/network side effect count가 0이고 body/quota/permit lease가 역순으로 회수되는지
|
||
검증한다. Engine은 pool/DNS wait 뒤 cutoff가 지나면 connect/TLS/write를 시작하지 않아야 한다.
|
||
|
||
### 36.5 Pool와 concurrency
|
||
|
||
- max-total/max-per-route 초과 연결 없음;
|
||
- pending acquire/queue finite;
|
||
- queue timeout은 `REJECTED_BEFORE_SEND`;
|
||
- logical bulkhead와 physical bulkhead 독립;
|
||
- virtual thread 수가 pool capacity를 우회하지 않음;
|
||
- canceled waiter가 queue에서 제거;
|
||
- response close 누락 방지;
|
||
- half-open probe가 bounded;
|
||
- two destinations/policies 간 pool isolation;
|
||
- old generation drain 중 new generation 정상;
|
||
- 반복 10k+ 호출 후 connection/thread/file-descriptor/heap 안정.
|
||
|
||
Soak의 exact 호출 수와 시간은 CI budget에 맞추되 leak assertion과 before/after resource delta를
|
||
artifact로 남긴다.
|
||
|
||
### 36.6 Retry, breaker와 mutation
|
||
|
||
Table-driven scenario:
|
||
|
||
| Scenario | Expected |
|
||
| --- | --- |
|
||
| safe GET, connect-before-send failure | bounded retry |
|
||
| safe GET, 503 + valid `Retry-After` | capped wait 후 retry |
|
||
| safe GET, 429 beyond deadline | no retry, stable rate-limit failure |
|
||
| one-shot upload failure | no retry |
|
||
| non-retryable mutation attempt 0 | retry token 없이 한 번 실행 |
|
||
| non-retryable mutation, exact `NOT_SENT`, explicit reopenable policy | pre-send restart budget을 소비해 최대 한 번 추가 실행 |
|
||
| non-retryable mutation, `MAYBE_SENT` 이상 | `INDETERMINATE`, auth/redirect 포함 blind replay 없음 |
|
||
| non-keyed POST, response loss | `INDETERMINATE`, no blind retry |
|
||
| keyed POST, response loss | same key/digest로 inspect/reconcile |
|
||
| keyed POST payload mismatch | local reject |
|
||
| 400/401/403/404 | breaker ignore, default no retry |
|
||
| 500/502/503/504 configured | physical attempt breaker record |
|
||
| codec/oversize/programmer failure | breaker ignore |
|
||
| CB open | no engine/pool acquisition |
|
||
| half-open | exact configured permits |
|
||
| retry + redirect + auth replay 조합 | 사유별 counter와 protected physical ceiling을 넘지 않음 |
|
||
| token refresh 자체 retry + protected replay | nested credential cap과 root-call total ceiling을 넘지 않음 |
|
||
| second stale-token 401 | terminal, no second refresh/replay |
|
||
| caller/deadline/shutdown cancel + exact `NOT_SENT` | typed `RETURN_CANCELLED(reason)` |
|
||
| mutation cancel + `MAYBE_SENT+|UNKNOWN` | `RETURN_INDETERMINATE`, not cancelled/permanent |
|
||
| declared redirect future card | `FOLLOW_DECLARED_REDIRECT`, new hop lease/ordinal/common ceilings |
|
||
| H2 exact not processed + opted-in replayable body | `RESTART_CONFIRMED_NOT_PROCESSED` |
|
||
| H2 maybe/unknown processed | no mutation reattempt |
|
||
|
||
README가 아니라 test에서 `retry -> CB(physical attempt)` 실행 순서와 exact attempt/breaker count를
|
||
검증한다. Initial/retry/pre-send restart/same-intent/auth/redirect/protocol resend 각각이 동일
|
||
`physicalAttemptOrdinal`과 protected/root shared token을 정확히 한 번 소비하는 property test를
|
||
둔다. Engine hidden resend가 pre-start gate를 우회하면 provider qualification이 실패해야 한다.
|
||
Pure eligibility 호출은 모든 counter가 불변이고, 각 후속 disposition에서
|
||
`AttemptAuthorizationLease`가 정확히 한 번 생성·bind/abort되며 auth replay token을 두 번 소비하지
|
||
않는지 검증한다. Final protected/root reservation CAS failure는 engine start/ordinal increment 0,
|
||
reverse cleanup과 authorization abort를 보장한다.
|
||
|
||
Authorization 직후 refresh/hop/protocol revalidation, body open, quota reject, bulkhead/CB acquire,
|
||
credential signing, local preflight와 각 common-gate failure를 하나씩 주입한다. 모든 pre-bind exit에서
|
||
`AttemptAuthorizationLease.abort` exactly once, engine/root commit/ordinal increment 0, 획득 resource
|
||
역순 정리를 검증한다. Reconciliation은 poll wire request마다 정확히 하나의
|
||
`NestedHttpAuthorizationLease(RECONCILIATION)`를 acquire/bind 또는 abort하고, retry를 포함한 실제
|
||
request count가 reconciliation child cap과 shared root ceiling을 넘지 않는지 검증한다.
|
||
|
||
`CircuitPermissionLease` contract는 다음 failure injection마다 terminal callback이 정확히 한
|
||
번인지 검증한다.
|
||
|
||
- `ACQUIRED` 뒤 body open/span creation/local preflight failure;
|
||
- `ACQUIRED -> STARTING` handoff 직전/직후 cancellation;
|
||
- engine synchronous start failure와 asynchronous failure, callback-before-return race;
|
||
- success, recordable failure, ignored outcome;
|
||
- execution cutoff/deadline/caller cancellation/shutdown;
|
||
- half-open concurrent permission race;
|
||
- response/connection cleanup failure;
|
||
- double completion 시도.
|
||
|
||
Local preflight에서 engine ownership이 없거나 ignored outcome이면 `releasePermission`,
|
||
`STARTING|STARTED`의 recordable success/failure면 tracker evidence에 따라 각각
|
||
`onSuccess`/`onError`만 호출한다. Permit leak, synchronous start race, half-open slot leak와 double
|
||
record는 모두 failure다.
|
||
|
||
Transmission tracker는 first possible request write 직전 `NOT_SENT -> MAYBE_SENT` callback과
|
||
단조 전이를 raw fixture로 검증한다. 각 전이 지점에서 cancel/valid response를 동시에 release해
|
||
terminal CAS winner가 하나뿐이고 mutation `MAYBE_SENT+|UNKNOWN`은 항상 `INDETERMINATE`, exact
|
||
`NOT_SENT`만 cancelled/pre-send restart가 되는지 검증한다. Handoff 뒤 confidence unknown에서
|
||
later authoritative response headers/completion만 progress를 refine하고 response 없이
|
||
`NOT_SENT|SENT`로 downgrade/guess하지 않는 property를 포함한다.
|
||
|
||
Mutation disposition은 네 operation semantics 각각에 대해
|
||
`NOT_SENT|MAYBE_SENT|SENT|RESPONSE_STARTED|RESPONSE_COMPLETE|UNKNOWN`, processing evidence와
|
||
cancellation winner 전부를 `VALID_COMPLETE|VALID_HEADERS_ONLY|NONE_OR_INVALID` 및 모든
|
||
`ResponseSemanticClass`와 교차한다. Same-intent
|
||
replay/reconciliation에서는 operation attempt ID, idempotency key, fingerprint, operation ID,
|
||
tenant/credential scope가 바뀌지 않음을 검증한다.
|
||
|
||
Attempt-number property test는 ordinal 0에서 reason token/replayability를 요구하지 않지만
|
||
protected/root token을 한 번 소비하고, ordinal>0에서는 disposition/body/reason authorization까지
|
||
요구함을 고정한다. Committed local provider-quota token은 success/failure/cancel 모두 환불하지 않고
|
||
uncommitted reservation만 release하는지도 검증한다. `SINGLE_USE_SOURCE`와
|
||
non-retryable default protected physical ceiling=1, confirmed-`NOT_SENT` pre-send restart opt-in ceiling=2,
|
||
restart/retry budget/metric 분리, `MAYBE_SENT`부터 receipt 반환을 각각 검증한다.
|
||
|
||
현재 결함을 닫는 회귀 test도 포함한다.
|
||
|
||
- 서로 다른 `OutboundRetryPolicy` instance를 resilience/client에 전달해도 silent retry disable이
|
||
재발하지 않으며 새 aggregate API는 그런 wiring 자체를 표현할 수 없음;
|
||
- response wire/decoded cap 초과는 `RESPONSE_TOO_LARGE` 계열, no-retry, breaker-ignore,
|
||
status-preserving, logical failure observation exactly once;
|
||
- oversized 4xx/5xx가 connect failure로 바뀌거나 반복 다운로드되지 않음.
|
||
|
||
### 36.7 Streaming과 body
|
||
|
||
- buffered path는 decoded hard cap 전 allocation을 제한;
|
||
- upload stream은 reopenable/one-shot을 구분;
|
||
- callback scope 밖 stream access 실패;
|
||
- callback return/throw/cancel 모두 response close;
|
||
- consumer가 일부만 읽고 반환해도 drain-or-discard policy;
|
||
- lying `Content-Length`;
|
||
- truncated fixed/chunked/gzip;
|
||
- compression bomb와 ratio limit;
|
||
- slow decompression/decoder deadline;
|
||
- multipart part/count/header/total limit;
|
||
- spooled temp file quota/permission/cleanup;
|
||
- range resume validator mismatch;
|
||
- error response가 success reader에 전달되지 않음.
|
||
|
||
현재 test의 `readAllBytes()`는 production streaming proof로 인정하지 않는다.
|
||
|
||
### 36.8 DNS와 SSRF
|
||
|
||
두 수준으로 검증한다.
|
||
|
||
1. scripted resolver unit test: A/AAAA/mixed/empty/timeout/rebind/TTL;
|
||
2. pinned CoreDNS/dnsmasq-like container: resolver integration, cache expiry, address rotation.
|
||
|
||
Cases:
|
||
|
||
- loopback, link-local, private, multicast, unspecified, IPv4-mapped IPv6;
|
||
- NAT64 well-known/custom prefix의 embedded private/metadata IPv4, 6to4, Teredo와 special-purpose;
|
||
- public + private mixed answer;
|
||
- validation 뒤 다른 address로 connect하지 않음;
|
||
- redirect hop 재해석/재검증;
|
||
- DNS timeout도 total deadline 포함;
|
||
- Kubernetes short name/search-domain ambiguity;
|
||
- proxy remote-DNS mode에서 local resolver bypass 정책.
|
||
|
||
### 36.9 TLS와 mTLS
|
||
|
||
Test가 매번 ephemeral CA/server/client certificate를 생성한다.
|
||
|
||
- trusted/wrong/untrusted/expired/not-yet-valid cert;
|
||
- hostname/SAN mismatch;
|
||
- TLS protocol/cipher floor;
|
||
- server requests client cert: present/missing/wrong;
|
||
- trust/key material malformed;
|
||
- OCSP/revocation profile behavior와 effective JVM PKI property assertion;
|
||
- loopback/metadata/private/oversized AIA·CRLDP 악성 certificate에서 automatic network side effect 0;
|
||
- named revocation responder의 SSRF/redirect/body/deadline/cache/concurrency bound;
|
||
- OCSP authorized signature/CertID/status/time/nonce와 CRL issuer/signature/scope/base+delta/freshness;
|
||
- 각 named HTTP OCSP/CRL request가 revocation child cap과 parent root HTTP token을 소비;
|
||
- revocation child authorization/root reservation 실패 시 responder network side effect 0, cache hit과
|
||
valid stapled evidence에서는 child/root token 소비 0;
|
||
- stale/wrong-responder/replayed-good/UNKNOWN OCSP와 wrong-scope/stale CRL reject, exact cache-key isolation;
|
||
- handshake timeout;
|
||
- `NORMAL_ROLLOVER` N -> N+1, atomic swap, old connection drain;
|
||
- normal rollover의 new certificate invalid이면 non-revoked old generation 유지;
|
||
- `EMERGENCY_REVOKE` old admission 즉시 차단, session/token/pool invalidate, no fallback,
|
||
replacement 전 NOT_READY와 policy-driven in-flight cancellation;
|
||
- secret/cert가 logs/JUnit/artifact에 없음.
|
||
|
||
### 36.10 Proxy
|
||
|
||
Pinned proxy fixture로:
|
||
|
||
- HTTP CONNECT success/failure/auth;
|
||
- proxy DNS와 local DNS policy;
|
||
- `NO_PROXY` precedence를 사용하지 않는 explicit bypass list;
|
||
- proxy unavailable 시 direct fallback 금지;
|
||
- proxy redirect/credential stripping;
|
||
- TLS tunnel hostname verification;
|
||
- pool isolation by proxy route와 parent profile fingerprint linkage;
|
||
- proxy dependency cycle/unknown maturity/ambient or direct fallback startup reject;
|
||
- `Proxy-Authorization`이 CONNECT hop에만 존재하고 tunnel origin/telemetry에는 0회인 raw capture;
|
||
- 각 HTTP CONNECT가 proxy child cap과 parent root HTTP token을 소비;
|
||
- CONNECT child authorization/root reservation 실패 시 CONNECT/origin request write 0, existing tunnel
|
||
재사용 시 CONNECT child/root token 소비 0;
|
||
- shared proxy owner/reference count와 shutdown/drain.
|
||
|
||
### 36.11 HTTP/2
|
||
|
||
`httpclient-http2` card 선택 시:
|
||
|
||
- ALPN H2 success;
|
||
- `H2_REQUIRED` fallback reject;
|
||
- stream concurrency/pending bound;
|
||
- SETTINGS reduction;
|
||
- GOAWAY/RST_STREAM/REFUSED_STREAM별 `CONFIRMED_NOT_PROCESSED|MAYBE_PROCESSED|UNKNOWN`과
|
||
exact disposition/protocol-restart lease/counter;
|
||
- flow-control stall/deadline;
|
||
- header list/HPACK abuse;
|
||
- server push disabled;
|
||
- connection coalescing disabled/verified;
|
||
- proxy CONNECT compatibility;
|
||
- negotiated protocol metric.
|
||
|
||
HTTP/1-only baseline test 성공이 HTTP/2 readiness를 의미하지 않는다.
|
||
|
||
### 36.12 Authentication
|
||
|
||
- API key/static bearer exact destination/header ownership와 conditional scenario derivation;
|
||
- OAuth token cache single-flight;
|
||
- token expiry skew/refresh failure;
|
||
- token endpoint 자체 exact auth-purpose/client-auth mode tuple, header/body/signature redaction,
|
||
timeout/pool/retry isolation과 acyclic dependency DAG;
|
||
- child profile fingerprint/maturity가 parent OAuth evidence에 귀속;
|
||
- nested credential cap과 root-call total amplification ceiling;
|
||
- exact cache/single-flight key 각 축의 collision/isolation과 normal/emergency invalidation;
|
||
- current generation attempt별 선택, waiter cancel detach와 shared refresh ownership;
|
||
- concurrent root callers의 creator budget lease, immutable flight deadline, owner detach, joiner
|
||
no-double-charge와 zero-waiter cancel;
|
||
- one 401 refresh replay upper bound, prior response/CB/bulkhead release와 second 401 terminal;
|
||
- replay-safe operation만 auth replay;
|
||
- redirect/cross-origin credential stripping;
|
||
- multi-tenant token cache scope;
|
||
- request-signature/HTTP Basic mode는 current card set에서 startup reject;
|
||
- future request-signature card가 추가될 때 canonicalization/body digest/replay suite;
|
||
- no secret in exception/log/span/metric/JUnit report.
|
||
|
||
### 36.13 Observability contract
|
||
|
||
In-memory OTel exporter와 meter registry로:
|
||
|
||
- logical call 1개, physical span N개;
|
||
- `http.request.resend_count`;
|
||
- parent context/`tracestate` 보존;
|
||
- sampling flag 강제 변경 없음;
|
||
- destination propagation deny;
|
||
- no duplicate CLIENT span;
|
||
- compatibility timer logical once;
|
||
- failure stage/code;
|
||
- metric tag allowlist/cardinality bound와 generation role enum만 사용;
|
||
- attribute 생성 시점부터 raw URL/query/header/body/token/tenant/idempotency key가 SDK/processor/
|
||
exporter 전체에 0회;
|
||
- `network.peer.address` trust-zone opt-in과 external/untrusted profile deny;
|
||
- untrusted fetch에서 auto HTTP instrumentation/`server.address`/`url.full`이 0회이고 fixed bounded
|
||
project telemetry만 생성;
|
||
- global MeterFilter side effect 없음;
|
||
- canceled/indeterminate span status.
|
||
|
||
### 36.14 Composition와 lifecycle
|
||
|
||
Spring context matrix:
|
||
|
||
- zero binding -> zero resource/bean/health side effect;
|
||
- one/two destination exact qualified adapter;
|
||
- unknown/duplicate/conflicting canonical+legacy fail;
|
||
- provider definition only -> disabled;
|
||
- derived selected card 또는 exact compatibility profile이 `release-eligible`이 아님 -> ACTIVE
|
||
fail;
|
||
- binary/API-key/static-bearer/custom-trust mode의 exact profile tuple 또는 conditional/interaction
|
||
scenario 누락 -> resource 생성 전 fail;
|
||
- individually supported axes를 섞은 미등록 조합(예: API-key + streaming)이 compatibility entry
|
||
없이 들어오면 resource 생성 전 fail;
|
||
- redirect-follow/request-signature/HTTP Basic처럼 canonical card 없는 mode -> resource 생성 전
|
||
fail;
|
||
- startup order;
|
||
- ingress drain before outbound admission close;
|
||
- accepted request lease allowed during grace;
|
||
- background new call reject;
|
||
- grace expiry cancel;
|
||
- pool/executor/resolver/watcher close once;
|
||
- normal generation swap과 emergency revoke/no-fallback/NOT_READY;
|
||
- OAuth token/proxy/revocation child dependency DAG, owner/reference count와 zero-binding close;
|
||
- advertised cold OAuth/401/proxy/revocation path보다 root cap이 작으면 startup/qualification failure;
|
||
- `QUALIFICATION_ONLY`는 candidate exact tuple을 composition하지만 `ACTIVE_READY`/release assertion을
|
||
절대 만들지 못함;
|
||
- context restart no resource leak.
|
||
|
||
### 36.15 Compatibility fixtures
|
||
|
||
Destination contract fixture는:
|
||
|
||
- request method/path/query/header/media/schema;
|
||
- success/error schema;
|
||
- tolerant optional field behavior;
|
||
- enum unknown policy;
|
||
- pagination/ETag/version;
|
||
- recorded sanitized examples;
|
||
|
||
를 검증한다. Consumer-driven contract 도구를 쓰더라도 secret/PII가 fixture에 들어가지 않고
|
||
provider verification 결과를 release artifact로 연결한다.
|
||
|
||
### 36.16 Fault-tool 경계
|
||
|
||
Toxiproxy는 TCP latency/reset/bandwidth/toxic 검증에만 사용한다. 다음을 대신하지 않는다.
|
||
|
||
- DNS rebinding;
|
||
- malformed HTTP framing;
|
||
- TLS certificate semantics;
|
||
- HTTP/2 stream/GOAWAY;
|
||
- application idempotency.
|
||
|
||
각 fault에 맞는 fixture를 사용해 한 도구가 모든 보장을 증명한다고 과장하지 않는다.
|
||
|
||
## 37. Readiness cards와 CI design
|
||
|
||
### 37.1 Maturity registry와 selection resolver
|
||
|
||
Canonical maturity registry:
|
||
|
||
```text
|
||
src/config/httpclient/readiness-cards.yaml
|
||
```
|
||
|
||
Registry는 card 구현 성숙도와 card 자체의 base evidence만 소유한다. 지원 profile 조합을 축별
|
||
목록으로 소유하지 않는다.
|
||
|
||
```text
|
||
id
|
||
maturity: not-implemented | implemented-candidate | release-eligible
|
||
base-scenario-ids
|
||
required-gradle-task
|
||
required-services/images
|
||
required-runbooks
|
||
owner
|
||
```
|
||
|
||
Canonical exact profile compatibility registry:
|
||
|
||
```text
|
||
src/config/httpclient/profile-compatibility.yaml
|
||
```
|
||
|
||
각 entry:
|
||
|
||
```text
|
||
profile-id
|
||
maturity: not-implemented | implemented-candidate | release-eligible
|
||
tuple:
|
||
provider-id
|
||
resolved-provider-artifact-version
|
||
jdk-major
|
||
protocol
|
||
request-body-mode
|
||
response-body-mode
|
||
request-codec-media-mode
|
||
response-codec-media-encoding-mode
|
||
tls-mode
|
||
auth-purpose
|
||
auth-mode
|
||
proxy-mode
|
||
redirect-mode
|
||
dns-address-mode
|
||
egress-trust-zone
|
||
resilience-mode
|
||
operation-semantics
|
||
operation-policy-revision
|
||
dependency-profile-fingerprints
|
||
effective-behavior-digest
|
||
required-cards
|
||
required-scenario-ids
|
||
interaction-scenario-ids
|
||
owner
|
||
```
|
||
|
||
Tuple dimension은 모두 필수이며 wildcard/version range/omission을 허용하지 않는다.
|
||
`required-cards`는 §33.2 requirement rules와 byte-for-byte 일치해야 한다.
|
||
`interaction-scenario-ids`는 streaming + API-key, proxy + mTLS처럼 개별 축 test의 합으로
|
||
증명할 수 없는 조합을 검증한다. 새로운 조합은 이 registry entry, interaction test와 evidence
|
||
fingerprint를 함께 추가해야 한다.
|
||
|
||
Compatibility profile maturity:
|
||
|
||
| Maturity | 의미 |
|
||
| --- | --- |
|
||
| `not-implemented` | planned tuple/coverage gap; ACTIVE 선택 불가 |
|
||
| `implemented-candidate` | exact tuple code와 모든 scenario가 있으나 release review/runbook 승인 전 |
|
||
| `release-eligible` | exact tuple/card composition, interaction evidence와 운영 자산이 승인됨 |
|
||
|
||
Card와 compatibility profile maturity는 서로 대체하지 않는다.
|
||
|
||
```text
|
||
effectiveReadiness =
|
||
all required cards release-eligible
|
||
AND exact compatibility profile release-eligible
|
||
AND exact scenario set passes
|
||
```
|
||
|
||
새 compatibility entry는 항상 `not-implemented`로 시작하고 evidence/review 없이 바로
|
||
`release-eligible`로 만들 수 없다. 초기 설계 직후 모든 profile entry도 `not-implemented`다.
|
||
|
||
Canonical release assertion registry:
|
||
|
||
```text
|
||
src/config/httpclient/release-profile-assertions.yaml
|
||
```
|
||
|
||
각 entry는 release `profile-id`, 실제 deployment configuration resource/digest,
|
||
`expected-state`, `expected-selected-cards`, `expected-compatibility-profile-ids`만 가진다.
|
||
Binding/provider/card·profile maturity나 tuple 내용을 복제하지 않는다. CI는 그 configuration을 실제
|
||
Spring property binding과 같은 resolver로 읽고 derived card/tuple set을 계산한 뒤 assertion과
|
||
대조한다.
|
||
|
||
`selected`는 registry state가 아니다. §33.2 resolver가 active bindings와 operation마다 exact
|
||
effective profile tuple을 만들고 compatibility entry의 required cards를 검증해 derived selected
|
||
set을 계산한다. Checked-in release assertion registry는 expected
|
||
state/card/compatibility-profile ID assertion만 소유한다. CLI flag, classpath, Docker
|
||
availability로 set을 바꾸지 않는다.
|
||
|
||
Maturity 의미:
|
||
|
||
| Maturity | 의미 |
|
||
| --- | --- |
|
||
| `not-implemented` | 설계/gap만 존재; release 선택 불가 |
|
||
| `implemented-candidate` | 코드와 evidence가 있으나 production release 승인 전 |
|
||
| `release-eligible` | exact supported profile evidence, review, runbook가 모두 승인됨 |
|
||
|
||
Derived selected card 또는 exact compatibility profile이 `release-eligible`이 아니면 ACTIVE
|
||
startup/readiness/release가 실패한다.
|
||
|
||
Candidate 승격 deadlock을 피하기 위해 test harness에만 `QUALIFICATION_ONLY` mode를 둔다.
|
||
Production과 동일한 property binding, dependency DAG, tuple resolver, behavior digest와 resource
|
||
composition을 사용하되 마지막 maturity predicate만 exact `implemented-candidate` profile을 허용한다.
|
||
이 mode는 isolated qualification task에서만 선택할 수 있고 runtime `ACTIVE_READY`, release
|
||
assertion 성공, ingress readiness 또는 production descriptor를 절대 만들지 않는다. 결과는
|
||
candidate evidence/review 입력일 뿐이며 registry가 실제 `release-eligible`로 승인되기 전에는
|
||
production 선택이 계속 실패한다.
|
||
|
||
### 37.2 Canonical card set
|
||
|
||
| Card ID | 범위 | 이번 설계 직후 실제 maturity |
|
||
| --- | --- | --- |
|
||
| `httpclient-static-buffered` | fixed destination, relative URI, H1, bounded JSON/bodiless minimum; registry-declared bounded binary/API-key/static-bearer/custom-trust conditional modes; server TLS, pool, deadline/hard cancel, safe-read retry/physical CB | `not-implemented`; first promotion target |
|
||
| `httpclient-idempotent-mutation` | declared idempotent/keyed mutation, same-identity replay, unknown outcome reconciliation | `not-implemented` |
|
||
| `httpclient-non-retryable-mutation` | one-shot mutation, transmission evidence, no blind retry, indeterminate receipt/reconciliation handoff | `not-implemented` |
|
||
| `httpclient-streaming-download` | bounded scoped streaming response | `not-implemented` |
|
||
| `httpclient-streaming-upload` | reopenable/one-shot upload, write cancellation | `not-implemented` |
|
||
| `httpclient-mtls` | client certificate, trust/key rotation | `not-implemented` |
|
||
| `httpclient-oauth2-client-credentials` | isolated token client/cache/refresh | `not-implemented` |
|
||
| `httpclient-egress-proxy` | explicit CONNECT/proxy DNS/no direct fallback | `not-implemented` |
|
||
| `httpclient-http2` | H2 negotiation, stream capacity, GOAWAY/flow control | `not-implemented` |
|
||
| `httpclient-untrusted-url-fetch` | dedicated public fetch security boundary | `not-implemented` |
|
||
|
||
Minimum target compatibility profile ID는 예를 들어
|
||
`httpclient-static-h1-json-auth-none-direct-safe-read.v1`처럼 bounded/stable하게 둔다. ID가
|
||
tuple 내용을 대신하지 않으며 registry의 exact tuple과 digest가 authority다. 이번 설계 직후 이
|
||
profile maturity도 `not-implemented`다.
|
||
|
||
현재 R1 skeleton test를 baseline card implementation으로 승격하지 않는다. Redirect-follow,
|
||
request-signature, HTTP Basic, HTTP caching, WebSocket, SSE, HTTP/3는 이 card set에 몰래
|
||
포함하지 않으며 활성화하면 실패한다.
|
||
|
||
Minimum R2는 §4.2의 `httpclient-static-buffered` JSON/bodiless + auth-none profile을 뜻한다.
|
||
Optional card나 conditional mode를 선택하지 않았다는 이유로 minimum R2를 실패시키지 않지만,
|
||
선택하지 않은 기능을 R2라고 부르지 않는다.
|
||
|
||
### 37.3 Expected deployment state
|
||
|
||
Release profile은 정확히 하나를 선언한다.
|
||
|
||
```text
|
||
DISABLED
|
||
ACTIVE
|
||
```
|
||
|
||
Truth table:
|
||
|
||
| Expected | Binding/derived card/resource | Result |
|
||
| --- | --- | --- |
|
||
| `DISABLED` | 모두 0 | `DISABLED_VERIFIED`, HTTP R2 label 없음 |
|
||
| `DISABLED` | 하나라도 존재 | failure |
|
||
| `ACTIVE` | binding 0 | failure |
|
||
| `ACTIVE` | binding >= 1, derived cards + exact compatibility profiles release-eligible/pass | selected profile ready |
|
||
| `ACTIVE` | card/profile/evidence/consumer 하나라도 누락 | failure |
|
||
|
||
Environment에서 key가 사라져 ACTIVE가 우연히 DISABLED green으로 바뀌지 않는다. Release gate는
|
||
task exit code와 함께 expected state, resolved selected card set, exact compatibility profile
|
||
set, runtime descriptor state가 모두 일치하는지 검사한다.
|
||
|
||
### 37.4 Evidence identity
|
||
|
||
Evidence key:
|
||
|
||
```text
|
||
compatibility-profile-id
|
||
× required-card-set
|
||
× provider-id
|
||
× resolved-provider-artifact-version
|
||
× JDK-version
|
||
× protocol
|
||
× request/response-body-mode
|
||
× request/response-codec-media-encoding-mode
|
||
× TLS/auth-purpose/auth/proxy/redirect mode
|
||
× DNS-address/egress-trust-zone/resilience mode
|
||
× operation-semantics
|
||
× operation/policy revision
|
||
× sorted child dependency profile fingerprints
|
||
× effective-behavior-digest
|
||
× evidence-scenario-id
|
||
```
|
||
|
||
Unique selected profile fingerprint는 위 effective values와 다음 SHA-256을 포함한다.
|
||
|
||
- card registry;
|
||
- exact profile compatibility registry;
|
||
- release profile assertion registry와 referenced deployment configuration;
|
||
- operation catalog;
|
||
- dependency lockfiles;
|
||
- dependency verification metadata;
|
||
- test-image manifest;
|
||
- Gradle wrapper/settings;
|
||
- selected provider descriptor.
|
||
|
||
다른 provider/version/profile의 evidence를 재사용하지 않는다.
|
||
|
||
```text
|
||
claimedReadyProfileTuples == resolvedEffectiveProfileTuples
|
||
```
|
||
|
||
가 byte-for-byte 일치해야 한다. 사람이 입력하는 `evidence-revision` 문자열만으로 귀속하지 않는다.
|
||
|
||
### 37.5 Evidence categories와 executable results
|
||
|
||
```text
|
||
HSM = semantic
|
||
HSEC = security
|
||
HRES = deadline/resource
|
||
HLIF = lifecycle/composition
|
||
HOBS = observability/privacy
|
||
HCMP = compatibility/protocol
|
||
```
|
||
|
||
각 card는 여섯 category에 하나 이상의 anchor scenario를 가지지만 anchor 하나가 category 전체를
|
||
증명하지 않는다. Card registry의 base scenario와 exact profile compatibility entry의
|
||
required/interaction scenario가 합쳐져 §36의 해당 tuple 필수 시나리오를 열거한다.
|
||
|
||
```text
|
||
requiredScenarioSet(tuple) =
|
||
union(for card in compatibility.requiredCards: card.baseScenarioIds)
|
||
union compatibility.requiredScenarioIds
|
||
union compatibility.interactionScenarioIds
|
||
|
||
claimedEvidenceScenarioIds(tuple) == requiredScenarioSet(tuple)
|
||
executedScenarioIds(task) contains-all claimedEvidenceScenarioIds(tuple)
|
||
```
|
||
|
||
Missing/duplicate/unknown/다른 tuple로 claim한 scenario는 실패한다. Task가 무관한 추가 regression
|
||
test를 실행하는 것은 허용하지만 그 test를 profile evidence로 자동 claim하지 않는다. Executed
|
||
전체 test도 pass/no-skip 조건을 만족해야 하며, extra 실행이 missing evidence를 대체하지 못한다.
|
||
|
||
JUnit source tag 존재만 보지 않는다. Resolved evidence tuple마다 JUnit XML과 task result에서:
|
||
|
||
```text
|
||
tests > 0
|
||
failures = 0
|
||
errors = 0
|
||
skipped = 0
|
||
aborted = 0
|
||
disabled = 0
|
||
```
|
||
|
||
를 확인한다. Duplicate scenario ID, unknown tag, empty filter, stale fingerprint, missing XML은
|
||
failure다.
|
||
|
||
`not-implemented` card/profile은 planned scenario ID를 가질 수 있지만 coverage gap을
|
||
`NOT_SELECTED_GAP`으로 보고할 뿐 readiness task를 통과하지 않는다.
|
||
`implemented-candidate`와 `release-eligible` card/profile은 exact required scenario set이 실제
|
||
성공해야 한다.
|
||
|
||
### 37.6 Minimum anchor matrix
|
||
|
||
| Card | HSM | HSEC | HRES | HLIF | HOBS | HCMP |
|
||
| --- | --- | --- | --- | --- | --- | --- |
|
||
| `httpclient-static-buffered` | `HSM-STATIC-STATUS-RETRY-CB` | `HSEC-STATIC-SSRF-TLS` | `HRES-STATIC-HARD-CANCEL` | `HLIF-STATIC-ZERO-DRAIN` | `HOBS-STATIC-SPANS-PRIVACY` | `HCMP-STATIC-H1-JSON` |
|
||
| `httpclient-idempotent-mutation` | `HSM-MUTATION-UNKNOWN` | `HSEC-MUTATION-KEY` | `HRES-MUTATION-BUDGET` | `HLIF-MUTATION-RECONCILE` | `HOBS-MUTATION-REDACT` | `HCMP-MUTATION-CONTRACT` |
|
||
| `httpclient-non-retryable-mutation` | `HSM-NONRETRY-UNKNOWN` | `HSEC-NONRETRY-REDACT` | `HRES-NONRETRY-CANCEL` | `HLIF-NONRETRY-RECEIPT` | `HOBS-NONRETRY-REDACT` | `HCMP-NONRETRY-CONTRACT` |
|
||
| `httpclient-streaming-download` | `HSM-DOWNLOAD-STATUS` | `HSEC-DOWNLOAD-BOMB` | `HRES-DOWNLOAD-CANCEL` | `HLIF-DOWNLOAD-DRAIN` | `HOBS-DOWNLOAD-BOUNDS` | `HCMP-DOWNLOAD-RANGE` |
|
||
| `httpclient-streaming-upload` | `HSM-UPLOAD-REPLAY` | `HSEC-UPLOAD-MULTIPART` | `HRES-UPLOAD-WRITE` | `HLIF-UPLOAD-CLEANUP` | `HOBS-UPLOAD-REDACT` | `HCMP-UPLOAD-100` |
|
||
| `httpclient-mtls` | `HSM-MTLS-HANDSHAKE` | `HSEC-MTLS-VERIFY` | `HRES-MTLS-TIMEOUT` | `HLIF-MTLS-ROTATE` | `HOBS-MTLS-EXPIRY` | `HCMP-MTLS-VERSION` |
|
||
| `httpclient-oauth2-client-credentials` | `HSM-OAUTH-REFRESH` | `HSEC-OAUTH-LEAK` | `HRES-OAUTH-BUDGET` | `HLIF-OAUTH-CLOSE` | `HOBS-OAUTH-REDACT` | `HCMP-OAUTH-ERROR` |
|
||
| `httpclient-egress-proxy` | `HSM-PROXY-CONNECT` | `HSEC-PROXY-NOFALLBACK` | `HRES-PROXY-TIMEOUT` | `HLIF-PROXY-CLOSE` | `HOBS-PROXY-BOUNDED` | `HCMP-PROXY-TLS` |
|
||
| `httpclient-http2` | `HSM-H2-GOAWAY` | `HSEC-H2-COALESCE` | `HRES-H2-FLOW` | `HLIF-H2-DRAIN` | `HOBS-H2-PROTOCOL` | `HCMP-H2-ALPN` |
|
||
| `httpclient-untrusted-url-fetch` | `HSM-FETCH-REDIRECT` | `HSEC-FETCH-REBIND` | `HRES-FETCH-BOUND` | `HLIF-FETCH-ISOLATE` | `HOBS-FETCH-PRIVATE` | `HCMP-FETCH-CONTENT` |
|
||
|
||
`httpclient-static-buffered`의 required scenario에는 anchor 외에도 status-first error, response
|
||
wire/decoded bound, server TLS/hostname, DNS binding, pool exhaustion, cleanup reserve/orphan
|
||
quarantine, circuit permission exact-once, retry amplification, shutdown, OTel sanitizer가 포함된다.
|
||
Bounded binary/API-key/static-bearer/custom-trust mode를 선택하면 해당 codec/size,
|
||
credential ownership·rotation·redaction 또는 trust/rotation scenario를 exact tuple에 추가한다.
|
||
Streaming/proxy/mTLS 등 다른 card와 결합되면 compatibility entry가 interaction scenario를
|
||
추가한다. Tuple이 활성인데 exact entry나 conditional/interaction scenario가 빠지면 card task는
|
||
실패한다.
|
||
|
||
### 37.7 Exact proposed Gradle tasks
|
||
|
||
Common evidence:
|
||
|
||
```text
|
||
:adapter:outbound:httpclient:test
|
||
:adapter:outbound:httpclient:httpSemanticContractTest
|
||
:adapter:outbound:httpclient:httpSecurityTest
|
||
:adapter:outbound:httpclient:httpTlsTest
|
||
:adapter:outbound:httpclient:httpResilienceTest
|
||
:adapter:outbound:httpclient:httpDeadlineAndResourceTest
|
||
:adapter:outbound:httpclient:httpObservabilityTest
|
||
:adapter:outbound:httpclient:httpFaultTest
|
||
:adapter:outbound:httpclient:httpCompatibilityTest
|
||
:app-bootstrap:httpClientCompositionTest
|
||
verifyHttpClientProfileCompatibility
|
||
httpClientConsumerContractTest
|
||
verifyHttpClientOperationalAssets
|
||
```
|
||
|
||
`verifyHttpClientProfileCompatibility`는 production resolver와 같은 code path로 모든 checked-in
|
||
release configuration을 resolve한다. Missing/duplicate tuple, wildcard/range/omitted dimension,
|
||
card requirement drift, unknown/missing interaction scenario, 미등록 cross-card 조합, tuple과
|
||
JUnit evidence fingerprint 불일치, required/claimed evidence set 불일치를 실패시킨다. 축별
|
||
cartesian product를 자동 허용하지 않는다.
|
||
|
||
```text
|
||
httpClientProductionReadiness.dependsOn(verifyHttpClientProfileCompatibility)
|
||
```
|
||
|
||
Card readiness:
|
||
|
||
```text
|
||
httpClientStaticBufferedReadiness
|
||
httpClientIdempotentMutationReadiness
|
||
httpClientNonRetryableMutationReadiness
|
||
httpClientStreamingDownloadReadiness
|
||
httpClientStreamingUploadReadiness
|
||
httpClientMtlsReadiness
|
||
httpClientOauth2ClientCredentialsReadiness
|
||
httpClientEgressProxyReadiness
|
||
httpClientHttp2Readiness
|
||
httpClientUntrustedUrlFetchReadiness
|
||
httpClientProductionReadiness
|
||
httpClientAllImplementedCandidates
|
||
```
|
||
|
||
이 task는 future public build contract이며 현재 존재한다고 주장하지 않는다. Card task mapping은
|
||
registry에 byte-for-byte 기록하고 configuration time에 생성/검증한다. Unknown task, duplicate
|
||
mapping, empty tag expression, dependency cycle은 test 실행 전 실패한다.
|
||
|
||
`httpClientProductionReadiness`:
|
||
|
||
1. card/compatibility/release-assertion registry schema, maturity와 IDs;
|
||
2. expected state와 binding resolve;
|
||
3. derived selected card/compatibility-profile set와 release profile assertion;
|
||
4. selected card와 exact compatibility profile이 모두 release-eligible;
|
||
5. every effective full profile tuple exact compatibility match + card-set equality +
|
||
base/required/interaction scenario set equality;
|
||
6. selected profile fingerprint별 card task;
|
||
7. ACTIVE consumer/operation contract 또는 DISABLED explicit N/A;
|
||
8. bootstrap composition/zero-resource contract;
|
||
9. operational assets;
|
||
10. architecture/config/public path/env gates;
|
||
11. supply-chain gates;
|
||
12. JUnit no-skip/zero-test assertion;
|
||
13. sanitized descriptor/evidence artifact;
|
||
|
||
을 aggregate한다.
|
||
|
||
`httpClientAllImplementedCandidates`는 card와 compatibility profile 중
|
||
`implemented-candidate`/`release-eligible` 전체 exact tuple을 nightly 실행한다.
|
||
`not-implemented`는 `NOT_SELECTED_GAP`으로 보고한다.
|
||
|
||
### 37.8 Consumer와 operational-asset gate
|
||
|
||
`httpClientConsumerContractTest`:
|
||
|
||
- ACTIVE binding마다 feature-specific port adapter가 하나;
|
||
- adapter operation ID와 catalog가 일치;
|
||
- request/response mapper/compatibility fixture;
|
||
- generic invoker를 controller/use case가 직접 사용하지 않음;
|
||
- sample shape이면 planned registry edge와 sample-local adapter;
|
||
- production shape이면 HTTP leaf의 direct `application-core` dependency;
|
||
- raw `repoUrl`/absolute URL이 application call에 없음.
|
||
|
||
`verifyHttpClientOperationalAssets`:
|
||
|
||
- selected card의 runbook path 존재;
|
||
- owner/escalation/rollback/recovery verification section;
|
||
- non-placeholder SLO/dashboard/alert registry link;
|
||
- provider upgrade and reconciliation runbook;
|
||
- descriptor/card/catalog revision link.
|
||
|
||
DISABLED일 때만 consumer/SLO가 explicit N/A일 수 있다. Provider candidate nightly는 fixture
|
||
consumer를 사용하되 deployment ACTIVE evidence로 가장하지 않는다.
|
||
|
||
### 37.9 Exact supply-chain gates
|
||
|
||
```text
|
||
verifyDependencyLocks
|
||
verifyDependencyVerificationMetadataCoverage
|
||
verifyHttpClientRuntimeClasspathIsolation
|
||
verifyHttpClientTestImageManifest
|
||
generateHttpClientRuntimeSbom
|
||
generateHttpClientTestSbom
|
||
httpClientVulnerabilityLicenseKevGate
|
||
```
|
||
|
||
`verifyHttpClientRuntimeClasspathIsolation`은:
|
||
|
||
- test server/Testcontainers/Toxiproxy/CA fixture가 production runtime에 없음;
|
||
- Apache/JDK/provider dependency가 selected provider policy와 일치;
|
||
- Jackson/Boot RestClient runtime이 module-isolated JSON test에 존재;
|
||
- forbidden duplicate major/engine leakage report;
|
||
|
||
를 검증한다.
|
||
|
||
Image manifest gate는 required service key, exact `tag@sha256`, placeholder/`latest` 금지, pull한
|
||
image digest 일치를 검증한다. Runtime/test SBOM을 분리하고 scan 결과는 commit SHA와 SBOM digest에
|
||
귀속한다.
|
||
|
||
### 37.10 CI service matrix와 no-skip
|
||
|
||
| Evidence | Fixture |
|
||
| --- | --- |
|
||
| HTTP semantics | MockWebServer + raw byte server |
|
||
| TCP fault | Toxiproxy |
|
||
| DNS | pinned authoritative DNS container/scripted resolver |
|
||
| TLS/mTLS | ephemeral CA + TLS endpoint |
|
||
| Proxy | pinned HTTP CONNECT proxy |
|
||
| HTTP/2 | ALPN/H2-capable test server |
|
||
| Observability | in-memory OTel exporter |
|
||
| Resource | process/JFR/OS counters where available |
|
||
|
||
Container image SSOT:
|
||
|
||
```text
|
||
src/gradle/httpclient-test-images.properties
|
||
```
|
||
|
||
Selected card service가 없으면 release/readiness는 실패한다. `assumeTrue`, Docker unavailable,
|
||
missing image를 skip-success로 바꾸지 않는다. Local developer task만 explicit `NOT_RUN`을 보고할
|
||
수 있고 release evidence가 아니다. Expected test가 0개이거나 skipped/aborted/disabled가 하나라도
|
||
있으면 selected evidence는 실패한다.
|
||
|
||
### 37.11 Repository workflow integration
|
||
|
||
현재 `.github/workflows/ci-quality-gates.yml`의 `release-gate`는
|
||
`quality-gates`, `sample-off`, `gate-matrix-lint`만 집계한다. 구현 change에서 exact job을 추가한다.
|
||
|
||
```yaml
|
||
httpclient-production-readiness:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
# checkout + exact Java + Gradle cache setup
|
||
- name: Verify resolved HTTP client readiness from a clean runner
|
||
working-directory: src
|
||
run: >-
|
||
./gradlew httpClientProductionReadiness
|
||
--rerun-tasks --no-build-cache --no-daemon --stacktrace
|
||
|
||
httpclient-supply-chain:
|
||
uses: ./.github/workflows/_dependency-vulnerability-reusable.yml
|
||
with:
|
||
capability: httpclient
|
||
```
|
||
|
||
`release-gate`:
|
||
|
||
```yaml
|
||
needs:
|
||
- quality-gates
|
||
- sample-off
|
||
- gate-matrix-lint
|
||
- httpclient-production-readiness
|
||
- httpclient-supply-chain
|
||
|
||
env:
|
||
HTTPCLIENT_RESULT: ${{ needs.httpclient-production-readiness.result }}
|
||
HTTPCLIENT_SUPPLY_CHAIN_RESULT: ${{ needs.httpclient-supply-chain.result }}
|
||
```
|
||
|
||
Success loop에도 두 result를 포함한다. Job만 추가하고 `needs`/env/loop 중 하나라도 빠지면 blocking
|
||
아니다.
|
||
|
||
별도 `dependency-vulnerability.yml` job은 다른 workflow의 `needs`로 직접 연결할 수 없다. 현재
|
||
install/Trivy/KEV/license logic을 pinned reusable workflow로 추출하고 기존 workflow와
|
||
`ci-quality-gates.yml`이 같은 implementation을 호출한다. Reusable workflow가 반환하는 commit
|
||
SHA/SBOM/scan digest가 현재 checkout과 다르면 실패한다.
|
||
|
||
같은 change에서:
|
||
|
||
- `.github/ci-gate-matrix.yml`에 `httpclient-production-readiness`,
|
||
`httpclient-supply-chain` 두 blocking gate 추가;
|
||
- 현재 19개 기준의 `EXPECTED_GATE_COUNT`를 21로 갱신;
|
||
- `.github/scripts/verify-gate-matrix.sh`가 blocking job set,
|
||
`release-gate.needs`, result env, shell success loop의 집합 동등성을 검증;
|
||
- workflow contract test가 unknown/missing/extra job을 실패;
|
||
|
||
하도록 한다.
|
||
|
||
### 37.12 Fresh-runner와 scheduled qualification
|
||
|
||
PR/release job의 root task가 required fixture를 Testcontainers/local server로 직접 provision한다.
|
||
이전 matrix artifact가 있어도 성공을 신뢰하지 않고 clean checkout에서
|
||
`--rerun-tasks --no-build-cache`로 selected evidence를 재실행한다. Fixture/image/secret reference가
|
||
없으면 실패한다.
|
||
|
||
별도 `.github/workflows/httpclient-production-readiness.yml`:
|
||
|
||
- `schedule`;
|
||
- `workflow_dispatch`;
|
||
- release-candidate trigger;
|
||
- `QUALIFICATION_ONLY`로 resolve한 all `implemented-candidate` profile matrix와 production
|
||
resolver로 resolve한 `release-eligible` matrix;
|
||
- selected minimum/next approved provider version;
|
||
- soak/fault/compatibility;
|
||
- final clean runner `httpClientAllImplementedCandidates`;
|
||
|
||
를 실행한다. Nightly candidate failure는 해당 card/compatibility profile maturity 승격
|
||
blocker이며 이미 별도로 검증된 selected release profile을 다른 not-implemented card 때문에
|
||
허위 실패로 바꾸지 않는다.
|
||
|
||
### 37.13 Sanitized artifact
|
||
|
||
- expected state와 `DISABLED_VERIFIED|ACTIVE_READY`;
|
||
- derived selected/maturity/gap card와 compatibility-profile set;
|
||
- exact compatibility profile IDs, full tuple와 fingerprints;
|
||
- provider/JDK/protocol/body/codec/media/encoding/TLS/auth-purpose/auth/proxy/redirect/DNS/egress/resilience/
|
||
operation-semantics matrix와 effective-behavior digest;
|
||
- required base/conditional/interaction scenario IDs;
|
||
- scenario별 JUnit counts;
|
||
- exact Gradle command;
|
||
- fault/resource timeline;
|
||
- readiness descriptor;
|
||
- operational asset validation;
|
||
- runtime/test SBOM digest;
|
||
- vulnerability/license/KEV result;
|
||
- card/compatibility/release-assertion registry, deployment config, catalog, lock와 image-manifest
|
||
SHA-256;
|
||
- commit SHA.
|
||
|
||
Raw URL/IP/header/body/certificate/key/token/tenant/idempotency key는 artifact에서 제거한다. Standard
|
||
OTel span sanitizer evidence는 raw span이 아니라 pass/fail과 bounded fixture IDs만 담는다.
|
||
|
||
### 37.14 Common repository gates
|
||
|
||
HTTP root task는 다음 existing/future exact task에 의존한다.
|
||
|
||
```text
|
||
verifyCleanArchitectureDependencies
|
||
verifyEnvKeys
|
||
verifyPublicPathSnapshot
|
||
verifyConfigurationPropertiesProcessor
|
||
verifyDependencyLocks
|
||
verifyDependencyVerificationMetadataCoverage
|
||
verifyHttpClientProfileCompatibility
|
||
verifyHttpClientRuntimeClasspathIsolation
|
||
verifyHttpClientTestImageManifest
|
||
generateHttpClientRuntimeSbom
|
||
generateHttpClientTestSbom
|
||
verifyHttpClientOperationalAssets
|
||
```
|
||
|
||
`httpClientVulnerabilityLicenseKevGate`는 generated SBOM과 exact image에 대한 workflow result를
|
||
release aggregator가 요구한다. 현재 존재하지 않는 task/job은 구현 전까지 gap이며 이 설계
|
||
문서만으로 CI가 보장한다고 주장하지 않는다.
|
||
|
||
## 38. Gradle, dependency와 supply chain
|
||
|
||
### 38.1 Production dependency ownership
|
||
|
||
초기 `apache-hc5-classic` provider 후보:
|
||
|
||
```text
|
||
implementation project(':application-core') // production fork port implementation이 있을 때만
|
||
implementation project(':shared-contract')
|
||
implementation project(':adapter:outbound:support')
|
||
implementation 'org.springframework.boot:spring-boot-autoconfigure'
|
||
implementation 'org.springframework.boot:spring-boot-restclient'
|
||
implementation 'org.springframework.boot:spring-boot-jackson' // JSON card/codec를 실제 소유할 때
|
||
implementation 'org.springframework:spring-web'
|
||
implementation 'org.apache.httpcomponents.client5:httpclient5'
|
||
implementation 'io.micrometer:micrometer-observation' // 직접 API 사용 시
|
||
implementation 'io.micrometer:micrometer-core' // 직접 meter 소유 시
|
||
implementation 'io.github.resilience4j:resilience4j-retry'
|
||
implementation 'io.github.resilience4j:resilience4j-circuitbreaker'
|
||
implementation 'org.slf4j:slf4j-api'
|
||
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
|
||
```
|
||
|
||
Spring Boot 4의 `spring-boot-restclient`가 preconfigured builder/HTTP client integration을,
|
||
`spring-boot-jackson`이 Jackson 3 runtime/auto-configuration을 명시적으로 소유한다. Inbound web
|
||
starter나 test classpath에서 우연히 들어오는 converter/codec에 의존하지 않는다.
|
||
|
||
실제 compile/runtime API 사용을 확인해 필요 없는 dependency는 제거한다. Broad
|
||
`spring-boot-starter-*`, Resilience4j Spring starter/AOP, WebFlux starter를 convenience로 넣지
|
||
않는다.
|
||
|
||
두 consumer shape:
|
||
|
||
- production fork가 HTTP leaf 안에 `application-core` port adapter를 둘 때만 HTTP leaf에 direct
|
||
`application-core` dependency 추가;
|
||
- template sample은 HTTP leaf에 sample dependency를 추가하지 않고
|
||
`sample-portfolio`가 HTTP leaf에 의존하도록 registry/build file을 변경.
|
||
|
||
JSON을 baseline에서 지원한다면 `spring-boot-restclient`, Boot Jackson 3와 selected
|
||
`JacksonJsonHttpMessageConverter`가 HTTP leaf isolated runtime test에서 실제 resolve되어야 한다.
|
||
|
||
### 38.2 Version policy
|
||
|
||
- Spring Boot BOM이 관리하는 버전은 version 없이 선언하되 실제 managed version을 CI에서 출력/
|
||
검증;
|
||
- Boot BOM이 Apache HC5를 관리하는지 구현 시 확인하고 미관리면 version catalog/constraint의
|
||
단일 SSOT 사용;
|
||
- 현재 module build의 Resilience4j `2.2.0` 세 번 직접 표기는 version catalog/provider
|
||
platform/constraint로 이동;
|
||
- 모든 configuration lockfile 갱신;
|
||
- dependency verification checksum 갱신;
|
||
- lock diff와 transitive dependency review.
|
||
|
||
BOM 사용이 exact selected provider의 compatibility 증거를 대신하지 않는다.
|
||
|
||
### 38.3 API leakage
|
||
|
||
Apache, Spring HTTP, Resilience4j, Micrometer type은 모두 adapter implementation detail이다.
|
||
|
||
- application/domain port signature에 노출 금지;
|
||
- `api` dependency로 export하지 않음;
|
||
- adapter public package 최소화;
|
||
- engine/provider는 internal package;
|
||
- feature adapter만 application port 구현;
|
||
- 예외적으로 §10.5의 framework-neutral registered-operation adapter-consumer SPI만 public;
|
||
- application/inbound가 이 SPI를 import하지 못하도록 ArchUnit;
|
||
- ArchUnit/public-path snapshot으로 leakage 검출.
|
||
|
||
### 38.4 Optional provider isolation
|
||
|
||
HTTP/2/async/reactive/cloud-auth provider가 추가되면 dependency set을 core leaf에 모두 넣지 않는다.
|
||
선택지:
|
||
|
||
1. 같은 leaf의 isolated source set + runtime factory;
|
||
2. registry 변경을 동반한 별도 provider leaf;
|
||
3. app-bootstrap-only composition dependency.
|
||
|
||
Provider가 서로 다른 Netty/Jackson/Apache major version을 강제하면 별도 leaf가 우선이다.
|
||
Classpath에 provider가 둘 존재해도 자동 fallback하지 않는다.
|
||
|
||
### 38.5 Test dependencies
|
||
|
||
Test-only 후보:
|
||
|
||
```text
|
||
testImplementation MockWebServer
|
||
testImplementation Testcontainers core/JUnit integration
|
||
testImplementation Toxiproxy module or pinned proxy fixture
|
||
testImplementation property-based test library already standardized by repository
|
||
testImplementation OTel/Micrometer test exporter APIs actually used
|
||
```
|
||
|
||
Runtime artifact에 test server, Docker client, CA generator, proxy implementation을 포함하지
|
||
않는다. Groovy/Spock을 유지할지 JUnit을 사용할지는 repository test convention과 실행 task
|
||
분리를 기준으로 결정하며 둘을 이유 없이 중복 도입하지 않는다.
|
||
|
||
Module-isolation test는 inbound web/app-bootstrap의 transitive classpath 없이 HTTP leaf runtime만
|
||
구성해 다음을 검증한다.
|
||
|
||
- `spring-boot-restclient` auto-configuration과 prototype builder;
|
||
- exact Apache request factory;
|
||
- Boot observation/customizer preservation;
|
||
- Jackson 3 JSON converter와 Java time behavior;
|
||
- no Jackson 2 converter ambiguity;
|
||
- selected TLS bundle integration;
|
||
- missing direct runtime dependency가 startup failure로 드러남.
|
||
|
||
### 38.6 Supply-chain gate
|
||
|
||
- dependency locks;
|
||
- checksum/signature verification;
|
||
- SBOM with runtime/test distinction;
|
||
- CVE + CISA KEV equivalent policy;
|
||
- license allow/deny;
|
||
- transitive logging/codec/network library review;
|
||
- container image digest/SBOM;
|
||
- no repository from unapproved URL;
|
||
- no dynamic version/range/SNAPSHOT;
|
||
- dependency update compatibility suite.
|
||
|
||
HTTP parser/TLS/compression/HTTP2 vulnerability는 high-risk로 분류하고 emergency update 때도 semantic
|
||
contract와 readiness cards를 다시 실행한다.
|
||
|
||
### 38.7 Current dependency findings
|
||
|
||
현재 증거:
|
||
|
||
- `spring-web`, `spring-boot-autoconfigure`, Micrometer core, SLF4J는 직접 선언;
|
||
- Resilience4j retry/circuitbreaker/micrometer가 각각 `2.2.0`으로 직접 pin;
|
||
- production engine은 JDK `HttpClient`;
|
||
- Apache HC5 production dependency와 pool manager가 없음;
|
||
- HTTP leaf runtime에는 `spring-boot-restclient`가 없고 preconfigured Boot
|
||
`RestClient.Builder` 소유권이 없음;
|
||
- HTTP leaf runtime에는 baseline JSON을 보장할 direct Boot Jackson 3 dependency가 없음;
|
||
- HTTP leaf는 `application-core`에 직접 의존하지 않고 production semantic port를 구현하지 않음;
|
||
- test dependency는 Spock만 직접 선언;
|
||
- Resilience4j transitive runtime module이 lockfile에 존재해도 명시적 production contract를
|
||
의미하지 않음.
|
||
|
||
이번 문서는 `build.gradle`을 바꾸지 않는다. 위 항목은 implementation plan의 dependency diff와
|
||
lock refresh 대상이다.
|
||
|
||
## 39. Implementation와 migration sequence
|
||
|
||
### 39.1 Phase 0 — Characterization and truth-in-labeling
|
||
|
||
작업:
|
||
|
||
- 현재 public path/bean/config/test characterization;
|
||
- stream 4xx/5xx success bug 재현;
|
||
- retry/CB actual order test;
|
||
- `globalCallTimeout` non-cancellation test;
|
||
- JDK request-factory `readTimeout`이 logical total/byte-idle timeout을 증명하지 않는
|
||
characterization;
|
||
- retry policy object-identity mismatch의 silent retry-disable 재현;
|
||
- response-size violation이 connect failure/no-log/retry candidate로 오분류되는 경로 재현;
|
||
- sample `RepoStatsPortClient` fixture와 raw `repoUrl` 전달 경계 기록;
|
||
- manual trace header test;
|
||
- current descriptor/README를 R0/R1로 정직하게 표시.
|
||
|
||
Acceptance:
|
||
|
||
- 현재 결함이 failing characterization 또는 explicit gap test로 재현;
|
||
- 기존 passing test의 증명 범위가 문서화;
|
||
- production-ready/R2 표현 없음.
|
||
|
||
Rollback:
|
||
|
||
- 문서/characterization만 제거 가능하나 보장 과장은 되살리지 않음.
|
||
|
||
### 39.2 Phase 1 — Semantic ports, catalog와 disabled composition
|
||
|
||
작업:
|
||
|
||
- production fork는 `application-core` feature-specific port와 HTTP leaf adapter, template sample은
|
||
sample-local port/adapter와 bounded adapter-consumer SPI 중 한 shape를 명시적으로 선택;
|
||
- sample shape의 `sample-portfolio -> adapter-outbound-httpclient` registry/build edge와
|
||
`RepositoryCoordinates` typed input;
|
||
- adapter wire DTO/mapper;
|
||
- destination/operation/card registry;
|
||
- `expected-state`와 binding에서 exact effective profile tuples/card sets/interaction scenarios를
|
||
계산하고 maturity/compatibility registry와 release profile assertion을 대조;
|
||
- new card/profile maturity starts `not-implemented` and explicit promotion evidence;
|
||
- canonical typed settings;
|
||
- zero-binding composition;
|
||
- legacy configuration conflict validation.
|
||
|
||
Acceptance:
|
||
|
||
- application에는 HTTP/Spring/adapter type 없음;
|
||
- arbitrary URL/header API 없음;
|
||
- unmatched full profile tuple/card-composition/interaction-scenario gap은 resource 생성 전 fail;
|
||
- card-base ∪ profile-required ∪ interaction required set과 claimed evidence set equality;
|
||
- no binding zero resources;
|
||
- architecture/config binding tests 통과.
|
||
|
||
### 39.3 Phase 2 — Apache HC5 baseline engine와 pool
|
||
|
||
작업:
|
||
|
||
- injected Boot `RestClient.Builder` 또는 equivalent fully configured builder;
|
||
- HTTP leaf가 `spring-boot-restclient`, baseline JSON이면 `spring-boot-jackson`, selected Apache
|
||
provider dependency를 직접 소유;
|
||
- Apache connection manager;
|
||
- finite pool/acquire/lifetime/idle settings;
|
||
- hidden retry/redirect/cookie disable;
|
||
- effective option startup assertion;
|
||
- close handle/lifecycle;
|
||
- inbound web/app-bootstrap transitive classpath 없이 Boot RestClient customization과 Jackson 3
|
||
converter를 검증하는 module-isolation test.
|
||
|
||
Acceptance:
|
||
|
||
- H1 static buffered semantic/pool contract;
|
||
- no resource leak;
|
||
- selected engine exact;
|
||
- JDK fallback 없음.
|
||
|
||
Counterargument:
|
||
|
||
Apache classic blocking cancellation이 DNS/TLS/write/body 단계의 hard total deadline을 증명하지
|
||
못하면 이 phase를 R2로 승인하지 않는다. 같은 Engine SPI의 Apache async provider를 reference로
|
||
승격한다.
|
||
|
||
### 39.4 Phase 3 — Deadline, cancellation와 resilience
|
||
|
||
작업:
|
||
|
||
- monotonic deadline;
|
||
- positive cleanup reserve와 execution cutoff;
|
||
- phase caps;
|
||
- cancellable engine handle;
|
||
- quarantine, bounded orphan registry/reaper와 generation degrade;
|
||
- logical/physical admission;
|
||
- explicit retry loop;
|
||
- physical-attempt CB와 exact-once `CircuitPermissionLease`;
|
||
- retry budget/`Retry-After`;
|
||
- failure taxonomy.
|
||
|
||
Acceptance:
|
||
|
||
- every blocked phase deadline/cancel test;
|
||
- every wait 직후 gate recheck와 cutoff 뒤 zero new resource/network side effect;
|
||
- ordinary/restart/auth/redirect/replay counter와 protected/root physical ceiling의 exact count;
|
||
- `NOT_SENT -> MAYBE_SENT` linearization과 terminal-result/cancel CAS race;
|
||
- exact attempt/CB count와 `ACQUIRED -> STARTING -> STARTED/terminal` lease handoff;
|
||
- ordinal 0은 ordinary retry token/replayability 없이 실행되지만 shared protected/root token은
|
||
소비하고, ordinal>0은 disposition/body/reason budget까지 검사;
|
||
- non-retryable default one attempt, explicit `NOT_SENT`-only pre-send restart with separate budget,
|
||
`MAYBE_SENT` receipt;
|
||
- no retry after deadline;
|
||
- caller return `<= D + tolerance`, normal cleanup `<= D`;
|
||
- quarantine resource no-reuse, permit hold until task termination, bounded reaper/degrade;
|
||
- pool/permit cleanup exactly once.
|
||
|
||
### 39.5 Phase 4 — Fixed egress, DNS, SSRF와 server TLS baseline
|
||
|
||
작업:
|
||
|
||
- fixed destination resolve-validate-connect;
|
||
- kernel/engine redirect disabled;
|
||
- direct-only, auth-none profile; proxy/mTLS/OAuth/dynamic fetch 설정은 fail-closed;
|
||
- public-system/private-CA server TLS와 SSL bundle generation swap;
|
||
- automatic certificate-directed AIA/CRLDP/implicit OCSP egress disabled;
|
||
- propagation allowlist.
|
||
|
||
Acceptance:
|
||
|
||
- fixed-origin SSRF/DNS/NAT64 security matrix;
|
||
- server trust/hostname/normal rotation와 emergency revoke;
|
||
- proxy/mTLS/OAuth/redirect를 켜면 startup failure;
|
||
- no secret/tenant leak.
|
||
|
||
### 39.6 Phase 5 — Status-first bounded buffered body baseline
|
||
|
||
작업:
|
||
|
||
- response integrity/semantic-class status-first 분기;
|
||
- wire/decoded/ratio cap;
|
||
- bounded buffered JSON/bodiless codec;
|
||
- error body original-status preservation;
|
||
- partial/truncated semantics;
|
||
- streaming/upload mode 설정은 fail-closed.
|
||
|
||
Acceptance:
|
||
|
||
- minimum static buffered card만 승격;
|
||
- compression bomb/truncation/slow body;
|
||
- response/body cleanup;
|
||
- streaming/upload card는 아직 `not-implemented`.
|
||
|
||
### 39.7 Phase 6 — Observability, health와 readiness
|
||
|
||
작업:
|
||
|
||
- OTel single propagation owner;
|
||
- logical/physical telemetry;
|
||
- registry metric/header/MDC migrations;
|
||
- descriptor/readiness;
|
||
- selected-card CI/root task;
|
||
- runbooks.
|
||
|
||
Acceptance:
|
||
|
||
- no duplicate CLIENT span/manual header;
|
||
- cardinality/privacy test;
|
||
- selected card no-skip gate;
|
||
- zero-binding descriptor/resource contract.
|
||
|
||
### 39.8 Phase 7 — Consumer migration and legacy removal
|
||
|
||
작업:
|
||
|
||
- actual feature adapters/consumers;
|
||
- old generic `OutboundHttpClient` caller 제거;
|
||
- `TraceContextPropagationInterceptor` 제거;
|
||
- `globalCallTimeout` enforcer 제거;
|
||
- legacy `app.outbound.http.*` alias 제거;
|
||
- README/runbook/public snapshot update.
|
||
|
||
Acceptance:
|
||
|
||
- no old bean/config/property/reference;
|
||
- every consumer uses feature port and registered operation;
|
||
- full architecture/config/public-path/CI checks.
|
||
|
||
### 39.9 Phase 8 — Optional cards
|
||
|
||
각 card를 별도 change로:
|
||
|
||
- idempotent mutation;
|
||
- non-retryable mutation;
|
||
- streaming download;
|
||
- streaming upload;
|
||
- mTLS;
|
||
- OAuth2 client credentials;
|
||
- egress proxy;
|
||
- HTTP/2;
|
||
- future untrusted fetch.
|
||
|
||
Card끼리 묶어 한 번에 R2를 선언하지 않는다. 각 card의 evidence matrix와 rollback을 독립적으로
|
||
완성한다.
|
||
|
||
### 39.10 Rollout
|
||
|
||
1. shadow descriptor와 metrics만 활성;
|
||
2. one non-critical fixed destination;
|
||
3. canary pod/traffic;
|
||
4. ordinary retry disabled, protected physical ceiling 1 baseline;
|
||
5. pool/deadline/cancellation 관찰;
|
||
6. safe operation retry 점진 활성;
|
||
7. required destination 전환;
|
||
8. legacy 제거.
|
||
|
||
자동 provider fallback/downgrade는 rollback이 아니다. Rollback은 checked-in binding/config를
|
||
이전 generation/provider revision으로 되돌리는 명시적 배포다.
|
||
|
||
## 40. Completion and R2 criteria
|
||
|
||
HTTP capability를 “운영에서 바로 사용 가능” 또는 R2라고 부르려면 selected scope에서 모두
|
||
충족해야 한다.
|
||
|
||
1. Application use case는 feature-specific port만 의존한다.
|
||
2. Domain/application에 Spring/HTTP/engine/adapter type이 없다.
|
||
3. 모든 destination/operation/provider/card/exact profile과 OAuth token/proxy/revocation child
|
||
dependency DAG가 checked-in registry에 있고 cycle/self-reference가 없다.
|
||
4. Arbitrary absolute URL과 raw credential/header API가 baseline에 없다.
|
||
5. Operation catalog가 response integrity/semantic class, status/media/body,
|
||
retry/replay/idempotency/reconciliation 계약을 가진다.
|
||
6. Canonical binding만 activation하며 no binding은 zero resource다.
|
||
7. Exact provider가 선택되고 shared pre-start gate를 통과하지 않는 hidden
|
||
retry/redirect/auth/protocol resend/cookie가 disabled다.
|
||
8. Total deadline이 admission부터 body까지 monotonic하게 적용되고 positive cleanup reserve가
|
||
execution cutoff와 caller-visible deadline을 분리한다. 모든 wait가 absolute cutoff를 받고
|
||
wait 직후와 engine network phase 직전에 gate를 재검사한다.
|
||
9. Deadline/cancel 시 normal cleanup은 `D` 안에 끝나며, 끝나지 않은 task/resource는 caller
|
||
반환을 지연하지 않고 quarantine되어 bounded reaper가 처리하고 재사용되지 않는다.
|
||
10. Initial/retry/pre-send restart/auth/redirect/same-intent/protocol restart와 nested
|
||
token/reconciliation/proxy-CONNECT/revocation HTTP request가 reason/child counter, protected
|
||
ceiling과 root-call total HTTP 상한을 함께 지키고, 모든 child wire start가 exactly-once
|
||
`NestedHttpAuthorizationLease`/root-token handoff를 거친다. Broker는 parent exact
|
||
`AllowedChildEdge`만 승인하고 bind된 child profile/operation/authority와 root가 일치하며 required
|
||
cold path가 그 안에서 실행 가능하다.
|
||
11. Ordinal 0은 ordinary reason token/replayability 없이 실행 가능하고, 후속 protected request는 pure
|
||
eligibility 뒤 exactly-once `AttemptAuthorizationLease`를 사용한다. 모든 pre-bind exit가 이를
|
||
abort하고 engine/root commit 0을 보장한다. `NOT_SENT -> MAYBE_SENT`
|
||
first-write linearization, confidence refinement, typed cancellation disposition와
|
||
terminal-result/cancel/handoff CAS가 test로 고정된다.
|
||
12. Mutation lost response와 `MAYBE_SENT+|UNKNOWN` cancellation은 `INDETERMINATE`와 bounded
|
||
reconciliation을 보존한다.
|
||
13. Stale-token 401은 prior resource release, exact cache key, immutable-deadline/creator-budget
|
||
single-flight, one auth lease/replay, same identity와 second-401 terminal 계약을 모두 지킨다.
|
||
14. Circuit breaker record/ignore와 physical/logical 단위,
|
||
`ACQUIRED -> STARTING -> STARTED/terminal` permission lease exact-once 완료가 test로 고정된다.
|
||
15. Admission, pool pending, connection/stream, body/temp, credential waiter와 orphan quota가 유한하다.
|
||
16. Response status/header/framing과 bounded decoder를 body consumer 전에 분기하고 body/decode
|
||
뒤 response integrity와 semantic control outcome을 독립적으로 finalize한다. Header-authoritative
|
||
outcome은 header 검증 직후, body-required outcome은 body/semantic 검증 뒤 terminal CAS를 시도하며
|
||
단일 `AttemptTerminalCoordinator`가 winner와 cleanup owner를 각각 하나만 만든다. Success, 401,
|
||
retry-control, redirect, reconciliation, partial response와 cancellation 모두 공통 cleanup과 exhaustive
|
||
disposition resolver를 지나며 CAS loser가 정상 결과로 반환되지 않는다.
|
||
17. Wire/decoded/ratio/header/body limit과 truncation semantics가 있다.
|
||
18. DNS resolve-validate-connect, NAT64/transition address, SSRF/CIDR, redirect와 named proxy policy가
|
||
증명된다.
|
||
19. TLS hostname/trust, certificate-directed revocation egress와 OCSP/CRL signature/identity/
|
||
freshness semantics, normal rotation와 emergency revoke/no-fallback/NOT_READY가 증명된다.
|
||
20. mTLS/OAuth/API-key credential lifecycle, exact OAuth token-client auth child tuple와
|
||
cache/single-flight isolation key, shared owner/close가 증명된다.
|
||
21. OTel이 propagation을 단독 소유하고 duplicate CLIENT span이 없으며 sanitized attribute만 생성해
|
||
raw URL/query/header가 SDK/processor/exporter에 한 번도 들어가지 않는다.
|
||
22. Metric/log에는 raw URL이 없고 metric generation/pool tag는 checked-in bounded role만
|
||
사용한다. 모든 telemetry에 secret/body/tenant/idempotency key가 없고 peer address는 trust-zone
|
||
policy를 따른다. Untrusted fetch는 attacker-controlled authority를 표준 HTTP telemetry에 넣지 않는다.
|
||
23. Liveness는 remote dependency와 분리되고 readiness impact/probe가 명시된다.
|
||
24. Ingress drain 뒤 outbound close/cancel/resource close와 normal/emergency generation lifecycle이
|
||
증명된다.
|
||
25. ACTIVE binding에서 derived selected card와 exact parent/child compatibility profile이 모두
|
||
`release-eligible`이고, required/claimed evidence scenario set이 완전 일치하며 executed set이 이를
|
||
포함하고 evidence matrix가 0 skip으로 통과한다. `QUALIFICATION_ONLY` 결과는 ACTIVE_READY나
|
||
release assertion으로 사용할 수 없다. DISABLED는 zero-resource를 증명하며 R2로 표시하지 않는다.
|
||
26. Architecture/env/config/public-path/dependency/supply-chain gate가 통과한다.
|
||
27. Container/test dependency가 digest/lock/checksum으로 고정된다.
|
||
28. Required runbook와 dashboard/alert/SLO link가 존재한다.
|
||
29. Current provider/version/card/parent-child compatibility profile/policy revision이 sanitized
|
||
descriptor에 나온다.
|
||
30. ACTIVE profile에는 actual feature consumer가 최소 하나 contract를 통과한다. DISABLED profile은
|
||
consumer를 명시적 N/A로 검증한다.
|
||
31. Full selected release workflow가 fresh runner에서 root readiness task로 재실행된다.
|
||
32. Known gaps는 `not-implemented` card/profile로 fail-closed되고 R2 범위에 포함되지 않는다. Card
|
||
registry와 compatibility registry가 각각 card/profile maturity의 유일한 SSOT이며 selection은
|
||
binding/catalog/provider의 exact tuple과 child dependency fingerprints에서만 파생된다.
|
||
33. LLM Wiki capture와 independent review evidence가 완료된다.
|
||
|
||
현재 구현은 위 조건을 충족하지 않는다. 이 문서 완료는 구현 R2 완료가 아니다.
|
||
|
||
## 41. Required runbooks
|
||
|
||
구현과 함께 최소 다음 runbook을 제공한다.
|
||
|
||
1. destination onboarding/offboarding;
|
||
2. operation catalog와 compatibility 변경;
|
||
3. timeout/deadline budget 조정;
|
||
4. pool saturation/pending acquire;
|
||
5. retry storm/retry budget exhaustion;
|
||
6. circuit open/half-open;
|
||
7. DNS failure/rebinding/TTL/address rotation;
|
||
8. TLS handshake/certificate expiry/trust rotation;
|
||
9. mTLS client certificate rotation;
|
||
10. OAuth/API key/secret rotation;
|
||
11. proxy outage/auth/direct-fallback verification;
|
||
12. streaming leak/oversize/decompression bomb;
|
||
13. `INDETERMINATE` mutation reconciliation;
|
||
14. readiness probe outage/false negative;
|
||
15. client generation stuck draining;
|
||
16. provider upgrade/rollback;
|
||
17. selected readiness card evidence failure;
|
||
18. security incident and credential/metadata leakage;
|
||
19. HTTP/2 GOAWAY/flow-control/downgrade;
|
||
20. untrusted URL fetch incident if that future card is ever selected;
|
||
21. combined retry/redirect/auth/reconciliation amplification ceiling exhaustion;
|
||
22. OCSP/CRL/AIA responder outage와 certificate-directed egress rejection;
|
||
23. credential/key/trust compromise emergency revoke, no-fallback와 NOT_READY recovery;
|
||
24. OAuth token/proxy/revocation child profile qualification and ownership leak.
|
||
|
||
각 runbook:
|
||
|
||
- symptom/alert;
|
||
- safe diagnostic query;
|
||
- secret-safe evidence;
|
||
- immediate containment;
|
||
- retry/restart 금지 조건;
|
||
- rollback;
|
||
- reconciliation/data impact;
|
||
- owner/escalation;
|
||
- recovery verification;
|
||
|
||
을 포함한다.
|
||
|
||
## 42. Primary references
|
||
|
||
이 설계는 2026-07-27 기준으로 다음 primary source를 참고했다.
|
||
|
||
- [Spring Framework REST Clients](https://docs.spring.io/spring-framework/reference/integration/rest-clients.html):
|
||
`RestClient`, `exchange()`와 status-handler 경계.
|
||
- [Spring Framework `RestClient` Javadoc](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/client/RestClient.html):
|
||
current API contract.
|
||
- [Spring Boot 4 REST Client](https://docs.spring.io/spring-boot/4.0/reference/io/rest-client.html):
|
||
preconfigured `RestClient.Builder`, HTTP Service, SSL integration.
|
||
- [Spring Boot `HttpClientSettings`](https://docs.spring.io/spring-boot/4.0/api/java/org/springframework/boot/http/client/HttpClientSettings.html):
|
||
Boot HTTP client settings surface.
|
||
- [Spring Boot `ClientHttpRequestFactoryBuilder`](https://docs.spring.io/spring-boot/4.0/api/java/org/springframework/boot/http/client/ClientHttpRequestFactoryBuilder.html):
|
||
request-factory selection/customization.
|
||
- [Java 21 `HttpClient`](https://docs.oracle.com/en/java/javase/21/docs/api/java.net.http/java/net/http/HttpClient.html):
|
||
JDK client lifecycle, executor, redirect, protocol surface.
|
||
- [Java 21 `HttpRequest.Builder`](https://docs.oracle.com/en/java/javase/21/docs/api/java.net.http/java/net/http/HttpRequest.Builder.html):
|
||
request timeout/header/method surface.
|
||
- [Java networking properties](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/net/doc-files/net-properties.html):
|
||
JVM networking/DNS properties and scope.
|
||
- [Java 21 PKI Programmer's Guide](https://docs.oracle.com/en/java/javase/21/security/java-pki-programmers-guide.html):
|
||
certification path, CRLDP/AIA/OCSP network retrieval와 security properties.
|
||
- [Apache HttpComponents 5 pooling manager builder](https://hc.apache.org/httpcomponents-client-5.6.x/current/httpclient5/apidocs/org/apache/hc/client5/http/impl/io/PoolingHttpClientConnectionManagerBuilder.html):
|
||
pool construction and connection-manager options.
|
||
- [Apache HttpComponents 5 request configuration](https://hc.apache.org/httpcomponents-client-5.6.x/current/httpclient5/apidocs/org/apache/hc/client5/http/config/RequestConfig.Builder.html):
|
||
request/connect/connection-request policy surface.
|
||
- [RFC 9110 — HTTP Semantics](https://www.rfc-editor.org/rfc/rfc9110.html):
|
||
methods, status, representation, retry-related HTTP semantics.
|
||
- [RFC 9112 — HTTP/1.1](https://www.rfc-editor.org/rfc/rfc9112.html):
|
||
framing and HTTP/1.1 message parsing.
|
||
- [RFC 9113 — HTTP/2](https://www.rfc-editor.org/rfc/rfc9113.html):
|
||
stream errors, `REFUSED_STREAM`, GOAWAY와 last-stream-ID semantics.
|
||
- [RFC 5280 — PKIX Certificate and CRL Profile](https://www.rfc-editor.org/rfc/rfc5280.html):
|
||
certificate AIA/CRL distribution point, CRL scope/signature/freshness semantics.
|
||
- [RFC 6960 — OCSP](https://www.rfc-editor.org/rfc/rfc6960.html):
|
||
authorized responder, CertID, response signature/status/time와 replay semantics.
|
||
- [RFC 6749 — OAuth 2.0](https://www.rfc-editor.org/rfc/rfc6749.html):
|
||
client authentication, token endpoint와 scope semantics.
|
||
- [RFC 6052 — IPv6 Addressing of IPv4/IPv6 Translators](https://www.rfc-editor.org/rfc/rfc6052.html):
|
||
NAT64 IPv4-embedded IPv6 prefix/translation format.
|
||
- [RFC 6890 — Special-Purpose Address Registries](https://www.rfc-editor.org/rfc/rfc6890.html):
|
||
IPv4/IPv6 special-purpose address classification context.
|
||
- [RFC 6585 — Additional HTTP Status Codes](https://www.rfc-editor.org/rfc/rfc6585.html):
|
||
429 and `Retry-After` context.
|
||
- [OWASP SSRF Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html):
|
||
application/network-layer SSRF controls.
|
||
- [OpenTelemetry HTTP spans](https://opentelemetry.io/docs/specs/semconv/http/http-spans/):
|
||
client span and resend semantic convention.
|
||
- [OpenTelemetry HTTP metrics](https://opentelemetry.io/docs/specs/semconv/http/http-metrics/):
|
||
HTTP client metric semantic convention.
|
||
- [OpenTelemetry metrics concepts](https://opentelemetry.io/docs/concepts/signals/metrics/):
|
||
attribute-set aggregation과 cardinality context.
|
||
- [W3C Trace Context](https://www.w3.org/TR/trace-context/):
|
||
`traceparent`/`tracestate` format and propagation.
|
||
- [Spring Boot SSL](https://docs.spring.io/spring-boot/reference/features/ssl.html):
|
||
SSL bundle and documented reload integration boundary.
|
||
- [Spring Security OAuth2 Client](https://docs.spring.io/spring-security/reference/servlet/oauth2/client/index.html):
|
||
client registration/provider/authorized-client model.
|
||
- [Resilience4j Retry](https://resilience4j.readme.io/docs/retry):
|
||
retry configuration and result/exception predicates.
|
||
- [Resilience4j CircuitBreaker](https://resilience4j.readme.io/docs/circuitbreaker):
|
||
circuit breaker state/configuration model.
|
||
- [Idempotency-Key Internet-Draft 07](https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/07/):
|
||
HTTP idempotency-key 설계 참고. 2026-04-18에 만료된 Internet-Draft이며 표준으로 간주하지
|
||
않는다. 실제 partner contract가 우선한다.
|
||
|
||
Primary source가 지원하는 API가 실제 Spring Boot BOM/selected provider version에서 동일한지는
|
||
구현 시 dependency lock과 compatibility test로 다시 확인한다. 문서 링크는 executable evidence를
|
||
대신하지 않는다.
|