network/pkg/gateway/cache_handlers.go
anonpenguin23 3196e91e85
feat: integrate Olric distributed cache support
- Added Olric cache server integration, including configuration options for Olric servers and timeout settings.
- Implemented HTTP handlers for cache operations: health check, get, put, delete, and scan.
- Enhanced Makefile with commands to run the Olric server and manage its configuration.
- Updated README and setup scripts to include Olric installation and configuration instructions.
- Introduced tests for cache handlers to ensure proper functionality and error handling.
2025-11-03 15:30:08 +02:00

357 lines
9.2 KiB
Go

package gateway
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
olriclib "github.com/olric-data/olric"
)
// Cache HTTP handlers for Olric distributed cache
func (g *Gateway) cacheHealthHandler(w http.ResponseWriter, r *http.Request) {
if g.olricClient == nil {
writeError(w, http.StatusServiceUnavailable, "Olric cache client not initialized")
return
}
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
err := g.olricClient.Health(ctx)
if err != nil {
writeError(w, http.StatusServiceUnavailable, fmt.Sprintf("cache health check failed: %v", err))
return
}
writeJSON(w, http.StatusOK, map[string]any{
"status": "ok",
"service": "olric",
})
}
func (g *Gateway) cacheGetHandler(w http.ResponseWriter, r *http.Request) {
if g.olricClient == nil {
writeError(w, http.StatusServiceUnavailable, "Olric cache client not initialized")
return
}
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
var req struct {
DMap string `json:"dmap"` // Distributed map name
Key string `json:"key"` // Key to retrieve
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
if strings.TrimSpace(req.DMap) == "" || strings.TrimSpace(req.Key) == "" {
writeError(w, http.StatusBadRequest, "dmap and key are required")
return
}
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
client := g.olricClient.GetClient()
dm, err := client.NewDMap(req.DMap)
if err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to create DMap: %v", err))
return
}
gr, err := dm.Get(ctx, req.Key)
if err != nil {
if err == olriclib.ErrKeyNotFound {
writeError(w, http.StatusNotFound, "key not found")
return
}
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to get key: %v", err))
return
}
// Try to decode the value from Olric
// Values stored as JSON bytes need to be deserialized, while basic types
// (strings, numbers, bools) can be retrieved directly
var value any
// First, try to get as bytes (for JSON-serialized complex types)
var bytesVal []byte
if err := gr.Scan(&bytesVal); err == nil && len(bytesVal) > 0 {
// Try to deserialize as JSON
var jsonVal any
if err := json.Unmarshal(bytesVal, &jsonVal); err == nil {
value = jsonVal
} else {
// If JSON unmarshal fails, treat as string
value = string(bytesVal)
}
} else {
// Try as string (for simple string values)
if strVal, err := gr.String(); err == nil {
value = strVal
} else {
// Fallback: try to scan as any type
var anyVal any
if err := gr.Scan(&anyVal); err == nil {
value = anyVal
} else {
// Last resort: try String() again, ignoring error
strVal, _ := gr.String()
value = strVal
}
}
}
writeJSON(w, http.StatusOK, map[string]any{
"key": req.Key,
"value": value,
"dmap": req.DMap,
})
}
func (g *Gateway) cachePutHandler(w http.ResponseWriter, r *http.Request) {
if g.olricClient == nil {
writeError(w, http.StatusServiceUnavailable, "Olric cache client not initialized")
return
}
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
var req struct {
DMap string `json:"dmap"` // Distributed map name
Key string `json:"key"` // Key to store
Value any `json:"value"` // Value to store
TTL string `json:"ttl"` // Optional TTL (duration string like "1h", "30m")
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
if strings.TrimSpace(req.DMap) == "" || strings.TrimSpace(req.Key) == "" {
writeError(w, http.StatusBadRequest, "dmap and key are required")
return
}
if req.Value == nil {
writeError(w, http.StatusBadRequest, "value is required")
return
}
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
client := g.olricClient.GetClient()
dm, err := client.NewDMap(req.DMap)
if err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to create DMap: %v", err))
return
}
// TODO: TTL support - need to check Olric v0.7 API for TTL/expiry options
// For now, ignore TTL if provided
if req.TTL != "" {
_, err := time.ParseDuration(req.TTL)
if err != nil {
writeError(w, http.StatusBadRequest, fmt.Sprintf("invalid ttl format: %v", err))
return
}
// TTL parsing succeeded but not yet implemented in API
// Will be added once we confirm the correct Olric API method
}
// Serialize complex types (maps, slices) to JSON bytes for Olric storage
// Olric can handle basic types (string, number, bool) directly, but complex
// types need to be serialized to bytes
var valueToStore any
switch req.Value.(type) {
case map[string]any:
// Serialize maps to JSON bytes
jsonBytes, err := json.Marshal(req.Value)
if err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to marshal value: %v", err))
return
}
valueToStore = jsonBytes
case []any:
// Serialize slices to JSON bytes
jsonBytes, err := json.Marshal(req.Value)
if err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to marshal value: %v", err))
return
}
valueToStore = jsonBytes
case string:
// Basic string type can be stored directly
valueToStore = req.Value
case float64:
// Basic number type can be stored directly
valueToStore = req.Value
case int:
// Basic int type can be stored directly
valueToStore = req.Value
case int64:
// Basic int64 type can be stored directly
valueToStore = req.Value
case bool:
// Basic bool type can be stored directly
valueToStore = req.Value
case nil:
// Nil can be stored directly
valueToStore = req.Value
default:
// For any other type, serialize to JSON to be safe
jsonBytes, err := json.Marshal(req.Value)
if err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to marshal value: %v", err))
return
}
valueToStore = jsonBytes
}
err = dm.Put(ctx, req.Key, valueToStore)
if err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to put key: %v", err))
return
}
writeJSON(w, http.StatusOK, map[string]any{
"status": "ok",
"key": req.Key,
"dmap": req.DMap,
})
}
func (g *Gateway) cacheDeleteHandler(w http.ResponseWriter, r *http.Request) {
if g.olricClient == nil {
writeError(w, http.StatusServiceUnavailable, "Olric cache client not initialized")
return
}
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
var req struct {
DMap string `json:"dmap"` // Distributed map name
Key string `json:"key"` // Key to delete
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
if strings.TrimSpace(req.DMap) == "" || strings.TrimSpace(req.Key) == "" {
writeError(w, http.StatusBadRequest, "dmap and key are required")
return
}
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
client := g.olricClient.GetClient()
dm, err := client.NewDMap(req.DMap)
if err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to create DMap: %v", err))
return
}
deletedCount, err := dm.Delete(ctx, req.Key)
if err != nil {
if err == olriclib.ErrKeyNotFound {
writeError(w, http.StatusNotFound, "key not found")
return
}
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to delete key: %v", err))
return
}
if deletedCount == 0 {
writeError(w, http.StatusNotFound, "key not found")
return
}
writeJSON(w, http.StatusOK, map[string]any{
"status": "ok",
"key": req.Key,
"dmap": req.DMap,
})
}
func (g *Gateway) cacheScanHandler(w http.ResponseWriter, r *http.Request) {
if g.olricClient == nil {
writeError(w, http.StatusServiceUnavailable, "Olric cache client not initialized")
return
}
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
var req struct {
DMap string `json:"dmap"` // Distributed map name
Match string `json:"match"` // Optional regex pattern to match keys
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
if strings.TrimSpace(req.DMap) == "" {
writeError(w, http.StatusBadRequest, "dmap is required")
return
}
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
client := g.olricClient.GetClient()
dm, err := client.NewDMap(req.DMap)
if err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to create DMap: %v", err))
return
}
var iterator olriclib.Iterator
if req.Match != "" {
iterator, err = dm.Scan(ctx, olriclib.Match(req.Match))
} else {
iterator, err = dm.Scan(ctx)
}
if err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to scan: %v", err))
return
}
defer iterator.Close()
var keys []string
for iterator.Next() {
keys = append(keys, iterator.Key())
}
writeJSON(w, http.StatusOK, map[string]any{
"keys": keys,
"count": len(keys),
"dmap": req.DMap,
})
}