Refactor caching functions and simplify response serialization
All checks were successful
Release Tag / release (push) Successful in 27s
All checks were successful
Release Tag / release (push) Successful in 27s
- Updated the `downloadThroughCache` function to remove the upstream URL parameter, streamlining the caching process. - Modified the `serializeRawResponse` function to eliminate unnecessary parameters, enhancing clarity and usability. - Adjusted integration tests to align with the new function signatures, ensuring consistent testing of caching behavior.
This commit is contained in:
@@ -52,7 +52,7 @@ func testSteamURL(t *testing.T, urlPath string) {
|
|||||||
directResp, directBody := downloadDirectly(t, steamURL)
|
directResp, directBody := downloadDirectly(t, steamURL)
|
||||||
|
|
||||||
// Test download through SteamCache
|
// Test download through SteamCache
|
||||||
cacheResp, cacheBody := downloadThroughCache(t, sc, steamURL, urlPath)
|
cacheResp, cacheBody := downloadThroughCache(t, sc, urlPath)
|
||||||
|
|
||||||
// Compare responses
|
// Compare responses
|
||||||
compareResponses(t, directResp, directBody, cacheResp, cacheBody, urlPath)
|
compareResponses(t, directResp, directBody, cacheResp, cacheBody, urlPath)
|
||||||
@@ -83,7 +83,7 @@ func downloadDirectly(t *testing.T, url string) (*http.Response, []byte) {
|
|||||||
return resp, body
|
return resp, body
|
||||||
}
|
}
|
||||||
|
|
||||||
func downloadThroughCache(t *testing.T, sc *SteamCache, upstreamURL, urlPath string) (*http.Response, []byte) {
|
func downloadThroughCache(t *testing.T, sc *SteamCache, urlPath string) (*http.Response, []byte) {
|
||||||
// Create a test server for SteamCache
|
// Create a test server for SteamCache
|
||||||
cacheServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
cacheServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
// For real Steam URLs, we need to set the upstream to the Steam hostname
|
// For real Steam URLs, we need to set the upstream to the Steam hostname
|
||||||
@@ -235,7 +235,7 @@ func TestCacheFileFormat(t *testing.T) {
|
|||||||
rawResponse := sc.reconstructRawResponse(resp, bodyData)
|
rawResponse := sc.reconstructRawResponse(resp, bodyData)
|
||||||
|
|
||||||
// Serialize to cache format
|
// Serialize to cache format
|
||||||
cacheData, err := serializeRawResponse("/test/format", rawResponse, contentHash, "sha256")
|
cacheData, err := serializeRawResponse(rawResponse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to serialize cache file: %v", err)
|
t.Fatalf("Failed to serialize cache file: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ package steamcache
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha1"
|
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -140,7 +139,7 @@ type CacheFileFormat struct {
|
|||||||
|
|
||||||
// serializeRawResponse serializes a raw HTTP response into our text-based cache format
|
// serializeRawResponse serializes a raw HTTP response into our text-based cache format
|
||||||
// upstreamHash and upstreamAlgo are used for verification during download but not stored
|
// upstreamHash and upstreamAlgo are used for verification during download but not stored
|
||||||
func serializeRawResponse(url string, rawResponse []byte, upstreamHash string, upstreamAlgo string) ([]byte, error) {
|
func serializeRawResponse(rawResponse []byte) ([]byte, error) {
|
||||||
// Extract body from raw response for hash calculation
|
// Extract body from raw response for hash calculation
|
||||||
bodyStart := bytes.Index(rawResponse, []byte("\r\n\r\n"))
|
bodyStart := bytes.Index(rawResponse, []byte("\r\n\r\n"))
|
||||||
if bodyStart == -1 {
|
if bodyStart == -1 {
|
||||||
@@ -514,178 +513,6 @@ func calculateSHA256(data []byte) string {
|
|||||||
return hex.EncodeToString(hasher.Sum(nil))
|
return hex.EncodeToString(hasher.Sum(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculateSHA1 calculates SHA1 hash of the given data (for legacy verification only)
|
|
||||||
func calculateSHA1(data []byte) string {
|
|
||||||
hasher := sha1.New()
|
|
||||||
hasher.Write(data)
|
|
||||||
return hex.EncodeToString(hasher.Sum(nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractHashFromURL extracts hash from URL path (Steam chunk URLs contain SHA1 hashes)
|
|
||||||
func extractHashFromURL(urlPath string) (hash string, algorithm string) {
|
|
||||||
// Steam chunk URLs: /depot/123/chunk/SHA1_HASH
|
|
||||||
// Steam manifest URLs: /depot/123/manifest/.../SHA1_HASH
|
|
||||||
// Steam patch URLs: /depot/123/patch/.../SHA1_HASH
|
|
||||||
|
|
||||||
// Look for chunk URLs with SHA1 hash
|
|
||||||
if strings.Contains(urlPath, "/chunk/") {
|
|
||||||
parts := strings.Split(urlPath, "/chunk/")
|
|
||||||
if len(parts) == 2 {
|
|
||||||
hashPart := parts[1]
|
|
||||||
// Remove any query parameters
|
|
||||||
if questionMark := strings.Index(hashPart, "?"); questionMark != -1 {
|
|
||||||
hashPart = hashPart[:questionMark]
|
|
||||||
}
|
|
||||||
// Check if it's a valid SHA1 hash (40 hex chars)
|
|
||||||
if len(hashPart) == 40 && isHexString(hashPart) {
|
|
||||||
logger.Logger.Debug().
|
|
||||||
Str("url_path", urlPath).
|
|
||||||
Str("sha1_hash", hashPart).
|
|
||||||
Msg("Extracted SHA1 hash from Steam chunk URL")
|
|
||||||
return hashPart, "sha1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look for manifest URLs with SHA1 hash at the end
|
|
||||||
if strings.Contains(urlPath, "/manifest/") {
|
|
||||||
parts := strings.Split(urlPath, "/")
|
|
||||||
if len(parts) > 0 {
|
|
||||||
lastPart := parts[len(parts)-1]
|
|
||||||
// Remove any query parameters
|
|
||||||
if questionMark := strings.Index(lastPart, "?"); questionMark != -1 {
|
|
||||||
lastPart = lastPart[:questionMark]
|
|
||||||
}
|
|
||||||
// Check if it's a valid SHA1 hash (40 hex chars)
|
|
||||||
if len(lastPart) == 40 && isHexString(lastPart) {
|
|
||||||
logger.Logger.Debug().
|
|
||||||
Str("url_path", urlPath).
|
|
||||||
Str("sha1_hash", lastPart).
|
|
||||||
Msg("Extracted SHA1 hash from Steam manifest URL")
|
|
||||||
return lastPart, "sha1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look for patch URLs with SHA1 hash at the end
|
|
||||||
if strings.Contains(urlPath, "/patch/") {
|
|
||||||
parts := strings.Split(urlPath, "/")
|
|
||||||
if len(parts) > 0 {
|
|
||||||
lastPart := parts[len(parts)-1]
|
|
||||||
// Remove any query parameters
|
|
||||||
if questionMark := strings.Index(lastPart, "?"); questionMark != -1 {
|
|
||||||
lastPart = lastPart[:questionMark]
|
|
||||||
}
|
|
||||||
// Check if it's a valid SHA1 hash (40 hex chars)
|
|
||||||
if len(lastPart) == 40 && isHexString(lastPart) {
|
|
||||||
logger.Logger.Debug().
|
|
||||||
Str("url_path", urlPath).
|
|
||||||
Str("sha1_hash", lastPart).
|
|
||||||
Msg("Extracted SHA1 hash from Steam patch URL")
|
|
||||||
return lastPart, "sha1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractUpstreamHash extracts hash from upstream server headers and URL path, prioritizing by security
|
|
||||||
// Returns the hash value and the algorithm used (sha256, sha1, or empty if none found)
|
|
||||||
func extractUpstreamHash(headers http.Header, urlPath string) (hash string, algorithm string) {
|
|
||||||
// Priority order: SHA256 (most secure) -> SHA1 (legacy) -> none
|
|
||||||
|
|
||||||
// 1. Try SHA256 headers first (highest priority)
|
|
||||||
sha256Headers := []string{
|
|
||||||
"X-SHA256", // Custom header
|
|
||||||
"Content-SHA256", // Content hash
|
|
||||||
"X-Content-SHA256", // Service specific
|
|
||||||
"Digest", // RFC 3230 digest header
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, headerName := range sha256Headers {
|
|
||||||
if value := headers.Get(headerName); value != "" {
|
|
||||||
// Remove quotes if present (ETag often has quotes)
|
|
||||||
value = strings.Trim(value, `"`)
|
|
||||||
|
|
||||||
// Check for SHA256 prefix in Digest header
|
|
||||||
if strings.HasPrefix(value, "sha256=") {
|
|
||||||
hash := strings.TrimPrefix(value, "sha256=")
|
|
||||||
if len(hash) == 64 && isHexString(hash) {
|
|
||||||
logger.Logger.Debug().
|
|
||||||
Str("header_name", headerName).
|
|
||||||
Str("sha256_hash", hash).
|
|
||||||
Msg("Extracted SHA256 hash from upstream header")
|
|
||||||
return hash, "sha256"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Direct SHA256 hash (64 chars)
|
|
||||||
if len(value) == 64 && isHexString(value) {
|
|
||||||
logger.Logger.Debug().
|
|
||||||
Str("header_name", headerName).
|
|
||||||
Str("sha256_hash", value).
|
|
||||||
Msg("Extracted SHA256 hash from upstream header")
|
|
||||||
return value, "sha256"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Fallback to SHA1 headers (legacy support)
|
|
||||||
sha1Headers := []string{
|
|
||||||
"X-SHA1", // Legacy custom header
|
|
||||||
"Content-SHA1", // Legacy content hash
|
|
||||||
"X-Content-SHA1", // Legacy Steam specific
|
|
||||||
"X-Content-Sha", // Legacy Steam specific (lowercase variant)
|
|
||||||
"ETag", // May contain SHA1
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, headerName := range sha1Headers {
|
|
||||||
if value := headers.Get(headerName); value != "" {
|
|
||||||
// Remove quotes if present (ETag often has quotes)
|
|
||||||
value = strings.Trim(value, `"`)
|
|
||||||
|
|
||||||
// Check for SHA1 prefix in Digest header
|
|
||||||
if strings.HasPrefix(value, "sha1=") {
|
|
||||||
hash := strings.TrimPrefix(value, "sha1=")
|
|
||||||
if len(hash) == 40 && isHexString(hash) {
|
|
||||||
logger.Logger.Debug().
|
|
||||||
Str("header_name", headerName).
|
|
||||||
Str("sha1_hash", hash).
|
|
||||||
Msg("Extracted SHA1 hash from upstream header (legacy)")
|
|
||||||
return hash, "sha1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Direct SHA1 hash (40 chars)
|
|
||||||
if len(value) == 40 && isHexString(value) {
|
|
||||||
logger.Logger.Debug().
|
|
||||||
Str("header_name", headerName).
|
|
||||||
Str("sha1_hash", value).
|
|
||||||
Msg("Extracted SHA1 hash from upstream header (legacy)")
|
|
||||||
return value, "sha1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Fallback to URL path extraction (Steam chunk URLs)
|
|
||||||
urlHash, urlAlgo := extractHashFromURL(urlPath)
|
|
||||||
if urlHash != "" {
|
|
||||||
return urlHash, urlAlgo
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Logger.Debug().Msg("No upstream hash found in headers or URL")
|
|
||||||
return "", ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// isHexString checks if a string contains only hexadecimal characters
|
|
||||||
func isHexString(s string) bool {
|
|
||||||
for _, r := range s {
|
|
||||||
if !((r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F')) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// verifyCompleteFile verifies that we received the complete file by checking Content-Length
|
// verifyCompleteFile verifies that we received the complete file by checking Content-Length
|
||||||
// Returns true if the file is complete, false if it's incomplete (allowing retry)
|
// Returns true if the file is complete, false if it's incomplete (allowing retry)
|
||||||
func (sc *SteamCache) verifyCompleteFile(bodyData []byte, resp *http.Response, urlPath string, cacheKey string) bool {
|
func (sc *SteamCache) verifyCompleteFile(bodyData []byte, resp *http.Response, urlPath string, cacheKey string) bool {
|
||||||
@@ -1547,7 +1374,7 @@ func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Serialize the raw response using our new cache format
|
// Serialize the raw response using our new cache format
|
||||||
cacheData, err := serializeRawResponse(urlPath, rawResponse, "", "")
|
cacheData, err := serializeRawResponse(rawResponse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Logger.Warn().
|
logger.Logger.Warn().
|
||||||
Str("key", cacheKey).
|
Str("key", cacheKey).
|
||||||
@@ -1711,131 +1538,3 @@ func (sc *SteamCache) recordCacheMiss(key string, size int64) {
|
|||||||
sc.cacheWarmer.RequestWarming(key, 3, "cache_miss", size, "cache_miss_analyzer")
|
sc.cacheWarmer.RequestWarming(key, 3, "cache_miss", size, "cache_miss_analyzer")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAdaptiveStats returns adaptive caching statistics
|
|
||||||
func (sc *SteamCache) GetAdaptiveStats() map[string]interface{} {
|
|
||||||
stats := make(map[string]interface{})
|
|
||||||
|
|
||||||
// Get current strategy
|
|
||||||
currentStrategy := sc.adaptiveManager.GetCurrentStrategy()
|
|
||||||
stats["current_strategy"] = currentStrategy
|
|
||||||
stats["adaptation_count"] = sc.adaptiveManager.GetAdaptationCount()
|
|
||||||
|
|
||||||
// Get dominant pattern (using public method)
|
|
||||||
// Note: In a real implementation, we'd need a public method to get the dominant pattern
|
|
||||||
stats["dominant_pattern"] = "unknown" // Placeholder for now
|
|
||||||
|
|
||||||
return stats
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPredictiveStats returns predictive caching statistics
|
|
||||||
func (sc *SteamCache) GetPredictiveStats() map[string]interface{} {
|
|
||||||
stats := make(map[string]interface{})
|
|
||||||
|
|
||||||
predictiveStats := sc.predictiveManager.GetStats()
|
|
||||||
stats["prefetch_hits"] = predictiveStats.PrefetchHits
|
|
||||||
stats["prefetch_misses"] = predictiveStats.PrefetchMisses
|
|
||||||
stats["prefetch_requests"] = predictiveStats.PrefetchRequests
|
|
||||||
stats["cache_warm_hits"] = predictiveStats.CacheWarmHits
|
|
||||||
stats["cache_warm_misses"] = predictiveStats.CacheWarmMisses
|
|
||||||
|
|
||||||
return stats
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetWarmingStats returns cache warming statistics
|
|
||||||
func (sc *SteamCache) GetWarmingStats() map[string]interface{} {
|
|
||||||
stats := make(map[string]interface{})
|
|
||||||
|
|
||||||
warmingStats := sc.cacheWarmer.GetStats()
|
|
||||||
stats["warm_requests"] = warmingStats.WarmRequests
|
|
||||||
stats["warm_successes"] = warmingStats.WarmSuccesses
|
|
||||||
stats["warm_failures"] = warmingStats.WarmFailures
|
|
||||||
stats["warm_bytes"] = warmingStats.WarmBytes
|
|
||||||
stats["warm_duration"] = warmingStats.WarmDuration
|
|
||||||
stats["active_warmers"] = warmingStats.ActiveWarmers
|
|
||||||
stats["warming_enabled"] = sc.cacheWarmer.IsWarmingEnabled()
|
|
||||||
|
|
||||||
return stats
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetWarmingEnabled enables or disables cache warming
|
|
||||||
func (sc *SteamCache) SetWarmingEnabled(enabled bool) {
|
|
||||||
sc.cacheWarmer.SetWarmingEnabled(enabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WarmPopularContent manually triggers warming of popular content
|
|
||||||
func (sc *SteamCache) WarmPopularContent(keys []string) {
|
|
||||||
sc.cacheWarmer.WarmPopularContent(keys, 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WarmPredictedContent manually triggers warming of predicted content
|
|
||||||
func (sc *SteamCache) WarmPredictedContent(keys []string) {
|
|
||||||
sc.cacheWarmer.WarmPredictedContent(keys, 3)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetAdaptiveEnabled enables or disables adaptive features
|
|
||||||
func (sc *SteamCache) SetAdaptiveEnabled(enabled bool) {
|
|
||||||
sc.adaptiveEnabled = enabled
|
|
||||||
if !enabled {
|
|
||||||
// Stop adaptive components when disabled
|
|
||||||
sc.adaptiveManager.Stop()
|
|
||||||
sc.predictiveManager.Stop()
|
|
||||||
sc.cacheWarmer.Stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsAdaptiveEnabled returns whether adaptive features are enabled
|
|
||||||
func (sc *SteamCache) IsAdaptiveEnabled() bool {
|
|
||||||
return sc.adaptiveEnabled
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMemoryStats returns memory monitoring statistics
|
|
||||||
func (sc *SteamCache) GetMemoryStats() map[string]interface{} {
|
|
||||||
if sc.memoryMonitor == nil {
|
|
||||||
return map[string]interface{}{"error": "memory monitoring not enabled"}
|
|
||||||
}
|
|
||||||
|
|
||||||
stats := sc.memoryMonitor.GetMemoryStats()
|
|
||||||
|
|
||||||
if sc.dynamicCacheMgr != nil {
|
|
||||||
dynamicStats := sc.dynamicCacheMgr.GetStats()
|
|
||||||
for k, v := range dynamicStats {
|
|
||||||
stats["dynamic_"+k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return stats
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDynamicCacheStats returns dynamic cache management statistics
|
|
||||||
func (sc *SteamCache) GetDynamicCacheStats() map[string]interface{} {
|
|
||||||
if sc.dynamicCacheMgr == nil {
|
|
||||||
return map[string]interface{}{"error": "dynamic cache management not enabled"}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sc.dynamicCacheMgr.GetStats()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetMemoryTarget sets the target memory usage for dynamic cache sizing
|
|
||||||
func (sc *SteamCache) SetMemoryTarget(targetBytes uint64) {
|
|
||||||
if sc.memoryMonitor != nil {
|
|
||||||
sc.memoryMonitor.SetTargetMemoryUsage(targetBytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ForceCacheAdjustment forces an immediate cache size adjustment
|
|
||||||
func (sc *SteamCache) ForceCacheAdjustment() {
|
|
||||||
if sc.dynamicCacheMgr != nil {
|
|
||||||
// This would trigger an immediate adjustment
|
|
||||||
// Implementation depends on the specific needs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMemoryFragmentationStats returns memory fragmentation statistics
|
|
||||||
func (sc *SteamCache) GetMemoryFragmentationStats() map[string]interface{} {
|
|
||||||
if sc.memory == nil {
|
|
||||||
return map[string]interface{}{"error": "memory cache not enabled"}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sc.memory.GetFragmentationStats()
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user