Skip to content

Instantly share code, notes, and snippets.

@AaronFeledy
Last active February 7, 2023 21:52
Show Gist options
  • Save AaronFeledy/315e1443cee29f4ee66bcc59c4000763 to your computer and use it in GitHub Desktop.
Save AaronFeledy/315e1443cee29f4ee66bcc59c4000763 to your computer and use it in GitHub Desktop.
PoC Tyk Secondary Cache
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