416 lines
16 KiB
Markdown
416 lines
16 KiB
Markdown
# 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>.<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`다.
|
|
|
|
```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/<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
|
|
|
|
필수 필드:
|
|
|
|
```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다.
|