Hone logo
Hone
Problems

Implementing a Time-Based Cache Invalidation Strategy in Go

Caching is a fundamental technique for improving application performance by storing frequently accessed data in memory. However, stale data can lead to incorrect results. This challenge requires you to implement a time-based cache invalidation mechanism for a simple key-value cache in Go, ensuring that cached data is considered invalid and re-fetched after a specified duration.

Problem Description

You need to create a Cache struct in Go that stores key-value pairs. Each entry in the cache should have an associated expiration time. The Cache should provide the following functionalities:

  1. NewCache(defaultTTL time.Duration): A constructor function to create a new cache instance. It should accept a defaultTTL (Time To Live) which will be used for new cache entries if not specified otherwise.
  2. Set(key string, value interface{}, ttl time.Duration): Adds or updates a key-value pair in the cache. If ttl is 0, the defaultTTL will be used. The entry should be marked with an expiration timestamp calculated from the current time plus the specified ttl.
  3. Get(key string): Retrieves the value associated with a given key. If the key exists and has not expired, its value should be returned. If the key does not exist or has expired, it should return nil and false.
  4. Delete(key string): Removes a key-value pair from the cache.
  5. Has(key string): Checks if a key exists in the cache and has not expired. Returns true if valid, false otherwise.

The cache should implicitly invalidate expired entries when Get or Has is called. There is no need for a separate background cleanup goroutine for this specific challenge.

Examples

Example 1:

package main

import (
	"fmt"
	"time"
)

// Assuming Cache struct and methods are defined elsewhere

func main() {
	cache := NewCache(2 * time.Second)

	cache.Set("user:1", "Alice", 0) // Uses default TTL of 2 seconds
	cache.Set("product:101", 12345, 5 * time.Second)

	value, ok := cache.Get("user:1")
	fmt.Printf("user:1: Value: %v, Found: %v\n", value, ok) // Expected: Value: Alice, Found: true

	time.Sleep(3 * time.Second)

	value, ok = cache.Get("user:1")
	fmt.Printf("user:1 (after 3s): Value: %v, Found: %v\n", value, ok) // Expected: Value: <nil>, Found: false

	value, ok = cache.Get("product:101")
	fmt.Printf("product:101: Value: %v, Found: %v\n", value, ok) // Expected: Value: 12345, Found: true
}

Output:

user:1: Value: Alice, Found: true
user:1 (after 3s): Value: <nil>, Found: false
product:101: Value: 12345, Found: true

Explanation: The "user:1" entry expires after 2 seconds (default TTL). When Get is called after 3 seconds, it's considered expired and returns nil, false. The "product:101" entry, with a TTL of 5 seconds, is still valid.

Example 2:

package main

import (
	"fmt"
	"time"
)

// Assuming Cache struct and methods are defined elsewhere

func main() {
	cache := NewCache(1 * time.Second)

	cache.Set("temp_data", []int{1, 2, 3}, 3 * time.Second)

	if cache.Has("temp_data") {
		fmt.Println("temp_data is in cache.") // Expected: temp_data is in cache.
	}

	cache.Delete("temp_data")

	if !cache.Has("temp_data") {
		fmt.Println("temp_data has been deleted.") // Expected: temp_data has been deleted.
	}
}

Output:

temp_data is in cache.
temp_data has been deleted.

Explanation: The "temp_data" is added and Has correctly reports its presence. After Delete is called, Has returns false.

Example 3: (Edge case: Setting with a very short TTL and checking immediately)

package main

import (
	"fmt"
	"time"
)

// Assuming Cache struct and methods are defined elsewhere

func main() {
	cache := NewCache(1 * time.Minute)

	cache.Set("short_lived", "data", 1*time.Millisecond)

	// Give a tiny moment for the system clock to tick if needed, though typically not required for this scale
	time.Sleep(2 * time.Millisecond)

	value, ok := cache.Get("short_lived")
	fmt.Printf("short_lived: Value: %v, Found: %v\n", value, ok) // Expected: Value: <nil>, Found: false
}

Output:

short_lived: Value: <nil>, Found: false

Explanation: Although set with a very short TTL, the explicit time.Sleep ensures that the expiration check after a minimal delay would correctly identify it as expired.

Constraints

  • The cache should be thread-safe. Multiple goroutines may access the cache concurrently.
  • The value stored in the cache can be of any type (interface{}).
  • The defaultTTL and individual ttl durations will be non-negative.
  • Keys are non-empty strings.

Notes

  • Consider using time.Now().Add(ttl) to calculate expiration timestamps.
  • A sync.RWMutex is a good choice for managing concurrent access to the cache data structure.
  • You'll need a way to store the key, the value, and its expiration timestamp for each cache entry.
  • When checking for expiration, compare the current time with the stored expiration timestamp.
  • For Get and Has operations, if an entry is found to be expired, it should be treated as if it doesn't exist and optionally removed from the cache's internal storage.
Loading editor...
go