Recovery: Restore¶
스냅샷 기준으로 길드 상태를 되돌리는 sub-context. Temporal Workflow 로 실행되며, Sync mode (Git reset 스타일) 으로 동작한다. 실행 전 diff 미리보기를 제공하고, 사용자 승인 후 단계별 Activity 로 Discord API 호출을 수행한다.
Sub-context position¶
- Parent — Recovery Core domain
- Location —
engine/recovery/subdomain/restore/ - Sibling sub-contexts — Snapshot (소스), Sync (복구 중 pause 대상), AntiNuke (자동 트리거)
Why this sub-context exists¶
복구는 Umbra 가 고객에게 한 핵심 약속이다. "nuke 당했을 때 되돌린다" 가 실제로 동작하지 않으면 제품 자체가 무의미하다. 따라서 Restore 는 다음 요구를 모두 만족해야 한다.
- 신뢰성 — 프로세스 장애에도 완주. 중간에 멈춘 복구는 사용자 신뢰를 깬다.
- Rate limit 대응 — Discord API 의 초당 제한을 자동 처리.
- 예측 가능성 — 사용자가 무엇이 바뀌는지 미리 알고 승인.
- 관측성 — 진행 상황을 실시간 조회 가능.
- 재시작 안전 — 특정 단계 실패 시 해당 단계부터 재실행.
이 요구는 Temporal Workflow 의 강점과 정확히 일치한다 (ADR-0014). asynq 나 자체 구현으로는 수동 상태 관리의 복잡도가 폭증한다.
Domain model¶
RestoreJob (aggregate)¶
RestoreJob
├─ id UUID v7 PK
├─ guild_id UUID → guild.guilds.id
├─ snapshot_id UUID → recovery.snapshots.id
├─ requested_by_user UUID → identity.users.id
├─ scope TEXT[] ['roles', 'channels', 'permission_overrides', 'guild_settings']
├─ options JSONB pre_restore_snapshot, etc.
├─ status TEXT 'pending' | 'running' | 'completed' | 'failed' | 'canceled'
├─ temporal_workflow_id TEXT UNIQUE
├─ preview_diff JSONB 승인 시점 diff (immutable)
├─ result_summary JSONB 완료 후 결과 (counts, errors)
├─ error_message TEXT
├─ started_at TIMESTAMPTZ
├─ completed_at TIMESTAMPTZ
├─ created_at TIMESTAMPTZ
└─ updated_at TIMESTAMPTZ
RestoreDiff (value object, preview 결과)¶
RestoreDiff
├─ roles_to_create []RoleDiffItem
├─ roles_to_update []RoleDiffItem
├─ roles_to_delete []RoleDiffItem
├─ channels_to_create []ChannelDiffItem
├─ channels_to_update []ChannelDiffItem
├─ channels_to_delete []ChannelDiffItem
├─ overrides_to_apply []OverrideDiffItem
├─ overrides_to_remove []OverrideDiffItem
├─ guild_settings_diff GuildSettingsDiff
└─ estimated_duration time.Duration (대략)
RestoreScope (enum set)¶
의존성 강제:
permission_overrides⊂ (roles∪channels)guild_settings는 독립적 선택 가능
Invariants¶
- Sync mode 만 지원 (MVP) — Add-only / Merge 는 out of scope
- Preview 필수 — Preview 없이 실행 불가 (UX 강제)
- Temporal workflow 1:1 — RestoreJob 당 하나의 workflow
- scope 는 의존성 준수 — 위반 조합은 생성 거부
- pre_restore snapshot 기본 ON — 옵션으로 off 가능하지만 경고 표시
- 동시 Restore 금지 — 한 Guild 에 status IN ('pending', 'running') 유일
- Preview diff immutable — 승인 시점 diff 를 기록, 실행 중 길드 상태 변경이 결과에 반영되어도 preview 는 수정 안 함
State machine¶
stateDiagram-v2
[*] --> Pending : Preview created & user approved
Pending --> Running : Workflow started
Running --> Completed : All phases success
Running --> Failed : Activity max retries exceeded
Running --> Canceled : User canceled / guild deleted
Pending --> Canceled : User canceled before start
Completed --> [*]
Failed --> [*]
Canceled --> [*]
Domain events¶
Published¶
| Event | Trigger | Payload | Subscribers |
|---|---|---|---|
RestoreStarted | Workflow 시작 | restore_job_id, snapshot_id, guild_id, scope | Notification, Audit |
RestoreCompleted | Workflow 성공 완료 | restore_job_id, result_summary | Notification, Audit |
RestoreFailed | Workflow 실패 | restore_job_id, error_summary, last_successful_phase | Notification, Audit |
RestoreCanceled | 사용자 또는 시스템 취소 | restore_job_id, reason | Notification, Audit |
MemberRestored | (별도 플로우) 멤버 수동 복원 | member_id, guild_id, restored_roles | Audit |
Consumed¶
| Source | Action |
|---|---|
| AntiNuke Temporal signal | (Enterprise auto-action) 자동 Restore 트리거 |
| 사용자 요청 (대시보드) | Preview 생성 → 승인 → StartRestore |
Restore Workflow (Temporal)¶
Temporal Workflow 의 의사 코드:
func RestoreWorkflow(ctx workflow.Context, input RestoreInput) (*RestoreResult, error) {
// 1. Validate
state, err := executeActivity(ValidateSnapshot, input.SnapshotID)
if err != nil { return nil, err }
// 2. Optional pre-restore snapshot
if input.Options.CreatePreRestoreSnapshot {
_, err := executeActivity(CreatePreRestoreSnapshot, input.GuildID)
if err != nil { /* warn, continue */ }
}
// 3. Pause Live Sync
err = executeActivity(PauseLiveSync, input.GuildID)
if err != nil { return nil, err }
defer executeActivity(ResumeLiveSync, input.GuildID, true) // clear buffer
// 4. Phase: Delete (역순 — overrides → channels → roles)
if contains(input.Scope, "permission_overrides") {
executeActivity(DeleteRemovedOverrides, input.SnapshotID, input.GuildID)
}
if contains(input.Scope, "channels") {
executeActivity(DeleteRemovedChannels, input.SnapshotID, input.GuildID)
}
if contains(input.Scope, "roles") {
executeActivity(DeleteRemovedRoles, input.SnapshotID, input.GuildID)
}
// 5. Phase: Create & Update (정순)
if contains(input.Scope, "roles") {
executeActivity(CreateOrUpdateRoles, input.SnapshotID, input.GuildID)
}
if contains(input.Scope, "channels") {
executeActivity(CreateOrUpdateChannels, input.SnapshotID, input.GuildID)
}
if contains(input.Scope, "permission_overrides") {
executeActivity(ApplyPermissionOverrides, input.SnapshotID, input.GuildID)
}
if contains(input.Scope, "guild_settings") {
executeActivity(UpdateGuildSettings, input.SnapshotID, input.GuildID)
}
// 6. Event
executeActivity(PublishRestoreCompleted, input.JobID)
return result, nil
}
Activity retry policy¶
각 Activity 는 Temporal retry policy:
InitialInterval: 1s
BackoffCoefficient: 2.0
MaximumInterval: 60s
MaximumAttempts: 10
NonRetryableErrors: [PermissionDenied, GuildNotFound]
Discord 429 (rate limit) 은 retryable. Discord 가 반환한 Retry-After 헤더를 Activity 가 context 에 반영.
Cancellation¶
- 사용자가 "Cancel" 클릭 → Temporal
CancelWorkflow호출 - Workflow 는 cancellation context 를 체크, 진행 중 Activity 는 완료 허용 (롤백 안 함)
defer ResumeLiveSync는 cancellation 에도 실행됨 (Sync 재개 보장)
No automatic rollback¶
복구 실행 중 실패 시 자동 롤백 하지 않는다. 이유:
- Discord API 는 원자적 multi-action 미제공
- 롤백 자체가 실패할 수 있음 (더 복잡한 상태 유발)
- Pre-restore snapshot 으로 사용자가 수동 "되돌리기" 가능
Diff calculation (Preview)¶
Input:
snapshot_payload (스냅샷 시점 상태)
current_state (Discord API 로부터 지금 fetch)
scope (복구 대상 카테고리)
For each scope category:
snap_items = snapshot_payload[category] (keyed by discord_id)
curr_items = current_state[category] (keyed by discord_id)
to_create = snap_items - curr_items (스냅샷에만 있음)
to_delete = curr_items - snap_items (현재에만 있음)
to_update = {k: snap_items[k] for k in (snap_items ∩ curr_items)
if attrs_differ(snap_items[k], curr_items[k])}
Return RestoreDiff
Preview 는 실행 시점과 실제 Restore 실행 시점 사이의 길드 변경을 반영하지 않는다 (Preview 이후 새 역할이 생겼다면 Restore 시 삭제됨). 이는 명시적 — "복구는 스냅샷 시점으로 되돌리기" 정의에 충실.
Ports¶
Inbound¶
// engine/recovery/subdomain/restore/port/service.go
type RestoreService interface {
Preview(ctx, snapshotID, scope []string) (*RestoreDiff, error)
Start(ctx, input StartRestoreInput) (*RestoreJob, error)
GetJob(ctx, jobID) (*RestoreJob, error)
ListJobs(ctx, guildID, opts) ([]*RestoreJob, error)
Cancel(ctx, jobID, reason string) error
// 별도 member 복원
RestoreMember(ctx, memberID, snapshotMemberState) error
}
type StartRestoreInput struct {
SnapshotID uuid.UUID
Scope []string
RequestedByUser uuid.UUID
CreatePreRestoreSnapshot bool
ApprovedDiff *RestoreDiff // preview 결과 재첨부 (감사)
}
Outbound¶
type RestoreJobRepository interface {
Insert(ctx, job RestoreJob) error
Update(ctx, job RestoreJob) error
GetByID(ctx, id) (*RestoreJob, error)
GetActiveByGuild(ctx, guildID) (*RestoreJob, error) // pending/running
ListByGuild(ctx, guildID, opts) ([]*RestoreJob, error)
}
type SnapshotReader interface {
GetByID(ctx, snapshotID) (*Snapshot, error)
CreatePreRestore(ctx, guildID) (*Snapshot, error) // trigger=pre_restore
}
type DiscordClient interface {
FetchCurrentState(ctx, discordGuildID) (*DiscordGuildState, error)
CreateRole(ctx, guildID, spec RoleSpec) (roleID string, err error)
UpdateRole(ctx, guildID, roleID, spec RoleSpec) error
DeleteRole(ctx, guildID, roleID) error
// channels, overrides, guild settings 동일
}
type TemporalClient interface {
StartRestoreWorkflow(ctx, input RestoreWorkflowInput) (workflowID string, err error)
CancelWorkflow(ctx, workflowID, reason string) error
QueryWorkflow(ctx, workflowID, queryName) (any, error)
}
type SyncSignaler interface {
PauseBuffer(ctx, guildID) error
ResumeBuffer(ctx, guildID, clearBuffer bool) error
}
type LicensingReader interface {
Can(ctx, guildID, feature) (bool, error)
}
type EventPublisher interface {
Publish(ctx, event DomainEvent) error
}
Adapters¶
- Persistence —
engine/recovery/subdomain/restore/adapter/persistence/ - Discord —
engine/recovery/subdomain/restore/adapter/discord/— Activity 구현 포함 - Temporal —
engine/recovery/subdomain/restore/adapter/temporal/— client + workflow + activities - Sync signaler —
engine/recovery/subdomain/restore/adapter/sync/— 같은 도메인이라 직접 호출 - Event —
engine/recovery/adapter/event/
Sync mode execution order¶
삭제 → 생성 → 업데이트 순이 맞다고 생각하기 쉽지만, Discord 의존성 때문에 다음 순서:
Delete phase (역순, 의존성 역방향)¶
- Delete removed permission overrides (채널 삭제 전 정리)
- Delete removed channels (역할 삭제 전 — 권한 참조 제거)
- Delete removed roles
Create & Update phase (정순, 의존성 정방향)¶
- Create / Update roles (먼저 생성해야 채널이 참조 가능)
- Create / Update channels
- Apply permission overrides (role/channel 존재 후)
- Update guild settings
각 phase 안에서는 여러 항목을 병렬 처리 가능 (Temporal Activity 병렬 실행), 단 rate limit 주의.
Permission model¶
- Preview — Pro 이상, Guild 의
MANAGE_GUILD - Start restore — Pro 이상, Guild 의
MANAGE_GUILD - Cancel — 본인이 시작한 job 또는 Guild owner
- Member 수동 복원 — Pro 이상, Guild 의
MANAGE_GUILD
Feature: RECOVERY_RESTORE. Multiple restore points 는 RECOVERY_RESTORE_POINTS_MULTIPLE (Enterprise).
AntiNuke auto-trigger¶
Enterprise + opt-in 시 AntiNuke 감지 → 자동 Restore:
- AntiNuke workflow 가
StartRestore직접 호출 (preview 없이) trigger = antinuke_auto기록- pre_restore snapshot 은 항상 ON (자동 복구의 안전장치)
Failure modes¶
- Activity max retries 초과 — Workflow
Failed로 종료,error_summary에 실패 phase 기록 - Discord API 전체 다운 — 모든 Activity retry 한계 도달 → Failed. 사용자에게 Discord 복구 후 재시작 안내
- Snapshot payload 손상 —
ValidateSnapshot에서 감지 → 즉시 실패 - Version mismatch — Workflow 가 지원 안 하는 payload version → 실패, manual intervention
- Cancellation 이후 Live Sync resume 실패 — Alert operator, 수동 resume
- Pre-restore snapshot 생성 실패 — 경고 후 진행 (결정은 사용자에게)
- 동시 Restore 시도 — DB 체크 + 에러 반환
Observability¶
- Temporal Web UI 로 workflow 진행 조회
RestoreJob.result_summary에 카운트 기록 (roles_created, channels_deleted, ...)- 대시보드에서 실시간 진행 (
workflow.QueryWorkflow로 현재 phase 조회)
See also¶
domain/recovery/overview.md— Recovery 전체domain/recovery/snapshot.md— 소스domain/recovery/sync.md— 복구 중 pauseflows/restore-execution.md— 전체 흐름data/recovery-schema.md— DB 스키마adr/0014-recovery-temporal-workflow.md— Temporal 채택adr/0024-restore-mode-sync.md— Sync modeguides/temporal-workflow.md— 워크플로우 작성 가이드