Files
document-haness/.run/redis/redis-advanced-surfaces.md
T

37 KiB

Batch·Transaction·Script·Function·Pub/Sub·Admin·Raw를 분리한 이유

Redis 코드 상세 시리즈 11/20 · 전체 지도 · 이전: 문자열 명령 대신 타입을 노출하는 RedisOperations 코드 지도 · 다음: Timeout 뒤 쓰였는지 모를 때: Executor와 실행 확실성 모델

이 글이 답하는 코드 질문

왜 advanced 기능을 RedisOperations 하나에 모두 넣지 않았으며, 각 surface는 어떤 connection·ACL·배포 계약을 가집니까?

이 분리는 기능 이름보다 failure mode와 ownership 차이에서 나옵니다.

  • batch는 pipeline 최적화이며 atomic하지 않습니다.
  • transaction은 한 connection의 WATCH/MULTI/EXEC 상태를 독점합니다.
  • script는 process에 등록한 source를 first use에 SCRIPT LOAD하고 NOSCRIPT에서 한 번 복구합니다.
  • function은 application이 load하지 않고 이미 배포된 library를 FCALL합니다.
  • Pub/Sub subscription은 long-lived connection lifecycle입니다.
  • admin은 read-only diagnostic account와 projection을 사용합니다.
  • raw는 catalog와 deployment approval이 모두 허용한 command만 실행합니다.

세부 class와 테스트는 있지만 이 surface들의 production bean 조립은 확인되지 않습니다.

먼저 보는 클래스·리소스 지도

surface 진입점 connection·권한 핵심 결과
Batch RedisBatchOperations ordinary guarded calls, batch bounds ordered per-item result
Transaction RedisTransactionOperations exclusive TRANSACTION lane executed/conflict, attempts
Script LettuceRedisScriptOperations scripting grant, guarded EVALSHA decoded script reply
Function LettuceRedisFunctionOperations capability-gated FCALL/FCALL_RO decoded function reply
Pub/Sub LettuceRedisPubSubOperations dedicated PUBSUB gateway publish count/subscription
Admin LettuceRedisAdminOperations own connection, admin-readonly account bounded/redacted diagnostic
Raw LettuceRedisRawGateway raw account 의도, catalog+approval caller decoder result
Extensions extension package의 LettuceRedis*Operations probed module capability JSON/TS/probabilistic/search

Connection lane은 API 모양과 함께 읽습니다

RedisConnectionKindREGULAR, BLOCKING, TRANSACTION, SCRIPT, PUBSUB, ADMIN 여섯 lane을 정의합니다.

다음 failure mode는 한 pool에 섞기 어렵습니다.

  • blocking command는 server block이 끝날 때까지 connection을 점유합니다.
  • transaction은 MULTI 이후 connection-local state를 가집니다.
  • subscribed connection은 ordinary command에 사용할 수 없습니다.
  • script와 admin은 application traffic과 다른 privilege가 필요합니다.
  • long-lived subscription close는 one-shot command reply와 lifecycle이 다릅니다.

다만 RedisConnectionKind.forCommand은 descriptor만으로 blocking/admin/regular을 정합니다. transaction, script, Pub/Sub의 실제 전용 connection 선택은 각 surface 조립이 맡아야 합니다. 이 조립은 production에서 확인되지 않습니다.

Batch: pipeline이지 transaction이 아닙니다

RedisBatchOperations은 세 가지를 명시합니다.

  • command는 독립적으로 성공하거나 실패할 수 있습니다.
  • 다른 client의 command가 사이에 실행될 수 있습니다.
  • write batch를 자동 retry하지 않습니다.

LettuceRedisBatchOperationsBatchExecution에 실행을 위임합니다. 외부에서 구현한 RedisBatch는 받지 않고 SDK builder가 만든 batch인지 확인합니다.

호출 흐름은 다음과 같습니다.

  1. builder가 item별 CommandRequest를 보존합니다.
  2. batch 자체 command count와 request bytes를 선검사합니다.
  3. 각 item을 guard에 미리 admission하면서 declared expectedReplyBytes를 합산하고 batch reply ceiling과 비교합니다.
  4. 한 item이 거절되거나 declared 합계가 ceiling을 넘으면 어느 item도 보내지 않습니다.
  5. dispatch는 in-flight bound와 batch/item timeout 중 짧은 값을 적용합니다.
  6. 전송 뒤에는 item별 success/failure를 input order로 수집합니다.
  7. decoded reply shape의 근사 누적값이 ceiling을 넘으면 그 지점의 item을 failure로 기록할 수 있습니다.

BatchExecution.measure는 driver가 이미 decode한 결과를 셉니다. byte[]는 길이, CharSequencelength(), collection과 map은 요소의 재귀 합계, unknown scalar는 1입니다. wire protocol의 byte 수를 계측하는 코드가 아니므로 이름이 observedReplyBytes여도 exact reply bytes로 읽으면 안 됩니다.

정상 결과에 partial failure flag가 있다는 사실은 atomicity가 없다는 API 신호입니다.

Transaction: rollback이 아니라 optimistic concurrency입니다

RedisTransactionOperations은 Redis transaction이 rollback하지 않는다고 명시합니다. EXEC 안의 한 command가 runtime error여도 다른 queued command는 실행될 수 있습니다.

LettuceRedisTransactionOperations.watchAndExecute의 흐름은 다음과 같습니다.

sequenceDiagram
    participant A as Caller
    participant T as TransactionOperations
    participant Q as QueueingExecutor
    participant R as Redis gateway
    A->>T: watched keys, callback, options
    T->>T: Cluster same-slot 선검사
    T->>Q: WATCH request admission/issue
    T->>R: MULTI
    T->>A: queue callback 실행
    A->>Q: typed queued commands
    Q->>R: +QUEUED, reply는 아직 미확정
    T->>R: EXEC
    alt executed
      R-->>T: replies
      T->>T: QueuedReply available 표시
    else watched key changed
      R-->>T: null/conflict
      T->>T: attempt 상한까지 재시도
    end

runOnce는 callback이나 guard가 실패해도 open window를 DISCARD하고, commit 뒤 watch가 남으면 UNWATCH합니다.

Cluster에서는 watched key와 queued write key를 attempt 단위로 누적해 same-slot인지 확인합니다. command 하나씩 보면 합법이어도 transaction 전체가 cross-slot일 수 있기 때문입니다.

QueueingRedisCommandExecutor+QUEUED에서 성공 observation을 기록하지 않습니다. reply stage가 EXEC에서 resolve될 때 성공/실패를 기록합니다.

transaction queue의 TTL 계약도 ordinary value API와 같지 않습니다. transaction set은 expiration을 받지만 Queue.increment은 plain INCRBY를 enqueue합니다. absent key면 persistent counter가 만들어질 수 있습니다. 같은 queue의 hash/list/set/zset write도 expiration이나 persistent permit을 받지 않습니다.

Script: 등록과 server load는 같은 시점이 아닙니다

RedisScriptRegistry.register는 process 안에서 reviewed script identity와 source를 등록합니다. 같은 id에 다른 body를 재등록하면 실패합니다.

하지만 register는 Redis에 SCRIPT LOAD를 보내지 않습니다. server load는 digest이 처음 호출되어 cache miss가 났을 때 수행합니다.

flowchart TD
    A[process setup: register script object] --> B[first execute]
    B --> C{digest cache hit인가}
    C -- 아니요 --> D[SCRIPT LOAD]
    D --> E[digest cache 저장]
    C -- 예 --> F[EVALSHA]
    E --> F
    F --> G{NOSCRIPT인가}
    G -- 아니요 --> H[result decode]
    G -- 예 --> I[digest forget]
    I --> J[SCRIPT LOAD 후 EVALSHA 한 번 재실행]

LettuceRedisScriptOperations.execute는 key가 비어 있거나 maxKeys를 넘으면 거절합니다. key는 namespace와 same-slot 검사를 받으며 request/reply/timeout budget도 붙습니다. 다만 EVALSHA requestexpectedReplyBytes는 0이고, decoder 호출 전 관측 reply 크기를 검사하지 않습니다. maxReplyBytes가 budget에 저장된다는 사실만 확인되며 실제 reply ceiling 집행은 빠져 있습니다.

NOSCRIPT만 자동 복구합니다. server가 EVALSHA 실행 전에 script 부재를 답했으므로 reload와 1회 재호출이 ambiguous write retry는 아닙니다. 다른 failure는 자동 재호출하지 않습니다.

RedisScriptRegistry class comment의 “registration is a deployment step”은 process registration을 뜻한다고 좁혀 읽어야 합니다. 실제 Redis SCRIPT LOAD는 first use입니다.

Function: deployment-time library와 request-time call을 나눕니다

RegisteredRedisFunction은 library, semantic version, function name, max keys, timeout, reply ceiling, read-only flag, decoder를 가집니다.

application surface에는 FUNCTION LOAD가 없습니다. policy에서 FUNCTION LOAD는 admin-only이며, LettuceRedisFunctionOperations은 probed FUNCTIONS capability가 있을 때만 instance를 만듭니다.

request-time에는 다음만 수행합니다.

  1. key가 1개 이상이고 declared maxKeys 이내인지 확인합니다.
  2. key와 arguments를 encode하고 request size를 계산합니다.
  3. reply ceiling과 timeout으로 OperationBudget을 만듭니다.
  4. read-only면 FCALL_RO, 아니면 FCALL을 선택합니다.
  5. guard admission 후 function name으로 call합니다.

function requestfunction.maxReplyBytes()로 budget을 만들지만 expectedReplyBytes는 0입니다. decoder 호출 앞에 관측 reply budget 검사가 없습니다.

script와 달리 function not found에서 library를 load하는 recovery가 없습니다. function library는 배포 pipeline이 먼저 설치해야 합니다.

현재 call path는 RegisteredRedisFunction.library()version()을 server request에 넣거나 server-side library metadata와 대조하지 않습니다. 실제 gateway 호출function.name()만 전달합니다. record가 version을 보유한다는 것과 runtime deployment check가 구현됐다는 것은 다릅니다.

Pub/Sub: publish와 subscription lifecycle이 다릅니다

LettuceRedisPubSubOperations은 publish는 guarded command로 보내지만 subscribe는 dedicated gateway로 시작해 caller가 닫아야 하는 Subscription을 반환합니다.

channel subscription은 channel별 codec map을 만듭니다. 여러 channel을 구독하면서 첫 channel codec으로 모든 payload를 decode하지 않습니다. 요청하지 않은 channel message가 오면 codec을 추측하지 않고 실패합니다.

pattern subscription은 concrete channel만 전달받으므로 어느 pattern codec인지 역산할 수 없습니다. 따라서 singleCodec이 모든 pattern의 codec id가 같은지 검사합니다.

sharded Pub/Sub은 capability-gated 별도 surface입니다. ordinary Pub/Sub과 topology routing 의미가 같다고 합치지 않습니다.

Admin: command allowlist가 아니라 projection까지 좁힙니다

LettuceRedisAdminOperations.run은 catalog policy가 ADMIN_ONLY이면서 read-only인지 다시 확인합니다.

노출 기능은 INFO, DBSIZE, MEMORY USAGE, bounded SLOWLOG, LATENCY LATEST, bounded CLIENT projection, CLUSTER INFO, fixed CONFIG GET projection, ACL DRYRUN입니다.

CONFIG GET은 glob을 받지 않고 DIAGNOSTIC_PARAMETERS에 고정된 이름만 요청합니다. 응답에서도 allowlist를 다시 적용하고 secret-shaped parameter name의 value를 redact합니다.

slow log에는 command family만 남기고 arguments를 버립니다. client projection에는 peer address와 connection name을 넣지 않습니다.

admin run은 OperationBudget을 request에 붙이지만 expectedReplyBytes를 0으로 두고 raw list를 그대로 반환합니다. projection별 count 상한은 있어도 실제 reply byte ceiling을 공통으로 집행하는 호출은 없습니다.

Raw: 두 개의 독립된 승인이 필요합니다

RawCommandApprovals은 두 조건을 모두 요구합니다.

  1. organization catalog가 command를 RAW_ONLY로 분류했습니다.
  2. deployment가 concrete ApprovedRawCommand를 등록했습니다.

approval은 policy id, command id, max arguments, request/reply ceiling, timeout, decoder를 고정합니다. token은 같은 registry가 같은 policy id에 대해 발급한 concrete instance여야 합니다.

여기서 reply ceiling을 고정한다는 말은 approval과 OperationBudget이 그 숫자를 보유한다는 뜻입니다. raw request의 expectedReplyBytes는 0이고 caller decoder 앞에도 관측 reply 크기 검사가 없어, 실제 ceiling 집행까지 완성되지는 않았습니다.

raw gateway는 argument에서 key를 추출해 bound namespace로 parse합니다. movable key command는 local parser가 정확히 위치를 결정할 수 있는 family만 허용합니다. 모르는 shape를 best guess하지 않습니다.

WAIT는 catalog에 없으므로 raw approval 대상으로도 등록할 수 없습니다. WAIT를 raw escape hatch로 쓸 수 있다는 근거는 없습니다.

Extension module: server capability가 bean 존재를 결정해야 합니다

JSON, Time Series, probabilistic structure, Search extension implementation은 각각 probed capability를 받는 ifSupported factory를 가집니다.

  • JSON path와 value ceiling을 검사합니다.
  • Time Series는 retention과 bounded range를 요구합니다.
  • probabilistic reserve는 error/capacity/compression 등의 bound를 요구합니다.
  • Search index name을 namespace에 묶고 page와 timeout을 요구합니다.

extension 공통 runner는 policy name이 있는 command에만 permit과 collection budget을 붙입니다. null policy path는 permit과 budget이 모두 비어 있습니다. 예를 들어 JSON.SET은 null을 넘기고, bounded JSON.GET은 policy name을 넘깁니다. 두 분기 모두 expectedReplyBytes가 0이며 runner가 반환된 List<Object>를 그대로 넘기므로 관측 reply 검사가 없습니다. bounded read에 budget 객체가 있다는 사실도 reply byte ceiling 집행을 뜻하지 않고, null policy command에는 그 객체조차 없습니다.

RedisExtensionModulesContractTest은 capability가 없으면 fixture에서 instance가 없음을 고정합니다. 이것은 production conditional bean이 실제로 조립됐다는 증거는 아닙니다.

테스트가 고정하는 계약

Batch 계약은 batch ceiling 선검사, refused item의 전체 batch 취소, item permit·budget 보존, foreign batch 거절, decoded shape 근사 누적값의 ceiling 교차 처리을 각각 고정합니다. 마지막 테스트는 ASCII string 사례에서 누적 failure가 나는 계약이며 exact wire-byte 계측을 증명하지 않습니다.

Transaction 계약도 사례별로 나뉩니다.

Script 계약은 first use 실행과 load, digest cache, NOSCRIPT 1회 reload, unregistered script 거절, id/body identity 안정성을 별도 테스트로 고정합니다.

Function 계약은 capability absence, deployed function call, key declaration·상한, semantic version identity을 각각 고정합니다.

Pub/Sub은 subscription close lifecycle, foreign namespace 거절, empty subscription 거절, channel별 codec, pattern mixed codec 거절, sharded capability gate, reactive cancellation cleanup을 서로 다른 테스트가 고정합니다.

Admin은 diagnostic parsing, fixed·redacted config projection, slow log argument 제거, client identity 제거, projection bound, destructive command 차단, ADMIN_ONLY read-only command 한정을 개별 사례로 고정합니다.

Raw는 approved command 실행, RAW_ONLY만 approval 가능, movable key parser 필수, token provenance, registered approval과 token 일치, namespace parse-back, argument ceiling을 각각 고정합니다.

이번 문서 작업에서는 이 테스트를 실행하지 않았습니다. production source와 테스트를 정적으로 대조했습니다.

현재 구현 공백과 잘못 읽기 쉬운 지점

  1. advanced surface implementation은 있지만 production Spring bean 조립은 확인되지 않습니다.
  2. script의 process registration과 Redis server load 시점은 다릅니다. SCRIPT LOAD는 first use입니다.
  3. function은 배포 시 load해야 하며 request-time load/recovery가 없습니다.
  4. RegisteredRedisFunction의 library/version은 call path에서 server deployment와 대조되지 않습니다. Javadoc이 말하는 deployment check 구현도 찾지 못했습니다.
  5. admin class는 FUNCTION LOAD를 public method로 노출하지 않습니다. function deployment는 이 application admin surface 밖의 작업입니다.
  6. raw role credential은 settings에서 해석되지만 RedisConnectionKind에는 RAW lane이 없고 descriptor는 RAW_GATEWAYREGULAR로 매핑합니다. 실제 별도 raw account connection 조립은 확인되지 않습니다.
  7. batch는 atomic하지 않고 transaction은 rollback하지 않습니다.
  8. script, function, raw, admin에는 reply budget 값이 있지만 관측한 reply byte를 decoder 전에 검사하지 않습니다. extension은 policy name이 있을 때만 budget이 있고 null policy path에는 budget 자체가 없으며, 어느 쪽도 관측 reply를 검사하지 않습니다.
  9. batch의 post-decode ceiling은 result shape의 근사 누적값에 적용됩니다. CharSequence.length()와 unknown scalar 1을 사용하므로 exact wire bytes가 아닙니다.
  10. transaction INCRBY와 transaction collection write는 expiration이나 persistent permit 없이 absent key를 만들 수 있습니다.
  11. extension fixture의 ifSupported 조립은 production conditional bean 증거가 아닙니다.

다음에 source를 열 때는 RedisConnectionKind, 각 public interface, implementation, contract test, 마지막으로 production auto-configuration 순으로 보면 됩니다.

시리즈의 관련 문서

관련 범위는 connection lifecycle, command admission, typed operations, execution failure certainty입니다.

시리즈에서 이어 읽기