Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions pkg/whatsmeow/service/auth_container_retry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package whatsmeow_service

import (
"os"
"path/filepath"
"testing"

_ "modernc.org/sqlite"

"github.com/EvolutionAPI/evolution-go/pkg/config"
)

// Valida a semântica do getAuthContainer compartilhado, usando o caminho SQLite
// (auto-contido, sem serviço externo — roda em qualquer CI):
// 1. falha na criação NÃO é memorizada (retry possível em vez de erro cacheado pra sempre)
// 2. sucesso é memorizado e o MESMO container é reusado nas chamadas seguintes
func TestGetAuthContainerRetryAndReuse(t *testing.T) {
// estado limpo do singleton
sharedAuthContainerMu.Lock()
sharedAuthContainer = nil
sharedAuthContainerMu.Unlock()

// Falha: exPath aponta pra diretório inexistente — o SQLite não cria
// diretórios intermediários, então o Upgrade falha na primeira conexão.
bad := whatsmeowService{
config: &config.Config{},
exPath: filepath.Join(t.TempDir(), "does-not-exist"),
}
if _, err := bad.getAuthContainer(); err == nil {
t.Fatal("esperava erro com diretório inexistente, veio nil")
}
Comment thread
guilhermeCassettari marked this conversation as resolved.
// Falha não deve ser memorizada no singleton
sharedAuthContainerMu.Lock()
if sharedAuthContainer != nil {
sharedAuthContainerMu.Unlock()
t.Fatalf("failed container should not be memoized (sharedAuthContainer = %#v)", sharedAuthContainer)
}
sharedAuthContainerMu.Unlock()

// Sucesso: exPath válido com o subdiretório dbdata esperado pelo DSN.
goodPath := t.TempDir()
if err := os.MkdirAll(filepath.Join(goodPath, "dbdata"), 0o755); err != nil {
t.Fatal(err)
}
good := whatsmeowService{
config: &config.Config{},
exPath: goodPath,
}
c1, err := good.getAuthContainer()
if err != nil {
t.Fatalf("retry após falha deveria funcionar em vez de devolver erro cacheado: %v", err)
}
if c1 == nil {
t.Fatal("container nil após sucesso")
}

c2, err := good.getAuthContainer()
if err != nil {
t.Fatalf("segunda chamada falhou: %v", err)
}
if c1 != c2 {
t.Fatal("container não foi reusado — deveria ser singleton compartilhado")
}
}
72 changes: 53 additions & 19 deletions pkg/whatsmeow/service/whatsmeow.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,57 @@ func (w whatsmeowService) ForceUpdateJid(instanceId string, number string) error
return nil
}

// Container/pool unico e capado pro store do whatsmeow.
// Antes, cada StartClient (conectar E cada reconexao) chamava sqlstore.New(), abrindo
// um *sql.DB novo sem cap e nunca fechado -> leak que saturava o Postgres do evogo_auth.
// Agora e um container compartilhado, com pool limitado e conexao direta.
// Somente o sucesso e memorizado: se a criacao falhar (ex.: Postgres indisponivel
// no boot), a proxima chamada tenta de novo em vez de devolver o erro pra sempre.
var (
sharedAuthContainer *sqlstore.Container
sharedAuthContainerMu sync.Mutex
)

func (w whatsmeowService) getAuthContainer() (*sqlstore.Container, error) {
sharedAuthContainerMu.Lock()
defer sharedAuthContainerMu.Unlock()

if sharedAuthContainer != nil {
return sharedAuthContainer, nil
}

var dbLog waLog.Logger
if w.config.WaDebug != "" {
dbLog = waLog.Stdout("Database", w.config.WaDebug, true)
}
var dialect, address string
if w.config.PostgresAuthDB != "" {
dialect, address = "postgres", w.config.PostgresAuthDB
} else {
dialect = "sqlite"
address = fmt.Sprintf("file:%s/dbdata/main.db?_pragma=foreign_keys(1)&_busy_timeout=5000&cache=shared&mode=rwc&_journal_mode=WAL", w.exPath)
}
db, err := sql.Open(dialect, address)
if err != nil {
return nil, fmt.Errorf("failed to open auth database: %w", err)
}
if dialect == "postgres" {
db.SetMaxOpenConns(20)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)
db.SetConnMaxIdleTime(2 * time.Minute)
} else {
db.SetMaxOpenConns(1)
}
container := sqlstore.NewWithDB(db, dialect, dbLog)
if err := container.Upgrade(context.Background()); err != nil {
_ = db.Close()
return nil, fmt.Errorf("failed to upgrade auth database: %w", err)
}
sharedAuthContainer = container
return sharedAuthContainer, nil
}

func (w whatsmeowService) StartClient(cd *ClientData) {

w.loggerWrapper.GetLogger(cd.Instance.Id).LogInfo("Starting websocket connection to Whatsapp for user '%s'", cd.Instance.Id)
Expand All @@ -293,26 +344,9 @@ func (w whatsmeowService) StartClient(cd *ClientData) {
}

var container *sqlstore.Container

if w.config.WaDebug != "" {
dbLog := waLog.Stdout("Database", w.config.WaDebug, true)
if w.config.PostgresAuthDB != "" {
container, err = sqlstore.New(context.Background(), "postgres", w.config.PostgresAuthDB, dbLog)
} else {
dsn := fmt.Sprintf("file:%s/dbdata/main.db?_pragma=foreign_keys(1)&_busy_timeout=5000&cache=shared&mode=rwc&_journal_mode=WAL", w.exPath)
container, err = sqlstore.New(context.Background(), "sqlite", dsn, dbLog)
}
} else {
if w.config.PostgresAuthDB != "" {
container, err = sqlstore.New(context.Background(), "postgres", w.config.PostgresAuthDB, nil)
} else {
dsn := fmt.Sprintf("file:%s/dbdata/main.db?_pragma=foreign_keys(1)&_busy_timeout=5000&cache=shared&mode=rwc&_journal_mode=WAL", w.exPath)
container, err = sqlstore.New(context.Background(), "sqlite", dsn, nil)
}
}

container, err = w.getAuthContainer()
if err != nil {
w.loggerWrapper.GetLogger(cd.Instance.Id).LogError("[%s] Failed to create container: %v", cd.Instance.Id, err)
w.loggerWrapper.GetLogger(cd.Instance.Id).LogError("[%s] Failed to get auth container: %v", cd.Instance.Id, err)
return
}

Expand Down