From 208114acff2e67b06abf55ff785cb135970ea8b5 Mon Sep 17 00:00:00 2001 From: Teemu Kataja Date: Tue, 29 Aug 2023 10:47:09 +0300 Subject: [PATCH] refactor middleware and cache in an attempt of clarifying its operation --- api/middleware/middleware.go | 45 ++++++++--------- api/middleware/middleware_test.go | 24 ++++----- api/s3/s3.go | 12 ++--- api/sda/sda.go | 14 +++--- api/sda/sda_test.go | 82 +++++++++++++++---------------- gl-code-quality-report.json | 1 + internal/config/config.go | 1 + internal/session/session.go | 38 +++++++------- internal/session/session_test.go | 4 +- 9 files changed, 110 insertions(+), 111 deletions(-) create mode 100644 gl-code-quality-report.json diff --git a/api/middleware/middleware.go b/api/middleware/middleware.go index e2e4bda..e63f000 100644 --- a/api/middleware/middleware.go +++ b/api/middleware/middleware.go @@ -10,11 +10,15 @@ import ( log "github.com/sirupsen/logrus" ) -var datasetsKey = "datasets" +// requestContextKey holds a name for the request context storage key +// which is used to store and get the permissions after passing middleware +const requestContextKey = "requestContextKey" // TokenMiddleware performs access token verification and validation // JWTs are verified and validated by the app, opaque tokens are sent to AAI for verification -// Successful auth results in list of authorised datasets +// Successful auth results in list of authorised datasets. +// The datasets are stored into a session cache for subsequent requests, and also +// to the current request context for use in the endpoints. func TokenMiddleware() gin.HandlerFunc { return func(c *gin.Context) { @@ -23,11 +27,11 @@ func TokenMiddleware() gin.HandlerFunc { if err != nil { log.Debugf("no session cookie received") } - var datasetCache session.DatasetCache + var cache session.Cache var exists bool if sessionCookie != "" { log.Debug("session cookie received") - datasetCache, exists = session.Get(sessionCookie) + cache, exists = session.Get(sessionCookie) } if !exists { @@ -57,14 +61,11 @@ func TokenMiddleware() gin.HandlerFunc { // 200 OK with [] empty dataset list, when listing datasets (use case for sda-filesystem download tool) // 404 dataset not found, when listing files from a dataset // 401 unauthorised, when downloading a file - datasets := auth.GetPermissions(*visas) - datasetCache = session.DatasetCache{ - Datasets: datasets, - } + cache.Datasets = auth.GetPermissions(*visas) // Start a new session and store datasets under the session key key := session.NewSessionKey() - session.Set(key, datasetCache) + session.Set(key, cache) c.SetCookie(config.Config.Session.Name, // name key, // value int(config.Config.Session.Expiration)/1e9, // max age @@ -77,7 +78,8 @@ func TokenMiddleware() gin.HandlerFunc { } // Store dataset list to request context, for use in the endpoint handlers - c = storeDatasets(c, datasetCache) + log.Debugf("storing %v to request context", cache) + c.Set(requestContextKey, cache) // Forward request to the next endpoint handler c.Next() @@ -85,23 +87,16 @@ func TokenMiddleware() gin.HandlerFunc { } -// storeDatasets stores the dataset list to the request context -func storeDatasets(c *gin.Context, datasets session.DatasetCache) *gin.Context { - log.Debugf("storing %v datasets to request context", datasets) - - c.Set(datasetsKey, datasets) - - return c -} - -// GetDatasets extracts the dataset list from the request context -var GetDatasets = func(c *gin.Context) session.DatasetCache { - var datasetCache session.DatasetCache - cached, exists := c.Get(datasetsKey) +// GetCacheFromContext is a helper function that endpoints can use to get data +// stored to the *current* request context (not the session storage). +// The request context was populated by the middleware, which in turn uses the session storage. +var GetCacheFromContext = func(c *gin.Context) session.Cache { + var cache session.Cache + cached, exists := c.Get(requestContextKey) if exists { - datasetCache = cached.(session.DatasetCache) + cache = cached.(session.Cache) } log.Debugf("returning %v from request context", cached) - return datasetCache + return cache } diff --git a/api/middleware/middleware_test.go b/api/middleware/middleware_test.go index edfb872..3a15ebd 100644 --- a/api/middleware/middleware_test.go +++ b/api/middleware/middleware_test.go @@ -182,8 +182,8 @@ func TestTokenMiddleware_Success_NoCache(t *testing.T) { // Now that we are modifying the request context, we need to place the context test inside the handler expectedDatasets := []string{"dataset1", "dataset2"} testEndpointWithContextData := func(c *gin.Context) { - datasets, _ := c.Get(datasetsKey) - if !reflect.DeepEqual(datasets.(session.DatasetCache).Datasets, expectedDatasets) { + datasets, _ := c.Get(requestContextKey) + if !reflect.DeepEqual(datasets.(session.Cache).Datasets, expectedDatasets) { t.Errorf("TestTokenMiddleware_Success_NoCache failed, got %s expected %s", datasets, expectedDatasets) } } @@ -224,9 +224,9 @@ func TestTokenMiddleware_Success_FromCache(t *testing.T) { originalGetCache := session.Get // Substitute mock functions - session.Get = func(key string) (session.DatasetCache, bool) { + session.Get = func(key string) (session.Cache, bool) { log.Warningf("session.Get %v", key) - cached := session.DatasetCache{ + cached := session.Cache{ Datasets: []string{"dataset1", "dataset2"}, } @@ -248,8 +248,8 @@ func TestTokenMiddleware_Success_FromCache(t *testing.T) { // Now that we are modifying the request context, we need to place the context test inside the handler expectedDatasets := []string{"dataset1", "dataset2"} testEndpointWithContextData := func(c *gin.Context) { - datasets, _ := c.Get(datasetsKey) - if !reflect.DeepEqual(datasets.(session.DatasetCache).Datasets, expectedDatasets) { + datasets, _ := c.Get(requestContextKey) + if !reflect.DeepEqual(datasets.(session.Cache).Datasets, expectedDatasets) { t.Errorf("TestTokenMiddleware_Success_FromCache failed, got %s expected %s", datasets, expectedDatasets) } } @@ -284,13 +284,13 @@ func TestStoreDatasets(t *testing.T) { c, _ := gin.CreateTestContext(w) // Store data to request context - datasets := session.DatasetCache{ + datasets := session.Cache{ Datasets: []string{"dataset1", "dataset2"}, } - modifiedContext := storeDatasets(c, datasets) + c.Set(requestContextKey, datasets) // Verify that context has new data - storedDatasets := modifiedContext.Value(datasetsKey).(session.DatasetCache) + storedDatasets := c.Value(requestContextKey).(session.Cache) if !reflect.DeepEqual(datasets, storedDatasets) { t.Errorf("TestStoreDatasets failed, got %s, expected %s", storedDatasets, datasets) } @@ -304,13 +304,13 @@ func TestGetDatasets(t *testing.T) { c, _ := gin.CreateTestContext(w) // Store data to request context - datasets := session.DatasetCache{ + datasets := session.Cache{ Datasets: []string{"dataset1", "dataset2"}, } - modifiedContext := storeDatasets(c, datasets) + c.Set(requestContextKey, datasets) // Verify that context has new data - storedDatasets := GetDatasets(modifiedContext) + storedDatasets := GetCacheFromContext(c) if !reflect.DeepEqual(datasets, storedDatasets) { t.Errorf("TestStoreDatasets failed, got %s, expected %s", storedDatasets, datasets) } diff --git a/api/s3/s3.go b/api/s3/s3.go index 83af590..0e333af 100644 --- a/api/s3/s3.go +++ b/api/s3/s3.go @@ -94,8 +94,8 @@ func ListBuckets(c *gin.Context) { } buckets := []Bucket{} - datasetCache := middleware.GetDatasets(c) - for _, dataset := range datasetCache.Datasets { + cache := middleware.GetCacheFromContext(c) + for _, dataset := range cache.Datasets { datasetInfo, err := database.GetDatasetInfo(dataset) if err != nil { log.Errorf("Failed to get dataset information: %v", err) @@ -123,8 +123,8 @@ func ListObjects(c *gin.Context) { dataset := c.Param("dataset") allowed := false - datasetCache := middleware.GetDatasets(c) - for _, known := range datasetCache.Datasets { + cache := middleware.GetCacheFromContext(c) + for _, known := range cache.Datasets { if dataset == known { allowed = true @@ -244,8 +244,8 @@ func parseParams(c *gin.Context) *gin.Context { path = string(protocolPattern.ReplaceAll([]byte(path), []byte("$1/$2"))) } - datasetCache := middleware.GetDatasets(c) - for _, dataset := range datasetCache.Datasets { + cache := middleware.GetCacheFromContext(c) + for _, dataset := range cache.Datasets { // check that the path starts with the dataset name, but also that the // path is only the dataset, or that the following character is a slash. // This prevents wrong matches in cases like when one dataset name is a diff --git a/api/sda/sda.go b/api/sda/sda.go index 9f63653..614a11e 100644 --- a/api/sda/sda.go +++ b/api/sda/sda.go @@ -35,10 +35,10 @@ func Datasets(c *gin.Context) { // Retrieve dataset list from request context // generated by the authentication middleware - datasetCache := middleware.GetDatasets(c) + cache := middleware.GetCacheFromContext(c) // Return response - c.JSON(http.StatusOK, datasetCache.Datasets) + c.JSON(http.StatusOK, cache.Datasets) } // find looks for a dataset name in a list of datasets @@ -60,11 +60,11 @@ var getFiles = func(datasetID string, ctx *gin.Context) ([]*database.FileInfo, i // Retrieve dataset list from request context // generated by the authentication middleware - datasetCache := middleware.GetDatasets(ctx) + cache := middleware.GetCacheFromContext(ctx) log.Debugf("request to process files for dataset %s", sanitizeString(datasetID)) - if find(datasetID, datasetCache.Datasets) { + if find(datasetID, cache.Datasets) { // Get file metadata files, err := database.GetFiles(datasetID) if err != nil { @@ -133,12 +133,12 @@ func Download(c *gin.Context) { } // Get datasets from request context, parsed previously by token middleware - datasetCache := middleware.GetDatasets(c) + cache := middleware.GetCacheFromContext(c) // Verify user has permission to datafile permission := false - for d := range datasetCache.Datasets { - if datasetCache.Datasets[d] == dataset { + for d := range cache.Datasets { + if cache.Datasets[d] == dataset { permission = true break diff --git a/api/sda/sda_test.go b/api/sda/sda_test.go index ebd842d..7bc4af3 100644 --- a/api/sda/sda_test.go +++ b/api/sda/sda_test.go @@ -22,11 +22,11 @@ import ( func TestDatasets(t *testing.T) { // Save original to-be-mocked functions - originalGetDatasets := middleware.GetDatasets + originalGetCacheFromContext := middleware.GetCacheFromContext // Substitute mock functions - middleware.GetDatasets = func(c *gin.Context) session.DatasetCache { - return session.DatasetCache{ + middleware.GetCacheFromContext = func(c *gin.Context) session.Cache { + return session.Cache{ Datasets: []string{"dataset1", "dataset2"}, } } @@ -34,7 +34,7 @@ func TestDatasets(t *testing.T) { // Mock request and response holders w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) - c.Set("datasets", session.DatasetCache{Datasets: []string{"dataset1", "dataset2"}}) + c.Set("datasets", session.Cache{Datasets: []string{"dataset1", "dataset2"}}) // Test the outcomes of the handler Datasets(c) @@ -55,7 +55,7 @@ func TestDatasets(t *testing.T) { } // Return mock functions to originals - middleware.GetDatasets = originalGetDatasets + middleware.GetCacheFromContext = originalGetCacheFromContext } @@ -96,12 +96,12 @@ func TestFind_NotFound(t *testing.T) { func TestGetFiles_Fail_Database(t *testing.T) { // Save original to-be-mocked functions - originalGetDatasets := middleware.GetDatasets + originalGetCacheFromContext := middleware.GetCacheFromContext originalGetFilesDB := database.GetFiles // Substitute mock functions - middleware.GetDatasets = func(ctx *gin.Context) session.DatasetCache { - return session.DatasetCache{ + middleware.GetCacheFromContext = func(ctx *gin.Context) session.Cache { + return session.Cache{ Datasets: []string{"dataset1", "dataset2"}, } } @@ -129,7 +129,7 @@ func TestGetFiles_Fail_Database(t *testing.T) { } // Return mock functions to originals - middleware.GetDatasets = originalGetDatasets + middleware.GetCacheFromContext = originalGetCacheFromContext database.GetFiles = originalGetFilesDB } @@ -137,11 +137,11 @@ func TestGetFiles_Fail_Database(t *testing.T) { func TestGetFiles_Fail_NotFound(t *testing.T) { // Save original to-be-mocked functions - originalGetDatasets := middleware.GetDatasets + originalGetCacheFromContext := middleware.GetCacheFromContext // Substitute mock functions - middleware.GetDatasets = func(ctx *gin.Context) session.DatasetCache { - return session.DatasetCache{ + middleware.GetCacheFromContext = func(ctx *gin.Context) session.Cache { + return session.Cache{ Datasets: []string{"dataset1", "dataset2"}, } } @@ -166,18 +166,18 @@ func TestGetFiles_Fail_NotFound(t *testing.T) { } // Return mock functions to originals - middleware.GetDatasets = originalGetDatasets + middleware.GetCacheFromContext = originalGetCacheFromContext } func TestGetFiles_Success(t *testing.T) { // Save original to-be-mocked functions - originalGetDatasets := middleware.GetDatasets + originalGetCacheFromContext := middleware.GetCacheFromContext originalGetFilesDB := database.GetFiles // Substitute mock functions - middleware.GetDatasets = func(ctx *gin.Context) session.DatasetCache { - return session.DatasetCache{ + middleware.GetCacheFromContext = func(ctx *gin.Context) session.Cache { + return session.Cache{ Datasets: []string{"dataset1", "dataset2"}, } } @@ -211,7 +211,7 @@ func TestGetFiles_Success(t *testing.T) { } // Return mock functions to originals - middleware.GetDatasets = originalGetDatasets + middleware.GetCacheFromContext = originalGetCacheFromContext database.GetFiles = originalGetFilesDB } @@ -449,15 +449,15 @@ func TestDownload_Fail_NoPermissions(t *testing.T) { // Save original to-be-mocked functions originalCheckFilePermission := database.CheckFilePermission - originalGetDatasets := middleware.GetDatasets + originalGetCacheFromContext := middleware.GetCacheFromContext // Substitute mock functions database.CheckFilePermission = func(fileID string) (string, error) { // nolint:goconst return "dataset1", nil } - middleware.GetDatasets = func(ctx *gin.Context) session.DatasetCache { - return session.DatasetCache{} + middleware.GetCacheFromContext = func(ctx *gin.Context) session.Cache { + return session.Cache{} } // Mock request and response holders @@ -484,7 +484,7 @@ func TestDownload_Fail_NoPermissions(t *testing.T) { // Return mock functions to originals database.CheckFilePermission = originalCheckFilePermission - middleware.GetDatasets = originalGetDatasets + middleware.GetCacheFromContext = originalGetCacheFromContext } @@ -492,15 +492,15 @@ func TestDownload_Fail_GetFile(t *testing.T) { // Save original to-be-mocked functions originalCheckFilePermission := database.CheckFilePermission - originalGetDatasets := middleware.GetDatasets + originalGetCacheFromContext := middleware.GetCacheFromContext originalGetFile := database.GetFile // Substitute mock functions database.CheckFilePermission = func(fileID string) (string, error) { return "dataset1", nil } - middleware.GetDatasets = func(ctx *gin.Context) session.DatasetCache { - return session.DatasetCache{ + middleware.GetCacheFromContext = func(ctx *gin.Context) session.Cache { + return session.Cache{ Datasets: []string{"dataset1"}, } } @@ -532,7 +532,7 @@ func TestDownload_Fail_GetFile(t *testing.T) { // Return mock functions to originals database.CheckFilePermission = originalCheckFilePermission - middleware.GetDatasets = originalGetDatasets + middleware.GetCacheFromContext = originalGetCacheFromContext database.GetFile = originalGetFile } @@ -541,7 +541,7 @@ func TestDownload_Fail_OpenFile(t *testing.T) { // Save original to-be-mocked functions originalCheckFilePermission := database.CheckFilePermission - originalGetDatasets := middleware.GetDatasets + originalGetCacheFromContext := middleware.GetCacheFromContext originalGetFile := database.GetFile Backend, _ = storage.NewBackend(config.Config.Archive) @@ -549,8 +549,8 @@ func TestDownload_Fail_OpenFile(t *testing.T) { database.CheckFilePermission = func(fileID string) (string, error) { return "dataset1", nil } - middleware.GetDatasets = func(ctx *gin.Context) session.DatasetCache { - return session.DatasetCache{ + middleware.GetCacheFromContext = func(ctx *gin.Context) session.Cache { + return session.Cache{ Datasets: []string{"dataset1"}, } } @@ -588,7 +588,7 @@ func TestDownload_Fail_OpenFile(t *testing.T) { // Return mock functions to originals database.CheckFilePermission = originalCheckFilePermission - middleware.GetDatasets = originalGetDatasets + middleware.GetCacheFromContext = originalGetCacheFromContext database.GetFile = originalGetFile } @@ -597,7 +597,7 @@ func TestDownload_Fail_ParseCoordinates(t *testing.T) { // Save original to-be-mocked functions originalCheckFilePermission := database.CheckFilePermission - originalGetDatasets := middleware.GetDatasets + originalGetCacheFromContext := middleware.GetCacheFromContext originalGetFile := database.GetFile originalParseCoordinates := parseCoordinates config.Config.Archive.Posix.Location = "." @@ -607,8 +607,8 @@ func TestDownload_Fail_ParseCoordinates(t *testing.T) { database.CheckFilePermission = func(fileID string) (string, error) { return "dataset1", nil } - middleware.GetDatasets = func(ctx *gin.Context) session.DatasetCache { - return session.DatasetCache{ + middleware.GetCacheFromContext = func(ctx *gin.Context) session.Cache { + return session.Cache{ Datasets: []string{"dataset1"}, } } @@ -649,7 +649,7 @@ func TestDownload_Fail_ParseCoordinates(t *testing.T) { // Return mock functions to originals database.CheckFilePermission = originalCheckFilePermission - middleware.GetDatasets = originalGetDatasets + middleware.GetCacheFromContext = originalGetCacheFromContext database.GetFile = originalGetFile parseCoordinates = originalParseCoordinates @@ -659,7 +659,7 @@ func TestDownload_Fail_StreamFile(t *testing.T) { // Save original to-be-mocked functions originalCheckFilePermission := database.CheckFilePermission - originalGetDatasets := middleware.GetDatasets + originalGetCacheFromContext := middleware.GetCacheFromContext originalGetFile := database.GetFile originalParseCoordinates := parseCoordinates originalStitchFile := stitchFile @@ -670,8 +670,8 @@ func TestDownload_Fail_StreamFile(t *testing.T) { database.CheckFilePermission = func(fileID string) (string, error) { return "dataset1", nil } - middleware.GetDatasets = func(ctx *gin.Context) session.DatasetCache { - return session.DatasetCache{ + middleware.GetCacheFromContext = func(ctx *gin.Context) session.Cache { + return session.Cache{ Datasets: []string{"dataset1"}, } } @@ -717,7 +717,7 @@ func TestDownload_Fail_StreamFile(t *testing.T) { // Return mock functions to originals database.CheckFilePermission = originalCheckFilePermission - middleware.GetDatasets = originalGetDatasets + middleware.GetCacheFromContext = originalGetCacheFromContext database.GetFile = originalGetFile parseCoordinates = originalParseCoordinates stitchFile = originalStitchFile @@ -728,7 +728,7 @@ func TestDownload_Success(t *testing.T) { // Save original to-be-mocked functions originalCheckFilePermission := database.CheckFilePermission - originalGetDatasets := middleware.GetDatasets + originalGetCacheFromContext := middleware.GetCacheFromContext originalGetFile := database.GetFile originalParseCoordinates := parseCoordinates originalStitchFile := stitchFile @@ -740,8 +740,8 @@ func TestDownload_Success(t *testing.T) { database.CheckFilePermission = func(fileID string) (string, error) { return "dataset1", nil } - middleware.GetDatasets = func(ctx *gin.Context) session.DatasetCache { - return session.DatasetCache{ + middleware.GetCacheFromContext = func(ctx *gin.Context) session.Cache { + return session.Cache{ Datasets: []string{"dataset1"}, } } @@ -791,7 +791,7 @@ func TestDownload_Success(t *testing.T) { // Return mock functions to originals database.CheckFilePermission = originalCheckFilePermission - middleware.GetDatasets = originalGetDatasets + middleware.GetCacheFromContext = originalGetCacheFromContext database.GetFile = originalGetFile parseCoordinates = originalParseCoordinates stitchFile = originalStitchFile diff --git a/gl-code-quality-report.json b/gl-code-quality-report.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/gl-code-quality-report.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/internal/config/config.go b/internal/config/config.go index fb716a2..2d66882 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -21,6 +21,7 @@ const POSIX = "posix" const S3 = "s3" // availableMiddlewares list the options for middlewares +// empty string "" is an alias for default, for when the config key is not set, or it's empty var availableMiddlewares = []string{"", "default"} // Config is a global configuration value store diff --git a/internal/session/session.go b/internal/session/session.go index 4f95d91..2621ce8 100644 --- a/internal/session/session.go +++ b/internal/session/session.go @@ -7,18 +7,18 @@ import ( log "github.com/sirupsen/logrus" ) -// SessionCache stores dataset permission lists +// SessionCache is the in-memory storage holding session keys and interfaces containing cached data var SessionCache *ristretto.Cache -// DatasetCache stores the dataset permissions +// Cache stores the dataset permissions // and information whether this information has // already been checked or not. This information // can then be used to skip the time-costly // authentication middleware -// DatasetCache==nil, session doesn't exist -// DatasetCache.Datasets==nil, session exists, user has no permissions (this case is not used in middleware.go) -// DatasetCache.Datasets==[]string{}, session exists, user has permissions -type DatasetCache struct { +// Cache==nil, session doesn't exist +// Cache.Datasets==nil, session exists, user has no permissions (this case is not used in middleware.go) +// Cache.Datasets==[]string{...}, session exists, user has permissions +type Cache struct { Datasets []string } @@ -49,24 +49,26 @@ func InitialiseSessionCache() (*ristretto.Cache, error) { return sessionCache, nil } -// Get returns a value from cache at key -var Get = func(key string) (DatasetCache, bool) { +// Get returns a cache item from the session storage at key +var Get = func(key string) (Cache, bool) { log.Debug("get value from cache") - cached, exists := SessionCache.Get(key) - var cachedDatasets DatasetCache // default nil - if cached != nil { - cachedDatasets = cached.(DatasetCache) + cachedItem, exists := SessionCache.Get(key) + var cached Cache + if exists { + // the storage is unaware of cached types, so if an item is found + // we must assert it is the expected interface type (Cache) + cached = cachedItem.(Cache) } - log.Debugf("cache response, exists=%t, cached=%v", exists, cachedDatasets) + log.Debugf("cache response, exists=%t, cached=%v", exists, cached) - return cachedDatasets, exists + return cached, exists } -func Set(key string, datasetCache DatasetCache) { - log.Debug("store to cache") +func Set(key string, toCache Cache) { + log.Debugf("store %v to cache", toCache) // Each item has a cost of 1, with max size of cache being 100,000 items - SessionCache.SetWithTTL(key, datasetCache, 1, config.Config.Session.Expiration) - log.Debug("stored to cache") + SessionCache.SetWithTTL(key, toCache, 1, config.Config.Session.Expiration) + log.Debugf("stored %v to cache", toCache) } // NewSessionKey generates a session key used for storing diff --git a/internal/session/session_test.go b/internal/session/session_test.go index 22a0fa5..b760eed 100644 --- a/internal/session/session_test.go +++ b/internal/session/session_test.go @@ -36,7 +36,7 @@ func TestGetSetCache_Found(t *testing.T) { cache, _ := InitialiseSessionCache() SessionCache = cache - Set("key1", DatasetCache{Datasets: []string{"dataset1", "dataset2"}}) + Set("key1", Cache{Datasets: []string{"dataset1", "dataset2"}}) time.Sleep(time.Duration(100 * time.Millisecond)) // need to give cache time to get ready datasets, exists := Get("key1") @@ -62,7 +62,7 @@ func TestGetSetCache_NotFound(t *testing.T) { cache, _ := InitialiseSessionCache() SessionCache = cache - Set("key1", DatasetCache{Datasets: []string{"dataset1", "dataset2"}}) + Set("key1", Cache{Datasets: []string{"dataset1", "dataset2"}}) time.Sleep(time.Duration(100 * time.Millisecond)) // need to give cache time to get ready datasets, exists := Get("key2")