Since we also have a lot of infrastructure built around Redis, we decided to use it for our consistency layer because it's very fast and has built-in key expiration. The implementation is fairly simple: all writes to S3 also write a key to Redis with the etag of the new object. When a read method is called, the etag in Redis is checked against the etag from the client to see if they match. If they do, the read proceeds as normal. If they don't, an AWS::S3::Errors::PreconditionFailed is thrown. The client then decides how to handle the error, whether that is retrying or doing something else. If the Redis key is nil, it is assumed the data is consistent.
In practice, it's never more than a second or two to get consistent data after a write, but we set the Redis key timeout to 24 hours to give ourselves plenty of buffer without polluting the the DB with an endless number of keys.
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
def write_with_strong_consistency(*args, &block) | |
result = write(*args, &block) | |
redis.set("s3:key:#{key}", result.etag, ex: 24*60*60) | |
result | |
end | |
def delete_with_strong_consistency(options = {}) | |
result = delete(options) | |
redis.set("s3:key:#{key}", 'deleted', ex: 24*60*60) unless options[:version_id] | |
result | |
end | |
def read_with_strong_consistency(options = {}, &read_block) | |
read(options, &read_block) if options[:version_id] # short-circuit the whole thing for versions | |
redis_etag = redis.get("s3:key:#{key}") | |
options[:if_match] ||= redis_etag | |
raise NoSuchKey, "The specified key does not exist" if redis_etag == 'deleted' | |
if redis_etag && redis_etag == options[:if_match] | |
begin | |
read(options, &read_block) | |
rescue AWS::S3::Errors::NoSuchKey => e | |
# if the key has actually been deleted, the next time you check you will get | |
# a real NoSuchKey exception | |
raise AWS::S3::Errors::PreconditionFailed, "The specified key does not exist yet" | |
end | |
else | |
read(options, &read_block) | |
end | |
end |