Add CI and production grade improvements #3

Merged
A8065384 merged 13 commits from push-kquouluryqwu into main 2026-03-05 23:09:11 +00:00
5 changed files with 94 additions and 18 deletions
Showing only changes of commit 2edb290563 - Show all commits

View File

@@ -9,6 +9,8 @@ import (
"log/slog" "log/slog"
"net/http" "net/http"
"os" "os"
"os/signal"
"syscall"
"time" "time"
_ "github.com/go-sql-driver/mysql" _ "github.com/go-sql-driver/mysql"
@@ -120,11 +122,45 @@ func main() {
IdleTimeout: 120 * time.Second, IdleTimeout: 120 * time.Second,
} }
// Set up signal handling for graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
// Run server in a goroutine
serverErrors := make(chan error, 1)
go func() {
logger.Info("open responses gateway listening", slog.String("address", addr)) logger.Info("open responses gateway listening", slog.String("address", addr))
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { serverErrors <- srv.ListenAndServe()
}()
// Wait for shutdown signal or server error
select {
case err := <-serverErrors:
if err != nil && err != http.ErrServerClosed {
logger.Error("server error", slog.String("error", err.Error())) logger.Error("server error", slog.String("error", err.Error()))
os.Exit(1) os.Exit(1)
} }
case sig := <-sigChan:
logger.Info("received shutdown signal", slog.String("signal", sig.String()))
// Create shutdown context with timeout
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer shutdownCancel()
// Shutdown the HTTP server gracefully
logger.Info("shutting down server gracefully")
if err := srv.Shutdown(shutdownCtx); err != nil {
logger.Error("server shutdown error", slog.String("error", err.Error()))
}
// Close conversation store
logger.Info("closing conversation store")
if err := convStore.Close(); err != nil {
logger.Error("error closing conversation store", slog.String("error", err.Error()))
}
logger.Info("shutdown complete")
}
} }
func initConversationStore(cfg config.ConversationConfig, logger *slog.Logger) (conversation.Store, error) { func initConversationStore(cfg config.ConversationConfig, logger *slog.Logger) (conversation.Store, error) {

View File

@@ -14,6 +14,7 @@ type Store interface {
Append(id string, messages ...api.Message) (*Conversation, error) Append(id string, messages ...api.Message) (*Conversation, error)
Delete(id string) error Delete(id string) error
Size() int Size() int
Close() error
} }
// MemoryStore manages conversation history in-memory with automatic expiration. // MemoryStore manages conversation history in-memory with automatic expiration.
@@ -21,6 +22,7 @@ type MemoryStore struct {
conversations map[string]*Conversation conversations map[string]*Conversation
mu sync.RWMutex mu sync.RWMutex
ttl time.Duration ttl time.Duration
done chan struct{}
} }
// Conversation holds the message history for a single conversation thread. // Conversation holds the message history for a single conversation thread.
@@ -37,6 +39,7 @@ func NewMemoryStore(ttl time.Duration) *MemoryStore {
s := &MemoryStore{ s := &MemoryStore{
conversations: make(map[string]*Conversation), conversations: make(map[string]*Conversation),
ttl: ttl, ttl: ttl,
done: make(chan struct{}),
} }
// Start cleanup goroutine if TTL is set // Start cleanup goroutine if TTL is set
@@ -141,7 +144,9 @@ func (s *MemoryStore) cleanup() {
ticker := time.NewTicker(1 * time.Minute) ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop() defer ticker.Stop()
for range ticker.C { for {
select {
case <-ticker.C:
s.mu.Lock() s.mu.Lock()
now := time.Now() now := time.Now()
for id, conv := range s.conversations { for id, conv := range s.conversations {
@@ -150,6 +155,9 @@ func (s *MemoryStore) cleanup() {
} }
} }
s.mu.Unlock() s.mu.Unlock()
case <-s.done:
return
}
} }
} }
@@ -159,3 +167,9 @@ func (s *MemoryStore) Size() int {
defer s.mu.RUnlock() defer s.mu.RUnlock()
return len(s.conversations) return len(s.conversations)
} }
// Close stops the cleanup goroutine and releases resources.
func (s *MemoryStore) Close() error {
close(s.done)
return nil
}

View File

@@ -122,3 +122,8 @@ func (s *RedisStore) Size() int {
return count return count
} }
// Close closes the Redis client connection.
func (s *RedisStore) Close() error {
return s.client.Close()
}

View File

@@ -41,6 +41,7 @@ type SQLStore struct {
db *sql.DB db *sql.DB
ttl time.Duration ttl time.Duration
dialect sqlDialect dialect sqlDialect
done chan struct{}
} }
// NewSQLStore creates a SQL-backed conversation store. It creates the // NewSQLStore creates a SQL-backed conversation store. It creates the
@@ -58,7 +59,12 @@ func NewSQLStore(db *sql.DB, driver string, ttl time.Duration) (*SQLStore, error
return nil, err return nil, err
} }
s := &SQLStore{db: db, ttl: ttl, dialect: newDialect(driver)} s := &SQLStore{
db: db,
ttl: ttl,
dialect: newDialect(driver),
done: make(chan struct{}),
}
if ttl > 0 { if ttl > 0 {
go s.cleanup() go s.cleanup()
} }
@@ -144,8 +150,19 @@ func (s *SQLStore) cleanup() {
ticker := time.NewTicker(1 * time.Minute) ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop() defer ticker.Stop()
for range ticker.C { for {
select {
case <-ticker.C:
cutoff := time.Now().Add(-s.ttl) cutoff := time.Now().Add(-s.ttl)
_, _ = s.db.Exec(s.dialect.cleanup, cutoff) _, _ = s.db.Exec(s.dialect.cleanup, cutoff)
case <-s.done:
return
}
} }
} }
// Close stops the cleanup goroutine and closes the database connection.
func (s *SQLStore) Close() error {
close(s.done)
return s.db.Close()
}

View File

@@ -220,6 +220,10 @@ func (m *mockConversationStore) Size() int {
return len(m.conversations) return len(m.conversations)
} }
func (m *mockConversationStore) Close() error {
return nil
}
func (m *mockConversationStore) setConversation(id string, conv *conversation.Conversation) { func (m *mockConversationStore) setConversation(id string, conv *conversation.Conversation) {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()