feat: 🎉 postgresql broker first commit of forked prototype from my original code

This commit is contained in:
2026-01-02 20:56:39 +02:00
parent e90e5902cd
commit 19e469ff54
29 changed files with 3325 additions and 2 deletions

View File

@@ -0,0 +1,77 @@
package adapter
import (
"context"
"database/sql"
)
// DBAdapter defines the interface for database operations
type DBAdapter interface {
// Connect establishes a connection to the database
Connect(ctx context.Context) error
// Close closes the database connection
Close() error
// Ping checks if the database is reachable
Ping(ctx context.Context) error
// Begin starts a new transaction
Begin(ctx context.Context) (DBTransaction, error)
// Exec executes a query without returning rows
Exec(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
// QueryRow executes a query that returns at most one row
QueryRow(ctx context.Context, query string, args ...interface{}) DBRow
// Query executes a query that returns rows
Query(ctx context.Context, query string, args ...interface{}) (DBRows, error)
// Listen starts listening on a PostgreSQL notification channel
Listen(ctx context.Context, channel string, handler NotificationHandler) error
// Unlisten stops listening on a channel
Unlisten(ctx context.Context, channel string) error
}
// DBTransaction defines the interface for database transactions
type DBTransaction interface {
// Exec executes a query within the transaction
Exec(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
// QueryRow executes a query that returns at most one row within the transaction
QueryRow(ctx context.Context, query string, args ...interface{}) DBRow
// Query executes a query that returns rows within the transaction
Query(ctx context.Context, query string, args ...interface{}) (DBRows, error)
// Commit commits the transaction
Commit() error
// Rollback rolls back the transaction
Rollback() error
}
// DBRow defines the interface for scanning a single row
type DBRow interface {
Scan(dest ...interface{}) error
}
// DBRows defines the interface for scanning multiple rows
type DBRows interface {
Next() bool
Scan(dest ...interface{}) error
Close() error
Err() error
}
// Notification represents a PostgreSQL NOTIFY event
type Notification struct {
Channel string
Payload string
PID int
}
// NotificationHandler is called when a notification is received
type NotificationHandler func(notification *Notification)

View File

@@ -0,0 +1,22 @@
package adapter
// Logger defines the interface for logging operations
type Logger interface {
// Debug logs a debug message
Debug(msg string, args ...interface{})
// Info logs an info message
Info(msg string, args ...interface{})
// Warn logs a warning message
Warn(msg string, args ...interface{})
// Error logs an error message
Error(msg string, args ...interface{})
// Fatal logs a fatal message and exits
Fatal(msg string, args ...interface{})
// With returns a new logger with additional context
With(key string, value interface{}) Logger
}

View File

@@ -0,0 +1,311 @@
package adapter
import (
"context"
"database/sql"
"fmt"
"sync"
"time"
"github.com/lib/pq"
)
// PostgresConfig holds PostgreSQL connection configuration
type PostgresConfig struct {
Host string
Port int
Database string
User string
Password string
SSLMode string
MaxOpenConns int
MaxIdleConns int
ConnMaxLifetime time.Duration
ConnMaxIdleTime time.Duration
}
// PostgresAdapter implements DBAdapter for PostgreSQL
type PostgresAdapter struct {
config PostgresConfig
db *sql.DB
listener *pq.Listener
logger Logger
mu sync.RWMutex
}
// NewPostgresAdapter creates a new PostgreSQL adapter
func NewPostgresAdapter(config PostgresConfig, logger Logger) *PostgresAdapter {
return &PostgresAdapter{
config: config,
logger: logger,
}
}
// Connect establishes a connection to PostgreSQL
func (p *PostgresAdapter) Connect(ctx context.Context) error {
connStr := p.buildConnectionString()
db, err := sql.Open("postgres", connStr)
if err != nil {
return fmt.Errorf("failed to open database: %w", err)
}
// Configure connection pool
db.SetMaxOpenConns(p.config.MaxOpenConns)
db.SetMaxIdleConns(p.config.MaxIdleConns)
db.SetConnMaxLifetime(p.config.ConnMaxLifetime)
db.SetConnMaxIdleTime(p.config.ConnMaxIdleTime)
// Test connection
if err := db.PingContext(ctx); err != nil {
db.Close()
return fmt.Errorf("failed to ping database: %w", err)
}
p.mu.Lock()
p.db = db
p.mu.Unlock()
p.logger.Info("PostgreSQL connection established", "host", p.config.Host, "database", p.config.Database)
return nil
}
// Close closes the database connection
func (p *PostgresAdapter) Close() error {
p.mu.Lock()
defer p.mu.Unlock()
if p.listener != nil {
if err := p.listener.Close(); err != nil {
p.logger.Error("failed to close listener", "error", err)
}
p.listener = nil
}
if p.db != nil {
if err := p.db.Close(); err != nil {
return fmt.Errorf("failed to close database: %w", err)
}
p.db = nil
}
p.logger.Info("PostgreSQL connection closed")
return nil
}
// Ping checks if the database is reachable
func (p *PostgresAdapter) Ping(ctx context.Context) error {
p.mu.RLock()
db := p.db
p.mu.RUnlock()
if db == nil {
return fmt.Errorf("database connection not established")
}
return db.PingContext(ctx)
}
// Begin starts a new transaction
func (p *PostgresAdapter) Begin(ctx context.Context) (DBTransaction, error) {
p.mu.RLock()
db := p.db
p.mu.RUnlock()
if db == nil {
return nil, fmt.Errorf("database connection not established")
}
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return nil, fmt.Errorf("failed to begin transaction: %w", err)
}
return &postgresTransaction{tx: tx}, nil
}
// Exec executes a query without returning rows
func (p *PostgresAdapter) Exec(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
p.mu.RLock()
db := p.db
p.mu.RUnlock()
if db == nil {
return nil, fmt.Errorf("database connection not established")
}
return db.ExecContext(ctx, query, args...)
}
// QueryRow executes a query that returns at most one row
func (p *PostgresAdapter) QueryRow(ctx context.Context, query string, args ...interface{}) DBRow {
p.mu.RLock()
db := p.db
p.mu.RUnlock()
if db == nil {
return &postgresRow{err: fmt.Errorf("database connection not established")}
}
return &postgresRow{row: db.QueryRowContext(ctx, query, args...)}
}
// Query executes a query that returns rows
func (p *PostgresAdapter) Query(ctx context.Context, query string, args ...interface{}) (DBRows, error) {
p.mu.RLock()
db := p.db
p.mu.RUnlock()
if db == nil {
return nil, fmt.Errorf("database connection not established")
}
rows, err := db.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
return &postgresRows{rows: rows}, nil
}
// Listen starts listening on a PostgreSQL notification channel
func (p *PostgresAdapter) Listen(ctx context.Context, channel string, handler NotificationHandler) error {
connStr := p.buildConnectionString()
reportProblem := func(ev pq.ListenerEventType, err error) {
if err != nil {
p.logger.Error("listener problem", "event", ev, "error", err)
}
}
minReconn := 10 * time.Second
maxReconn := 1 * time.Minute
p.mu.Lock()
p.listener = pq.NewListener(connStr, minReconn, maxReconn, reportProblem)
listener := p.listener
p.mu.Unlock()
if err := listener.Listen(channel); err != nil {
return fmt.Errorf("failed to listen on channel %s: %w", channel, err)
}
p.logger.Info("listening on channel", "channel", channel)
// Start notification handler in goroutine
go func() {
for {
select {
case n := <-listener.Notify:
if n != nil {
handler(&Notification{
Channel: n.Channel,
Payload: n.Extra,
PID: n.BePid,
})
}
case <-ctx.Done():
p.logger.Info("stopping listener", "channel", channel)
return
case <-time.After(90 * time.Second):
go listener.Ping()
}
}
}()
return nil
}
// Unlisten stops listening on a channel
func (p *PostgresAdapter) Unlisten(ctx context.Context, channel string) error {
p.mu.RLock()
listener := p.listener
p.mu.RUnlock()
if listener == nil {
return nil
}
return listener.Unlisten(channel)
}
// buildConnectionString builds a PostgreSQL connection string
func (p *PostgresAdapter) buildConnectionString() string {
sslMode := p.config.SSLMode
if sslMode == "" {
sslMode = "disable"
}
return fmt.Sprintf(
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
p.config.Host,
p.config.Port,
p.config.User,
p.config.Password,
p.config.Database,
sslMode,
)
}
// postgresTransaction implements DBTransaction
type postgresTransaction struct {
tx *sql.Tx
}
func (t *postgresTransaction) Exec(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
return t.tx.ExecContext(ctx, query, args...)
}
func (t *postgresTransaction) QueryRow(ctx context.Context, query string, args ...interface{}) DBRow {
return &postgresRow{row: t.tx.QueryRowContext(ctx, query, args...)}
}
func (t *postgresTransaction) Query(ctx context.Context, query string, args ...interface{}) (DBRows, error) {
rows, err := t.tx.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
return &postgresRows{rows: rows}, nil
}
func (t *postgresTransaction) Commit() error {
return t.tx.Commit()
}
func (t *postgresTransaction) Rollback() error {
return t.tx.Rollback()
}
// postgresRow implements DBRow
type postgresRow struct {
row *sql.Row
err error
}
func (r *postgresRow) Scan(dest ...interface{}) error {
if r.err != nil {
return r.err
}
return r.row.Scan(dest...)
}
// postgresRows implements DBRows
type postgresRows struct {
rows *sql.Rows
}
func (r *postgresRows) Next() bool {
return r.rows.Next()
}
func (r *postgresRows) Scan(dest ...interface{}) error {
return r.rows.Scan(dest...)
}
func (r *postgresRows) Close() error {
return r.rows.Close()
}
func (r *postgresRows) Err() error {
return r.rows.Err()
}

View File

@@ -0,0 +1,80 @@
package adapter
import (
"log/slog"
"os"
)
// SlogLogger implements Logger interface using slog
type SlogLogger struct {
logger *slog.Logger
}
// NewSlogLogger creates a new slog-based logger
func NewSlogLogger(level slog.Level) *SlogLogger {
opts := &slog.HandlerOptions{
Level: level,
}
handler := slog.NewJSONHandler(os.Stdout, opts)
logger := slog.New(handler)
return &SlogLogger{
logger: logger,
}
}
// NewSlogLoggerWithHandler creates a new slog-based logger with a custom handler
func NewSlogLoggerWithHandler(handler slog.Handler) *SlogLogger {
return &SlogLogger{
logger: slog.New(handler),
}
}
// Debug logs a debug message
func (l *SlogLogger) Debug(msg string, args ...interface{}) {
l.logger.Debug(msg, l.convertArgs(args)...)
}
// Info logs an info message
func (l *SlogLogger) Info(msg string, args ...interface{}) {
l.logger.Info(msg, l.convertArgs(args)...)
}
// Warn logs a warning message
func (l *SlogLogger) Warn(msg string, args ...interface{}) {
l.logger.Warn(msg, l.convertArgs(args)...)
}
// Error logs an error message
func (l *SlogLogger) Error(msg string, args ...interface{}) {
l.logger.Error(msg, l.convertArgs(args)...)
}
// Fatal logs a fatal message and exits
func (l *SlogLogger) Fatal(msg string, args ...interface{}) {
l.logger.Error(msg, l.convertArgs(args)...)
os.Exit(1)
}
// With returns a new logger with additional context
func (l *SlogLogger) With(key string, value interface{}) Logger {
return &SlogLogger{
logger: l.logger.With(key, value),
}
}
// convertArgs converts variadic args to slog.Attr pairs
func (l *SlogLogger) convertArgs(args []interface{}) []any {
if len(args) == 0 {
return nil
}
// Convert pairs of key-value to slog format
attrs := make([]any, 0, len(args))
for i := 0; i < len(args); i += 2 {
if i+1 < len(args) {
attrs = append(attrs, args[i], args[i+1])
}
}
return attrs
}