From aac54ff988e59beec343b491ee19a1ce1a5ddae6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 13:00:34 +0000 Subject: [PATCH 1/4] Initial plan From 43aaac6dc6cccad16c6c584f07d906ad77019a3f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 13:06:39 +0000 Subject: [PATCH 2/4] fix physical backup failure with unsupported read concern Explicitly set local read concern for $backupCursor and $backupCursorExtend aggregate operations. These stages only support local read concern, but the client may be configured with majority, causing backup failures. Co-authored-by: igroene <9805809+igroene@users.noreply.github.com> --- pbm/backup/physical.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pbm/backup/physical.go b/pbm/backup/physical.go index 8ed1559b7..9c3a2a4c2 100644 --- a/pbm/backup/physical.go +++ b/pbm/backup/physical.go @@ -16,6 +16,8 @@ import ( "go.mongodb.org/mongo-driver/bson/bsontype" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "go.mongodb.org/mongo-driver/mongo/readconcern" "go.mongodb.org/mongo-driver/x/bsonx/bsoncore" "github.com/percona/percona-backup-mongodb/pbm/compress" @@ -91,7 +93,7 @@ func (bc *BackupCursor) create(ctx context.Context, retry int) (*mongo.Cursor, e } } - cur, err := bc.conn.Database("admin").Aggregate(ctx, mongo.Pipeline{ + cur, err := bc.conn.Database("admin", options.Database().SetReadConcern(readconcern.Local())).Aggregate(ctx, mongo.Pipeline{ {{"$backupCursor", opts}}, }) if err != nil { @@ -189,7 +191,7 @@ func (bc *BackupCursor) Data(ctx context.Context) (_ *BackupCursorData, err erro func (bc *BackupCursor) Journals(upto primitive.Timestamp) ([]File, error) { ctx := context.Background() - cur, err := bc.conn.Database("admin").Aggregate(ctx, + cur, err := bc.conn.Database("admin", options.Database().SetReadConcern(readconcern.Local())).Aggregate(ctx, mongo.Pipeline{ {{"$backupCursorExtend", bson.D{{"backupId", bc.id}, {"timestamp", upto}}}}, }) From 1a16adb9ca461e8c284aac6ccabca91091c05f38 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 13:25:43 +0000 Subject: [PATCH 3/4] allow physical backup for MongoDB Enterprise edition Add enterprise edition detection to MongoVersion and update FullPhysicalBackup() and IncrementalPhysicalBackup() to also allow physical backups when MongoDB Enterprise is in use, since it supports $backupCursor just like PSMDB. Co-authored-by: igroene <9805809+igroene@users.noreply.github.com> --- pbm/version/version.go | 28 ++++-- pbm/version/version_test.go | 172 ++++++++++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+), 8 deletions(-) diff --git a/pbm/version/version.go b/pbm/version/version.go index 06bd16a5d..7faf2bcf1 100644 --- a/pbm/version/version.go +++ b/pbm/version/version.go @@ -176,9 +176,20 @@ var BreakingChangesMap = map[defs.BackupType][]string{ } type MongoVersion struct { - PSMDBVersion string `bson:"psmdbVersion,omitempty"` - VersionString string `bson:"version"` - Version []int `bson:"versionArray"` + PSMDBVersion string `bson:"psmdbVersion,omitempty"` + VersionString string `bson:"version"` + Version []int `bson:"versionArray"` + Modules []string `bson:"modules,omitempty"` +} + +// IsEnterprise returns true if the MongoDB instance is running Enterprise edition. +func (v MongoVersion) IsEnterprise() bool { + for _, m := range v.Modules { + if m == "enterprise" { + return true + } + } + return false } func (v MongoVersion) String() string { @@ -274,7 +285,7 @@ func (f FeatureSupport) PBMSupport() error { func (f FeatureSupport) FullPhysicalBackup() bool { // PSMDB 4.2.15, 4.4.6 v := MongoVersion(f) - if v.PSMDBVersion == "" { + if v.PSMDBVersion == "" && !v.IsEnterprise() { return false } @@ -293,7 +304,7 @@ func (f FeatureSupport) FullPhysicalBackup() bool { func (f FeatureSupport) IncrementalPhysicalBackup() bool { // PSMDB 4.2.24, 4.4.18, 5.0.14, 6.0.3 v := MongoVersion(f) - if v.PSMDBVersion == "" { + if v.PSMDBVersion == "" && !v.IsEnterprise() { return false } @@ -318,17 +329,18 @@ func (f FeatureSupport) BackupType(t defs.BackupType) error { case defs.PhysicalBackup: if !f.FullPhysicalBackup() { return errors.New("full physical backup works since " + - "Percona Server for MongoDB 4.2.15, 4.4.6") + "Percona Server for MongoDB 4.2.15, 4.4.6 or MongoDB Enterprise 4.2.15, 4.4.6") } case defs.IncrementalBackup: if !f.IncrementalPhysicalBackup() { return errors.New("incremental physical backup works since " + - "Percona Server for MongoDB 4.2.24, 4.4.18, 5.0.14, 6.0.3") + "Percona Server for MongoDB 4.2.24, 4.4.18, 5.0.14, 6.0.3 or " + + "MongoDB Enterprise 4.2.24, 4.4.18, 5.0.14, 6.0.3") } case defs.ExternalBackup: if !f.FullPhysicalBackup() { return errors.New("external backup works since " + - "Percona Server for MongoDB 4.2.15, 4.4.6") + "Percona Server for MongoDB 4.2.15, 4.4.6 or MongoDB Enterprise 4.2.15, 4.4.6") } } diff --git a/pbm/version/version_test.go b/pbm/version/version_test.go index 2148a8a37..e2e7b88db 100644 --- a/pbm/version/version_test.go +++ b/pbm/version/version_test.go @@ -100,6 +100,178 @@ func TestCompatibility(t *testing.T) { } } +func TestFullPhysicalBackup(t *testing.T) { + cases := []struct { + name string + ver MongoVersion + want bool + }{ + // PSMDB cases + { + name: "psmdb 4.2.14 (too old)", + ver: MongoVersion{PSMDBVersion: "4.2.14-14", Version: []int{4, 2, 14}}, + want: false, + }, + { + name: "psmdb 4.2.15", + ver: MongoVersion{PSMDBVersion: "4.2.15-15", Version: []int{4, 2, 15}}, + want: true, + }, + { + name: "psmdb 4.4.5 (too old)", + ver: MongoVersion{PSMDBVersion: "4.4.5-5", Version: []int{4, 4, 5}}, + want: false, + }, + { + name: "psmdb 4.4.6", + ver: MongoVersion{PSMDBVersion: "4.4.6-6", Version: []int{4, 4, 6}}, + want: true, + }, + { + name: "psmdb 7.0.0", + ver: MongoVersion{PSMDBVersion: "7.0.0-1", Version: []int{7, 0, 0}}, + want: true, + }, + // Plain MongoDB (no enterprise) + { + name: "mongodb community 7.0.0", + ver: MongoVersion{Version: []int{7, 0, 0}}, + want: false, + }, + // MongoDB Enterprise cases + { + name: "mongodb enterprise 4.2.14 (too old)", + ver: MongoVersion{Modules: []string{"enterprise"}, Version: []int{4, 2, 14}}, + want: false, + }, + { + name: "mongodb enterprise 4.2.15", + ver: MongoVersion{Modules: []string{"enterprise"}, Version: []int{4, 2, 15}}, + want: true, + }, + { + name: "mongodb enterprise 4.4.5 (too old)", + ver: MongoVersion{Modules: []string{"enterprise"}, Version: []int{4, 4, 5}}, + want: false, + }, + { + name: "mongodb enterprise 4.4.6", + ver: MongoVersion{Modules: []string{"enterprise"}, Version: []int{4, 4, 6}}, + want: true, + }, + { + name: "mongodb enterprise 7.0.0", + ver: MongoVersion{Modules: []string{"enterprise"}, Version: []int{7, 0, 0}}, + want: true, + }, + { + name: "mongodb enterprise 8.0.0", + ver: MongoVersion{Modules: []string{"enterprise"}, Version: []int{8, 0, 0}}, + want: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := FeatureSupport(tc.ver).FullPhysicalBackup() + if got != tc.want { + t.Errorf("FullPhysicalBackup() = %v, want %v", got, tc.want) + } + }) + } +} + +func TestIncrementalPhysicalBackup(t *testing.T) { + cases := []struct { + name string + ver MongoVersion + want bool + }{ + // PSMDB cases + { + name: "psmdb 4.2.23 (too old)", + ver: MongoVersion{PSMDBVersion: "4.2.23-23", Version: []int{4, 2, 23}}, + want: false, + }, + { + name: "psmdb 4.2.24", + ver: MongoVersion{PSMDBVersion: "4.2.24-24", Version: []int{4, 2, 24}}, + want: true, + }, + { + name: "psmdb 7.0.0", + ver: MongoVersion{PSMDBVersion: "7.0.0-1", Version: []int{7, 0, 0}}, + want: true, + }, + // Plain MongoDB (no enterprise) + { + name: "mongodb community 7.0.0", + ver: MongoVersion{Version: []int{7, 0, 0}}, + want: false, + }, + // MongoDB Enterprise cases + { + name: "mongodb enterprise 4.2.23 (too old)", + ver: MongoVersion{Modules: []string{"enterprise"}, Version: []int{4, 2, 23}}, + want: false, + }, + { + name: "mongodb enterprise 4.2.24", + ver: MongoVersion{Modules: []string{"enterprise"}, Version: []int{4, 2, 24}}, + want: true, + }, + { + name: "mongodb enterprise 4.4.17 (too old)", + ver: MongoVersion{Modules: []string{"enterprise"}, Version: []int{4, 4, 17}}, + want: false, + }, + { + name: "mongodb enterprise 4.4.18", + ver: MongoVersion{Modules: []string{"enterprise"}, Version: []int{4, 4, 18}}, + want: true, + }, + { + name: "mongodb enterprise 5.0.13 (too old)", + ver: MongoVersion{Modules: []string{"enterprise"}, Version: []int{5, 0, 13}}, + want: false, + }, + { + name: "mongodb enterprise 5.0.14", + ver: MongoVersion{Modules: []string{"enterprise"}, Version: []int{5, 0, 14}}, + want: true, + }, + { + name: "mongodb enterprise 6.0.2 (too old)", + ver: MongoVersion{Modules: []string{"enterprise"}, Version: []int{6, 0, 2}}, + want: false, + }, + { + name: "mongodb enterprise 6.0.3", + ver: MongoVersion{Modules: []string{"enterprise"}, Version: []int{6, 0, 3}}, + want: true, + }, + { + name: "mongodb enterprise 7.0.0", + ver: MongoVersion{Modules: []string{"enterprise"}, Version: []int{7, 0, 0}}, + want: true, + }, + { + name: "mongodb enterprise 8.0.0", + ver: MongoVersion{Modules: []string{"enterprise"}, Version: []int{8, 0, 0}}, + want: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := FeatureSupport(tc.ver).IncrementalPhysicalBackup() + if got != tc.want { + t.Errorf("IncrementalPhysicalBackup() = %v, want %v", got, tc.want) + } + }) + } +} + func TestHasPhysicalFilesMetadata(t *testing.T) { cases := map[string]bool{ "": false, From 38786c4b967643c0a8a92fb322b362b5cfea9f30 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 15:16:18 +0000 Subject: [PATCH 4/4] fix empty nomination list for MongoDB Enterprise edition AgentStat was missing the Enterprise edition flag, causing MongoVersion() to reconstruct without Modules, making IsEnterprise() return false during candidate filtering. Add IsEnterprise bool field to AgentStat, populate it in HbStatus(), and use it in MongoVersion() to set Modules. Co-authored-by: igroene <9805809+igroene@users.noreply.github.com> --- cmd/pbm-agent/agent.go | 11 ++++++----- pbm/topo/agent.go | 7 +++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/cmd/pbm-agent/agent.go b/cmd/pbm-agent/agent.go index 9e2781cae..d8ce6b038 100644 --- a/cmd/pbm-agent/agent.go +++ b/cmd/pbm-agent/agent.go @@ -276,11 +276,12 @@ func (a *Agent) HbStatus(ctx context.Context) { } hb := topo.AgentStat{ - Node: a.brief.Me, - RS: a.brief.SetName, - AgentVer: version.Current().Version, - MongoVer: nodeVersion.VersionString, - PerconaVer: nodeVersion.PSMDBVersion, + Node: a.brief.Me, + RS: a.brief.SetName, + AgentVer: version.Current().Version, + MongoVer: nodeVersion.VersionString, + PerconaVer: nodeVersion.PSMDBVersion, + IsEnterprise: nodeVersion.IsEnterprise(), } updateAgentStat(ctx, a, l, true, &hb) diff --git a/pbm/topo/agent.go b/pbm/topo/agent.go index 07a738a17..6bac870c7 100644 --- a/pbm/topo/agent.go +++ b/pbm/topo/agent.go @@ -59,6 +59,9 @@ type AgentStat struct { // Empty for non-PSMDB (e.i MongoDB CE). PerconaVer string `bson:"pv,omitempty"` + // IsEnterprise is set when the node runs MongoDB Enterprise edition. + IsEnterprise bool `bson:"ent,omitempty"` + // PBMStatus is the agent status. PBMStatus SubsysStatus `bson:"pbms"` @@ -114,6 +117,10 @@ func (s *AgentStat) MongoVersion() version.MongoVersion { VersionString: s.MongoVer, } + if s.IsEnterprise { + v.Modules = []string{"enterprise"} + } + vs := semver.Canonical("v" + s.MongoVer)[1:] vs = strings.SplitN(vs, "-", 2)[0] for _, a := range strings.Split(vs, ".")[:3] {