feat(T1.8): implement health checks for Kubernetes deployment
- Add internal/health package with health checker - Implement three endpoints: /health, /health/live, /health/ready - /health returns full JSON report with component status, latency, timestamp - /health/live for K8s liveness probe (service running) - /health/ready for K8s readiness probe (ready to accept traffic) - Temporal connectivity check via GetWorkflow call with timeout - Health check caching (30s interval) to prevent excessive checks - Graceful shutdown: health server stops on SIGINT/SIGTERM - Add --health flag to starter command to run health check - Worker runs health server on port 8081 alongside task queue worker - 10/10 unit tests passing - All verification criteria met Closes T1.8
This commit is contained in:
+22
-6
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"go.temporal.io/sdk/client"
|
||||
"github.com/rockliang/poimen/workflows/internal/config"
|
||||
"github.com/rockliang/poimen/workflows/internal/health"
|
||||
"github.com/rockliang/poimen/workflows/statemachine"
|
||||
)
|
||||
|
||||
@@ -22,15 +23,11 @@ func main() {
|
||||
plannerModel = flag.String("planner-model", "ornith", "planner model ID")
|
||||
judgeModel = flag.String("judge-model", "ornith", "judge model ID")
|
||||
implementerModel = flag.String("implementer-model", "claude-sonnet-5", "implementer model ID")
|
||||
healthCheck = flag.Bool("health", false, "check health and exit")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
// Validate required flags
|
||||
if *repoPath == "" || *remoteURL == "" {
|
||||
log.Fatalf("--repo and --remote flags are required")
|
||||
}
|
||||
|
||||
// Load configuration
|
||||
// Load configuration first
|
||||
cfg, err := config.LoadConfig()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to load config: %v", err)
|
||||
@@ -46,6 +43,25 @@ func main() {
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
// If health check requested, do it and exit
|
||||
if *healthCheck {
|
||||
healthChecker := health.NewChecker(c)
|
||||
report := healthChecker.Check(context.Background())
|
||||
jsonReport, _ := report.ToJSON()
|
||||
fmt.Println(string(jsonReport))
|
||||
if report.Status != health.StatusHealthy {
|
||||
log.Fatalf("health check failed")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Validate required flags for workflow start
|
||||
if *repoPath == "" || *remoteURL == "" {
|
||||
log.Fatalf("--repo and --remote flags are required")
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Build OrchestratorInput
|
||||
input := statemachine.OrchestratorInput{
|
||||
TargetRepoPath: *repoPath,
|
||||
|
||||
+54
-3
@@ -1,13 +1,20 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"go.temporal.io/sdk/client"
|
||||
"go.temporal.io/sdk/worker"
|
||||
"github.com/rockliang/poimen/workflows/action"
|
||||
"github.com/rockliang/poimen/workflows/internal/config"
|
||||
"github.com/rockliang/poimen/workflows/internal/health"
|
||||
"github.com/rockliang/poimen/workflows/statemachine"
|
||||
)
|
||||
|
||||
@@ -55,9 +62,53 @@ func main() {
|
||||
// w.RegisterActivity(action.UpdateLessonsActivity)
|
||||
// w.RegisterActivity(action.ReadLessonsActivity)
|
||||
|
||||
// Run worker
|
||||
fmt.Println("Starting worker on queue 'poimen-taskqueue'...")
|
||||
if err := w.Run(worker.InterruptCh()); err != nil {
|
||||
// Initialize health checker
|
||||
healthChecker := health.NewChecker(c)
|
||||
healthHandler := health.NewHandler(healthChecker)
|
||||
|
||||
// Set up HTTP server for health checks
|
||||
mux := http.NewServeMux()
|
||||
healthHandler.RegisterRoutes(mux)
|
||||
|
||||
healthServer := &http.Server{
|
||||
Addr: ":8081",
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
// Start health check server in a goroutine
|
||||
go func() {
|
||||
log.Printf("Health check server listening on %s", healthServer.Addr)
|
||||
if err := healthServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Printf("health check server error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Set up signal handling for graceful shutdown
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
// Run worker in a goroutine
|
||||
workerErrChan := make(chan error, 1)
|
||||
go func() {
|
||||
fmt.Println("Starting worker on queue 'poimen-taskqueue'...")
|
||||
if err := w.Run(worker.InterruptCh()); err != nil {
|
||||
workerErrChan <- err
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for either worker error or signal
|
||||
select {
|
||||
case err := <-workerErrChan:
|
||||
log.Fatalf("worker failed: %v", err)
|
||||
case sig := <-sigChan:
|
||||
log.Printf("received signal: %v, shutting down gracefully", sig)
|
||||
w.Stop()
|
||||
|
||||
// Shutdown health check server
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
if err := healthServer.Shutdown(ctx); err != nil {
|
||||
log.Printf("health check server shutdown error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user