Files
tech-log-backend/docs/superpowers/specs/2026-07-28-fileserver-r2-control-plane-provider-selection-design.md
T

16 KiB

Fileserver R2 Control Plane and Provider Selection Design

  • Date: 2026-07-28
  • Status: 승인된 설계, 구현 전
  • Scope: provider-neutral R2 control plane, explicit destination/provider selection, first local-persistent qualification provider
  • Parent: Fileserver Production Capability Deep Design

1. 목표

현재 LocalFilePublicationAdapter의 single-node process-restart R1을 운영 topology의 기본값으로 승격하지 않는다. 이번 increment는 다음을 구현한다.

  1. application에는 기존 provider-neutral FilePublicationPort만 유지한다.
  2. adapter 내부에 destination binding, provider descriptor, durable operation/manifest/reference control plane을 둔다.
  3. 활성화된 Fileserver는 정확한 destination과 provider를 명시해야 하며 implicit local fallback을 금지한다.
  4. 첫 qualification provider로 pre-provisioned persistent filesystem을 사용하는 local-persistent를 구현한다.
  5. shared-mountedsftp가 같은 control-plane state machine을 재사용할 수 있게 하되 이번 increment에서 가짜 provider나 동작하지 않는 bean을 만들지 않는다.

local-persistent는 container writable layer나 임시 디렉터리를 의미하지 않는다. 단일 노드 또는 node-attached persistent volume과 private owner boundary가 증명된 환경만 대상으로 한다.

2. 비범위

이번 increment에 포함하지 않는다.

  • NFS 또는 다른 shared mount의 multi-client correctness;
  • SFTP SDK, connection pool, credential, OpenSSH qualification;
  • cross-node producer fencing;
  • background reaper, retention delete, quota reservation;
  • metrics/tracing/health implementation;
  • optional content read/delete/list API;
  • object storage. Object storage는 별도 outbound leaf의 책임이다.

이 항목은 seam만 만들지 않는다. 실제 semantic provider를 구현하는 후속 increment에서만 dependency, bean, setting을 추가한다.

3. 검토한 접근

A. 현재 local adapter를 바로 R2로 표시

설정과 change surface는 작지만 provider selector, terminal manifest, opaque-reference direct lookup과 strict startup evidence가 없다. R2를 과장하므로 선택하지 않는다.

B. Local, NFS, SFTP를 동시에 구현

최종 기능은 많지만 서로 다른 보장과 real-service CI가 한 change surface에 결합된다. NFS와 OpenSSH 인프라가 없으면 검증되지 않은 provider가 남으므로 선택하지 않는다.

C. Provider-neutral control plane + local-persistent 첫 qualification

공통 state machine과 binding을 먼저 고정하고 한 provider를 실제 crash/security 테스트로 qualification한다. 이후 provider가 control-plane 계약을 재사용하면서도 각자의 보장을 별도로 증명할 수 있다. 이 접근을 선택한다.

4. 계층과 모듈 경계

application-core
  FilePublicationPort
  FilePublishRequest
  FilePublishReceipt
          |
          v
adapter:outbound:fileserver
  RoutingFilePublicationAdapter
          |
          +-- DestinationBindingRegistry
          +-- FilePublicationProviderRegistry
          +-- DurablePublicationCoordinator
          +-- ProviderControlPlane
                    |
                    +-- LocalPersistentPublicationProvider
  • application/domain에는 provider ID, filesystem path, manifest locator, Spring 또는 NIO 타입을 추가하지 않는다.
  • RoutingFilePublicationAdapter만 production FilePublicationPort bean이다.
  • provider와 control-plane SPI는 fileserver package 내부 타입이다. 범용 filesystem/SDK API를 public bean으로 노출하지 않는다.
  • shared-mountedsftp 타입 값은 구현 전까지 accepted setting으로 등록하지 않는다.

5. Application 계약 변경

기존 request와 opaque reference를 유지한다. R2 provider가 달성한 보장을 정확히 보고할 수 있도록 FilePublishReceipt.DurabilityGuarantee에 다음 값만 추가한다.

FILE_AND_DIRECTORY_SYNC

이 값은 startup probe와 process-crash qualification을 모두 통과한 provider만 반환한다. 호출한 sync가 물리 device, volume replica 또는 storage-controller power-loss protection까지 완료됐다는 뜻은 아니다. 그 축은 deployment/storage evidence로 별도 판정한다. PROCESS_LOCAL_SYNC 또는 PROVIDER_ACK_ONLY를 요구 보장보다 약한 상태에서 자동으로 R2 값으로 올리지 않는다.

새 opaque reference 형식은 다음 의미를 가지되 application은 내부 segment를 해석하지 않는다.

fsr1.<route-token>.<file-id>.<check-digits>
  • route-token: startup에서 생성된 bounded destination route allowlist 값;
  • file-id: CSPRNG 128-bit 이상;
  • check-digits: accidental truncation/corruption 검출;
  • provider locator, operation ID, tenant/user ID, host/path는 포함하지 않는다.

Reference는 authorization token이 아니다. authorization은 application use case의 책임이다.

6. 명시적 설정과 선택

새 canonical prefix는 app.fileserver다.

app:
  fileserver:
    enabled: false
    destinations:
      local-export:
        provider-ref: local-primary
        required-publication: unique-atomic-create
        required-durability: file-and-directory-sync
        maximum-rows: 1000000
        maximum-encoded-bytes: 1073741824
    providers:
      local-primary:
        type: local-persistent
        root-directory: ${APP_FILESERVER_LOCAL_ROOT:}
        auto-create: false
        strict-path-security: true
        expected-file-store-name: ${APP_FILESERVER_LOCAL_EXPECTED_FILE_STORE_NAME:}
        expected-file-store-type: ${APP_FILESERVER_LOCAL_EXPECTED_FILE_STORE_TYPE:}
        mount-sentinel-name: .ca-fileserver-volume
        mount-sentinel-sha256: ${APP_FILESERVER_LOCAL_MOUNT_SENTINEL_SHA256:}
        expected-owner: ${APP_FILESERVER_LOCAL_EXPECTED_OWNER:}
        maximum-root-mode: "0750"

규칙:

  • enabled=true이면 destination과 provider가 각각 하나 이상 필요하다.
  • 모든 destination은 존재하는 provider 하나를 참조한다.
  • request destination에 binding이 없으면 producer 호출 전에 실패한다.
  • provider type의 기본값은 없다.
  • local-persistent root는 absolute, existing, pre-provisioned directory여야 한다.
  • auto-create=truelocal-persistent에서 거부한다.
  • root와 mount sentinel은 operator가 미리 만든다. Root attestation이 끝난 뒤 adapter가 private top-level control/data directory와 bounded hash shard를 restrictive POSIX creation mode로 생성할 수 있으며, 생성할 때마다 parent identity와 directory sync를 확인한다.
  • container ephemeral 경로를 위한 local-dev는 별도 후속 profile이다. production 설정과 같은 guarantee를 공유하지 않는다.
  • 기존 ca-skeleton.fileserver.*는 R1/legacy compatibility selector로만 남는다. 새 R2 설정과 동시에 활성화되면 startup을 실패시킨다. 암묵 migration이나 precedence를 두지 않는다.

7. Startup capability compilation

application traffic을 받기 전에 destination별 effective descriptor를 한 번 compile한다.

local-persistent는 다음을 모두 검증한다.

  1. root와 모든 ancestor가 symbolic link가 아니다.
  2. root real path가 설정 absolute path와 일치한다.
  3. configured owner와 실제 owner가 일치한다.
  4. POSIX permission이 configured maximum보다 넓지 않고 group/world writable이 아니다.
  5. FileStore.name()type()이 설정 값과 일치한다.
  6. mount sentinel이 regular no-follow file이고 configured SHA-256와 일치한다.
  7. data, staging, operations, manifests, references, quarantine directory가 같은 FileStore에 있다.
  8. control directory는 private owner boundary이며 symlink가 아니다.
  9. SecureDirectoryStream을 열 수 있다.
  10. exclusive create, file force, hard-link create, directory force가 private probe directory에서 성공한다.

Probe artifact는 unique name만 사용하며 successful cleanup과 parent directory force까지 완료해야 한다. Probe 실패는 capability downgrade가 아니라 startup failure다.

JDK가 directory-relative hard-link primitive를 제공하지 않으므로 hard-link publish는 다음 boundary에서만 허용한다.

  • root/control/data directories가 adapter owner 전용이고 untrusted writer가 없음;
  • publish 직전과 직후 root identity, directory file key, mount sentinel을 다시 확인;
  • target은 CSPRNG unique name;
  • pre/post identity가 바뀌면 성공을 반환하지 않고 PUBLISH_INDETERMINATE;
  • privileged host administrator 또는 same-owner malicious process와의 경쟁은 guarantee 범위가 아니며 deployment isolation requirement로 기록한다.

untrusted writer가 같은 root에 entry를 만들 수 있는 환경은 strict local R2가 아니다.

8. Durable control plane

.ca-fileserver/
  operations/<prefix>/<operation-id>.json
  manifests/<prefix>/<file-id>.json
  references/<prefix>/<file-id>.json
  staging/<prefix>/<operation-id>.part
  quarantine/
  probe/
data/<prefix>/<generated-file-name>

모든 locator는 validated single segment 또는 adapter가 생성한 bounded relative segment다. Caller path를 받지 않는다.

8.1 Operation journal v2

필수 필드:

schemaVersion
stateRevision
state
operationId
requestFingerprint
effectivePolicyRevision
effectivePolicyDigest
destinationId
providerId
fileId
routeToken
publishedFileName
stageFileName
byteSize
rowCount
columnCount
sha256
formulaMitigatedCount
manifestDigest
referenceDigest
createdAt
sealedAt
publishedAt
lastFailureCode
receiptSnapshot

State는 WRITING, SEALED, DATA_PUBLISHED, MANIFEST_PUBLISHED, REFERENCE_PUBLISHED, PUBLISHED, QUARANTINED다.

8.2 Private manifest v1

Manifest는 operation/file/provider/reference/fingerprint, schema·format·policy digest, byte/count, SHA-256, achieved guarantees, internal relative locator를 기록한다. Absolute path, raw row/cell, credential, raw tenant/user ID는 저장하지 않는다.

8.3 Reference index v1

Reference index는 opaque file-id에서 operation ID, file version, manifest digest와 internal relative locator로 direct lookup한다. Directory scan은 receipt restoration의 authority가 아니다.

8.4 Record update

각 control record는:

  1. sibling private temp file을 CREATE_NEW;
  2. bounded canonical JSON encoding;
  3. file force(true);
  4. same-directory atomic replace;
  5. parent directory force;
  6. read-back schema/revision/digest verification;

순서로 갱신한다. 낮은 revision, fingerprint mismatch, newer schema는 자동 덮어쓰지 않는다.

9. Publication ordering

J-WRITING
  -> stage stream/force
J-SEALED
  -> exclusive hard-link data publish
  -> data directory force
J-DATA_PUBLISHED
  -> private manifest publish/force
J-MANIFEST_PUBLISHED
  -> reference index publish/force
J-REFERENCE_PUBLISHED
  -> terminal journal + receipt snapshot publish/force
J-PUBLISHED
  -> receipt return
  • Producer는 accepted attempt에서 최대 한 번 호출한다.
  • SEALED 이후 retry/recovery는 staged bytes만 사용한다.
  • terminal journal force 전에는 receipt를 반환하지 않는다.
  • target collision, digest mismatch 또는 root identity change는 자동 overwrite하지 않는다.
  • final data가 있어도 manifest/reference가 없으면 아직 terminal success가 아니다.

10. Deterministic recovery

Recovery는 operation ID direct lookup으로 실행하며 startup full scan에 의존하지 않는다.

확인된 상태 조치
terminal journal + matching manifest/reference/data 저장된 receipt 복원
SEALED + valid stage, data 없음 data publication부터 재개
SEALED + matching data manifest publication부터 재개
DATA_PUBLISHED + matching data manifest publication 재개
MANIFEST_PUBLISHED + matching manifest/data reference publication 재개
REFERENCE_PUBLISHED + all matching terminal journal 완성
data digest mismatch QUARANTINED, integrity failure
marker/manifest/reference schema newer 보존 후 fail-fast/quarantine
fingerprint conflict typed conflict, 기존 artifact 보존
root/mount identity change indeterminate, write/recovery 중단

Truth priority:

matching data + private manifest + reference
  > terminal operation record
  > non-terminal operation record
  > in-memory state

모순이 있으면 임의 성공이나 삭제 대신 quarantine evidence를 기록한다.

11. Compatibility

  • R1 journal schema v1은 읽을 수 있어야 한다.
  • R1 terminal receipt는 기존 PROCESS_LOCAL_SYNC 보장 그대로 복원한다.
  • R1 artifact를 자동으로 R2 manifest/reference로 승격하지 않는다.
  • R2 writer는 journal v2만 생성한다.
  • 기존 overwrite-capable legacy port는 별도 root와 opt-in을 유지하며 R2 control plane에 접근하지 않는다.
  • R1과 R2 selector가 동시에 활성화되면 ambiguous composition으로 startup을 실패시킨다.

12. Failure semantics

  • 설정/보장 mismatch: startup failure;
  • destination 없음: producer 전 deterministic request failure;
  • stage 이전 capacity/validation failure: not applied;
  • stage/write failure: failed, partial stage는 recovery evidence가 아니면 정리;
  • sealed 이후 filesystem timeout/IO/root identity change: indeterminate;
  • published data와 metadata 불일치: integrity/quarantine;
  • journal/control record corruption: provider exception을 노출하지 않고 typed indeterminate;
  • guarantee를 낮춰 성공시키는 fallback은 없다.

13. 테스트와 증거

13.1 Unit/contract

  • exact destination/provider selection과 no-default;
  • R1/R2 simultaneous activation rejection;
  • reference grammar/check digits/forged route rejection;
  • journal v2, manifest, reference canonical round-trip;
  • state revision과 fingerprint conflict;
  • achieved durability value invariants.

13.2 Local integration

  • pre-provisioned root requirement;
  • owner/mode/FileStore/sentinel mismatch startup failure;
  • symlink ancestor/control/data rejection;
  • staging/final/control same FileStore;
  • successful capability probe와 cleanup;
  • partial final visibility 0건;
  • same operation concurrency와 producer once;
  • target collision no overwrite;
  • data/manifest/reference digest mismatch quarantine.

13.3 Crash qualification

Forked JVM helper를 사용해 다음 force boundary 직후 process를 강제 종료하고 새 JVM에서 같은 operation을 재시도한다.

J-WRITING
stage force
J-SEALED
data link
data directory force
manifest force
manifest directory force
reference force
reference directory force
terminal journal force
terminal journal directory force

각 boundary에서 결과는 다음 중 하나여야 한다.

  • producer 재실행 없이 동일 receipt 복원;
  • verified sealed bytes로 publication 완성;
  • typed indeterminate/quarantine.

partial final, overwrite, 다른 receipt, silent guarantee downgrade는 허용하지 않는다.

13.4 플랫폼

  • Linux/POSIX + SecureDirectoryStream + directory force qualification lane에서만 FILE_AND_DIRECTORY_SYNC을 검증한다.
  • capability가 없는 일반 unit-test filesystem에서는 R1 보장만 테스트하며 R2 service test를 skip 성공으로 처리하지 않는다.

14. 완료 기준

이번 increment의 완료는 “Fileserver 전체가 모든 운영환경에서 R2”라는 뜻이 아니다.

완료를 주장하려면:

  1. provider 기본값 없이 exact binding이 동작한다.
  2. local-persistent startup probe가 모든 required capability를 증명한다.
  3. terminal manifest/reference direct lookup이 구현된다.
  4. 모든 publication force boundary의 crash test가 deterministic result를 낸다.
  5. strict path/mount identity/security tests가 통과한다.
  6. public path와 clean architecture gate가 통과한다.
  7. R1 compatibility artifact를 R2로 자동 승격하지 않는다.
  8. 문서와 receipt는 local-persistent qualification만 R2라고 표시한다.

후속 순서는 Phase 3 maintenance/resource limits, Phase 4 SFTP, Phase 5 shared-mounted/NFS evidence다.