Skip to content

Commit 132c87e

Browse files
committed
PeerDB validation: Do not re-use MySQL peers in CDC mirrors when server_id is set.
1 parent 729225c commit 132c87e

1 file changed

Lines changed: 73 additions & 0 deletions

File tree

flow/cmd/validate_mirror.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"regexp"
99
"slices"
1010

11+
"google.golang.org/protobuf/proto"
12+
1113
"github.com/PeerDB-io/peerdb/flow/connectors"
1214
"github.com/PeerDB-io/peerdb/flow/generated/proto_conversions"
1315
"github.com/PeerDB-io/peerdb/flow/generated/protos"
@@ -102,6 +104,10 @@ func (h *FlowRequestHandler) validateCDCMirrorImpl(
102104
}
103105
}
104106

107+
if apiErr := h.checkSourcePeerReuse(ctx, connectionConfigs); apiErr != nil {
108+
return nil, apiErr
109+
}
110+
105111
srcConn, srcClose, err := connectors.GetByNameAs[connectors.MirrorSourceValidationConnector](
106112
ctx, connectionConfigs.Env, h.pool, connectionConfigs.SourceName,
107113
)
@@ -158,6 +164,73 @@ func (h *FlowRequestHandler) validateCDCMirrorImpl(
158164
return &protos.ValidateCDCMirrorResponse{}, nil
159165
}
160166

167+
// checkSourcePeerReuse rejects a CDC mirror whose MySQL source peer pins a fixed server_id while
168+
// that peer already backs another streaming CDC mirror. A fixed server_id can only be used by one
169+
// concurrent binlog connection, so sharing such a peer across mirrors makes their replicas collide
170+
// on the source. Peers without a fixed server_id fall back to a random per-connection id and are
171+
// safe to reuse.
172+
func (h *FlowRequestHandler) checkSourcePeerReuse(
173+
ctx context.Context, cfg *protos.FlowConnectionConfigsCore,
174+
) APIError {
175+
// A pinned server_id only matters for CDC; a snapshot-only mirror never opens a binlog connection.
176+
if cfg.DoInitialSnapshot && cfg.InitialSnapshotOnly {
177+
return nil
178+
}
179+
180+
peer, err := connectors.LoadPeer(ctx, h.pool, cfg.SourceName)
181+
if err != nil {
182+
return NewInternalApiError(fmt.Errorf("failed to load source peer %s: %w", cfg.SourceName, err))
183+
}
184+
mysqlCfg := peer.GetMysqlConfig()
185+
if mysqlCfg == nil || mysqlCfg.ServerId == nil {
186+
return nil
187+
}
188+
189+
// CDC flows for this source peer other than the mirror being validated.
190+
// query_string IS NULL filters out QRep flows, which do
191+
// not stream the binlog.
192+
query := `
193+
SELECT f.name name, f.config_proto config_proto
194+
FROM flows f
195+
JOIN peers p ON f.source_peer = p.id AND p.name = $1
196+
WHERE f.name != $2 AND f.config_proto IS NOT NULL AND f.query_string IS NULL
197+
`
198+
199+
rows, err := h.pool.Query(ctx, query, cfg.SourceName, cfg.FlowJobName)
200+
if err != nil {
201+
return NewInternalApiError(fmt.Errorf("failed to check source peer reuse for %s: %w", cfg.SourceName, err))
202+
}
203+
defer rows.Close()
204+
205+
for rows.Next() {
206+
var name string
207+
var configBytes []byte
208+
if err := rows.Scan(&name, &configBytes); err != nil {
209+
return NewInternalApiError(fmt.Errorf("failed to read flow row while checking peer reuse: %w", err))
210+
}
211+
var existing protos.FlowConnectionConfigsCore
212+
if err := proto.Unmarshal(configBytes, &existing); err != nil {
213+
return NewInternalApiError(fmt.Errorf("failed to unmarshal config for flow %s: %w", name, err))
214+
}
215+
// A snapshot-only mirror never streams the binlog, so it cannot collide on server_id.
216+
if existing.DoInitialSnapshot && existing.InitialSnapshotOnly {
217+
continue
218+
}
219+
return NewFailedPreconditionApiError(
220+
fmt.Errorf("source peer %q pins server_id=%d, which cannot be shared across mirrors; "+
221+
"it is already used by CDC mirror %q. Use a distinct source peer (or one without a fixed server_id) for this mirror",
222+
cfg.SourceName, mysqlCfg.GetServerId(), name),
223+
NewMirrorErrorInfo(map[string]string{
224+
common.ErrorMetadataOffendingField: "source_name",
225+
}))
226+
}
227+
if err := rows.Err(); err != nil {
228+
return NewInternalApiError(fmt.Errorf("failed to iterate flows while checking peer reuse for %s: %w", cfg.SourceName, err))
229+
}
230+
231+
return nil
232+
}
233+
161234
func (h *FlowRequestHandler) checkIfMirrorNameExists(ctx context.Context, mirrorName string) (bool, error) {
162235
var nameExists bool
163236
if err := h.pool.QueryRow(ctx, "SELECT EXISTS(SELECT * FROM flows WHERE name = $1)", mirrorName).Scan(&nameExists); err != nil {

0 commit comments

Comments
 (0)