How to properly disconnect MongoDB client - mongodb

As far as I understand, you have to disconnect from MongoDB after you finish using it, but I'm not entirely sure how to do it right
var collection *mongo.Collection
var ctx = context.TODO()
func init() {
clientOptions := options.Client().ApplyURI("mongodb://localhost:27017/")
client, err := mongo.Connect(ctx, clientOptions)
if err != nil {
log.Fatal(err)
}
//defer client.Disconnect(ctx)
err = client.Ping(ctx, nil)
if err != nil {
log.Fatal(err)
}
fmt.Println("Connected successfully")
collection = client.Database("testDB").Collection("testCollection") //create DB
}
There is commented out function call
defer client.Disconnect(ctx)
Which would work fine if all code happens in main() function, but since defer gets called right after init() executes, DB in main() function is already disconnected.
So the question is - what would be the right way to approach this case?

Your application needs the connected MongoDB client in your all services or repositories and therefore it is easier, if you have separated MongoDB client connect and disconnect functions in application package. You don't need to connect MongoDB client, if your server is starting, you can connect first if your services or repositories need the MongoDB client connection.
// db.go
package application
import (
"context"
"fmt"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"log"
"os"
)
var client *mongo.Client
func ResolveClientDB() *mongo.Client {
if client != nil {
return client
}
var err error
// TODO add to your .env.yml or .config.yml MONGODB_URI: mongodb://localhost:27017
clientOptions := options.Client().ApplyURI(os.Getenv("MONGODB_URI"))
client, err = mongo.Connect(context.Background(), clientOptions)
if err != nil {
log.Fatal(err)
}
// check the connection
err = client.Ping(context.Background(), nil)
if err != nil {
log.Fatal(err)
}
// TODO optional you can log your connected MongoDB client
return client
}
func CloseClientDB() {
if client == nil {
return
}
err := client.Disconnect(context.TODO())
if err != nil {
log.Fatal(err)
}
// TODO optional you can log your closed MongoDB client
fmt.Println("Connection to MongoDB closed.")
}
In main:
func main() {
// TODO add your main code here
defer application.CloseClientDB()
}
In your repositories or services you can get now your MongoDB client easily:
// account_repository.go
// TODO add here your account repository interface
func (repository *accountRepository) getClient() *mongo.Client {
if repository.client != nil {
return repository.client
}
repository.client = application.ResolveClientDB()
return repository.client
}
func (repository *accountRepository) FindOneByFilter(filter bson.D) (*model.Account, error) {
var account *model.Account
collection := repository.getClient().Database("yourDB").Collection("account")
err := collection.FindOne(context.Background(), filter).Decode(&account)
return account, err
}

Related

Golang beginner not able solve the problem

I would like to make a user login from this method. This has to be in three parts. The same as user registration, but I am not understanding how to do this? Could you please write how I can write a user login logic this same way? It is created using gorilla mux.
One method has to be in db_service.go then one method has to be in login_service.go
one method has to be in login.go.
This is db_service.go code:
/* Used to create a singleton object of MongoDB client.
Initialized and exposed through GetMongoClient().*/
var clientInstance *mongo.Client
//Used during creation of singleton client object in GetMongoClient().
var clientInstanceError error
//Used to execute client creation procedure only once.
var mongoOnce sync.Once
//I have used below constants just to hold required database config's.
const (
CONNECTIONSTRING = "http://127.0.0.1:27017"
AUTH_DB = "Cluster0"
USER_COLLECTION = "user"
)
//GetMongoClient - Return mongodb connection to work with
func GetMongoClient() (*mongo.Client, error) {
//Perform connection creation operation only once.
mongoOnce.Do(func() {
// Set client options
clientOptions := options.Client().ApplyURI(CONNECTIONSTRING)
// Connect to MongoDB
client, err := mongo.Connect(context.TODO(), clientOptions)
if err != nil {
clientInstanceError = err
}
// Check the connection
err = client.Ping(context.TODO(), nil)
if err != nil {
clientInstanceError = err
}
log.Println("Connected Mongodb!")
clientInstance = client
})
return clientInstance, clientInstanceError
}
//CreateIssue - Insert a new document in the collection.
func User_Collection(user *model.User) (*mongo.InsertOneResult, error) {
//Create a handle to the respective collection in the database.
collection := clientInstance.Database(AUTH_DB).Collection(USER_COLLECTION)
//Perform InsertOne operation & validate against the error.
return collection.InsertOne(context.TODO(), user)
}
login_service.go contained code for user registration:
func Register_User(user *model.User) (interface{}, error) {
user.CreatedAt = time.Now().UTC()
user.UpdatedAt = time.Now().UTC()
if result, err := util.User_Collection(user); err == nil {
return result.InsertedID, err
} else {
return nil, err
}
}
User registration code:
func Register(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var user model.User
if err := json.NewDecoder(r.Body).Decode(&user); err == nil {
if _, err := service.Register_User(&user); err == nil {
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
} else {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(err)
}
} else {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
}
Something like this for the handler
func Login(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var user model.User
if err := json.NewDecoder(r.Body).Decode(&user); err == nil {
if _, err := service.Login_User(&user); err == nil {
json.NewEncoder(w).Encode(user)
} else {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(err)
}
} else {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
}
And something like this for the DB (Need to implement the query instead of insert)
func Login_User(user *model.User) (interface{}, error) {
// Implement this query
if result, err := util.Find_User(user); err == nil {
return result, err
} else {
return nil, err
}
}

Can I have insecure GET HTTP requests whilst having MTLS securing all other HTTP requests?

I have a HTTP REST service written in golang demonstrating what I'm attempting.
I want GET requests insecure and all other REST requests secured with MTLS.
My implementation already uses the gin web server library so I'd like to stick with that if possible.
My issue is that I have only been able to apply the tlsConfig to both groups or neither. I've been unable to find a way to apply this at the group level.
package main
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io/ioutil"
"net/http"
"log"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
// Unprotected public router for GET requests
public := router.Group("/")
// Private router with MTLS
private := router.Group("/")
public.GET("/insecure-ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "insecure pong",
})
})
private.POST("/secure-ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "secure pong",
})
})
// Get the SystemCertPool, continue with an empty pool on error
rootCAs, err := x509.SystemCertPool()
if err != nil {
log.Fatal(err)
}
if rootCAs == nil {
rootCAs = x509.NewCertPool()
}
// Create a CA certificate pool and add cacert.pem to it
caCert, err := ioutil.ReadFile("cacert.pem")
if err != nil {
log.Fatal(err)
}
if ok := rootCAs.AppendCertsFromPEM(caCert); !ok {
err := errors.New("failed to append CA cert to local system certificate pool")
log.Fatal(err)
}
server := http.Server{
Addr: fmt.Sprintf(":%v", 8080),
Handler: router,
}
server.TLSConfig = &tls.Config{
RootCAs: rootCAs,
}
err = server.ListenAndServeTLS("certificate.crt", "privateKey.key")
if err != nil {
log.Fatal(err)
}
}
Just create two Server instances and run them both, one with ListenAndServe and one with ListenAndServeTLS, configured with the same routes. Because HTTP and HTTPS operate on different ports, they have to have different listeners, but both listeners can use the same (or different) handlers. For example:
publicRouter := gin.Default()
// Unprotected public router for GET requests
public := publicRouter.Group("/")
public.GET("/insecure-ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "insecure pong",
})
})
server := http.Server{
Addr: fmt.Sprintf(":%v", 8081), // Or whatever
Handler: publicRouter,
}
go func() {
err = tlsServer.ListenAndServe()
if err != nil {
log.Fatal(err)
}
}()
// Private router with MTLS
router := gin.Default()
private := router.Group("/")
private.POST("/secure-ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "secure pong",
})
})
// Get the SystemCertPool, continue with an empty pool on error
rootCAs, err := x509.SystemCertPool()
if err != nil {
log.Fatal(err)
}
if rootCAs == nil {
rootCAs = x509.NewCertPool()
}
// Create a CA certificate pool and add cacert.pem to it
caCert, err := ioutil.ReadFile("cacert.pem")
if err != nil {
log.Fatal(err)
}
if ok := rootCAs.AppendCertsFromPEM(caCert); !ok {
err := errors.New("failed to append CA cert to local system certificate pool")
log.Fatal(err)
}
tlsServer := http.Server{
Addr: fmt.Sprintf(":%v", 8080),
Handler: router,
}
tlsServer.TLSConfig = &tls.Config{
RootCAs: rootCAs,
}
err = tlsServer.ListenAndServeTLS("certificate.crt", "privateKey.key")
if err != nil {
log.Fatal(err)
}

Go: client is disconnected

It's been few weeks since I joined in Gophers team. So far so good.
I started a new project using a fiber web framework to build backend APIs.
I am using MongoDB as my database.
database/db.go
package database
import (
"context"
"log"
"time"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"go.mongodb.org/mongo-driver/mongo/readpref"
)
var DB *mongo.Database
// InitMongo : Initialize mongodb...
func connectToMongo() {
log.Printf("Initializing database")
client, err := mongo.NewClient(options.Client().ApplyURI("mongodb://localhost:27017"))
if err != nil {
log.Fatal("Could not able to connect to the database, Reason:", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
err = client.Connect(ctx)
if err != nil {
log.Fatal("Context error, mongoDB:", err)
}
//Cancel context to avoid memory leak
defer cancel()
defer client.Disconnect(ctx)
// Ping our db connection
err = client.Ping(context.Background(), readpref.Primary())
if err != nil {
log.Fatal("Ping, mongoDB:", err)
}
log.Printf("Database connected!")
// Create a database
DB = client.Database("golang-test")
return
}
// In Golang, init() functions always initialize whenever the package is called.
// So, whenever DB variable called, the init() function initialized
func init() {
connectToMongo()
}
controllers/mongo.controller/mongo.controller.go
package mongocontroller
import (
"log"
"github.com/gofiber/fiber/v2"
service "gitlab.com/.../services/mongoservice"
)
// GetPersons godoc
// #Summary Get persons.
// #Description Get persons
// #Tags persons
// #Produce json
// #Success 200 {object} []service.Person
// #Failure 400 {object} httputil.HTTPError
// #Failure 404 {object} httputil.HTTPError
// #Failure 500 {object} httputil.HTTPError
// #Router /v1/persons [get]
func GetPersons(c *fiber.Ctx) error {
res, err := service.GetPersons()
if err != nil {
log.Fatal("ERROR: in controller...", err)
}
return c.JSON(res)
}
services/mongoservice/mongo.service.go
package mongoservice
import (
"context"
"log"
database "gitlab.com/.../database"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
// Person : ...
type Person struct {
ID primitive.ObjectID `bson:"_id,omitempty" json:"_id,omitempty"`
Name string `bson:"name,omitempty" json:"name,omitempty"`
Age int `bson:"age,omitempty" json:"age,omitempty"`
}
func GetPersons() ([]Person, error) {
ctx := context.Background()
persons := []Person{}
log.Printf("mongo data...", ctx)
cur, err := database.DB.Collection("persons").Find(ctx, bson.M{})
if err != nil {
log.Fatal(err)
}
// Iterate through the returned cursor.
for cur.Next(ctx) {
var person Person
cur.Decode(&person)
persons = append(persons, person)
}
defer cur.Close(ctx)
return persons, err
}
Here is my data stored in the database:
The problem is, the line cur, err := database.DB.Collection("persons").Find(ctx, bson.M{}) from service always throwing Client is disconnected.
Any help is appreciated!
Thank you.
You are calling defer client.Disconnect(ctx) in the same function which is creating the connection (connectToMongo ) so it will close the connection after calling the function. You should return the connection and close after finishing your task. I mean these parts:
defer cancel()
defer client.Disconnect(ctx)

Why is Postgres complaining 'FATAL #53300 sorry, too many clients already' when I am using only one client?

I have a global instance to Postgres database which is being used throughout the code.
There are 500 goroutines inserting some data into the database using the same instance.
So when there is just one instance (client) why does it fail with panic: FATAL #53300 sorry, too many clients already?
My understanding is that, there is just one instance of the database which is being created by ConnectToDB() method when it is called for the first time & on the subsequent calls, it merely returns this instance instead of creating a new one.
Finally when main ends, that one single instance is closed.
I am not able to understand how could it possibly create too many instances as indicated by the error message.
package main
import (
"github.com/go-pg/pg/v10"
"github.com/go-pg/pg/v10/orm"
"sync"
)
var db *pg.DB
var once sync.Once
type DummyTable struct {
IdOne int
IdTwo int
}
func main() {
var wg sync.WaitGroup
db := ConnectToDB()
defer db.Close()
for i := 0; i < 500; i++ {
wg.Add(1)
go insertIntoDB(&wg)
}
wg.Wait()
}
func insertIntoDB(wg *sync.WaitGroup) {
defer wg.Done()
localDb := ConnectToDB()
_, err := localDb.Model(&DummyTable{
IdOne: 2,
IdTwo: 3,
}).Insert()
if err != nil {
panic(err)
}
}
func createSchema(db *pg.DB) error {
models := []interface{}{
(*DummyTable)(nil),
}
for _, model := range models {
err := db.Model(model).CreateTable(&orm.CreateTableOptions{
Temp: false,
})
if err != nil {
return err
}
}
return nil
}
func ConnectToDB() *pg.DB {
once.Do(func() {
db = pg.Connect(&pg.Options{
User: "username",
Database: "dbName",
})
err := createSchema(db)
if err != nil {
panic(err)
}
})
return db
}

Connection(localhost:27017[-4]) failed to write: context canceled

Here I read it's necessary to cancel the context. This is how my db.go looks like:
package db
import (
"context"
"fmt"
"log"
"os"
"time"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
const (
connectionStringTemplate = "mongodb://%s:%s#%s"
)
var DB *mongo.Client
var Ctx context.Context
// Connect with create the connection to MongoDB
func Connect() {
username := os.Getenv("MONGODB_USERNAME")
password := os.Getenv("MONGODB_PASSWORD")
clusterEndpoint := os.Getenv("MONGODB_ENDPOINT")
connectionURI := fmt.Sprintf(connectionStringTemplate, username, password, clusterEndpoint)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
client, err := mongo.NewClient(options.Client().ApplyURI(connectionURI))
if err != nil {
log.Printf("Failed to create client: %v", err)
}
err = client.Connect(ctx)
if err != nil {
log.Printf("Failed to connect to cluster: %v", err)
}
// Force a connection to verify our connection string
err = client.Ping(ctx, nil)
if err != nil {
log.Printf("Failed to ping cluster: %v", err)
}
DB = client
Ctx = ctx
log.Printf("Connected to MongoDB!")
}
I execute this in the main.go:
func main() {
// Configure
db.Connect()
defer db.DB.Disconnect(context.Background())
r := gin.Default()
// Routes
//r.GET("/movies", handlers.GetAllMoviesHandler)
r.POST("/movies", handlers.AddMovieHandler)
// listen and serve on 0.0.0.0:8080
r.Run()
}
and it works fine if my local mongodb is running.
Now when I use the client like this (I know the naming is still bad):
func AddMovie(movie *Movie) (primitive.ObjectID, error) {
movie.ID = primitive.NewObjectID()
result, err := db.DB.Database("movies").Collection("movies").InsertOne(db.Ctx, movie)
if err != nil {
log.Printf("Could not create movie: %v", err)
return primitive.NilObjectID, err
}
oid := result.InsertedID.(primitive.ObjectID)
return oid, nil
}
I get an error like this
{"msg":{"Code":0,"Message":"connection(localhost:27017[-4]) failed to write: context canceled","Labels":["NetworkError","RetryableWriteError"],"Name":"","Wrapped":{"ConnectionID":"localhost:27017[-4]","Wrapped":{}}}}
It works fine when I put the defer cancel() in comment but I suppose this is not correct?. What am I doing wrong here.
Update:
It works when I use:
result, err :=db.DB.Database("movies").Collection("movies").InsertOne(context.Background(), movie) instead of ctx. I don't fully understand the usage of the context in these use case. I'm not sure if I should do the Ctx = ctx in the Connect function either.