fiber/state.go
Manuel de la Peña 80db4de1a5
🔥 feat: Add Support for service dependencies (#3434)
* 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>
2025-05-19 14:35:13 +02:00

419 lines
11 KiB
Go

package fiber
import (
"encoding/hex"
"strings"
"sync"
"github.com/google/uuid"
)
const servicesStatePrefix = "gofiber-services-"
var servicesStatePrefixHash string
func init() {
servicesStatePrefixHash = hex.EncodeToString([]byte(servicesStatePrefix + uuid.New().String()))
}
// State is a key-value store for Fiber's app in order to be used as a global storage for the app's dependencies.
// It's a thread-safe implementation of a map[string]any, using sync.Map.
type State struct {
dependencies sync.Map
servicePrefix string
}
// NewState creates a new instance of State.
func newState() *State {
// Initialize the services state prefix using a hashed random string
return &State{
dependencies: sync.Map{},
servicePrefix: servicesStatePrefixHash,
}
}
// Set sets a key-value pair in the State.
func (s *State) Set(key string, value any) {
s.dependencies.Store(key, value)
}
// Get retrieves a value from the State.
func (s *State) Get(key string) (any, bool) {
return s.dependencies.Load(key)
}
// MustGet retrieves a value from the State and panics if the key is not found.
func (s *State) MustGet(key string) any {
if dep, ok := s.Get(key); ok {
return dep
}
panic("state: dependency not found!")
}
// Has checks if a key is present in the State.
// It returns a boolean indicating if the key is present.
func (s *State) Has(key string) bool {
_, ok := s.Get(key)
return ok
}
// Delete removes a key-value pair from the State.
func (s *State) Delete(key string) {
s.dependencies.Delete(key)
}
// Reset resets the State by removing all keys.
func (s *State) Reset() {
s.dependencies.Clear()
}
// Keys returns a slice containing all keys present in the State.
func (s *State) Keys() []string {
keys := make([]string, 0)
s.dependencies.Range(func(key, _ any) bool {
keyStr, ok := key.(string)
if !ok {
return false
}
keys = append(keys, keyStr)
return true
})
return keys
}
// Len returns the number of keys in the State.
func (s *State) Len() int {
length := 0
s.dependencies.Range(func(_, _ any) bool {
length++
return true
})
return length
}
// GetState retrieves a value from the State and casts it to the desired type.
// It returns the casted value and a boolean indicating if the cast was successful.
func GetState[T any](s *State, key string) (T, bool) {
dep, ok := s.Get(key)
if ok {
depT, okCast := dep.(T)
return depT, okCast
}
var zeroVal T
return zeroVal, false
}
// MustGetState retrieves a value from the State and casts it to the desired type.
// It panics if the key is not found or if the type assertion fails.
func MustGetState[T any](s *State, key string) T {
dep, ok := GetState[T](s, key)
if !ok {
panic("state: dependency not found!")
}
return dep
}
// GetStateWithDefault retrieves a value from the State,
// casting it to the desired type. If the key is not present,
// it returns the provided default value.
func GetStateWithDefault[T any](s *State, key string, defaultVal T) T {
dep, ok := GetState[T](s, key)
if !ok {
return defaultVal
}
return dep
}
// GetString retrieves a string value from the State.
// It returns the string and a boolean indicating successful type assertion.
func (s *State) GetString(key string) (string, bool) {
dep, ok := s.Get(key)
if ok {
depString, okCast := dep.(string)
return depString, okCast
}
return "", false
}
// GetInt retrieves an integer value from the State.
// It returns the int and a boolean indicating successful type assertion.
func (s *State) GetInt(key string) (int, bool) {
dep, ok := s.Get(key)
if ok {
depInt, okCast := dep.(int)
return depInt, okCast
}
return 0, false
}
// GetBool retrieves a boolean value from the State.
// It returns the bool and a boolean indicating successful type assertion.
func (s *State) GetBool(key string) (value, ok bool) { //nolint:nonamedreturns // Better idea to use named returns here
dep, ok := s.Get(key)
if ok {
depBool, okCast := dep.(bool)
return depBool, okCast
}
return false, false
}
// GetFloat64 retrieves a float64 value from the State.
// It returns the float64 and a boolean indicating successful type assertion.
func (s *State) GetFloat64(key string) (float64, bool) {
dep, ok := s.Get(key)
if ok {
depFloat64, okCast := dep.(float64)
return depFloat64, okCast
}
return 0, false
}
// GetUint retrieves a uint value from the State.
// It returns the float64 and a boolean indicating successful type assertion.
func (s *State) GetUint(key string) (uint, bool) {
dep, ok := s.Get(key)
if ok {
if depUint, okCast := dep.(uint); okCast {
return depUint, true
}
}
return 0, false
}
// GetInt8 retrieves an int8 value from the State.
// It returns the float64 and a boolean indicating successful type assertion.
func (s *State) GetInt8(key string) (int8, bool) {
dep, ok := s.Get(key)
if ok {
if depInt8, okCast := dep.(int8); okCast {
return depInt8, true
}
}
return 0, false
}
// GetInt16 retrieves an int16 value from the State.
// It returns the float64 and a boolean indicating successful type assertion.
func (s *State) GetInt16(key string) (int16, bool) {
dep, ok := s.Get(key)
if ok {
if depInt16, okCast := dep.(int16); okCast {
return depInt16, true
}
}
return 0, false
}
// GetInt32 retrieves an int32 value from the State.
// It returns the float64 and a boolean indicating successful type assertion.
func (s *State) GetInt32(key string) (int32, bool) {
dep, ok := s.Get(key)
if ok {
if depInt32, okCast := dep.(int32); okCast {
return depInt32, true
}
}
return 0, false
}
// GetInt64 retrieves an int64 value from the State.
// It returns the float64 and a boolean indicating successful type assertion.
func (s *State) GetInt64(key string) (int64, bool) {
dep, ok := s.Get(key)
if ok {
if depInt64, okCast := dep.(int64); okCast {
return depInt64, true
}
}
return 0, false
}
// GetUint8 retrieves a uint8 value from the State.
// It returns the float64 and a boolean indicating successful type assertion.
func (s *State) GetUint8(key string) (uint8, bool) {
dep, ok := s.Get(key)
if ok {
if depUint8, okCast := dep.(uint8); okCast {
return depUint8, true
}
}
return 0, false
}
// GetUint16 retrieves a uint16 value from the State.
// It returns the float64 and a boolean indicating successful type assertion.
func (s *State) GetUint16(key string) (uint16, bool) {
dep, ok := s.Get(key)
if ok {
if depUint16, okCast := dep.(uint16); okCast {
return depUint16, true
}
}
return 0, false
}
// GetUint32 retrieves a uint32 value from the State.
// It returns the float64 and a boolean indicating successful type assertion.
func (s *State) GetUint32(key string) (uint32, bool) {
dep, ok := s.Get(key)
if ok {
if depUint32, okCast := dep.(uint32); okCast {
return depUint32, true
}
}
return 0, false
}
// GetUint64 retrieves a uint64 value from the State.
// It returns the float64 and a boolean indicating successful type assertion.
func (s *State) GetUint64(key string) (uint64, bool) {
dep, ok := s.Get(key)
if ok {
if depUint64, okCast := dep.(uint64); okCast {
return depUint64, true
}
}
return 0, false
}
// GetUintptr retrieves a uintptr value from the State.
// It returns the float64 and a boolean indicating successful type assertion.
func (s *State) GetUintptr(key string) (uintptr, bool) {
dep, ok := s.Get(key)
if ok {
if depUintptr, okCast := dep.(uintptr); okCast {
return depUintptr, true
}
}
return 0, false
}
// GetFloat32 retrieves a float32 value from the State.
// It returns the float64 and a boolean indicating successful type assertion.
func (s *State) GetFloat32(key string) (float32, bool) {
dep, ok := s.Get(key)
if ok {
if depFloat32, okCast := dep.(float32); okCast {
return depFloat32, true
}
}
return 0, false
}
// GetComplex64 retrieves a complex64 value from the State.
// It returns the float64 and a boolean indicating successful type assertion.
func (s *State) GetComplex64(key string) (complex64, bool) {
dep, ok := s.Get(key)
if ok {
if depComplex64, okCast := dep.(complex64); okCast {
return depComplex64, true
}
}
return 0, false
}
// GetComplex128 retrieves a complex128 value from the State.
// It returns the float64 and a boolean indicating successful type assertion.
func (s *State) GetComplex128(key string) (complex128, bool) {
dep, ok := s.Get(key)
if ok {
if depComplex128, okCast := dep.(complex128); okCast {
return depComplex128, true
}
}
return 0, false
}
// serviceKey returns a key for a service in the State.
// A key is composed of the State's servicePrefix (hashed) and the hash of the service string.
// This way we can avoid collisions and have a unique key for each service.
func (s *State) serviceKey(key string) string {
// hash the service string to avoid collisions
return s.servicePrefix + hex.EncodeToString([]byte(key))
}
// setService sets a service in the State.
func (s *State) setService(srv Service) {
// Always prepend the service key with the servicesStateKey to avoid collisions
s.Set(s.serviceKey(srv.String()), srv)
}
// Delete removes a key-value pair from the State.
func (s *State) deleteService(srv Service) {
s.Delete(s.serviceKey(srv.String()))
}
// serviceKeys returns a slice containing all keys present for services in the application's State.
func (s *State) serviceKeys() []string {
keys := make([]string, 0)
s.dependencies.Range(func(key, _ any) bool {
keyStr, ok := key.(string)
if !ok {
return false
}
if !strings.HasPrefix(keyStr, s.servicePrefix) {
return true // Continue iterating if key doesn't have service prefix
}
keys = append(keys, keyStr)
return true
})
return keys
}
// Services returns a map containing all services present in the State.
// The key is the hash of the service String() value and the value is the service itself.
func (s *State) Services() map[string]Service {
services := make(map[string]Service)
for _, key := range s.serviceKeys() {
services[key] = MustGetState[Service](s, key)
}
return services
}
// ServicesLen returns the number of keys for services in the State.
func (s *State) ServicesLen() int {
length := 0
s.dependencies.Range(func(key, _ any) bool {
if str, ok := key.(string); ok && strings.HasPrefix(str, s.servicePrefix) {
length++
}
return true
})
return length
}
// GetService returns a service present in the application's State.
func GetService[T Service](s *State, key string) (T, bool) {
srv, ok := GetState[T](s, s.serviceKey(key))
return srv, ok
}
// MustGetService returns a service present in the application's State.
// It panics if the service is not found.
func MustGetService[T Service](s *State, key string) T {
srv, ok := GetService[T](s, key)
if !ok {
panic("state: service not found!")
}
return srv
}