refactor(33-data-model-integrity): harden shutdown lifecycle in pkg/audit

This commit is contained in:
rcourtman
2026-02-12 01:34:50 +00:00
parent a1c3baf608
commit 7cc896424d
4 changed files with 50 additions and 13 deletions

View File

@@ -31,6 +31,8 @@ type SQLiteLogger struct {
retentionDays int
stopChan chan struct{}
wg sync.WaitGroup
closeOnce sync.Once
closeErr error
}
// NewSQLiteLogger creates a new SQLite-backed audit logger.
@@ -387,20 +389,24 @@ func (l *SQLiteLogger) VerifySignature(event Event) bool {
// Close gracefully shuts down the logger.
func (l *SQLiteLogger) Close() error {
close(l.stopChan)
l.closeOnce.Do(func() {
close(l.stopChan)
if l.webhookDelivery != nil {
l.webhookDelivery.Stop()
}
if l.webhookDelivery != nil {
l.webhookDelivery.Stop()
}
l.wg.Wait()
l.wg.Wait()
if err := l.db.Close(); err != nil {
return fmt.Errorf("failed to close audit database: %w", err)
}
if err := l.db.Close(); err != nil {
l.closeErr = fmt.Errorf("failed to close audit database: %w", err)
return
}
log.Info().Msg("SQLite audit logger closed")
return nil
log.Info().Msg("SQLite audit logger closed")
})
return l.closeErr
}
// loadWebhookURLs loads webhook URLs from the config table.

View File

@@ -502,3 +502,23 @@ func TestSQLiteLoggerConcurrentAccess(t *testing.T) {
t.Errorf("Expected 100 events, got %d", count)
}
}
func TestSQLiteLoggerCloseIdempotent(t *testing.T) {
tempDir := t.TempDir()
logger, err := NewSQLiteLogger(SQLiteLoggerConfig{
DataDir: tempDir,
CryptoMgr: newMockCryptoManager(),
RetentionDays: 30,
})
if err != nil {
t.Fatalf("NewSQLiteLogger failed: %v", err)
}
if err := logger.Close(); err != nil {
t.Fatalf("first Close failed: %v", err)
}
if err := logger.Close(); err != nil {
t.Fatalf("second Close failed: %v", err)
}
}

View File

@@ -32,6 +32,7 @@ type WebhookDelivery struct {
client *http.Client
queue chan Event
stopChan chan struct{}
stopOnce sync.Once
wg sync.WaitGroup
}
@@ -72,9 +73,11 @@ func (w *WebhookDelivery) Start() {
// Stop gracefully stops the delivery workers.
func (w *WebhookDelivery) Stop() {
close(w.stopChan)
w.wg.Wait()
log.Debug().Msg("Audit webhook delivery stopped")
w.stopOnce.Do(func() {
close(w.stopChan)
w.wg.Wait()
log.Debug().Msg("Audit webhook delivery stopped")
})
}
// Enqueue adds an event to the delivery queue.

View File

@@ -158,3 +158,11 @@ func TestWebhookDeliveryDeliverInvalidURL(t *testing.T) {
t.Fatalf("expected URL blocked error, got %v", err)
}
}
func TestWebhookDeliveryStopIdempotent(t *testing.T) {
delivery := NewWebhookDelivery([]string{})
delivery.Start()
delivery.Stop()
delivery.Stop()
}