Skip to content

Commit

Permalink
gracefull shutdown
Browse files Browse the repository at this point in the history
  • Loading branch information
mstgnz committed Dec 29, 2024
1 parent efcd525 commit eb984ff
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 12 deletions.
72 changes: 62 additions & 10 deletions cmd/main.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package main

import (
"context"
"fmt"
"log"
"os"
"os/signal"
"strings"
"syscall"
"time"

"github.com/fsnotify/fsnotify"
Expand Down Expand Up @@ -34,6 +38,10 @@ func main() {
observability.InitLogger()
logger := observability.Logger()

// Context for graceful shutdown
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

// Tracer
cleanup, initErr := observability.InitTracer("cdn-service", "http://localhost:14268/api/traces")
if initErr != nil {
Expand All @@ -47,7 +55,8 @@ func main() {
}

// watch .env
go watchEnvChanges()
envWatcher := make(chan bool)
go watchEnvChanges(ctx, envWatcher)

awsService = service.NewAwsService()
minioClient = service.MinioClient()
Expand All @@ -70,6 +79,11 @@ func main() {

app := fiber.New(fiber.Config{
BodyLimit: 25 * 1024 * 2014,
// Enable graceful shutdown
DisableStartupMessage: true,
IdleTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
})

// Global rate limiter - 100 requests per minute with IP + Token based protection
Expand Down Expand Up @@ -159,18 +173,55 @@ func main() {
uploadGroup.Post("/upload", AuthMiddleware, imageHandler.UploadImage)
uploadGroup.Post("/upload-url", AuthMiddleware, imageHandler.UploadWithUrl)
uploadGroup.Post("/batch/upload", AuthMiddleware, imageHandler.BatchUpload)
uploadGroup.Post("/batch/delete", AuthMiddleware, imageHandler.BatchDelete)
uploadGroup.Delete("/batch/delete", AuthMiddleware, imageHandler.BatchDelete)
}

// Index
app.Get("/", func(c *fiber.Ctx) error {
return c.SendFile("./public/index.html")
})

port := fmt.Sprintf(":%s", config.GetEnvOrDefault("APP_PORT", "9090"))
if err := app.Listen(port); err != nil {
logger.Fatal().Err(err).Msg("Failed to start server")
// Graceful shutdown setup
shutdownChan := make(chan os.Signal, 1)
signal.Notify(shutdownChan, os.Interrupt, syscall.SIGTERM)

// Start server in a goroutine
go func() {
port := fmt.Sprintf(":%s", config.GetEnvOrDefault("APP_PORT", "9090"))
if err := app.Listen(port); err != nil {
if err.Error() != "server closed" {
logger.Fatal().Err(err).Msg("Failed to start server")
}
}
}()

logger.Info().Msg("Server started successfully")

// Wait for shutdown signal
<-shutdownChan
logger.Info().Msg("Shutting down server...")

// Cancel context to stop background tasks
cancel()

// Stop env watcher
envWatcher <- true

// Shutdown with timeout
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer shutdownCancel()

// Perform cleanup
if err := app.ShutdownWithContext(shutdownCtx); err != nil {
logger.Error().Err(err).Msg("Server shutdown failed")
}

// Close other connections
if err := cacheService.Close(); err != nil {
logger.Error().Err(err).Msg("Cache service shutdown failed")
}

logger.Info().Msg("Server gracefully stopped")
}

func AuthMiddleware(c *fiber.Ctx) error {
Expand All @@ -180,11 +231,8 @@ func AuthMiddleware(c *fiber.Ctx) error {
return c.Next()
}

// Cross-platform file system notifications for Go.
// Q: Watching a file doesn't work well
// A: Watch the parent directory and use Event.Name to filter out files you're not interested in.
// There is an example of this in cmd/fsnotify/file.go.
func watchEnvChanges() {
// watchEnvChanges monitors .env file changes with context support
func watchEnvChanges(ctx context.Context, done chan bool) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatalf("Failed to create watcher: %v", err)
Expand All @@ -198,6 +246,10 @@ func watchEnvChanges() {

for {
select {
case <-ctx.Done():
return
case <-done:
return
case event, ok := <-watcher.Events:
if !ok {
return
Expand Down
27 changes: 25 additions & 2 deletions public/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,16 @@ paths:
path:
type: string
description: Target directory path (optional)
aws_upload:
type: boolean
description: Whether to upload to AWS S3 as well
default: false
height:
type: integer
description: Target height in pixels
width:
type: integer
description: Target width in pixels
responses:
"200":
description: Successful upload
Expand Down Expand Up @@ -245,6 +255,19 @@ paths:
bucket:
type: string
description: Target bucket name
path:
type: string
description: Target directory path (optional)
aws_upload:
type: boolean
description: Whether to upload to AWS S3 as well
default: false
height:
type: integer
description: Target height in pixels
width:
type: integer
description: Target width in pixels
responses:
"200":
description: Successful batch upload
Expand Down Expand Up @@ -311,7 +334,7 @@ paths:
description: Bucket name
- name: path
in: path
required: true
required: false
schema:
type: string
description: File path to delete
Expand Down Expand Up @@ -548,7 +571,7 @@ paths:
$ref: "#/components/schemas/Error"

/batch/delete:
post:
delete:
summary: Batch delete files
description: |
Deletes multiple files in a single request.
Expand Down
23 changes: 23 additions & 0 deletions service/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type CacheService interface {
GetResizedImage(bucket, path string, width, height uint) ([]byte, error)
SetResizedImage(bucket, path string, width, height uint, data []byte) error
FlushAll() error
Close() error
}

type redisCache struct {
Expand Down Expand Up @@ -180,3 +181,25 @@ func (c *redisCache) FlushAll() error {
err = c.client.FlushAll(ctx).Err()
return err
}

func (c *redisCache) Close() error {
start := time.Now()
var err error

defer func() {
duration := time.Since(start).Seconds()
status := "success"
if err != nil {
status = "error"
}
observability.CacheOperations.WithLabelValues("close", status).Inc()
observability.CacheOperationDuration.WithLabelValues("close", status).Observe(duration)

if err != nil {
c.logger.Error().Err(err).Msg("Cache close failed")
}
}()

err = c.client.Close()
return err
}

0 comments on commit eb984ff

Please sign in to comment.