Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions benchmark_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package modular

import (
"context"
"fmt"
"testing"
"time"
)

// --- Benchmark helpers ---

// benchModule is a minimal Module for bootstrap benchmarks.
type benchModule struct{ name string }

func (m *benchModule) Name() string { return m.name }
func (m *benchModule) Init(_ Application) error { return nil }

// benchReloadable is a fast Reloadable for reload benchmarks.
type benchReloadable struct{ name string }

func (m *benchReloadable) Name() string { return m.name }
func (m *benchReloadable) Init(_ Application) error { return nil }
func (m *benchReloadable) Reload(_ context.Context, _ []ConfigChange) error {
return nil
}
func (m *benchReloadable) CanReload() bool { return true }
func (m *benchReloadable) ReloadTimeout() time.Duration { return 5 * time.Second }

// benchLogger is a no-op logger for benchmarks.
type benchLogger struct{}

func (l *benchLogger) Info(_ string, _ ...any) {}
func (l *benchLogger) Error(_ string, _ ...any) {}
func (l *benchLogger) Warn(_ string, _ ...any) {}
func (l *benchLogger) Debug(_ string, _ ...any) {}

// BenchmarkBootstrap measures Init time with 10 modules. Target: <150ms.
func BenchmarkBootstrap(b *testing.B) {
modules := make([]Module, 10)
for i := range modules {
modules[i] = &benchModule{name: fmt.Sprintf("bench-mod-%d", i)}
}

b.ResetTimer()
for b.Loop() {
app, err := NewApplication(
WithLogger(&benchLogger{}),
WithConfigProvider(NewStdConfigProvider(&struct{}{})),
WithModules(modules...),
)
if err != nil {
b.Fatalf("NewApplication failed: %v", err)
}

if err := app.Init(); err != nil {
b.Fatalf("Init failed: %v", err)
}
}
}

// BenchmarkServiceLookup measures service registry lookup. Target: <2us.
func BenchmarkServiceLookup(b *testing.B) {
registry := NewEnhancedServiceRegistry()
_, _ = registry.RegisterService("bench-service", &struct{ Value int }{42})
svcReg := registry.AsServiceRegistry()

b.ResetTimer()
for b.Loop() {
_ = svcReg["bench-service"]
}
}

// BenchmarkReload measures a single reload cycle with 5 modules. Target: <80ms.
func BenchmarkReload(b *testing.B) {
log := &benchLogger{}
orchestrator := NewReloadOrchestrator(log, nil)

for i := 0; i < 5; i++ {
mod := &benchReloadable{name: fmt.Sprintf("reload-mod-%d", i)}
orchestrator.RegisterReloadable(mod.name, mod)
}

diff := ConfigDiff{
Changed: map[string]FieldChange{
"key1": {OldValue: "a", NewValue: "b", FieldPath: "key1", ChangeType: ChangeModified},
},
Added: make(map[string]FieldChange),
Removed: make(map[string]FieldChange),
}

ctx := context.Background()

b.ResetTimer()
for b.Loop() {
req := ReloadRequest{
Trigger: ReloadManual,
Diff: diff,
Ctx: ctx,
}
// Call processReload directly to measure the actual reload cycle
// without channel/goroutine overhead.
if err := orchestrator.processReload(ctx, req); err != nil {
b.Fatalf("processReload failed: %v", err)
}
}
}

// BenchmarkHealthAggregation measures health check aggregation with 10 providers.
// Target: <5ms.
func BenchmarkHealthAggregation(b *testing.B) {
svc := NewAggregateHealthService(WithCacheTTL(0))

for i := 0; i < 10; i++ {
name := fmt.Sprintf("provider-%d", i)
provider := NewSimpleHealthProvider(name, "main", func(_ context.Context) (HealthStatus, string, error) {
return StatusHealthy, "ok", nil
})
svc.AddProvider(name, provider)
}

// Force refresh on every call by using ForceHealthRefreshKey.
ctx := context.WithValue(context.Background(), ForceHealthRefreshKey, true)

b.ResetTimer()
for b.Loop() {
_, err := svc.Check(ctx)
if err != nil {
b.Fatalf("Check failed: %v", err)
}
}
}
33 changes: 33 additions & 0 deletions builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package modular

import (
"context"
"fmt"

cloudevents "github.com/cloudevents/sdk-go/v2"
)
Expand All @@ -21,6 +22,8 @@ type ApplicationBuilder struct {
enableObserver bool
enableTenant bool
configLoadedHooks []func(Application) error // Hooks to run after config loading
tenantGuard *StandardTenantGuard
tenantGuardConfig *TenantGuardConfig
}

// ObserverFunc is a functional observer that can be registered with the application
Expand Down Expand Up @@ -97,6 +100,16 @@ func (b *ApplicationBuilder) Build() (Application, error) {
app = NewObservableDecorator(app, b.observers...)
}

// Create and register tenant guard if configured.
// Use RegisterService so that the EnhancedServiceRegistry (if enabled) tracks
// the entry and subsequent RegisterService calls don't overwrite it.
if b.tenantGuardConfig != nil {
b.tenantGuard = NewStandardTenantGuard(*b.tenantGuardConfig)
if err := app.RegisterService("tenant.guard", b.tenantGuard); err != nil {
return nil, fmt.Errorf("failed to register tenant guard service: %w", err)
}
}

// Register modules
for _, module := range b.modules {
app.RegisterModule(module)
Expand Down Expand Up @@ -194,6 +207,26 @@ func WithOnConfigLoaded(hooks ...func(Application) error) Option {
}
}

// WithTenantGuardMode enables the tenant guard with the specified mode using default config.
func WithTenantGuardMode(mode TenantGuardMode) Option {
return func(b *ApplicationBuilder) error {
if b.tenantGuardConfig == nil {
cfg := DefaultTenantGuardConfig()
b.tenantGuardConfig = &cfg
}
b.tenantGuardConfig.Mode = mode
return nil
}
}

// WithTenantGuardConfig enables the tenant guard with a full configuration.
func WithTenantGuardConfig(config TenantGuardConfig) Option {
return func(b *ApplicationBuilder) error {
b.tenantGuardConfig = &config
return nil
}
}

// Convenience functions for creating common decorators

// InstanceAwareConfig creates an instance-aware configuration decorator
Expand Down
Loading
Loading