fic_tracker/lib/fic_tracker/util/cache.rb

199 lines
5.4 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, :compress
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