Илья Глазунов bd0b381195 feat(caching): Implement cache hit detection and response header management
- Added functionality to mark responses as cache hits to prevent incorrect X-Cache headers.
- Introduced setCacheHitFlag function to traverse response writer wrappers and set cache hit flag.
- Updated cachingResponseWriter to manage cache hit state and adjust X-Cache header accordingly.
- Enhanced ProcessRequest and ProcessResponse methods to utilize new caching logic.

feat(extension): Introduce ResponseWriterWrapper and ResponseFinalizer interfaces

- Added ResponseWriterWrapper interface for extensions to wrap response writers.
- Introduced ResponseFinalizer interface for finalizing responses after processing.

refactor(manager): Improve response writer wrapping and finalization

- Updated Manager.Handler to wrap response writers through all enabled extensions.
- Implemented finalization of response writers after processing requests.

test(caching): Add comprehensive integration tests for caching behavior

- Created caching_test.go with tests for cache hit/miss, TTL expiration, pattern-based caching, and more.
- Ensured that caching logic works correctly for various scenarios including query strings and error responses.

test(routing): Add integration tests for routing behavior

- Created routing_test.go with tests for route priority, case sensitivity, default routes, and return directives.
- Verified that routing behaves as expected with multiple regex routes and named groups.
2025-12-12 01:03:32 +03:00

272 lines
6.6 KiB
Go

package extension
import (
"context"
"fmt"
"net/http"
"sort"
"sync"
"github.com/konduktor/konduktor/internal/logging"
)
// Manager manages all loaded extensions
type Manager struct {
extensions []Extension
registry map[string]ExtensionFactory
logger *logging.Logger
mu sync.RWMutex
}
// NewManager creates a new extension manager
func NewManager(logger *logging.Logger) *Manager {
m := &Manager{
extensions: make([]Extension, 0),
registry: make(map[string]ExtensionFactory),
logger: logger,
}
// Register built-in extensions
m.RegisterFactory("routing", NewRoutingExtension)
m.RegisterFactory("security", NewSecurityExtension)
m.RegisterFactory("caching", NewCachingExtension)
return m
}
// RegisterFactory registers an extension factory
func (m *Manager) RegisterFactory(name string, factory ExtensionFactory) {
m.mu.Lock()
defer m.mu.Unlock()
m.registry[name] = factory
}
// LoadExtension loads an extension by type and config
func (m *Manager) LoadExtension(extType string, config map[string]interface{}) error {
m.mu.Lock()
defer m.mu.Unlock()
factory, ok := m.registry[extType]
if !ok {
return fmt.Errorf("unknown extension type: %s", extType)
}
ext, err := factory(config, m.logger)
if err != nil {
return fmt.Errorf("failed to create extension %s: %w", extType, err)
}
if err := ext.Initialize(); err != nil {
return fmt.Errorf("failed to initialize extension %s: %w", extType, err)
}
m.extensions = append(m.extensions, ext)
// Sort by priority (lower first)
sort.Slice(m.extensions, func(i, j int) bool {
return m.extensions[i].Priority() < m.extensions[j].Priority()
})
m.logger.Info("Loaded extension", "type", extType, "name", ext.Name(), "priority", ext.Priority())
return nil
}
// AddExtension adds a pre-created extension
func (m *Manager) AddExtension(ext Extension) error {
m.mu.Lock()
defer m.mu.Unlock()
if err := ext.Initialize(); err != nil {
return fmt.Errorf("failed to initialize extension %s: %w", ext.Name(), err)
}
m.extensions = append(m.extensions, ext)
// Sort by priority
sort.Slice(m.extensions, func(i, j int) bool {
return m.extensions[i].Priority() < m.extensions[j].Priority()
})
m.logger.Info("Added extension", "name", ext.Name(), "priority", ext.Priority())
return nil
}
// ProcessRequest runs all extensions' ProcessRequest in order
// Returns true if any extension handled the request
func (m *Manager) ProcessRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
m.mu.RLock()
extensions := m.extensions
m.mu.RUnlock()
for _, ext := range extensions {
if !ext.Enabled() {
continue
}
handled, err := ext.ProcessRequest(ctx, w, r)
if err != nil {
m.logger.Error("Extension error", "extension", ext.Name(), "error", err)
// Continue to next extension on error
continue
}
if handled {
return true, nil
}
}
return false, nil
}
// ProcessResponse runs all extensions' ProcessResponse in reverse order
func (m *Manager) ProcessResponse(ctx context.Context, w http.ResponseWriter, r *http.Request) {
m.mu.RLock()
extensions := m.extensions
m.mu.RUnlock()
// Process in reverse order for response
for i := len(extensions) - 1; i >= 0; i-- {
ext := extensions[i]
if !ext.Enabled() {
continue
}
ext.ProcessResponse(ctx, w, r)
}
}
// Cleanup cleans up all extensions
func (m *Manager) Cleanup() {
m.mu.Lock()
defer m.mu.Unlock()
for _, ext := range m.extensions {
if err := ext.Cleanup(); err != nil {
m.logger.Error("Extension cleanup error", "extension", ext.Name(), "error", err)
}
}
m.extensions = nil
}
// GetExtension returns an extension by name
func (m *Manager) GetExtension(name string) Extension {
m.mu.RLock()
defer m.mu.RUnlock()
for _, ext := range m.extensions {
if ext.Name() == name {
return ext
}
}
return nil
}
// Extensions returns all loaded extensions
func (m *Manager) Extensions() []Extension {
m.mu.RLock()
defer m.mu.RUnlock()
result := make([]Extension, len(m.extensions))
copy(result, m.extensions)
return result
}
// Handler returns an http.Handler that processes requests through all extensions
func (m *Manager) Handler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// Wrap response writer through all extensions that support it
// Process in reverse priority order so highest priority wrapper is outermost
wrappedWriter := w
var finalizers []ResponseFinalizer
m.mu.RLock()
extensions := m.extensions
m.mu.RUnlock()
// Wrap response writer (lowest priority first, so they wrap in correct order)
for _, ext := range extensions {
if !ext.Enabled() {
continue
}
if wrapper, ok := ext.(ResponseWriterWrapper); ok {
wrappedWriter = wrapper.WrapResponseWriter(wrappedWriter, r)
// Check if the wrapped writer implements Finalizer
if finalizer, ok := wrappedWriter.(ResponseFinalizer); ok {
finalizers = append(finalizers, finalizer)
}
}
}
// Create response wrapper to capture status code
responseWrapper := newResponseWrapper(wrappedWriter)
// Process request through extensions
handled, err := m.ProcessRequest(ctx, responseWrapper, r)
if err != nil {
m.logger.Error("Error processing request", "error", err)
}
if handled {
// Extension handled the request, process response
m.ProcessResponse(ctx, responseWrapper, r)
// Finalize all response writers
for i := len(finalizers) - 1; i >= 0; i-- {
finalizers[i].Finalize()
}
return
}
// No extension handled, pass to next handler
next.ServeHTTP(responseWrapper, r)
// Process response
m.ProcessResponse(ctx, responseWrapper, r)
// Finalize all response writers
for i := len(finalizers) - 1; i >= 0; i-- {
finalizers[i].Finalize()
}
})
}
// responseWrapper wraps http.ResponseWriter to allow response modification
type responseWrapper struct {
http.ResponseWriter
statusCode int
written bool
}
func newResponseWrapper(w http.ResponseWriter) *responseWrapper {
return &responseWrapper{
ResponseWriter: w,
statusCode: http.StatusOK,
}
}
func (rw *responseWrapper) WriteHeader(code int) {
if !rw.written {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
rw.written = true
}
}
func (rw *responseWrapper) Write(b []byte) (int, error) {
if !rw.written {
rw.WriteHeader(http.StatusOK)
}
return rw.ResponseWriter.Write(b)
}
func (rw *responseWrapper) StatusCode() int {
return rw.statusCode
}
// Unwrap returns the underlying ResponseWriter (for type assertions)
func (rw *responseWrapper) Unwrap() http.ResponseWriter {
return rw.ResponseWriter
}