feat: 🎉 postgresql broker first commit of forked prototype from my original code
This commit is contained in:
77
pkg/broker/adapter/database.go
Normal file
77
pkg/broker/adapter/database.go
Normal 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)
|
||||
22
pkg/broker/adapter/logger.go
Normal file
22
pkg/broker/adapter/logger.go
Normal 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
|
||||
}
|
||||
311
pkg/broker/adapter/postgres.go
Normal file
311
pkg/broker/adapter/postgres.go
Normal 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()
|
||||
}
|
||||
80
pkg/broker/adapter/slog_logger.go
Normal file
80
pkg/broker/adapter/slog_logger.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user