* feat: support for starting devtime dependencies in an abstract manner * feat: support for starting devtime dependencies in an abstract manner * fix: spell * fix: lint * fix: markdown lint * fix: b.Helper * fix: lint spell * fix: field padding * chore: protect the usage of dev dependencies with the "dev" build tag * fix: error message * docs: fix type name * fix: mock context cancellation * docs: simpler * fix: lint unused receiver * fix: handle error in benchmarks * lint: remove build tag * fix: wrap error * fix: lint * fix: explain why lint exclusion * chore: best effort while terminating dependencies * gix: lintern name * fix: reduce flakiness in tests * chore: get dependency state for logs * chore: protect dev time tests and benchmarks under build tag * chore: add build tag in more places * fix: more conservative context cancellation timeout in tests * chore: remove build tags * chore: rename to Services * fix: update tests * fix: lint * fix: lint * fix: apply coderrabit suggestion * chore: add more unit tests * chore: add more unit tests * chore: refactor tests * fix: avoid control flags in tests * chore: consistent error message in start * chore: simplify error logic * chore: remove flag coupling * chore: simplify benchmarks * chore: add corerabbit suggetion * fix: wording * chore: log error on service termination * docs: wording * fix: typo in error message * fix: wording * fix: panic on startup error * chore: store started services separately, so that we can terminate them properly * docs: update example * fix: use context provider instead of storing the context * chore: use require.Empty * fix: no tabs in docs * chore: move field for better alignment * docs: do not use interface as method receiver * docs: proper usage of JSON bind * fix: use startup context for bootstrap log * chore: move happy path to the left * fix: use configured consistently * chore: terminate started services in reverse order * fix: consistent access to the config context * chore: test names and benchmarks location * chore: benchmark refinement * chore: store the services into the global State * chore: add functions to access the Services in the state * chore: hex-encode the hashes * chore: consistent var name for services * chore: non racey service initialisation * fix: wrong range iteration in service keys * fix: use inline * chore: more tests for the generics functions for services * chore: add benchmarks for service functions * fix: benchmarks refactor was wrong * fix. refine error message * fix: do not cause overhead in newState, instead pre-calculate the prefix hash at init * chore: simplify hashing * chore: use smaller, and testable function for initServices * chore: initialize services in the app.init * chore: init services before blocking the app init * Revert "chore: init services before blocking the app init" This reverts commit bb67cf6380cb71ad5ae4ab4807cdfbf0c7eafa1b. * chore: move happy path to the left at initServices * fix: register shutdown hooks for services after app's mutext is unlocked --------- Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com>
10 KiB
id, title, sidebar_position
id | title | sidebar_position |
---|---|---|
services | 🥡 Services | 9 |
Services provide external services needed to run the application, stored as dependencies in the application's State Management. They are supposed to be used while developing and testing the application, being started and stopped automatically by the application.
Once you add a service to the configuration of the GoFiber application, it is automatically started when the application starts and stopped when the application shuts down. You can retrieve the service from the application's State using the GetService
or MustGetService
functions (see State Management).
Service Interface
Service
is an interface that defines the methods for a service.
Definition
// Service is an interface that defines the methods for a service.
type Service interface {
// Start starts the service, returning an error if it fails.
Start(ctx context.Context) error
// String returns a string representation of the service.
// It is used to print a human-readable name of the service in the startup message.
String() string
// State returns the current state of the service.
State(ctx context.Context) (string, error)
// Terminate terminates the service, returning an error if it fails.
Terminate(ctx context.Context) error
}
Methods on the Service
Start
Starts the service, returning an error if it fails. This method is automatically called when the application starts.
func (s *SomeService) Start(ctx context.Context) error
String
Returns a string representation of the service, used to print the service in the startup message.
func (s *SomeService) String() string
State
Returns the current state of the service, used to print the service in the startup message.
func (s *SomeService) State(ctx context.Context) (string, error)
Terminate
Terminate terminates the service after the application shuts down using a post shutdown hook, returning an error if it fails.
func (s *SomeService) Terminate(ctx context.Context) error
Comprehensive Examples
Example: Adding a service
This example demonstrates how to add a Redis store as a service to the application, backed by the Testcontainers Redis Go module.
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/gofiber/fiber/v3"
"github.com/redis/go-redis/v9"
tcredis "github.com/testcontainers/testcontainers-go/modules/redis"
)
const redisServiceName = "redis-store"
type redisService struct {
ctr *tcredis.RedisContainer
}
// Start initializes and starts the service. It implements the [fiber.Service] interface.
func (s *redisService) Start(ctx context.Context) error {
// start the service
c, err := tcredis.Run(ctx, "redis:latest")
if err != nil {
return err
}
s.ctr = c
return nil
}
// String returns a string representation of the service.
// It is used to print a human-readable name of the service in the startup message.
// It implements the [fiber.Service] interface.
func (s *redisService) String() string {
return redisServiceName
}
// State returns the current state of the service.
// It implements the [fiber.Service] interface.
func (s *redisService) State(ctx context.Context) (string, error) {
state, err := s.ctr.State(ctx)
if err != nil {
return "", fmt.Errorf("container state: %w", err)
}
return state.Status, nil
}
// Terminate stops and removes the service. It implements the [fiber.Service] interface.
func (s *redisService) Terminate(ctx context.Context) error {
// stop the service
return s.ctr.Terminate(ctx)
}
func main() {
cfg := &fiber.Config{}
// Initialize service.
cfg.Services = append(cfg.Services, &redisService{})
// Define a context provider for the services startup.
// This is useful to cancel the startup of the services if the context is canceled.
// Default is context.Background().
startupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
cfg.ServicesStartupContextProvider = func() context.Context {
return startupCtx
}
// Define a context provider for the services shutdown.
// This is useful to cancel the shutdown of the services if the context is canceled.
// Default is context.Background().
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
cfg.ServicesShutdownContextProvider = func() context.Context {
return shutdownCtx
}
app := fiber.New(*cfg)
ctx := context.Background()
// Obtain the Redis service from the application's State.
redisSrv, ok := fiber.GetService[*redisService](app.State(), redisServiceName)
if !ok || redisSrv == nil {
log.Printf("Redis service not found")
return
}
// Obtain the connection string from the service.
connString, err := redisSrv.ctr.ConnectionString(ctx)
if err != nil {
log.Printf("Could not get connection string: %v", err)
return
}
// Parse the connection string to create a Redis client.
options, err := redis.ParseURL(connString)
if err != nil {
log.Printf("failed to parse connection string: %s", err)
return
}
// Initialize the Redis client.
rdb := redis.NewClient(options)
// Check the Redis connection.
if err := rdb.Ping(ctx).Err(); err != nil {
log.Fatalf("Could not connect to Redis: %v", err)
}
app.Listen(":3000")
}
Example: Adding a service with a Store Middleware
This example demonstrates how to use Services with the Store Middleware for dependency injection in a Fiber application. It uses a Redis store, backed by the Testcontainers Redis Go module.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"time"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/logger"
redisStore "github.com/gofiber/storage/redis/v3"
"github.com/redis/go-redis/v9"
tcredis "github.com/testcontainers/testcontainers-go/modules/redis"
)
const (
redisServiceName = "redis-store"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
type redisService struct {
ctr *tcredis.RedisContainer
}
// Start initializes and starts the service. It implements the [fiber.Service] interface.
func (s *redisService) Start(ctx context.Context) error {
// start the service
c, err := tcredis.Run(ctx, "redis:latest")
if err != nil {
return err
}
s.ctr = c
return nil
}
// String returns a string representation of the service.
// It is used to print a human-readable name of the service in the startup message.
// It implements the [fiber.Service] interface.
func (s *redisService) String() string {
return redisServiceName
}
// State returns the current state of the service.
// It implements the [fiber.Service] interface.
func (s *redisService) State(ctx context.Context) (string, error) {
state, err := s.ctr.State(ctx)
if err != nil {
return "", fmt.Errorf("container state: %w", err)
}
return state.Status, nil
}
// Terminate stops and removes the service. It implements the [fiber.Service] interface.
func (s *redisService) Terminate(ctx context.Context) error {
// stop the service
return s.ctr.Terminate(ctx)
}
func main() {
cfg := &fiber.Config{}
// Initialize service.
cfg.Services = append(cfg.Services, &redisService{})
// Define a context provider for the services startup.
// This is useful to cancel the startup of the services if the context is canceled.
// Default is context.Background().
startupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
cfg.ServicesStartupContextProvider = func() context.Context {
return startupCtx
}
// Define a context provider for the services shutdown.
// This is useful to cancel the shutdown of the services if the context is canceled.
// Default is context.Background().
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
cfg.ServicesShutdownContextProvider = func() context.Context {
return shutdownCtx
}
app := fiber.New(*cfg)
// Initialize default config
app.Use(logger.New())
ctx := context.Background()
// Obtain the Redis service from the application's State.
redisSrv, ok := fiber.GetService[*redisService](app.State(), redisServiceName)
if !ok || redisSrv == nil {
log.Printf("Redis service not found")
return
}
// Obtain the connection string from the service.
connString, err := redisSrv.ctr.ConnectionString(ctx)
if err != nil {
log.Printf("Could not get connection string: %v", err)
return
}
// define a GoFiber session store, backed by the Redis service
store := redisStore.New(redisStore.Config{
URL: connString,
})
app.Post("/user/create", func(c fiber.Ctx) error {
var user User
if err := c.Bind().JSON(&user); err != nil {
return c.Status(fiber.StatusBadRequest).SendString(err.Error())
}
json, err := json.Marshal(user)
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
// Save the user to the database.
err = store.Set(user.Email, json, time.Hour*24)
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
return c.JSON(user)
})
app.Get("/user/:id", func(c fiber.Ctx) error {
id := c.Params("id")
user, err := store.Get(id)
if err == redis.Nil {
return c.Status(fiber.StatusNotFound).SendString("User not found")
} else if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
return c.JSON(string(user))
})
app.Listen(":3000")
}