Files
document-haness/docs/clean-architecture-backend-template/final/evidence/raw/185-notification-provider-core-probes.txt
T
DongHyeonkaandClaude Opus 5 b2963105a8 docs(keycloak-session-store): import the session-storage lab as a new project
The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.

Follows the import procedure in README.md.

  source/     the originating repository verbatim — 78 documents, 28 SVGs,
              8 manifests, plus .source-revision recording the commit
  final/      the SSOT
    document.md   729 lines written from the 29 experiment documents, not
                  concatenated: what was predicted, what was measured, and
                  where the measurement itself was wrong
    evidence/raw    125 outputs, flattened to <experiment>__<file> because
                    the originals collided (01-baseline.txt appeared three
                    times) and the audit only globs the top level
    evidence/meta   one per raw file; command and exitCode are null and the
                    README says why rather than inventing them
    evidence/browser  22 captures
    assets/       three diagrams through techviz
    .techviz/     their VizSpecs

A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.

Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.

verify-pipeline.py passes. audit-records.py reports no issues.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 22:51:59 +09:00

129 lines
10 KiB
Plaintext

Sub-scope 05 (notification) provider + core + platform/{provider,observation,reactor} — manifest and probes
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
generatedAt=2026-08-30T06:52:39+00:00
=== OWNED FILES (main) ===
60b282fdaf571b03713ec3322598cc026599956e main/java/dev/caskeleton/adapter/outbound/notification/core/FailOpenNotificationProvider.java 45
fae64a058a039a7ecc240272827fbac9959d6fdc main/java/dev/caskeleton/adapter/outbound/notification/core/NotificationProvider.java 31
1fc3ed6bfcd98beccd79980aa8b35121a09d233d main/java/dev/caskeleton/adapter/outbound/notification/core/RoutingNotifier.java 143
10a762d66040f26d0f0827ab3bddf691cb44709b main/java/dev/caskeleton/adapter/outbound/notification/platform/observation/LoggingNotificationAudit.java 60
6760f65f4adbb5b0567b0f3694568aebb1533cc6 main/java/dev/caskeleton/adapter/outbound/notification/platform/observation/LoggingNotificationMetrics.java 59
c710af99e34cf4eab650d0aa7a084fd2bacb16c9 main/java/dev/caskeleton/adapter/outbound/notification/platform/observation/NotificationHealthReporter.java 109
a41a5704d0da558c81a19daca41e41dd75c2d905 main/java/dev/caskeleton/adapter/outbound/notification/platform/observation/NotificationHealthSnapshot.java 31
861144530bb50837f313e785d0f8cd87504d1cc3 main/java/dev/caskeleton/adapter/outbound/notification/platform/observation/NotificationServingThresholds.java 77
8eabe4b61fa791a15280b5a2740d41c62cc801db main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/EmailAttachments.java 74
3536d27bf4bb68c4b77de1cdf520d4c128ea781b main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/ProviderResults.java 100
579bf96a1b0635342ac56f1550c73256214754ef main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/UnconfiguredAttachmentResolver.java 29
bc130299645984991cbc7e73499a55161a34f214 main/java/dev/caskeleton/adapter/outbound/notification/platform/reactor/ReactiveNotificationOrchestrator.java 27
4be68bc232781fed6174ddc72dc10741ae8449c7 main/java/dev/caskeleton/adapter/outbound/notification/platform/reactor/ReactorContextBridge.java 41
45e34dc34bfe4a170a3f87c3b2f7c8cf9352e072 main/java/dev/caskeleton/adapter/outbound/notification/platform/reactor/ReactorNotificationOrchestrator.java 74
3a1929a0d2efc4c07a381711ed48365021cf25a2 main/java/dev/caskeleton/adapter/outbound/notification/provider/AttemptCorrelationId.java 34
8426e14502c0df2e96d7db106d11f379d7f17a64 main/java/dev/caskeleton/adapter/outbound/notification/provider/InlineNotificationAttemptAdapter.java 54
df65ef07819220cb95aa5f2928015ed6868d3561 main/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationAdmissionReadinessAdapter.java 163
28f03e15069e1d3ae7431c10c38e9783a962e217 main/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationAttemptContext.java 54
83b67ce2cc2cb1737a7b0565e7fbb8084bacf1ca main/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationProviderAttemptAdapter.java 363
0f67f36f30ebd67abf8366471e9ca81efe8e5407 main/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationProviderAttemptClient.java 104
49fbf9f8d45dde999dc6d49e4081a073c984bfd9 main/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationProviderRateAdmission.java 35
90f5abc482ada17ed5dfde249b621d487c866baa main/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationProviderReadinessProbe.java 12
3c4eee63e00d175b104bd210e120e12cb25c4b01 main/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationProviderReadinessSnapshot.java 42
effdc813379835d984da6aff60d04d0cde3cf7ce main/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationProviderSecretMaterialProvider.java 10
a869b5935248b5813d2ea5b04e07936632ddb653 main/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationReconciliationAdapter.java 151
3b4d8e838567ebb873b994f17bf48ff8c02edce2 main/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationSecretMaterialHandle.java 93
157d1b26ff0f939fd34e25a324f9be9407d81e03 main/java/dev/caskeleton/adapter/outbound/notification/provider/PreparedNotificationAttempt.java 31
a164147837b12da9e75f4d46a4ba49b9730a43ef main/java/dev/caskeleton/adapter/outbound/notification/provider/ProviderMessageReference.java 15
fdd03f4d009e5c08444b5d0ebf4a43bb9b4e3ac2 main/java/dev/caskeleton/adapter/outbound/notification/provider/ReconciliationLookupMode.java 9
main_count=29
=== OWNED FILES (test) ===
450f4afda7e0ae1af60aab81b48bfed733b91416 test/java/dev/caskeleton/adapter/outbound/notification/core/NotificationAdapterTest.java 174
8f120fc7d63f4022d8d7098da4435d3b3082d79d test/java/dev/caskeleton/adapter/outbound/notification/core/RoutingNotifierTest.java 359
3357d050c0bd755ad75f726cf38cf43bbe913a8a test/java/dev/caskeleton/adapter/outbound/notification/platform/provider/ProviderSandboxSmokeTest.java 37
ef82853f024be998396b1dec61826b51875a57ad test/java/dev/caskeleton/adapter/outbound/notification/platform/provider/SubmissionCompatibilityTest.java 73
a2bff37f2be9a3f9ee338391d7bd5955492b2d32 test/java/dev/caskeleton/adapter/outbound/notification/provider/InlineNotificationAttemptAdapterTest.java 55
6e46da9d5aeaf1416b9775292555ceea5791cb62 test/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationAdmissionReadinessAdapterTest.java 195
a2558bf4bba9b894b767093c046fb2da39da9352 test/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationProviderAttemptContractTest.java 402
0fdad560acb5aaa1b0c499fe005339957cd97afd test/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationReconciliationAdapterTest.java 194
d2e8ba7b4ac70ae3303ab965c5f33d9ec4e433f6 test/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationSecretMaterialHandleTest.java 38
test_count=9
=== 8.1 Retry-After 힌트의 최종 소비처 ===
$ grep -rn 'ProviderFailure::retryAfter' --include=*.java application-core/src/main
application-core/src/main/java/dev/caskeleton/application/notification/platform/dispatch/NotificationDispatchService.java:381: result.failure().flatMap(ProviderFailure::retryAfter),
exit=0
$ grep -n 'chosen\|max' application-core/src/main/java/dev/caskeleton/application/notification/platform/policy/RetryBackoff.java
11: * the configured maximum: a provider asking for an hour must not silently extend a delivery
14:public record RetryBackoff(Duration base, Duration max, double multiplier) {
21: Objects.requireNonNull(max, "max");
25: if (max.compareTo(base) < 0) {
26: throw new IllegalArgumentException("max must not be smaller than base");
41: long ceilingMillis = (long) Math.min(exponential, (double) max.toMillis());
43: Duration computed = Duration.ofMillis(Math.max(jittered, base.toMillis()));
44: Duration chosen = retryAfter.filter(hint -> hint.compareTo(computed) > 0).orElse(computed);
45: return chosen.compareTo(max) > 0 ? max : chosen;
exit=0
=== 8.2 음수 Retry-After: 파서와 생성자의 계약 비교 ===
$ sed -n '89,99p' adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/ProviderResults.java
/** Parse a {@code Retry-After} header expressed in seconds. */
public static Optional<Duration> retryAfter(Optional<String> headerValue) {
return headerValue.flatMap(
value -> {
try {
return Optional.of(Duration.ofSeconds(Long.parseLong(value.trim())));
} catch (NumberFormatException notSeconds) {
return Optional.empty();
}
});
}
exit=0
$ sed -n '28,35p' application-core/src/main/java/dev/caskeleton/application/notification/platform/provider/ProviderFailure.java
}
retryAfter.ifPresent(
delay -> {
if (delay.isNegative()) {
throw new IllegalArgumentException("retryAfter");
}
});
}
exit=0
$ grep -rn 'ProviderResults.retryAfter' --include=*.java adapter/outbound/notification/src/main
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/ses/SesFailureClassifier.java:37: ProviderResults.retryAfter(response.header("retry-after")),
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/ses/SesFailureClassifier.java:49: response.statusCode(), ProviderResults.retryAfter(response.header("retry-after")));
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/webpush/WebPushFailureClassifier.java:39: status, ProviderResults.retryAfter(response.header("retry-after")));
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/twilio/TwilioFailureClassifier.java:35: ProviderResults.retryAfter(response.header("retry-after")),
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/twilio/TwilioFailureClassifier.java:39: response.statusCode(), ProviderResults.retryAfter(response.header("retry-after")));
exit=0
=== 8.3 첨부 무결성: resolve 안에 verify 가 있는가 ===
$ grep -n 'expectedSize\|expectedDigest\|isEqual' application-core/src/main/java/dev/caskeleton/application/notification/platform/dispatch/AttachmentIntegrityGuard.java
35: if (attachment.size() != reference.expectedSize()) {
39: if (!constantTimeEquals(attachment.digest(), reference.expectedDigest())) {
64: return java.security.MessageDigest.isEqual(left, right);
exit=0
=== 8.4 헬스: 어떤 런타임 상태가 unhealthy 인가 ===
$ grep -n 'healthy = false' -B 3 adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/observation/NotificationHealthReporter.java
55- for (ProviderProfileId profileId : monitoredProfiles) {
56- var runtime = runtimes.find(profileId);
57- if (runtime.isEmpty()) {
58: healthy = false;
--
63- ProviderRuntimeState state = runtime.get().state();
64- if (state == ProviderRuntimeState.AUTHENTICATION_FAILED
65- || state == ProviderRuntimeState.DISABLED) {
66: healthy = false;
--
76- // A platform with providers but no route accepts every request and delivers none. It was
77- // reported healthy because every runtime was healthy — which was true and beside the point.
78- if (!monitoredProfiles.isEmpty() && routedChannels.isEmpty()) {
79: healthy = false;
--
81-
82- NotificationServingState serving = servingState.currentState();
83- if (thresholds.exceededBy(serving)) {
84: healthy = false;
exit=0