package registry import ( "context" "database/sql" "encoding/json" "errors" ) // Recovery handovers remain pending until the successor's prelaunch is durable. // The existing isolated registry transport owns these writes, including at root. func (r *Registry) BeginRecoveryHandover(ctx context.Context, id, body string) error { if r.remote != nil { _, err := r.remote.call(ctx, remoteRequest{Method: "BeginRecoveryHandover", Id: id, Journal: body}, nil) return err } if id == "" || body == "" { return ErrClaimOwnership } if err := r.recoveryJournal(ctx); err != nil { return err } tx, err := r.beginClaimTransaction(ctx, true) if err != nil { return err } defer tx.Rollback() var saved string err = tx.QueryRowContext(ctx, `SELECT body FROM recovery_handovers WHERE id=?`, id).Scan(&saved) if err == nil { if saved != body { return ErrClaimOwnership } return nil } if !errors.Is(err, sql.ErrNoRows) { return err } if _, err = tx.ExecContext(ctx, `INSERT INTO recovery_handovers(id,body,complete) VALUES(?,?,0)`, id, body); err != nil { return err } return tx.Commit() } func (r *Registry) PendingRecoveryHandover(ctx context.Context, id string) (string, error) { if r.remote != nil { out, err := r.remote.call(ctx, remoteRequest{Method: "PendingRecoveryHandover", Id: id}, nil) return out.Text, err } if err := r.recoveryJournal(ctx); err != nil { return "", err } var body string err := r.sql.QueryRowContext(ctx, `SELECT body FROM recovery_handovers WHERE id=? AND complete=0`, id).Scan(&body) if errors.Is(err, sql.ErrNoRows) { return "", nil } return body, err } func (r *Registry) CompleteRecoveryHandover(ctx context.Context, id, body string) error { if r.remote != nil { _, err := r.remote.call(ctx, remoteRequest{Method: "CompleteRecoveryHandover", Id: id, Journal: body}, nil) return err } if err := r.recoveryJournal(ctx); err != nil { return err } result, err := r.sql.ExecContext(ctx, `UPDATE recovery_handovers SET complete=1 WHERE id=? AND body=?`, id, body) if err != nil { return err } n, err := result.RowsAffected() if err != nil { return err } if n != 1 { return ErrClaimOwnership } return nil } func (r *Registry) recoveryJournal(ctx context.Context) error { _, err := r.sql.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS recovery_handovers(id TEXT PRIMARY KEY,body TEXT NOT NULL,complete INTEGER NOT NULL)`) return err } func (r *Registry) PendingRecoveryHandovers(ctx context.Context) (map[string]string, error) { if r.remote != nil { out, err := r.remote.call(ctx, remoteRequest{Method: "PendingRecoveryHandovers"}, nil) if err != nil { return nil, err } var bodies map[string]string err = json.Unmarshal([]byte(out.Text), &bodies) return bodies, err } if err := r.recoveryJournal(ctx); err != nil { return nil, err } rows, err := r.sql.QueryContext(ctx, `SELECT id,body FROM recovery_handovers WHERE complete=0`) if err != nil { return nil, err } defer rows.Close() bodies := map[string]string{} for rows.Next() { var id, body string if err := rows.Scan(&id, &body); err != nil { return nil, err } bodies[id] = body } return bodies, rows.Err() } func (r *Registry) ReviseRecoveryHandover(ctx context.Context, id, prior, body string) error { if r.remote != nil { _, err := r.remote.call(ctx, remoteRequest{Method: "ReviseRecoveryHandover", Id: id, Expected: prior, Journal: body}, nil) return err } if id == "" || prior == "" || body == "" { return ErrClaimOwnership } if err := r.recoveryJournal(ctx); err != nil { return err } tx, err := r.beginClaimTransaction(ctx, true) if err != nil { return err } defer tx.Rollback() result, err := tx.ExecContext(ctx, `UPDATE recovery_handovers SET body=? WHERE id=? AND body=? AND complete=0`, body, id, prior) if err != nil { return err } n, err := result.RowsAffected() if err != nil { return err } if n != 1 { return ErrClaimOwnership } return tx.Commit() }