Introduce header and Rails caching for schemas

This commit introduces cached project-scoped work packages schema.
Their caches are expired on a key based on

- the Project ID
- the Type ID
- the allowed custom fields for the given project

This commit only caches schema, not form endpoints.
This commit is contained in:
Oliver Günther
2016-04-12 11:43:34 +02:00
parent 3848c8aef0
commit 29d69a3fc5
9 changed files with 67 additions and 24 deletions
+2
View File
@@ -200,6 +200,8 @@ end
# API gems
gem 'grape', '~> 0.10.1'
gem 'grape-cache_control', '~> 1.0.1'
gem 'roar', '~> 1.0.0'
gem 'reform', '~> 1.2.6', require: false
+3
View File
@@ -286,6 +286,8 @@ GEM
rack-accept
rack-mount
virtus (>= 1.0.0)
grape-cache_control (1.0.1)
grape (~> 0.3)
gravatar_image_tag (1.2.0)
hashie (3.4.1)
health_check (1.5.1)
@@ -595,6 +597,7 @@ DEPENDENCIES
globalize (~> 5.0.1)
gon (~> 4.0)
grape (~> 0.10.1)
grape-cache_control (~> 1.0.1)
gravatar_image_tag (~> 1.2.0)
health_check
htmldiff
+26
View File
@@ -0,0 +1,26 @@
module API
module Caching
module Helpers
def with_etag!(key)
etag = %(W/"#{::Digest::SHA1.hexdigest(key.to_s)}")
error!('Not Modified'.freeze, 304) if headers['If-None-Match'.freeze] == etag
header 'ETag'.freeze, etag
end
##
# Store a represented object in its JSON representation
def cache(key, args = {})
# Save serialization since we're only dealing with strings here
args[:raw] = true
json = Rails.cache.fetch(key, args) {
result = yield
result.to_json
}
::API::Caching::StoredRepresenter.new json
end
end
end
end
+13
View File
@@ -0,0 +1,13 @@
module API
module Caching
class StoredRepresenter
def initialize(json)
@json = json
end
def to_json
@json
end
end
end
end
+1
View File
@@ -66,6 +66,7 @@ module API
use OpenProject::Authentication::Manager
helpers API::Caching::Helpers
helpers do
def current_user
User.current
@@ -126,9 +126,6 @@ module API
schema :spent_time,
type: 'Duration',
show_if: -> (_) do
current_user_allowed_to(:view_time_entries, context: represented.project)
end,
required: false
schema :percentage_done,
@@ -46,6 +46,10 @@ module API
def raise404
raise ::API::Errors::NotFound.new
end
def cache_key(project_id, type_id)
"api/v3/work_packages/schema/#{project_id}-#{type_id}"
end
end
# The schema identifier is an artificial identifier that is composed of a work packages
@@ -56,25 +60,30 @@ module API
namespace ':project-:type' do
before do
begin
project = Project.find(params[:project])
type = Type.find(params[:type])
@project = Project.find(params[:project])
@type = Type.find(params[:type])
rescue ActiveRecord::RecordNotFound
raise404
end
authorize(:view_work_packages, context: project) do
authorize(:view_work_packages, context: @project) do
raise404
end
schema = TypedWorkPackageSchema.new(project: project, type: type)
self_link = api_v3_paths.work_package_schema(project.id, type.id)
@representer = WorkPackageSchemaRepresenter.create(schema,
self_link: self_link,
current_user: current_user)
# Compare with ETag composed of project and customizations
# to avoid evaluating the server request
@custom_fields = @project.all_work_package_custom_fields
with_etag! "#{@project.id}/#{@custom_fields.count}/#{@custom_fields.to_param}"
end
get do
@representer
cache(key: [cache_key(@project.id, @type.id), @custom_fields]) do
schema = TypedWorkPackageSchema.new(project: @project, type: @type)
self_link = api_v3_paths.work_package_schema(@project.id, @type.id)
WorkPackageSchemaRepresenter.create(schema,
self_link: self_link,
current_user: nil)
end
end
end
@@ -280,18 +280,6 @@ describe ::API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do
let(:required) { false }
let(:writable) { false }
end
context 'not allowed to view time entries' do
before do
allow(current_user).to receive(:allowed_to?).with(:view_time_entries,
work_package.project)
.and_return false
end
it 'does not show spentTime' do
is_expected.not_to have_json_path('spentTime')
end
end
end
describe 'percentageDone' do
@@ -50,6 +50,10 @@ describe API::V3::WorkPackages::Schema::WorkPackageSchemasAPI, type: :request do
it 'should return HTTP 200' do
expect(last_response.status).to eql(200)
end
it 'should set a weak ETag' do
expect(last_response.headers['ETag']).to match(/W\/\"\w+\"/)
end
end
context 'id is too long' do