Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions cmd/instance/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ func main() {
primaryEndpoint := os.Getenv("PRIMARY_ENDPOINT")
endpoint := instanceEndpoint(podName, cluster, int32(shardOrdinal), namespace)
restartedPrimaryAsStandby, err := prepareRestartedPrimaryAsStandby(
dataDir, primaryEndpoint, binDir, podName, memberCount, logger,
dataDir, primaryEndpoint, endpoint, binDir, podName, memberCount, logger,
)
if err != nil {
logger.Error("Failed to prepare restarted former primary as standby",
Expand Down Expand Up @@ -477,7 +477,7 @@ func parsePodOrdinalOrDie(podName string) int {
}

func prepareRestartedPrimaryAsStandby(
dataDir, primaryEndpoint, binDir, applicationName string,
dataDir, primaryEndpoint, selfEndpoint, binDir, applicationName string,
memberCount int,
logger *slog.Logger,
) (bool, error) {
Expand All @@ -487,6 +487,7 @@ func prepareRestartedPrimaryAsStandby(
prepared, err := supervise.PrepareRestartedPrimaryAsStandbyWithRewind(context.Background(), supervise.RejoinOptions{
DataDir: dataDir,
PrimaryEndpoint: primaryEndpoint,
SelfEndpoint: selfEndpoint,
ApplicationName: applicationName,
BinDir: binDir,
BasebackupOnRewindFailure: true,
Expand Down
1 change: 1 addition & 0 deletions cmd/instance/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ func TestPrepareRestartedPrimaryAsStandby_UsesCurrentPrimaryEndpointForAnyOrdina
prepared, err := prepareRestartedPrimaryAsStandby(
dir,
"demo-shard-0-2.demo-shard-0-headless.ns1.svc.cluster.local:5432",
"demo-shard-0-0.demo-shard-0-headless.ns1.svc.cluster.local:5432", // selfEndpoint ≠ primary → standby 화 동작 유지
"",
"demo-shard-0-0",
2,
Expand Down
18 changes: 18 additions & 0 deletions internal/instance/supervise/standby.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ type RejoinOptions struct {
BinDir string
Runner CommandRunner
BasebackupOnRewindFailure bool
// SelfEndpoint 는 본 인스턴스 자신의 endpoint(host:port). PrimaryEndpoint 가
// SelfEndpoint 와 같으면 operator 가 *본 노드를 primary 로 지정*한 것이므로
// standby 화(pg_rewind to self → connection refused 무한 crashloop)를 skip 한다.
// INC-2026-06-23: 죽은 옛 primary 가 PRIMARY_ENDPOINT 에 남아 standby 화 시도 →
// DNS 실패 crashloop 했고, 수동 marker 제거로만 복구되던 라이브 사고의 코드 fix.
SelfEndpoint string
}

// IsStandby 는 dataDir 안에 standby.signal 파일이 존재하는지 검사한다.
Expand Down Expand Up @@ -138,6 +144,18 @@ func PrepareRestartedPrimaryAsStandbyWithRewind(ctx context.Context, opts Rejoin
}
return false, fmt.Errorf("stat %s: %w", marker, err)
}
// INC-2026-06-23 guard: operator 가 PRIMARY_ENDPOINT 를 *본 노드 자신* 으로
// 지정했다면 본 노드가 primary 다. marker 가 남아있어도 standby 화(pg_rewind to
// self → "connection refused" 무한 crashloop)를 하면 안 된다. marker 를 정리하고
// primary 로 부팅하도록 skip. (죽은 옛 primary endpoint 가 PRIMARY_ENDPOINT 에
// stale 하게 남아 standby 화 시도 → DNS 실패 crashloop 한 라이브 사고의 코드 fix —
// 그때 수동 marker 제거로만 복구됐다.)
if opts.SelfEndpoint != "" && opts.PrimaryEndpoint == opts.SelfEndpoint {
if err := os.Remove(marker); err != nil && !errors.Is(err, os.ErrNotExist) {
return false, fmt.Errorf("remove self-primary marker %s: %w", marker, err)
}
return false, nil
}
if opts.PrimaryEndpoint == "" {
return false, errors.New("primaryEndpoint must not be empty")
}
Expand Down
30 changes: 30 additions & 0 deletions internal/instance/supervise/standby_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,36 @@ func TestPrepareRestartedPrimaryAsStandby_NoMarker(t *testing.T) {
}
}

// TestPrepareRestartedPrimaryAsStandby_SelfPrimarySkips 는 INC-2026-06-23 회귀 가드:
// PrimaryEndpoint 가 SelfEndpoint 와 같으면(operator 가 본 노드를 primary 로 지정)
// marker 가 있어도 standby 화를 skip 하고 marker 를 제거해야 한다 (self pg_rewind →
// connection refused 무한 crashloop 차단). 본 guard 제거 시 본 테스트 실패.
func TestPrepareRestartedPrimaryAsStandby_SelfPrimarySkips(t *testing.T) {
dir := t.TempDir()
marker := filepath.Join(dir, RestartPrimaryAsStandbyMarker)
if err := os.WriteFile(marker, []byte("1"), 0o600); err != nil {
t.Fatalf("write marker: %v", err)
}
self := "postgres-prod-shard-0-0.svc.cluster.local:5432"
prepared, err := PrepareRestartedPrimaryAsStandbyWithRewind(context.Background(), RejoinOptions{
DataDir: dir,
PrimaryEndpoint: self, // operator 가 본 노드를 primary 로 지정
SelfEndpoint: self,
})
if err != nil {
t.Fatalf("PrepareRestartedPrimaryAsStandbyWithRewind: %v", err)
}
if prepared {
t.Fatal("prepared = true; self-primary 는 standby 화 skip(false) 해야")
}
if _, err := os.Stat(marker); !os.IsNotExist(err) {
t.Errorf("marker 가 self-primary skip 후 잔존(stat err=%v) — 제거 기대", err)
}
if _, err := os.Stat(filepath.Join(dir, "standby.signal")); !os.IsNotExist(err) {
t.Error("standby.signal 이 self-primary 인데 생성됨 — primary 부팅 차단")
}
}

func TestPrepareRestartedPrimaryAsStandby_ConfiguresStandby(t *testing.T) {
dir := t.TempDir()
marker := filepath.Join(dir, RestartPrimaryAsStandbyMarker)
Expand Down