199 lines
5.3 KiB
Ruby
199 lines
5.3 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
begin
|
|
require 'cbor'
|
|
rescue LoadError # Load CBOR if possible
|
|
end
|
|
require 'json'
|
|
|
|
module FicTracker::Util
|
|
class Cache
|
|
include Enumerable
|
|
|
|
def self.create(type: :none, compress: nil, encoding: nil, options: {})
|
|
require_relative 'cache/base'
|
|
cache = nil
|
|
case type
|
|
when :memory, 'memory'
|
|
require_relative 'cache/memory'
|
|
cache = CacheImpl::Memory.new(**options)
|
|
when :database, 'database'
|
|
require_relative 'cache/database'
|
|
cache = CacheImpl::Database.new(**options)
|
|
when :file, 'file'
|
|
require_relative 'cache/file'
|
|
cache = CacheImpl::File.new(**options)
|
|
when :redis, 'redis'
|
|
require_relative 'cache/redis'
|
|
cache = CacheImpl::Redis.new(**options)
|
|
when :s3, 'S3'
|
|
require_relative 'cache/s3'
|
|
cache = CacheImpl::Redis.new(**options)
|
|
when :none, 'none', nil
|
|
require_relative 'cache/dummy'
|
|
cache = CacheImpl::Dummy.new
|
|
else
|
|
raise ConfigError, "Unknown cache type #{type.inspect}" unless type.respond_to?(:get) && type.respond_to?(:set)
|
|
|
|
cache = type
|
|
end
|
|
|
|
new(cache, compress: compress, encoding: encoding)
|
|
end
|
|
|
|
attr_accessor :compress, :encoding
|
|
|
|
def initialize(backend, compress: nil, encoding: nil)
|
|
@backend = backend
|
|
|
|
# Always compress >1.5KB unless otherwise specified
|
|
@compress = compress || 1500
|
|
@compress = (@compress..) if @compress.is_a? Numeric
|
|
@encoding = encoding
|
|
end
|
|
|
|
# Expire any unwanted entries from the cache
|
|
def expire
|
|
@backend.expire if @backend.respond_to? :expire
|
|
end
|
|
|
|
# Check if the cache contains a given key which has not expired
|
|
def has?(key)
|
|
key = expand_key(key)
|
|
return @backend.has?(key) if @backend.respond_to? :has?
|
|
|
|
!@backend.get(key).nil?
|
|
end
|
|
# Get the value of a key, will return nil if the key is expired
|
|
def get(key)
|
|
key = expand_key(key)
|
|
decode_data @backend.get(key)
|
|
end
|
|
# Set the value for a given key, with an optional expiry
|
|
def set(key, value, expire = nil)
|
|
key = expand_key(key)
|
|
@backend.set(key, encode_data(value), expire)
|
|
end
|
|
# Get the value for a given key, or set it as the result of the block with an optional expiry
|
|
def get_or_set(key, expire = nil, &block)
|
|
key = expand_key(key)
|
|
return decode_data(@backend.get_or_set(key, expire) { encode_data(block.call) }) if @backend.respond_to? :get_or_set
|
|
return get(key) if has?(key)
|
|
|
|
value = block.call
|
|
|
|
@backend.set(key, encode_data(value), expire)
|
|
|
|
value
|
|
end
|
|
# Clear the value for a given key
|
|
def delete(key)
|
|
key = expand_key(key)
|
|
@backend.delete key if @backend.respond_to? :delete
|
|
end
|
|
# Clear the entire cache
|
|
def clear
|
|
@backend.clear if @backend.respond_to? :clear
|
|
end
|
|
|
|
private
|
|
|
|
def logger
|
|
Logging.logger[self]
|
|
end
|
|
|
|
attr_accessor :encoder
|
|
|
|
ENCODING_FLAGS = 1 << 0 | 1 << 1 | 1 << 2
|
|
COMPRESSED_FLAG = 1 << 3
|
|
|
|
ENCODING_NONE = 0
|
|
ENCODING_MARSHAL = 1
|
|
ENCODING_JSON = 2
|
|
ENCODING_CBOR = 3
|
|
ENCODING_CUSTOM = 4
|
|
|
|
def expand_key(key)
|
|
return key.join '/' if key.is_a? Array
|
|
|
|
key
|
|
end
|
|
|
|
# Check if a piece of data is a plain-old-object - i.e. simple data
|
|
def is_pod?(data)
|
|
return true if data.nil? || data == true || data == false || data.is_a?(String) || data.is_a?(Numeric)
|
|
return true if data.is_a?(Array) && data.all? { |k| is_pod?(k) }
|
|
return true if data.is_a?(Hash && data.all? { |k, v| k.is_a?(Symbol) && is_pod?(v) })
|
|
|
|
false
|
|
end
|
|
|
|
def best_encoder?(data)
|
|
pod_encoder = :none if data.is_a?(String)
|
|
pod_encoder ||= :cbor if Object.const_defined?(:CBOR) && data.respond_to?(:to_cbor)
|
|
pod_encoder ||= :json if Object.const_defined?(:JSON) && data.respond_to?(:to_json)
|
|
pod_encoder ||= :marshal
|
|
return pod_encoder if is_pod?(data)
|
|
|
|
nil
|
|
end
|
|
|
|
def encode_data(data, encode: nil)
|
|
encode ||= @encoder
|
|
encode ||= best_encoder?(data)
|
|
|
|
flags = ENCODING_NONE
|
|
case encode
|
|
when :none
|
|
when :marshal
|
|
data = Marshal.dump(data)
|
|
flags |= ENCODING_MARSHAL
|
|
when :json
|
|
data = data.to_json
|
|
flags |= ENCODING_JSON
|
|
when :cbor
|
|
data = data.to_cbor
|
|
flags |= ENCODING_CBOR
|
|
else
|
|
raise "Unknown encoder #{(encode || @encoder).inspect}" unless encode.nil? && @encoder.respond_to?(:dump)
|
|
|
|
@encode.dump(data)
|
|
flags |= ENCODING_CUSTOM
|
|
end
|
|
|
|
if @compress && @compress.include?(data.bytesize)
|
|
require 'zlib'
|
|
data = Zlib::Deflate.deflate(data, Zlib::BEST_COMPRESSION)
|
|
flags |= COMPRESSED_FLAG
|
|
end
|
|
|
|
data.dup.prepend([flags].pack('C'))
|
|
end
|
|
|
|
def decode_data(data)
|
|
return unless data
|
|
|
|
flags = data[0].unpack('C').first
|
|
data = data[1..]
|
|
|
|
if (flags & COMPRESSED_FLAG) == COMPRESSED_FLAG
|
|
require 'zlib'
|
|
data = Zlib::Inflate.inflate(data)
|
|
end
|
|
data.force_encoding('UTF-8')
|
|
|
|
case (flags & ENCODING_FLAGS)
|
|
when ENCODING_MARSHAL
|
|
Marshal.load(data)
|
|
when ENCODING_JSON
|
|
JSON.parse(data, symbolize_names: true)
|
|
when ENCODING_CBOR
|
|
CBOR.decode(data)
|
|
when ENCODING_CUSTOM
|
|
@encode.load(data)
|
|
else
|
|
data
|
|
end
|
|
end
|
|
end
|
|
end
|