diff --git a/lib/coursemology/evaluator.rb b/lib/coursemology/evaluator.rb index 32046a4..67b2d96 100644 --- a/lib/coursemology/evaluator.rb +++ b/lib/coursemology/evaluator.rb @@ -28,6 +28,9 @@ module Coursemology::Evaluator # The logger to use for the client. mattr_reader(:logger) { ActiveSupport::Logger.new(STDOUT) } + # The cache to use for the client. + mattr_reader(:cache) { ActiveSupport::Cache.lookup_store } + def self.eager_load! super Coursemology::Polyglot.eager_load! diff --git a/lib/coursemology/evaluator/docker_container.rb b/lib/coursemology/evaluator/docker_container.rb index 1e91ab0..560c1ec 100644 --- a/lib/coursemology/evaluator/docker_container.rb +++ b/lib/coursemology/evaluator/docker_container.rb @@ -15,12 +15,31 @@ def create(image, argv: nil) private + # Pulls the given image from Docker Hub. + # + # This caches images for 5 minutes, because the overhead for querying for images is quite high. + # + # @param [String] image The image to pull. def pull_image(image) ActiveSupport::Notifications.instrument('pull.docker.evaluator.coursemology', - image: image) do - Docker::Image.create('fromImage' => image) + image: image) do |payload| + cached([:image, image], expires_in: 5.minutes) do + Docker::Image.create('fromImage' => image) + payload[:cached] = false + end end end + + # Cache the result of the given block using the key given. + # + # @param [Array, String, Symbol] key The key to use. This will be expanded with + # +ActiveSupport::Cache.expand_cache_key+. + # @param [Hash] options The options to use. These are the same as + # +ActiveSupport::Cache::Store#fetch+. + def cached(key, options = {}, &proc) + key = ActiveSupport::Cache.expand_cache_key(key, name.underscore) + Coursemology::Evaluator.cache.fetch(key, options, &proc) + end end # Waits for the container to exit the Running state. diff --git a/lib/coursemology/evaluator/logging/docker_log_subscriber.rb b/lib/coursemology/evaluator/logging/docker_log_subscriber.rb index c0cc6b1..f02d128 100644 --- a/lib/coursemology/evaluator/logging/docker_log_subscriber.rb +++ b/lib/coursemology/evaluator/logging/docker_log_subscriber.rb @@ -1,7 +1,10 @@ # frozen_string_literal: true class Coursemology::Evaluator::Logging::DockerLogSubscriber < ActiveSupport::LogSubscriber def pull(event) - info "#{color("Docker Pull (#{event.duration.round(1)}ms)", GREEN)} #{event.payload[:image]}" + cached = event.payload[:cached].nil? || event.payload[:cached] ? 'Cached ' : '' + header_colour = cached ? GREEN : YELLOW + info "#{color("#{cached}Docker Pull (#{event.duration.round(1)}ms)", header_colour)} "\ + "#{event.payload[:image]}" end def create(event) diff --git a/spec/coursemology/evaluator/docker_container_spec.rb b/spec/coursemology/evaluator/docker_container_spec.rb index 5b152dd..721a735 100644 --- a/spec/coursemology/evaluator/docker_container_spec.rb +++ b/spec/coursemology/evaluator/docker_container_spec.rb @@ -15,6 +15,24 @@ end end + describe '.cached' do + let(:delete_subject) { false } + subject { Coursemology::Evaluator::DockerContainer } + + it 'caches the call' do + calls = 0 + cache_options = { expires_in: 1.second } + cache_miss_block = proc { calls += 1 } + subject.send(:cached, :test, cache_options, &cache_miss_block) + subject.send(:cached, :test, cache_options, &cache_miss_block) + expect(calls).to eq(1) + sleep(1.second) + + subject.send(:cached, :test, cache_options, &cache_miss_block) + expect(calls).to eq(2) + end + end + describe '#wait' do it 'retries until the container finishes' do subject.start!