From a4c46884123f2c6bc0987ab6993053d9468d8aab Mon Sep 17 00:00:00 2001 From: wood chen Date: Sun, 16 Feb 2025 13:57:12 +0800 Subject: [PATCH] feat(cache): Add dynamic cache configuration management - Implement GetConfig and UpdateConfig methods in CacheManager - Add cache configuration endpoints in CacheAdminHandler - Create frontend UI for dynamically updating cache settings - Support configurable max age, cleanup interval, and cache size - Add input validation for cache configuration updates --- internal/cache/manager.go | 152 +++++++++++++++++--------- internal/handler/cache_admin.go | 62 +++++++++++ internal/metrics/collector.go | 17 ++- web/app/dashboard/cache/page.tsx | 116 +++++++++++++++++++- web/app/dashboard/page.tsx | 177 +++---------------------------- web/components/ui/label.tsx | 26 +++++ web/package-lock.json | 24 +++++ web/package.json | 1 + 8 files changed, 360 insertions(+), 215 deletions(-) create mode 100644 web/components/ui/label.tsx diff --git a/internal/cache/manager.go b/internal/cache/manager.go index de3e256..759798f 100644 --- a/internal/cache/manager.go +++ b/internal/cache/manager.go @@ -72,10 +72,12 @@ type CacheManager struct { maxAge time.Duration cleanupTick time.Duration maxCacheSize int64 - enabled atomic.Bool // 缓存开关 - hitCount atomic.Int64 // 命中计数 - missCount atomic.Int64 // 未命中计数 - bytesSaved atomic.Int64 // 节省的带宽 + enabled atomic.Bool // 缓存开关 + hitCount atomic.Int64 // 命中计数 + missCount atomic.Int64 // 未命中计数 + bytesSaved atomic.Int64 // 节省的带宽 + cleanupTimer *time.Ticker // 添加清理定时器 + stopCleanup chan struct{} // 添加停止信号通道 } // NewCacheManager 创建新的缓存管理器 @@ -89,12 +91,13 @@ func NewCacheManager(cacheDir string) (*CacheManager, error) { maxAge: 30 * time.Minute, cleanupTick: 5 * time.Minute, maxCacheSize: 10 * 1024 * 1024 * 1024, // 10GB + stopCleanup: make(chan struct{}), } cm.enabled.Store(true) // 默认启用缓存 // 启动清理协程 - go cm.cleanup() + cm.startCleanup() return cm, nil } @@ -214,60 +217,57 @@ func (cm *CacheManager) Put(key CacheKey, resp *http.Response, body []byte) (*Ca // cleanup 定期清理过期的缓存项 func (cm *CacheManager) cleanup() { - ticker := time.NewTicker(cm.cleanupTick) - for range ticker.C { - var totalSize int64 - var keysToDelete []CacheKey + var totalSize int64 + var keysToDelete []CacheKey - // 收集需要删除的键和计算总大小 + // 收集需要删除的键和计算总大小 + cm.items.Range(func(k, v interface{}) bool { + key := k.(CacheKey) + item := v.(*CacheItem) + totalSize += item.Size + + if time.Since(item.LastAccess) > cm.maxAge { + keysToDelete = append(keysToDelete, key) + } + return true + }) + + // 如果总大小超过限制,按最后访问时间排序删除 + if totalSize > cm.maxCacheSize { + var items []*CacheItem cm.items.Range(func(k, v interface{}) bool { - key := k.(CacheKey) - item := v.(*CacheItem) - totalSize += item.Size - - if time.Since(item.LastAccess) > cm.maxAge { - keysToDelete = append(keysToDelete, key) - } + items = append(items, v.(*CacheItem)) return true }) - // 如果总大小超过限制,按最后访问时间排序删除 - if totalSize > cm.maxCacheSize { - var items []*CacheItem + // 按最后访问时间排序 + sort.Slice(items, func(i, j int) bool { + return items[i].LastAccess.Before(items[j].LastAccess) + }) + + // 删除最旧的项直到总大小小于限制 + for _, item := range items { + if totalSize <= cm.maxCacheSize { + break + } cm.items.Range(func(k, v interface{}) bool { - items = append(items, v.(*CacheItem)) + if v.(*CacheItem) == item { + keysToDelete = append(keysToDelete, k.(CacheKey)) + totalSize -= item.Size + return false + } return true }) - - // 按最后访问时间排序 - sort.Slice(items, func(i, j int) bool { - return items[i].LastAccess.Before(items[j].LastAccess) - }) - - // 删除最旧的项直到总大小小于限制 - for _, item := range items { - if totalSize <= cm.maxCacheSize { - break - } - cm.items.Range(func(k, v interface{}) bool { - if v.(*CacheItem) == item { - keysToDelete = append(keysToDelete, k.(CacheKey)) - totalSize -= item.Size - return false - } - return true - }) - } } + } - // 删除过期和超出大小限制的缓存项 - for _, key := range keysToDelete { - if item, ok := cm.items.Load(key); ok { - cacheItem := item.(*CacheItem) - os.Remove(cacheItem.FilePath) - cm.items.Delete(key) - log.Printf("[Cache] Removed expired item: %s", key.URL) - } + // 删除过期和超出大小限制的缓存项 + for _, key := range keysToDelete { + if item, ok := cm.items.Load(key); ok { + cacheItem := item.(*CacheItem) + os.Remove(cacheItem.FilePath) + cm.items.Delete(key) + log.Printf("[Cache] Removed expired item: %s", key.URL) } } } @@ -408,3 +408,57 @@ func (cm *CacheManager) Commit(key CacheKey, tempPath string, resp *http.Respons log.Printf("[Cache] Cached %s (%s)", key.URL, formatBytes(size)) return nil } + +// GetConfig 获取缓存配置 +func (cm *CacheManager) GetConfig() CacheConfig { + return CacheConfig{ + MaxAge: int64(cm.maxAge.Minutes()), + CleanupTick: int64(cm.cleanupTick.Minutes()), + MaxCacheSize: cm.maxCacheSize / (1024 * 1024 * 1024), // 转换为GB + } +} + +// UpdateConfig 更新缓存配置 +func (cm *CacheManager) UpdateConfig(maxAge, cleanupTick, maxCacheSize int64) error { + if maxAge <= 0 || cleanupTick <= 0 || maxCacheSize <= 0 { + return fmt.Errorf("invalid config values: all values must be positive") + } + + cm.maxAge = time.Duration(maxAge) * time.Minute + cm.maxCacheSize = maxCacheSize * 1024 * 1024 * 1024 // 转换为字节 + + // 如果清理间隔发生变化,重启清理协程 + newCleanupTick := time.Duration(cleanupTick) * time.Minute + if cm.cleanupTick != newCleanupTick { + cm.cleanupTick = newCleanupTick + // 停止当前的清理协程 + cm.stopCleanup <- struct{}{} + // 启动新的清理协程 + cm.startCleanup() + } + + return nil +} + +// CacheConfig 缓存配置结构 +type CacheConfig struct { + MaxAge int64 `json:"max_age"` // 最大缓存时间(分钟) + CleanupTick int64 `json:"cleanup_tick"` // 清理间隔(分钟) + MaxCacheSize int64 `json:"max_cache_size"` // 最大缓存大小(GB) +} + +// startCleanup 启动清理协程 +func (cm *CacheManager) startCleanup() { + cm.cleanupTimer = time.NewTicker(cm.cleanupTick) + go func() { + for { + select { + case <-cm.cleanupTimer.C: + cm.cleanup() + case <-cm.stopCleanup: + cm.cleanupTimer.Stop() + return + } + } + }() +} diff --git a/internal/handler/cache_admin.go b/internal/handler/cache_admin.go index e33cabe..ce36e9b 100644 --- a/internal/handler/cache_admin.go +++ b/internal/handler/cache_admin.go @@ -20,6 +20,13 @@ func NewCacheAdminHandler(proxyCache, mirrorCache, fixedPathCache *cache.CacheMa } } +// CacheConfig 缓存配置结构 +type CacheConfig struct { + MaxAge int64 `json:"max_age"` // 最大缓存时间(分钟) + CleanupTick int64 `json:"cleanup_tick"` // 清理间隔(分钟) + MaxCacheSize int64 `json:"max_cache_size"` // 最大缓存大小(GB) +} + // GetCacheStats 获取缓存统计信息 func (h *CacheAdminHandler) GetCacheStats(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { @@ -37,6 +44,61 @@ func (h *CacheAdminHandler) GetCacheStats(w http.ResponseWriter, r *http.Request json.NewEncoder(w).Encode(stats) } +// GetCacheConfig 获取缓存配置 +func (h *CacheAdminHandler) GetCacheConfig(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + configs := map[string]cache.CacheConfig{ + "proxy": h.proxyCache.GetConfig(), + "mirror": h.mirrorCache.GetConfig(), + "fixedPath": h.fixedPathCache.GetConfig(), + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(configs) +} + +// UpdateCacheConfig 更新缓存配置 +func (h *CacheAdminHandler) UpdateCacheConfig(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + var req struct { + Type string `json:"type"` // "proxy", "mirror" 或 "fixedPath" + Config CacheConfig `json:"config"` // 新的配置 + } + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + var targetCache *cache.CacheManager + switch req.Type { + case "proxy": + targetCache = h.proxyCache + case "mirror": + targetCache = h.mirrorCache + case "fixedPath": + targetCache = h.fixedPathCache + default: + http.Error(w, "Invalid cache type", http.StatusBadRequest) + return + } + + if err := targetCache.UpdateConfig(req.Config.MaxAge, req.Config.CleanupTick, req.Config.MaxCacheSize); err != nil { + http.Error(w, "Failed to update config: "+err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) +} + // SetCacheEnabled 设置缓存开关状态 func (h *CacheAdminHandler) SetCacheEnabled(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { diff --git a/internal/metrics/collector.go b/internal/metrics/collector.go index 7c34920..c3dd788 100644 --- a/internal/metrics/collector.go +++ b/internal/metrics/collector.go @@ -194,6 +194,21 @@ func (c *Collector) RecordRequest(path string, status int, latency time.Duration c.recentRequestsMutex.Unlock() } +// formatUptime 格式化运行时间 +func formatUptime(d time.Duration) string { + days := int(d.Hours()) / 24 + hours := int(d.Hours()) % 24 + minutes := int(d.Minutes()) % 60 + + if days > 0 { + return fmt.Sprintf("%d天%d小时%d分钟", days, hours, minutes) + } + if hours > 0 { + return fmt.Sprintf("%d小时%d分钟", hours, minutes) + } + return fmt.Sprintf("%d分钟", minutes) +} + // GetStats 获取统计数据 func (c *Collector) GetStats() map[string]interface{} { var mem runtime.MemStats @@ -274,7 +289,7 @@ func (c *Collector) GetStats() map[string]interface{} { } return map[string]interface{}{ - "uptime": time.Since(c.startTime).String(), + "uptime": formatUptime(time.Since(c.startTime)), "active_requests": atomic.LoadInt64(&c.activeRequests), "total_requests": atomic.LoadInt64(&c.totalRequests), "total_errors": atomic.LoadInt64(&c.totalErrors), diff --git a/web/app/dashboard/cache/page.tsx b/web/app/dashboard/cache/page.tsx index 95c4dba..f881740 100644 --- a/web/app/dashboard/cache/page.tsx +++ b/web/app/dashboard/cache/page.tsx @@ -5,6 +5,8 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { useToast } from "@/components/ui/use-toast" import { Switch } from "@/components/ui/switch" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" interface CacheStats { total_items: number @@ -16,12 +18,24 @@ interface CacheStats { enabled: boolean } +interface CacheConfig { + max_age: number + cleanup_tick: number + max_cache_size: number +} + interface CacheData { proxy: CacheStats mirror: CacheStats fixedPath: CacheStats } +interface CacheConfigs { + proxy: CacheConfig + mirror: CacheConfig + fixedPath: CacheConfig +} + function formatBytes(bytes: number) { const units = ['B', 'KB', 'MB', 'GB'] let size = bytes @@ -37,6 +51,7 @@ function formatBytes(bytes: number) { export default function CachePage() { const [stats, setStats] = useState(null) + const [configs, setConfigs] = useState(null) const [loading, setLoading] = useState(true) const { toast } = useToast() @@ -57,14 +72,30 @@ export default function CachePage() { } }, [toast]) + const fetchConfigs = useCallback(async () => { + try { + const response = await fetch("/admin/api/cache/config") + if (!response.ok) throw new Error("获取缓存配置失败") + const data = await response.json() + setConfigs(data) + } catch (error) { + toast({ + title: "错误", + description: error instanceof Error ? error.message : "获取缓存配置失败", + variant: "destructive", + }) + } + }, [toast]) + useEffect(() => { // 立即获取一次数据 fetchStats() + fetchConfigs() // 设置定时刷新 const interval = setInterval(fetchStats, 5000) return () => clearInterval(interval) - }, [fetchStats]) + }, [fetchStats, fetchConfigs]) const handleToggleCache = async (type: "proxy" | "mirror" | "fixedPath", enabled: boolean) => { try { @@ -91,6 +122,31 @@ export default function CachePage() { } } + const handleUpdateConfig = async (type: "proxy" | "mirror" | "fixedPath", config: CacheConfig) => { + try { + const response = await fetch("/admin/api/cache/config", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ type, config }), + }) + + if (!response.ok) throw new Error("更新缓存配置失败") + + toast({ + title: "成功", + description: "缓存配置已更新", + }) + + fetchConfigs() + } catch (error) { + toast({ + title: "错误", + description: error instanceof Error ? error.message : "更新缓存配置失败", + variant: "destructive", + }) + } + } + const handleClearCache = async (type: "proxy" | "mirror" | "fixedPath" | "all") => { try { const response = await fetch("/admin/api/cache/clear", { @@ -116,6 +172,61 @@ export default function CachePage() { } } + const renderCacheConfig = (type: "proxy" | "mirror" | "fixedPath") => { + if (!configs) return null + + const config = configs[type] + return ( +
+

缓存配置

+
+
+ + { + const newConfigs = { ...configs } + newConfigs[type].max_age = parseInt(e.target.value) + setConfigs(newConfigs) + }} + onBlur={() => handleUpdateConfig(type, config)} + /> +
+
+ + { + const newConfigs = { ...configs } + newConfigs[type].cleanup_tick = parseInt(e.target.value) + setConfigs(newConfigs) + }} + onBlur={() => handleUpdateConfig(type, config)} + /> +
+
+ + { + const newConfigs = { ...configs } + newConfigs[type].max_cache_size = parseInt(e.target.value) + setConfigs(newConfigs) + }} + onBlur={() => handleUpdateConfig(type, config)} + /> +
+
+
+ ) + } + if (loading) { return (
@@ -181,6 +292,7 @@ export default function CachePage() {
{formatBytes(stats?.proxy.bytes_saved ?? 0)}
+ {renderCacheConfig("proxy")} @@ -229,6 +341,7 @@ export default function CachePage() {
{formatBytes(stats?.mirror.bytes_saved ?? 0)}
+ {renderCacheConfig("mirror")} @@ -277,6 +390,7 @@ export default function CachePage() {
{formatBytes(stats?.fixedPath.bytes_saved ?? 0)}
+ {renderCacheConfig("fixedPath")} diff --git a/web/app/dashboard/page.tsx b/web/app/dashboard/page.tsx index aadcea4..8f9d466 100644 --- a/web/app/dashboard/page.tsx +++ b/web/app/dashboard/page.tsx @@ -1,24 +1,9 @@ "use client" -import { useEffect, useState } from "react" +import { useEffect, useState, useCallback } from "react" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { useToast } from "@/components/ui/use-toast" import { useRouter } from "next/navigation" -import { - BarChart, - Bar, - XAxis, - YAxis, - CartesianGrid, - Tooltip, - Legend, - ResponsiveContainer, - PieChart, - Pie, - Cell, - LineChart, - Line, -} from "recharts" interface Metrics { uptime: string @@ -29,6 +14,7 @@ interface Metrics { memory_usage: string avg_response_time: string requests_per_second: number + bytes_per_second: number status_code_stats: Record top_paths: Array<{ path: string @@ -60,9 +46,6 @@ interface Metrics { total_bytes: number } -// 颜色常量 -const COLORS = ['#0088FE', '#FF8042', '#00C49F', '#FFBB28', '#FF0000'] - export default function DashboardPage() { const [metrics, setMetrics] = useState(null) const [loading, setLoading] = useState(true) @@ -70,7 +53,7 @@ export default function DashboardPage() { const { toast } = useToast() const router = useRouter() - const fetchMetrics = async () => { + const fetchMetrics = useCallback(async () => { try { const token = localStorage.getItem("token") if (!token) { @@ -108,16 +91,13 @@ export default function DashboardPage() { } finally { setLoading(false) } - } + }, [router, toast]) useEffect(() => { - // 立即获取一次数据 fetchMetrics() - - // 设置定时刷新 const interval = setInterval(fetchMetrics, 5000) return () => clearInterval(interval) - }, []) + }, [fetchMetrics]) if (loading) { return ( @@ -176,6 +156,14 @@ export default function DashboardPage() {
错误数
{metrics.total_errors}
+
+
总传输数据
+
{formatBytes(metrics.total_bytes)}
+
+
+
每秒传输数据
+
{formatBytes(metrics.bytes_per_second)}/s
+
@@ -307,145 +295,6 @@ export default function DashboardPage() { - - - - 延迟统计 - - -
-
-
最小响应时间
-
{metrics?.latency_stats?.min}
-
-
-
最大响应时间
-
{metrics?.latency_stats?.max}
-
-
-
平均响应时间
-
{metrics?.avg_response_time}
-
-
-
- - ({ - name, - value, - }))} - margin={{ top: 20, right: 30, left: 20, bottom: 5 }} - > - - - - - - - - -
-
-
- -
- - - 错误分布 - - -
- - - `${name}: ${(percent * 100).toFixed(0)}%`} - outerRadius={80} - fill="#8884d8" - dataKey="value" - > - {[0, 1].map((entry, index) => ( - - ))} - - - - - -
-
-
- - - - 错误类型统计 - - -
- - ({ - name, - value, - }))} - layout="vertical" - margin={{ top: 5, right: 30, left: 120, bottom: 5 }} - > - - - - - - - - -
-
-
-
- - - - 带宽统计 - - -
-
-
当前带宽
-
{metrics?.current_bandwidth}
-
-
-
总传输数据
-
{formatBytes(metrics?.total_bytes || 0)}
-
-
-
- - ({ - time, - value: parseFloat(value.split(' ')[0]), - unit: value.split(' ')[1], - }))} - margin={{ top: 5, right: 30, left: 20, bottom: 5 }} - > - - - - [`${value} ${props.payload.unit}`, '带宽']} /> - - - - -
-
-
) } diff --git a/web/components/ui/label.tsx b/web/components/ui/label.tsx new file mode 100644 index 0000000..5341821 --- /dev/null +++ b/web/components/ui/label.tsx @@ -0,0 +1,26 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } diff --git a/web/package-lock.json b/web/package-lock.json index 91594b6..745d8ca 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -8,6 +8,7 @@ "name": "web", "version": "0.1.0", "dependencies": { + "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-switch": "^1.1.3", "@radix-ui/react-toast": "^1.2.6", @@ -988,6 +989,29 @@ } } }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.2.tgz", + "integrity": "sha512-zo1uGMTaNlHehDyFQcDZXRJhUPDuukcnHz0/jnrup0JA6qL+AFpAnty+7VKa9esuU5xTblAZzTGYJKSKaBxBhw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-portal": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz", diff --git a/web/package.json b/web/package.json index 05d6a88..e736c38 100644 --- a/web/package.json +++ b/web/package.json @@ -9,6 +9,7 @@ "lint": "next lint" }, "dependencies": { + "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-switch": "^1.1.3", "@radix-ui/react-toast": "^1.2.6",