# CRRF: Record `LogBackupID` in `CRRFSetting`

**Date:** 2026-05-20  
**Status:** Approved

---

## Background

CRRF (Cross-Region Replication based Failover) depends on PiTR log backup. The `enable_pitr` action
already confirms that a running log backup exists before the workflow proceeds, but the log backup ID
is never persisted to `CRRFSetting`.

Every time PiTR is toggled off and then on, a new log backup record is created. If CRRF was enabled
with log backup A and PiTR is later toggled, a new log backup B becomes the running one. The disable
workflow's `LoadCRRFActionContext` currently queries for *any* running log backup, which would
silently find B instead of A — the log backup that the CRRF daemon was actually created with.

---

## Goal

Persist the log backup ID into `CRRFSetting` at enable time so that the disable workflow can
reference the exact log backup used when CRRF was created, regardless of subsequent PiTR toggles.

---

## Design

### 1. Schema — `entity/crrf.go`

Add a new field to `CRRFSetting`:

```go
// LogBackupID is the ID of the log backup used to enable CRRF.
// Recorded in enable_pitr.RunAfter and cleared on disable.
LogBackupID ID `gorm:"type:bigint(20) unsigned;default:0"`
```

Placed alongside the existing `MasterBucketID` / `SecondaryBucketID` fields.

### 2. Cleanup — `entity/crrf_ext.go`

`Disable()` adds `s.LogBackupID = 0` to the field-clearing block so the field is reset when CRRF
is disabled.

### 3. Recording — `action/crrf/enable/enable_pitr.go`

`enablePitrAction` gains a new `crrfSettingRepo crrfRepo.CRRFSettingRepo` field.

`NewEnablePitrAction` constructor gains the `crrfSettingRepo` parameter.

In `RunAfter`, after confirming the running log backup:

```go
setting, bizErr := a.crrfSettingRepo.GetByClusterID(ctx, p.ClusterID)
if bizErr != nil {
    return bizErr
}
if setting.LogBackupID != logBackups[0].ID {
    setting.LogBackupID = logBackups[0].ID
    if saveErr := a.crrfSettingRepo.Save(ctx, setting); saveErr != nil {
        return saveErr
    }
}
```

The comparison `!= logBackups[0].ID` handles both the initial case (ID was 0) and the re-enable
case (PiTR was toggled and a new log backup was generated).

`dag/crrf/enable.go` passes `c.CRRFSettingRepo` as the new argument to `NewEnablePitrAction`.

### 4. Usage — `action/crrf/common/load_crrf_action_context.go`

`LoadCRRFActionContext` prefers the stored `LogBackupID` when set, falling back to querying for a
running log backup for backward compatibility with existing records:

```go
var logBackupName string
if crrfSetting.LogBackupID != 0 {
    lb, bizErr := logBackupRepo.Load(ctx, uint64(crrfSetting.LogBackupID))
    if bizErr != nil {
        return nil, bizErr
    }
    logBackupName = lb.CRName()
} else {
    logBackupName, err = FindRunningLogBackupCRName(ctx, clusterID, logBackupRepo)
    if err != nil {
        return nil, err
    }
}
```

`FindRunningLogBackupCRName` remains unchanged and is kept as the fallback.

### 5. Migration

New file `migrate/record_202605201000_add_log_backup_id_to_crrf_setting.go`:

```go
var _ = AddDataflowDBMigrateRecord(
    "202605201000",
    func(db *gorm.DB) error {
        if err := db.AutoMigrate(&entity.CRRFSetting{}); err != nil {
            return errors.Annotate(err, "auto migrate 'crrf_settings' failed")
        }
        return nil
    },
)
```

### 6. Tests — `enable_pitr_test.go`

- Add `crrfSettingRepo *repoMock.CRRFSettingRepo` to the test suite
- Update `SetupSuite` and `TearDownTest`
- Add `RunAfter` test cases:
  - `save fails` — `crrfSettingRepo.GetByClusterID` returns error
  - `log backup ID already matches — idempotent skip` — no `Save` call
  - `log backup ID differs — updates` — `Save` is called with new ID

---

## Files Changed

| File | Change |
|------|--------|
| `app/repo/entity/crrf.go` | Add `LogBackupID` field |
| `app/repo/entity/crrf_ext.go` | Clear `LogBackupID` in `Disable()` |
| `app/workflow/action/crrf/enable/enable_pitr.go` | Add `crrfSettingRepo` dep, save `LogBackupID` in `RunAfter` |
| `app/workflow/action/crrf/enable/enable_pitr_test.go` | Update test suite, add save test cases |
| `app/workflow/action/crrf/common/load_crrf_action_context.go` | Prefer stored `LogBackupID` over querying running backup |
| `app/workflow/dag/crrf/enable.go` | Pass `crrfSettingRepo` to `NewEnablePitrAction` |
| `migrate/record_202605201000_add_log_backup_id_to_crrf_setting.go` | New migration |

---

## Non-Goals

- No changes to the disable DAG actions beyond `Disable()` clearing the field
- No changes to the CRRF API layer
- No changes to `FindRunningLogBackupCRName` (kept as fallback)
