Add CI and production grade improvements #3
@@ -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,10 +122,44 @@ func main() {
|
|||||||
IdleTimeout: 120 * time.Second,
|
IdleTimeout: 120 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("open responses gateway listening", slog.String("address", addr))
|
// Set up signal handling for graceful shutdown
|
||||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
sigChan := make(chan os.Signal, 1)
|
||||||
logger.Error("server error", slog.String("error", err.Error()))
|
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
||||||
os.Exit(1)
|
|
||||||
|
// Run server in a goroutine
|
||||||
|
serverErrors := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
logger.Info("open responses gateway listening", slog.String("address", addr))
|
||||||
|
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()))
|
||||||
|
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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,15 +144,20 @@ 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 {
|
||||||
s.mu.Lock()
|
select {
|
||||||
now := time.Now()
|
case <-ticker.C:
|
||||||
for id, conv := range s.conversations {
|
s.mu.Lock()
|
||||||
if now.Sub(conv.UpdatedAt) > s.ttl {
|
now := time.Now()
|
||||||
delete(s.conversations, id)
|
for id, conv := range s.conversations {
|
||||||
|
if now.Sub(conv.UpdatedAt) > s.ttl {
|
||||||
|
delete(s.conversations, id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
s.mu.Unlock()
|
||||||
|
case <-s.done:
|
||||||
|
return
|
||||||
}
|
}
|
||||||
s.mu.Unlock()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
cutoff := time.Now().Add(-s.ttl)
|
select {
|
||||||
_, _ = s.db.Exec(s.dialect.cleanup, cutoff)
|
case <-ticker.C:
|
||||||
|
cutoff := time.Now().Add(-s.ttl)
|
||||||
|
_, _ = 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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user