diff --git a/bin/expiration_worker b/bin/expiration_worker new file mode 100755 index 00000000..27be4506 --- /dev/null +++ b/bin/expiration_worker @@ -0,0 +1,13 @@ +#!/usr/bin/env ruby + +require 'identity_cache' + +# This is meant to run a long lived process along these lines +# Parse args from command line / env then call +# +# IdentityCache::BinlogExpirationWorker.new(*args).run +# +# BinlongExpiration worker should be able to receive binlog events +# and based on those events expire the cache + + diff --git a/lib/identity_cache.rb b/lib/identity_cache.rb index 87520f4e..6e3fbb73 100644 --- a/lib/identity_cache.rb +++ b/lib/identity_cache.rb @@ -40,6 +40,8 @@ require "identity_cache/fallback_fetcher" require 'identity_cache/without_primary_index' require 'identity_cache/with_primary_index' +require 'identity_cache/noop_expirator' +require 'identity_cache/inline_expirator' module IdentityCache extend ActiveSupport::Concern @@ -50,6 +52,7 @@ module IdentityCache BATCH_SIZE = 1000 DELETED = :idc_cached_deleted DELETED_TTL = 1000 + EXPIRATION_STRATEGIES = {inline: InlineExpirator, worker: NoopExpirator} class AlreadyIncludedError < StandardError; end class AssociationError < StandardError; end @@ -57,6 +60,14 @@ class InverseAssociationError < StandardError; end class UnsupportedScopeError < StandardError; end class UnsupportedAssociationError < StandardError; end class DerivedModelError < StandardError; end + class ExpirationStrategyNotFound < StandardError + attr_reader :strategy + def initialize(strategy=nil) + msg = "#{strategy.to_s} is not a valid expiration strategy" + super(msg) + end + end + mattr_accessor :cache_namespace self.cache_namespace = "IDC:#{CACHE_VERSION}:" @@ -67,6 +78,9 @@ class DerivedModelError < StandardError; end mattr_accessor :fetch_read_only_records self.fetch_read_only_records = true + mattr_accessor :expiration_strategy + self.expiration_strategy = :inline + class << self include IdentityCache::CacheHash @@ -132,6 +146,24 @@ def unmap_cached_nil_for(value) value == IdentityCache::CACHED_NIL ? nil : value end + def reset_expiration_strategy(strategy) + @expirator = nil + self.expiration_strategy = strategy + end + + def expirator + return @expirator if defined?(@expirator) and @expirator + + strategy = self.expiration_strategy + + unless EXPIRATION_STRATEGIES[strategy] + raise ExpirationStrategyNotFound.new(strategy) + end + + @expirator = EXPIRATION_STRATEGIES[strategy].new + + end + # Same as +fetch+, except that it will try a collection of keys, using the # multiget operation of the cache adaptor. # diff --git a/lib/identity_cache/cache_key_loader.rb b/lib/identity_cache/cache_key_loader.rb index 4025858f..c0eb923b 100644 --- a/lib/identity_cache/cache_key_loader.rb +++ b/lib/identity_cache/cache_key_loader.rb @@ -31,6 +31,7 @@ def load(cache_fetcher, db_key) db_value = nil cache_value = IdentityCache.fetch(cache_key) do + IdentityCache.logger.debug "Resolving miss key=#{self.name} db_key=#{db_key}" db_value = cache_fetcher.load_one_from_db(db_key) cache_fetcher.cache_encode(db_value) end diff --git a/lib/identity_cache/cached/attribute.rb b/lib/identity_cache/cached/attribute.rb index 81b23039..b7102ec2 100644 --- a/lib/identity_cache/cached/attribute.rb +++ b/lib/identity_cache/cached/attribute.rb @@ -37,12 +37,13 @@ def fetch(db_key) def expire(record) unless record.send(:was_new_record?) old_key = old_cache_key(record) - IdentityCache.cache.delete(old_key) + IdentityCache.expirator.expire(old_key) end + unless record.destroyed? new_key = new_cache_key(record) if new_key != old_key - IdentityCache.cache.delete(new_key) + IdentityCache.expirator.expire(new_key) end end end diff --git a/lib/identity_cache/cached/primary_index.rb b/lib/identity_cache/cached/primary_index.rb index d3e3beff..cac068d0 100644 --- a/lib/identity_cache/cached/primary_index.rb +++ b/lib/identity_cache/cached/primary_index.rb @@ -43,8 +43,8 @@ def fetch_multi(ids) end def expire(id) - id = cast_id(id) - IdentityCache.cache.delete(cache_key(id)) + key = cache_key(cast_id(id)) + IdentityCache.expirator.expire(key) end def cache_key(id) diff --git a/lib/identity_cache/inline_expirator.rb b/lib/identity_cache/inline_expirator.rb new file mode 100644 index 00000000..8b32d0b3 --- /dev/null +++ b/lib/identity_cache/inline_expirator.rb @@ -0,0 +1,8 @@ +module IdentityCache + class InlineExpirator + def expire(key) + IdentityCache.logger.debug "Expiring key=#{key}" + IdentityCache.cache.delete(key) + end + end +end \ No newline at end of file diff --git a/lib/identity_cache/noop_expirator.rb b/lib/identity_cache/noop_expirator.rb new file mode 100644 index 00000000..c66f7aa5 --- /dev/null +++ b/lib/identity_cache/noop_expirator.rb @@ -0,0 +1,7 @@ +module IdentityCache + class NoopExpirator + def expire(*) + #NOOP + end + end +end \ No newline at end of file diff --git a/test/identity_cache_test.rb b/test/identity_cache_test.rb index 381fc4e5..05216ea6 100644 --- a/test/identity_cache_test.rb +++ b/test/identity_cache_test.rb @@ -26,4 +26,24 @@ def test_should_use_cache_in_transaction assert_equal false, IdentityCache.should_use_cache? end end + + def test_should_use_inline_expirator_by_default + assert_instance_of IdentityCache::InlineExpirator, IdentityCache.expirator + end + + def test_should_be_able_to_set_expirator_to_worker + with_expiration_strategy(:worker) do + assert_instance_of IdentityCache::NoopExpirator, IdentityCache.expirator + end + end + + def test_should_raise_when_expiration_strategy_is_not_supported + error = assert_raises(IdentityCache::ExpirationStrategyNotFound) do + with_expiration_strategy(:hope) do + IdentityCache.expirator + end + end + + assert_equal "hope is not a valid expiration strategy", error.message + end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 0268f8b5..eb598f96 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -49,6 +49,16 @@ def teardown teardown_models end + def with_expiration_strategy(new_strategy, &b) + old_strategy = IdentityCache.expiration_strategy + IdentityCache.reset_expiration_strategy(new_strategy) + yield + + ensure + + IdentityCache.reset_expiration_strategy(old_strategy) + end + private def create(class_symbol)