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>
200 lines
16 KiB
Plaintext
200 lines
16 KiB
Plaintext
Sub-scope 03 (cache-redis) sdk/api/key + error + command + reactive — manifest and probes
|
|
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
|
generatedAt=2026-08-30T04:41:02+00:00
|
|
|
|
=== OWNED FILES ===
|
|
7d962093375f0c36434d719a09907a754381fba3 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/command/AdvancedOperationPermit.java 19
|
|
e4a4f7d278375104757f5ecd0a3c346d5ca7da31 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/command/CommandAccess.java 17
|
|
131e7c9bbdd7a2e23f0d8d0957c72c37cb6c585d main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/command/CommandId.java 85
|
|
6112df8fa18da163230367f89313984aba55c367 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/command/CommandSupport.java 17
|
|
360a583fbefc2bdd84587bb477ece62a5e20a2a5 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/command/KeySpec.java 76
|
|
fe9a3abb85761461514966f8d1405ee531625cd8 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/command/MultiKeyPermit.java 16
|
|
05028e3b1776e895906398640fb1dcbabedef274 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/command/OperationBudget.java 54
|
|
c37cf3b690f18a08ada8832eab4cfe3994dd8148 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/command/PersistentKeyPermit.java 16
|
|
4bf3808adfef86de7ef1052a3141a7c8fedeec4c main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/command/RedisCommandDescriptor.java 60
|
|
afb4b1bf976b42e9688efc76cea1aacded73d96c main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/command/RedisPermitVerifier.java 34
|
|
169daebddd2191ef5d74820b224aad5561ec9133 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/command/RedisPolicyAuthority.java 34
|
|
81a060369f496b1d00cc107b8d299b0acef1052a main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/command/RedisRiskLevel.java 31
|
|
447e777029e7682f6db4560a3fad669d4ee7861c main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/command/TimeoutProfile.java 42
|
|
2cbe99146bf79f371b9e3a171645cf5efa168c02 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/error/RedisAccessDeniedException.java 28
|
|
50d3fdc25bd971caa27a8491939a62b13210d1fa main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/error/RedisAmbiguousExecutionException.java 31
|
|
582d3dc835dee3ac3e3e1f82a79b1086dd335ab1 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/error/RedisBusyException.java 28
|
|
d3e2640731f096877327f5b001786ee25eb911a4 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/error/RedisCapabilityUnavailableException.java 29
|
|
4c40194c7e1f58b3c86c2d6b600e7cdb8ddc9121 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/error/RedisCommandRejectedException.java 29
|
|
c4a8e084d5670092265d860b93a316004bccdf35 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/error/RedisConnectionException.java 28
|
|
3fc7929765ff43d2f45c295e6dcc45b137595aea main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/error/RedisCrossSlotException.java 28
|
|
0572a35da76bd519b6a330bf6e7d18ed627e775f main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/error/RedisDataTypeMismatchException.java 29
|
|
a402d3b6c7370647521d0168e1f4e8eaaa3f7615 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/error/RedisFailureMetadata.java 106
|
|
a96ed0bf7af82ec61fcb1b04d0728c54def23140 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/error/RedisNoScriptException.java 28
|
|
d558d87666acd779b42692013bb760890f65b62b main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/error/RedisOperationException.java 58
|
|
11985da1f6c97dbeb55d7337c5677255ec990c50 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/error/RedisRedirectionException.java 28
|
|
4f6cc4740449cc386983c7d561bbd50570783fb5 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/error/RedisSerializationException.java 32
|
|
db98a87e30b031319274ca50008877b6d7531994 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/error/RedisTimeoutException.java 28
|
|
3b16f5498a0845e2b229434dd97b7f60010f9162 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/key/BitmapKey.java 17
|
|
eb8c5391da357f39ef13dfda4f62a1551bd551d2 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/key/GeoKey.java 19
|
|
e73bac153261577fc9be192a7d88df4e8b932429 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/key/HashKey.java 24
|
|
46d7ff5dfd5dc47c1e16264fe84bd07c79ba0751 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/key/HyperLogLogKey.java 20
|
|
21f1498ac07bbdfabc99b57dbb809f46d1a9c226 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/key/ListKey.java 20
|
|
c572cb9c1d6c4ddddfcb8d47fd0506c098094847 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/key/QualifiedRedisKey.java 49
|
|
5dfaa3350559cff62d0ea587d964ed4d38dc9ee5 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/key/RedisKeyName.java 15
|
|
f9ec9cd3ac14adcd50f1816fef01fa689ded8370 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/key/RedisKeyRenderer.java 69
|
|
55a7b9d74b3c8e48e872de189f318ab475edad1a main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/key/RedisKeyRules.java 101
|
|
56c73270d657b105ffd1454671efad1cddfded65 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/key/RedisNamespace.java 29
|
|
6203eeb20f8e52ef8fc25cd569f121fa67e4c113 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/key/RedisSlotTag.java 17
|
|
3b6ab0a06a2b425f0b56c189722f70291fd58e60 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/key/RedisTypedKey.java 26
|
|
73238fa9db52df2498392e599c5e92e2fd966e69 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/key/SetKey.java 19
|
|
fd2d1444bca91ed05c71eb1cbb60a1fcf035a440 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/key/SortedSetKey.java 20
|
|
9de8ed5736db8ea61abecbfc3a673afa4c889518 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/key/StreamKey.java 20
|
|
ce887cd0141b876bdf057fc3e791271886e34aa1 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/key/TypedRedisKeys.java 217
|
|
4a0371eb46afe4aa9bc3bf866bf456f86c00a1e1 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/key/ValueKey.java 20
|
|
544c98b1e8070aceec7cb0ac72145a9738c57454 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/reactive/ReactiveRedisBatchOperations.java 19
|
|
fb776ae0d021527e7d943c2caf2827ef7d254cb4 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/reactive/ReactiveRedisBitFieldOperations.java 28
|
|
a58ae7b617c8a459906224680ff6571a08aeb76f main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/reactive/ReactiveRedisBitmapOperations.java 69
|
|
6168d8c02497f01f218aef7b96d24c3173f3a1bb main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/reactive/ReactiveRedisBlockingListOperations.java 48
|
|
e11b517f2a1921b5b40f5d79eee79b9eb3d41922 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/reactive/ReactiveRedisBlockingStreamOperations.java 46
|
|
2f6a10d87deea0ac91617bb01c37f0229fc0b5e5 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/reactive/ReactiveRedisGeoOperations.java 82
|
|
598e00d8481b8fe1aef4ce9253f6e4a7a167258c main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/reactive/ReactiveRedisHashFieldExpirationOperations.java 51
|
|
269ce5a7289e3e771e47c20e317929d0dffe6d17 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/reactive/ReactiveRedisHashOperations.java 150
|
|
fe7a26f6e8c33e39b8bfde63fe4eed0ef1b689b4 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/reactive/ReactiveRedisHyperLogLogOperations.java 42
|
|
682733bb7aea5fa65521d6ecda60256bb1e9428f main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/reactive/ReactiveRedisKeyOperations.java 133
|
|
f895b644161fbfbb40c29e107ab87fdbb1a51586 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/reactive/ReactiveRedisListOperations.java 160
|
|
8f8c69eb989cfd1d1537e4729b939aa6dabeef7c main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/reactive/ReactiveRedisPubSubOperations.java 44
|
|
650a75218a60ae8a655551f86070b728601cea8b main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/reactive/ReactiveRedisSetOperations.java 168
|
|
1b61d246aa7b5cb154566e7765a0617d373b1052 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/reactive/ReactiveRedisShardedPubSubOperations.java 29
|
|
26655ff49e817146815e436d1facaf97099d819a main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/reactive/ReactiveRedisSortedSetOperations.java 196
|
|
5443d0091ca147737ba0932d5ebbb8f25bc1efd9 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/reactive/ReactiveRedisStreamOperations.java 202
|
|
621356218e9051ba5b09709f6cf57c40889369e5 main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/reactive/ReactiveRedisValueOperations.java 163
|
|
d5b25f5ff10a32318609756fb03006d76faa6982 test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/command/OperationBudgetTest.java 36
|
|
8576733068fdf68d13b00d8b54d0952bbbb65225 test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/key/RedisKeyRendererTest.java 64
|
|
5439821ee078d9eee8a0a327e529ae1ce5e1f517 test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/key/RedisKeyRulesTest.java 69
|
|
|
|
main: 61 test: 3
|
|
=== 8.1 도달 불가 검증 분기: requireIdentifier의 다섯 검사 ===
|
|
$ sed -n '22,30p;60,80p' adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/key/RedisKeyRules.java
|
|
|
|
private static final Pattern IDENTIFIER = Pattern.compile("^[A-Za-z0-9][A-Za-z0-9._~-]{0,127}$");
|
|
|
|
private static final Pattern JSON_WEB_TOKEN =
|
|
Pattern.compile("^[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}$");
|
|
|
|
private static final Pattern INTERNATIONAL_PHONE = Pattern.compile("^\\+\\d[\\d.~-]{7,}$");
|
|
|
|
private RedisKeyRules() {
|
|
if (value == null || !IDENTIFIER.matcher(value).matches()) {
|
|
throw new IllegalArgumentException(
|
|
"identifier must be 1..128 characters of [A-Za-z0-9._~-] and must not contain a"
|
|
+ " key separator");
|
|
}
|
|
String lowerCase = value.toLowerCase(Locale.ROOT);
|
|
if (value.indexOf('@') >= 0) {
|
|
throw new IllegalArgumentException("identifier must not contain a mail address");
|
|
}
|
|
if (JSON_WEB_TOKEN.matcher(value).matches()) {
|
|
throw new IllegalArgumentException("identifier must not contain a JSON web token");
|
|
}
|
|
if (INTERNATIONAL_PHONE.matcher(value).matches()) {
|
|
throw new IllegalArgumentException("identifier must not contain a phone number");
|
|
}
|
|
if (lowerCase.startsWith("bearer") || lowerCase.startsWith("eyj")) {
|
|
throw new IllegalArgumentException("identifier must not contain authentication material");
|
|
}
|
|
return value;
|
|
}
|
|
|
|
exit=0
|
|
|
|
--- IDENTIFIER 문자 클래스가 '@'와 '+'를 이미 배제하는가 ---
|
|
$ grep -n 'IDENTIFIER =' adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/key/RedisKeyRules.java
|
|
23: private static final Pattern IDENTIFIER = Pattern.compile("^[A-Za-z0-9][A-Za-z0-9._~-]{0,127}$");
|
|
exit=0
|
|
|
|
$ grep -n 'rejectsEmailInIdentifier' -A 5 adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/key/RedisKeyRulesTest.java
|
|
12: void rejectsEmailInIdentifier() {
|
|
13- assertThatThrownBy(() -> new RedisKeyName("user", "person@example.com"))
|
|
14- .isInstanceOf(IllegalArgumentException.class);
|
|
15- }
|
|
16-
|
|
17- @Test
|
|
exit=0
|
|
|
|
$ grep -n 'rejectsInternationalPhoneNumbersInIdentifier' -A 8 adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/key/RedisKeyRulesTest.java
|
|
31: void rejectsInternationalPhoneNumbersInIdentifier() {
|
|
32- assertThatThrownBy(() -> new RedisKeyName("contact", "+821012345678"))
|
|
33- .isInstanceOf(IllegalArgumentException.class);
|
|
34- }
|
|
35-
|
|
36- @Test
|
|
37- void rejectsSeparatorInjectionInEveryKeyPart() {
|
|
38- assertThatThrownBy(() -> new RedisKeyName("user", "1:2"))
|
|
39- .isInstanceOf(IllegalArgumentException.class);
|
|
exit=0
|
|
|
|
=== 8.1b 선언되었으나 읽히지 않는 술어 ===
|
|
$ git grep -n 'requiresPermit()' -- 'adapter/outbound/cache-redis/src'
|
|
adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/command/RedisRiskLevel.java:19: public boolean requiresPermit() {
|
|
exit=0
|
|
|
|
$ git grep -n 'deniedToApplications()' -- 'adapter/outbound/cache-redis/src'
|
|
adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/command/RedisRiskLevel.java:28: public boolean deniedToApplications() {
|
|
adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/CommandPolicyGuard.java:126: if (policy.riskLevel().deniedToApplications()
|
|
adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/raw/RawCommandApprovals.java:116: if (policy.riskLevel().deniedToApplications()) {
|
|
exit=0
|
|
|
|
=== 8.2 sync/reactive 대칭: getRange 포함 실제 대조 ===
|
|
$ grep -c ';' adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/operations/RedisValueOperations.java adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/reactive/ReactiveRedisValueOperations.java
|
|
adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/operations/RedisValueOperations.java:20
|
|
adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/reactive/ReactiveRedisValueOperations.java:25
|
|
exit=0
|
|
|
|
$ diff <(grep -oE '[a-z][A-Za-z]*\(' adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/operations/RedisValueOperations.java | sort -u) <(grep -oE '[a-z][A-Za-z]*\(' adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/reactive/ReactiveRedisValueOperations.java | sort -u)
|
|
exit=0
|
|
|
|
=== 8.2b RedisCommandDescriptor 교차 필드 불변식 ===
|
|
$ grep -n 'throw new IllegalArgumentException' -B 4 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/command/RedisCommandDescriptor.java
|
|
30- Objects.requireNonNull(access, "access must be non-null");
|
|
31- Objects.requireNonNull(keySpec, "key specification must be non-null");
|
|
32- Objects.requireNonNull(timeoutProfile, "timeout profile must be non-null");
|
|
33- if (support == CommandSupport.BLOCKED && access != CommandAccess.NONE) {
|
|
34: throw new IllegalArgumentException("a blocked command must not carry an ACL account");
|
|
35- }
|
|
36- if (riskLevel == RedisRiskLevel.R4 && support != CommandSupport.BLOCKED) {
|
|
37: throw new IllegalArgumentException("an R4 command must be blocked");
|
|
38- }
|
|
39- if (riskLevel == RedisRiskLevel.R3
|
|
40- && support != CommandSupport.ADMIN_ONLY
|
|
41- && support != CommandSupport.BLOCKED) {
|
|
42: throw new IllegalArgumentException("an R3 command must be admin-only or blocked");
|
|
43- }
|
|
44- if (!readOnly && retrySafe && mayBeAmbiguous) {
|
|
45: throw new IllegalArgumentException(
|
|
exit=0
|
|
|
|
=== 8.3 실패 메타데이터: 모호 실행과 재시도의 배타 ===
|
|
$ grep -n 'ambiguousExecution\|retryable\|storedDataCorruption\|notSent' adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/error/RedisFailureMetadata.java | head -20
|
|
21: boolean retryable,
|
|
22: boolean ambiguousExecution,
|
|
41: if (retryable && ambiguousExecution) {
|
|
42: throw new IllegalArgumentException("an ambiguous execution must never be marked retryable");
|
|
61: * <p>Deliberately not {@link #notSent}: that factory derives {@code retryable} from {@code
|
|
71: * @return metadata marked not retryable and not ambiguous
|
|
73: public static RedisFailureMetadata storedDataCorruption(
|
|
90: public static RedisFailureMetadata notSent(
|
|
exit=0
|
|
|
|
$ git grep -n 'storedDataCorruption(' -- 'adapter/outbound/cache-redis/src/main' | head
|
|
adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/error/RedisFailureMetadata.java:73: public static RedisFailureMetadata storedDataCorruption(
|
|
adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/codec/JsonEnvelopeFraming.java:202: RedisFailureMetadata.storedDataCorruption(
|
|
adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/codec/VersionedJsonCodec.java:103: RedisFailureMetadata.storedDataCorruption(
|
|
exit=0
|
|
|
|
=== 8.4 예외 계층: transient metadata ===
|
|
$ grep -n 'transient\|serialVersionUID' adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/error/RedisOperationException.java
|
|
14: private static final long serialVersionUID = 1L;
|
|
16: private final transient RedisFailureMetadata metadata;
|
|
exit=0
|
|
|
|
$ git ls-files 'adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/error' | wc -l
|
|
14
|
|
exit=0
|
|
|