ODawah
ODawah

Reputation: 21

Mongodb Connection leak using mongo-go driver

I'm developing a Go application that integrates with MongoDB to create, read, and delete images. I've set the connection pool to 4 for testing purposes, but I've noticed that the number of connections can go up to 10 even if I haven't hit any endpoint related to MongoDB. If I don't hit any endpoint, the number of connections stays between 2-3. There's a connection leak in my code, and I'm having trouble figuring out how to release resources correctly to ensure that they are returned to the pool.

As the documentation says mongo-go driver is goroutine safe so I have a function that initialize mongo and set a global variable with the initialized mongo client

package persistence

import (
    "context"
    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
    "time"
)

var MongoClient *mongo.Client

func InitMongo(ctx context.Context, URI string) error {
    if MongoClient != nil {
        return nil
    }
    serverAPI := options.ServerAPI(options.ServerAPIVersion1)
    opts := options.Client().ApplyURI(URI).SetServerAPIOptions(serverAPI)

    opts.SetMinPoolSize(2)
    opts.SetMaxPoolSize(4)
    opts.SetMaxConnIdleTime(2 * time.Second)

    // Create a new client and connect to the server
    client, err := mongo.Connect(ctx, opts)
    if err != nil {
        return err
    }
    MongoClient = client

    // Send a ping to confirm a successful connection
    var result bson.M
    if err = client.Database("admin").RunCommand(context.TODO(), bson.D{{"ping", 1}}).Decode(&result); err != nil {
        return err
    }

    return nil

}

and I use this function in the main to initialize it

func main() {
    configuration := config.NewEnvConfigs()

    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    err = persistence.InitMongo(ctx, configuration.MongoDBURI)
    if err != nil {
        fmt.Println("Mongo not ready")
        panic(err)
    }
    fmt.Println("mongo connected")
    defer func() {
        if err = persistence.MongoClient.Disconnect(context.TODO()); err != nil {
            panic(err)
        }
    }()
    r := server.Routes()
    http.ListenAndServe("0.0.0.0:3333", r)
}

also the last piece of code that uses the mongo client is the handlers for images endpoints

package handlers

import (
    "context"
    "encoding/base64"
    "errors"
    "fmt"
    "github.com/bycultivaet/backend/internal/infrastructure/persistence"
    "github.com/go-chi/chi"
    "github.com/go-chi/render"
    "github.com/google/uuid"
    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/bson/primitive"
    "io/ioutil"
    "mime/multipart"
    "net/http"
)

func GetImageByID() http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        ctx, cancel := context.WithCancel(r.Context())
        defer cancel()
        Uuid := chi.URLParam(r, "uuid")
        _, err := uuid.Parse(Uuid)
        if err != nil {
            render.Status(r, 400)
            render.Respond(w, r, errors.New("invalid uuid").Error())
            return
        }
        filter := bson.M{"uuid": Uuid}
        var result bson.M
        collection := persistence.MongoClient.Database("hassad-media").Collection("images")
        err = collection.FindOne(ctx, filter).Decode(&result)
        if err != nil {
            render.Status(r, 404)
            render.Respond(w, r, errors.New("image not found").Error())
            return
        }
        imageData, ok := result["image"].(primitive.Binary)
        if !ok {
            render.Status(r, 500)
            render.Respond(w, r, errors.New("error parsing image").Error())
            return
        }
        imageBase64 := base64.StdEncoding.EncodeToString(imageData.Data)
        render.Status(r, 200)
        render.JSON(w, r, map[string]string{"image": imageBase64})
    }
}

func UploadImage() http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        ctx, cancel := context.WithCancel(r.Context())
        defer cancel()
        cancelled := r.Context().Done()
        select {
        case <-cancelled:
            render.Status(r, 499)
            render.Respond(w, r, errors.New("request cancelled").Error())
            return
        default:
            err := r.ParseMultipartForm(10 << 20)
            if err != nil {
                render.Status(r, http.StatusBadRequest)
                render.Respond(w, r, err.Error())
                return
            }
            file, _, err := r.FormFile("image")
            if err != nil {
                render.Status(r, http.StatusBadRequest)
                render.Respond(w, r, err.Error())
                return
            }
            defer file.Close()
            Uuid, err := uuid.NewRandom()
            if err != nil {
                render.Status(r, http.StatusInternalServerError)
                render.Respond(w, r, errors.New("error generating uuid").Error())
                return
            }
            // Pass the contents of the file to GetMongoDB
            _, err = UploadPhoto(file, Uuid.String(), ctx)
            if err != nil {
                render.Status(r, http.StatusInternalServerError)
                render.Respond(w, r, err.Error())
                return
            }

            render.Status(r, http.StatusOK)
            render.JSON(w, r, map[string]string{"uuid": Uuid.String()})

        }
    }
}

func UploadPhoto(file multipart.File, uuid string, ctx context.Context) (interface{}, error) {
    imageBytes, err := ioutil.ReadAll(file)
    if err != nil {
        return nil, err
    }
    imageDoc := bson.M{"image": imageBytes, "uuid": uuid}
    defer file.Close()
    collection := persistence.MongoClient.Database("hassad-media").Collection("images")
    data, err := collection.InsertOne(ctx, imageDoc)
    if err != nil {
        return 0, err
    }

    fmt.Println("Inserted image into MongoDB!")
    fmt.Println(data.InsertedID)
    return data.InsertedID, nil
}

func DeleteImageByID() http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        ctx, cancel := context.WithCancel(r.Context())
        defer cancel()

        uuid := chi.URLParam(r, "uuid")
        filter := bson.M{"uuid": uuid}
        collection := persistence.MongoClient.Database("hassad-media").Collection("images")
        res, err := collection.DeleteOne(ctx, filter)
        if err != nil {
            if res.DeletedCount == 0 {
                render.Status(r, 404)
                render.Respond(w, r, errors.New("image not found").Error())
                return
            }
            render.Status(r, 500)
            render.Respond(w, r, err.Error())
            return
        }
        render.JSON(w, r, map[string]string{"answer": "deleted"})
    }
}

what I'm doing wrong closing the connections to be returned to the pool or any other configuration

I test my code by with python script that hits the endpoints excessively

I tried to set collection and database global instead of client it didn't work

Upvotes: 1

Views: 170

Answers (0)

Related Questions