fix health checks on startup

This commit is contained in:
Evan Jarrett
2025-10-20 12:22:25 -05:00
parent b155534d1b
commit 4ca90fc3af
8 changed files with 1087 additions and 608 deletions
+43
View File
@@ -309,6 +309,49 @@ func GetUserRepositories(db *sql.DB, did string) ([]Repository, error) {
return repos, nil
}
// GetRepositoryMetadata retrieves metadata for a repository from its most recent manifest
func GetRepositoryMetadata(db *sql.DB, did string, repository string) (title, description, sourceURL, documentationURL, licenses, iconURL string, err error) {
var titleNull, descriptionNull, sourceURLNull, documentationURLNull, licensesNull, iconURLNull sql.NullString
err = db.QueryRow(`
SELECT title, description, source_url, documentation_url, licenses, icon_url
FROM manifests
WHERE did = ? AND repository = ?
ORDER BY created_at DESC
LIMIT 1
`, did, repository).Scan(&titleNull, &descriptionNull, &sourceURLNull, &documentationURLNull, &licensesNull, &iconURLNull)
if err == sql.ErrNoRows {
// No manifests found - return empty strings
return "", "", "", "", "", "", nil
}
if err != nil {
return "", "", "", "", "", "", err
}
// Convert NullString to string
if titleNull.Valid {
title = titleNull.String
}
if descriptionNull.Valid {
description = descriptionNull.String
}
if sourceURLNull.Valid {
sourceURL = sourceURLNull.String
}
if documentationURLNull.Valid {
documentationURL = documentationURLNull.String
}
if licensesNull.Valid {
licenses = licensesNull.String
}
if iconURLNull.Valid {
iconURL = iconURLNull.String
}
return title, description, sourceURL, documentationURL, licenses, iconURL, nil
}
// GetUserByDID retrieves a user by DID
func GetUserByDID(db *sql.DB, did string) (*User, error) {
var user User
+120
View File
@@ -0,0 +1,120 @@
package db
import (
"testing"
"time"
)
func TestGetRepositoryMetadata(t *testing.T) {
// Create in-memory test database
db, err := InitDB(":memory:")
if err != nil {
t.Fatalf("Failed to init database: %v", err)
}
defer db.Close()
// Insert test user
testUser := &User{
DID: "did:plc:test123",
Handle: "testuser.bsky.social",
PDSEndpoint: "https://test.pds.example.com",
Avatar: "",
LastSeen: time.Now(),
}
if err := UpsertUser(db, testUser); err != nil {
t.Fatalf("Failed to insert user: %v", err)
}
// Test 1: No manifests - should return empty strings
title, description, sourceURL, documentationURL, licenses, iconURL, err := GetRepositoryMetadata(db, testUser.DID, "nonexistent")
if err != nil {
t.Fatalf("Expected no error for nonexistent repo, got: %v", err)
}
if title != "" || description != "" || sourceURL != "" || documentationURL != "" || licenses != "" || iconURL != "" {
t.Error("Expected all empty strings for nonexistent repository")
}
// Test 2: Insert manifest with metadata
_, err = db.Exec(`
INSERT INTO manifests (did, repository, digest, hold_endpoint, schema_version, media_type, created_at,
title, description, source_url, documentation_url, licenses, icon_url)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, testUser.DID, "myapp", "sha256:abc123", "did:web:hold.example.com", 2, "application/vnd.oci.image.manifest.v1+json",
time.Now().Add(-2*time.Hour),
"My App", "A cool application", "https://github.com/user/myapp", "https://docs.example.com", "MIT", "https://example.com/icon.png")
if err != nil {
t.Fatalf("Failed to insert manifest: %v", err)
}
// Test 3: Retrieve metadata
title, description, sourceURL, documentationURL, licenses, iconURL, err = GetRepositoryMetadata(db, testUser.DID, "myapp")
if err != nil {
t.Fatalf("Failed to get repository metadata: %v", err)
}
if title != "My App" {
t.Errorf("Expected title 'My App', got '%s'", title)
}
if description != "A cool application" {
t.Errorf("Expected description 'A cool application', got '%s'", description)
}
if sourceURL != "https://github.com/user/myapp" {
t.Errorf("Expected sourceURL 'https://github.com/user/myapp', got '%s'", sourceURL)
}
if documentationURL != "https://docs.example.com" {
t.Errorf("Expected documentationURL 'https://docs.example.com', got '%s'", documentationURL)
}
if licenses != "MIT" {
t.Errorf("Expected licenses 'MIT', got '%s'", licenses)
}
if iconURL != "https://example.com/icon.png" {
t.Errorf("Expected iconURL 'https://example.com/icon.png', got '%s'", iconURL)
}
// Test 4: Insert newer manifest with different metadata
_, err = db.Exec(`
INSERT INTO manifests (did, repository, digest, hold_endpoint, schema_version, media_type, created_at,
title, description, source_url, documentation_url, licenses, icon_url)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, testUser.DID, "myapp", "sha256:def456", "did:web:hold.example.com", 2, "application/vnd.oci.image.manifest.v1+json",
time.Now(), // Most recent
"My App v2", "An even cooler application", "https://github.com/user/myapp-v2", "https://v2.docs.example.com", "Apache-2.0", "https://example.com/icon-v2.png")
if err != nil {
t.Fatalf("Failed to insert newer manifest: %v", err)
}
// Test 5: Should return metadata from most recent manifest
title, description, sourceURL, documentationURL, licenses, iconURL, err = GetRepositoryMetadata(db, testUser.DID, "myapp")
if err != nil {
t.Fatalf("Failed to get repository metadata: %v", err)
}
if title != "My App v2" {
t.Errorf("Expected title from newest manifest 'My App v2', got '%s'", title)
}
if description != "An even cooler application" {
t.Errorf("Expected description from newest manifest, got '%s'", description)
}
if licenses != "Apache-2.0" {
t.Errorf("Expected licenses 'Apache-2.0', got '%s'", licenses)
}
// Test 6: Manifest with NULL metadata fields
_, err = db.Exec(`
INSERT INTO manifests (did, repository, digest, hold_endpoint, schema_version, media_type, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
`, testUser.DID, "minimal-app", "sha256:minimal", "did:web:hold.example.com", 2, "application/vnd.oci.image.manifest.v1+json", time.Now())
if err != nil {
t.Fatalf("Failed to insert minimal manifest: %v", err)
}
// Test 7: Should handle NULL fields gracefully
title, description, sourceURL, documentationURL, licenses, iconURL, err = GetRepositoryMetadata(db, testUser.DID, "minimal-app")
if err != nil {
t.Fatalf("Failed to get repository metadata for minimal app: %v", err)
}
if title != "" || description != "" || sourceURL != "" || documentationURL != "" || licenses != "" || iconURL != "" {
t.Error("Expected all empty strings for manifest with NULL metadata fields")
}
}
+14
View File
@@ -134,6 +134,20 @@ func (h *RepositoryPageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
ManifestCount: len(manifests),
}
// Fetch repository metadata from most recent manifest
title, description, sourceURL, documentationURL, licenses, iconURL, err := db.GetRepositoryMetadata(h.DB, owner.DID, repository)
if err != nil {
log.Printf("Failed to fetch repository metadata: %v", err)
// Continue without metadata on error
} else {
repo.Title = title
repo.Description = description
repo.SourceURL = sourceURL
repo.DocumentationURL = documentationURL
repo.Licenses = licenses
repo.IconURL = iconURL
}
// Fetch star count
stats, err := db.GetRepositoryStats(h.DB, owner.DID, repository)
if err != nil {
+17
View File
@@ -251,3 +251,20 @@ func TestGetCacheStats(t *testing.T) {
t.Errorf("Expected unreachable=1, got %v", stats["unreachable"])
}
}
func TestNewWorkerWithStartupDelay(t *testing.T) {
checker := NewChecker(15 * time.Minute)
// Test NewWorker (no delay)
worker := NewWorker(checker, nil, 5*time.Minute)
if worker.startupDelay != 0 {
t.Errorf("Expected startupDelay=0 for NewWorker, got %v", worker.startupDelay)
}
// Test NewWorkerWithStartupDelay
startupDelay := 5 * time.Second
workerWithDelay := NewWorkerWithStartupDelay(checker, nil, 5*time.Minute, startupDelay)
if workerWithDelay.startupDelay != startupDelay {
t.Errorf("Expected startupDelay=%v, got %v", startupDelay, workerWithDelay.startupDelay)
}
}
+27 -1
View File
@@ -22,6 +22,7 @@ type Worker struct {
cleanupTicker *time.Ticker
stopChan chan struct{}
wg sync.WaitGroup
startupDelay time.Duration
}
// NewWorker creates a new background worker
@@ -32,6 +33,19 @@ func NewWorker(checker *Checker, db DBQuerier, refreshInterval time.Duration) *W
refreshTicker: time.NewTicker(refreshInterval),
cleanupTicker: time.NewTicker(30 * time.Minute), // Cleanup every 30 minutes
stopChan: make(chan struct{}),
startupDelay: 0, // No delay by default for backward compatibility
}
}
// NewWorkerWithStartupDelay creates a new background worker with a startup delay
func NewWorkerWithStartupDelay(checker *Checker, db DBQuerier, refreshInterval, startupDelay time.Duration) *Worker {
return &Worker{
checker: checker,
db: db,
refreshTicker: time.NewTicker(refreshInterval),
cleanupTicker: time.NewTicker(30 * time.Minute), // Cleanup every 30 minutes
stopChan: make(chan struct{}),
startupDelay: startupDelay,
}
}
@@ -43,7 +57,19 @@ func (w *Worker) Start(ctx context.Context) {
log.Println("Hold health worker: Starting background health checks")
// Perform initial check immediately
// Wait for services to be ready (Docker startup race condition)
if w.startupDelay > 0 {
log.Printf("Hold health worker: Waiting %s for services to be ready...", w.startupDelay)
select {
case <-time.After(w.startupDelay):
// Continue with initial check
case <-ctx.Done():
log.Println("Hold health worker: Context cancelled during startup delay")
return
}
}
// Perform initial check
w.refreshAllHolds(ctx)
for {