Skip to content

Commit

Permalink
Add caching for lookups from LDP.
Browse files Browse the repository at this point in the history
Also added Instrumentation and a log subscriber

If you run ActiveFedora in Rails, it injects a middleware that enables the cache
at the beginning of the request. Then if you ever ask for an object from
LDP, it caches the response and returns the cached result. At the end of
the request the cache is cleared. If you have a list of N objects and
you iterate over each to get an associated object (e.g. collection),
you'd have N loads. This prevents extra loads if the associated object
has already been loaded.
  • Loading branch information
jcoyne committed Jan 23, 2015
1 parent 17af32c commit 33c3095
Show file tree
Hide file tree
Showing 16 changed files with 263 additions and 11 deletions.
2 changes: 1 addition & 1 deletion active-fedora.gemspec
Expand Up @@ -22,7 +22,7 @@ Gem::Specification.new do |s|
s.add_dependency "rdf-rdfxml", '~> 1.1.0'
s.add_dependency "linkeddata"
s.add_dependency "deprecation"
s.add_dependency "ldp", '~> 0.2.0'
s.add_dependency "ldp", '~> 0.2.1'
s.add_dependency "rdf-ldp"

s.add_development_dependency "rdoc"
Expand Down
2 changes: 2 additions & 0 deletions lib/active_fedora.rb
Expand Up @@ -46,6 +46,7 @@ module ActiveFedora #:nodoc:
autoload :Attributes
autoload :AutosaveAssociation
autoload :Base
autoload :CachingConnection
autoload :Callbacks
autoload :ChangeSet
autoload :Config
Expand All @@ -62,6 +63,7 @@ module ActiveFedora #:nodoc:
autoload :FixityService
autoload :Indexing
autoload :IndexingService
autoload :LdpCache
autoload :LdpResource
autoload :LdpResourceService
autoload :LoadableFromJson
Expand Down
2 changes: 2 additions & 0 deletions lib/active_fedora/base.rb
Expand Up @@ -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

Expand All @@ -26,6 +27,7 @@ module ActiveFedora
class Base
extend ActiveModel::Naming
extend ActiveSupport::DescendantsTracker
extend LdpCache::ClassMethods

include Core
include Persistence
Expand Down
80 changes: 80 additions & 0 deletions lib/active_fedora/caching_connection.rb
@@ -0,0 +1,80 @@
module ActiveFedora
class CachingConnection < Ldp::Client
def initialize(host)
super
@cache = {}
@cache_enabled = false
end

def get(url, options = {})
if @cache_enabled
cache_resource(url) { super }
else
super
end
end

def post(*)
clear_cache if @cache_enabled
super
end

def put(*)
clear_cache if @cache_enabled
super
end

def patch(*)
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 log(url)
ActiveSupport::Notifications.instrument("ldp.active_fedora",
id: url, name: "Load LDP", ldp_service: object_id) { yield }
end

def cache_resource(url, &block)
result =
if @cache.key?(url)
ActiveSupport::Notifications.instrument("ldp.active_fedora",
id: url, name: "CACHE", ldp_service: object_id)
@cache[url]
else
@cache[url] = log(url) { yield }
end
result.dup
end
end
end
2 changes: 1 addition & 1 deletion lib/active_fedora/core.rb
Expand Up @@ -177,7 +177,7 @@ def init_internals
end

def build_ldp_resource(id=nil)
ActiveFedora.fedora.ldp_resource_service.get(self.class, id)
ActiveFedora.fedora.ldp_resource_service.build(self.class, id)
end

def check_persistence
Expand Down
2 changes: 1 addition & 1 deletion lib/active_fedora/fedora.rb
Expand Up @@ -14,7 +14,7 @@ def base_path
end

def connection
@connection ||= Ldp::Client.new(host)
@connection ||= CachingConnection.new(host)
end

def ldp_resource_service
Expand Down
46 changes: 46 additions & 0 deletions 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)
connection = ActiveFedora.fedora.connection
connection.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.connection.uncached(&block)
end
end

def initialize(app)
@app = app

This comment has been minimized.

Copy link
@awead

awead Jan 23, 2015

Contributor

Is this an attr_accessor from another class?

This comment has been minimized.

Copy link
@jcoyne

jcoyne Jan 23, 2015

Author Member

?

end

def call(env)

This comment has been minimized.

Copy link
@awead

awead Jan 23, 2015

Contributor

I'm not sure I follow what's going on here. Is this borrowed from Rails?

This comment has been minimized.

Copy link
@jcoyne
ActiveFedora.fedora.connection.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.connection.clear_cache
ActiveFedora.fedora.connection.disable_cache!
end

end
end

9 changes: 8 additions & 1 deletion lib/active_fedora/ldp_resource_service.rb
Expand Up @@ -6,17 +6,24 @@ def initialize(conn)
@connection = conn
end

def get(klass, id)
def build(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

# TODO break the cache.
def update(change_set, klass, id)

This comment has been minimized.

Copy link
@awead

awead Jan 23, 2015

Contributor

Are you bypassing the LDP client here and hitting Fedora directly?

This comment has been minimized.

SparqlInsert.new(change_set.changes).execute(to_uri(klass, id))
end


private
def to_uri(klass, id)
klass.id_to_uri(id)
end

end
end
38 changes: 38 additions & 0 deletions 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

4 changes: 1 addition & 3 deletions lib/active_fedora/persistence.rb
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions 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
Expand Down
5 changes: 2 additions & 3 deletions lib/active_fedora/relation/finder_methods.rb
Expand Up @@ -188,7 +188,7 @@ def find_one(id, cast=nil)

def load_from_fedora(id, cast)
raise ActiveFedora::ObjectNotFoundError if id.empty?
resource = ActiveFedora.fedora.ldp_resource_service.get(klass, id)
resource = ActiveFedora.fedora.ldp_resource_service.build(klass, id)
raise ActiveFedora::ObjectNotFoundError if resource.new?
class_to_load(resource, cast).allocate.init_with_resource(resource) # Triggers the find callback
end
Expand All @@ -202,9 +202,8 @@ def class_to_load(resource, cast)
end
end

# TODO just use has_model
def has_model_value(resource)
Ldp::Orm.new(resource).value(ActiveFedora::RDF::Fcrepo::Model.hasModel).first.to_s
resource.graph.query([nil, ActiveFedora::RDF::Fcrepo::Model.hasModel, nil]).first.object.to_s
end

def find_with_ids(ids, cast)
Expand Down
59 changes: 59 additions & 0 deletions spec/integration/caching_spec.rb
@@ -0,0 +1,59 @@
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_any_instance_of(Faraday::Connection).to receive(:get).once.and_call_original
ActiveFedora::Base.cache do
o1 = TestClass.find(object.id)
o2 = TestClass.find(object.id)
expect(o1.ldp_source.get.body.object_id).to eq o2.ldp_source.get.body.object_id
end
end

it "should clear the cache at the end of the block" do
expect_any_instance_of(Faraday::Connection).to receive(:get).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_any_instance_of(Faraday::Connection).to receive(:get).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_any_instance_of(Faraday::Connection).to receive(:get).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
1 change: 1 addition & 0 deletions spec/spec_helper.rb
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions spec/unit/ldp_resource_spec.rb
@@ -0,0 +1,17 @@
require 'spec_helper'

describe ActiveFedora::LdpResource do
let(:obj) { ActiveFedora::Base.create! }
let!(:r1) { ActiveFedora::LdpResource.new(ActiveFedora.fedora.connection, obj.uri) }
let!(:r2) { ActiveFedora::LdpResource.new(ActiveFedora.fedora.connection, obj.uri) }

it "should cache requests" do
expect_any_instance_of(Faraday::Connection).to receive(:get).once.and_call_original
ActiveFedora::Base.cache do
r1.get
r2.get
end
end


end
2 changes: 1 addition & 1 deletion 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
Expand Down

0 comments on commit 33c3095

Please sign in to comment.