-
Notifications
You must be signed in to change notification settings - Fork 198
support filtering out deletion operation types for mongo #4585
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ import ( | |
| "errors" | ||
| "fmt" | ||
| "log/slog" | ||
| "slices" | ||
| "strings" | ||
| "sync/atomic" | ||
| "time" | ||
|
|
@@ -17,6 +18,7 @@ import ( | |
| "go.opentelemetry.io/otel/trace" | ||
|
|
||
| "github.com/PeerDB-io/peerdb/flow/generated/protos" | ||
| "github.com/PeerDB-io/peerdb/flow/internal" | ||
| "github.com/PeerDB-io/peerdb/flow/model" | ||
| "github.com/PeerDB-io/peerdb/flow/otel_metrics" | ||
| "github.com/PeerDB-io/peerdb/flow/pkg/common" | ||
|
|
@@ -25,6 +27,24 @@ import ( | |
| "github.com/PeerDB-io/peerdb/flow/shared/types" | ||
| ) | ||
|
|
||
| type operationType string | ||
|
|
||
| const ( | ||
| operationTypeInsert operationType = "insert" | ||
| operationTypeUpdate operationType = "update" | ||
| operationTypeReplace operationType = "replace" | ||
| operationTypeDelete operationType = "delete" | ||
| ) | ||
|
|
||
| func parseOperationType(s string) (operationType, bool) { | ||
| switch op := operationType(s); op { | ||
| case operationTypeInsert, operationTypeUpdate, operationTypeReplace, operationTypeDelete: | ||
| return op, true | ||
| default: | ||
| return "", false | ||
| } | ||
| } | ||
|
|
||
| type Namespace struct { | ||
| Db string `bson:"db"` | ||
| Coll string `bson:"coll"` | ||
|
|
@@ -117,7 +137,7 @@ func (c *MongoConnector) SetupReplication( | |
| SetComment("PeerDB changeStream"). | ||
| SetFullDocument(options.UpdateLookup) | ||
|
|
||
| pipeline, err := createPipeline(nil) | ||
| pipeline, err := createPipeline(nil, nil) | ||
| if err != nil { | ||
| return model.SetupReplicationResult{}, fmt.Errorf("failed to create changestream pipeline: %w", err) | ||
| } | ||
|
|
@@ -201,7 +221,7 @@ func (c *MongoConnector) PullRecords( | |
| changeStreamOpts.SetResumeAfter(resumeToken) | ||
| } | ||
|
|
||
| pipeline, err := createPipeline(req.TableNameMapping) | ||
| pipeline, err := createPipeline(req.TableNameMapping, c.excludedOps) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
@@ -444,8 +464,8 @@ func (c *MongoConnector) PullRecords( | |
| } | ||
|
|
||
| items := model.NewMongoRecordItems(2) | ||
| switch changeEvent.OperationType { | ||
| case "insert": | ||
| switch operationType(changeEvent.OperationType) { | ||
| case operationTypeInsert: | ||
| if err := addRecordItems(changeEvent.DocumentKey, changeEvent.FullDocument, &items, sourceTableName); err != nil { | ||
| return fmt.Errorf("failed to process document: %w", err) | ||
| } | ||
|
|
@@ -458,7 +478,7 @@ func (c *MongoConnector) PullRecords( | |
| }); err != nil { | ||
| return fmt.Errorf("failed to add insert record: %w", err) | ||
| } | ||
| case "update", "replace": | ||
| case operationTypeUpdate, operationTypeReplace: | ||
| if err := addRecordItems(changeEvent.DocumentKey, changeEvent.FullDocument, &items, sourceTableName); err != nil { | ||
| return fmt.Errorf("failed to process document: %w", err) | ||
| } | ||
|
|
@@ -471,7 +491,7 @@ func (c *MongoConnector) PullRecords( | |
| }); err != nil { | ||
| return fmt.Errorf("failed to add update record: %w", err) | ||
| } | ||
| case "delete": | ||
| case operationTypeDelete: | ||
| if err := addRecordItems(changeEvent.DocumentKey, changeEvent.FullDocument, &items, sourceTableName); err != nil { | ||
| return fmt.Errorf("failed to process document: %w", err) | ||
| } | ||
|
|
@@ -496,7 +516,7 @@ func (c *MongoConnector) PullRecords( | |
| return nil | ||
| } | ||
|
|
||
| func createPipeline(tableNameMapping map[string]model.NameAndExclude) (mongo.Pipeline, error) { | ||
| func createPipeline(tableNameMapping map[string]model.NameAndExclude, excludedOps []operationType) (mongo.Pipeline, error) { | ||
| pipeline := mongo.Pipeline{} | ||
|
|
||
| // filter out events from tables that are not in the mapping | ||
|
|
@@ -528,6 +548,13 @@ func createPipeline(tableNameMapping map[string]model.NameAndExclude) (mongo.Pip | |
| }}}) | ||
| } | ||
|
|
||
| // filter out excluded operation types | ||
| if len(excludedOps) > 0 { | ||
| pipeline = append(pipeline, bson.D{{Key: "$match", Value: bson.D{ | ||
| {Key: "operationType", Value: bson.D{{Key: "$nin", Value: excludedOps}}}, | ||
| }}}) | ||
| } | ||
|
|
||
| // Mongo recommends using '$project' first to reduce change event size, and only use | ||
| // '$changeStreamSplitLargeEvent' in the pipeline if still necessary. Given the document | ||
| // themselves have a 16MB limit, project required fields for now for code simplicity. | ||
|
|
@@ -569,7 +596,28 @@ func (c *MongoConnector) FinishExport(any) error { | |
| return nil | ||
| } | ||
|
|
||
| func (c *MongoConnector) SetupReplConn(context.Context, map[string]string) error { | ||
| func (c *MongoConnector) SetupReplConn(ctx context.Context, env map[string]string) error { | ||
| // Unlike Postgres, MongoDB doesn't need a dedicated replication connection: | ||
| // change streams are cursors served by the connector's pooled client. | ||
| // Since SetupReplConn is called once per SyncFlow activity, resolving | ||
| // dynamic config here avoids per-batch catalog reads. | ||
|
Comment on lines
+600
to
+603
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also evaluated populating this in the mongo connector itself similar to Postgres, but this is a more logical place for it despite the I think we have a few other cdc-related settings that are being fetched every batch today (when we expect them to be fetched every SyncFlow and only update on pause/resume), so they behave more like |
||
| excludedOps, err := internal.PeerDBMongoDBExcludedOperationTypes(ctx, env) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to get excluded operation types: %w", err) | ||
| } | ||
| c.excludedOps = make([]operationType, 0, len(excludedOps)) | ||
| for _, op := range excludedOps { | ||
| if parsed, ok := parseOperationType(op); ok { | ||
| if !slices.Contains(c.excludedOps, parsed) { | ||
| c.excludedOps = append(c.excludedOps, parsed) | ||
| } | ||
| } else { | ||
| c.logger.Warn("ignoring invalid operation type in exclusion list", slog.String("operationType", op)) | ||
| } | ||
| } | ||
| if len(c.excludedOps) > 0 { | ||
| c.logger.Info("excluding operation types from replication", slog.Any("operationTypes", c.excludedOps)) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -509,6 +509,15 @@ var DynamicSettings = [...]*protos.DynamicSetting{ | |
| ApplyMode: protos.DynconfApplyMode_APPLY_MODE_AFTER_RESUME, | ||
| TargetForSetting: protos.DynconfTarget_ALL, | ||
| }, | ||
| { | ||
| Name: "PEERDB_MONGODB_EXCLUDED_OPERATION_TYPES", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since the set of options is bound and known, should this be a multi-select field instead to reduce the risk of typos? Not sure if we have building blocks for this in the UI components, so a comma-separated list is fine to start; just wanted to flag.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For global settings tab, this isn't super trivial to change; but for the pipe-level configs we can build something more user-friendly and translate it to env var on our end |
||
| Description: "Comma-separated list of MongoDB change stream operation types to exclude from CDC " + | ||
| "(allowed values: insert, update, replace, delete)", | ||
| DefaultValue: "", | ||
| ValueType: protos.DynconfValueType_STRING, | ||
| ApplyMode: protos.DynconfApplyMode_APPLY_MODE_AFTER_RESUME, | ||
| TargetForSetting: protos.DynconfTarget_ALL, | ||
| }, | ||
| } | ||
|
|
||
| var DynamicIndex = func() map[string]int { | ||
|
|
@@ -901,3 +910,17 @@ func PeerDBOffloadPartitionRanges(ctx context.Context, env map[string]string) (b | |
| func PeerDBPGAutomatedSchemaDump(ctx context.Context, env map[string]string) (bool, error) { | ||
| return dynamicConfBool(ctx, env, "PEERDB_PG_AUTOMATED_SCHEMA_DUMP") | ||
| } | ||
|
|
||
| func PeerDBMongoDBExcludedOperationTypes(ctx context.Context, env map[string]string) ([]string, error) { | ||
| value, err := dynLookup(ctx, env, "PEERDB_MONGODB_EXCLUDED_OPERATION_TYPES") | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| var ops []string | ||
| for op := range strings.SplitSeq(value, ",") { | ||
| if op := strings.ToLower(strings.TrimSpace(op)); op != "" { | ||
| ops = append(ops, op) | ||
| } | ||
| } | ||
| return ops, nil | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.