mirror of
https://github.com/gofiber/fiber.git
synced 2025-05-31 11:52:41 +00:00
* 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>
750 lines
18 KiB
Go
750 lines
18 KiB
Go
package fiber
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gofiber/fiber/v3/log"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const (
|
|
terminateErrorMessage = "terminate error"
|
|
startErrorMessage = "start error"
|
|
)
|
|
|
|
// mockService implements Service interface for testing
|
|
type mockService struct {
|
|
startError error
|
|
terminateError error
|
|
stateError error
|
|
name string
|
|
started bool
|
|
terminated bool
|
|
startDelay time.Duration
|
|
terminateDelay time.Duration
|
|
}
|
|
|
|
func (m *mockService) Start(ctx context.Context) error {
|
|
select {
|
|
case <-ctx.Done():
|
|
return fmt.Errorf("context canceled: %w", ctx.Err())
|
|
default:
|
|
}
|
|
|
|
if m.startDelay > 0 {
|
|
timer := time.NewTimer(m.startDelay)
|
|
select {
|
|
case <-ctx.Done():
|
|
timer.Stop()
|
|
return fmt.Errorf("context canceled: %w", ctx.Err())
|
|
case <-timer.C:
|
|
// Continue after delay
|
|
}
|
|
}
|
|
|
|
if m.startError != nil {
|
|
m.started = false
|
|
return m.startError
|
|
}
|
|
|
|
m.started = true
|
|
return nil
|
|
}
|
|
|
|
func (m *mockService) String() string {
|
|
return m.name
|
|
}
|
|
|
|
func (m *mockService) State(ctx context.Context) (string, error) {
|
|
if ctx.Err() != nil {
|
|
return "", fmt.Errorf("context canceled: %w", ctx.Err())
|
|
}
|
|
|
|
if m.stateError != nil {
|
|
return "error", m.stateError
|
|
}
|
|
|
|
if m.started {
|
|
return "running", nil
|
|
}
|
|
|
|
if m.terminated {
|
|
return "stopped", nil
|
|
}
|
|
|
|
return "unknown", nil
|
|
}
|
|
|
|
func (m *mockService) Terminate(ctx context.Context) error {
|
|
select {
|
|
case <-ctx.Done():
|
|
return fmt.Errorf("context canceled: %w", ctx.Err())
|
|
default:
|
|
}
|
|
|
|
if m.terminateDelay > 0 {
|
|
timer := time.NewTimer(m.terminateDelay)
|
|
select {
|
|
case <-ctx.Done():
|
|
timer.Stop()
|
|
return fmt.Errorf("context canceled: %w", ctx.Err())
|
|
case <-timer.C:
|
|
// Continue after delay
|
|
}
|
|
}
|
|
|
|
if m.terminateError != nil {
|
|
m.terminated = false
|
|
return m.terminateError
|
|
}
|
|
|
|
m.started = false
|
|
m.terminated = true
|
|
return nil
|
|
}
|
|
|
|
func Test_HasConfiguredServices(t *testing.T) {
|
|
testHasConfiguredServicesFn := func(t *testing.T, app *App, expected bool) {
|
|
t.Helper()
|
|
|
|
result := app.hasConfiguredServices()
|
|
require.Equal(t, expected, result)
|
|
}
|
|
|
|
t.Run("no-services", func(t *testing.T) {
|
|
testHasConfiguredServicesFn(t, &App{configured: Config{}}, false)
|
|
})
|
|
|
|
t.Run("has-services", func(t *testing.T) {
|
|
testHasConfiguredServicesFn(t, &App{configured: Config{Services: []Service{&mockService{name: "test-dep"}}}}, true)
|
|
})
|
|
}
|
|
|
|
func Test_InitServices(t *testing.T) {
|
|
t.Run("no-services", func(t *testing.T) {
|
|
app := &App{configured: Config{}}
|
|
require.NotPanics(t, app.initServices)
|
|
})
|
|
|
|
t.Run("start/success", func(t *testing.T) {
|
|
// Initialize the app using the struct and defining the state and hooks manually,
|
|
// because we are not checking the shutdown hooks in this test.
|
|
app := &App{
|
|
configured: Config{
|
|
Services: []Service{
|
|
&mockService{name: "dep1"},
|
|
&mockService{name: "dep2"},
|
|
},
|
|
},
|
|
state: newState(),
|
|
}
|
|
|
|
app.hooks = newHooks(app)
|
|
|
|
require.NotPanics(t, app.initServices)
|
|
})
|
|
|
|
t.Run("start/error", func(t *testing.T) {
|
|
// Initialize the app using the struct and defining the state and hooks manually,
|
|
// because we are not checking the shutdown hooks in this test.
|
|
app := &App{
|
|
configured: Config{
|
|
Services: []Service{
|
|
&mockService{name: "dep1", startError: errors.New(startErrorMessage + " 1")},
|
|
&mockService{name: "dep2", startError: errors.New(startErrorMessage + " 2")},
|
|
&mockService{name: "dep3"},
|
|
},
|
|
},
|
|
state: newState(),
|
|
}
|
|
|
|
app.hooks = newHooks(app)
|
|
|
|
require.Panics(t, app.initServices)
|
|
})
|
|
|
|
t.Run("shutdown-hooks/success", func(t *testing.T) {
|
|
// Initialize the app using the New function to verify that the shutdown hooks are registered
|
|
// and the app mutex is not causing a deadlock.
|
|
app := New(Config{
|
|
Services: []Service{&mockService{name: "dep1"}},
|
|
})
|
|
|
|
require.NotPanics(t, app.initServices)
|
|
|
|
type stringsLogger struct {
|
|
strings.Builder
|
|
}
|
|
|
|
var buf stringsLogger
|
|
log.SetOutput(&buf)
|
|
|
|
app.Hooks().executeOnPostShutdownHooks(nil)
|
|
|
|
require.NotContains(t, buf.String(), "failed to call post shutdown hook:")
|
|
})
|
|
|
|
t.Run("shutdown-hooks/error", func(t *testing.T) {
|
|
// Initialize the app using the New function to verify that the shutdown hooks are registered
|
|
// and the app mutex is not causing a deadlock.
|
|
app := New(Config{
|
|
Services: []Service{
|
|
&mockService{name: "dep1"},
|
|
&mockService{name: "dep2", terminateError: errors.New(terminateErrorMessage + " 2")},
|
|
},
|
|
})
|
|
|
|
require.NotPanics(t, app.initServices)
|
|
|
|
type stringsLogger struct {
|
|
strings.Builder
|
|
}
|
|
|
|
var buf stringsLogger
|
|
log.SetOutput(&buf)
|
|
|
|
app.Hooks().executeOnPostShutdownHooks(nil)
|
|
|
|
require.Contains(t, buf.String(), "failed to shutdown services: service dep2 terminate: terminate error 2")
|
|
})
|
|
}
|
|
|
|
func Test_StartServices(t *testing.T) {
|
|
t.Run("no-services", func(t *testing.T) {
|
|
app := &App{
|
|
configured: Config{
|
|
Services: []Service{},
|
|
},
|
|
state: newState(),
|
|
}
|
|
|
|
err := app.startServices(context.Background())
|
|
require.NoError(t, err)
|
|
require.Zero(t, app.state.ServicesLen())
|
|
})
|
|
|
|
t.Run("successful-start", func(t *testing.T) {
|
|
app := &App{
|
|
configured: Config{
|
|
Services: []Service{
|
|
&mockService{name: "dep1"},
|
|
&mockService{name: "dep2"},
|
|
},
|
|
},
|
|
state: newState(),
|
|
}
|
|
|
|
err := app.startServices(context.Background())
|
|
require.NoError(t, err)
|
|
require.Equal(t, 2, app.state.ServicesLen())
|
|
})
|
|
|
|
t.Run("failed-start", func(t *testing.T) {
|
|
app := &App{
|
|
configured: Config{
|
|
Services: []Service{
|
|
&mockService{name: "dep1", startError: errors.New(startErrorMessage + " 1")},
|
|
&mockService{name: "dep2", startError: errors.New(startErrorMessage + " 2")},
|
|
&mockService{name: "dep3"},
|
|
},
|
|
},
|
|
state: newState(),
|
|
}
|
|
|
|
err := app.startServices(context.Background())
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), startErrorMessage+" 1")
|
|
require.Contains(t, err.Error(), startErrorMessage+" 2")
|
|
require.Equal(t, 1, app.state.ServicesLen())
|
|
})
|
|
|
|
t.Run("context", func(t *testing.T) {
|
|
t.Run("already-canceled", func(t *testing.T) {
|
|
app := &App{
|
|
configured: Config{
|
|
Services: []Service{
|
|
&mockService{name: "dep1"},
|
|
},
|
|
},
|
|
state: newState(),
|
|
}
|
|
|
|
// Create a context that is already canceled
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
|
|
err := app.startServices(ctx)
|
|
require.ErrorIs(t, err, context.Canceled)
|
|
require.Zero(t, app.state.ServicesLen())
|
|
})
|
|
|
|
t.Run("cancellation", func(t *testing.T) {
|
|
// Create a service that takes some time to start
|
|
slowDep := &mockService{name: "slow-dep", startDelay: 200 * time.Millisecond}
|
|
|
|
app := &App{
|
|
configured: Config{
|
|
Services: []Service{slowDep},
|
|
},
|
|
state: newState(),
|
|
}
|
|
|
|
// Create a context that will be canceled immediately
|
|
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
|
defer cancel()
|
|
|
|
// Start services with a delay that is longer than the timeout
|
|
err := app.startServices(ctx)
|
|
require.ErrorIs(t, err, context.DeadlineExceeded)
|
|
require.Zero(t, app.state.ServicesLen())
|
|
})
|
|
})
|
|
}
|
|
|
|
func Test_ShutdownServices(t *testing.T) {
|
|
t.Run("no-services", func(t *testing.T) {
|
|
app := &App{
|
|
configured: Config{
|
|
Services: []Service{},
|
|
},
|
|
state: newState(),
|
|
}
|
|
|
|
err := app.shutdownServices(context.Background())
|
|
require.NoError(t, err)
|
|
require.Zero(t, app.state.ServicesLen())
|
|
})
|
|
|
|
t.Run("successful-shutdown", func(t *testing.T) {
|
|
srv1 := &mockService{name: "dep1"}
|
|
srv2 := &mockService{name: "dep2"}
|
|
|
|
// Expected state, including the two started services
|
|
expectedState := newState()
|
|
expectedState.setService(srv1)
|
|
expectedState.setService(srv2)
|
|
|
|
app := &App{
|
|
configured: Config{
|
|
Services: []Service{srv1, srv2},
|
|
},
|
|
state: expectedState,
|
|
}
|
|
|
|
err := app.shutdownServices(context.Background())
|
|
require.NoError(t, err)
|
|
require.Zero(t, app.state.ServicesLen())
|
|
})
|
|
|
|
t.Run("failed-shutdown", func(t *testing.T) {
|
|
srv1 := &mockService{name: "dep1", terminateError: errors.New(terminateErrorMessage + " 1")}
|
|
srv2 := &mockService{name: "dep2", terminateError: errors.New(terminateErrorMessage + " 2")}
|
|
srv3 := &mockService{name: "dep3"}
|
|
|
|
// Expected state, including the two started services
|
|
expectedState := newState()
|
|
expectedState.setService(srv1)
|
|
expectedState.setService(srv2)
|
|
expectedState.setService(srv3)
|
|
|
|
app := &App{
|
|
configured: Config{
|
|
Services: []Service{srv1, srv2, srv3},
|
|
},
|
|
state: expectedState,
|
|
}
|
|
|
|
err := app.shutdownServices(context.Background())
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), terminateErrorMessage+" 1")
|
|
require.Contains(t, err.Error(), terminateErrorMessage+" 2")
|
|
require.Equal(t, 2, app.state.ServicesLen()) // 2 services failed to terminate
|
|
})
|
|
|
|
t.Run("context", func(t *testing.T) {
|
|
t.Run("already-canceled", func(t *testing.T) {
|
|
srv1 := &mockService{name: "dep1"}
|
|
|
|
// Expected state, including the two started services
|
|
expectedState := newState()
|
|
expectedState.setService(srv1)
|
|
|
|
app := &App{
|
|
configured: Config{
|
|
Services: []Service{srv1},
|
|
},
|
|
state: expectedState,
|
|
}
|
|
|
|
// Create a context that is already canceled
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
|
|
err := app.shutdownServices(ctx)
|
|
require.Error(t, err)
|
|
require.ErrorIs(t, err, context.Canceled)
|
|
require.Contains(t, err.Error(), "service dep1 terminate")
|
|
require.Equal(t, 1, app.state.ServicesLen())
|
|
})
|
|
|
|
t.Run("cancellation", func(t *testing.T) {
|
|
// Create a service that takes some time to terminate
|
|
slowDep := &mockService{name: "slow-dep", terminateDelay: 200 * time.Millisecond}
|
|
|
|
// Expected state, including the two started services
|
|
expectedState := newState()
|
|
expectedState.setService(slowDep)
|
|
|
|
app := &App{
|
|
configured: Config{
|
|
Services: []Service{slowDep},
|
|
},
|
|
state: expectedState,
|
|
}
|
|
|
|
// Create a new context for shutdown
|
|
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
|
defer cancel()
|
|
|
|
// Shutdown services with canceled context
|
|
err := app.shutdownServices(ctx)
|
|
require.ErrorIs(t, err, context.DeadlineExceeded)
|
|
require.Equal(t, 1, app.state.ServicesLen())
|
|
})
|
|
})
|
|
}
|
|
|
|
func Test_LogServices(t *testing.T) {
|
|
// Service with successful State
|
|
runningService := &mockService{name: "running", started: true}
|
|
// Service with State error
|
|
errorService := &mockService{name: "error", stateError: errors.New("state error")}
|
|
|
|
expectedState := newState()
|
|
expectedState.setService(runningService)
|
|
expectedState.setService(errorService)
|
|
|
|
app := &App{
|
|
configured: Config{
|
|
Services: []Service{runningService, errorService},
|
|
},
|
|
state: expectedState,
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
|
|
colors := Colors{
|
|
Green: "\033[32m",
|
|
Reset: "\033[0m",
|
|
Blue: "\033[34m",
|
|
Red: "\033[31m",
|
|
}
|
|
|
|
app.logServices(context.Background(), &buf, colors)
|
|
|
|
output := buf.String()
|
|
|
|
for _, srv := range app.state.Services() {
|
|
stateColor := colors.Blue
|
|
state := "RUNNING"
|
|
if _, err := srv.State(context.Background()); err != nil {
|
|
stateColor = colors.Red
|
|
state = "ERROR"
|
|
}
|
|
|
|
expected := fmt.Sprintf("%sINFO%s 🥡 %s[ %s ] %s%s\n", colors.Green, colors.Reset, stateColor, strings.ToUpper(state), srv.String(), colors.Reset)
|
|
require.Contains(t, output, expected)
|
|
}
|
|
}
|
|
|
|
func Test_ServiceContextProviders(t *testing.T) {
|
|
t.Run("no-provider", func(t *testing.T) {
|
|
app := &App{
|
|
configured: Config{},
|
|
state: newState(),
|
|
}
|
|
require.Equal(t, context.Background(), app.servicesStartupCtx())
|
|
require.Equal(t, context.Background(), app.servicesShutdownCtx())
|
|
})
|
|
|
|
t.Run("with-provider", func(t *testing.T) {
|
|
ctx := context.TODO()
|
|
app := &App{
|
|
configured: Config{
|
|
ServicesStartupContextProvider: func() context.Context {
|
|
return ctx
|
|
},
|
|
ServicesShutdownContextProvider: func() context.Context {
|
|
return ctx
|
|
},
|
|
},
|
|
state: newState(),
|
|
}
|
|
|
|
require.Equal(t, ctx, app.servicesStartupCtx())
|
|
require.Equal(t, ctx, app.servicesShutdownCtx())
|
|
})
|
|
}
|
|
|
|
func Benchmark_StartServices(b *testing.B) {
|
|
benchmarkFn := func(b *testing.B, services []Service) {
|
|
b.Helper()
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
app := New(Config{
|
|
Services: services,
|
|
})
|
|
|
|
ctx := context.Background()
|
|
if err := app.startServices(ctx); err != nil {
|
|
b.Fatal("Expected no error but got", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
b.Run("no-services", func(b *testing.B) {
|
|
benchmarkFn(b, []Service{})
|
|
})
|
|
|
|
b.Run("single-service", func(b *testing.B) {
|
|
benchmarkFn(b, []Service{
|
|
&mockService{name: "dep1"},
|
|
})
|
|
})
|
|
|
|
b.Run("multiple-services", func(b *testing.B) {
|
|
benchmarkFn(b, []Service{
|
|
&mockService{name: "dep1"},
|
|
&mockService{name: "dep2"},
|
|
&mockService{name: "dep3"},
|
|
})
|
|
})
|
|
|
|
b.Run("multiple-services-with-delays", func(b *testing.B) {
|
|
benchmarkFn(b, []Service{
|
|
&mockService{name: "dep1", startDelay: 1 * time.Millisecond},
|
|
&mockService{name: "dep2", startDelay: 2 * time.Millisecond},
|
|
&mockService{name: "dep3", startDelay: 3 * time.Millisecond},
|
|
})
|
|
})
|
|
}
|
|
|
|
func Benchmark_ShutdownServices(b *testing.B) {
|
|
benchmarkFn := func(b *testing.B, services []Service) {
|
|
b.Helper()
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
app := New(Config{
|
|
Services: services,
|
|
})
|
|
|
|
ctx := context.Background()
|
|
if err := app.shutdownServices(ctx); err != nil {
|
|
b.Fatal("Expected no error but got", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
b.Run("no-services", func(b *testing.B) {
|
|
benchmarkFn(b, []Service{})
|
|
})
|
|
|
|
b.Run("single-service", func(b *testing.B) {
|
|
benchmarkFn(b, []Service{
|
|
&mockService{name: "dep1"},
|
|
})
|
|
})
|
|
|
|
b.Run("multiple-services", func(b *testing.B) {
|
|
benchmarkFn(b, []Service{
|
|
&mockService{name: "dep1"},
|
|
&mockService{name: "dep2"},
|
|
&mockService{name: "dep3"},
|
|
})
|
|
})
|
|
|
|
b.Run("multiple-services-with-delays", func(b *testing.B) {
|
|
benchmarkFn(b, []Service{
|
|
&mockService{name: "dep1", terminateDelay: 1 * time.Millisecond},
|
|
&mockService{name: "dep2", terminateDelay: 2 * time.Millisecond},
|
|
&mockService{name: "dep3", terminateDelay: 3 * time.Millisecond},
|
|
})
|
|
})
|
|
}
|
|
|
|
func Benchmark_StartServices_withContextCancellation(b *testing.B) {
|
|
benchmarkFn := func(b *testing.B, services []Service, timeout time.Duration) {
|
|
b.Helper()
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
app := New(Config{
|
|
Services: services,
|
|
})
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
err := app.startServices(ctx)
|
|
// We expect an error here due to the short timeout
|
|
if err == nil && timeout < time.Second {
|
|
b.Fatal("Expected error due to context cancellation but got none")
|
|
}
|
|
cancel()
|
|
}
|
|
}
|
|
|
|
b.Run("single-service/immediate-cancellation", func(b *testing.B) {
|
|
benchmarkFn(b, []Service{
|
|
&mockService{name: "dep1", startDelay: 100 * time.Millisecond},
|
|
}, 10*time.Millisecond)
|
|
})
|
|
|
|
b.Run("multiple-services/immediate-cancellation", func(b *testing.B) {
|
|
benchmarkFn(b, []Service{
|
|
&mockService{name: "dep1", startDelay: 100 * time.Millisecond},
|
|
&mockService{name: "dep2", startDelay: 200 * time.Millisecond},
|
|
&mockService{name: "dep3", startDelay: 300 * time.Millisecond},
|
|
}, 10*time.Millisecond)
|
|
})
|
|
|
|
b.Run("multiple-services/successful-completion", func(b *testing.B) {
|
|
const timeout = 500 * time.Millisecond
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
app := New(Config{
|
|
Services: []Service{
|
|
&mockService{name: "dep1", startDelay: 10 * time.Millisecond},
|
|
&mockService{name: "dep2", startDelay: 20 * time.Millisecond},
|
|
&mockService{name: "dep3", startDelay: 30 * time.Millisecond},
|
|
},
|
|
})
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
err := app.startServices(ctx)
|
|
if err != nil {
|
|
b.Fatal("Expected no error but got", err)
|
|
}
|
|
cancel()
|
|
}
|
|
})
|
|
}
|
|
|
|
func Benchmark_ShutdownServices_withContextCancellation(b *testing.B) {
|
|
benchmarkFn := func(b *testing.B, services []Service, timeout time.Duration) {
|
|
b.Helper()
|
|
|
|
b.ResetTimer()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
app := New(Config{
|
|
Services: services,
|
|
})
|
|
|
|
err := app.startServices(context.Background())
|
|
if err != nil {
|
|
b.Fatal("Expected no error during startup but got", err)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
err = app.shutdownServices(ctx)
|
|
// We expect an error here due to the short timeout
|
|
if err == nil && timeout < time.Second {
|
|
b.Fatal("Expected error due to context cancellation but got none")
|
|
}
|
|
cancel()
|
|
}
|
|
}
|
|
|
|
b.Run("single-service/immediate-cancellation", func(b *testing.B) {
|
|
benchmarkFn(b, []Service{
|
|
&mockService{name: "dep1", terminateDelay: 100 * time.Millisecond},
|
|
}, 10*time.Millisecond)
|
|
})
|
|
|
|
b.Run("multiple-services/immediate-cancellation", func(b *testing.B) {
|
|
benchmarkFn(b, []Service{
|
|
&mockService{name: "dep1", terminateDelay: 100 * time.Millisecond},
|
|
&mockService{name: "dep2", terminateDelay: 200 * time.Millisecond},
|
|
&mockService{name: "dep3", terminateDelay: 300 * time.Millisecond},
|
|
}, 10*time.Millisecond)
|
|
})
|
|
|
|
b.Run("multiple-services/successful-completion", func(b *testing.B) {
|
|
const timeout = 500 * time.Millisecond
|
|
|
|
b.ResetTimer()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
app := New(Config{
|
|
Services: []Service{
|
|
&mockService{name: "dep1", terminateDelay: 10 * time.Millisecond},
|
|
&mockService{name: "dep2", terminateDelay: 20 * time.Millisecond},
|
|
&mockService{name: "dep3", terminateDelay: 30 * time.Millisecond},
|
|
},
|
|
})
|
|
|
|
err := app.startServices(context.Background())
|
|
if err != nil {
|
|
b.Fatal("Expected no error but got", err)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
err = app.shutdownServices(ctx)
|
|
if err != nil {
|
|
b.Fatal("Expected no error but got", err)
|
|
}
|
|
cancel()
|
|
}
|
|
})
|
|
}
|
|
|
|
func Benchmark_ServicesMemory(b *testing.B) {
|
|
benchmarkFn := func(b *testing.B, services []Service) {
|
|
b.Helper()
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
var err error
|
|
for i := 0; i < b.N; i++ {
|
|
app := New(Config{
|
|
Services: services,
|
|
})
|
|
|
|
ctx := context.Background()
|
|
err = app.startServices(ctx)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
err = app.shutdownServices(ctx)
|
|
}
|
|
require.NoError(b, err)
|
|
}
|
|
|
|
b.Run("no-services", func(b *testing.B) {
|
|
benchmarkFn(b, []Service{})
|
|
})
|
|
|
|
b.Run("single-service", func(b *testing.B) {
|
|
benchmarkFn(b, []Service{
|
|
&mockService{name: "dep1"},
|
|
})
|
|
})
|
|
|
|
b.Run("multiple-services", func(b *testing.B) {
|
|
benchmarkFn(b, []Service{
|
|
&mockService{name: "dep1"},
|
|
&mockService{name: "dep2"},
|
|
&mockService{name: "dep3"},
|
|
})
|
|
})
|
|
}
|