Last active
February 7, 2023 21:52
-
-
Save AaronFeledy/315e1443cee29f4ee66bcc59c4000763 to your computer and use it in GitHub Desktop.
PoC Tyk Secondary Cache
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 mw_secondary_cache | |
| import ( | |
| "github.com/TykTechnologies/tyk/config" | |
| ogctx "github.com/TykTechnologies/tyk/ctx" | |
| "github.com/TykTechnologies/tyk/storage" | |
| "github.com/metrotranscom/511_tyk_api/pkg/" | |
| "github.com/metrotranscom/511_tyk_api/pkg/errs" | |
| "github.com/metrotranscom/511_tyk_api/pkg/log" | |
| "github.com/metrotranscom/511_tyk_api/pkg/mw" | |
| "net/http" | |
| "os" | |
| "time" | |
| ) | |
| const name = "mw_secondary_cache" | |
| var ( | |
| logger = *log.Get(name) | |
| errHandler = &errs.Handler{} | |
| ) | |
| // This is called when the gateway is started. | |
| func init() { | |
| logger.Debug("Middleware Package Init") | |
| } | |
| func Register() { | |
| mw.RegisterRequestProcessor(Checkpoint) | |
| } | |
| // Checkpoint processes the inbound request when there is no cached response available. It will determine if the request | |
| // should be processed by the upstream API or if it should be returned from the secondary cache. This should be called | |
| // from an API's "post" hook to ensure it only runs when there is no existing Tyk cache. | |
| func Checkpoint(rw http.ResponseWriter, req *http.Request) { | |
| logger.Debug("Executing Checkpoint()") | |
| // Let's use the same cache key as Tyk would use | |
| cacheKey := GetCacheKey(req) | |
| if cacheKey == "" { | |
| logger.Debug("No cache key, returning") | |
| return | |
| } | |
| logger.Debug("Got cache key: ", cacheKey) | |
| // Get the API definition so we can key per API ID | |
| apiDef := ctx.GetApiDefinition(req) | |
| // TODO: Can we prefix our keys in a way that allows the "Clear Cache" | |
| // button in Tyk dashboard to clear this as well? | |
| keyPrefix := "cache2-" + apiDef.APIID | |
| logger.Debug("Got API Definition: ", apiDef.APIID) | |
| // Get the currently loaded config for the Tyk Gateway | |
| // so we can use the Redis connection info | |
| conf := config.Global() | |
| logger.Debug("Got config with some data? Hostname:",conf.HostName) | |
| // Create a Redis Controller, which will handle the Redis connection for the storage object | |
| rc := storage.NewRedisController(req.Context()) | |
| logger.Debug("Created Redis Controller. Connected?", rc.Connected()) | |
| // TODO: Implement a redis store for reading/writing locks | |
| // TODO: Write a lock then return so the first user to get here will process the upstream request | |
| // TODO: If the lock is already set and there is a cache2 available, return the cache2 | |
| // TODO: If the lock is already set and there is no cache2 available, wait here for the lock to be released | |
| // TODO: When the lock is released, check if there is a Tyk cache or cache2 available, if so return it, if not, process the upstream request | |
| // TODO: See if we can manage the queue by triggering Tyk's throttling mechanism and sending users there when both caches are empty. | |
| // Create a storage object, which will handle Redis operations using "cache2-" key prefix | |
| store := storage.RedisCluster{KeyPrefix: keyPrefix, HashKeys: conf.HashKeys, RedisController: rc} | |
| logger.Debug("Created Redis Store Object") | |
| // TODO: Put the store in context so we can reuse it in the response? | |
| go rc.ConnectToRedis(req.Context(), nil, &conf) | |
| for i := 0; i < 5; i++ { // max 5 attempts | |
| if rc.Connected() { | |
| logger.Info("Redis Controller connected") | |
| break | |
| } | |
| logger.Warn("Redis Controller not connected, will retry") | |
| time.Sleep(10 * time.Millisecond) | |
| } | |
| if !rc.Connected() { | |
| logger.Error("Could not connect to storage") | |
| rw.WriteHeader(http.StatusInternalServerError) | |
| return | |
| } | |
| // See if our test value is already stored in the cache | |
| value, err := store.GetKey("test") | |
| if err != nil { | |
| logger.Debug("Failed to get cache key:", err.Error()) | |
| } | |
| if value != "" { | |
| logger.Debug("Cache2 hit") | |
| logger.Debug("Cache2 value:", value) | |
| // Using the response writer will terminate the request and return | |
| // the response to the client | |
| rw.WriteHeader(http.StatusOK) | |
| rw.Write([]byte(value + ", served by " + os.Getenv("HOSTNAME"))) | |
| return | |
| } | |
| logger.Debug("Cache2 miss") | |
| // TODO: This should be the cache timeout from the API definition plus a buffer | |
| var cacheTTL int64 = 20 | |
| // If we get here, the cache was empty, so we need to set the value. | |
| // This will be returned to the client on the next request. | |
| // TODO: Move this into a response hook so we can save the actual response from the upstream API | |
| err = store.SetKey("test", "This is a test cached by " + os.Getenv("HOSTNAME"), cacheTTL) | |
| if err != nil { | |
| logger.Debug("Failed to set cache key:", err.Error()) | |
| return | |
| } | |
| logger.Debug("Cache key set") | |
| } | |
| // GetCacheKey returns a cache key if we need to cache request | |
| func GetCacheKey(r *http.Request) string { | |
| optionsInterface := r.Context().Value(ogctx.CacheOptions) | |
| if optionsInterface != nil { | |
| // Using slow reflection to get the key from the private cacheOptions struct created by | |
| // the Redis cache middleware. Maybe PR to Tyk to make this public? We're only using | |
| // this during cache rebuilds, which are already slow so it's not a big deal. | |
| value := reflect.ValueOf(optionsInterface) | |
| key := value.Elem().FieldByName("key").String() | |
| return key | |
| } | |
| return "" | |
| } | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment