콘텐츠로 이동

Location Store provider SPI와 공식 Redis 구현

Location·Relocation 주제 목차 · 스펙 목차 · 이전: 01. Location runtime · 다음: 03. Relocation Store (Redis)

Provider가 지켜야 하는 공개 SPI(조건부 commit, 페이지 제한 snapshot)와, 공식 Redis 구현이 언어 간 상호 운용을 위해 반드시 따라야 하는 key·byte 형식을 정의한다.

1. 범위와 독자

Message를 받는 논리 대상인 Spot과 Actor의 현재 owner와 상태를 여러 node가 함께 확인할 수 있도록 보관하는 저장소가 Location Store다. 이 문서는 그 provider를 구현하는 개발자가 지켜야 하는 계약을 정의한다. Provider는 Framework가 만든 opaque key와 bytes를 저장한다. 여러 key의 조건 검사와 변경은 하나의 commit으로 적용한다. 복구용 snapshot은 page 크기를 제한해 제공한다.

Provider가 Actor·Spot authority, owner lease, placement reservation, aggregate commit과 relocation phase의 의미를 알아야 하는 것은 아니다. Framework가 이 SPI를 사용해 해당 상태를 구성하는 방법과 Store 등록 조건은 Location runtime이 소유한다. 이 문서는 그 동작을 반복하지 않는다.

Application은 이 SPI의 operation을 직접 호출하지 않는다. Provider package만 SPI를 구현하며 Framework가 등록된 instance를 사용한다.

이 문서는 두 층의 계약을 함께 담는다. §2~§6은 Location Store를 구현하는 모든 provider(Redis가 아닌 provider 포함)가 지켜야 하는 SPI 계약이다 — 코드가 이와 다르면 코드를 고친다. §8~§9는 공식 Redis provider가 사용하는 Redis 자체의 key 배치와 data type을 정하는 구현 계약이다. 이 중 §8~§9에서 서로 다른 언어의 Redis provider가 상호 읽어야 하는 부분(counter 발급 key, 5개 record의 opaque 저장 형식)은 MUST-level 공개 계약이며 코드가 이와 다르면 코드를 고친다 — Redis가 아닌 provider는 이 부분을 따를 필요가 없다. 그 밖에 Redis provider가 내부적으로 선택하는 세부 구현(Lua script 분할, connection 관리)은 §9 끝에서 명시적으로 구분하며, 이 부분은 구현이 바뀌면 문서를 코드에 맞춘다.

2. 공개 SPI의 책임

Location Store SPI는 다음 세 operation군만 제공한다.

Operation군 Provider가 보장하는 결과
Exact read 하나의 opaque key에 대해 현재 bytes, provider version, optional expiry와 StoreNow를 같은 관측으로 반환한다.
Conditional atomic batch 모든 condition이 참일 때만 모든 mutation을 하나의 commit으로 적용한다.
Snapshot scan 첫 page에서 고정한 snapshot을 정해진 page 크기와 opaque cursor로 이어 읽는다.

SPI type과 interface가 따르는 provider abstraction package 경계는 Location runtime §2.1이 정의한다. 공개 SPI에는 descriptor, authority, reservation, capacity, aggregate, lease와 change-stamp별 public method나 DTO를 추가하지 않는다. Redis command, key layout, script와 private record encoding도 공개 SPI에 노출하지 않는다.

다음 .NET 발췌는 공통 SPI의 최소 모양을 보여준다. 정식 선언은 .NET 언어별 interface에 있다.

public interface IZLinkLocationStore
{
    ValueTask<ZLinkStoreReadResult> ReadAsync(
        ZLinkStoreKey key,
        CancellationToken cancellationToken = default);

    ValueTask<ZLinkStoreWriteResult> WriteAsync(
        ZLinkStoreWriteRequest request,
        CancellationToken cancellationToken = default);

    ValueTask<ZLinkStoreScanResult> ScanAsync(
        ZLinkStoreScanRequest request,
        CancellationToken cancellationToken = default);
}

다른 언어의 정식 모양은 Java, Kotlin, Node.jsC++ 언어별 interface를 따른다. 두 Store의 등록 예제는 Location runtime §2에만 둔다.

3. Key, value, version과 clock

항목 계약
Key Framework가 발급하는 opaque UTF-8 1..1024 bytes다. 대소문자를 구분해 그대로 비교하며 normalization과 case folding을 적용하지 않는다.
Value 최대 1 MiB인 bytes다. Commit 뒤에는 해당 version이 교체되거나 삭제될 때까지 변경되지 않는다. Expiry가 없으면 explicit delete까지 유지한다.
Version Provider가 발급하는 opaque UTF-8 1..4096 bytes다. Framework는 값의 크기나 내부 구성을 해석하지 않는다.
StoreNow Read, commit과 scan page가 기준으로 삼는 provider wall clock이다. TTL과 expiry correctness는 이 시각만 사용한다.

Exact read는 Missing(StoreNow) 또는 Found(bytes, version, optional expiry, StoreNow)를 반환한다. 만료된 value는 Missing으로 반환한다. Provider는 consumer가 read result를 사용하는 동안 bytes를 변경하거나 다른 result buffer에 재사용하지 않는다.

Framework domain generation은 provider version과 다르다. Provider는 domain counter를 해석하거나 별도 generation API를 제공하지 않는다.

4. Conditional atomic batch

Write request는 condition 집합과 mutation 집합으로 구성한다.

  • Missing(key)는 key가 없거나 만료된 경우에만 참이다.
  • Version(key, expected)는 current version이 expected와 같은 경우에만 참이다.
  • Put(key, bytes, optional retention)은 새 opaque version을 발급한다.
  • Delete(key)는 key를 제거한다.

Provider는 모든 condition을 먼저 검사한다. 모두 참일 때만 모든 mutation을 하나의 commit으로 적용한다. 다른 caller는 commit의 중간 상태를 관찰할 수 없다. Condition 하나라도 거짓이면 Conflict를 반환하고 mutation과 version 증가는 0이다. Conflict는 실패한 condition이나 current value를 반환하지 않는다.

Batch에는 다음 bound를 적용한다.

  • Condition과 mutation에 나타난 unique key 합계는 최대 2,048개다.
  • Encoded request는 최대 4 MiB다.
  • 같은 key를 condition 안에서 또는 mutation 안에서 두 번 사용하지 않는다.
  • Applied는 각 Put의 opaque version과 commit에서 관측한 하나의 StoreNow를 반환한다.

User Spot participant 전체를 이 batch 하나에 넣지 않는다. Framework는 이동 대상 목록 페이지 하나당 최대 1,024개 항목과 encoded 1 MiB로 제한한 immutable inventory chunk를 미리 저장한다 — 이 페이지 상한은 위 CAS batch의 unique key 합계 상한과는 다른 대상에 적용되는 별개의 수치다. 마지막 batch에는 aggregate authority, inventory root·count·digest와 capacity counter처럼 공개 시점에 함께 바뀌어야 하는 작은 record만 넣는다.

따라서 한 User Spot에 속할 수 있는 Actor 총수는 batch의 2,048-key 제한으로 정하지 않는다. Provider는 inventory chunk, participant와 aggregate의 의미를 해석하지 않는다.

5. 크기를 제한한 snapshot scan

Snapshot scan은 복구와 maintenance가 Framework record를 제한된 크기로 읽게 한다.

  • Prefix는 UTF-8 0..1024 bytes이며 key와 같은 방식으로 대소문자를 구분해 그대로 비교한다.
  • 첫 page 요청에는 cursor가 없다. Provider는 크기를 제한한 snapshot을 만들고 다음 page가 있으면 opaque cursor를 반환한다.
  • 같은 cursor의 다음 page는 처음 고정한 snapshot만 읽는다.
  • Page limit은 1..1000이고 encoded page 크기는 최대 4 MiB다.
  • Cursor는 opaque UTF-8 1..4096 bytes다.
  • Snapshot이 더 이상 존재하지 않거나 cursor가 유효하지 않으면 Expired를 반환한다.

Framework는 Expired를 받으면 이전 page 결과를 버리고 첫 page부터 다시 읽는다. Scan item은 복구 후보일 뿐이므로 mutation 전에 직접 읽기와 expected version 조건으로 다시 확인한다.

Cursor encoding, snapshot 보존 구조와 Redis SCAN 사용 여부는 provider implementation detail이다 — Redis provider가 무엇을 골라도 이 계약과 무관하다(§9).

6. Cancellation, 결과 유실과 오류

Operation 시작 전 cancellation은 I/O와 commit 시작을 막는다. Operation 시작 뒤 cancellation, timeout 또는 transport error가 발생하면 commit 여부가 불명확할 수 있다. Provider는 이를 성공이나 Conflict로 추정하지 않는다. Framework가 직접 읽기와 expected version으로 결과를 재구성할 수 있어야 한다.

입력 bound 위반과 같은 key를 condition 또는 mutation 안에서 중복 지정한 caller 오류는 언어별 argument validation error다. Missing, ConflictExpired는 정상적인 closed result다. Provider-specific failure는 Framework가 Store failure로 분류할 수 있어야 하지만 Redis command, key layout이나 script 정보를 application public API에 노출하지 않는다.

Input bytes는 asynchronous operation이 끝날 때까지 변경되지 않아야 한다. Provider가 그 뒤에도 보관하려면 복사한다. Success result의 bytes는 consumer가 사용하는 동안 stable해야 한다.

7. 등록과 provider instance 수명

Provider instance의 등록 조건과 Framework root의 소유권은 Location runtime §2를 따른다. Framework가 instance 수명을 소유하는 구성에서는 Store를 사용하는 runtime과 background operation이 모두 끝난 뒤 정확히 한 번 정리한다. 여러 Store가 물리 connection을 공유할 때 중복 정리를 막는 책임은 provider 구현에 있다.

이 등록 조건은 provider 종류와 무관한 SPI 계약이다. 아래부터는 공식 Redis provider가 이 계약을 만족하기 위해 실제로 사용하는 key와 data type을 정의한다.

8. 공식 Redis provider — Counter 발급

공식 Redis extension package는 언어별 naming convention에 맞는 RedisLocationStore 구현을 제공한다. 공개 options는 instance 생성에 필요한 connection, key namespace와 operation timeout으로 제한한다.

이 절부터 §9까지 서술하는 Redis key·data type은 provider가 자유롭게 고르는 내부 구현이 아니라, 서로 다른 언어의 공식 Redis provider가 같은 record를 상호 읽기 위한 MUST-level 공개 계약이다. 코드가 이와 다르면 코드를 고친다. 이 계약은 공식 Redis extension package에만 적용되며, Redis가 아닌 provider나 Redis를 쓰더라도 공식 extension이 아닌 provider는 §2~§6의 SPI만 만족하면 되고 이 key 형식을 따를 필요가 없다.

다음 logical key는 provider version이나 Redis implementation counter가 아닌, 언어 공통의 하나의 계약이다. 각 counter는 현재 owner가 속한 host process lifecycle을 구분하는 OwnerLeaseGeneration, 같은 ActorId·Spot ID의 서로 다른 logical incarnation을 구분하는 ObjectGeneration, 또는 같은 object incarnation 안에서 authority owner가 바뀐 순서를 나타내는 AuthorityOwnerGeneration 값을 발급한다.

Logical key 발급 값
zlink:v11:owner-counter OwnerLeaseGeneration(변경 없음)
zlink:v11:object-counter ObjectGeneration
zlink:v11:authority-owner-counter AuthorityOwnerGeneration

각 value는 sign, leading zero, JSON envelope, recordVersion이 없는 bare UTF-8 canonical decimal이다. 행이 없으면 다음 값은 1이고, v를 발급하면 CAS로 v + 1을 Put한다. n개 묶음은 v..v+n-1을 발급하고 v+n을 Put한다. 저장 가능한 counter 범위는 1..2^63-1이며, 0은 저장하거나 발급하지 않는다. 2^63-1을 저장한 행은 소진 상태이며 typed GenerationExhausted 결과를 반환하고 record와 counter를 모두 바꾸지 않는다. 따라서 최대로 발급할 수 있는 값은 2^63-2다.

Counter mutation은 그 값이 gate하는 record와 같은 conditional write batch(하나의 EVAL)에 반드시 들어가야 한다. Provider mapping {prefix}:{zlink-location-v3}:opaque:{sha256hex(logicalKey)}은 모든 logical counter를 자동으로 같은 {zlink-location-v3} hash slot에 둔다. Counter logical key는 authority\0 및 descriptor scan-preimage prefix 밖에 둔다.

운영 clean break: Store마다 한 번, 다음 retired logical literal의 SHA-256으로 계산한 physical opaque key를 flush한다: zlink:v11:counter:object, zlink:v11:counter:authority-owner, zlink:v11:authority:object-generation-counter, zlink:v11:authority:owner-generation-counter, zlink:v11:authority-generations. 위 mapping으로 literal마다 physical key를 다시 계산하며 record나 scan prefix에서 추측하지 않는다.

9. 공식 Redis provider — 5-record opaque 저장 형식

Automatic discovery에서 message를 보내거나 받는 runtime node인 MeshNode가 자신의 identity와 접속 정보를 다른 node에 알리기 위해 게시하는 MeshNode descriptor, owner lease, ClientServer server descriptor, fanout publisher descriptor와 authority record는 언어가 달라도 같은 opaque record 표현을 사용해야 한다 — 그래야 한 언어가 쓴 record를 다른 언어가 읽을 수 있다. 이 다섯 record는 다음 저장 방식을 반드시 따른다.

Redis key는 {prefix}:{zlink-location-v3}:opaque:{sha256hex(preimage)}이며, {prefix}는 provider가 등록 시 지정하는 key namespace, preimageLocation runtime §3.4가 정하는 record별 logical key preimage다(주의 — 이 sha256hex(preimage)는 §8의 sha256hex(logicalKey)와 다른 입력을 쓴다. Counter는 짧은 literal logical key를 그대로 해시하고, 이 다섯 record는 record별로 구성한 preimage 문자열을 해시한다).

{zlink-location-v3}을 감싼 중괄호는 Redis Cluster hashtag다 — Put이 record·sequence counter·index를 같은 script 안에서 함께 바꾸므로(§4), 이 domain 전체를 하나의 hash slot에 고정해야 그 multi-key EVAL이 Cluster에서도 원자적으로 남는다. 중괄호를 빼면 Cluster가 각 key를 다른 slot으로 흩어 놓을 수 있어 원자성이 깨진다.

자료구조는 Redis ZSET이며, Put마다 provider 쪽 단조 증가 INCR counter를 score로 붙여 append-log로 기록한다 — 가장 큰 score의 member가 현재 값이다. Member value는 {originalKey, rawBytes(value), version, expiresAtMs, tombstone}를 담은 cmsgpack array 앞에 1-byte format tag 0x01을 붙인 값이다. rawBytes는 base64로 다시 인코딩하지 않은 원본 bytes를 그대로 담는다. Provider는 인식하지 못하는 format tag를 만나면 명시적으로 실패시키며, 값을 추측해서 읽지 않는다.

cmsgpack은 Redis Lua cmsgpack library가 만드는 표준 MessagePack 인코딩을 뜻하며, array의 다섯 member는 다음 MessagePack type을 쓴다 — 일반 encoder의 기본값(예: byte 문자열에 bin family를 쓰는 선택)은 Lua cmsgpack의 출력과 byte 단위로 일치하지 않으므로 명시적으로 고정한다.

Member MessagePack type
originalKey str family(길이에 따라 fixstr/str8/str16/str32) — bin이 아니다
rawBytes str family, 같은 규칙 — Lua 문자열은 별도 binary type 없이 raw bytes를 담는다
version str family — 숫자가 아닌 opaque StoreVersion 문자열이다
expiresAtMs 부호 없는 int family(크기에 따라 positive fixint/uint8/uint16/uint32/uint64); 0은 만료 없음을 뜻한다
tombstone bool(0xc2 false / 0xc3 true)

바깥 array 자체는 array family(요소 5개이므로 fixarray)를 쓴다.

Redis에 이미 저장된 상태를 옛 key·value 형식에서 읽어 새 opaque record로 변환하는 하위 호환 경로는 두지 않는다. 이 형식으로 올라가는 배포는 clean break다 — 기존 Redis 상태는 draining하거나 유실을 감수해야 하며, format tag와 recordVersion이 다른 버전이 섞여 있으면 조용히 하나를 골라 읽지 않고 명시적으로 실패한다.

Location Store와, cold activation에 필요한 activation envelope와 relocation 완료 뒤의 reply payload를 opaque byte로 보관하는 Relocation Store는 같은 Redis deployment에서 서로 다른 key namespace를 사용할 수도 있고 물리적으로 분리할 수도 있다. Correctness는 connection 공유나 cross-store Redis transaction에 의존하지 않는다.

여기까지가 MUST-level 공개 계약이다. 위 다섯 record와 그 opaque record 표현을 제외하면, 다음 항목은 Redis provider가 자유롭게 고르는 implementation detail이며 public contract가 아니다 — 문서가 코드와 어긋나면 문서를 코드에 맞춘다.

  • Lua script와 transaction 분할 방식
  • Connection lease, retry와 snapshot cursor 구현
  • Change stamp와 polling 최적화

Redis provider도 §4의 generic atomic batch와 §5의 snapshot scan을 그대로 지원해야 한다. authority DTO와 change-stamp capability interface는 공개하지 않는다.

10. 검증 요구

공개 표면 — Location Store SPI 세 operation의 반환값과, 공식 Redis provider가 store record golden fixture로 검증하는 key·value byte — 만으로 다음을 확인한다. 각 항목은 test 하나로 이어진다.

SPI(모든 provider 공통)

  • Exact read가 bytes, version, optional expiry와 StoreNow를 같은 관측으로 반환한다.
  • 만료된 value는 provider clock 기준 Missing이고 durable value는 explicit delete 전까지 유지된다.
  • Condition 하나가 실패하면 모든 mutation과 version 증가가 0이다.
  • 최대 2,048 unique key와 encoded 4 MiB request가 하나의 atomic commit으로 적용된다.
  • Scan page가 같은 snapshot을 사용하며 snapshot 또는 cursor가 유효하지 않으면 Expired를 반환한다.
  • Cursor는 4,096 bytes까지 opaque하게 왕복하고 page는 1,000 item·4 MiB bound를 지킨다.
  • Cancellation이나 결과 유실 뒤 직접 읽기와 version으로 commit 여부를 재구성할 수 있다.
  • Redis provider public declaration에 authority·reservation·aggregate DTO, script와 key layout type이 없다.
  • 같은 Redis와 분리 Redis 구성에서 Location Store와 Relocation Store를 각각 등록해 사용할 수 있다.

공식 Redis provider의 key·byte 형식

  • MeshNode descriptor, owner lease, ClientServer server descriptor와 fanout publisher descriptor는 store record golden fixture(framework/runtime/protocol/golden/store-record-v1.json)의 key 파생 벡터(preimage → SHA-256 → 전체 key 문자열)와 value byte 벡터(tombstone·만료 variant 포함)를 그대로 소비하는 conformance test로 검증한다.
  • 인식하지 못하는 format tag나 recordVersion은 명시적으로 실패하며, 옛 key·value 형식을 읽어 새 opaque record로 변환하는 하위 호환 경로가 없다.

Location·Relocation 주제 목차 · 스펙 목차 · 이전: 01. Location runtime · 다음: 03. Relocation Store (Redis)