Skip to content

Instantly share code, notes, and snippets.

@philosoralphter
Created November 7, 2018 21:49
Show Gist options
  • Save philosoralphter/ce8ee328fb9fe4ba7ac6a919077db19b to your computer and use it in GitHub Desktop.
Save philosoralphter/ce8ee328fb9fe4ba7ac6a919077db19b to your computer and use it in GitHub Desktop.
#Stub for memcached client wrapper
require 'dalli'
MEMCACHED_URL = 'localhost:11211'
LOCK_TIMEOUT_SECONDS = 5
MAX_SLEEP_MILLIS = LOCK_TIMEOUT_SECONDS * 1000
RETRY_TIMEOUT_MS = LOCK_TIMEOUT_SECONDS * 2 * 1000
class InstaCache < Dalli
def initialize (options)
begin
client = Dalli::Client.new (MEMCACHED_URL, options)
rescue Dalli::DalliError => e
raise CacheUnreachableError.new('Could not create memcached client', e)
end
raise CacheUnreachableError.new('Cannot verify memcached alive!') if ! client.alive!
end
#API
def fetch (key, options=nil)
return getSafe(key, options)
end
#Errors
class CacheUnreachableError < ServiceError
def initialize(message, error)
message |||= "Cannot Reach cache service instance"
super("memcached", MEMCACHED_URL, message, error)
end
end
private
#
#
def fetchSafe (key, options, attempts, startTime)
attempts ||= 0
startTime ||= Time.now
result = client.get(key, options)
#Cache hit!
return result if result
#Cache Miss
#if no block passed as re-caching function, return nil to the caller
return nil if ! block_given?
#if we exhausted timeout, return nil
return nil if Time.now - startTime > RETRY_TIMEOUT_MS
#We have a re-caching block. let's see if we're the first to notice and let's get a lock if so
if client.add(key + ".lock", LOCK_TIMEOUT)
#We got the lock
#call re-caching function, and return the result, and release lock (maybe async after returning?)
yield.tap { |result|
client.delete(key + ".lock")
return result
}
else
#someone else has already gotten a lock and should try and re-cache
#sleep with an exponential backoff
sleeptime = [self.getBackoffTimeMs(attempts), MAX_SLEEP_MILLIS].min
sleep(sleeptime)
#Now try again, adding to backoff
getSafe(key, options, attempts++)
end
end
#utils
def getBackoffTimeMs(attempts)
#naive exponential backoff
return 100 * (2 ** n) + Random.rand(100) #adding random prevents clients syncing up during a synced startup, e.g.
#(does 7 retries over 5 seconds)
end
end
class ServiceError < StandardError
def initialize (service, serviceUrl, message, error)
message ||= "Encountered an error accessing or using external service"
end
end
@philosoralphter
Copy link
Author

This is not runnable code! it is ruby-flavored pseudocode to show how wrapping hte client can be used to deal with cache misses, protecting DBs from thundering.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment