Files
document-haness/docs/clean-architecture-backend-template/final/evidence/meta/a13-f005-retry-after-illegalargumentexception-negative.json
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

15 lines
5.6 KiB
JSON

{
"assetKey": "a13-f005-retry-after-illegalargumentexception-negative",
"kind": "terminal",
"command": "set -e\nset -o pipefail\nD=$(mktemp -d); trap 'rm -rf \"$D\"' EXIT\nC=/shared/codebase/clean-architecture-backend-template\nN=$C/src/adapter/outbound/notification\nCACHE=/root/.gradle/caches/modules-2/files-2.1\njava -version 2>&1 | head -1\ncp=\"\"\nwhile IFS= read -r line; do\n coord=${line%%=*}; confs=${line#*=}\n case \",$confs,\" in *\",runtimeClasspath,\"*) ;; *) continue ;; esac\n g=${coord%%:*}; rest=${coord#*:}; n=${rest%%:*}; v=${rest##*:}\n jar=$(find \"$CACHE/$g/$n/$v\" -name '*.jar' ! -name '*sources*' ! -name '*javadoc*' 2>/dev/null | head -1)\n [ -n \"$jar\" ] && cp=\"$cp:$jar\"\ndone < <(grep -E '^[a-zA-Z0-9._-]+:[^=]+=' \"$N/gradle.lockfile\")\nCP=$(find \"$N/build/libs\" \"$C/src/application-core/build/libs\" -name '*+21234e38cdb9.jar' | tr '\\n' ':')${cp#:}\ncat > \"$D/RetryAfterProbe.java\" <<'JAVA'\nimport dev.caskeleton.adapter.outbound.notification.platform.provider.ProviderResults;\nimport dev.caskeleton.adapter.outbound.notification.platform.provider.apns.ApnsFailureClassifier;\nimport dev.caskeleton.adapter.outbound.notification.platform.provider.fcm.FcmFailureClassifier;\nimport dev.caskeleton.adapter.outbound.notification.platform.provider.http.NotificationHttpResponse;\nimport dev.caskeleton.adapter.outbound.notification.platform.provider.ses.SesFailureClassifier;\nimport dev.caskeleton.adapter.outbound.notification.platform.provider.twilio.TwilioFailureClassifier;\nimport dev.caskeleton.adapter.outbound.notification.platform.provider.webpush.WebPushFailureClassifier;\nimport java.lang.reflect.InvocationTargetException;\nimport java.lang.reflect.Method;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.function.Function;\n\n/** 같은 Retry-After 헤더를 파서 넷과 분류기 다섯에 넣고 무엇이 나오는지 잰다. */\npublic final class RetryAfterProbe {\n\n private static final String[] HEADERS = {\"30\", \"-30\", \"0\", \"Wed, 21 Oct 2026 07:28:00 GMT\"};\n\n private static NotificationHttpResponse throttled(String header) {\n return new NotificationHttpResponse(\n 429, Map.of(\"retry-after\", List.of(header)), \"{}\".getBytes(StandardCharsets.UTF_8));\n }\n\n private static String outcome(java.util.concurrent.Callable<Object> call) {\n try {\n Object value = call.call();\n if (value instanceof Optional<?> parsed) {\n return parsed.map(Object::toString).orElse(\"빈 값\");\n }\n return String.valueOf(value);\n } catch (Exception thrown) {\n Throwable failure = thrown instanceof InvocationTargetException wrapped ? wrapped.getCause() : thrown;\n return failure.getClass().getSimpleName() + \": \" + failure.getMessage();\n }\n }\n\n private static Method webhookParser() throws Exception {\n Class<?> adapter = Class.forName(\"dev.caskeleton.adapter.outbound.notification.platform\"\n + \".provider.webhook.WebhookNotificationProviderAdapter\");\n Method parser = adapter.getDeclaredMethod(\"retryAfter\", NotificationHttpResponse.class);\n parser.setAccessible(true);\n return parser;\n }\n\n public static void main(String[] args) throws Exception {\n Method webhook = webhookParser();\n FcmFailureClassifier fcm = new FcmFailureClassifier();\n\n System.out.println();\n System.out.println(\"[알림 리프의 파서 셋에 같은 헤더를 넣는다]\");\n for (String header : HEADERS) {\n System.out.println(\" retry-after: \" + header);\n System.out.println(\" ProviderResults.retryAfter \" + outcome(() ->\n ProviderResults.retryAfter(Optional.of(header))));\n System.out.println(\" FcmFailureClassifier.retryAfter \" + outcome(() ->\n fcm.retryAfter(Optional.of(header))));\n System.out.println(\" Webhook 어댑터의 것 \" + outcome(() ->\n webhook.invoke(null, throttled(header))));\n }\n System.out.println();\n\n System.out.println(\"[헤더가 -30 인 429 응답을 분류기 넷에 넣는다]\");\n NotificationHttpResponse negative = throttled(\"-30\");\n record Case(String name, Function<NotificationHttpResponse, Object> run) {}\n List<Case> cases = List.of(\n new Case(\"SesFailureClassifier\", r -> new SesFailureClassifier().classify(r)),\n new Case(\"TwilioFailureClassifier\", r -> new TwilioFailureClassifier().classify(r)),\n new Case(\"WebPushFailureClassifier\", r -> new WebPushFailureClassifier().classify(r)),\n new Case(\"ApnsFailureClassifier\", r -> new ApnsFailureClassifier().classify(r)));\n for (Case one : cases) {\n String result = outcome(() -> one.run().apply(negative));\n System.out.println(\" \" + one.name());\n System.out.println(\" \" + (result.length() > 118 ? result.substring(0, 118) + \" …\" : result));\n }\n }\n}\nJAVA\njavac -encoding UTF-8 -cp \"$CP\" -d \"$D\" \"$D/RetryAfterProbe.java\"\njava -Dstdout.encoding=UTF-8 -cp \"$CP:$D\" RetryAfterProbe\n",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T16:28:48+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a13-f005-retry-after-illegalargumentexception-negative.txt",
"svg": "evidence/rendered/a13-f005-retry-after-illegalargumentexception-negative.svg",
"rawSha256": "9e0d03cfad03369251269723d06c8d1b9bc79399d9b80746df779ecfb98ad62a",
"lines": 29,
"redaction": "none — 헤더 값과 응답 본문은 이 프로브가 만든 합성 값이다"
}