# 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](2026-07-26-fileserver-production-capability-design.md) ## 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-mounted`와 `sftp`가 같은 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. 계층과 모듈 경계 ```text 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-mounted`와 `sftp` 타입 값은 구현 전까지 accepted setting으로 등록하지 않는다. ## 5. Application 계약 변경 기존 request와 opaque reference를 유지한다. R2 provider가 달성한 보장을 정확히 보고할 수 있도록 `FilePublishReceipt.DurabilityGuarantee`에 다음 값만 추가한다. ```text 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를 해석하지 않는다. ```text fsr1... ``` - `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`다. ```yaml 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=true`는 `local-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 ```text .ca-fileserver/ operations//.json manifests//.json references//.json staging//.part quarantine/ probe/ data// ``` 모든 locator는 validated single segment 또는 adapter가 생성한 bounded relative segment다. Caller path를 받지 않는다. ### 8.1 Operation journal v2 필수 필드: ```text 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 ```text 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: ```text 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을 재시도한다. ```text 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다.