165 lines
7.2 KiB
Text
165 lines
7.2 KiB
Text
<!-- task=cli_terminal_cycle plan=2 tag=REVIEW_REVIEW_API -->
|
|
|
|
# Lifecycle Start Rollback 보강
|
|
|
|
## 이 파일을 읽는 구현 에이전트에게
|
|
|
|
각 항목의 체크리스트를 완료하면 즉시 체크 표시한다. 중간/최종 검증 명령을 실제로 실행하고 출력을 `CODE_REVIEW.md`의 검증 결과 섹션에 붙여 넣는다. 계획과 다르게 구현해야 하는 부분이 생기면 이유를 `CODE_REVIEW.md`의 `계획 대비 변경 사항`에 남기고, 모든 구현 항목의 실제 동작과 수동 검증 결과를 기록한다.
|
|
|
|
## 배경
|
|
|
|
후속 구현은 persistent CLI의 response boundary와 process failure 전파를 바로잡았다. 하지만 lifecycle start가 여러 단계 중간에 실패할 때 이미 시작된 adapter/process가 남을 수 있다. node startup 실패 시 프로세스가 남지 않는 것을 보장하려면 registry, CLI adapter 내부, bootstrap 실패 경로가 모두 rollback을 수행해야 한다.
|
|
|
|
---
|
|
|
|
### [REVIEW_REVIEW_API-1] Partial lifecycle start 실패 시 started resources 정리
|
|
|
|
#### 문제
|
|
|
|
`apps/node/internal/adapters/registry.go:66-74`:
|
|
```go
|
|
func (r *Registry) Start(ctx context.Context) error {
|
|
for _, name := range r.order {
|
|
lc, ok := r.adapters[name].(LifecycleAdapter)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if err := lc.Start(ctx); err != nil {
|
|
return fmt.Errorf("adapter %q start: %w", name, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
```
|
|
|
|
앞선 lifecycle adapter가 성공한 뒤 뒤의 adapter가 실패하면 이미 시작된 adapter의 `Stop`이 호출되지 않는다.
|
|
|
|
`apps/node/internal/adapters/cli/cli.go:77-86`:
|
|
```go
|
|
func (c *CLI) Start(ctx context.Context) error {
|
|
for name, profile := range c.profiles {
|
|
if !profile.Persistent {
|
|
continue
|
|
}
|
|
sess, err := startProfileSession(ctx, name, profile, c.logger)
|
|
if err != nil {
|
|
return fmt.Errorf("cli adapter: start profile %q: %w", name, err)
|
|
}
|
|
c.sessions[name] = sess
|
|
```
|
|
|
|
하나의 `cli` adapter 안에서도 여러 persistent profile 중 앞선 profile이 성공하고 다음 profile이 실패하면 이미 시작한 process가 남는다.
|
|
|
|
`apps/node/internal/bootstrap/module.go:61-65`:
|
|
```go
|
|
if err := reg.Start(ctx); err != nil {
|
|
_ = result.Session.Close()
|
|
_ = st.Close()
|
|
return fmt.Errorf("bootstrap: start adapters: %w", err)
|
|
}
|
|
```
|
|
|
|
`reg.Start` 실패 경로에서 session/store는 닫지만 registry lifecycle rollback을 추가로 호출하지 않는다. `Registry.Start`와 `CLI.Start`가 rollback을 하더라도 bootstrap에서 `reg.Stop`을 한 번 더 호출하는 것은 idempotent cleanup 관점에서 안전해야 한다.
|
|
|
|
#### 해결 방법
|
|
|
|
`Registry.Start`는 started lifecycle 목록을 추적하고 실패 시 역순으로 `Stop`을 호출한다.
|
|
|
|
```go
|
|
func (r *Registry) Start(ctx context.Context) error {
|
|
started := make([]string, 0, len(r.order))
|
|
for _, name := range r.order {
|
|
lc, ok := r.adapters[name].(LifecycleAdapter)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if err := lc.Start(ctx); err != nil {
|
|
for i := len(started) - 1; i >= 0; i-- {
|
|
if startedLC, ok := r.adapters[started[i]].(LifecycleAdapter); ok {
|
|
_ = startedLC.Stop(context.Background())
|
|
}
|
|
}
|
|
return fmt.Errorf("adapter %q start: %w", name, err)
|
|
}
|
|
started = append(started, name)
|
|
}
|
|
return nil
|
|
}
|
|
```
|
|
|
|
`CLI.Start`는 profile start 실패 시 이미 `c.sessions`에 등록된 session들을 `Stop(context.Background())`으로 정리하고, 정리 후 `c.sessions`를 빈 map으로 되돌린다.
|
|
|
|
```go
|
|
if err != nil {
|
|
_ = c.Stop(context.Background())
|
|
c.sessions = make(map[string]*profileSession)
|
|
return fmt.Errorf("cli adapter: start profile %q: %w", name, err)
|
|
}
|
|
```
|
|
|
|
`CLI.Stop`은 여러 번 호출돼도 안전하도록 nil/empty sessions를 허용하고, close/kill 이후 세션 map을 비운다. 이미 종료된 process의 close/kill error는 기존처럼 firstErr로 반환하되 cleanup은 끝까지 진행한다.
|
|
|
|
`bootstrap.Module`의 `reg.Start` 실패 경로에는 `_ = reg.Stop(context.Background())`를 추가한다.
|
|
|
|
```go
|
|
if err := reg.Start(ctx); err != nil {
|
|
_ = reg.Stop(context.Background())
|
|
_ = result.Session.Close()
|
|
_ = st.Close()
|
|
return fmt.Errorf("bootstrap: start adapters: %w", err)
|
|
}
|
|
```
|
|
|
|
#### 수정 파일 및 체크리스트
|
|
|
|
- [x] `apps/node/internal/adapters/registry.go` - `Start` 실패 시 이미 시작된 lifecycle adapter를 역순으로 stop
|
|
- [x] `apps/node/internal/adapters/registry_test.go` - second lifecycle adapter start 실패 시 first adapter stop 호출 테스트 추가
|
|
- [x] `apps/node/internal/adapters/cli/cli.go` - `Start` 실패 시 이미 시작된 persistent sessions 정리
|
|
- [x] `apps/node/internal/adapters/cli/cli.go` - `Stop`을 반복 호출해도 안전하게 sessions map 정리
|
|
- [x] `apps/node/internal/adapters/cli/cli_test.go` - 여러 persistent profile 중 뒤 profile 실패 시 앞 profile process cleanup 테스트 추가
|
|
- [x] `apps/node/internal/bootstrap/module.go` - `reg.Start` 실패 경로에서 `reg.Stop(context.Background())` 호출
|
|
|
|
#### 테스트 작성
|
|
|
|
작성:
|
|
|
|
- `apps/node/internal/adapters/registry_test.go` - `TestRegistryLifecycle_StartFailureStopsStartedAdapters`
|
|
- first lifecycle adapter는 start 성공, second lifecycle adapter는 start error를 반환하게 만든다.
|
|
- `reg.Start(ctx)`가 error를 반환하고, log에 `start:first`, `start:second`, `stop:first` 순서가 남는지 검증한다.
|
|
- `apps/node/internal/adapters/cli/cli_test.go` - `TestCLIStartCleansUpStartedProfilesWhenLaterProfileFails`
|
|
- `runtime.GOOS == "windows"`면 skip한다.
|
|
- 첫 profile은 `sh -c 'touch "$MARKER"; while true; do sleep 1; done'`, env에 `MARKER=<marker path>`를 둔다.
|
|
- 두 번째 persistent profile은 command가 존재하지 않거나 즉시 startup error가 나게 만든다.
|
|
- `Start`가 error를 반환한 뒤, `Stop`이 반복 호출되어도 panic/error 없이 끝나고, 첫 process가 종료되었음을 marker 외 추가 pid/check 파일 또는 `pgrep` 없이 검증 가능한 방식으로 확인한다. 간단히 첫 profile을 trap 기반으로 `EXIT_MARKER`를 남기게 하고 cleanup 후 exit marker가 생성됐는지 확인한다.
|
|
|
|
스킵:
|
|
|
|
- bootstrap의 `reg.Start` 실패 rollback은 registry/CLI 단위 테스트와 compile로 커버한다. mock edge + process 기반 integration test를 추가하면 복잡도가 커지므로 이번 항목에서는 추가하지 않는다.
|
|
|
|
#### 중간 검증
|
|
|
|
```bash
|
|
go test ./apps/node/internal/adapters ./apps/node/internal/adapters/cli
|
|
```
|
|
|
|
기대 결과: registry rollback 테스트와 CLI partial profile rollback 테스트가 통과한다.
|
|
|
|
## 수정 파일 요약
|
|
|
|
| 파일 경로 | 작업 항목 |
|
|
|-----------|-----------|
|
|
| `apps/node/internal/adapters/registry.go` | [REVIEW_REVIEW_API-1] |
|
|
| `apps/node/internal/adapters/registry_test.go` | [REVIEW_REVIEW_API-1] |
|
|
| `apps/node/internal/adapters/cli/cli.go` | [REVIEW_REVIEW_API-1] |
|
|
| `apps/node/internal/adapters/cli/cli_test.go` | [REVIEW_REVIEW_API-1] |
|
|
| `apps/node/internal/bootstrap/module.go` | [REVIEW_REVIEW_API-1] |
|
|
|
|
## 최종 검증
|
|
|
|
```bash
|
|
go test ./apps/node/internal/adapters ./apps/node/internal/adapters/cli
|
|
go test ./apps/node/internal/bootstrap ./apps/node/...
|
|
go test ./...
|
|
```
|
|
|
|
기대 결과: 모든 테스트가 통과하고, partial lifecycle start 실패 시 이미 시작된 adapter/process가 정리된다.
|