diff --git a/cache_store_redis/.rspec b/cache_store_redis/.rspec index 7e3eb5c..9c07434 100644 --- a/cache_store_redis/.rspec +++ b/cache_store_redis/.rspec @@ -1,3 +1,4 @@ +require spec_helper --color --require spec_helper --format doc diff --git a/cache_store_redis/lib/cache_store_redis.rb b/cache_store_redis/lib/cache_store_redis.rb index 098942d..bbfd0e2 100644 --- a/cache_store_redis/lib/cache_store_redis.rb +++ b/cache_store_redis/lib/cache_store_redis.rb @@ -5,212 +5,5 @@ require 'redis' require 'securerandom' -# This class is used to implement a redis cache store. -class RedisCacheStore - def initialize(namespace = nil, config = nil) - unless RUBY_PLATFORM == 'java' - require 'oj' - end - - @namespace = namespace - @config = config - @queue = Queue.new - - @connections_created = 0 - @connections_in_use = 0 - @mutex = Mutex.new - @enable_stats = false - end - - def enable_stats=(value) - @enable_stats = value - end - - def increment_created_stat - @mutex.synchronize do - @connections_created += 1 - end - end - - def increment_using_stat - @mutex.synchronize do - @connections_in_use += 1 - end - end - - def decrement_using_stat - @mutex.synchronize do - @connections_in_use -= 1 - end - end - - # This method is called to configure the connection to the cache store. - def configure(host = 'localhost', - port = 6379, - db = 'default', - password = nil, - driver: nil, - url: nil, - connect_timeout: 0.5, - read_timeout: 1, - write_timeout: 0.5) - if !url.nil? - @config = {} - @config[:url] = url - @config[:db] = db - else - @config = { host: host, port: port, db: db } - end - - @config[:password] = password unless password.nil? - @config[:driver] = driver unless driver.nil? - - @config[:connect_timeout] = connect_timeout - @config[:read_timeout] = read_timeout - @config[:write_timeout] = write_timeout - end - - def fetch_client - begin - @queue.pop(true) - rescue - increment_created_stat - Redis.new(@config) - end - end - - def clean - while @queue.length.positive? - client = @queue.pop(true) - client.close - end - end - - def log_stats - return unless @enable_stats == true - S1Logging.logger.debug do - "[#{self.class}] - REDIS Connection Stats. Process: #{Process.pid} | " \ -"Created: #{@connections_created} | Pending: #{@queue.length} | In use: #{@connections_in_use}" - end - end - - def with_client - log_stats - begin - client = fetch_client - increment_using_stat - log_stats - yield client - ensure - @queue.push(client) - decrement_using_stat - log_stats - end - end - - # This method is called to set a value within this cache store by it's key. - # - # @param key [String] This is the unique key to reference the value being set within this cache store. - # @param value [Object] This is the value to set within this cache store. - # @param expires_in [Integer] This is the number of seconds from the current time that this value should expire. - def set(key, value, expires_in = 0) - k = build_key(key) - - v = if value.nil? || (value.is_a?(String) && value.strip.empty?) - nil - else - serialize(value) - end - - with_client do |client| - client.multi do - client.set(k, v) - - client.expire(k, expires_in) if expires_in.positive? - end - end - end - - # This method is called to get a value from this cache store by it's unique key. - # - # @param key [String] This is the unique key to reference the value to fetch from within this cache store. - # @param expires_in [Integer] This is the number of seconds from the current time that this value should expire. - # (This is used in conjunction with the block to hydrate the cache key if it is empty.) - # @param &block [Block] This block is provided to hydrate this cache store with the value for the request key - # when it is not found. - # @return [Object] The value for the specified unique key within the cache store. - def get(key, expires_in = 0, &block) - k = build_key(key) - - value = with_client do |client| - client.get(k) - end - - if !value.nil? && value.strip.empty? - value = nil - else - value = deserialize(value) unless value.nil? - end - - if value.nil? && block_given? - value = yield - set(key, value, expires_in) - end - - value - end - - # This method is called to remove a value from this cache store by it's unique key. - # - # @param key [String] This is the unique key to reference the value to remove from this cache store. - def remove(key) - with_client do |client| - client.del(build_key(key)) - end - end - - # This method is called to check if a value exists within this cache store for a specific key. - # - # @param key [String] This is the unique key to reference the value to check for within this cache store. - # @return [Boolean] True or False to specify if the key exists in the cache store. - def exist?(key) - with_client do |client| - client.exists(build_key(key)) - end - end - - # Ping the cache store. - # - # @return [String] `PONG` - def ping - with_client do |client| - client.ping - end - end - - private - - def serialize(object) - if RUBY_PLATFORM == 'java' - Marshal::dump(object) - else - Oj.dump(object) - end - end - - def deserialize(object) - if RUBY_PLATFORM == 'java' - Marshal::load(object) - else - Oj.load(object) - end - end - - def build_key(key) - if !@namespace.nil? - @namespace + ':' + key.to_s - else - key.to_s - end - end -end +require_relative 'cache_store_redis/redis_cache_store' +require_relative 'cache_store_redis/optional_redis_cache_store' diff --git a/cache_store_redis/lib/cache_store_redis/optional_redis_cache_store.rb b/cache_store_redis/lib/cache_store_redis/optional_redis_cache_store.rb new file mode 100644 index 0000000..77dcb2f --- /dev/null +++ b/cache_store_redis/lib/cache_store_redis/optional_redis_cache_store.rb @@ -0,0 +1,97 @@ +require 'logger' + +# This class is used to define a redis cache store that logs failures as warnings but does not raise errors for +# cache connections +class OptionalRedisCacheStore + def initialize(namespace: nil, config: nil, logger: nil) + @cache_store = RedisCacheStore.new(namespace, config) + @logger = logger || Logger.new(STDOUT) + end + + def redis_store + @cache_store + end + + # This method is called to configure the connection to the cache store. + def configure( + host = 'localhost', + port = 6379, + db = 'default', + password = nil, + driver: nil, + url: nil, + connect_timeout: 0.5, + read_timeout: 1, + write_timeout: 0.5) + redis_store.configure( + host, + port, + db, + password, + driver: driver, + url: url, + connect_timeout: connect_timeout, + read_timeout: read_timeout, + write_timeout: write_timeout + ) + end + + def optional_get(key, expires_in = 0) + redis_store.get(key, expires_in) + rescue => e + @logger.error( + "[#{self.class}] - An error occurred requesting data from the cache. " \ +"Key: #{key} | Error: #{e.message} | Backtrace: #{e.backtrace}" + ) + nil + end + + def get(key, expires_in = 0, &block) + value = optional_get(key, expires_in) + + if value.nil? && block_given? + value = yield + set(key, value, expires_in) + end + + value + end + + def set(key, value, expires_in = 0) + redis_store.set(key, value, expires_in) + rescue => e + @logger.error( + "[#{self.class}] - An error occurred storing data in the cache. " \ +"Key: #{key} | Error: #{e.message} | Backtrace: #{e.backtrace}" + ) + end + + def remove(key) + redis_store.remove(key) + rescue => e + @logger.error( + "[#{self.class}] - An error occurred removing data from the cache. " \ +"Key: #{key} | Error: #{e.message} | Backtrace: #{e.backtrace}" + ) + end + + def exist?(key) + redis_store.exist?(key) + rescue => e + @logger.error( + "[#{self.class}] - An error occurred checking if a key exists in the cache. " \ +"Key: #{key} | Error: #{e.message} | Backtrace: #{e.backtrace}" + ) + false + end + + def ping + redis_store.ping + rescue => e + @logger.error( + "[#{self.class}] - An error occurred checking pinging the cache. " \ +"Error: #{e.message} | Backtrace: #{e.backtrace}" + ) + false + end +end diff --git a/cache_store_redis/lib/cache_store_redis/redis_cache_store.rb b/cache_store_redis/lib/cache_store_redis/redis_cache_store.rb new file mode 100644 index 0000000..4a06316 --- /dev/null +++ b/cache_store_redis/lib/cache_store_redis/redis_cache_store.rb @@ -0,0 +1,209 @@ +# This class is used to implement a redis cache store. +class RedisCacheStore + def initialize(namespace = nil, config = nil) + unless RUBY_PLATFORM == 'java' + require 'oj' + end + + @namespace = namespace + @config = config + @queue = Queue.new + + @connections_created = 0 + @connections_in_use = 0 + @mutex = Mutex.new + @enable_stats = false + end + + def enable_stats=(value) + @enable_stats = value + end + + def increment_created_stat + @mutex.synchronize do + @connections_created += 1 + end + end + + def increment_using_stat + @mutex.synchronize do + @connections_in_use += 1 + end + end + + def decrement_using_stat + @mutex.synchronize do + @connections_in_use -= 1 + end + end + + # This method is called to configure the connection to the cache store. + def configure(host = 'localhost', + port = 6379, + db = 'default', + password = nil, + driver: nil, + url: nil, + connect_timeout: 0.5, + read_timeout: 1, + write_timeout: 0.5) + if !url.nil? + @config = {} + @config[:url] = url + @config[:db] = db + else + @config = { host: host, port: port, db: db } + end + + @config[:password] = password unless password.nil? + @config[:driver] = driver unless driver.nil? + + @config[:connect_timeout] = connect_timeout + @config[:read_timeout] = read_timeout + @config[:write_timeout] = write_timeout + end + + def fetch_client + begin + @queue.pop(true) + rescue + increment_created_stat + Redis.new(@config) + end + end + + def clean + while @queue.length.positive? + client = @queue.pop(true) + client.close + end + end + + def log_stats + return unless @enable_stats == true + S1Logging.logger.debug do + "[#{self.class}] - REDIS Connection Stats. Process: #{Process.pid} | " \ +"Created: #{@connections_created} | Pending: #{@queue.length} | In use: #{@connections_in_use}" + end + end + + def with_client + log_stats + begin + client = fetch_client + increment_using_stat + log_stats + yield client + ensure + @queue.push(client) + decrement_using_stat + log_stats + end + end + + # This method is called to set a value within this cache store by it's key. + # + # @param key [String] This is the unique key to reference the value being set within this cache store. + # @param value [Object] This is the value to set within this cache store. + # @param expires_in [Integer] This is the number of seconds from the current time that this value should expire. + def set(key, value, expires_in = 0) + k = build_key(key) + + v = if value.nil? || (value.is_a?(String) && value.strip.empty?) + nil + else + serialize(value) + end + + with_client do |client| + client.multi do + client.set(k, v) + + client.expire(k, expires_in) if expires_in.positive? + end + end + end + + # This method is called to get a value from this cache store by it's unique key. + # + # @param key [String] This is the unique key to reference the value to fetch from within this cache store. + # @param expires_in [Integer] This is the number of seconds from the current time that this value should expire. + # (This is used in conjunction with the block to hydrate the cache key if it is empty.) + # @param &block [Block] This block is provided to hydrate this cache store with the value for the request key + # when it is not found. + # @return [Object] The value for the specified unique key within the cache store. + def get(key, expires_in = 0, &block) + k = build_key(key) + + value = with_client do |client| + client.get(k) + end + + if !value.nil? && value.strip.empty? + value = nil + else + value = deserialize(value) unless value.nil? + end + + if value.nil? && block_given? + value = yield + set(key, value, expires_in) + end + + value + end + + # This method is called to remove a value from this cache store by it's unique key. + # + # @param key [String] This is the unique key to reference the value to remove from this cache store. + def remove(key) + with_client do |client| + client.del(build_key(key)) + end + end + + # This method is called to check if a value exists within this cache store for a specific key. + # + # @param key [String] This is the unique key to reference the value to check for within this cache store. + # @return [Boolean] True or False to specify if the key exists in the cache store. + def exist?(key) + with_client do |client| + client.exists(build_key(key)) + end + end + + # Ping the cache store. + # + # @return [String] `PONG` + def ping + with_client do |client| + client.ping + end + end + + private + + def serialize(object) + if RUBY_PLATFORM == 'java' + Marshal::dump(object) + else + Oj.dump(object) + end + end + + def deserialize(object) + if RUBY_PLATFORM == 'java' + Marshal::load(object) + else + Oj.load(object) + end + end + + def build_key(key) + if !@namespace.nil? + @namespace + ':' + key.to_s + else + key.to_s + end + end +end diff --git a/cache_store_redis/lib/cache_store_redis/version.rb b/cache_store_redis/lib/cache_store_redis/version.rb index f322ba1..ea935a5 100644 --- a/cache_store_redis/lib/cache_store_redis/version.rb +++ b/cache_store_redis/lib/cache_store_redis/version.rb @@ -1,3 +1,3 @@ module CacheStoreRedis - VERSION = '0.5.5' + VERSION = '0.6.0' end diff --git a/cache_store_redis/spec/cache_store_redis_spec.rb b/cache_store_redis/spec/cache_store_redis_spec.rb index 802d496..4972653 100644 --- a/cache_store_redis/spec/cache_store_redis_spec.rb +++ b/cache_store_redis/spec/cache_store_redis_spec.rb @@ -1,14 +1,5 @@ -require 'spec_helper' -require_relative '../lib/cache_store_redis' - -class TestObject - attr_accessor :text - attr_accessor :numeric -end - describe RedisCacheStore do before do - # @cache_store = RedisCacheStore.new('test',{ url: 'redis://redis:6379'}) @cache_store = RedisCacheStore.new('test') @cache_store.configure(url: 'redis://redis:6379') end diff --git a/cache_store_redis/spec/optional_cache_store_redis_spec.rb b/cache_store_redis/spec/optional_cache_store_redis_spec.rb new file mode 100644 index 0000000..a860fcf --- /dev/null +++ b/cache_store_redis/spec/optional_cache_store_redis_spec.rb @@ -0,0 +1,86 @@ +describe OptionalRedisCacheStore do + before do + @cache_store = OptionalRedisCacheStore.new(namespace: 'test') + @cache_store.configure(url: 'redis://redis:6379') + end + + describe '#set' do + let(:key) { 'setkey' } + let(:value) { double } + it 'should pass the key/value to the redis_store' do + expect(@cache_store.redis_store).to receive(:set).with(key, value, 0).once + @cache_store.set(key, value) + end + context 'when an error occurs' do + before do + allow(@cache_store.redis_store).to receive(:set).and_raise(StandardError) + end + it 'does not raise error' do + expect{ @cache_store.set(key, value) }.not_to raise_error + end + end + end + + describe '#get' do + let(:key) { 'getkey' } + it 'requests the key from the redis_store' do + expect(@cache_store.redis_store).to receive(:get).with(key, 0).once + @cache_store.get(key) + end + context 'when an error occurs' do + before do + allow(@cache_store.redis_store).to receive(:get).and_raise(StandardError) + end + it 'returns nil' do + expect(@cache_store.get(key)).to be nil + end + end + end + + describe '#exist?' do + let(:key) { 'exists_key' } + it 'should pass the key to the redis_store' do + expect(@cache_store.redis_store).to receive(:exist?).with(key).once + @cache_store.exist?(key) + end + context 'when an error occurs' do + before do + allow(@cache_store.redis_store).to receive(:exist?).and_raise(StandardError) + end + it 'returns false' do + expect(@cache_store.exist?(key)).to be false + end + end + end + + describe '#remove' do + let(:key) { 'remove_key' } + it 'should pass the key to the redis_store' do + expect(@cache_store.redis_store).to receive(:remove).with(key).once + @cache_store.remove(key) + end + context 'when an error occurs' do + before do + allow(@cache_store.redis_store).to receive(:remove).and_raise(StandardError) + end + it 'does not raise error' do + expect{ @cache_store.remove(key) }.not_to raise_error + end + end + end + + describe '#ping' do + it 'should call ping on the redis_store' do + expect(@cache_store.redis_store).to receive(:ping).once + @cache_store.ping + end + context 'when an error occurs' do + before do + allow(@cache_store.redis_store).to receive(:ping).and_raise(StandardError) + end + it 'returns false' do + expect(@cache_store.ping).to be false + end + end + end +end diff --git a/cache_store_redis/spec/spec_helper.rb b/cache_store_redis/spec/spec_helper.rb index 03cd089..2afaa39 100644 --- a/cache_store_redis/spec/spec_helper.rb +++ b/cache_store_redis/spec/spec_helper.rb @@ -1,4 +1,6 @@ require 'pry' +require_relative 'test_object' +require_relative '../lib/cache_store_redis' RSpec.configure do |config| # rspec-expectations config goes here. You can use an alternate @@ -24,4 +26,4 @@ mocks.verify_partial_doubles = true end -end \ No newline at end of file +end diff --git a/cache_store_redis/spec/test_object.rb b/cache_store_redis/spec/test_object.rb new file mode 100644 index 0000000..a47e34a --- /dev/null +++ b/cache_store_redis/spec/test_object.rb @@ -0,0 +1,4 @@ +class TestObject + attr_accessor :text + attr_accessor :numeric +end