Created
November 7, 2018 21:49
-
-
Save philosoralphter/ce8ee328fb9fe4ba7ac6a919077db19b to your computer and use it in GitHub Desktop.
This file contains 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
#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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.