|
8 | 8 | "regexp" |
9 | 9 | "slices" |
10 | 10 |
|
| 11 | + "google.golang.org/protobuf/proto" |
| 12 | + |
11 | 13 | "github.com/PeerDB-io/peerdb/flow/connectors" |
12 | 14 | "github.com/PeerDB-io/peerdb/flow/generated/proto_conversions" |
13 | 15 | "github.com/PeerDB-io/peerdb/flow/generated/protos" |
@@ -102,6 +104,10 @@ func (h *FlowRequestHandler) validateCDCMirrorImpl( |
102 | 104 | } |
103 | 105 | } |
104 | 106 |
|
| 107 | + if apiErr := h.checkSourcePeerReuse(ctx, connectionConfigs); apiErr != nil { |
| 108 | + return nil, apiErr |
| 109 | + } |
| 110 | + |
105 | 111 | srcConn, srcClose, err := connectors.GetByNameAs[connectors.MirrorSourceValidationConnector]( |
106 | 112 | ctx, connectionConfigs.Env, h.pool, connectionConfigs.SourceName, |
107 | 113 | ) |
@@ -158,6 +164,73 @@ func (h *FlowRequestHandler) validateCDCMirrorImpl( |
158 | 164 | return &protos.ValidateCDCMirrorResponse{}, nil |
159 | 165 | } |
160 | 166 |
|
| 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 | + |
161 | 234 | func (h *FlowRequestHandler) checkIfMirrorNameExists(ctx context.Context, mirrorName string) (bool, error) { |
162 | 235 | var nameExists bool |
163 | 236 | if err := h.pool.QueryRow(ctx, "SELECT EXISTS(SELECT * FROM flows WHERE name = $1)", mirrorName).Scan(&nameExists); err != nil { |
|
0 commit comments