Простое переподключение к БД в Go

При разработке web-сервисов, которые должны работать в режиме 24/7, в не последнюю очередь важно правильно работать с ресурсами лежащими за рамками кода, такие как обращение к другим сервисам или работа с базой данных. О последних мы и поговорим.

Не стоит забывать, что разрыв соединение с базой может произойти в любой момент и по многим причинам включая разрыв интернет соединения между серверами, перезагрузка самой БД и тд. Если не уделять этому внимание при разработке, то в лучшем случае ваше приложение упадет и будет перезапущено (если автозапуск настроен). Но лучше перестраховаться заранее.

Ниже приведет пример как это можно сделать простой реконнет к БД работая с Go. Мы используем pgx реализацию Go клиента для Postgres, которая предоставляет различные плюшки.

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/jackc/pgx"
)

type DatabaseInstance struct {
	conn               *pgx.Conn
	connConfig         pgx.ConnConfig
	maxConnectAttempts int
}

func NewConn(uri string, maxAttempts int) (*DatabaseInstance, error) {
	connConfig, err := pgx.ParseURI(uri)
	if err != nil {
		return nil, err
	}

	instance := DatabaseInstance{}
	instance.connConfig = connConfig
	instance.maxConnectAttempts = maxAttempts

	return &instance, err
}

func (db *DatabaseInstance) reconnect() (*pgx.Conn, error) {
	conn, err := pgx.Connect(db.connConfig)
	if err != nil {
		return nil, fmt.Errorf("unable to connection to database: %v", err)
	}

	if err = conn.Ping(context.Background()); err != nil {
		return nil, fmt.Errorf("couldn't ping postgre database: %v", err)
	}

	return conn, err
}

func (db *DatabaseInstance) GetConn() *pgx.Conn {
	var err error

	if db.conn == nil {
		if db.conn, err = db.reconnect(); err != nil {
			log.Fatalf("%s", err)
		}
	}

	if err = db.conn.Ping(context.Background()); err != nil {
		attempt := 0
		ticker := time.NewTicker(5 * time.Second)
		defer ticker.Stop()

		for range ticker.C {
			if attempt >= db.maxConnectAttempts {
				log.Fatalf("connection failed after %d attempt\n", attempt)
			}
			attempt++

			log.Println("reconnecting...")

			db.conn, err = db.reconnect()
			if err == nil {
				return db.conn
			}

			log.Printf("connection was lost. Error: %s. Waiting for 5 sec...\n", err)
		}
	}

	return db.conn
}

Код довольно прост: мы перед взятием соединения пингуем БД, если этого не удалось сделать, то джем 5 секунд и пытаемся снова.

Подобное решение подойдет не всем! Так как перед каждым запросом осуществляется опрос БД, что создает огромный оверхед и подобное решение лучше использовать в сервисах, которые редко ходят с базу. Для сервисов которые постоянно работают с БД лучше использовать пулы соединений. Но даже работая через пулы, код остается полезным, внеся небольшие изменения можно использовать его при первоначальном соединении с базой, это особенно полезно в работе с docker-compose (старых версий), где сервис мог запуститься раньше базы.