From e4ad57b74b80bbdd3bfc1a7bb9f91f4bd449b108 Mon Sep 17 00:00:00 2001 From: Justin Coyne Date: Thu, 22 Jan 2015 11:30:15 -0600 Subject: [PATCH] Add caching for lookups from LDP. Also added Instrumentation and a log subscriber --- lib/active_fedora.rb | 2 + lib/active_fedora/base.rb | 2 + lib/active_fedora/ldp_cache.rb | 46 ++++++++++++++++ lib/active_fedora/ldp_caching_service.rb | 66 +++++++++++++++++++++++ lib/active_fedora/ldp_resource_service.rb | 42 ++++++++++----- lib/active_fedora/log_subscriber.rb | 38 +++++++++++++ lib/active_fedora/persistence.rb | 4 +- lib/active_fedora/railtie.rb | 3 ++ spec/integration/caching_spec.rb | 58 ++++++++++++++++++++ spec/spec_helper.rb | 1 + spec/unit/logger_spec.rb | 2 +- 11 files changed, 248 insertions(+), 16 deletions(-) create mode 100644 lib/active_fedora/ldp_cache.rb create mode 100644 lib/active_fedora/ldp_caching_service.rb create mode 100644 lib/active_fedora/log_subscriber.rb create mode 100644 spec/integration/caching_spec.rb diff --git a/lib/active_fedora.rb b/lib/active_fedora.rb index ec7d472f7..7d48736cb 100644 --- a/lib/active_fedora.rb +++ b/lib/active_fedora.rb @@ -62,6 +62,8 @@ module ActiveFedora #:nodoc: autoload :FixityService autoload :Indexing autoload :IndexingService + autoload :LdpCache + autoload :LdpCachingService autoload :LdpResource autoload :LdpResourceService autoload :LoadableFromJson diff --git a/lib/active_fedora/base.rb b/lib/active_fedora/base.rb index 27681e1ae..d312497c4 100644 --- a/lib/active_fedora/base.rb +++ b/lib/active_fedora/base.rb @@ -2,6 +2,7 @@ ENABLE_SOLR_UPDATES = true unless defined?(ENABLE_SOLR_UPDATES) require 'active_support/descendants_tracker' require 'active_fedora/errors' +require 'active_fedora/log_subscriber' module ActiveFedora @@ -26,6 +27,7 @@ module ActiveFedora class Base extend ActiveModel::Naming extend ActiveSupport::DescendantsTracker + extend LdpCache::ClassMethods include Core include Persistence diff --git a/lib/active_fedora/ldp_cache.rb b/lib/active_fedora/ldp_cache.rb new file mode 100644 index 000000000..24225b888 --- /dev/null +++ b/lib/active_fedora/ldp_cache.rb @@ -0,0 +1,46 @@ +module ActiveFedora + # = Active Fedora Ldp Cache + class LdpCache + module ClassMethods + # Enable the query cache within the block if Active Fedora is configured. + # If it's not, it will execute the given block. + def cache(&block) + service = ActiveFedora.fedora.ldp_resource_service + service.cache(&block) + end + + # Disable the query cache within the block if Active Fedora is configured. + # If it's not, it will execute the given block. + def uncached(&block) + ActiveFedora.fedora.ldp_resource_service.uncached(&block) + end + end + + def initialize(app) + @app = app + end + + def call(env) + ActiveFedora.fedora.ldp_resource_service.enable_cache! + + response = @app.call(env) + response[2] = Rack::BodyProxy.new(response[2]) do + reset_cache_settings + end + + response + rescue Exception => e + reset_cache_settings + raise e + end + + private + + def reset_cache_settings + ActiveFedora.fedora.ldp_resource_service.clear_cache + ActiveFedora.fedora.ldp_resource_service.disable_cache! + end + + end +end + diff --git a/lib/active_fedora/ldp_caching_service.rb b/lib/active_fedora/ldp_caching_service.rb new file mode 100644 index 000000000..ffa5913bb --- /dev/null +++ b/lib/active_fedora/ldp_caching_service.rb @@ -0,0 +1,66 @@ +module ActiveFedora + module LdpCachingService + + def initialize(*) + super + @cache = {} + @cache_enabled = false + end + + def get(klass, id) + if @cache_enabled + cache_resource(id) { super } + else + super + end + end + + def update(*) + clear_cache if @cache_enabled + super + end + + # Enable the cache within the block. + def cache + old, @cache_enabled = @cache_enabled, true + yield + ensure + @cache_enabled = old + clear_cache unless @cache_enabled + end + + def enable_cache! + @cache_enabled = true + end + + def disable_cache! + @cache_enabled = false + end + + # Disable the query cache within the block. + def uncached + old, @cache_enabled = @cache_enabled, false + yield + ensure + @cache_enabled = old + end + + def clear_cache + @cache.clear + end + + private + + def cache_resource(id, &block) + result = + if @cache.key?(id) + ActiveSupport::Notifications.instrument("ldp.active_fedora", + id: id, name: "CACHE", ldp_service: object_id) + @cache[id] + else + @cache[id] = yield + end + result.dup + end + end +end diff --git a/lib/active_fedora/ldp_resource_service.rb b/lib/active_fedora/ldp_resource_service.rb index feff15b08..3adf69182 100644 --- a/lib/active_fedora/ldp_resource_service.rb +++ b/lib/active_fedora/ldp_resource_service.rb @@ -1,22 +1,40 @@ module ActiveFedora class LdpResourceService + module LdpOperations + def get(klass, id) + log(klass, id) do + if id + LdpResource.new(connection, to_uri(klass, id)) + else + LdpResource.new(connection, nil, nil, ActiveFedora.fedora.host + ActiveFedora.fedora.base_path) + end + end + end + + # TODO break the cache. + def update(change_set, klass, id) + SparqlInsert.new(change_set.changes).execute(to_uri(klass, id)) + end + + + private + def log(klass, id) + ActiveSupport::Notifications.instrument("ldp.active_fedora", + id: id, name: "Load #{klass}", ldp_service: object_id) { yield } + end + + def to_uri(klass, id) + klass.id_to_uri(id) + end + end attr_reader :connection + include LdpOperations + include LdpCachingService def initialize(conn) @connection = conn + super() end - def get(klass, id) - if id - LdpResource.new(connection, to_uri(klass, id)) - else - LdpResource.new(connection, nil, nil, ActiveFedora.fedora.host + ActiveFedora.fedora.base_path) - end - end - - private - def to_uri(klass, id) - klass.id_to_uri(id) - end end end diff --git a/lib/active_fedora/log_subscriber.rb b/lib/active_fedora/log_subscriber.rb new file mode 100644 index 000000000..7e1b85ea6 --- /dev/null +++ b/lib/active_fedora/log_subscriber.rb @@ -0,0 +1,38 @@ +module ActiveFedora + class LogSubscriber < ActiveSupport::LogSubscriber + + def initialize + super + @odd = false + end + + def ldp(event) + return unless logger.debug? + + payload = event.payload + + name = "#{payload[:name]} (#{event.duration.round(1)}ms)" + id = payload[:id] || "[no id]" + + if odd? + name = color(name, CYAN, true) + id = color(id, nil, true) + else + name = color(name, MAGENTA, true) + end + + debug " #{name} #{id} Service: #{payload[:ldp_service]}" + end + + def odd? + @odd = !@odd + end + + def logger + ActiveFedora::Base.logger + end + end +end + +ActiveFedora::LogSubscriber.attach_to :active_fedora + diff --git a/lib/active_fedora/persistence.rb b/lib/active_fedora/persistence.rb index c2c3aa79e..f9c4333ef 100644 --- a/lib/active_fedora/persistence.rb +++ b/lib/active_fedora/persistence.rb @@ -152,16 +152,14 @@ def update_record(options = {}) end def refresh - # TODO break the cache @ldp_source = build_ldp_resource(id) @resource = nil end - # TODO break the cache. def execute_sparql_update change_set = ChangeSet.new(self, self.resource, self.changed_attributes.keys) return true if change_set.empty? - SparqlInsert.new(change_set.changes).execute(uri) + ActiveFedora.fedora.ldp_resource_service.update(change_set, self.class, id) end # Override to tie in an ID minting service diff --git a/lib/active_fedora/railtie.rb b/lib/active_fedora/railtie.rb index 1f09d8cfd..463b47dad 100644 --- a/lib/active_fedora/railtie.rb +++ b/lib/active_fedora/railtie.rb @@ -1,6 +1,9 @@ module ActiveFedora class Railtie < Rails::Railtie + config.app_middleware.insert_after "::ActionDispatch::Callbacks", + "ActiveFedora::LdpCache" + initializer 'active_fedora.autoload', before: :set_autoload_paths do |app| app.config.autoload_paths << 'app/models/datastreams' end diff --git a/spec/integration/caching_spec.rb b/spec/integration/caching_spec.rb new file mode 100644 index 000000000..5b9a5b3a5 --- /dev/null +++ b/spec/integration/caching_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' + +describe "Caching" do + before do + class TestClass < ActiveFedora::Base + property :title, predicate: ::RDF::DC.title + end + end + + after { Object.send(:remove_const, :TestClass) } + + let!(:object) { TestClass.create(id: '123') } + + describe "#cache" do + it "should find records in the cache" do + expect(ActiveFedora::LdpResource).to receive(:new).once.and_call_original + ActiveFedora::Base.cache do + TestClass.find(object.id) + TestClass.find(object.id) + end + end + + it "should clear the cache at the end of the block" do + expect(ActiveFedora::LdpResource).to receive(:new).twice.and_call_original + ActiveFedora::Base.cache do + TestClass.find(object.id) + end + ActiveFedora::Base.cache do + TestClass.find(object.id) + end + end + + context "an update" do + it "should flush the cache" do + expect(ActiveFedora::LdpResource).to receive(:new).twice.and_call_original + ActiveFedora::Base.cache do + TestClass.find(object.id) + object.title= ['foo'] + object.save! + TestClass.find(object.id) + end + end + end + end + + describe "#uncached" do + it "should not use the cache" do + expect(ActiveFedora::LdpResource).to receive(:new).twice.and_call_original + ActiveFedora::Base.cache do + TestClass.find(object.id) + ActiveFedora::Base.uncached do + TestClass.find(object.id) + end + TestClass.find(object.id) + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 33f9b95d2..6aaa09e02 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -20,6 +20,7 @@ ActiveFedora::Base.logger = Logger.new(STDERR) ActiveFedora::Base.logger.level = Logger::WARN +# require 'http_logger' # HttpLogger.logger = Logger.new(STDOUT) # HttpLogger.ignore = [/localhost:8983\/solr/] # HttpLogger.colorize = false diff --git a/spec/unit/logger_spec.rb b/spec/unit/logger_spec.rb index 885fe5a3e..463799da3 100644 --- a/spec/unit/logger_spec.rb +++ b/spec/unit/logger_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe ActiveFedora::Base do - let(:logger1) { double } + let(:logger1) { double(debug?: false) } before do @initial_logger = ActiveFedora::Base.logger