Last active
April 23, 2025 10:51
-
-
Save caseylmanus/5b1e6e6d874c95437ec1a9a44dd2c78f to your computer and use it in GitHub Desktop.
pullthroughcache.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package main | |
| import ( | |
| "context" | |
| "fmt" | |
| "log" | |
| "time" | |
| "github.com/patrickmn/go-cache" | |
| ) | |
| // Define the function signature for the underlying function we want to cache. | |
| // The key is removed, as per the user request. Added context.Context. | |
| type DataFetcher func(ctx context.Context, app, principal string) (map[string]struct{}, error) | |
| // cacheKey generates a unique cache key based on the app and principal. | |
| // The key is NOT included here. | |
| func cacheKey(app, principal string) string { | |
| return fmt.Sprintf("%s:%s", app, principal) | |
| } | |
| // NewPullThroughCache creates a new pull-through cache using go-cache. | |
| func NewPullThroughCache(fetcher DataFetcher) *PullThroughCache { | |
| // Create a new go-cache with a default expiration time of 5 minutes and | |
| // a purge interval of 10 minutes. The purge interval doesn't drastically | |
| // affect the sliding expiration, but it's good practice to set it. | |
| c := cache.New(5*time.Minute, 10*time.Minute) | |
| return &PullThroughCache{ | |
| cache: c, | |
| fetcher: fetcher, | |
| } | |
| } | |
| // PullThroughCache is a struct that wraps the go-cache and the data fetching function. | |
| type PullThroughCache struct { | |
| cache *cache.Cache | |
| fetcher DataFetcher | |
| } | |
| // GetOrLoad retrieves data from the cache, or loads it using the provided | |
| // fetcher function if it's not in the cache. | |
| func (p *PullThroughCache) GetOrLoad(ctx context.Context, app, principal, key string) (map[string]struct{}, error) { | |
| cacheKey := cacheKey(app, principal) // key no longer part of cache key. | |
| // Check if the value is in the cache. | |
| cachedValue, found := p.cache.Get(cacheKey) | |
| if found { | |
| // Type assert the cached value to the correct type. This is crucial. | |
| if value, ok := cachedValue.(map[string]struct{}); ok { | |
| return value, nil // Return the cached value. | |
| } else { | |
| // Should not happen, unless there's a bug in the cache or an external modification. | |
| log.Printf("error: unexpected type in cache for key %s: got %T, expected %T", cacheKey, cachedValue, map[string]struct{}{}) | |
| p.cache.Delete(cacheKey) //remove the invalid entry | |
| } | |
| } | |
| // If the value is not in the cache, fetch it using the provided fetcher function. | |
| // The key is no longer passed to the fetcher. Pass the context. | |
| value, err := p.fetcher(ctx, app, principal) | |
| if err != nil { | |
| return nil, err // Return the error from the fetcher function. | |
| } | |
| // Only cache the result if the key is present in the returned map. | |
| _, keyPresent := value[key] | |
| if keyPresent { | |
| // Set the value in the cache with a 5-minute expiration time. | |
| p.cache.Set(cacheKey, value, cache.DefaultExpiration) | |
| } else { | |
| // Invalidate the entire cache for this app and principal | |
| p.invalidateCache(app, principal) | |
| } | |
| return value, nil // Return the fetched value. | |
| } | |
| // invalidateCache invalidates all cache entries for a given app and principal. | |
| // This is necessary when the underlying data changes. The key is not part of the cache key. | |
| func (p *PullThroughCache) invalidateCache(app, principal string) { | |
| p.cache.Delete(cacheKey(app, principal)) // Delete exact key | |
| } | |
| func main() { | |
| // Simulate a data fetching function. | |
| // The key parameter is removed from the function signature. | |
| // Added context parameter. | |
| fetchData := func(ctx context.Context, app, principal string) (map[string]struct{}, error) { | |
| fmt.Printf("Fetching data for app: %s, principal: %s from source with context: %v...\n", app, principal, ctx) | |
| // Simulate a database or API call with a delay. | |
| time.Sleep(500 * time.Millisecond) // Simulate latency | |
| // Simulate different results based on the key. | |
| // The key logic remains here, to simulate different data sets | |
| // being returned. | |
| if app == "myApp" && principal == "user123" { | |
| return map[string]struct{}{"key1": {}, "key2": {}}, nil | |
| } else if app == "myApp" && principal == "user456" { | |
| return map[string]struct{}{"keyA": {}, "keyB": {}}, nil | |
| } | |
| return map[string]struct{}{}, nil | |
| } | |
| // Create a new pull-through cache. | |
| cache := NewPullThroughCache(fetchData) | |
| // --- Example Usage --- | |
| app := "myApp" | |
| principal := "user123" | |
| ctx := context.Background() // Use a background context. | |
| // First call for key1: data will be fetched and cached. | |
| result1, err := cache.GetOrLoad(ctx, app, principal, "key1") | |
| if err != nil { | |
| log.Fatalf("Error getting data for key1: %v", err) | |
| } | |
| fmt.Printf("Result 1: %v\n", result1) | |
| // Second call for key1: data will be retrieved from the cache. | |
| result2, err := cache.GetOrLoad(ctx, app, principal, "key2") | |
| if err != nil { | |
| log.Fatalf("Error getting data for key2: %v", err) | |
| } | |
| fmt.Printf("Result 2: %v (from cache)\n", result2) | |
| // Call for key3: data will be fetched, and because key3 is not in the | |
| // result, the cache will be invalidated. | |
| result3, err := cache.GetOrLoad(ctx, app, principal, "key3") | |
| if err != nil { | |
| log.Fatalf("Error getting data for key3: %v", err) | |
| } | |
| fmt.Printf("Result 3: %v\n", result3) | |
| // Call for key1 again: data will be fetched from source because the cache | |
| // was invalidated in the previous step. | |
| result4, err := cache.GetOrLoad(ctx, app, principal, "key1") | |
| if err != nil { | |
| log.Fatalf("Error getting data for key1: %v", err) | |
| } | |
| fmt.Printf("Result 4: %v (after invalidation)\n", result4) | |
| // Wait for longer than the TTL. | |
| time.Sleep(6 * time.Minute) | |
| // Call for key1 again: Data will be fetched again after TTL expiry | |
| result5, err := cache.GetOrLoad(ctx, app, principal, "key1") | |
| if err != nil { | |
| log.Fatalf("Error getting data for key1: %v", err) | |
| } | |
| fmt.Printf("Result 5: %v (after TTL expiry)\n", result5) | |
| // Example with a different principal to show cache isolation. | |
| principal2 := "user456" | |
| result6, err := cache.GetOrLoad(ctx, app, principal2, "keyA") // Should not be affected by previous invalidation | |
| if err != nil { | |
| log.Fatalf("Error getting data for keyA: %v", err) | |
| } | |
| fmt.Printf("Result 6 (different principal): %v\n", result6) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment