136 lines
		
	
	
		
			2.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			136 lines
		
	
	
		
			2.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package repos
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"gralias/config"
 | |
| 	"log/slog"
 | |
| 	"os"
 | |
| 	"sync"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/jmoiron/sqlx"
 | |
| 	_ "github.com/mattn/go-sqlite3"
 | |
| )
 | |
| 
 | |
| type AllRepos interface {
 | |
| 	RoomsRepo
 | |
| 	ActionsRepo
 | |
| 	PlayersRepo
 | |
| 	SessionsRepo
 | |
| 	WordCardsRepo
 | |
| 	SettingsRepo
 | |
| 	CardMarksRepo
 | |
| 	PlayerStatsRepo
 | |
| 	JournalRepo
 | |
| 	InitTx(ctx context.Context) (context.Context, *sqlx.Tx, error)
 | |
| 	Close()
 | |
| }
 | |
| 
 | |
| type RepoProvider struct {
 | |
| 	DB       *sqlx.DB
 | |
| 	mu       sync.RWMutex
 | |
| 	pathToDB string
 | |
| }
 | |
| 
 | |
| var RP AllRepos
 | |
| 
 | |
| func init() {
 | |
| 	cfg := config.LoadConfigOrDefault("")
 | |
| 	// sqlite3 has lock on write, so we need to have only one connection per whole app
 | |
| 	// https://github.com/mattn/go-sqlite3/issues/274#issuecomment-232942571
 | |
| 	RP = NewRepoProvider(cfg.DBPath)
 | |
| }
 | |
| 
 | |
| func NewRepoProvider(pathToDB string) AllRepos {
 | |
| 	db, err := sqlx.Connect("sqlite3", pathToDB)
 | |
| 	if err != nil {
 | |
| 		slog.Error("Unable to connect to database", "error", err, "pathToDB", pathToDB)
 | |
| 		os.Exit(1)
 | |
| 	}
 | |
| 	stmts := []string{
 | |
| 		"PRAGMA foreign_keys = ON;",
 | |
| 		"PRAGMA busy_timeout=200;",
 | |
| 	}
 | |
| 	for _, stmt := range stmts {
 | |
| 		_, err = db.Exec(stmt)
 | |
| 		if err != nil {
 | |
| 			slog.Error("Unable to enable foreign keys", "error", err)
 | |
| 			os.Exit(1)
 | |
| 		}
 | |
| 	}
 | |
| 	slog.Info("Successfully connected to database")
 | |
| 	// db.SetMaxOpenConns(2)
 | |
| 	rp := &RepoProvider{
 | |
| 		DB:       db,
 | |
| 		pathToDB: pathToDB,
 | |
| 	}
 | |
| 	go rp.pingLoop()
 | |
| 	return rp
 | |
| }
 | |
| 
 | |
| func (rp *RepoProvider) pingLoop() {
 | |
| 	ticker := time.NewTicker(1 * time.Minute)
 | |
| 	defer ticker.Stop()
 | |
| 
 | |
| 	for range ticker.C {
 | |
| 		if err := rp.pingDB(); err != nil {
 | |
| 			slog.Error("Database ping failed, attempting to reconnect...", "error", err)
 | |
| 			rp.reconnect()
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (rp *RepoProvider) pingDB() error {
 | |
| 	rp.mu.RLock()
 | |
| 	defer rp.mu.RUnlock()
 | |
| 	if rp.DB == nil {
 | |
| 		return os.ErrClosed
 | |
| 	}
 | |
| 	return rp.DB.Ping()
 | |
| }
 | |
| 
 | |
| func (rp *RepoProvider) reconnect() {
 | |
| 	rp.mu.Lock()
 | |
| 	defer rp.mu.Unlock()
 | |
| 
 | |
| 	// Double-check if connection is still down
 | |
| 	if rp.DB != nil {
 | |
| 		if err := rp.DB.Ping(); err == nil {
 | |
| 			slog.Info("Database connection already re-established.")
 | |
| 			return
 | |
| 		}
 | |
| 		// if ping fails, we continue to reconnect
 | |
| 		rp.DB.Close() // close old connection
 | |
| 	}
 | |
| 
 | |
| 	slog.Info("Reconnecting to database...")
 | |
| 	db, err := sqlx.Connect("sqlite3", rp.pathToDB)
 | |
| 	if err != nil {
 | |
| 		slog.Error("Failed to reconnect to database", "error", err)
 | |
| 		rp.DB = nil // make sure DB is nil if connection failed
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	rp.DB = db
 | |
| 	slog.Info("Successfully reconnected to database")
 | |
| }
 | |
| 
 | |
| func getDB(ctx context.Context, db *sqlx.DB) sqlx.ExtContext {
 | |
| 	if tx, ok := ctx.Value("tx").(*sqlx.Tx); ok {
 | |
| 		return tx
 | |
| 	}
 | |
| 	return db
 | |
| }
 | |
| 
 | |
| func (p *RepoProvider) InitTx(ctx context.Context) (context.Context, *sqlx.Tx, error) {
 | |
| 	tx, err := p.DB.BeginTxx(ctx, nil)
 | |
| 	if err != nil {
 | |
| 		return nil, nil, err
 | |
| 	}
 | |
| 	return context.WithValue(ctx, "tx", tx), tx, nil
 | |
| }
 | |
| 
 | |
| func (p *RepoProvider) Close() {
 | |
| 	p.DB.Close()
 | |
| }
 | 
