From 3398f87dc8a16a801bdd940932be177c9b4efc9e Mon Sep 17 00:00:00 2001 From: Jefferson Rodrigues Date: Tue, 19 May 2026 23:47:15 -0300 Subject: [PATCH 1/3] feat(mongo): migrate from mongo-driver v1 to v2 In-place upgrade of the MongoDB driver across commons/mongo, commons/outbox/mongo, commons/tenant-manager/core, and commons/tenant-manager/mongo. Module path stays at /v5; public function signatures are unchanged. Consumer services that hold the returned *mongo.Database/*mongo.Client will need to update their own MongoDB calls to the v2 driver (notably: Collection.Distinct now returns *DistinctResult, mongo.Connect no longer pings, primitive types live under bson, WithTransaction callback receives context.Context instead of mongo.SessionContext). Driver pinned at go.mongodb.org/mongo-driver/v2 v2.6.0. Unit (4534) and integration (142) suites pass. Lint clean. X-Lerian-Ref: 0x1 --- CHANGELOG.md | 5 +++++ commons/mongo/mongo.go | 10 +++++----- commons/mongo/mongo_integration_test.go | 4 ++-- commons/mongo/mongo_test.go | 6 +++--- commons/outbox/mongo/claim.go | 6 +++--- commons/outbox/mongo/coverage_boost_test.go | 7 +++---- commons/outbox/mongo/document.go | 9 ++++----- commons/outbox/mongo/extra_test.go | 2 +- commons/outbox/mongo/helpers.go | 2 +- commons/outbox/mongo/indexes.go | 6 +++--- commons/outbox/mongo/pure_functions_test.go | 2 +- commons/outbox/mongo/repository.go | 15 +++++++++++---- .../outbox/mongo/repository_integration_test.go | 12 ++++++------ commons/outbox/mongo/repository_test.go | 2 +- commons/tenant-manager/core/context.go | 2 +- commons/tenant-manager/core/context_test.go | 2 +- commons/tenant-manager/mongo/manager.go | 11 ++++++++--- commons/tenant-manager/mongo/manager_test.go | 15 ++++++++++----- go.mod | 5 +---- go.sum | 6 ------ 20 files changed, 70 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74708706..68a40bca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Lib-commons Changelog +## [5.3.0] + +- **Changes:** + - **mongo**: Migrated from `go.mongodb.org/mongo-driver` v1 to `go.mongodb.org/mongo-driver/v2` (v2.6.0) in-place across `commons/mongo/`, `commons/outbox/mongo/`, `commons/tenant-manager/core/`, and `commons/tenant-manager/mongo/`. Public function signatures are unchanged, but the concrete `*mongo.Database`/`*mongo.Client` types now resolve to v2 — consumers updating to v5.3.0 must update their own MongoDB query code (notably `options.UpdateOne()`/`options.Find()`/`options.Index()` return `*XxxOptionsBuilder`, `primitive.ObjectID` → `bson.ObjectID`, `primitive.DateTime` → `bson.DateTime`, `mongo.Connect` no longer takes `context.Context`, `Collection.Distinct` returns `*DistinctResult` instead of `([]any, error)`, and `WithTransaction` callbacks receive `context.Context` instead of `mongo.SessionContext`). + ## [5.2.0](https://github.com/LerianStudio/lib-commons/releases/tag/v5.2.0) - **Features:** diff --git a/commons/mongo/mongo.go b/commons/mongo/mongo.go index ee49e6a1..94940059 100644 --- a/commons/mongo/mongo.go +++ b/commons/mongo/mongo.go @@ -22,9 +22,9 @@ import ( "github.com/LerianStudio/lib-observability/log" "github.com/LerianStudio/lib-observability/metrics" libOpentelemetry "github.com/LerianStudio/lib-observability/tracing" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" + "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo" + "go.mongodb.org/mongo-driver/v2/mongo/options" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" ) @@ -143,8 +143,8 @@ type clientDeps struct { func defaultDeps() clientDeps { return clientDeps{ - connect: func(ctx context.Context, clientOptions *options.ClientOptions) (*mongo.Client, error) { - return mongo.Connect(ctx, clientOptions) + connect: func(_ context.Context, clientOptions *options.ClientOptions) (*mongo.Client, error) { + return mongo.Connect(clientOptions) }, ping: func(ctx context.Context, client *mongo.Client) error { return client.Ping(ctx, nil) diff --git a/commons/mongo/mongo_integration_test.go b/commons/mongo/mongo_integration_test.go index a615c3c0..b0cea177 100644 --- a/commons/mongo/mongo_integration_test.go +++ b/commons/mongo/mongo_integration_test.go @@ -14,8 +14,8 @@ import ( "github.com/testcontainers/testcontainers-go" tcmongo "github.com/testcontainers/testcontainers-go/modules/mongodb" "github.com/testcontainers/testcontainers-go/wait" - "go.mongodb.org/mongo-driver/bson" - mongodriver "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/v2/bson" + mongodriver "go.mongodb.org/mongo-driver/v2/mongo" ) const ( diff --git a/commons/mongo/mongo_test.go b/commons/mongo/mongo_test.go index 8be3d8f0..07913061 100644 --- a/commons/mongo/mongo_test.go +++ b/commons/mongo/mongo_test.go @@ -23,9 +23,9 @@ import ( "github.com/LerianStudio/lib-observability/log" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" + "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo" + "go.mongodb.org/mongo-driver/v2/mongo/options" ) // --------------------------------------------------------------------------- diff --git a/commons/outbox/mongo/claim.go b/commons/outbox/mongo/claim.go index 809434fd..dd8b16c1 100644 --- a/commons/outbox/mongo/claim.go +++ b/commons/outbox/mongo/claim.go @@ -8,9 +8,9 @@ import ( "github.com/LerianStudio/lib-commons/v5/commons/outbox" libOpentelemetry "github.com/LerianStudio/lib-observability/tracing" "github.com/google/uuid" - "go.mongodb.org/mongo-driver/bson" - mongodriver "go.mongodb.org/mongo-driver/mongo" - mongooptions "go.mongodb.org/mongo-driver/mongo/options" + "go.mongodb.org/mongo-driver/v2/bson" + mongodriver "go.mongodb.org/mongo-driver/v2/mongo" + mongooptions "go.mongodb.org/mongo-driver/v2/mongo/options" ) func (repo *Repository) claimPending(ctx context.Context, limit int, eventType string, spanName string) ([]*outbox.OutboxEvent, error) { diff --git a/commons/outbox/mongo/coverage_boost_test.go b/commons/outbox/mongo/coverage_boost_test.go index e0099de2..0e57e4ea 100644 --- a/commons/outbox/mongo/coverage_boost_test.go +++ b/commons/outbox/mongo/coverage_boost_test.go @@ -9,8 +9,7 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/v2/bson" ) // ------------------------------------------------------------------- @@ -131,7 +130,7 @@ func TestTimeField_PrimitiveDateTime(t *testing.T) { t.Parallel() now := time.Now().UTC().Truncate(time.Millisecond) - raw := bson.M{"ts": primitive.NewDateTimeFromTime(now)} + raw := bson.M{"ts": bson.NewDateTimeFromTime(now)} val, err := timeField(raw, "ts") require.NoError(t, err) assert.WithinDuration(t, now, val, time.Millisecond) @@ -183,7 +182,7 @@ func TestOptionalTimeField_PrimitiveDateTime(t *testing.T) { t.Parallel() now := time.Now().UTC().Truncate(time.Millisecond) - raw := bson.M{"ts": primitive.NewDateTimeFromTime(now)} + raw := bson.M{"ts": bson.NewDateTimeFromTime(now)} val, err := optionalTimeField(raw, "ts") require.NoError(t, err) require.NotNil(t, val) diff --git a/commons/outbox/mongo/document.go b/commons/outbox/mongo/document.go index f9f9dd17..93c77819 100644 --- a/commons/outbox/mongo/document.go +++ b/commons/outbox/mongo/document.go @@ -9,9 +9,8 @@ import ( "github.com/LerianStudio/lib-commons/v5/commons/outbox" "github.com/google/uuid" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/bson/primitive" - mongodriver "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/v2/bson" + mongodriver "go.mongodb.org/mongo-driver/v2/mongo" ) func (doc document) toBSON(tenantField string) bson.M { @@ -418,7 +417,7 @@ func timeField(raw bson.M, key string) (time.Time, error) { switch typed := value.(type) { case time.Time: return typed, nil - case primitive.DateTime: + case bson.DateTime: return typed.Time(), nil default: return time.Time{}, fmt.Errorf("field %q must be time", key) @@ -434,7 +433,7 @@ func optionalTimeField(raw bson.M, key string) (*time.Time, error) { switch typed := value.(type) { case time.Time: return &typed, nil - case primitive.DateTime: + case bson.DateTime: timeValue := typed.Time() return &timeValue, nil default: diff --git a/commons/outbox/mongo/extra_test.go b/commons/outbox/mongo/extra_test.go index 50c85245..b491cfec 100644 --- a/commons/outbox/mongo/extra_test.go +++ b/commons/outbox/mongo/extra_test.go @@ -8,7 +8,7 @@ import ( libMongo "github.com/LerianStudio/lib-commons/v5/commons/mongo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/v2/bson" ) // TestWithLogger covers the WithLogger option. diff --git a/commons/outbox/mongo/helpers.go b/commons/outbox/mongo/helpers.go index fff79ace..32b8e528 100644 --- a/commons/outbox/mongo/helpers.go +++ b/commons/outbox/mongo/helpers.go @@ -4,7 +4,7 @@ import ( "maps" "regexp" - "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/v2/bson" ) var mongoIdentifierPattern = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`) diff --git a/commons/outbox/mongo/indexes.go b/commons/outbox/mongo/indexes.go index 043982b9..4bb492fe 100644 --- a/commons/outbox/mongo/indexes.go +++ b/commons/outbox/mongo/indexes.go @@ -1,9 +1,9 @@ package mongo import ( - "go.mongodb.org/mongo-driver/bson" - mongodriver "go.mongodb.org/mongo-driver/mongo" - mongooptions "go.mongodb.org/mongo-driver/mongo/options" + "go.mongodb.org/mongo-driver/v2/bson" + mongodriver "go.mongodb.org/mongo-driver/v2/mongo" + mongooptions "go.mongodb.org/mongo-driver/v2/mongo/options" ) func buildIndexes(tenantField string) []mongodriver.IndexModel { diff --git a/commons/outbox/mongo/pure_functions_test.go b/commons/outbox/mongo/pure_functions_test.go index 88775ce3..c80b86f3 100644 --- a/commons/outbox/mongo/pure_functions_test.go +++ b/commons/outbox/mongo/pure_functions_test.go @@ -10,7 +10,7 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/v2/bson" ) // --------------------------------------------------------------------------- diff --git a/commons/outbox/mongo/repository.go b/commons/outbox/mongo/repository.go index 23716ae5..d3e2e8e0 100644 --- a/commons/outbox/mongo/repository.go +++ b/commons/outbox/mongo/repository.go @@ -18,8 +18,8 @@ import ( libLog "github.com/LerianStudio/lib-observability/log" libOpentelemetry "github.com/LerianStudio/lib-observability/tracing" "github.com/google/uuid" - "go.mongodb.org/mongo-driver/bson" - mongodriver "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/v2/bson" + mongodriver "go.mongodb.org/mongo-driver/v2/mongo" "go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace/noop" ) @@ -345,16 +345,23 @@ func (repo *Repository) ListTenants(ctx context.Context) ([]string, error) { return nil, err } - values, err := collection.Distinct(ctx, repo.tenantField, bson.M{ + distinctResult := collection.Distinct(ctx, repo.tenantField, bson.M{ mongoFieldStatus: bson.M{"$in": bson.A{outbox.OutboxStatusPending, outbox.OutboxStatusFailed, outbox.OutboxStatusProcessing}}, repo.tenantField: bson.M{"$ne": defaultScopeTenantID}, }) - if err != nil { + if err := distinctResult.Err(); err != nil { libOpentelemetry.HandleSpanError(span, "failed to list tenants", err) return nil, fmt.Errorf("listing tenants: %w", err) } + var values []any + if err := distinctResult.Decode(&values); err != nil { + libOpentelemetry.HandleSpanError(span, "failed to decode tenants", err) + + return nil, fmt.Errorf("listing tenants: %w", err) + } + tenants := make([]string, 0, len(values)) for _, value := range values { tenantID, ok := value.(string) diff --git a/commons/outbox/mongo/repository_integration_test.go b/commons/outbox/mongo/repository_integration_test.go index 0e1e7c9a..239045a2 100644 --- a/commons/outbox/mongo/repository_integration_test.go +++ b/commons/outbox/mongo/repository_integration_test.go @@ -20,9 +20,9 @@ import ( "github.com/testcontainers/testcontainers-go" tcmongo "github.com/testcontainers/testcontainers-go/modules/mongodb" "github.com/testcontainers/testcontainers-go/wait" - "go.mongodb.org/mongo-driver/bson" - mongodriver "go.mongodb.org/mongo-driver/mongo" - mongooptions "go.mongodb.org/mongo-driver/mongo/options" + "go.mongodb.org/mongo-driver/v2/bson" + mongodriver "go.mongodb.org/mongo-driver/v2/mongo" + mongooptions "go.mongodb.org/mongo-driver/v2/mongo/options" "go.opentelemetry.io/otel/trace/noop" ) @@ -115,7 +115,7 @@ func waitForMongoPrimary(t *testing.T, uri string) { for time.Now().Before(deadline) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - client, err := mongodriver.Connect(ctx, mongooptions.Client().ApplyURI(uri)) + client, err := mongodriver.Connect(mongooptions.Client().ApplyURI(uri)) if err == nil { err = client.Ping(ctx, nil) } @@ -294,7 +294,7 @@ func TestIntegration_Repository_CreateJoinsMongoTransactionContext(t *testing.T) tenantCtx := outbox.ContextWithTenantID(suite.ctx, "tenant-a") var committedID uuid.UUID - _, err = commitSession.WithTransaction(tenantCtx, func(sessionCtx mongodriver.SessionContext) (any, error) { + _, err = commitSession.WithTransaction(tenantCtx, func(sessionCtx context.Context) (any, error) { event, eventErr := outbox.NewOutboxEvent(sessionCtx, "payment.tx.commit", uuid.New(), []byte(`{"ok":true}`)) if eventErr != nil { return nil, eventErr @@ -322,7 +322,7 @@ func TestIntegration_Repository_CreateJoinsMongoTransactionContext(t *testing.T) var rolledBackID uuid.UUID rollbackErr := errors.New("rollback sentinel") - _, err = rollbackSession.WithTransaction(tenantCtx, func(sessionCtx mongodriver.SessionContext) (any, error) { + _, err = rollbackSession.WithTransaction(tenantCtx, func(sessionCtx context.Context) (any, error) { event, eventErr := outbox.NewOutboxEvent(sessionCtx, "payment.tx.rollback", uuid.New(), []byte(`{"ok":true}`)) if eventErr != nil { return nil, eventErr diff --git a/commons/outbox/mongo/repository_test.go b/commons/outbox/mongo/repository_test.go index 2363138d..13725240 100644 --- a/commons/outbox/mongo/repository_test.go +++ b/commons/outbox/mongo/repository_test.go @@ -14,7 +14,7 @@ import ( tmcore "github.com/LerianStudio/lib-commons/v5/commons/tenant-manager/core" "github.com/google/uuid" "github.com/stretchr/testify/require" - "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/v2/bson" ) func TestNewRepository_Validation(t *testing.T) { diff --git a/commons/tenant-manager/core/context.go b/commons/tenant-manager/core/context.go index 2d87bfc6..59390634 100644 --- a/commons/tenant-manager/core/context.go +++ b/commons/tenant-manager/core/context.go @@ -4,7 +4,7 @@ import ( "context" "github.com/bxcodec/dbresolver/v2" - "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/v2/mongo" ) // nonNilContext returns ctx if non-nil, otherwise context.Background(). diff --git a/commons/tenant-manager/core/context_test.go b/commons/tenant-manager/core/context_test.go index 949447ae..fc7b14c4 100644 --- a/commons/tenant-manager/core/context_test.go +++ b/commons/tenant-manager/core/context_test.go @@ -11,7 +11,7 @@ import ( "github.com/bxcodec/dbresolver/v2" "github.com/stretchr/testify/assert" - "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/v2/mongo" ) func TestContextWithTenantID(t *testing.T) { diff --git a/commons/tenant-manager/mongo/manager.go b/commons/tenant-manager/mongo/manager.go index ba5ee4ad..3af33524 100644 --- a/commons/tenant-manager/mongo/manager.go +++ b/commons/tenant-manager/mongo/manager.go @@ -23,8 +23,8 @@ import ( observability "github.com/LerianStudio/lib-observability" "github.com/LerianStudio/lib-observability/log" libOpentelemetry "github.com/LerianStudio/lib-observability/tracing" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" + "go.mongodb.org/mongo-driver/v2/mongo" + "go.mongodb.org/mongo-driver/v2/mongo/options" "go.opentelemetry.io/otel/trace" ) @@ -153,11 +153,16 @@ func (c *MongoConnection) connectWithTLS(ctx context.Context) error { clientOptions.SetTLSConfig(c.tlsConfig) - mongoClient, err := mongo.Connect(ctx, clientOptions) + mongoClient, err := mongo.Connect(clientOptions) if err != nil { return fmt.Errorf("mongo connect with TLS failed: %w", err) } + if err := mongoClient.Ping(ctx, nil); err != nil { + _ = mongoClient.Disconnect(ctx) + return fmt.Errorf("mongo connect with TLS failed: %w", err) + } + c.DB = mongoClient return nil diff --git a/commons/tenant-manager/mongo/manager_test.go b/commons/tenant-manager/mongo/manager_test.go index 72d9b455..b8c46871 100644 --- a/commons/tenant-manager/mongo/manager_test.go +++ b/commons/tenant-manager/mongo/manager_test.go @@ -29,9 +29,9 @@ import ( "github.com/LerianStudio/lib-commons/v5/commons/tenant-manager/internal/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" + "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo" + "go.mongodb.org/mongo-driver/v2/mongo/options" "go.uber.org/goleak" ) @@ -41,6 +41,11 @@ func TestMain(m *testing.M) { goleak.IgnoreTopFunction("internal/poll.runtime_pollWait"), goleak.IgnoreTopFunction("net/http.(*persistConn).writeLoop"), goleak.IgnoreTopFunction("net/http.(*persistConn).readLoop"), + // v2 driver spawns topology goroutines that wind down asynchronously + // after Disconnect; ignore the known shutdown/monitor stacks. + goleak.IgnoreAnyFunction("go.mongodb.org/mongo-driver/v2/x/mongo/driver/topology.(*Server).Disconnect"), + goleak.IgnoreAnyFunction("go.mongodb.org/mongo-driver/v2/x/mongo/driver/topology.(*rttMonitor).runHellos"), + goleak.IgnoreAnyFunction("go.mongodb.org/mongo-driver/v2/x/mongo/driver/topology.(*contextDoneListener).Listen"), ) } @@ -66,9 +71,9 @@ func startFakeMongoServer(t *testing.T) (*mongo.Client, func()) { addr := ln.Addr().String() - mongoClient, err := mongo.Connect(ctx, options.Client(). + mongoClient, err := mongo.Connect(options.Client(). ApplyURI(fmt.Sprintf("mongodb://%s/?directConnection=true", addr)). - SetServerSelectionTimeout(2*time.Second)) + SetServerSelectionTimeout(2 * time.Second)) require.NoError(t, err) require.NoError(t, mongoClient.Ping(ctx, nil)) diff --git a/go.mod b/go.mod index 7dbbf1bd..6f7160e3 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0 github.com/testcontainers/testcontainers-go/modules/rabbitmq v0.42.0 github.com/testcontainers/testcontainers-go/modules/redis v0.42.0 - go.mongodb.org/mongo-driver v1.17.9 + go.mongodb.org/mongo-driver/v2 v2.6.0 go.opentelemetry.io/otel v1.43.0 go.opentelemetry.io/otel/metric v1.43.0 go.opentelemetry.io/otel/sdk v1.43.0 @@ -77,7 +77,6 @@ require ( github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/golang/snappy v1.0.0 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect github.com/googleapis/gax-go/v2 v2.22.0 // indirect @@ -105,7 +104,6 @@ require ( github.com/moby/sys/user v0.4.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect github.com/moby/term v0.5.2 // indirect - github.com/montanaflynn/stats v0.9.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect @@ -122,7 +120,6 @@ require ( github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/yuin/gopher-lua v1.1.2 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect - go.mongodb.org/mongo-driver/v2 v2.6.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect diff --git a/go.sum b/go.sum index 56bbf30a..6c52f34f 100644 --- a/go.sum +++ b/go.sum @@ -118,8 +118,6 @@ github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7g github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= -github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomodule/redigo v1.9.3 h1:dNPSXeXv6HCq2jdyWfjgmhBdqnR6PRO3m/G05nvpPC8= github.com/gomodule/redigo v1.9.3/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -193,8 +191,6 @@ github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= -github.com/montanaflynn/stats v0.9.0 h1:tsBJ0RXwph9BmAuFoCmqGv6e8xa0MENQ8m0ptKq29mQ= -github.com/montanaflynn/stats v0.9.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -274,8 +270,6 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= -go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU= -go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= go.mongodb.org/mongo-driver/v2 v2.6.0 h1:b9sJOYrkmt4l8bY43ZenFBcPlhYIjaOfYHLtbB/5qi8= go.mongodb.org/mongo-driver/v2 v2.6.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= From 6d219376775dec5b39f902c5511434d9a63e4da8 Mon Sep 17 00:00:00 2001 From: Jefferson Rodrigues Date: Tue, 19 May 2026 23:56:02 -0300 Subject: [PATCH 2/3] fix(mongo): address CodeRabbit feedback on PR review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - manager.go: use bounded context for Disconnect on Ping failure. Reusing the request context risked carrying an already-canceled ctx into Disconnect, which prevents the driver from cleanly tearing down topology/monitoring goroutines. - CHANGELOG: split the v5.3.0 migration notes into a scannable bulleted list and document that v2 mongo.Connect no longer pings the server implicitly — callers that relied on Connect-time reachability validation must add an explicit client.Ping(ctx, nil) after connecting. X-Lerian-Ref: 0x1 --- CHANGELOG.md | 7 ++++++- commons/tenant-manager/mongo/manager.go | 4 +++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68a40bca..9a5979f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,12 @@ ## [5.3.0] - **Changes:** - - **mongo**: Migrated from `go.mongodb.org/mongo-driver` v1 to `go.mongodb.org/mongo-driver/v2` (v2.6.0) in-place across `commons/mongo/`, `commons/outbox/mongo/`, `commons/tenant-manager/core/`, and `commons/tenant-manager/mongo/`. Public function signatures are unchanged, but the concrete `*mongo.Database`/`*mongo.Client` types now resolve to v2 — consumers updating to v5.3.0 must update their own MongoDB query code (notably `options.UpdateOne()`/`options.Find()`/`options.Index()` return `*XxxOptionsBuilder`, `primitive.ObjectID` → `bson.ObjectID`, `primitive.DateTime` → `bson.DateTime`, `mongo.Connect` no longer takes `context.Context`, `Collection.Distinct` returns `*DistinctResult` instead of `([]any, error)`, and `WithTransaction` callbacks receive `context.Context` instead of `mongo.SessionContext`). + - **mongo**: Migrated from `go.mongodb.org/mongo-driver` v1 to `go.mongodb.org/mongo-driver/v2` (v2.6.0) in-place across `commons/mongo/`, `commons/outbox/mongo/`, `commons/tenant-manager/core/`, and `commons/tenant-manager/mongo/`. Public function signatures are unchanged, but the concrete `*mongo.Database`/`*mongo.Client` types now resolve to v2. Consumers updating to v5.3.0 must adapt their own MongoDB query code for the following v2 API changes: + - `options.UpdateOne()`, `options.Find()`, and `options.Index()` now return builder types (`*XxxOptionsBuilder`). Fluent `.SetX()` chains are unchanged. + - `primitive.ObjectID` → `bson.ObjectID`, `primitive.DateTime` → `bson.DateTime`, `primitive.NewObjectID()` → `bson.NewObjectID()` (the `primitive` package was merged into `bson` in v2). + - `mongo.Connect` no longer accepts a `context.Context` parameter **and no longer pings the server to validate reachability**. If you relied on `Connect` to fail on unreachable deployments, call `client.Ping(ctx, nil)` explicitly after connecting. + - `Collection.Distinct` returns `*DistinctResult` instead of `([]any, error)` — use `result.Err()` followed by `result.Decode(&values)`. + - `WithTransaction` callbacks receive `context.Context` instead of `mongo.SessionContext`. Use `mongo.SessionFromContext(ctx)` if you need the session. ## [5.2.0](https://github.com/LerianStudio/lib-commons/releases/tag/v5.2.0) diff --git a/commons/tenant-manager/mongo/manager.go b/commons/tenant-manager/mongo/manager.go index 3af33524..0a838499 100644 --- a/commons/tenant-manager/mongo/manager.go +++ b/commons/tenant-manager/mongo/manager.go @@ -159,7 +159,9 @@ func (c *MongoConnection) connectWithTLS(ctx context.Context) error { } if err := mongoClient.Ping(ctx, nil); err != nil { - _ = mongoClient.Disconnect(ctx) + disconnectCtx, disconnectCancel := context.WithTimeout(context.Background(), mongoPingTimeout) + _ = mongoClient.Disconnect(disconnectCtx) + disconnectCancel() return fmt.Errorf("mongo connect with TLS failed: %w", err) } From 624ca67d85c186b9ab6a9f02195249991ca37f68 Mon Sep 17 00:00:00 2001 From: Jefferson Rodrigues Date: Wed, 20 May 2026 00:05:31 -0300 Subject: [PATCH 3/3] refactor(mongo): use bounded context for all Disconnect call sites The pattern of passing a request-scoped context to mongo.Client.Disconnect risks leaking topology/monitoring goroutines when the caller's context has already been canceled. The driver requires a non-canceled context to complete its graceful shutdown sequence. Centralize the fix in a disconnectClient helper that always derives a fresh bounded context from context.Background() with a 10s timeout. Apply it to all 7 Disconnect call sites in tenant-manager/mongo/manager.go: - connectWithTLS (Ping-failure cleanup, addressed earlier in this PR) - swapMongoConnection (config-change replacement) - cacheConnection (closed-manager and excess-connection branches) - evictLRU (LRU pool eviction) - Close (Manager shutdown) - CloseConnection (per-tenant disconnect) The swapMongoConnection signature drops its unused ctx parameter as a consequence; updated the single test caller accordingly. X-Lerian-Ref: 0x1 --- .../mongo/coverage_boost_test.go | 2 +- commons/tenant-manager/mongo/manager.go | 48 ++++++++++++------- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/commons/tenant-manager/mongo/coverage_boost_test.go b/commons/tenant-manager/mongo/coverage_boost_test.go index 9bda3ed4..246e45b8 100644 --- a/commons/tenant-manager/mongo/coverage_boost_test.go +++ b/commons/tenant-manager/mongo/coverage_boost_test.go @@ -78,7 +78,7 @@ func TestSwapMongoConnection_UpdatesEntry(t *testing.T) { m.databaseNames["tenant-swap"] = "olddb" newConn := &MongoConnection{DB: nil} - m.swapMongoConnection(context.Background(), "tenant-swap", newConn, "newdb") + m.swapMongoConnection("tenant-swap", newConn, "newdb") m.mu.RLock() stored := m.connections["tenant-swap"] diff --git a/commons/tenant-manager/mongo/manager.go b/commons/tenant-manager/mongo/manager.go index 0a838499..7fed5269 100644 --- a/commons/tenant-manager/mongo/manager.go +++ b/commons/tenant-manager/mongo/manager.go @@ -31,6 +31,27 @@ import ( // mongoPingTimeout is the maximum duration for MongoDB connection health check pings. const mongoPingTimeout = 3 * time.Second +// mongoDisconnectTimeout is the maximum duration allowed for a graceful Disconnect. +// Using a value derived from context.Background() ensures the driver can tear down +// topology and monitoring goroutines cleanly even if the surrounding request context +// has already been canceled. +const mongoDisconnectTimeout = 10 * time.Second + +// disconnectClient closes a MongoDB client using a fresh bounded context. The driver +// requires a non-canceled context to fully shut down topology and monitoring +// goroutines; reusing a request-scoped context risks leaving background resources +// in an inconsistent state when the caller's context has already expired. +func disconnectClient(c *mongo.Client) error { + if c == nil { + return nil + } + + ctx, cancel := context.WithTimeout(context.Background(), mongoDisconnectTimeout) + defer cancel() + + return c.Disconnect(ctx) +} + // defaultConnectionsCheckInterval is the default interval between periodic // connection pool settings revalidation checks. When a cached connection is // returned by GetConnection and this interval has elapsed since the last check, @@ -159,9 +180,7 @@ func (c *MongoConnection) connectWithTLS(ctx context.Context) error { } if err := mongoClient.Ping(ctx, nil); err != nil { - disconnectCtx, disconnectCancel := context.WithTimeout(context.Background(), mongoPingTimeout) - _ = mongoClient.Disconnect(disconnectCtx) - disconnectCancel() + _ = disconnectClient(mongoClient) return fmt.Errorf("mongo connect with TLS failed: %w", err) } @@ -516,7 +535,7 @@ func (p *Manager) reconnectMongo(ctx context.Context, tenantID string, mongoConf return true } - p.swapMongoConnection(ctx, tenantID, newConn, mongoConfig.Database) + p.swapMongoConnection(tenantID, newConn, mongoConfig.Database) return true } @@ -550,7 +569,7 @@ func (p *Manager) canStoreMongoConnection(ctx context.Context, tenantID string, // swapMongoConnection replaces the cached connection with newConn under write // lock and disconnects the old connection after releasing the lock. // Caller must NOT hold p.mu. -func (p *Manager) swapMongoConnection(ctx context.Context, tenantID string, newConn *MongoConnection, database string) { +func (p *Manager) swapMongoConnection(tenantID string, newConn *MongoConnection, database string) { p.mu.Lock() oldConn := p.connections[tenantID] @@ -562,12 +581,9 @@ func (p *Manager) swapMongoConnection(ctx context.Context, tenantID string, newC // Disconnect the old connection after releasing the lock. if oldConn != nil && oldConn.DB != nil { - discCtx, discCancel := context.WithTimeout(ctx, mongoPingTimeout) - if discErr := oldConn.DB.Disconnect(discCtx); discErr != nil && p.logger != nil { + if discErr := disconnectClient(oldConn.DB); discErr != nil && p.logger != nil { p.logger.Warnf("config change: failed to disconnect old MongoDB for tenant %s: %v", tenantID, discErr) } - - discCancel() } if p.logger != nil { @@ -811,7 +827,7 @@ func (p *Manager) cacheConnection( if p.closed { if conn.DB != nil { - if discErr := conn.DB.Disconnect(ctx); discErr != nil && p.logger != nil { + if discErr := disconnectClient(conn.DB); discErr != nil && p.logger != nil { p.logger.Base().Log(ctx, log.LevelWarn, "failed to disconnect mongo connection on closed manager", log.String("tenant_id", tenantID), log.Err(discErr), @@ -824,7 +840,7 @@ func (p *Manager) cacheConnection( if cached, ok := p.connections[tenantID]; ok && cached != nil && cached.DB != nil { if conn.DB != nil { - if discErr := conn.DB.Disconnect(ctx); discErr != nil && p.logger != nil { + if discErr := disconnectClient(conn.DB); discErr != nil && p.logger != nil { p.logger.Base().Log(ctx, log.LevelWarn, "failed to disconnect excess mongo connection", log.String("tenant_id", tenantID), log.Err(discErr), @@ -862,7 +878,7 @@ func (p *Manager) evictLRU(ctx context.Context, logger log.Logger) { // Manager-specific cleanup: disconnect the MongoDB client and remove from all maps. if conn, ok := p.connections[candidateID]; ok { if conn.DB != nil { - if discErr := conn.DB.Disconnect(ctx); discErr != nil { + if discErr := disconnectClient(conn.DB); discErr != nil { if logger != nil { logger.Log(ctx, log.LevelWarn, "failed to disconnect evicted mongo connection", @@ -983,7 +999,7 @@ func (p *Manager) Close(ctx context.Context) error { for _, conn := range snapshot { if conn.DB != nil { - if err := conn.DB.Disconnect(ctx); err != nil { + if err := disconnectClient(conn.DB); err != nil { errs = append(errs, err) } } @@ -1019,11 +1035,7 @@ func (p *Manager) CloseConnection(ctx context.Context, tenantID string) error { p.mu.Unlock() // Step 2: Outside lock — disconnect the captured connection. - if conn.DB != nil { - return conn.DB.Disconnect(ctx) - } - - return nil + return disconnectClient(conn.DB) } // Stats returns connection statistics.