2468 lines
92 KiB
Markdown
2468 lines
92 KiB
Markdown
# Fileserver Production Capability Deep Design
|
||
|
||
- 작성일: 2026-07-26
|
||
- 상태: 상세 설계 완료, Phase 0–1 및 Phase 2 일부 local R1 구현, R2 이상 미구현
|
||
- 독립 아키텍처 재리뷰: blocker/high 0건
|
||
- 기준: Java 21, Spring Boot 4.0.0, Gradle 멀티모듈 Clean Architecture
|
||
- 대상 leaf: `adapter-outbound-fileserver`
|
||
- 구현 추적: 이 문서의 목표 전체가 아니라 framework-free port, local staged CSV, single-node
|
||
operation journal/recovery까지만 적용되었다.
|
||
- 상위 문서:
|
||
[Production Capability Platform Design](2026-07-26-production-capability-platform-design.md)
|
||
|
||
## 0. 구현 상태
|
||
|
||
2026-07-28 기준 구현된 범위:
|
||
|
||
- `application-core`의 `FilePublicationPort`, typed schema/cell/row, producer/sink, opaque receipt;
|
||
- 행 단위 UTF-8 CSV encoding과 row/byte/cell 제한;
|
||
- spreadsheet formula 완화 정책과 SHA-256/count 계산;
|
||
- private staging, exclusive create, file/directory force, no-replace hard-link publish;
|
||
- 실패 시 staging 정리와 `PUBLISH_INDETERMINATE` 오류 분류;
|
||
- opt-in typed settings와 local R1 bean composition;
|
||
- canonical request fingerprint와 private `WRITING`/`SEALED`/`PUBLISHED` operation journal;
|
||
- forced temp record + atomic replace와 single-node terminal receipt restoration;
|
||
- sealed staging/final의 size·SHA-256 검증 후 producer 재실행 없는 local reconciliation.
|
||
- operation-scoped JVM/OS file lock과 hard-link-only publication protocol;
|
||
- overwrite-capable legacy port의 별도 opt-in/root 및 canonical overlap 차단;
|
||
- 안전한 commit primitive가 없을 때 copy-to-final로 downgrade하지 않는 fail-closed 동작.
|
||
|
||
아직 구현되지 않은 범위:
|
||
|
||
- Phase 2의 cross-node fencing, reference/private-manifest index, exhaustive crash/symlink-race
|
||
qualification;
|
||
- 운영 cleanup/quota/retention과 effective capability probe인 Phase 3;
|
||
- SFTP provider인 Phase 4;
|
||
- NFS/HA/bootstrap evidence인 Phase 5;
|
||
- optional delete/read/scan operation인 Phase 6.
|
||
|
||
따라서 현재 journal은 single-node local recovery seam이며 Fileserver R2 완료 증거가 아니다.
|
||
기존 `FileExportPort`도 호환성을 위해
|
||
남아 있으며, 전체 행 materialization과 absolute path receipt를 사용하는 legacy 경로다.
|
||
|
||
## 1. 설계 판정
|
||
|
||
현재 Fileserver 구현은 운영 파일서버가 아니라 다음 한 경로만 제공하는 R1 이하의 로컬
|
||
CSV 예제다.
|
||
|
||
```text
|
||
List<List<String>>
|
||
-> 전체 StringBuilder
|
||
-> 전체 String
|
||
-> 전체 byte[]
|
||
-> final path 직접 truncate/write
|
||
-> absolute server path 반환
|
||
```
|
||
|
||
목표는 범용 파일시스템 CRUD API가 아니다. 목표는 다음 capability다.
|
||
|
||
> 계층형 경로, 외부 파일명, drop-zone, rename 또는 ready-marker 완료 계약이 필요한 생성
|
||
> 파일을 local/mounted filesystem 또는 SFTP에 안전하게 publish하고, 그 결과를 opaque
|
||
> reference로 추적·검증·복구하는 기능
|
||
|
||
선택한 핵심 구조는 다음과 같다.
|
||
|
||
1. Application은 목적지를 path/host가 아닌 `FileDestinationId`로 선택한다.
|
||
2. 안정적인 `FilePublishOperationId`가 retry와 reconciliation의 기준이다.
|
||
3. 행 데이터는 동기식 row-producer callback으로 한 번만 흘려보낸다.
|
||
4. Adapter가 staging, encoding, checksum, close, publish, abort를 모두 소유한다.
|
||
5. Provider는 설정값이 아니라 실제 probe 결과인 effective capability를 보고한다.
|
||
6. 요구한 atomicity/durability보다 provider 보장이 약하면 자동 downgrade하지 않는다.
|
||
7. publish 응답 유실은 실패가 아니라 `INDETERMINATE`로 모델링하고 먼저 reconcile한다.
|
||
8. 기본 파일명 정책은 immutable/versioned이며 unconditional overwrite와 append는 금지한다.
|
||
9. File Server와 business database 사이의 원자적 commit은 주장하지 않는다.
|
||
10. 사용하지 않는 provider는 connection, scheduler, scan, directory 생성 같은 side effect를
|
||
일으키지 않는다.
|
||
|
||
## 2. 기존 통합 설계에서 다룬 범위와 이번 심화 범위
|
||
|
||
상위 설계서는 Fileserver에 대해 다음 운영 기준만 정의했다.
|
||
|
||
- streaming writer;
|
||
- temporary file;
|
||
- restrictive permissions;
|
||
- fsync와 atomic rename;
|
||
- symlink defense;
|
||
- quota와 retention;
|
||
- CSV formula defense;
|
||
- opaque receipt;
|
||
- NFS/SFTP semantics를 provider capability로 표시.
|
||
|
||
이 기준은 방향은 맞지만 다음 구현 결정이 없었다.
|
||
|
||
- streaming port의 정확한 signature와 lifecycle;
|
||
- producer exception과 storage exception의 분리;
|
||
- operation ID와 request-intent/operation fingerprint;
|
||
- publish state machine과 unknown outcome;
|
||
- create-only, replace, versioned naming의 정확한 보장;
|
||
- atomic visibility와 crash durability의 분리;
|
||
- local, NFS, SFTP의 effective capability matrix;
|
||
- target-side staging과 cross-filesystem 처리;
|
||
- mount 누락 시 local fallback 방지;
|
||
- SFTP extension negotiation과 reconnect 후 reconciliation;
|
||
- manifest/marker commit protocol;
|
||
- multi-node cleanup claim;
|
||
- quota reservation의 한계;
|
||
- CSV dialect, typed cell, null, control character, formula 정책;
|
||
- error taxonomy, health, metric, test 및 CI task;
|
||
- 기존 API에서 새 API로의 migration.
|
||
|
||
이번 문서는 위 항목을 구현 계획을 작성할 수 있는 수준까지 확정한다.
|
||
|
||
## 3. 현재 코드의 증거 기반 진단
|
||
|
||
| 영역 | 현재 구현 | 운영상 의미 |
|
||
| --- | --- | --- |
|
||
| Port | `exportCsv(String, List<String>, List<List<String>>)` | 호출자가 전체 행을 먼저 메모리에 적재해야 한다. |
|
||
| Encoding | `StringBuilder -> String -> byte[]` | 파일 크기에 비례한 heap 복제가 추가된다. |
|
||
| Write | `Files.write(finalTarget, bytes)` | 기존 파일을 truncate하고 partial final을 노출할 수 있다. |
|
||
| Naming | caller-controlled `fileName` | namespace, operation ID, version, precondition이 없다. |
|
||
| Receipt | `ExportedFile.path` absolute path | SFTP와 cluster에서 의미가 없고 서버 topology가 유출된다. |
|
||
| Path safety | `normalize().startsWith(baseDir)` | lexical traversal만 막고 symlink/TOCTOU를 막지 못한다. |
|
||
| CSV | UTF-8, LF, 최소 quoting | RFC 4180 CRLF, row width, formula, control/size 정책이 없다. |
|
||
| Collision | unconditional overwrite | concurrent writer의 결과가 정의되지 않는다. |
|
||
| Durability | 없음 | file force, directory sync, remote fsync를 구분하지 않는다. |
|
||
| Error | 모든 `IOException`을 `INTERNAL_ERROR` | retry, conflict, capacity, unknown outcome을 구분할 수 없다. |
|
||
| Settings | `enabled`, 상대 `baseDirectory` | provider, guarantee, limits, timeout, mount identity가 없다. |
|
||
| Startup | enabled이면 `createDirectories` | NFS mount 누락을 로컬 디렉터리로 오인할 수 있다. |
|
||
| Composition | leaf는 registry에만 등록 | 기본 `app-bootstrap` artifact에 fileserver가 없다. |
|
||
| Consumer | 없음 | sample을 포함해 실제 use case가 port를 호출하지 않는다. |
|
||
| Tests | local unit 7개 | concurrency, fault, symlink, memory, NFS, SFTP 증거가 없다. |
|
||
|
||
주요 근거 파일:
|
||
|
||
- `src/application-core/src/main/java/dev/caskeleton/application/fileexport/FileExportPort.java`
|
||
- `src/application-core/src/main/java/dev/caskeleton/application/fileexport/ExportedFile.java`
|
||
- `src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FilesystemCsvExportAdapter.java`
|
||
- `src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FileExportProperties.java`
|
||
- `src/adapter/outbound/fileserver/src/test/java/dev/caskeleton/adapter/outbound/fileserver/FilesystemCsvExportAdapterTest.java`
|
||
- `src/config/architecture/modules.json`
|
||
- `src/app-bootstrap/build.gradle`
|
||
|
||
현재 7개 fileserver unit test와 leaf `check`는 성공한다. 이는 현재 문서화된 로컬 happy-path
|
||
계약이 동작한다는 증거일 뿐 production readiness 증거는 아니다.
|
||
|
||
## 4. 범위와 명시적 비범위
|
||
|
||
### 4.1 이번 R2 baseline에 포함
|
||
|
||
- tabular data의 streaming CSV publication;
|
||
- local persistent filesystem provider;
|
||
- mounted/shared filesystem provider profile;
|
||
- SFTP outbound publication provider;
|
||
- staging, sealing, publish, abort;
|
||
- SHA-256, byte/row count, versioned manifest;
|
||
- create-unique 및 conditional replace;
|
||
- operation-id 기반 idempotency와 reconciliation;
|
||
- opaque inspection/reference;
|
||
- managed namespace의 staging cleanup;
|
||
- opt-in managed retention/delete;
|
||
- typed settings, exact provider binding, startup safety;
|
||
- health, metrics, traces, structured error;
|
||
- real local/OpenSSH/NFS integration and failure tests.
|
||
|
||
### 4.2 안전하게 열어둘 optional operation
|
||
|
||
- opaque reference 기반 metadata inspection;
|
||
- opaque reference 기반 content transfer;
|
||
- expected-version 기반 managed delete;
|
||
- operation ID 기반 publish resolution;
|
||
- marker/manifest verification.
|
||
|
||
이 operation은 구현할 수 있게 계약을 분리하지만 하나의 범용 `FileServerPort` CRUD로 합치지
|
||
않는다.
|
||
|
||
### 4.3 이번 범위에서 제외
|
||
|
||
- HTTP download endpoint와 authorization: `adapter:inbound:web` 책임;
|
||
- 사용자 multipart upload: inbound transport와 object-storage/quarantine workflow 책임;
|
||
- presigned URL, multipart object upload, ETag/bucket lifecycle: object storage 책임;
|
||
- domain별 export job entity, snapshot query, pagination: application/sample 책임;
|
||
- DB write와 file publish의 distributed transaction;
|
||
- inbound SFTP/NFS pickup으로 use case를 구동하는 기능;
|
||
- directory watch/poll을 business event source로 사용하는 기능;
|
||
- FTP/FTPS/SMB native client provider;
|
||
- 제품별 성능 수치와 partner별 filename/dialect.
|
||
|
||
외부 파일이 들어와 use case를 구동하는 inbound pickup은 driving adapter다. 필요해지면
|
||
`adapter:inbound:fileserver` leaf를 registry migration으로 추가한다. 현재 outbound leaf에
|
||
listener/poller와 business handler를 넣지 않는다.
|
||
|
||
## 5. HARD invariants
|
||
|
||
다음 조건은 구현 편의를 위해 낮출 수 없다.
|
||
|
||
1. `application-core`에 Spring, `Path`, `File`, `InputStream`, SFTP SDK, Micrometer 타입을
|
||
노출하지 않는다.
|
||
2. Application request에는 host, port, credential, base directory, remote path가 없다.
|
||
3. Physical final path는 adapter의 target binding에서만 계산한다.
|
||
4. Caller 문자열을 `Path.resolve`의 자유 경로로 사용하지 않는다.
|
||
5. Final name을 가진 파일에 직접 streaming write하지 않는다.
|
||
6. Temp/staging은 publish target과 동일 filesystem/remote namespace에 존재한다.
|
||
7. `ATOMIC_MOVE` 실패 시 non-atomic move/copy로 조용히 fallback하지 않는다.
|
||
8. Atomic visibility와 crash durability를 같은 guarantee로 표현하지 않는다.
|
||
9. 하나의 accepted publish attempt 안에서 producer는 최대 한 번만 실행한다. Process 또는
|
||
node 장애를 넘는 global at-most-once는 durable ownership evidence 없이는 주장하지 않는다.
|
||
10. 같은 accepted attempt의 provider retry를 위해 application row producer를 다시 호출하지
|
||
않는다.
|
||
11. `INDETERMINATE` 결과를 blind retry하지 않는다.
|
||
12. `APPEND`는 R2 publication mode가 아니다.
|
||
13. Default collision policy는 unconditional replace가 아니다.
|
||
14. File name/path/content/checksum/user/tenant를 metric tag로 사용하지 않는다.
|
||
15. Unknown file과 유효하지 않은 manifest를 reaper가 자동 삭제하지 않는다.
|
||
16. Fileserver가 persistence, Redis lock, objectstorage adapter에 의존하지 않는다.
|
||
17. `local-dev` provider는 prod profile에서 시작하지 않는다.
|
||
18. SFTP unknown host key 허용, password literal, infinite timeout/pool은 금지한다.
|
||
19. 현재 mount가 기대한 mount인지 증명하지 못하면 mounted provider는 ready가 아니다.
|
||
20. Provider capability가 요구 guarantee를 만족하지 못하면 startup 또는 required readiness를
|
||
실패한다.
|
||
|
||
## 6. 대안 검토
|
||
|
||
### A. 현재 port를 유지하고 `BufferedWriter`만 추가
|
||
|
||
Adapter의 추가 `StringBuilder`는 제거할 수 있지만 caller가 이미 `List<List<String>>` 전체를
|
||
materialize한다. Absolute path, overwrite, provider, unknown outcome 문제도 남는다. 선택하지
|
||
않는다.
|
||
|
||
### B. 범용 `FileServerPort`에 open/read/write/list/move/delete를 모두 제공
|
||
|
||
Application이 path, directory, wildcard, file handle에 결합되고 raw filesystem facade가 된다.
|
||
Redis의 raw command API를 core에 노출하지 않는 것과 같은 이유로 선택하지 않는다.
|
||
|
||
### C. Mutable `begin -> append -> commit/abort` session을 application에 노출
|
||
|
||
페이지 단위 쓰기는 쉽지만 caller가 commit/abort/close를 누락할 수 있고 adapter resource
|
||
lifecycle이 application으로 새어 나온다. SDK-like session이 되므로 기본 계약으로 선택하지
|
||
않는다.
|
||
|
||
### D. 단일 publish 호출과 synchronous row-producer callback
|
||
|
||
선택한 방식이다.
|
||
|
||
- Adapter가 staging부터 abort까지 전 lifecycle을 소유한다.
|
||
- Sink의 동기 호출 자체가 backpressure boundary가 된다.
|
||
- Producer는 페이지 조회를 선택할 수 있어 전체 materialization이 필요 없다.
|
||
- Adapter가 producer를 한 번만 실행하는 것을 통제할 수 있다.
|
||
- Checked IO와 provider 타입이 application으로 새지 않는다.
|
||
|
||
### E. Provider별 Gradle leaf를 즉시 분리
|
||
|
||
`fileserver-local`, `fileserver-mounted`, `fileserver-sftp`는 dependency isolation이 가장
|
||
강하지만 지금은 exact-19 registry 변경과 설정/계약 중복 비용이 크다. 우선 기존 leaf 내부
|
||
provider package로 구현한다. SFTP SDK 보안/릴리스 lifecycle이 독립 배포를 요구할 때 ADR과
|
||
registry migration으로 분리한다.
|
||
|
||
## 7. 목표 아키텍처
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
USECASE[Application export use case] --> PORT[FilePublicationPort]
|
||
PORT --> COORD[Publication coordinator]
|
||
COORD --> VALIDATE[Schema / limits / formula policy]
|
||
VALIDATE --> CSV[Streaming CSV encoder]
|
||
CSV --> PIPE[Count + digest + optional transform]
|
||
PIPE --> PROVIDER{Target provider}
|
||
PROVIDER --> LOCAL[Local persistent filesystem]
|
||
PROVIDER --> MOUNT[Mounted/shared filesystem]
|
||
PROVIDER --> SFTP[SFTP remote endpoint]
|
||
COORD --> MANIFEST[Manifest / marker / reconcile]
|
||
BOOT[app-bootstrap] -. binds destination and verifies guarantees .-> COORD
|
||
OBS[Health / metrics / traces] -. observes .-> COORD
|
||
```
|
||
|
||
Target package shape:
|
||
|
||
```text
|
||
adapter:outbound:fileserver
|
||
config/
|
||
core/
|
||
FilePublicationCoordinator
|
||
FileProvider
|
||
FileProviderDescriptor
|
||
PublicationPlan
|
||
format/csv/
|
||
CsvEncoder
|
||
CsvDialect
|
||
FormulaPolicy
|
||
publication/
|
||
StagingArtifact
|
||
ManifestCodec
|
||
PublishReconciler
|
||
provider/local/
|
||
provider/mounted/
|
||
provider/sftp/
|
||
maintenance/
|
||
StagingReaper
|
||
RetentionExecutor
|
||
observability/
|
||
```
|
||
|
||
## 8. 모듈과 계층 소유권
|
||
|
||
| 책임 | 소유 모듈 |
|
||
| --- | --- |
|
||
| Export use case, destination intent, schema, row production | `application-core` 또는 feature application |
|
||
| Framework-free publication/read/delete ports와 값 타입 | `application-core` |
|
||
| Skeleton-wide file operational error와 provider descriptor 공통 값 | `shared-contract` |
|
||
| CSV encoding, staging, manifest, local/mounted/SFTP provider | `adapter:outbound:fileserver` |
|
||
| Provider selection, exact binding, prod safety, readiness composition | `app-bootstrap` |
|
||
| Export job persistence, source snapshot, keyset query | application + persistence adapter |
|
||
| HTTP response streaming | `adapter:inbound:web` |
|
||
| Incoming file pickup/listener | future inbound fileserver leaf |
|
||
| Mount, quota, backup, NFS export, remote account | deployment/IaC/operations |
|
||
|
||
Fileserver leaf의 허용 edge는 현재처럼 `application-core`, `shared-contract`만 유지한다.
|
||
Sibling adapter edge를 추가하지 않는다.
|
||
|
||
## 9. Application 계약
|
||
|
||
### 9.1 Port 분리
|
||
|
||
```java
|
||
public interface FilePublicationPort {
|
||
FilePublishReceipt publish(
|
||
FilePublishRequest request,
|
||
TabularRowProducer producer);
|
||
}
|
||
|
||
public interface FilePublishResolutionPort {
|
||
FilePublishResolution resolve(FilePublishOperationId operationId);
|
||
}
|
||
|
||
public interface PublishedFileInspectionPort {
|
||
PublishedFileMetadata inspect(PublishedFileReference reference);
|
||
}
|
||
|
||
public interface PublishedFileTransferPort {
|
||
void transfer(PublishedFileReference reference, FileChunkSink sink);
|
||
}
|
||
|
||
public interface ManagedFileDeletionPort {
|
||
FileDeletionReceipt delete(
|
||
PublishedFileReference reference,
|
||
FileVersion expectedVersion);
|
||
}
|
||
|
||
public interface FileRegenerationPort {
|
||
FilePublishReceipt regenerate(
|
||
FileRegenerationRequest request,
|
||
TabularRowProducer producer);
|
||
}
|
||
```
|
||
|
||
R2 구현 순서는 publication과 resolution이 먼저다. Inspection, transfer, delete는 필요 없는
|
||
애플리케이션에 bean과 runtime behavior를 만들지 않는다. Regeneration도 durable spool을
|
||
복구할 수 없는 topology에서만 opt-in한다.
|
||
|
||
금지되는 port:
|
||
|
||
```text
|
||
open(Path)
|
||
list(String glob)
|
||
move(String from, String to)
|
||
delete(String rawPath)
|
||
getSftpSession()
|
||
```
|
||
|
||
### 9.2 Request
|
||
|
||
`FilePublishRequest`는 다음 값만 가진다.
|
||
|
||
| 필드 | 의미 |
|
||
| --- | --- |
|
||
| `operationId` | retry 전반에서 유지되는 안정적인 ID |
|
||
| `destinationId` | 설정에 등록된 logical destination |
|
||
| `logicalFileName` | display/naming input이며 path가 아님 |
|
||
| `sourceRevision` | snapshot 또는 source fingerprint |
|
||
| `schema` | column 이름, type, null/formula 정책 |
|
||
| `formatProfileId` | 등록된 CSV profile |
|
||
| `publishCondition` | create unique 또는 expected-version replace |
|
||
| `retentionClassId` | 등록된 관리 정책 |
|
||
| `protectionProfileId` | 등록된 민감도/transform 정책 |
|
||
|
||
Request에 다음 값은 없다.
|
||
|
||
- absolute/relative directory;
|
||
- provider ID;
|
||
- local/SFTP path;
|
||
- delimiter나 charset 임의 값;
|
||
- username, host, credential;
|
||
- POSIX mode;
|
||
- timeout과 pool size.
|
||
|
||
Provider와 세부 정책은 `destinationId`의 bootstrap binding이 결정한다.
|
||
|
||
### 9.3 Operation ID와 fingerprint
|
||
|
||
`FilePublishOperationId`는 필수다. Caller intent와 runtime policy를 분리해 canonical하게
|
||
계산한다.
|
||
|
||
```text
|
||
requestIntentDigest = SHA-256(
|
||
destinationId
|
||
+ logicalFileName normalized form
|
||
+ sourceRevision
|
||
+ schema version/digest
|
||
+ formatProfileId
|
||
+ publish condition
|
||
+ protection profile ID
|
||
+ retention class ID
|
||
)
|
||
|
||
operationFingerprint = SHA-256(
|
||
requestIntentDigest
|
||
+ effectivePolicyDigest
|
||
)
|
||
```
|
||
|
||
규칙:
|
||
|
||
- 동일 ID + 동일 intent/frozen policy + PUBLISHED: producer를 실행하지 않고 기존 receipt 반환;
|
||
- 동일 ID + 다른 request intent: `FILESERVER_OPERATION_MISMATCH`;
|
||
- 동일 ID + active STAGING: in-progress 또는 bounded wait;
|
||
- 동일 ID + INDETERMINATE: reconcile만 수행;
|
||
- 새 ID: 새 publication.
|
||
|
||
`effectivePolicyDigest`는 ID 문자열이 아니라 adapter가 first reservation 때 freeze한 canonical
|
||
policy snapshot의 digest다.
|
||
|
||
```text
|
||
destination binding revision
|
||
naming policy revision
|
||
format profile revision + canonical options
|
||
publication/guarantee requirements
|
||
protection transform revision
|
||
payload encryption/signing key version (사용 시)
|
||
retention policy revision
|
||
ownership/control-plane mode
|
||
```
|
||
|
||
SFTP login private-key version처럼 payload 의미를 바꾸지 않는 transport credential은 operation
|
||
fingerprint에 넣지 않되 audit/session lifecycle에는 기록한다. 반대로 payload encryption key
|
||
version은 최종 bytes와 복구에 영향을 주므로 포함한다. Raw key material은 snapshot에 없다.
|
||
|
||
기존 operation journal이 있으면 incoming `requestIntentDigest`를 먼저 비교한 뒤 현재 배포의
|
||
same-name profile을 다시 해석하지 않고 journal에 freeze된 policy snapshot으로
|
||
`operationFingerprint`를 복원한다. 해당 revision이나 key version을 더 이상 사용할 수 없으면
|
||
현재 정책으로 조용히 바꾸지 않고 `FILESERVER_POLICY_REVISION_UNAVAILABLE`로 중단한다. 새
|
||
operation만 현재 effective policy를 freeze한다.
|
||
|
||
`accepted attempt`는 request/fingerprint 검증, capacity 획득, ownership 판정까지 성공해 새
|
||
staging generation이 할당된 한 번의 실행이다. Pre-commit terminal failure 뒤 source
|
||
regeneration은 기존 operation을 다시 여는 방식이 아니라 별도 regeneration 계약으로 새
|
||
operation ID를 만든다.
|
||
|
||
Fileserver의 operation protocol은 business side effect exactly-once를 의미하지 않는다.
|
||
|
||
### 9.4 Streaming row producer
|
||
|
||
```java
|
||
@FunctionalInterface
|
||
public interface TabularRowProducer {
|
||
void produce(TabularRowSink sink);
|
||
}
|
||
|
||
public interface TabularRowSink {
|
||
void write(TabularRow row);
|
||
void checkpoint();
|
||
}
|
||
```
|
||
|
||
계약:
|
||
|
||
- 새 payload 생성이 필요한 하나의 accepted attempt에서 `produce`는 caller thread에서
|
||
동기적으로 최대 한 번 호출된다. 이미 완료된 operation은 0회다.
|
||
- Sink는 thread-safe API가 아니며 producer가 다른 thread로 넘기거나 보관할 수 없다.
|
||
- 각 `write`가 실제 bounded encoding/write를 진행하므로 자연스러운 backpressure가 생긴다.
|
||
- Deadline, cancellation, row/byte limit은 sink가 매 호출 전후에 검사한다. Producer는 page
|
||
조회 전후처럼 row를 쓰지 않는 구간에도 `checkpoint()`를 호출한다.
|
||
- Sink failure는 runtime application exception으로 producer stack을 중단시킨다.
|
||
- Producer가 던진 domain/application exception은 fileserver dependency error로 바꾸지 않는다.
|
||
- Adapter는 producer failure 시 staging만 abort하고 원래 exception을 다시 던진다.
|
||
- Cleanup failure는 원래 exception의 suppressed cause와 metric으로 남긴다.
|
||
- Adapter 내부 retry 때문에 producer를 다시 실행하지 않는다.
|
||
|
||
대용량 export use case는 producer 내부에서 stable cursor/keyset으로 source를 page 단위 조회한다.
|
||
File IO 전체를 감싸는 장시간 DB transaction은 허용하지 않는다.
|
||
|
||
이 callback은 arbitrary blocking code를 강제로 중단할 수 있는 hard-timeout 경계가 아니다.
|
||
`checkpoint()`와 thread interruption은 cooperative cancellation이며, JDBC/HTTP page source에는
|
||
각 source port의 statement/request timeout도 별도로 설정한다. Producer가 interruption을
|
||
무시하거나 외부 호출에서 영구 block될 수 있는 topology는 process/job 격리를 사용한다.
|
||
|
||
### 9.5 Typed cell과 schema
|
||
|
||
이미 문자열화된 임의 값만 받으면 locale, timezone, null 의미가 caller마다 달라진다. 따라서
|
||
다음 작은 framework-free cell set을 제공한다.
|
||
|
||
```text
|
||
TextCell
|
||
IntegerCell
|
||
DecimalCell
|
||
BooleanCell
|
||
DateCell
|
||
InstantCell
|
||
NullCell
|
||
```
|
||
|
||
임의 객체와 reflection serialization은 없다.
|
||
|
||
`ExportSchema`는 다음을 고정한다.
|
||
|
||
- schema ID와 version;
|
||
- ordered columns;
|
||
- column name;
|
||
- expected cell type;
|
||
- nullable;
|
||
- formula policy;
|
||
- max cell bytes override;
|
||
- canonical formatter 또는 caller-supplied text 허용 여부.
|
||
|
||
`InstantCell` 기본 표현은 UTC ISO-8601, number는 locale-independent canonical form이다. Partner
|
||
format이 다르면 named format profile을 등록한다.
|
||
|
||
### 9.6 Receipt
|
||
|
||
```text
|
||
operationId
|
||
fileReference
|
||
destinationId
|
||
publishedFileName
|
||
fileVersion
|
||
formatProfileId
|
||
mediaType
|
||
charset
|
||
byteSize
|
||
dataRowCount
|
||
columnCount
|
||
sha256
|
||
publishedAt
|
||
publicationGuaranteeAchieved
|
||
durabilityGuaranteeAchieved
|
||
formulaMitigatedCount
|
||
manifestSchemaVersion
|
||
effectivePolicyRevision
|
||
```
|
||
|
||
`PublishedFileReference`와 `FileVersion`은 opaque value다. Receipt에는 absolute path, SFTP URI,
|
||
host, username, credential, mount path가 없다.
|
||
|
||
### 9.7 Safe operation catalog
|
||
|
||
개발자에게 기본 제공하는 operation은 capability-oriented하다.
|
||
|
||
| Operation | 제공 목적 | 금지되는 raw 대체 |
|
||
| --- | --- | --- |
|
||
| `publishTabular` | staged CSV publish | final path direct write |
|
||
| `resolvePublish` | unknown/in-progress reconciliation | blind retry |
|
||
| `inspectPublished` | opaque reference metadata | `stat(rawPath)` |
|
||
| `verifyPublished` | size/digest/marker 검사 | caller checksum 구현 |
|
||
| `transferPublished` | bounded chunk transfer | raw `InputStream` 반환 |
|
||
| `regenerateExplicit` | prior revision을 고정한 새 operation 생성 | stale retry의 producer 재실행 |
|
||
| `deleteManaged` | version/ownership 조건부 삭제 | `Files.delete(path)` |
|
||
| `reapStaging` | 알려진 staging만 cleanup | recursive base delete |
|
||
|
||
각 operation은 atomicity scope, retry safety, state growth, provider requirement, failure result를
|
||
capability card에 기록한다.
|
||
|
||
### 9.8 Explicit regeneration
|
||
|
||
일반 `publish` retry는 `REGENERATION_REQUIRED` operation의 producer를 다시 실행하지 않는다.
|
||
Application이 데이터 재조회와 재생성을 승인할 때만 별도 계약을 사용한다.
|
||
|
||
```text
|
||
FileRegenerationRequest
|
||
priorOperationId
|
||
expectedPriorStateRevision
|
||
expectedPriorAttemptId
|
||
newRequest (full FilePublishRequest with a new operationId)
|
||
regenerationDecisionId
|
||
boundedReasonCode
|
||
policyMode = REUSE_FROZEN_POLICY
|
||
```
|
||
|
||
규칙:
|
||
|
||
1. Authorized application use case가 principal/tenant/business 승인을 먼저 수행한다.
|
||
`regenerationDecisionId`는 그 결정을 audit에 연결하는 opaque ID이지 bearer credential이
|
||
아니다.
|
||
2. Adapter는 prior operation을 먼저 reconcile한다. Final commit 가능성이 남은
|
||
`INDETERMINATE`에서는 regeneration을 거부한다.
|
||
3. Prior state가 정확히 `REGENERATION_REQUIRED`이고 expected revision/attempt가 일치해야 한다.
|
||
4. `newRequest.operationId`는 새 ID여야 하며 old journal을 다시 열지 않는다.
|
||
나머지 request intent와 schema를 canonicalize해 prior `requestIntentDigest`와 비교한다.
|
||
5. Provider control plane의 고정 key `supersessions/{priorOperationId}.json`을
|
||
exclusive-create하고 record 안에 `newRequest.operationId`, expected revision/attempt,
|
||
decision ID를 저장한다.
|
||
6. 같은 prior operation에 이미 다른 supersession이 있으면
|
||
`FILESERVER_REGENERATION_CONFLICT`다.
|
||
7. 새 operation은 원래 freeze된 policy/source revision/request intent를 재사용한다. 현재
|
||
정책으로 바꾸고 싶으면 regeneration이 아니라 별도 신규 publication이다.
|
||
8. Prior sealed digest가 남아 있으면 regenerated payload digest가 정확히 일치해야 한다.
|
||
Digest 전에 실패한 attempt는 application이 repeatable source revision을 증명할 때만
|
||
regeneration한다.
|
||
9. Provider가 exclusive supersession record를 보장하지 못하면 external job coordination이
|
||
필수며, required topology는 evidence 없이는 시작하지 않는다.
|
||
|
||
상태는 `REGENERATION_REQUIRED -> SUPERSEDED(newOperationId)`로 닫고 새 operation이
|
||
`RESERVING`에서 시작한다. 새 attempt가 실패해도 또 다른 regeneration을 만들지 않고 이미
|
||
연결된 `newOperationId`를 resolve/retry한다. Supersession record가 journal state update보다
|
||
우선하며, journal update 중 crash해도 fixed-key record에서 `SUPERSEDED`를 복원한다.
|
||
|
||
## 10. Error와 outcome 모델
|
||
|
||
현재 모든 IO failure를 `INTERNAL_ERROR`로 묶는 방식을 제거한다.
|
||
|
||
권장 skeleton-wide code:
|
||
|
||
| Error | 의미 | 기본 retry |
|
||
| --- | --- | --- |
|
||
| `FILESERVER_DISABLED` | binding/provider 비활성 | false |
|
||
| `FILESERVER_INVALID_REQUEST` | schema/name/profile 위반 | false |
|
||
| `FILESERVER_OPERATION_MISMATCH` | operation ID fingerprint 충돌 | false |
|
||
| `FILESERVER_CONFLICT` | create/expected-version 충돌 | false |
|
||
| `FILESERVER_CAPACITY_EXCEEDED` | byte/row/disk/quota 제한 | 조건부 |
|
||
| `FILESERVER_PERMISSION_DENIED` | local/remote permission | false |
|
||
| `FILESERVER_UNAVAILABLE` | mount/session/provider unavailable | true |
|
||
| `FILESERVER_TIMEOUT` | acquire/connect/idle/overall timeout | true |
|
||
| `FILESERVER_GUARANTEE_UNAVAILABLE` | required capability 미지원 | false |
|
||
| `FILESERVER_POLICY_REVISION_UNAVAILABLE` | freeze된 policy/key revision 재생 불가 | false |
|
||
| `FILESERVER_INTEGRITY_FAILED` | size/checksum/manifest mismatch | false |
|
||
| `FILESERVER_PUBLISH_INDETERMINATE` | commit 여부 불명 | blind retry 금지 |
|
||
| `FILESERVER_REGENERATION_REQUIRED` | durable sealed source가 없어 application 판단 필요 | false |
|
||
| `FILESERVER_REGENERATION_CONFLICT` | prior state/revision/supersession 충돌 | false |
|
||
| `FILESERVER_CANCELLED` | deadline/shutdown/user cancel | 조건부 |
|
||
| `FILESERVER_CLEANUP_FAILED` | staging/retention cleanup 실패 | true |
|
||
|
||
`retryable=true`는 같은 operation ID로 reconcile 또는 안전한 단계 retry가 가능하다는 뜻이다.
|
||
새 operation ID로 payload를 다시 쓰라는 뜻이 아니다.
|
||
|
||
Outcome phase를 별도 값으로 둔다.
|
||
|
||
```text
|
||
PRE_COMMIT_FAILED
|
||
PUBLISHED
|
||
INDETERMINATE
|
||
CONFLICT
|
||
ABORTED
|
||
QUARANTINED
|
||
REGENERATION_REQUIRED
|
||
SUPERSEDED
|
||
```
|
||
|
||
## 11. Publication 상태 머신
|
||
|
||
```mermaid
|
||
stateDiagram-v2
|
||
[*] --> RESERVING
|
||
RESERVING --> STAGING
|
||
STAGING --> WRITING
|
||
WRITING --> SEALED
|
||
SEALED --> PUBLISHING
|
||
PUBLISHING --> COMMITTED_UNCONFIRMED: publish ACK
|
||
PUBLISHING --> INDETERMINATE: ACK lost
|
||
COMMITTED_UNCONFIRMED --> CONFIRMING
|
||
CONFIRMING --> PUBLISHED
|
||
CONFIRMING --> INDETERMINATE: stat or journal unavailable
|
||
CONFIRMING --> QUARANTINED: integrity mismatch
|
||
|
||
RESERVING --> ABORTING
|
||
STAGING --> ABORTING
|
||
WRITING --> ABORTING
|
||
SEALED --> ABORTING
|
||
ABORTING --> ABORTED
|
||
ABORTING --> ORPHANED
|
||
|
||
PUBLISHING --> INDETERMINATE
|
||
INDETERMINATE --> RECONCILING
|
||
RECONCILING --> PUBLISHED
|
||
RECONCILING --> ABORTED
|
||
RECONCILING --> QUARANTINED
|
||
RECONCILING --> REGENERATION_REQUIRED
|
||
REGENERATION_REQUIRED --> SUPERSEDED
|
||
|
||
PUBLISHED --> DELETE_PENDING
|
||
DELETE_PENDING --> DELETED
|
||
```
|
||
|
||
Commit point:
|
||
|
||
- `ATOMIC_RENAME`: final name으로의 atomic rename 성공 응답;
|
||
- `READY_MARKER`: data와 manifest 검증 후 marker의 exclusive publish 성공;
|
||
- SFTP `POSIX_RENAME`: negotiated extension rename 성공 응답;
|
||
- 응답을 잃었다면 commit 여부를 단정하지 않고 `INDETERMINATE`.
|
||
|
||
Commit primitive가 성공해도 즉시 `PUBLISHED`가 아니다. 먼저 `COMMITTED_UNCONFIRMED`로
|
||
전환하고 final stat, digest/manifest/marker, terminal control record를 확인한다. 이 confirm이
|
||
실패하면 이미 생성됐을 수 있으므로 pre-commit failure로 되돌리지 않고 `INDETERMINATE`다.
|
||
내용 불일치가 증명되면 `QUARANTINED`다.
|
||
|
||
`PUBLISHED`는 선택 protocol이 요구하는 file size, digest, manifest, marker와 terminal receipt
|
||
snapshot이 모두 확인됐을 때만 반환한다. 이후 HTTP 응답이나 caller DB 저장이 유실되더라도
|
||
같은 operation ID의 재호출은 producer를 재실행하지 않고 이 terminal record에서 receipt를
|
||
복원한다.
|
||
|
||
### 11.1 Durable control plane과 source of truth
|
||
|
||
Operation ID만으로 idempotency가 생기지 않는다. R2 destination은 payload namespace와 별도로
|
||
provider-owned private control plane을 가져야 한다.
|
||
|
||
```text
|
||
.ca-fileserver/
|
||
operations/{operation-id-prefix}/{operation-id}.json
|
||
references/{file-id-prefix}/{file-id}.json
|
||
supersessions/{prior-operation-id}.json
|
||
staging/{operation-id}/...
|
||
quarantine/...
|
||
```
|
||
|
||
Operation journal v1 최소 필드:
|
||
|
||
```text
|
||
journalSchemaVersion
|
||
operationId
|
||
requestIntentDigest
|
||
operationFingerprint
|
||
policySnapshotSchemaVersion
|
||
effectivePolicyRevision
|
||
effectivePolicyDigest
|
||
effectivePolicySnapshot
|
||
formatPolicyDigest
|
||
protectionPolicyDigest
|
||
retentionPolicyDigest
|
||
payloadKeyVersion
|
||
destinationId
|
||
sourceRevision
|
||
schemaDigest
|
||
publishCondition
|
||
fileId
|
||
referenceRoute
|
||
generatedRelativeLocator
|
||
publishedFileName
|
||
state
|
||
stateRevision
|
||
attemptId
|
||
coordinationMode
|
||
ownerInstanceId
|
||
heartbeatAt
|
||
stageRelativeLocator
|
||
sealedByteSize
|
||
sealedSha256
|
||
manifestDigest
|
||
receiptSnapshot
|
||
createdAt
|
||
updatedAt
|
||
lastFailureCode
|
||
supersededByOperationId
|
||
```
|
||
|
||
`generatedRelativeLocator`와 staging locator는 adapter 내부 control data이며 receipt, metric,
|
||
일반 info log로 노출하지 않는다. Journal에는 raw row/cell, credential, absolute path, remote
|
||
URI를 저장하지 않는다. `effectivePolicySnapshot`은 canonical non-secret options만 포함하고
|
||
key/credential은 logical ID와 version만 기록한다.
|
||
|
||
`PublishedFileReference`는 versioned opaque route token과 random `fileId`로 구성하고 실제
|
||
provider locator를 포함하지 않는다. Adapter는 route token으로 bounded destination을 찾고
|
||
`references/{fileId}.json`을 조회한다. Reference index에는 operation ID, version, internal
|
||
relative locator, manifest digest만 저장한다. 따라서 inspect/transfer/delete가 directory
|
||
scan이나 caller path에 의존하지 않는다.
|
||
|
||
`fileId`는 충분한 entropy의 CSPRNG 값이고 reference parser는 version, 길이, route allowlist를
|
||
검증한다. 그러나 opaque/unguessable reference는 authorization token이 아니다. 어느 principal이
|
||
inspect/transfer/delete할 수 있는지는 application use case와 inbound authorization이
|
||
검증하며, fileserver는 tenant/user 권한을 추론하지 않는다.
|
||
|
||
Control record update:
|
||
|
||
1. 새 record를 private temp name으로 `CREATE_NEW`한다.
|
||
2. canonical encoding과 digest를 검증한다.
|
||
3. provider가 증명한 atomic replace 또는 marker protocol로 `stateRevision`을 전진시킨다.
|
||
4. required durability 수준에 맞춰 record와 directory/remote file을 sync한다.
|
||
5. 더 낮거나 중복된 revision은 무시하고 fingerprint가 다르면 conflict로 격리한다.
|
||
|
||
Atomic record replace는 torn/partial control file 노출을 막을 뿐 compare-and-set이나 fencing이
|
||
아니다. `stateRevision`만으로 stale writer를 막지 않는다. Record를 갱신할 writer의
|
||
single-owner evidence가 없으면 multi-node destination은 global coordination guarantee를
|
||
claim하지 않으며 required profile은 fail-fast한다.
|
||
|
||
Journal은 진행 상태와 lookup의 durable evidence지만 remote payload와 한 transaction이 아니다.
|
||
Truth priority는 다음과 같다.
|
||
|
||
```text
|
||
valid final payload + matching terminal manifest/marker
|
||
> terminal reference/operation record
|
||
> non-terminal journal
|
||
> in-memory registry
|
||
```
|
||
|
||
서로 모순되면 자동 성공/삭제하지 않고 reconciliation 또는 quarantine으로 전환한다.
|
||
|
||
Provider별 control plane:
|
||
|
||
- local/mounted: target의 private managed root 안에 두고 payload와 동일한 provider 보장으로
|
||
갱신한다.
|
||
- SFTP: remote private control directory 또는 pre-provisioned persistent local/shared control
|
||
volume 중 하나를 명시한다.
|
||
- `HANDOFF` destination이 sidecar/control file을 허용하지 않으면 persistent local/shared
|
||
control volume이 필수다.
|
||
- Ephemeral pod disk만 있는 SFTP destination은 restart 후 receipt 복원과 global
|
||
reconciliation을 보장할 수 없으므로 R2가 아니다.
|
||
|
||
Final manifest와 journal은 상호 복구에 필요한 fingerprint, file ID, manifest digest를
|
||
공유한다. Startup 전체 scan은 금지하고 direct operation/file ID lookup과 bounded background
|
||
reconciliation만 수행한다.
|
||
|
||
## 12. 공통 staged publish protocol
|
||
|
||
### 12.1 Plan
|
||
|
||
1. destination binding과 provider effective capability를 조회한다.
|
||
2. request invariant, fingerprint, duplicate operation 상태를 확인한다.
|
||
3. required guarantee와 provider descriptor를 비교한다.
|
||
4. final name, staging name, marker/manifest plan을 생성한다.
|
||
5. quota/concurrency slot과 deadline budget을 획득한다.
|
||
|
||
### 12.2 Stage
|
||
|
||
1. target filesystem/remote namespace 내부 private staging에
|
||
`.<operationId>.part`를 exclusive-create한다.
|
||
2. Temp와 final이 같은 filesystem/fsid/remote root인지 검증한다.
|
||
3. 제한 권한을 creation 시점에 설정한다.
|
||
4. CSV encoder를 bounded byte buffer 위에 구성한다.
|
||
5. row마다 schema, column count, formula, control, size, deadline을 검사한다.
|
||
6. SHA-256, byte count, row count를 streaming 중 계산한다.
|
||
7. 선택된 compression/encryption transform도 streaming pipeline 안에서 처리한다.
|
||
|
||
```text
|
||
typed cells
|
||
-> CSV encoder
|
||
-> optional compression
|
||
-> optional encryption/signature
|
||
-> digest/count/limit
|
||
-> provider staging sink
|
||
```
|
||
|
||
Baseline transform은 `NONE`이다. GZIP, PGP encryption/signature는 named protection profile로
|
||
열어두되 실제 reference implementation과 key-rotation test가 생기기 전에는 R0로 표시한다.
|
||
|
||
### 12.3 Seal
|
||
|
||
1. Encoder를 flush하고 malformed/unmappable character를 `REPORT` 정책으로 검사한다.
|
||
2. Channel/remote handle을 close하기 전에 요구되는 file sync를 수행한다.
|
||
3. Final byte size와 digest를 확정한다.
|
||
4. Frozen policy와 final locator를 포함한 private manifest draft를 canonical JSON으로 만든다.
|
||
5. Sealed digest와 manifest draft를 operation journal에 durable하게 기록한다.
|
||
6. Provider가 지원하면 staging content를 다시 stat/read-back한다.
|
||
7. Permission과 regular-file/no-link 조건을 재확인한다.
|
||
|
||
### 12.4 Publish
|
||
|
||
선택 가능한 protocol:
|
||
|
||
| Protocol | Commit point | 사용 조건 |
|
||
| --- | --- | --- |
|
||
| `UNIQUE_ATOMIC_CREATE` | staged inode -> unique final hard-link create | same filesystem, hard-link support proven |
|
||
| `ATOMIC_REPLACE` | temp -> existing final atomic replace | provider-specific replace semantics proven |
|
||
| `READY_MARKER` | verified marker exclusive publish | consumer가 marker를 이해함 |
|
||
| `SFTP_POSIX_RENAME` | negotiated posix rename | extension과 same remote filesystem |
|
||
| `DIRECT_FINAL_WRITE` | close | prod 금지 |
|
||
|
||
Required protocol이 불가능하면 실패한다. Copy+delete, remove+rename 같은 fallback으로 guarantee를
|
||
낮추지 않는다. Publish primitive의 성공 응답은 `COMMITTED_UNCONFIRMED`이며 아직 caller에게
|
||
receipt를 반환하지 않는다.
|
||
|
||
### 12.5 Confirm
|
||
|
||
1. Final stat의 type, size, version을 확인한다.
|
||
2. 가능한 provider는 digest를 read-back 검증한다.
|
||
3. Marker/manifest가 final artifact를 정확히 가리키는지 확인한다.
|
||
4. Reference index와 achieved guarantee를 포함한 receipt snapshot을 만든다.
|
||
5. Operation journal을 terminal `PUBLISHED`로 durable하게 전진시킨다.
|
||
6. quota slot을 반환하고 receipt를 반환한다.
|
||
|
||
### 12.6 Failure
|
||
|
||
- Publish 이전: staging abort/delete, 실패하면 orphan 등록;
|
||
- Publish 요청 전송 후 응답 유실: `INDETERMINATE`;
|
||
- Publish ACK 뒤 final stat/control record update 실패: `INDETERMINATE`;
|
||
- Final exists + expected digest match: PUBLISHED로 reconcile;
|
||
- Final exists + digest/fingerprint mismatch: conflict/quarantine;
|
||
- Marker exists + data 없음: integrity incident;
|
||
- Data exists + marker 없음: marker protocol에서는 unpublished residue;
|
||
- Cleanup은 원래 source exception을 덮지 않는다.
|
||
|
||
### 12.7 Artifact publication ordering
|
||
|
||
다음 artifact를 구분한다.
|
||
|
||
```text
|
||
D-stage staged payload
|
||
D-final consumer-visible payload
|
||
J-sealed durable non-terminal operation journal + manifest draft
|
||
M-private provider control-plane terminal manifest
|
||
M-public optional consumer-facing sidecar manifest
|
||
K-ready consumer-aware ready marker
|
||
R-ref opaque reference index
|
||
J-final terminal operation journal + receipt snapshot
|
||
```
|
||
|
||
모든 protocol에서 `J-sealed`가 publish primitive보다 먼저 durable해야 한다. 그래야 data commit
|
||
후 process가 죽어도 producer를 다시 실행하지 않고 final locator, expected size/digest,
|
||
fingerprint, frozen policy로 복구할 수 있다.
|
||
|
||
| Protocol | 순서 | Consumer commit point |
|
||
| --- | --- | --- |
|
||
| `UNIQUE_ATOMIC_CREATE` | D-stage sync → J-sealed → data atomic hard-link create → final confirm → M-private → R-ref → J-final | data link |
|
||
| `ATOMIC_REPLACE` | D-stage sync + expected version → J-sealed → proven atomic replace → final confirm → M-private → R-ref → J-final | data replace |
|
||
| `SFTP_POSIX_RENAME` | local spool seal/sync → J-sealed → remote part upload/(remote fsync) → POSIX rename → remote confirm → M-private → R-ref → J-final | POSIX rename |
|
||
| `READY_MARKER` | D-stage + M-public stage/sync → J-sealed → versioned data/manifest publish → verify → K-ready exclusive publish → confirm → M-private → R-ref → J-final | ready marker |
|
||
|
||
`M-private`, `R-ref`, `J-final`은 각각 temp+verified atomic replace/marker를 사용하며 마지막
|
||
`J-final`이 durable하기 전에는 port가 receipt를 반환하지 않는다. 이 세 record가 provider와
|
||
원자 transaction을 이루는 것은 아니다. 각 write는 idempotent하고 operation ID,
|
||
state revision, manifest digest로 재구성 가능해야 한다.
|
||
|
||
Single-file atomic rename protocol에서 `M-public`은 commit 구성요소가 될 수 없다. Data와 public
|
||
manifest를 하나의 consumer contract로 원자 공개해야 하면 `READY_MARKER` 또는 provider가
|
||
실패 시험으로 증명한 atomic directory/bundle publish를 선택한다. Data rename 뒤 sidecar를
|
||
추가하면서 “둘이 atomic”이라고 주장하지 않는다.
|
||
|
||
Crash 판정:
|
||
|
||
| 관찰 상태 | 복구 |
|
||
| --- | --- |
|
||
| D-stage만 있고 J-sealed 없음 | never committed; bounded abort/reap |
|
||
| J-sealed + D-stage, final/marker 없음 | sealed payload로 publish resume 또는 abort; producer 0회 |
|
||
| D-final matches J-sealed, M-private/R-ref/J-final 일부 없음 | `COMMITTED_UNCONFIRMED`; missing control record 재구성 |
|
||
| D-final digest가 J-sealed와 다름 | conflict/quarantine; overwrite 금지 |
|
||
| K-ready 존재 + matching data/public manifest | committed; private control records 재구성 |
|
||
| data/public manifest 존재 + K-ready 없음 | marker protocol에서 unpublished residue |
|
||
| R-ref 존재 + J-final 없음 | final/manifest 확인 후 J-final 복구 |
|
||
| J-final 존재 + final/marker 없음 | integrity incident; 성공으로 반환 금지 |
|
||
| publish request ACK 유실 | `INDETERMINATE`; 위 evidence로 reconcile |
|
||
|
||
Public manifest가 필요 없는 single-file consumer도 mandatory private manifest/control record는
|
||
유지한다. `HANDOFF` partner root가 private sidecar를 허용하지 않으면 별도 persistent control
|
||
volume에 둔다.
|
||
|
||
## 13. Guarantee 모델
|
||
|
||
하나의 `productionReady=true` boolean으로 provider를 표현하지 않는다.
|
||
|
||
### 13.1 Descriptor
|
||
|
||
`FileServerProviderDescriptor`는 configured claim이 아니라 effective claim이다.
|
||
|
||
| 축 | 값 예시 |
|
||
| --- | --- |
|
||
| Provider | `LOCAL_POSIX`, `SHARED_POSIX`, `SFTP` |
|
||
| Visibility scope | `NODE`, `CLUSTER`, `REMOTE_ENDPOINT` |
|
||
| Streaming | write/read/range/resume |
|
||
| Publish primitive | atomic rename, hardlink publish, marker, remote rename |
|
||
| Atomicity | atomic unique, no-replace, replace |
|
||
| Durability | file sync, directory sync, remote fsync, NFS stable commit |
|
||
| Consistency | local immediate, close-to-open, remote-server |
|
||
| Concurrency | none, advisory, leased |
|
||
| Fencing | supported/unsupported |
|
||
| Security | secure directory, no-follow, server chroot |
|
||
| Permissions | POSIX/ACL/remote chmod |
|
||
| Capacity | usable-space observation, native quota |
|
||
| Recovery | stage list/delete, reconcile, read-back digest |
|
||
| Control plane | durable operation index, reference lookup, atomic revision |
|
||
| Coordination | process-local, provider reservation, external required |
|
||
| Limits | file/chunk/concurrency/queue/in-flight/request handles/timeouts |
|
||
|
||
각 항목은 다음 상태와 증거를 함께 가진다.
|
||
|
||
```text
|
||
SupportStatus:
|
||
SUPPORTED
|
||
UNSUPPORTED
|
||
UNVERIFIABLE
|
||
|
||
Evidence:
|
||
SPEC
|
||
NEGOTIATED_EXTENSION
|
||
ACTIVE_PROBE
|
||
OPERATOR_ATTESTED
|
||
FAILURE_TESTED
|
||
```
|
||
|
||
API가 존재한다는 것과 보장이 검증됐다는 것은 다르다.
|
||
|
||
### 13.2 분리해야 하는 보장
|
||
|
||
- Atomic visibility: reader가 partial final name을 보지 않는가?
|
||
- Crash durability: host/server crash 뒤 data와 name이 남는가?
|
||
- Immediate cross-node visibility: 다른 node가 즉시 관찰하는가?
|
||
- Create-only atomicity: 같은 name 경쟁에서 정확히 한 writer만 성공하는가?
|
||
- Replace atomicity: old 또는 new만 보이고 중간 상태가 없는가?
|
||
- Outcome certainty: timeout 뒤 commit 여부를 알아낼 수 있는가?
|
||
- Integrity: provider에 저장된 bytes가 digest와 일치하는가?
|
||
|
||
한 축의 성공을 다른 축의 증거로 사용하지 않는다.
|
||
|
||
## 14. Local persistent filesystem provider
|
||
|
||
### 14.1 용도
|
||
|
||
- single-node 또는 node-attached persistent volume;
|
||
- application이 소유하는 private directory;
|
||
- generated/versioned file publication;
|
||
- 동일 node에서 소비하거나 별도 delivery가 있는 경우.
|
||
|
||
Container ephemeral directory를 production persistent filesystem으로 분류하지 않는다.
|
||
|
||
### 14.2 Startup 조건
|
||
|
||
- prod root는 absolute path;
|
||
- `autoCreate=false`;
|
||
- directory가 미리 존재;
|
||
- root와 ancestor가 symlink가 아님;
|
||
- expected owner/group/mode;
|
||
- world/group writable 정책 위반 없음;
|
||
- expected `FileStore`/device/mount sentinel 일치;
|
||
- staging과 final directory가 동일 `FileStore`;
|
||
- minimum usable-space watermark;
|
||
- required `SecureDirectoryStream`/atomic move capability probe.
|
||
|
||
Mount가 빠졌을 때 underlying local directory를 자동 생성해 성공하면 안 된다.
|
||
|
||
### 14.3 Write와 durability
|
||
|
||
- temp file `CREATE_NEW`;
|
||
- creation attribute로 기본 0600;
|
||
- publish 직전 target policy에 맞춰 0640 등 설정;
|
||
- `FileChannel.force(true)`로 file data/metadata sync;
|
||
- same-filesystem atomic move;
|
||
- required profile에서 directory sync 수행.
|
||
|
||
Java `FileChannel.force`는 local storage device에만 강한 저장장치 기록 보장을 주고 non-local
|
||
device에는 보장하지 않는다. Directory fsync는 Java portability가 낮으므로:
|
||
|
||
- portable JDK provider는 `FILE_SYNC`까지만 claim;
|
||
- Linux-specific tested implementation만 `FILE_AND_DIRECTORY_SYNC` claim;
|
||
- directory sync가 요구되는데 구현이 없으면 fail-fast;
|
||
- site replication과 backup은 별도 operation guarantee.
|
||
|
||
### 14.4 Path security
|
||
|
||
Strict mode:
|
||
|
||
- open directory-relative operation;
|
||
- `SecureDirectoryStream` 지원 시 이를 사용;
|
||
- 모든 target/staging operation은 relative single-segment name;
|
||
- `NOFOLLOW_LINKS` attribute/stat;
|
||
- final과 temp가 regular file인지 확인;
|
||
- untrusted user가 root에 entry를 만들 수 없도록 permission boundary.
|
||
|
||
`toRealPath` 선검사 후 일반 open만 수행하는 것은 check/open 사이 race를 제거하지 못한다.
|
||
Provider가 secure relative operation을 지원하지 않고 root에 untrusted writer가 있으면 R2
|
||
strict mode를 claim할 수 없다.
|
||
|
||
### 14.5 Collision
|
||
|
||
Portable Java `ATOMIC_MOVE`는 target이 이미 존재할 때 replace/fail이 implementation-specific다.
|
||
따라서 기본은 server-generated unique/versioned final name이다.
|
||
|
||
- `CREATE_UNIQUE`: UUID/ULID 기반 final name, collision 시 hard fail;
|
||
- `CREATE_IF_ABSENT` stable name: native no-replace/hardlink primitive가 증명된 provider만;
|
||
- `REPLACE_IF_VERSION`: expected version 확인 + provider-specific atomic replace evidence;
|
||
- unconditional replace: legacy profile만;
|
||
- append: 금지.
|
||
|
||
## 15. Mounted/shared filesystem provider
|
||
|
||
Mounted provider는 local provider class의 별칭이 아니다. 동일 JDK API를 사용하더라도
|
||
guarantee와 운영 검증이 다르다.
|
||
|
||
### 15.1 공통 조건
|
||
|
||
- mount는 IaC가 pre-provision;
|
||
- application auto-mount/auto-create 금지;
|
||
- expected mount sentinel과 `FileStore` identity;
|
||
- stage와 final은 target mount 내부;
|
||
- bounded concurrency, queue, in-flight bytes;
|
||
- blocking/hung IO timeout과 shutdown 정책;
|
||
- multi-client integration evidence.
|
||
|
||
Virtual thread는 blocked platform-thread 비용을 줄일 수 있지만 filesystem 또는 server 부하,
|
||
queue, byte pressure를 제한하지 않는다. Semaphore와 byte budget은 별도다.
|
||
|
||
### 15.2 NFS semantics
|
||
|
||
NFSv4 rename은 client 관점에서 atomic이며 source/target directory가 같은 server filesystem
|
||
이어야 한다. 이것이 의미하는 것은 partial final name을 피할 수 있다는 것이지 다음을
|
||
의미하지 않는다.
|
||
|
||
- 즉시 cluster-wide directory visibility;
|
||
- strong cache coherence;
|
||
- fenced lock;
|
||
- application-level exactly-once;
|
||
- Java `force`가 local disk와 같은 durability를 제공함.
|
||
|
||
NFS profile 규칙:
|
||
|
||
- immutable/versioned file 우선;
|
||
- reader는 close/reopen과 marker/manifest contract 사용;
|
||
- directory polling을 authoritative event source로 사용하지 않음;
|
||
- final receipt나 DB/message가 discoverability source;
|
||
- `FileLock`/NFS lease를 correctness의 단독 근거로 사용하지 않음;
|
||
- 동일 final name multi-writer와 append 금지;
|
||
- server `sync` export/backend stable storage는 operator attestation과 failure test 필요;
|
||
- `async` export는 strong durability profile에서 거부.
|
||
|
||
### 15.3 NFS effective guarantee
|
||
|
||
| Guarantee | 기본 판정 |
|
||
| --- | --- |
|
||
| Streaming write | SUPPORTED |
|
||
| Same-fsid rename visibility | SPEC + ACTIVE_PROBE |
|
||
| Immediate other-node discovery | UNVERIFIABLE |
|
||
| File sync from Java | UNVERIFIABLE |
|
||
| Server stable commit | OPERATOR_ATTESTED + FAILURE_TESTED 필요 |
|
||
| File lock fencing | UNSUPPORTED |
|
||
| Multi-node immutable key | SUPPORTED |
|
||
| Stable-name multi-writer replace | external coordination 필요 |
|
||
|
||
### 15.4 Mount loss
|
||
|
||
다음 상태를 구분한다.
|
||
|
||
- mount unavailable;
|
||
- stale file handle;
|
||
- mount identity changed;
|
||
- underlying local mountpoint visible;
|
||
- read-only remount;
|
||
- free space/inode exhaustion;
|
||
- server reboot/cache delay.
|
||
|
||
Sentinel mismatch나 mount identity change는 새 publication을 fail-closed하고 readiness를 내린다.
|
||
기존 staging을 자동 삭제하지 않는다.
|
||
|
||
## 16. SFTP provider
|
||
|
||
### 16.1 구현 선택
|
||
|
||
초기 구현은 fileserver leaf 안에서 Spring Integration SFTP의 programmatic API를 사용한다.
|
||
|
||
- `DefaultSftpSessionFactory`;
|
||
- bounded `CachingSessionFactory`;
|
||
- `RemoteFileTemplate.execute` 또는 `executeWithClient`;
|
||
- underlying Apache MINA `SftpClient`로 extension negotiation.
|
||
|
||
Message channel, SpEL path expression, outbound adapter를 application API로 노출하지 않는다.
|
||
Boot BOM이 관리하는 compatible `spring-integration-sftp` version을 사용하며 SDK type은 leaf 밖으로
|
||
나가지 않는다.
|
||
|
||
### 16.2 Security
|
||
|
||
- host key verification 필수;
|
||
- known-hosts 또는 pinned fingerprint/host CA;
|
||
- `allowUnknownKeys=false`;
|
||
- TOFU와 changed-key 자동 수락 금지;
|
||
- private key/agent 또는 secret-source credential;
|
||
- password/private-key literal을 YAML/log에 저장하지 않음;
|
||
- key rotation은 old/new dual trust window와 audit;
|
||
- modern cipher/KEX/MAC allowlist;
|
||
- remote account는 chroot 또는 restricted root;
|
||
- root-owned/non-group-writable chroot hierarchy;
|
||
- writable child만 service account에 허용;
|
||
- remote path는 destination binding의 fixed relative segments만 사용.
|
||
|
||
#### 16.2.1 Secret material resolution과 rotation
|
||
|
||
YAML의 `private-key-secret-ref`와 `known-hosts-secret-ref`는 문자열 치환용 secret 값이 아니라
|
||
등록된 logical reference다. 다음 adapter-private bootstrap SPI를 fileserver leaf가 소유한다.
|
||
|
||
```text
|
||
FileServerSecretMaterialProvider
|
||
acquire(SecretReference) -> SecretMaterialLease
|
||
|
||
SecretMaterialLease
|
||
version
|
||
expiresAt
|
||
readOnlyBytes/readOnlyChars
|
||
close()
|
||
```
|
||
|
||
- 이 SPI는 application port가 아니며 `application-core`에 노출하지 않는다.
|
||
- `app-bootstrap`은 허용된 config-tree/file-mounted secret source 또는 명시적으로 설치한
|
||
runtime provider를 조합한다.
|
||
- Fileserver는 sibling secret/cache adapter를 직접 호출하지 않는다.
|
||
- 여러 adapter가 같은 seam을 실제로 필요로 할 때만 별도 skeleton-wide secret contract
|
||
ADR을 작성하며, 이 설계에서 `shared-contract`를 선제 확장하지 않는다.
|
||
|
||
`SecretReference`는 bounded registry ID이고 raw file path, URI, environment variable name,
|
||
secret value를 허용하지 않는다. Config-tree 구현은 bootstrap이 고정한 private root 아래에서
|
||
no-follow/owner/mode를 검증해 읽는다. SDK가 임시 `Path`만 받는 경우 fileserver가 0600 temp
|
||
file의 생성·삭제를 소유한다.
|
||
|
||
Lifecycle:
|
||
|
||
1. inactive destination은 secret을 resolve하지 않는다.
|
||
2. required destination은 startup/readiness에서 reference 존재와 trust material parse를
|
||
확인하되 private key 내용을 log하지 않는다.
|
||
3. 새 physical SSH connection을 만들 때 current secret/trust version을 lease한다.
|
||
4. Session cache entry는 credential version과 trust version으로 표기한다.
|
||
5. TTL/rotation signal에서 old-version session을 새 대여에서 제외하고 bounded drain 후
|
||
폐기한다.
|
||
6. 진행 중 upload를 강제 중단할지는 security policy가 정하며, 중단 시 publish outcome을
|
||
reconcile한다.
|
||
7. Lease close 시 mutable buffer/temp file을 best-effort zeroize/delete한다.
|
||
|
||
JVM/SDK가 복사한 key material의 완전한 zeroization은 보장할 수 없다. Heap dump, crash dump,
|
||
debug log 접근 통제와 process isolation도 운영 통제에 포함한다. Reference resolve 실패,
|
||
만료, rotation mismatch는 credential literal fallback 없이 readiness/error로 드러낸다.
|
||
|
||
### 16.3 Pool과 timeout
|
||
|
||
명시적으로 제한한다.
|
||
|
||
- max physical SSH connections;
|
||
- max SFTP sessions/channels;
|
||
- session cache size;
|
||
- session acquisition timeout;
|
||
- connect timeout;
|
||
- authentication timeout;
|
||
- socket/read/write idle timeout;
|
||
- overall operation deadline;
|
||
- max outstanding requests;
|
||
- max packet/read/write size;
|
||
- keepalive와 stale-session test;
|
||
- shutdown drain timeout.
|
||
|
||
Spring Integration의 unbounded cache와 사실상 infinite wait default를 그대로 사용하지 않는다.
|
||
|
||
### 16.4 Selected staging strategy
|
||
|
||
R2 SFTP는 producer를 remote network retry 때문에 재실행하지 않도록 local secure spool을 기본으로
|
||
사용한다.
|
||
|
||
```text
|
||
row producer
|
||
-> bounded local encrypted/secure spool + digest
|
||
-> remote .part upload
|
||
-> remote stat/fsync if supported
|
||
-> negotiated publish
|
||
-> local spool cleanup
|
||
```
|
||
|
||
Trade-off:
|
||
|
||
- heap은 bounded;
|
||
- local disk capacity는 추가 필요;
|
||
- network retry 시 source query를 반복하지 않음;
|
||
- sensitive file은 local spool encryption/protection profile 필요;
|
||
- spool reaper와 quota가 필수.
|
||
|
||
Direct pipe streaming은 retry와 failure isolation이 약하므로 R1 opt-in으로만 둔다.
|
||
|
||
### 16.5 Extension handshake
|
||
|
||
다음 extension은 존재한다고 가정하지 않고 연결마다 협상한다.
|
||
|
||
| Extension | 제공 가능 보장 |
|
||
| --- | --- |
|
||
| `posix-rename@openssh.com` | POSIX rename semantics |
|
||
| `fsync@openssh.com` | open remote file의 server fsync |
|
||
| `statvfs@openssh.com` | capacity observation |
|
||
| `limits@openssh.com` | packet/request/handle 제한 |
|
||
| `hardlink@openssh.com` | 검증된 same-fs hardlink publication 후보 |
|
||
|
||
Base SFTP v3 rename은 atomicity를 약속하지 않는다. `posix-rename`이 없고 destination이
|
||
atomic rename을 요구하면 startup/readiness 실패다.
|
||
|
||
### 16.6 SFTP durability 한계
|
||
|
||
`fsync@openssh.com`은 file handle만 sync한다. Directory-entry fsync extension은 없으므로
|
||
SFTP provider는 `FILE_AND_DIRECTORY_CRASH_DURABLE`을 claim하지 않는다.
|
||
|
||
가능한 receipt:
|
||
|
||
```text
|
||
REMOTE_FILE_SYNCED
|
||
REMOTE_RENAME_ACKNOWLEDGED
|
||
REMOTE_NAME_DURABILITY_UNVERIFIED
|
||
```
|
||
|
||
### 16.7 Unknown outcome
|
||
|
||
다음은 `FAILED`가 아니라 `INDETERMINATE`다.
|
||
|
||
- rename request 이후 connection reset;
|
||
- server가 commit 후 response 전 crash;
|
||
- timeout이 publish request와 겹침;
|
||
- client shutdown 중 remote ACK 유실.
|
||
|
||
Reconcile:
|
||
|
||
1. 같은 operation ID의 final/part/marker를 stat;
|
||
2. final size와 manifest digest 비교;
|
||
3. 가능한 경우 read-back digest;
|
||
4. final match면 기존 receipt 복원;
|
||
5. part만 있으면 resume 조건 또는 cleanup 판단;
|
||
6. final mismatch면 overwrite하지 않고 conflict/quarantine.
|
||
|
||
### 16.8 Resume
|
||
|
||
Resume는 기본 off다. 다음 조건을 모두 만족할 때만 가능하다.
|
||
|
||
- source spool이 immutable/repeatable;
|
||
- expected total size와 digest가 있음;
|
||
- remote partial prefix가 동일 source prefix임을 검증;
|
||
- server offset write semantics가 검증됨;
|
||
- operation ID와 staging name이 동일;
|
||
- overall deadline 안에서 재개.
|
||
|
||
단순 remote size만 보고 offset부터 이어 쓰지 않는다.
|
||
|
||
## 17. CSV format와 spreadsheet safety
|
||
|
||
### 17.1 Named profile
|
||
|
||
임의 delimiter option을 request마다 받지 않고 named profile을 등록한다.
|
||
|
||
Baseline:
|
||
|
||
- `CSV_RFC4180_MACHINE`;
|
||
- `CSV_SPREADSHEET_SAFE`.
|
||
|
||
Partner dialect는 fork가 별도 profile ID로 추가한다.
|
||
|
||
### 17.2 RFC 4180 machine profile
|
||
|
||
- media type `text/csv`;
|
||
- UTF-8;
|
||
- CRLF record separator;
|
||
- optional header 여부 명시;
|
||
- 모든 row는 schema와 같은 field count;
|
||
- comma separator;
|
||
- double-quote escaping;
|
||
- null과 empty string 정책 분리;
|
||
- BOM off가 기본;
|
||
- malformed/unmappable encoding은 fail;
|
||
- NUL 및 금지 control character 정책;
|
||
- final line break 정책 명시.
|
||
|
||
현재 구현의 LF-only 출력을 그대로 RFC 4180이라고 부르지 않는다.
|
||
|
||
### 17.3 Spreadsheet formula
|
||
|
||
CSV quoting은 formula execution을 막지 않는다. 모든 column은 다음 중 하나를 선택한다.
|
||
|
||
| Policy | 동작 |
|
||
| --- | --- |
|
||
| `REJECT_FORMULA_LIKE` | formula-like value면 export 실패 |
|
||
| `SPREADSHEET_TEXT_PREFIX` | named profile의 명시적 literalization |
|
||
| `PRESERVE_MACHINE_DATA` | 변환하지 않으며 spreadsheet용 아님 |
|
||
| `TRUSTED_VALUE` | 생성값에만 제한적으로 사용 |
|
||
|
||
판정은 leading whitespace/control normalization 후 `=`, `+`, `-`, `@`, tab, CR/LF와
|
||
separator/quote를 통한 새 cell 형성 가능성을 검사한다.
|
||
|
||
범용으로 모든 spreadsheet와 machine re-import에 동시에 안전한 변환은 없다. 따라서:
|
||
|
||
- machine용과 human-spreadsheet용 profile을 분리;
|
||
- 변환은 schema에 명시;
|
||
- 변환된 cell count를 receipt/metric에 기록;
|
||
- 원문 cell을 log/audit에 남기지 않음;
|
||
- round-trip data preservation을 요구하면 formula-like cell을 reject.
|
||
|
||
### 17.4 Limits
|
||
|
||
Binding과 profile이 제한한다.
|
||
|
||
- max columns;
|
||
- max rows;
|
||
- max cell characters;
|
||
- max encoded cell bytes;
|
||
- max row bytes;
|
||
- max total output bytes;
|
||
- max header bytes;
|
||
- max multiline cell lines;
|
||
- max operation duration.
|
||
|
||
Limit은 사전 추정만 하지 않고 streaming 중 byte counter로 강제한다.
|
||
|
||
### 17.5 CSV test oracle
|
||
|
||
- golden byte snapshot;
|
||
- RFC edge case;
|
||
- comma/quote/CR/LF/CRLF;
|
||
- emoji와 multi-byte chunk boundary;
|
||
- unpaired surrogate/encoding failure;
|
||
- null vs empty;
|
||
- row width mismatch;
|
||
- duplicate/blank header policy;
|
||
- formula vector와 leading whitespace;
|
||
- output limit 직전/초과;
|
||
- locale/timezone 독립.
|
||
|
||
## 18. File name, path, permission security
|
||
|
||
### 18.1 Logical name
|
||
|
||
`logicalFileName`은 path가 아니다.
|
||
|
||
- Unicode NFC normalization;
|
||
- single logical stem;
|
||
- max UTF-8 bytes;
|
||
- control/NUL/separator 금지;
|
||
- `.`/`..` 금지;
|
||
- Windows device name/drive/UNC grammar 금지;
|
||
- extension은 format/protection profile이 생성;
|
||
- physical final name은 constrained template이 생성.
|
||
|
||
예:
|
||
|
||
```text
|
||
{logicalStem}-{utcDate}-{operationId}.csv
|
||
```
|
||
|
||
Arbitrary SpEL과 caller-supplied subdirectory expression을 사용하지 않는다.
|
||
|
||
### 18.2 Directory binding
|
||
|
||
Directory는 configuration에서만 온다.
|
||
|
||
- normalized fixed relative segments;
|
||
- no `..`;
|
||
- no absolute override;
|
||
- provider root 바깥으로 나갈 수 없음;
|
||
- segment별 max length;
|
||
- startup 시 resolved root 검증.
|
||
|
||
### 18.3 TOCTOU와 links
|
||
|
||
Unsafe pattern:
|
||
|
||
```text
|
||
normalize -> startsWith -> later open
|
||
```
|
||
|
||
Safe baseline:
|
||
|
||
- private trusted root;
|
||
- secure directory-relative open/move;
|
||
- `CREATE_NEW`;
|
||
- no-follow attribute checks;
|
||
- target type regular-file 검증;
|
||
- unpredictable staging name;
|
||
- strict directory ownership/permissions.
|
||
|
||
Hardlink 공격은 portable Java stat만으로 완전히 막기 어렵다. Root에 untrusted writer가 없다는
|
||
permission precondition이 핵심이다.
|
||
|
||
### 18.4 Permission
|
||
|
||
- staging file: owner read/write only;
|
||
- final file: binding의 explicit owner/group/mode;
|
||
- directory: traversal 가능한 최소 권한;
|
||
- umask에만 의존하지 않고 creation attribute/chmod 검증;
|
||
- SFTP는 publish 전에 remote permission 설정;
|
||
- ACL/POSIX 미지원 provider는 required permission guarantee를 claim하지 않음.
|
||
|
||
## 19. Collision, concurrency, idempotency
|
||
|
||
### 19.1 Publication mode
|
||
|
||
```text
|
||
CREATE_UNIQUE
|
||
CREATE_IF_ABSENT
|
||
REPLACE_IF_VERSION
|
||
```
|
||
|
||
`REPLACE_ALWAYS`는 dev/legacy exception이고 `APPEND`는 baseline에서 금지한다.
|
||
|
||
### 19.2 Same operation concurrency
|
||
|
||
한 process:
|
||
|
||
- in-memory bounded operation registry로 같은 process의 concurrent duplicate producer 실행 방지;
|
||
- terminal result cache는 optimization일 뿐 source of truth가 아님.
|
||
|
||
Multi-node:
|
||
|
||
- shared filesystem의 exclusive reservation과 stale-owner recovery가 failure test로 검증됐으면
|
||
operation claim `CREATE_NEW`;
|
||
- SFTP에는 portable fenced claim이 없으므로 application export-job store가 단일 worker claim;
|
||
- fileserver는 DB/Redis adapter를 직접 호출하지 않음;
|
||
- provider descriptor가 `EXTERNAL_OPERATION_COORDINATION_REQUIRED`를 표시.
|
||
|
||
보장 수준을 구분한다.
|
||
|
||
| 보장 | 필요 evidence |
|
||
| --- | --- |
|
||
| accepted attempt 내부 producer at-most-once | 모든 provider baseline |
|
||
| process 내부 concurrent single-flight | in-memory registry |
|
||
| crash 뒤 sealed payload 재사용 | durable journal + persistent spool/staging |
|
||
| 여러 node에서 producer global at-most-once | durable single-owner claim과 실패 후 ownership 규칙 |
|
||
| 하나의 logical final로 수렴 | stable source revision + immutable operation-derived name + reconcile |
|
||
| stable name conditional replace | provider CAS/fencing primitive 또는 authoritative external metadata |
|
||
|
||
외부 job claim이 lease 만료 뒤 old worker를 fence하지 못하면 global at-most-once로 기록하지
|
||
않는다. 이 경우 두 producer가 실행될 수 있으므로 source revision은 repeatable해야 하고,
|
||
동일 operation의 sealed digest가 다르면 어느 쪽도 overwrite하지 않고 quarantine한다.
|
||
|
||
SFTP local spool을 seal한 뒤에는 network retry가 producer를 재실행하지 않는다. 그러나 pod
|
||
장애로 ephemeral spool이 사라졌다면 기존 attempt를 재생할 수 없다. Persistent spool/control
|
||
plane이 없으면 operation은 `REGENERATION_REQUIRED`로 끝나며 global at-most-once 또는
|
||
restart-safe idempotency를 claim하지 않는다. R2 required destination은 이 topology를
|
||
fail-fast한다.
|
||
|
||
### 19.3 Stable final name
|
||
|
||
여러 pod가 같은 stable name을 교체해야 하면 fileserver lock만으로 correctness를 만들지 않는다.
|
||
|
||
- immutable version file을 먼저 publish;
|
||
- current pointer/marker를 expected-version 조건으로 교체;
|
||
- provider가 atomic conditional replace를 증명하지 못하면 external authoritative metadata 사용;
|
||
- reader가 generation/version을 검증.
|
||
|
||
### 19.4 FileLock
|
||
|
||
Java `FileLock`은 advisory로 취급하며:
|
||
|
||
- 같은 JVM thread coordination 용도가 아님;
|
||
- NFS에서는 lease/failover 한계;
|
||
- fencing token을 제공하지 않음;
|
||
- correctness의 단독 근거가 아님.
|
||
|
||
## 20. Transaction과 async export workflow
|
||
|
||
Database와 fileserver는 하나의 transaction이 아니다.
|
||
|
||
### 20.1 금지 shape
|
||
|
||
```text
|
||
DB write transaction begin
|
||
-> file publish
|
||
-> DB save
|
||
commit
|
||
```
|
||
|
||
DB rollback이 이미 published file을 되돌리지 못한다.
|
||
|
||
### 20.2 권장 large export
|
||
|
||
```text
|
||
REQUESTED export job commit
|
||
-> worker claims job
|
||
-> stable snapshot/cursor pages
|
||
-> fileserver publish(operationId)
|
||
-> receipt persist
|
||
-> COMPLETED
|
||
```
|
||
|
||
규칙:
|
||
|
||
- Fileserver는 job table과 scheduler를 소유하지 않음;
|
||
- application이 retry/cancel/authorization/source snapshot을 소유;
|
||
- fileserver는 one publish attempt와 reconcile을 소유;
|
||
- page query는 짧은 read transaction;
|
||
- source revision이 변하면 same operation fingerprint conflict;
|
||
- HTTP request thread에서 무제한 대용량 export를 실행하지 않음.
|
||
|
||
### 20.3 DB command와 file delivery
|
||
|
||
File delivery가 command 결과에 필수면:
|
||
|
||
- business transaction에 delivery intent/outbox/job을 기록;
|
||
- commit 후 worker가 file publish;
|
||
- 실패는 retry/terminal compensation;
|
||
- downstream acknowledgement가 필요하면 별도 inbound receipt use case.
|
||
|
||
## 21. Quota, backpressure, timeout, shutdown
|
||
|
||
### 21.1 Resource budgets
|
||
|
||
Per binding/provider:
|
||
|
||
- max active publications;
|
||
- max queued publications;
|
||
- max in-flight buffer bytes;
|
||
- max local spool bytes;
|
||
- max file bytes;
|
||
- max rows/columns/cell bytes;
|
||
- min usable-space watermark;
|
||
- max staging artifacts/bytes;
|
||
- max remote sessions/handles/outstanding requests.
|
||
|
||
Virtual thread를 사용해도 이 budget은 제거되지 않는다.
|
||
|
||
### 21.2 Capacity observation 한계
|
||
|
||
`FileStore.getUsableSpace`와 SFTP `statvfs`는 관찰값이지 reservation이 아니다. 다른 process와
|
||
node가 동시에 쓸 수 있다.
|
||
|
||
- Application은 per-operation byte limit을 강제;
|
||
- native user/group/project quota가 있으면 operator capability로 기록;
|
||
- logical tenant quota가 필요하면 external reservation ledger가 필요;
|
||
- disk low watermark는 새 publish를 거부;
|
||
- pressure 상황에서 unknown final을 임의 삭제하지 않음.
|
||
|
||
### 21.3 Timeout 분리
|
||
|
||
```text
|
||
queueAcquireTimeout
|
||
sessionAcquireTimeout
|
||
connectTimeout
|
||
authenticationTimeout
|
||
idleReadWriteTimeout
|
||
contentProductionDeadline (cooperative)
|
||
publishCommitTimeout
|
||
overallOperationDeadline
|
||
shutdownDrainTimeout
|
||
```
|
||
|
||
Timeout 뒤 underlying local/NFS/SFTP IO가 즉시 중단됐다고 가정하지 않는다. Publish 단계와 겹치면
|
||
`INDETERMINATE`로 전환한다.
|
||
|
||
Timeout guarantee를 capability에 기록한다.
|
||
|
||
- queue/session/connect/provider IO: 해당 client가 제공하는 cancel/close와 deadline으로 강제;
|
||
- content production: `sink.write/checkpoint`와 thread interruption 기반 cooperative deadline;
|
||
- upstream DB/HTTP query: 그 port/client의 statement/request timeout이 별도로 필요;
|
||
- arbitrary producer code: Java thread를 안전하게 강제 종료할 수 없으므로 hard timeout을
|
||
주장하지 않음;
|
||
- hard wall-clock isolation이 필수인 대규모 export: 별도 worker process/job을 종료한 뒤
|
||
fileserver staging을 reconcile.
|
||
|
||
### 21.4 Cancellation
|
||
|
||
- row sink가 deadline/cancel을 확인;
|
||
- local channel은 interrupt/close;
|
||
- SFTP session/channel은 cancel 시 dirty/close;
|
||
- staging은 commit 전이면 abort;
|
||
- publish commit이 시작됐으면 결과 reconcile;
|
||
- cancellation 결과도 operation journal에 남김.
|
||
|
||
### 21.5 Graceful shutdown
|
||
|
||
1. 새 publication 접수 중단;
|
||
2. queued request reject;
|
||
3. active stage를 bounded drain;
|
||
4. commit phase는 atomic operation을 마치거나 INDETERMINATE 기록;
|
||
5. SFTP pool close;
|
||
6. 남은 staging은 다음 startup reconciliation 대상.
|
||
|
||
## 22. Manifest, marker, retention, reconciliation
|
||
|
||
### 22.1 Manifest v1
|
||
|
||
Canonical JSON:
|
||
|
||
```text
|
||
manifestSchemaVersion
|
||
operationId
|
||
requestIntentDigest
|
||
operationFingerprint
|
||
effectivePolicyRevision
|
||
effectivePolicyDigest
|
||
fileReference
|
||
fileId
|
||
fileVersion
|
||
destinationId
|
||
logicalFileName
|
||
publishedFileName
|
||
sourceRevision
|
||
schemaId
|
||
exportSchemaVersion
|
||
schemaDigest
|
||
publishCondition
|
||
formatProfileId
|
||
protectionProfileId
|
||
protectionPolicyDigest
|
||
payloadKeyVersion
|
||
retentionPolicyDigest
|
||
byteSize
|
||
rowCount
|
||
columnCount
|
||
sha256
|
||
publicationGuarantee
|
||
durabilityGuarantee
|
||
createdAt
|
||
publishedAt
|
||
retentionClassId
|
||
formulaMitigatedCount
|
||
```
|
||
|
||
Provider-private terminal manifest에는 복구용 `generatedRelativeLocator`와 operation journal
|
||
revision을 추가할 수 있다. 외부 consumer가 보는 public manifest와 private control manifest를
|
||
분리하며, public manifest에는 internal locator를 넣지 않는다.
|
||
|
||
포함 금지:
|
||
|
||
- absolute/remote path;
|
||
- credential;
|
||
- raw row/cell;
|
||
- raw tenant/user ID;
|
||
- secret key ID를 넘어선 key material.
|
||
|
||
Checksum은 accidental corruption 검출이지 authenticity가 아니다. Shared directory를 신뢰할 수
|
||
없으면 signed/HMAC manifest protection profile이 필요하다.
|
||
|
||
### 22.2 Marker protocol
|
||
|
||
```text
|
||
version/data.csv
|
||
version/manifest.json
|
||
version/_SUCCESS
|
||
```
|
||
|
||
`_SUCCESS`가 manifest digest를 담고 마지막 commit point가 된다.
|
||
|
||
Marker protocol은 marker를 이해하는 consumer에게만 atomic publication이다. 외부 시스템이
|
||
단순히 `*.csv`를 scan하면 marker profile을 선택할 수 없다.
|
||
|
||
### 22.3 Ownership mode
|
||
|
||
```text
|
||
HANDOFF
|
||
MANAGED
|
||
```
|
||
|
||
- `HANDOFF`: published final은 외부 consumer 소유로 간주하고 auto-delete하지 않음;
|
||
- `MANAGED`: manifest/ref/version이 fileserver 소유임을 증명하는 artifact만 retention 적용.
|
||
|
||
두 mode 모두 staging cleanup은 수행할 수 있다.
|
||
|
||
### 22.4 Reaper
|
||
|
||
Default는 `REPORT_ONLY`.
|
||
|
||
1. bounded batch로 staging manifest 조회;
|
||
2. operation heartbeat/deadline + clock-skew grace;
|
||
3. active operation 보호;
|
||
4. stale candidate를 `.reap` 또는 provider claim으로 이동;
|
||
5. final/marker/manifest 재확인;
|
||
6. 알려진 fileserver-owned entry만 no-follow delete;
|
||
7. unknown/malformed entry quarantine 또는 report;
|
||
8. delete rate/duration limit;
|
||
9. audit와 metric 기록.
|
||
|
||
Atomic cleanup claim이 없는 multi-node provider:
|
||
|
||
- single maintenance runner를 deployment가 보장하거나;
|
||
- application composition이 leadership을 제공하거나;
|
||
- report-only로 제한.
|
||
|
||
Fileserver가 Redis/JDBC lock adapter를 직접 의존하지 않는다.
|
||
|
||
### 22.5 Reconciliation cases
|
||
|
||
| 관찰 상태 | 기본 판정 |
|
||
| --- | --- |
|
||
| final + matching manifest/digest | PUBLISHED 복원 |
|
||
| final + digest mismatch | QUARANTINED/incident |
|
||
| temp only, active lease | 유지 |
|
||
| stale temp only | abort/reap 후보 |
|
||
| final data, marker 없음 | marker protocol에서 unpublished |
|
||
| marker, data 없음 | integrity incident |
|
||
| manifest, data 없음 | integrity incident |
|
||
| unknown external file | 보존/report |
|
||
| newer unknown manifest schema | 보존/quarantine |
|
||
| SFTP part + no final | resume 조건 또는 cleanup |
|
||
| SFTP final + ACK lost | stat/digest 후 PUBLISHED 복원 |
|
||
|
||
### 22.6 Retention delete
|
||
|
||
```text
|
||
ELIGIBLE
|
||
-> DELETE_PENDING/tombstone
|
||
-> provider delete
|
||
-> DELETED audit
|
||
```
|
||
|
||
- expected version;
|
||
- legal hold;
|
||
- retention grace;
|
||
- unknown schema 보존;
|
||
- delete failure retry;
|
||
- no recursive delete of base/root;
|
||
- target resolution은 opaque reference only.
|
||
|
||
## 23. Configuration design
|
||
|
||
### 23.1 Activation SSOT
|
||
|
||
Binding map이 activation SSOT다. Binding이 없으면 capability는 inactive다.
|
||
|
||
```yaml
|
||
ca-skeleton:
|
||
fileserver:
|
||
destinations:
|
||
worklog-export:
|
||
required: true
|
||
policy-revision: worklog-export-v1
|
||
provider-ref: mounted-primary
|
||
directory: outbound/worklog
|
||
format-profile-ref: csv-machine
|
||
publication:
|
||
protocol: unique-atomic-rename
|
||
collision: create-unique
|
||
required-visibility: atomic-final-name
|
||
required-durability: file-sync
|
||
ownership: managed
|
||
retention-class-ref: export-7d
|
||
protection-profile-ref: internal
|
||
limits:
|
||
max-file-size: 1GB
|
||
max-rows: 5000000
|
||
max-columns: 100
|
||
max-cell-size: 1MB
|
||
max-duration: 30m
|
||
|
||
providers:
|
||
mounted-primary:
|
||
type: mounted
|
||
root-directory: ${APP_FILESERVER_PRIMARY_ROOT}
|
||
auto-create: false
|
||
mount-sentinel: .ca-fileserver-mount
|
||
expected-mount-id: ${APP_FILESERVER_PRIMARY_MOUNT_ID}
|
||
min-usable-space: 10GB
|
||
max-concurrent-publications: 4
|
||
max-queued-publications: 16
|
||
control-plane:
|
||
mode: target-private
|
||
directory: .ca-fileserver
|
||
|
||
partner-sftp:
|
||
type: sftp
|
||
host: ${APP_FILESERVER_SFTP_HOST}
|
||
port: 22
|
||
username: ${APP_FILESERVER_SFTP_USERNAME}
|
||
secret-material-provider-ref: config-tree-primary
|
||
private-key-secret-ref: ${APP_FILESERVER_SFTP_PRIVATE_KEY_SECRET_REF}
|
||
known-hosts-secret-ref: ${APP_FILESERVER_SFTP_KNOWN_HOSTS_SECRET_REF}
|
||
allow-unknown-keys: false
|
||
session-cache-size: 4
|
||
session-wait-timeout: 2s
|
||
connect-timeout: 5s
|
||
operation-timeout: 2m
|
||
require-extensions:
|
||
- posix-rename@openssh.com
|
||
control-plane:
|
||
mode: persistent-local
|
||
root-directory: ${APP_FILESERVER_SFTP_CONTROL_ROOT}
|
||
local-spool:
|
||
root-directory: ${APP_FILESERVER_SFTP_SPOOL_ROOT}
|
||
persistent: true
|
||
max-total-size: 20GB
|
||
at-rest-protection-profile-ref: spool-internal
|
||
|
||
secret-material-providers:
|
||
config-tree-primary:
|
||
type: config-tree
|
||
root-directory: ${APP_FILESERVER_SECRET_CONFIG_ROOT}
|
||
auto-create: false
|
||
|
||
format-profiles:
|
||
csv-machine:
|
||
type: csv
|
||
revision: csv-machine-v1
|
||
dialect: rfc4180
|
||
charset: UTF-8
|
||
line-ending: CRLF
|
||
bom: false
|
||
formula-mode: preserve-machine-data
|
||
|
||
protection-profiles:
|
||
internal:
|
||
revision: internal-v1
|
||
payload-transform: none
|
||
spool-internal:
|
||
revision: spool-internal-v1
|
||
payload-transform: none
|
||
require-encrypted-spool-volume: true
|
||
|
||
retention-classes:
|
||
export-7d:
|
||
revision: export-7d-v1
|
||
duration: 7d
|
||
mode: report-only
|
||
```
|
||
|
||
위 값은 topology 예시이며 제품별 실제 size/timeout 수치를 의미하지 않는다.
|
||
|
||
### 23.2 Typed settings
|
||
|
||
- immutable constructor-bound record;
|
||
- Bean Validation과 cross-field validator;
|
||
- duration/data-size typed value;
|
||
- explicit immutable policy revision과 canonical snapshot digest;
|
||
- provider별 sealed settings;
|
||
- blank/default path 금지;
|
||
- prod relative path 금지;
|
||
- secret material 대신 secret reference;
|
||
- unknown property fail;
|
||
- inactive provider는 bean/connection 생성 없음.
|
||
|
||
### 23.3 Startup validation
|
||
|
||
- destination/provider/profile/reference 존재;
|
||
- duplicate ID 없음;
|
||
- same ID/revision에 다른 canonical policy 금지;
|
||
- journal이 참조하는 N/N-1 frozen policy revision 가용;
|
||
- exact provider binding;
|
||
- required guarantee 충족;
|
||
- local-dev prod 금지;
|
||
- mounted root absolute/pre-provisioned;
|
||
- auto-create prod 금지;
|
||
- R2 control plane이 persistent이고 operation/reference direct lookup을 지원;
|
||
- SFTP spool/control root의 absolute/pre-provisioned/owner/mode/capacity;
|
||
- cluster-wide resolution을 요구하면 control volume의 모든 node 접근성과 coordination evidence;
|
||
- marker protocol과 consumer compatibility;
|
||
- replace mode와 provider replace capability;
|
||
- SFTP known-host와 bounded timeout/pool;
|
||
- secret provider root와 reference grammar, material owner/mode, rotation/session-drain policy;
|
||
- managed retention과 manifest support;
|
||
- staging/final same target namespace;
|
||
- required health/metrics registration.
|
||
|
||
### 23.4 Environment registry
|
||
|
||
`docs/registries/env-keys.yaml`, `application.yml`, typed settings, conditional beans를 end-to-end
|
||
검증한다.
|
||
|
||
Template baseline에 필요한 key 예:
|
||
|
||
```text
|
||
APP_FILESERVER_PRIMARY_ROOT
|
||
APP_FILESERVER_PRIMARY_MOUNT_ID
|
||
APP_FILESERVER_SFTP_HOST
|
||
APP_FILESERVER_SFTP_USERNAME
|
||
APP_FILESERVER_SFTP_PRIVATE_KEY_SECRET_REF
|
||
APP_FILESERVER_SFTP_KNOWN_HOSTS_SECRET_REF
|
||
APP_FILESERVER_SFTP_CONTROL_ROOT
|
||
APP_FILESERVER_SFTP_SPOOL_ROOT
|
||
APP_FILESERVER_SECRET_CONFIG_ROOT
|
||
```
|
||
|
||
Dynamic destination topology는 YAML/config tree가 소유하고 secret value는 secret source가
|
||
제공한다.
|
||
|
||
## 24. Health와 observability
|
||
|
||
### 24.1 Health
|
||
|
||
| Probe | 내용 |
|
||
| --- | --- |
|
||
| Liveness | JVM/process만; filesystem/SFTP 금지 |
|
||
| Startup | settings, destination graph, capability probe, manifest compatibility |
|
||
| Readiness | enabled + required destination만 |
|
||
| Component health | optional destination도 상태 노출 |
|
||
|
||
Filesystem readiness:
|
||
|
||
- root/mount sentinel;
|
||
- read-only/permission;
|
||
- mount identity;
|
||
- usable-space/inode watermark;
|
||
- cached bounded create/write/force/rename/delete probe;
|
||
- 전체 directory scan 금지.
|
||
|
||
SFTP readiness:
|
||
|
||
- bounded session acquire/connect/auth;
|
||
- host key;
|
||
- cached extension capability;
|
||
- remote root stat;
|
||
- pool saturation;
|
||
- write probe는 dedicated hidden probe directory에서 low-frequency opt-in.
|
||
|
||
### 24.2 Metrics
|
||
|
||
```text
|
||
fileserver.operation.duration
|
||
fileserver.operation.total{provider_type,operation,outcome,failure_code}
|
||
fileserver.bytes
|
||
fileserver.rows
|
||
fileserver.active
|
||
fileserver.queue.depth
|
||
fileserver.queue.rejected
|
||
fileserver.quota.rejected
|
||
fileserver.publish.indeterminate
|
||
fileserver.reconcile.total{outcome}
|
||
fileserver.integrity.failure
|
||
fileserver.staging.age
|
||
fileserver.staging.bytes
|
||
fileserver.cleanup.total{outcome}
|
||
fileserver.usable_space.ratio
|
||
fileserver.sftp.session.active
|
||
fileserver.sftp.session.wait
|
||
fileserver.sftp.reconnect
|
||
fileserver.formula.mitigated
|
||
```
|
||
|
||
Allowed tags:
|
||
|
||
- provider type/ID from bounded registry;
|
||
- destination ID from bounded registry;
|
||
- operation kind;
|
||
- outcome/failure code;
|
||
- format profile;
|
||
- publication guarantee.
|
||
|
||
Forbidden tags:
|
||
|
||
- operation/file ID;
|
||
- file name/path;
|
||
- checksum;
|
||
- tenant/user;
|
||
- host when dynamically unbounded;
|
||
- row/cell content.
|
||
|
||
### 24.3 Trace
|
||
|
||
Span:
|
||
|
||
```text
|
||
fileserver.publish
|
||
fileserver.stage
|
||
fileserver.provider.upload
|
||
fileserver.publish.commit
|
||
fileserver.reconcile
|
||
fileserver.cleanup
|
||
```
|
||
|
||
Attributes are bounded provider/destination/profile/guarantee/outcome only. Operation ID는 log/trace
|
||
correlation field로 사용할 수 있지만 metric tag로 쓰지 않는다.
|
||
|
||
### 24.4 Log와 audit
|
||
|
||
- physical path와 remote URI를 info log에 기록하지 않음;
|
||
- credential, known-host content, cell value 금지;
|
||
- publish/unknown/reconcile/delete는 structured event;
|
||
- overwrite/delete/retention은 audit 대상;
|
||
- diagnostic cause는 server log only;
|
||
- filename이 민감할 수 있으므로 logical name도 기본 mask.
|
||
|
||
## 25. Threat model
|
||
|
||
| 위협 | 통제 |
|
||
| --- | --- |
|
||
| `../`, absolute, drive/UNC | caller path 제거, destination ID + logical stem |
|
||
| nested/target symlink | secure relative operation, no-follow, trusted root |
|
||
| check/open TOCTOU | `SecureDirectoryStream` 또는 provider strict capability |
|
||
| hardlink attack | private directory UID/mode, untrusted writer 금지 |
|
||
| mount 누락 local fallback | pre-provision, auto-create off, mount sentinel/identity |
|
||
| partial final | staging + atomic rename/marker |
|
||
| old good file truncation | direct final write 금지 |
|
||
| arbitrary overwrite | create unique/expected version |
|
||
| same-name writer race | immutable name, provider primitive, external coordination |
|
||
| process crash | state/manifest/reconcile |
|
||
| publish ACK loss | INDETERMINATE, no blind retry |
|
||
| CSV formula | column formula policy |
|
||
| delimiter/control injection | schema/dialect encoder |
|
||
| heap/disk/inode exhaustion | streaming limits, quota, watermarks |
|
||
| PII leakage | path/content-free logs/metrics/receipt |
|
||
| SFTP MITM | pinned known-host/host CA |
|
||
| credential leakage | secret reference and log scrub |
|
||
| malicious remote server | size/time/request limits, strict extension parsing |
|
||
| forged manifest | trusted control dir 또는 signed manifest |
|
||
| reaper over-delete | owned manifest only, report-only, no recursive delete |
|
||
| NFS weak coherence | immutable versions, marker/receipt, no polling authority |
|
||
| advisory/lease lock loss | lock을 correctness boundary로 사용하지 않음 |
|
||
| stale resume corruption | digest/prefix/repeatability 검증 없으면 resume off |
|
||
|
||
## 26. Test와 CI design
|
||
|
||
### 26.1 Application contract tests
|
||
|
||
- request/value/receipt invariant;
|
||
- receipt에 path/URI 없음;
|
||
- opaque reference direct lookup과 forged/unknown route 거부;
|
||
- operation ID fingerprint;
|
||
- effective policy snapshot digest와 frozen revision resume;
|
||
- 동일 completed operation에서 producer 0회;
|
||
- 하나의 accepted attempt에서 producer 최대 1회;
|
||
- global coordination capability가 없을 때 at-most-once claim 거부;
|
||
- producer source exception 보존;
|
||
- cooperative checkpoint/deadline과 blocking producer 한계 표면화;
|
||
- sink failure에서 staging abort;
|
||
- mismatch conflict;
|
||
- regeneration은 prior expected revision/attempt + new operation ID + exclusive supersession 필수;
|
||
- stale retry가 regeneration port를 우회하지 못함;
|
||
- Spring/Path/File/InputStream/vendor type leakage ArchUnit.
|
||
|
||
### 26.2 CSV unit/property tests
|
||
|
||
- RFC 4180 golden bytes;
|
||
- CRLF;
|
||
- comma/quote/CR/LF/multiline;
|
||
- UTF-8/emoji/multi-byte buffer boundary;
|
||
- malformed surrogate;
|
||
- null/empty;
|
||
- row width/type mismatch;
|
||
- blank/duplicate header;
|
||
- formula vectors와 whitespace/control prefix;
|
||
- cell/row/total byte limit;
|
||
- locale/timezone independence;
|
||
- randomized round-trip parser property.
|
||
|
||
### 26.3 Local filesystem integration
|
||
|
||
- staging/final same FileStore;
|
||
- restrictive create permission;
|
||
- reader가 partial final을 보지 않음;
|
||
- same operation concurrency;
|
||
- create conflict;
|
||
- expected-version replace;
|
||
- symlink parent/target swap;
|
||
- root/mount identity change;
|
||
- permission denied/read-only;
|
||
- ENOSPC/EDQUOT/inode exhaustion;
|
||
- write/flush/force/rename/dir-sync 단계별 fault;
|
||
- commit ACK 뒤 stat/reference-index/journal update failure와 `INDETERMINATE`;
|
||
- J-sealed/data/M-private/R-ref/J-final 각 경계 crash와 deterministic reconstruction;
|
||
- data-only atomic rename에서 public sidecar atomicity를 claim하지 않음;
|
||
- operation/reference journal atomic revision, corruption, direct recovery;
|
||
- journal과 final/manifest 모순의 truth-priority/quarantine;
|
||
- process kill 후 상태별 reconciliation;
|
||
- active writer와 reaper race;
|
||
- two reaper claim race;
|
||
- unknown file preservation;
|
||
- bounded heap with generated millions of rows.
|
||
|
||
### 26.4 SFTP real integration
|
||
|
||
실제 OpenSSH container를 production-readiness profile에서 사용한다.
|
||
|
||
- known-host success/mismatch/rotation;
|
||
- key auth failure;
|
||
- connect/auth/read/write/overall timeout;
|
||
- bounded pool wait/reject;
|
||
- large spool/upload;
|
||
- disconnect during upload;
|
||
- disconnect before/after rename ACK;
|
||
- persistent spool/control volume 유실 및 재시작 receipt 복원;
|
||
- server restart;
|
||
- extension present/absent;
|
||
- `posix-rename`, `fsync`, `statvfs`, `limits`;
|
||
- remote permission;
|
||
- private-key/known-host rotation에서 old session drain과 새 version 사용;
|
||
- secret reference 미해결, path escape, material log/heap fixture 부재;
|
||
- part/final/marker reconciliation;
|
||
- resume prefix mismatch;
|
||
- graceful shutdown.
|
||
|
||
Mock-only test로 SFTP guarantee를 증명하지 않는다.
|
||
|
||
### 26.5 NFS/multi-client
|
||
|
||
전용 Linux runner 또는 nightly profile:
|
||
|
||
- 실제 NFS server와 두 mount client;
|
||
- same-fsid rename;
|
||
- close/reopen visibility;
|
||
- directory cache delay;
|
||
- server restart;
|
||
- temporary disconnect/stale handle;
|
||
- lease expiry;
|
||
- async export profile 거부;
|
||
- mount missing/local fallback;
|
||
- two-node immutable publication;
|
||
- marker-aware consumer.
|
||
|
||
NFS service가 없으면 production-readiness job은 skip하지 않고 실패한다.
|
||
|
||
### 26.6 Maintenance/security/observability
|
||
|
||
- active stage 보존;
|
||
- stale/unknown/newer manifest 처리;
|
||
- report-only default;
|
||
- retention/legal hold/version;
|
||
- repeated cleanup idempotency;
|
||
- metric cardinality;
|
||
- path/content/credential log absence;
|
||
- trace propagation;
|
||
- readiness cache;
|
||
- liveness independence;
|
||
- provider capability mismatch startup failure;
|
||
- content producer가 checkpoint를 호출하지 않는 경우 hard-timeout을 주장하지 않음;
|
||
- control plane persistence/cluster access와 secret provider binding validation.
|
||
|
||
### 26.7 Compatibility
|
||
|
||
- manifest N/N-1 read;
|
||
- format/protection/retention/binding frozen policy N/N-1 resume;
|
||
- same profile ID/revision의 canonical digest drift 기동 실패;
|
||
- newer schema quarantine;
|
||
- rolling deployment writer/reader matrix;
|
||
- OpenSSH supported-version matrix;
|
||
- NFS server/client supported matrix;
|
||
- Linux/macOS/Windows local grammar;
|
||
- legacy `exportCsv` migration wrapper;
|
||
- public contract snapshot.
|
||
|
||
### 26.8 CI tasks
|
||
|
||
```text
|
||
:adapter:outbound:fileserver:test
|
||
:adapter:outbound:fileserver:integrationTest
|
||
:adapter:outbound:fileserver:sftpIntegrationTest
|
||
:adapter:outbound:fileserver:filesystemFailureTest
|
||
:adapter:outbound:fileserver:securityTest
|
||
:adapter:outbound:fileserver:contractTest
|
||
fileserverProductionReadiness
|
||
```
|
||
|
||
PR:
|
||
|
||
- application/CSV/local contract;
|
||
- architecture/config gating;
|
||
- deterministic OpenSSH baseline.
|
||
|
||
Nightly:
|
||
|
||
- NFS multi-client;
|
||
- failure injection;
|
||
- supported provider matrix;
|
||
- process kill/recovery;
|
||
- longer concurrency/heap soak.
|
||
|
||
## 27. Gradle, dependency, bootstrap design
|
||
|
||
### 27.1 Dependency ownership
|
||
|
||
현재 가장 가까운 `src/adapter/outbound/fileserver/CLAUDE.md`는 pure JDK, external dependency
|
||
없음, NFS/SFTP stand-in만을 허용한다. 따라서 이 문서만으로 SFTP SDK를 Gradle에 추가할 수
|
||
없다. 구현 Phase 0에서 아키텍처 승인과 함께 다음 rule drift를 먼저 갱신한다.
|
||
|
||
- local `CLAUDE.md`의 책임을 local-only demo에서 provider-based publication으로 변경;
|
||
- external `NONE` 규칙을 exact allowlist로 변경;
|
||
- broad `spring-boot-starter` 허용 문구를 실제 narrow autoconfigure 정책으로 수정;
|
||
- README의 registry SSOT와 runtime composition 설명 수정;
|
||
- architecture/Gradle test가 새 allowlist를 강제.
|
||
|
||
이 rule migration 전 SFTP dependency 추가나 runtime activation은 HARD-STOP이다.
|
||
|
||
`adapter-outbound-fileserver`:
|
||
|
||
- JDK NIO local/mounted provider;
|
||
- Spring autoconfigure;
|
||
- SLF4J API;
|
||
- Micrometer/Observation instrumentation if direct;
|
||
- `spring-integration-sftp` implementation dependency;
|
||
- Apache MINA types transitive/implementation only;
|
||
- provider test tools in test configurations.
|
||
|
||
`application-core`:
|
||
|
||
- project dependencies와 Java standard types only;
|
||
- SFTP/Spring/Micrometer 없음.
|
||
|
||
SFTP dependency는 `api`로 노출하지 않고 Boot BOM compatible version을 사용한다. 별도 broad
|
||
starter를 추가하지 않는다.
|
||
|
||
### 27.2 Bootstrap composition
|
||
|
||
안전한 explicit binding/gating과 config test가 먼저 구현된 후:
|
||
|
||
1. `modules.json`의 `app-bootstrap.allowed_dependencies`에
|
||
`adapter-outbound-fileserver` 추가;
|
||
2. `app-bootstrap/build.gradle`에 implementation dependency 추가;
|
||
3. exact module count는 19 유지;
|
||
4. no destination이면 zero bean/connection/scheduler;
|
||
5. optional adapter gating test에 fileserver 추가;
|
||
6. disabled-adapter architecture scan에 fileserver 추가;
|
||
7. env/settings/readiness contract 추가.
|
||
|
||
Classpath에 들어왔다는 이유로 local provider가 활성화되면 안 된다.
|
||
|
||
### 27.3 SDK split trigger
|
||
|
||
다음 중 하나가 실제로 발생하면 `fileserver-sftp` leaf split ADR을 작성한다.
|
||
|
||
- SFTP SDK security patch cadence가 독립적;
|
||
- local-only runtime에서 SFTP transitive dependency 제거 필요;
|
||
- provider별 deployment artifact 분리;
|
||
- 팀/릴리스 ownership 분리;
|
||
- module test/runtime 시간이 독립 관리되어야 함.
|
||
|
||
## 28. Migration
|
||
|
||
### Phase 0 — Truthful topology와 contract freeze
|
||
|
||
- 현재 Fileserver를 R1 local CSV demo로 명시;
|
||
- Fileserver `CLAUDE.md`와 README의 responsibility/dependency/registry SSOT drift 수정;
|
||
- current bootstrap 미합성 상태 명시;
|
||
- v2 contract와 error registry 승인;
|
||
- journal/reference/control-plane schema 승인;
|
||
- accepted-attempt와 global coordination guarantee 분리;
|
||
- effective policy freeze/revision/digest와 N/N-1 resume 정책 승인;
|
||
- explicit regeneration/supersession 계약 승인;
|
||
- protocol별 artifact publication/crash ordering 승인;
|
||
- secret material SPI와 lifecycle 승인;
|
||
- activation/binding/settings schema 승인;
|
||
- 기존 API deprecation 계획.
|
||
|
||
Acceptance:
|
||
|
||
- 문서와 startup diagnostics가 NFS/SFTP 구현이 있다고 주장하지 않는다.
|
||
|
||
### Phase 1 — Streaming application contract와 CSV
|
||
|
||
- `FilePublicationPort`;
|
||
- operation ID/fingerprint;
|
||
- effective policy snapshot;
|
||
- row producer/sink;
|
||
- typed cell/schema;
|
||
- CSV profiles/formula/limits;
|
||
- opaque receipt;
|
||
- legacy adapter wrapper.
|
||
|
||
Acceptance:
|
||
|
||
- 전체 rows/CSV/byte[] materialization 없이 bounded heap test 통과.
|
||
|
||
### Phase 2 — Secure local/mounted publication
|
||
|
||
- staging;
|
||
- digest/manifest;
|
||
- sealed journal과 protocol별 artifact ordering;
|
||
- file sync;
|
||
- atomic publish;
|
||
- path/permission/mount safety;
|
||
- error/outcome state;
|
||
- local reconciliation.
|
||
|
||
Acceptance:
|
||
|
||
- reader partial final 0건, 단계별 crash recovery, symlink race test 통과.
|
||
|
||
### Phase 3 — Resource/maintenance/observability
|
||
|
||
- concurrency/byte quota;
|
||
- timeout/cancel/shutdown;
|
||
- staging reaper/report;
|
||
- managed retention;
|
||
- health/metrics/traces/audit.
|
||
|
||
Acceptance:
|
||
|
||
- capacity failure가 bounded하고 unknown file을 삭제하지 않는다.
|
||
|
||
### Phase 4 — SFTP provider
|
||
|
||
- Spring Integration/Apache MINA;
|
||
- host key/secrets;
|
||
- bounded pool/timeouts;
|
||
- local spool;
|
||
- extension negotiation;
|
||
- remote unknown-outcome reconciliation;
|
||
- OpenSSH contract tests.
|
||
|
||
Acceptance:
|
||
|
||
- required extension 없음, ACK loss, server restart 경로가 silent downgrade 없이 검증된다.
|
||
|
||
### Phase 5 — NFS/HA evidence와 bootstrap
|
||
|
||
- multi-client NFS profile;
|
||
- operator attestation;
|
||
- app-bootstrap composition;
|
||
- env/readiness/architecture gates;
|
||
- sample/reference export workflow.
|
||
|
||
Acceptance:
|
||
|
||
- 선택한 deployment profile의 effective guarantee만 R2로 표시된다.
|
||
|
||
### Phase 6 — Optional read/delete와 module split review
|
||
|
||
- opaque content transfer;
|
||
- expected-version managed delete;
|
||
- provider split 조건 재평가;
|
||
- rolling compatibility matrix.
|
||
|
||
## 29. 완료 기준
|
||
|
||
Fileserver R2 완료를 주장하려면:
|
||
|
||
- application contract에 path/provider/SDK가 없음;
|
||
- bounded-memory streaming;
|
||
- stable operation ID와 fingerprint;
|
||
- effective policy revision/digest freeze와 rolling resume;
|
||
- durable operation/reference control plane과 restart receipt 복원;
|
||
- accepted-attempt와 global producer execution guarantee를 분리;
|
||
- direct final write 없음;
|
||
- no silent atomicity/durability downgrade;
|
||
- receipt에 achieved guarantee와 opaque reference;
|
||
- local/mounted/SFTP provider가 각자 capability card 제공;
|
||
- required guarantee startup validation;
|
||
- symlink/mount/permission/host-key security;
|
||
- secret reference resolution/rotation/session-drain lifecycle;
|
||
- create/replace concurrency semantics;
|
||
- unknown outcome reconciliation;
|
||
- explicit regeneration/supersession revision guard;
|
||
- commit 후 confirm/control-record 실패의 `INDETERMINATE` 처리;
|
||
- protocol별 data/manifest/reference/journal crash ordering;
|
||
- quota/backpressure/timeouts/shutdown;
|
||
- cooperative content deadline과 hard provider timeout의 분리;
|
||
- manifest/reaper/retention 안전성;
|
||
- CSV schema/formula/encoding/limit;
|
||
- classified error;
|
||
- bounded observability;
|
||
- real provider/failure/compatibility tests;
|
||
- app-bootstrap opt-in composition;
|
||
- runbook과 capacity inputs;
|
||
- core dependency purity와 exact-19 architecture gate 통과.
|
||
|
||
다음 문구는 금지한다.
|
||
|
||
- “NFS/SFTP stand-in이므로 production-ready”
|
||
- “rename이 atomic이므로 crash durable”
|
||
- “fsync를 호출했으므로 모든 remote storage에서 durable”
|
||
- “FileLock으로 distributed correctness 보장”
|
||
- “CSV quoting으로 formula injection 해결”
|
||
- “retry하면 같은 파일이 정확히 한 번 생성”
|
||
- “path normalize로 symlink 공격 해결”
|
||
- “usable space가 남았으므로 quota 확보”
|
||
|
||
## 30. 운영 runbook 요구
|
||
|
||
- mounted root/mount identity mismatch;
|
||
- disk/inode/tenant quota;
|
||
- staging backlog/orphan;
|
||
- publish indeterminate;
|
||
- checksum/manifest mismatch;
|
||
- SFTP host-key rotation;
|
||
- SFTP credential rotation;
|
||
- pool saturation/session leak;
|
||
- remote extension/version drift;
|
||
- NFS server restart/stale handle;
|
||
- async/sync export configuration;
|
||
- retention legal hold;
|
||
- manifest schema rolling upgrade;
|
||
- cleanup report-only에서 delete mode 전환;
|
||
- backup/restore 후 operation reconciliation;
|
||
- frozen policy/key revision unavailable;
|
||
- regeneration approval과 supersession conflict;
|
||
- committed data와 private control record 불일치.
|
||
|
||
## 31. Primary references
|
||
|
||
- [Java 21 `Files.move` and `ATOMIC_MOVE`](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/nio/file/Files.html#move(java.nio.file.Path,java.nio.file.Path,java.nio.file.CopyOption...))
|
||
- [Java 21 `FileChannel.force`](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/nio/channels/FileChannel.html#force(boolean))
|
||
- [Java 21 `SecureDirectoryStream`](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/nio/file/SecureDirectoryStream.html)
|
||
- [Java 21 `FileLock` platform dependencies](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/nio/channels/FileLock.html)
|
||
- [Java 21 `FileStore.getUsableSpace`](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/nio/file/FileStore.html#getUsableSpace())
|
||
- [Linux `fsync(2)`](https://man7.org/linux/man-pages/man2/fsync.2.html)
|
||
- [Linux `renameat2(2)`](https://man7.org/linux/man-pages/man2/renameat2.2.html)
|
||
- [Linux `openat2(2)`](https://man7.org/linux/man-pages/man2/openat2.2.html)
|
||
- [NFSv4.1 RFC 8881 RENAME](https://datatracker.ietf.org/doc/html/rfc8881#section-18.26.3)
|
||
- [NFSv4.1 RFC 8881 WRITE](https://datatracker.ietf.org/doc/html/rfc8881#section-18.32.3)
|
||
- [NFSv4.1 RFC 8881 COMMIT](https://datatracker.ietf.org/doc/html/rfc8881#section-18.3.3)
|
||
- [Spring Integration SFTP support](https://docs.spring.io/spring-integration/reference/sftp.html)
|
||
- [Spring Integration SFTP outbound temporary-name protocol](https://docs.spring.io/spring-integration/reference/sftp/outbound.html)
|
||
- [Spring Integration SFTP session caching](https://docs.spring.io/spring-integration/reference/sftp/session-caching.html)
|
||
- [Spring Integration `SftpSession` API](https://docs.spring.io/spring-integration/docs/current/api/org/springframework/integration/sftp/session/SftpSession.html)
|
||
- [OpenSSH SFTP extensions](https://raw.githubusercontent.com/openssh/openssh-portable/master/PROTOCOL)
|
||
- [SFTP v3 draft](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02)
|
||
- [RFC 4180 CSV](https://www.rfc-editor.org/info/rfc4180/)
|
||
- [OWASP CSV Injection](https://owasp.org/www-community/attacks/CSV_Injection)
|