Commit 55ca2da7 authored by Sean McGivern's avatar Sean McGivern

Merge branch 'smarter-cache-invalidation' into 'master'

Smarter cache invalidation

Fixes https://gitlab.com/gitlab-org/gitlab-ce/issues/23550

See merge request !7360
parents d3236af1 289e4336
...@@ -1086,7 +1086,7 @@ class Project < ActiveRecord::Base ...@@ -1086,7 +1086,7 @@ class Project < ActiveRecord::Base
"refs/heads/#{branch}", "refs/heads/#{branch}",
force: true) force: true)
repository.copy_gitattributes(branch) repository.copy_gitattributes(branch)
repository.expire_avatar_cache(branch) repository.expire_avatar_cache
reload_default_branch reload_default_branch
end end
......
require 'securerandom' require 'securerandom'
class Repository class Repository
include Gitlab::ShellAdapter
attr_accessor :path_with_namespace, :project
class CommitError < StandardError; end class CommitError < StandardError; end
# Files to use as a project avatar in case no avatar was uploaded via the web # Methods that cache data from the Git repository.
# UI. #
AVATAR_FILES = %w{logo.png logo.jpg logo.gif} # Each entry in this Array should have a corresponding method with the exact
# same name. The cache key used by those methods must also match method's
# name.
#
# For example, for entry `:readme` there's a method called `readme` which
# stores its data in the `readme` cache key.
CACHED_METHODS = %i(size commit_count readme version contribution_guide
changelog license_blob license_key gitignore koding_yml
gitlab_ci_yml branch_names tag_names branch_count
tag_count avatar exists? empty? root_ref)
# Certain method caches should be refreshed when certain types of files are
# changed. This Hash maps file types (as returned by Gitlab::FileDetector) to
# the corresponding methods to call for refreshing caches.
METHOD_CACHES_FOR_FILE_TYPES = {
readme: :readme,
changelog: :changelog,
license: %i(license_blob license_key),
contributing: :contribution_guide,
version: :version,
gitignore: :gitignore,
koding: :koding_yml,
gitlab_ci: :gitlab_ci_yml,
avatar: :avatar
}
# Wraps around the given method and caches its output in Redis and an instance
# variable.
#
# This only works for methods that do not take any arguments.
def self.cache_method(name, fallback: nil)
original = :"_uncached_#{name}"
include Gitlab::ShellAdapter alias_method(original, name)
attr_accessor :path_with_namespace, :project define_method(name) do
cache_method_output(name, fallback: fallback) { __send__(original) }
end
end
def self.storages def self.storages
Gitlab.config.repositories.storages Gitlab.config.repositories.storages
...@@ -37,24 +75,6 @@ class Repository ...@@ -37,24 +75,6 @@ class Repository
) )
end end
def exists?
return @exists unless @exists.nil?
@exists = cache.fetch(:exists?) do
begin
raw_repository && raw_repository.rugged ? true : false
rescue Gitlab::Git::Repository::NoRepository
false
end
end
end
def empty?
return @empty unless @empty.nil?
@empty = cache.fetch(:empty?) { raw_repository.empty? }
end
# #
# Git repository can contains some hidden refs like: # Git repository can contains some hidden refs like:
# /refs/notes/* # /refs/notes/*
...@@ -221,10 +241,6 @@ class Repository ...@@ -221,10 +241,6 @@ class Repository
branch_names + tag_names branch_names + tag_names
end end
def branch_names
@branch_names ||= cache.fetch(:branch_names) { branches.map(&:name) }
end
def branch_exists?(branch_name) def branch_exists?(branch_name)
branch_names.include?(branch_name) branch_names.include?(branch_name)
end end
...@@ -274,34 +290,6 @@ class Repository ...@@ -274,34 +290,6 @@ class Repository
ref_exists?(keep_around_ref_name(sha)) ref_exists?(keep_around_ref_name(sha))
end end
def tag_names
cache.fetch(:tag_names) { raw_repository.tag_names }
end
def commit_count
cache.fetch(:commit_count) do
begin
raw_repository.commit_count(self.root_ref)
rescue
0
end
end
end
def branch_count
@branch_count ||= cache.fetch(:branch_count) { branches.size }
end
def tag_count
@tag_count ||= cache.fetch(:tag_count) { raw_repository.rugged.tags.count }
end
# Return repo size in megabytes
# Cached in redis
def size
cache.fetch(:size) { raw_repository.size }
end
def diverging_commit_counts(branch) def diverging_commit_counts(branch)
root_ref_hash = raw_repository.rev_parse_target(root_ref).oid root_ref_hash = raw_repository.rev_parse_target(root_ref).oid
cache.fetch(:"diverging_commit_counts_#{branch.name}") do cache.fetch(:"diverging_commit_counts_#{branch.name}") do
...@@ -317,48 +305,55 @@ class Repository ...@@ -317,48 +305,55 @@ class Repository
end end
end end
# Keys for data that can be affected for any commit push. def expire_tags_cache
def cache_keys expire_method_caches(%i(tag_names tag_count))
%i(size commit_count @tags = nil
readme version contribution_guide changelog
license_blob license_key gitignore koding_yml)
end end
# Keys for data on branch/tag operations. def expire_branches_cache
def cache_keys_for_branches_and_tags expire_method_caches(%i(branch_names branch_count))
%i(branch_names tag_names branch_count tag_count) @local_branches = nil
end end
def build_cache def expire_statistics_caches
(cache_keys + cache_keys_for_branches_and_tags).each do |key| expire_method_caches(%i(size commit_count))
unless cache.exist?(key)
send(key)
end
end
end end
def expire_tags_cache def expire_all_method_caches
cache.expire(:tag_names) expire_method_caches(CACHED_METHODS)
@tags = nil
end end
def expire_branches_cache # Expires the caches of a specific set of methods
cache.expire(:branch_names) def expire_method_caches(methods)
@branch_names = nil methods.each do |key|
@local_branches = nil cache.expire(key)
ivar = cache_instance_variable_name(key)
remove_instance_variable(ivar) if instance_variable_defined?(ivar)
end
end end
def expire_cache(branch_name = nil, revision = nil) def expire_avatar_cache
cache_keys.each do |key| expire_method_caches(%i(avatar))
cache.expire(key) end
# Refreshes the method caches of this repository.
#
# types - An Array of file types (e.g. `:readme`) used to refresh extra
# caches.
def refresh_method_caches(types)
to_refresh = []
types.each do |type|
methods = METHOD_CACHES_FOR_FILE_TYPES[type.to_sym]
to_refresh.concat(Array(methods)) if methods
end end
expire_branch_cache(branch_name) expire_method_caches(to_refresh)
expire_avatar_cache(branch_name, revision)
# This ensures this particular cache is flushed after the first commit to a to_refresh.each { |method| send(method) }
# new repository.
expire_emptiness_caches if empty?
end end
def expire_branch_cache(branch_name = nil) def expire_branch_cache(branch_name = nil)
...@@ -377,15 +372,14 @@ class Repository ...@@ -377,15 +372,14 @@ class Repository
end end
def expire_root_ref_cache def expire_root_ref_cache
cache.expire(:root_ref) expire_method_caches(%i(root_ref))
@root_ref = nil
end end
# Expires the cache(s) used to determine if a repository is empty or not. # Expires the cache(s) used to determine if a repository is empty or not.
def expire_emptiness_caches def expire_emptiness_caches
cache.expire(:empty?) return unless empty?
@empty = nil
expire_method_caches(%i(empty?))
expire_has_visible_content_cache expire_has_visible_content_cache
end end
...@@ -394,51 +388,22 @@ class Repository ...@@ -394,51 +388,22 @@ class Repository
@has_visible_content = nil @has_visible_content = nil
end end
def expire_branch_count_cache
cache.expire(:branch_count)
@branch_count = nil
end
def expire_tag_count_cache
cache.expire(:tag_count)
@tag_count = nil
end
def lookup_cache def lookup_cache
@lookup_cache ||= {} @lookup_cache ||= {}
end end
def expire_avatar_cache(branch_name = nil, revision = nil)
# Avatars are pulled from the default branch, thus if somebody pushes to a
# different branch there's no need to expire anything.
return if branch_name && branch_name != root_ref
# We don't want to flush the cache if the commit didn't actually make any
# changes to any of the possible avatar files.
if revision && commit = self.commit(revision)
return unless commit.raw_diffs(deltas_only: true).
any? { |diff| AVATAR_FILES.include?(diff.new_path) }
end
cache.expire(:avatar)
@avatar = nil
end
def expire_exists_cache def expire_exists_cache
cache.expire(:exists?) expire_method_caches(%i(exists?))
@exists = nil
end end
# expire cache that doesn't depend on repository data (when expiring) # expire cache that doesn't depend on repository data (when expiring)
def expire_content_cache def expire_content_cache
expire_tags_cache expire_tags_cache
expire_tag_count_cache
expire_branches_cache expire_branches_cache
expire_branch_count_cache
expire_root_ref_cache expire_root_ref_cache
expire_emptiness_caches expire_emptiness_caches
expire_exists_cache expire_exists_cache
expire_statistics_caches
end end
# Runs code after a repository has been created. # Runs code after a repository has been created.
...@@ -453,9 +418,8 @@ class Repository ...@@ -453,9 +418,8 @@ class Repository
# Runs code just before a repository is deleted. # Runs code just before a repository is deleted.
def before_delete def before_delete
expire_exists_cache expire_exists_cache
expire_all_method_caches
expire_cache if exists? expire_branch_cache if exists?
expire_content_cache expire_content_cache
repository_event(:remove_repository) repository_event(:remove_repository)
...@@ -472,9 +436,9 @@ class Repository ...@@ -472,9 +436,9 @@ class Repository
# Runs code before pushing (= creating or removing) a tag. # Runs code before pushing (= creating or removing) a tag.
def before_push_tag def before_push_tag
expire_cache expire_statistics_caches
expire_emptiness_caches
expire_tags_cache expire_tags_cache
expire_tag_count_cache
repository_event(:push_tag) repository_event(:push_tag)
end end
...@@ -482,7 +446,7 @@ class Repository ...@@ -482,7 +446,7 @@ class Repository
# Runs code before removing a tag. # Runs code before removing a tag.
def before_remove_tag def before_remove_tag
expire_tags_cache expire_tags_cache
expire_tag_count_cache expire_statistics_caches
repository_event(:remove_tag) repository_event(:remove_tag)
end end
...@@ -494,12 +458,14 @@ class Repository ...@@ -494,12 +458,14 @@ class Repository
# Runs code after a repository has been forked/imported. # Runs code after a repository has been forked/imported.
def after_import def after_import
expire_content_cache expire_content_cache
build_cache expire_tags_cache
expire_branches_cache
end end
# Runs code after a new commit has been pushed. # Runs code after a new commit has been pushed.
def after_push_commit(branch_name, revision) def after_push_commit(branch_name)
expire_cache(branch_name, revision) expire_statistics_caches
expire_branch_cache(branch_name)
repository_event(:push_commit, branch: branch_name) repository_event(:push_commit, branch: branch_name)
end end
...@@ -508,7 +474,6 @@ class Repository ...@@ -508,7 +474,6 @@ class Repository
def after_create_branch def after_create_branch
expire_branches_cache expire_branches_cache
expire_has_visible_content_cache expire_has_visible_content_cache
expire_branch_count_cache
repository_event(:push_branch) repository_event(:push_branch)
end end
...@@ -523,7 +488,6 @@ class Repository ...@@ -523,7 +488,6 @@ class Repository
# Runs code after an existing branch has been removed. # Runs code after an existing branch has been removed.
def after_remove_branch def after_remove_branch
expire_has_visible_content_cache expire_has_visible_content_cache
expire_branch_count_cache
expire_branches_cache expire_branches_cache
end end
...@@ -550,86 +514,127 @@ class Repository ...@@ -550,86 +514,127 @@ class Repository
Gitlab::Git::Blob.raw(self, oid) Gitlab::Git::Blob.raw(self, oid)
end end
def root_ref
if raw_repository
raw_repository.root_ref
else
# When the repo does not exist we raise this error so no data is cached.
raise Rugged::ReferenceError
end
end
cache_method :root_ref
def exists?
refs_directory_exists?
end
cache_method :exists?
def empty?
raw_repository.empty?
end
cache_method :empty?
# The size of this repository in megabytes.
def size
exists? ? raw_repository.size : 0.0
end
cache_method :size, fallback: 0.0
def commit_count
root_ref ? raw_repository.commit_count(root_ref) : 0
end
cache_method :commit_count, fallback: 0
def branch_names
branches.map(&:name)
end
cache_method :branch_names, fallback: []
def tag_names
raw_repository.tag_names
end
cache_method :tag_names, fallback: []
def branch_count
branches.size
end
cache_method :branch_count, fallback: 0
def tag_count
raw_repository.rugged.tags.count
end
cache_method :tag_count, fallback: 0
def avatar
if tree = file_on_head(:avatar)
tree.path
end
end
cache_method :avatar
def readme def readme
cache.fetch(:readme) { tree(:head).readme } if head = tree(:head)
head.readme
end
end end
cache_method :readme
def version def version
cache.fetch(:version) do file_on_head(:version)
tree(:head).blobs.find do |file|
file.name.casecmp('version').zero?
end
end
end end
cache_method :version
def contribution_guide def contribution_guide
cache.fetch(:contribution_guide) do file_on_head(:contributing)
tree(:head).blobs.find do |file|
file.contributing?
end
end
end end
cache_method :contribution_guide
def changelog def changelog
cache.fetch(:changelog) do file_on_head(:changelog)
file_on_head(/\A(changelog|history|changes|news)/i)
end
end end
cache_method :changelog
def license_blob def license_blob
return nil unless head_exists? file_on_head(:license)
cache.fetch(:license_blob) do
file_on_head(/\A(licen[sc]e|copying)(\..+|\z)/i)
end
end end
cache_method :license_blob
def license_key def license_key
return nil unless head_exists? return unless exists?
cache.fetch(:license_key) do Licensee.license(path).try(:key)
Licensee.license(path).try(:key)
end
end end
cache_method :license_key
def gitignore def gitignore
return nil if !exists? || empty? file_on_head(:gitignore)
cache.fetch(:gitignore) do
file_on_head(/\A\.gitignore\z/)
end
end end
cache_method :gitignore
def koding_yml def koding_yml
return nil unless head_exists? file_on_head(:koding)
cache.fetch(:koding_yml) do
file_on_head(/\A\.koding\.yml\z/)
end
end end
cache_method :koding_yml
def gitlab_ci_yml def gitlab_ci_yml
return nil unless head_exists? file_on_head(:gitlab_ci)
@gitlab_ci_yml ||= tree(:head).blobs.find do |file|
file.name == '.gitlab-ci.yml'
end
rescue Rugged::ReferenceError
# For unknow reason spinach scenario "Scenario: I change project path"
# lead to "Reference 'HEAD' not found" exception from Repository#empty?
nil
end end
cache_method :gitlab_ci_yml
def head_commit def head_commit
@head_commit ||= commit(self.root_ref) @head_commit ||= commit(self.root_ref)
end end
def head_tree def head_tree
@head_tree ||= Tree.new(self, head_commit.sha, nil) if head_commit
@head_tree ||= Tree.new(self, head_commit.sha, nil)
end
end end
def tree(sha = :head, path = nil, recursive: false) def tree(sha = :head, path = nil, recursive: false)
if sha == :head if sha == :head
return unless head_commit
if path.nil? if path.nil?
return head_tree return head_tree
else else
...@@ -779,10 +784,6 @@ class Repository ...@@ -779,10 +784,6 @@ class Repository
@tags ||= raw_repository.tags @tags ||= raw_repository.tags
end end
def root_ref
@root_ref ||= cache.fetch(:root_ref) { raw_repository.root_ref }
end
def commit_dir(user, path, message, branch, author_email: nil, author_name: nil) def commit_dir(user, path, message, branch, author_email: nil, author_name: nil)
update_branch_with_hooks(user, branch) do |ref| update_branch_with_hooks(user, branch) do |ref|
options = { options = {
...@@ -1140,28 +1141,55 @@ class Repository ...@@ -1140,28 +1141,55 @@ class Repository
end end
end end
def avatar # Caches the supplied block both in a cache and in an instance variable.
return nil unless exists? #
# The cache key and instance variable are named the same way as the value of
# the `key` argument.
#
# This method will return `nil` if the corresponding instance variable is also
# set to `nil`. This ensures we don't keep yielding the block when it returns
# `nil`.
#
# key - The name of the key to cache the data in.
# fallback - A value to fall back to in the event of a Git error.
def cache_method_output(key, fallback: nil, &block)
ivar = cache_instance_variable_name(key)
@avatar ||= cache.fetch(:avatar) do if instance_variable_defined?(ivar)
AVATAR_FILES.find do |file| instance_variable_get(ivar)
blob_at_branch(root_ref, file) else
begin
instance_variable_set(ivar, cache.fetch(key, &block))
rescue Rugged::ReferenceError, Gitlab::Git::Repository::NoRepository
# if e.g. HEAD or the entire repository doesn't exist we want to
# gracefully handle this and not cache anything.
fallback
end end
end end
end end
private def cache_instance_variable_name(key)
:"@#{key.to_s.tr('?!', '')}"
end
def cache def file_on_head(type)
@cache ||= RepositoryCache.new(path_with_namespace, @project.id) if head = tree(:head)
head.blobs.find do |file|
Gitlab::FileDetector.type_of(file.name) == type
end
end
end end
def head_exists? private
exists? && !empty? && !rugged.head_unborn?
def refs_directory_exists?
return false unless path_with_namespace
File.exist?(File.join(path_to_repo, 'refs'))
end end
def file_on_head(regex) def cache
tree(:head).blobs.find { |file| file.name =~ regex } @cache ||= RepositoryCache.new(path_with_namespace, @project.id)
end end
def tags_sorted_by_committed_date def tags_sorted_by_committed_date
......
...@@ -18,7 +18,9 @@ class Tree ...@@ -18,7 +18,9 @@ class Tree
def readme def readme
return @readme if defined?(@readme) return @readme if defined?(@readme)
available_readmes = blobs.select(&:readme?) available_readmes = blobs.select do |blob|
Gitlab::FileDetector.type_of(blob.name) == :readme
end
previewable_readmes = available_readmes.select do |blob| previewable_readmes = available_readmes.select do |blob|
previewable?(blob.name) previewable?(blob.name)
......
...@@ -18,7 +18,7 @@ class GitPushService < BaseService ...@@ -18,7 +18,7 @@ class GitPushService < BaseService
# #
def execute def execute
@project.repository.after_create if @project.empty_repo? @project.repository.after_create if @project.empty_repo?
@project.repository.after_push_commit(branch_name, params[:newrev]) @project.repository.after_push_commit(branch_name)
if push_remove_branch? if push_remove_branch?
@project.repository.after_remove_branch @project.repository.after_remove_branch
...@@ -51,12 +51,32 @@ class GitPushService < BaseService ...@@ -51,12 +51,32 @@ class GitPushService < BaseService
execute_related_hooks execute_related_hooks
perform_housekeeping perform_housekeeping
update_caches
end end
def update_gitattributes def update_gitattributes
@project.repository.copy_gitattributes(params[:ref]) @project.repository.copy_gitattributes(params[:ref])
end end
def update_caches
if is_default_branch?
paths = Set.new
@push_commits.each do |commit|
commit.raw_diffs(deltas_only: true).each do |diff|
paths << diff.new_path
end
end
types = Gitlab::FileDetector.types_in_paths(paths.to_a)
else
types = []
end
ProjectCacheWorker.perform_async(@project.id, types)
end
protected protected
def execute_related_hooks def execute_related_hooks
...@@ -70,7 +90,6 @@ class GitPushService < BaseService ...@@ -70,7 +90,6 @@ class GitPushService < BaseService
@project.execute_hooks(build_push_data.dup, :push_hooks) @project.execute_hooks(build_push_data.dup, :push_hooks)
@project.execute_services(build_push_data.dup, :push_hooks) @project.execute_services(build_push_data.dup, :push_hooks)
Ci::CreatePipelineService.new(@project, current_user, build_push_data).execute Ci::CreatePipelineService.new(@project, current_user, build_push_data).execute
ProjectCacheWorker.perform_async(@project.id)
if push_remove_branch? if push_remove_branch?
AfterBranchDeleteService AfterBranchDeleteService
......
# Worker for updating any project specific caches. # Worker for updating any project specific caches.
#
# This worker runs at most once every 15 minutes per project. This is to ensure
# that multiple instances of jobs for this worker don't hammer the underlying
# storage engine as much.
class ProjectCacheWorker class ProjectCacheWorker
include Sidekiq::Worker include Sidekiq::Worker
include DedicatedSidekiqQueue include DedicatedSidekiqQueue
LEASE_TIMEOUT = 15.minutes.to_i LEASE_TIMEOUT = 15.minutes.to_i
def self.lease_for(project_id) # project_id - The ID of the project for which to flush the cache.
Gitlab::ExclusiveLease. # refresh - An Array containing extra types of data to refresh such as
new("project_cache_worker:#{project_id}", timeout: LEASE_TIMEOUT) # `:readme` to flush the README and `:changelog` to flush the
end # CHANGELOG.
def perform(project_id, refresh = [])
project = Project.find_by(id: project_id)
# Overwrite Sidekiq's implementation so we only schedule when actually needed. return unless project && project.repository.exists?
def self.perform_async(project_id)
# If a lease for this project is still being held there's no point in
# scheduling a new job.
super unless lease_for(project_id).exists?
end
def perform(project_id) update_repository_size(project)
if try_obtain_lease_for(project_id) project.update_commit_count
Rails.logger.
info("Obtained ProjectCacheWorker lease for project #{project_id}")
else
Rails.logger.
info("Could not obtain ProjectCacheWorker lease for project #{project_id}")
return
end
update_caches(project_id) project.repository.refresh_method_caches(refresh.map(&:to_sym))
end end
def update_caches(project_id) def update_repository_size(project)
project = Project.find(project_id) return unless try_obtain_lease_for(project.id, :update_repository_size)
return unless project.repository.exists? Rails.logger.info("Updating repository size for project #{project.id}")
project.update_repository_size project.update_repository_size
project.update_commit_count
if project.repository.root_ref
project.repository.build_cache
end
end end
def try_obtain_lease_for(project_id) private
self.class.lease_for(project_id).try_obtain
def try_obtain_lease_for(project_id, section)
Gitlab::ExclusiveLease.
new("project_cache_worker:#{project_id}:#{section}", timeout: LEASE_TIMEOUT).
try_obtain
end end
end end
---
title: Rework cache invalidation so only changed data is refreshed
merge_request: 7360
author:
require 'set'
module Gitlab
# Module that can be used to detect if a path points to a special file such as
# a README or a CONTRIBUTING file.
module FileDetector
PATTERNS = {
readme: /\Areadme/i,
changelog: /\A(changelog|history|changes|news)/i,
license: /\A(licen[sc]e|copying)(\..+|\z)/i,
contributing: /\Acontributing/i,
version: 'version',
gitignore: '.gitignore',
koding: '.koding.yml',
gitlab_ci: '.gitlab-ci.yml',
avatar: /\Alogo\.(png|jpg|gif)\z/
}
# Returns an Array of file types based on the given paths.
#
# This method can be used to check if a list of file paths (e.g. of changed
# files) involve any special files such as a README or a LICENSE file.
#
# Example:
#
# types_in_paths(%w{README.md foo/bar.txt}) # => [:readme]
def self.types_in_paths(paths)
types = Set.new
paths.each do |path|
type = type_of(path)
types << type if type
end
types.to_a
end
# Returns the type of a file path, or nil if none could be detected.
#
# Returned types are Symbols such as `:readme`, `:version`, etc.
#
# Example:
#
# type_of('README.md') # => :readme
# type_of('VERSION') # => :version
def self.type_of(path)
name = File.basename(path)
PATTERNS.each do |type, search|
did_match = if search.is_a?(Regexp)
name =~ search
else
name.casecmp(search) == 0
end
return type if did_match
end
nil
end
end
end
require 'spec_helper'
describe Gitlab::FileDetector do
describe '.types_in_paths' do
it 'returns the file types for the given paths' do
expect(described_class.types_in_paths(%w(README.md CHANGELOG VERSION VERSION))).
to eq(%i{readme changelog version})
end
it 'does not include unrecognized file paths' do
expect(described_class.types_in_paths(%w(README.md foo.txt))).
to eq(%i{readme})
end
end
describe '.type_of' do
it 'returns the type of a README file' do
expect(described_class.type_of('README.md')).to eq(:readme)
end
it 'returns the type of a changelog file' do
%w(CHANGELOG HISTORY CHANGES NEWS).each do |file|
expect(described_class.type_of(file)).to eq(:changelog)
end
end
it 'returns the type of a license file' do
%w(LICENSE LICENCE COPYING).each do |file|
expect(described_class.type_of(file)).to eq(:license)
end
end
it 'returns the type of a version file' do
expect(described_class.type_of('VERSION')).to eq(:version)
end
it 'returns the type of a .gitignore file' do
expect(described_class.type_of('.gitignore')).to eq(:gitignore)
end
it 'returns the type of a Koding config file' do
expect(described_class.type_of('.koding.yml')).to eq(:koding)
end
it 'returns the type of a GitLab CI config file' do
expect(described_class.type_of('.gitlab-ci.yml')).to eq(:gitlab_ci)
end
it 'returns the type of an avatar' do
%w(logo.gif logo.png logo.jpg).each do |file|
expect(described_class.type_of(file)).to eq(:avatar)
end
end
it 'returns nil for an unknown file' do
expect(described_class.type_of('foo.txt')).to be_nil
end
end
end
...@@ -1572,7 +1572,7 @@ describe Project, models: true do ...@@ -1572,7 +1572,7 @@ describe Project, models: true do
end end
it 'expires the avatar cache' do it 'expires the avatar cache' do
expect(project.repository).to receive(:expire_avatar_cache).with(project.default_branch) expect(project.repository).to receive(:expire_avatar_cache)
project.change_head(project.default_branch) project.change_head(project.default_branch)
end end
......
...@@ -464,11 +464,7 @@ describe Repository, models: true do ...@@ -464,11 +464,7 @@ describe Repository, models: true do
end end
end end
describe "#changelog" do describe "#changelog", caching: true do
before do
repository.send(:cache).expire(:changelog)
end
it 'accepts changelog' do it 'accepts changelog' do
expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('changelog')]) expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('changelog')])
...@@ -500,17 +496,16 @@ describe Repository, models: true do ...@@ -500,17 +496,16 @@ describe Repository, models: true do
end end
end end
describe "#license_blob" do describe "#license_blob", caching: true do
before do before do
repository.send(:cache).expire(:license_blob)
repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master') repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master')
end end
it 'handles when HEAD points to non-existent ref' do it 'handles when HEAD points to non-existent ref' do
repository.commit_file(user, 'LICENSE', 'Copyright!', 'Add LICENSE', 'master', false) repository.commit_file(user, 'LICENSE', 'Copyright!', 'Add LICENSE', 'master', false)
rugged = double('rugged')
expect(rugged).to receive(:head_unborn?).and_return(true) allow(repository).to receive(:file_on_head).
expect(repository).to receive(:rugged).and_return(rugged) and_raise(Rugged::ReferenceError)
expect(repository.license_blob).to be_nil expect(repository.license_blob).to be_nil
end end
...@@ -537,22 +532,18 @@ describe Repository, models: true do ...@@ -537,22 +532,18 @@ describe Repository, models: true do
end end
end end
describe '#license_key' do describe '#license_key', caching: true do
before do before do
repository.send(:cache).expire(:license_key)
repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master') repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master')
end end
it 'handles when HEAD points to non-existent ref' do it 'returns nil when no license is detected' do
repository.commit_file(user, 'LICENSE', 'Copyright!', 'Add LICENSE', 'master', false)
rugged = double('rugged')
expect(rugged).to receive(:head_unborn?).and_return(true)
expect(repository).to receive(:rugged).and_return(rugged)
expect(repository.license_key).to be_nil expect(repository.license_key).to be_nil
end end
it 'returns nil when no license is detected' do it 'returns nil when the repository does not exist' do
expect(repository).to receive(:exists?).and_return(false)
expect(repository.license_key).to be_nil expect(repository.license_key).to be_nil
end end
...@@ -569,7 +560,7 @@ describe Repository, models: true do ...@@ -569,7 +560,7 @@ describe Repository, models: true do
end end
end end
describe "#gitlab_ci_yml" do describe "#gitlab_ci_yml", caching: true do
it 'returns valid file' do it 'returns valid file' do
files = [TestBlob.new('file'), TestBlob.new('.gitlab-ci.yml'), TestBlob.new('copying')] files = [TestBlob.new('file'), TestBlob.new('.gitlab-ci.yml'), TestBlob.new('copying')]
expect(repository.tree).to receive(:blobs).and_return(files) expect(repository.tree).to receive(:blobs).and_return(files)
...@@ -583,7 +574,7 @@ describe Repository, models: true do ...@@ -583,7 +574,7 @@ describe Repository, models: true do
end end
it 'returns nil for empty repository' do it 'returns nil for empty repository' do
expect(repository).to receive(:empty?).and_return(true) allow(repository).to receive(:file_on_head).and_raise(Rugged::ReferenceError)
expect(repository.gitlab_ci_yml).to be_nil expect(repository.gitlab_ci_yml).to be_nil
end end
end end
...@@ -778,7 +769,6 @@ describe Repository, models: true do ...@@ -778,7 +769,6 @@ describe Repository, models: true do
expect(repository).not_to receive(:expire_emptiness_caches) expect(repository).not_to receive(:expire_emptiness_caches)
expect(repository).to receive(:expire_branches_cache) expect(repository).to receive(:expire_branches_cache)
expect(repository).to receive(:expire_has_visible_content_cache) expect(repository).to receive(:expire_has_visible_content_cache)
expect(repository).to receive(:expire_branch_count_cache)
repository.update_branch_with_hooks(user, 'new-feature') { new_rev } repository.update_branch_with_hooks(user, 'new-feature') { new_rev }
end end
...@@ -797,7 +787,6 @@ describe Repository, models: true do ...@@ -797,7 +787,6 @@ describe Repository, models: true do
expect(empty_repository).to receive(:expire_emptiness_caches) expect(empty_repository).to receive(:expire_emptiness_caches)
expect(empty_repository).to receive(:expire_branches_cache) expect(empty_repository).to receive(:expire_branches_cache)
expect(empty_repository).to receive(:expire_has_visible_content_cache) expect(empty_repository).to receive(:expire_has_visible_content_cache)
expect(empty_repository).to receive(:expire_branch_count_cache)
empty_repository.commit_file(user, 'CHANGELOG', 'Changelog!', empty_repository.commit_file(user, 'CHANGELOG', 'Changelog!',
'Updates file content', 'master', false) 'Updates file content', 'master', false)
...@@ -811,8 +800,7 @@ describe Repository, models: true do ...@@ -811,8 +800,7 @@ describe Repository, models: true do
end end
it 'returns false when a repository does not exist' do it 'returns false when a repository does not exist' do
expect(repository.raw_repository).to receive(:rugged). allow(repository).to receive(:refs_directory_exists?).and_return(false)
and_raise(Gitlab::Git::Repository::NoRepository)
expect(repository.exists?).to eq(false) expect(repository.exists?).to eq(false)
end end
...@@ -916,34 +904,6 @@ describe Repository, models: true do ...@@ -916,34 +904,6 @@ describe Repository, models: true do
end end
end end
describe '#expire_cache' do
it 'expires all caches' do
expect(repository).to receive(:expire_branch_cache)
repository.expire_cache
end
it 'expires the caches for a specific branch' do
expect(repository).to receive(:expire_branch_cache).with('master')
repository.expire_cache('master')
end
it 'expires the emptiness caches for an empty repository' do
expect(repository).to receive(:empty?).and_return(true)
expect(repository).to receive(:expire_emptiness_caches)
repository.expire_cache
end
it 'does not expire the emptiness caches for a non-empty repository' do
expect(repository).to receive(:empty?).and_return(false)
expect(repository).not_to receive(:expire_emptiness_caches)
repository.expire_cache
end
end
describe '#expire_root_ref_cache' do describe '#expire_root_ref_cache' do
it 'expires the root reference cache' do it 'expires the root reference cache' do
repository.root_ref repository.root_ref
...@@ -1003,12 +963,23 @@ describe Repository, models: true do ...@@ -1003,12 +963,23 @@ describe Repository, models: true do
describe '#expire_emptiness_caches' do describe '#expire_emptiness_caches' do
let(:cache) { repository.send(:cache) } let(:cache) { repository.send(:cache) }
it 'expires the caches' do it 'expires the caches for an empty repository' do
allow(repository).to receive(:empty?).and_return(true)
expect(cache).to receive(:expire).with(:empty?) expect(cache).to receive(:expire).with(:empty?)
expect(repository).to receive(:expire_has_visible_content_cache) expect(repository).to receive(:expire_has_visible_content_cache)
repository.expire_emptiness_caches repository.expire_emptiness_caches
end end
it 'does not expire the cache for a non-empty repository' do
allow(repository).to receive(:empty?).and_return(false)
expect(cache).not_to receive(:expire).with(:empty?)
expect(repository).not_to receive(:expire_has_visible_content_cache)
repository.expire_emptiness_caches
end
end end
describe :skip_merged_commit do describe :skip_merged_commit do
...@@ -1120,24 +1091,12 @@ describe Repository, models: true do ...@@ -1120,24 +1091,12 @@ describe Repository, models: true do
repository.before_delete repository.before_delete
end end
it 'flushes the tag count cache' do
expect(repository).to receive(:expire_tag_count_cache)
repository.before_delete
end
it 'flushes the branches cache' do it 'flushes the branches cache' do
expect(repository).to receive(:expire_branches_cache) expect(repository).to receive(:expire_branches_cache)
repository.before_delete repository.before_delete
end end
it 'flushes the branch count cache' do
expect(repository).to receive(:expire_branch_count_cache)
repository.before_delete
end
it 'flushes the root ref cache' do it 'flushes the root ref cache' do
expect(repository).to receive(:expire_root_ref_cache) expect(repository).to receive(:expire_root_ref_cache)
...@@ -1162,36 +1121,18 @@ describe Repository, models: true do ...@@ -1162,36 +1121,18 @@ describe Repository, models: true do
allow(repository).to receive(:exists?).and_return(true) allow(repository).to receive(:exists?).and_return(true)
end end
it 'flushes the caches that depend on repository data' do
expect(repository).to receive(:expire_cache)
repository.before_delete
end
it 'flushes the tags cache' do it 'flushes the tags cache' do
expect(repository).to receive(:expire_tags_cache) expect(repository).to receive(:expire_tags_cache)
repository.before_delete repository.before_delete
end end
it 'flushes the tag count cache' do
expect(repository).to receive(:expire_tag_count_cache)
repository.before_delete
end
it 'flushes the branches cache' do it 'flushes the branches cache' do
expect(repository).to receive(:expire_branches_cache) expect(repository).to receive(:expire_branches_cache)
repository.before_delete repository.before_delete
end end
it 'flushes the branch count cache' do
expect(repository).to receive(:expire_branch_count_cache)
repository.before_delete
end
it 'flushes the root ref cache' do it 'flushes the root ref cache' do
expect(repository).to receive(:expire_root_ref_cache) expect(repository).to receive(:expire_root_ref_cache)
...@@ -1222,8 +1163,9 @@ describe Repository, models: true do ...@@ -1222,8 +1163,9 @@ describe Repository, models: true do
describe '#before_push_tag' do describe '#before_push_tag' do
it 'flushes the cache' do it 'flushes the cache' do
expect(repository).to receive(:expire_cache) expect(repository).to receive(:expire_statistics_caches)
expect(repository).to receive(:expire_tag_count_cache) expect(repository).to receive(:expire_emptiness_caches)
expect(repository).to receive(:expire_tags_cache)
repository.before_push_tag repository.before_push_tag
end end
...@@ -1240,17 +1182,23 @@ describe Repository, models: true do ...@@ -1240,17 +1182,23 @@ describe Repository, models: true do
describe '#after_import' do describe '#after_import' do
it 'flushes and builds the cache' do it 'flushes and builds the cache' do
expect(repository).to receive(:expire_content_cache) expect(repository).to receive(:expire_content_cache)
expect(repository).to receive(:build_cache) expect(repository).to receive(:expire_tags_cache)
expect(repository).to receive(:expire_branches_cache)
repository.after_import repository.after_import
end end
end end
describe '#after_push_commit' do describe '#after_push_commit' do
it 'flushes the cache' do it 'expires statistics caches' do
expect(repository).to receive(:expire_cache).with('master', '123') expect(repository).to receive(:expire_statistics_caches).
and_call_original
repository.after_push_commit('master', '123') expect(repository).to receive(:expire_branch_cache).
with('master').
and_call_original
repository.after_push_commit('master')
end end
end end
...@@ -1302,7 +1250,8 @@ describe Repository, models: true do ...@@ -1302,7 +1250,8 @@ describe Repository, models: true do
describe '#before_remove_tag' do describe '#before_remove_tag' do
it 'flushes the tag cache' do it 'flushes the tag cache' do
expect(repository).to receive(:expire_tag_count_cache) expect(repository).to receive(:expire_tags_cache).and_call_original
expect(repository).to receive(:expire_statistics_caches).and_call_original
repository.before_remove_tag repository.before_remove_tag
end end
...@@ -1320,23 +1269,23 @@ describe Repository, models: true do ...@@ -1320,23 +1269,23 @@ describe Repository, models: true do
end end
end end
describe '#expire_branch_count_cache' do describe '#expire_branches_cache' do
let(:cache) { repository.send(:cache) }
it 'expires the cache' do it 'expires the cache' do
expect(cache).to receive(:expire).with(:branch_count) expect(repository).to receive(:expire_method_caches).
with(%i(branch_names branch_count)).
and_call_original
repository.expire_branch_count_cache repository.expire_branches_cache
end end
end end
describe '#expire_tag_count_cache' do describe '#expire_tags_cache' do
let(:cache) { repository.send(:cache) }
it 'expires the cache' do it 'expires the cache' do
expect(cache).to receive(:expire).with(:tag_count) expect(repository).to receive(:expire_method_caches).
with(%i(tag_names tag_count)).
and_call_original
repository.expire_tag_count_cache repository.expire_tags_cache
end end
end end
...@@ -1412,170 +1361,316 @@ describe Repository, models: true do ...@@ -1412,170 +1361,316 @@ describe Repository, models: true do
describe '#avatar' do describe '#avatar' do
it 'returns nil if repo does not exist' do it 'returns nil if repo does not exist' do
expect(repository).to receive(:exists?).and_return(false) expect(repository).to receive(:file_on_head).
and_raise(Rugged::ReferenceError)
expect(repository.avatar).to eq(nil) expect(repository.avatar).to eq(nil)
end end
it 'returns the first avatar file found in the repository' do it 'returns the first avatar file found in the repository' do
expect(repository).to receive(:blob_at_branch). expect(repository).to receive(:file_on_head).
with('master', 'logo.png'). with(:avatar).
and_return(true) and_return(double(:tree, path: 'logo.png'))
expect(repository.avatar).to eq('logo.png') expect(repository.avatar).to eq('logo.png')
end end
it 'caches the output' do it 'caches the output' do
allow(repository).to receive(:blob_at_branch). expect(repository).to receive(:file_on_head).
with('master', 'logo.png'). with(:avatar).
and_return(true) once.
and_return(double(:tree, path: 'logo.png'))
expect(repository.avatar).to eq('logo.png')
expect(repository).not_to receive(:blob_at_branch) 2.times { expect(repository.avatar).to eq('logo.png') }
expect(repository.avatar).to eq('logo.png')
end end
end end
describe '#expire_avatar_cache' do describe '#expire_exists_cache' do
let(:cache) { repository.send(:cache) } let(:cache) { repository.send(:cache) }
before do it 'expires the cache' do
allow(repository).to receive(:cache).and_return(cache) expect(cache).to receive(:expire).with(:exists?)
repository.expire_exists_cache
end end
end
context 'without a branch or revision' do describe "#keep_around" do
it 'flushes the cache' do it "does not fail if we attempt to reference bad commit" do
expect(cache).to receive(:expire).with(:avatar) expect(repository.kept_around?('abc1234')).to be_falsey
end
repository.expire_avatar_cache it "stores a reference to the specified commit sha so it isn't garbage collected" do
end repository.keep_around(sample_commit.id)
expect(repository.kept_around?(sample_commit.id)).to be_truthy
end
it "attempting to call keep_around on truncated ref does not fail" do
repository.keep_around(sample_commit.id)
ref = repository.send(:keep_around_ref_name, sample_commit.id)
path = File.join(repository.path, ref)
# Corrupt the reference
File.truncate(path, 0)
expect(repository.kept_around?(sample_commit.id)).to be_falsey
repository.keep_around(sample_commit.id)
expect(repository.kept_around?(sample_commit.id)).to be_falsey
File.delete(path)
end
end
describe '#update_ref!' do
it 'can create a ref' do
repository.update_ref!('refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA)
expect(repository.find_branch('foobar')).not_to be_nil
end end
context 'with a branch' do it 'raises CommitError when the ref update fails' do
it 'does not flush the cache if the branch is not the default branch' do expect do
expect(cache).not_to receive(:expire) repository.update_ref!('refs/heads/master', 'refs/heads/master', Gitlab::Git::BLANK_SHA)
end.to raise_error(Repository::CommitError)
end
end
describe '#contribution_guide', caching: true do
it 'returns and caches the output' do
expect(repository).to receive(:file_on_head).
with(:contributing).
and_return(Gitlab::Git::Tree.new(path: 'CONTRIBUTING.md')).
once
repository.expire_avatar_cache('cats') 2.times do
expect(repository.contribution_guide).
to be_an_instance_of(Gitlab::Git::Tree)
end end
end
end
it 'flushes the cache if the branch equals the default branch' do describe '#gitignore', caching: true do
expect(cache).to receive(:expire).with(:avatar) it 'returns and caches the output' do
expect(repository).to receive(:file_on_head).
with(:gitignore).
and_return(Gitlab::Git::Tree.new(path: '.gitignore')).
once
repository.expire_avatar_cache(repository.root_ref) 2.times do
expect(repository.gitignore).to be_an_instance_of(Gitlab::Git::Tree)
end end
end end
end
context 'with a branch and revision' do describe '#koding_yml', caching: true do
let(:commit) { double(:commit) } it 'returns and caches the output' do
expect(repository).to receive(:file_on_head).
with(:koding).
and_return(Gitlab::Git::Tree.new(path: '.koding.yml')).
once
before do 2.times do
allow(repository).to receive(:commit).and_return(commit) expect(repository.koding_yml).to be_an_instance_of(Gitlab::Git::Tree)
end end
end
end
it 'does not flush the cache if the commit does not change any logos' do describe '#readme', caching: true do
diff = double(:diff, new_path: 'test.txt') context 'with a non-existing repository' do
it 'returns nil' do
expect(repository).to receive(:tree).with(:head).and_return(nil)
expect(commit).to receive(:raw_diffs).and_return([diff]) expect(repository.readme).to be_nil
expect(cache).not_to receive(:expire) end
end
repository.expire_avatar_cache(repository.root_ref, '123') context 'with an existing repository' do
it 'returns the README' do
expect(repository.readme).to be_an_instance_of(Gitlab::Git::Blob)
end end
end
end
it 'flushes the cache if the commit changes any of the logos' do describe '#expire_statistics_caches' do
diff = double(:diff, new_path: Repository::AVATAR_FILES[0]) it 'expires the caches' do
expect(repository).to receive(:expire_method_caches).
with(%i(size commit_count))
repository.expire_statistics_caches
end
end
expect(commit).to receive(:raw_diffs).and_return([diff]) describe '#expire_method_caches' do
expect(cache).to receive(:expire).with(:avatar) it 'expires the caches of the given methods' do
expect_any_instance_of(RepositoryCache).to receive(:expire).with(:readme)
expect_any_instance_of(RepositoryCache).to receive(:expire).with(:gitignore)
repository.expire_avatar_cache(repository.root_ref, '123') repository.expire_method_caches(%i(readme gitignore))
end
end end
end end
describe '#expire_exists_cache' do describe '#expire_all_method_caches' do
let(:cache) { repository.send(:cache) } it 'expires the caches of all methods' do
expect(repository).to receive(:expire_method_caches).
with(Repository::CACHED_METHODS)
repository.expire_all_method_caches
end
end
describe '#expire_avatar_cache' do
it 'expires the cache' do it 'expires the cache' do
expect(cache).to receive(:expire).with(:exists?) expect(repository).to receive(:expire_method_caches).with(%i(avatar))
repository.expire_exists_cache repository.expire_avatar_cache
end end
end end
describe '#build_cache' do describe '#file_on_head' do
let(:cache) { repository.send(:cache) } context 'with a non-existing repository' do
it 'returns nil' do
expect(repository).to receive(:tree).with(:head).and_return(nil)
it 'builds the caches if they do not already exist' do expect(repository.file_on_head(:readme)).to be_nil
cache_keys = repository.cache_keys + repository.cache_keys_for_branches_and_tags end
end
expect(cache).to receive(:exist?). context 'with a repository that has no blobs' do
exactly(cache_keys.length). it 'returns nil' do
times. expect_any_instance_of(Tree).to receive(:blobs).and_return([])
and_return(false)
expect(repository.file_on_head(:readme)).to be_nil
end
end
context 'with an existing repository' do
it 'returns a Gitlab::Git::Tree' do
expect(repository.file_on_head(:readme)).
to be_an_instance_of(Gitlab::Git::Tree)
end
end
end
describe '#head_tree' do
context 'with an existing repository' do
it 'returns a Tree' do
expect(repository.head_tree).to be_an_instance_of(Tree)
end
end
context 'with a non-existing repository' do
it 'returns nil' do
expect(repository).to receive(:head_commit).and_return(nil)
expect(repository.head_tree).to be_nil
end
end
end
cache_keys.each do |key| describe '#tree' do
expect(repository).to receive(key) context 'using a non-existing repository' do
before do
allow(repository).to receive(:head_commit).and_return(nil)
end
it 'returns nil' do
expect(repository.tree(:head)).to be_nil
end end
repository.build_cache it 'returns nil when using a path' do
expect(repository.tree(:head, 'README.md')).to be_nil
end
end end
it 'does not build any caches that already exist' do context 'using an existing repository' do
cache_keys = repository.cache_keys + repository.cache_keys_for_branches_and_tags it 'returns a Tree' do
expect(repository.tree(:head)).to be_an_instance_of(Tree)
end
end
end
expect(cache).to receive(:exist?). describe '#size' do
exactly(cache_keys.length). context 'with a non-existing repository' do
times. it 'returns 0' do
and_return(true) expect(repository).to receive(:exists?).and_return(false)
cache_keys.each do |key| expect(repository.size).to eq(0.0)
expect(repository).not_to receive(key)
end end
end
repository.build_cache context 'with an existing repository' do
it 'returns the repository size as a Float' do
expect(repository.size).to be_an_instance_of(Float)
end
end end
end end
describe "#keep_around" do describe '#commit_count' do
it "does not fail if we attempt to reference bad commit" do context 'with a non-existing repository' do
expect(repository.kept_around?('abc1234')).to be_falsey it 'returns 0' do
expect(repository).to receive(:root_ref).and_return(nil)
expect(repository.commit_count).to eq(0)
end
end end
it "stores a reference to the specified commit sha so it isn't garbage collected" do context 'with an existing repository' do
repository.keep_around(sample_commit.id) it 'returns the commit count' do
expect(repository.commit_count).to be_an_instance_of(Fixnum)
end
end
end
expect(repository.kept_around?(sample_commit.id)).to be_truthy describe '#cache_method_output', caching: true do
context 'with a non-existing repository' do
let(:value) do
repository.cache_method_output(:cats, fallback: 10) do
raise Rugged::ReferenceError
end
end
it 'returns a fallback value' do
expect(value).to eq(10)
end
it 'does not cache the data' do
value
expect(repository.instance_variable_defined?(:@cats)).to eq(false)
expect(repository.send(:cache).exist?(:cats)).to eq(false)
end
end end
it "attempting to call keep_around on truncated ref does not fail" do context 'with an existing repository' do
repository.keep_around(sample_commit.id) it 'caches the output' do
ref = repository.send(:keep_around_ref_name, sample_commit.id) object = double
path = File.join(repository.path, ref)
# Corrupt the reference
File.truncate(path, 0)
expect(repository.kept_around?(sample_commit.id)).to be_falsey expect(object).to receive(:number).once.and_return(10)
repository.keep_around(sample_commit.id) 2.times do
val = repository.cache_method_output(:cats) { object.number }
expect(repository.kept_around?(sample_commit.id)).to be_falsey expect(val).to eq(10)
end
File.delete(path) expect(repository.send(:cache).exist?(:cats)).to eq(true)
expect(repository.instance_variable_get(:@cats)).to eq(10)
end
end end
end end
describe '#update_ref!' do describe '#refresh_method_caches' do
it 'can create a ref' do it 'refreshes the caches of the given types' do
repository.update_ref!('refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA) expect(repository).to receive(:expire_method_caches).
with(%i(readme license_blob license_key))
expect(repository.find_branch('foobar')).not_to be_nil expect(repository).to receive(:readme)
end expect(repository).to receive(:license_blob)
expect(repository).to receive(:license_key)
it 'raises CommitError when the ref update fails' do repository.refresh_method_caches(%i(readme license))
expect do
repository.update_ref!('refs/heads/master', 'refs/heads/master', Gitlab::Git::BLANK_SHA)
end.to raise_error(Repository::CommitError)
end end
end end
end end
...@@ -14,7 +14,7 @@ describe API::API, api: true do ...@@ -14,7 +14,7 @@ describe API::API, api: true do
describe "GET /projects/:id/repository/branches" do describe "GET /projects/:id/repository/branches" do
it "returns an array of project branches" do it "returns an array of project branches" do
project.repository.expire_cache project.repository.expire_all_method_caches
get api("/projects/#{project.id}/repository/branches", user) get api("/projects/#{project.id}/repository/branches", user)
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
......
...@@ -27,27 +27,14 @@ describe GitPushService, services: true do ...@@ -27,27 +27,14 @@ describe GitPushService, services: true do
it { is_expected.to be_truthy } it { is_expected.to be_truthy }
it 'flushes general cached data' do it 'calls the after_push_commit hook' do
expect(project.repository).to receive(:expire_cache). expect(project.repository).to receive(:after_push_commit).with('master')
with('master', newrev)
subject subject
end end
it 'flushes the visible content cache' do it 'calls the after_create_branch hook' do
expect(project.repository).to receive(:expire_has_visible_content_cache) expect(project.repository).to receive(:after_create_branch)
subject
end
it 'flushes the branches cache' do
expect(project.repository).to receive(:expire_branches_cache)
subject
end
it 'flushes the branch count cache' do
expect(project.repository).to receive(:expire_branch_count_cache)
subject subject
end end
...@@ -56,21 +43,8 @@ describe GitPushService, services: true do ...@@ -56,21 +43,8 @@ describe GitPushService, services: true do
context 'existing branch' do context 'existing branch' do
it { is_expected.to be_truthy } it { is_expected.to be_truthy }
it 'flushes general cached data' do it 'calls the after_push_commit hook' do
expect(project.repository).to receive(:expire_cache). expect(project.repository).to receive(:after_push_commit).with('master')
with('master', newrev)
subject
end
it 'does not flush the branches cache' do
expect(project.repository).not_to receive(:expire_branches_cache)
subject
end
it 'does not flush the branch count cache' do
expect(project.repository).not_to receive(:expire_branch_count_cache)
subject subject
end end
...@@ -81,27 +55,14 @@ describe GitPushService, services: true do ...@@ -81,27 +55,14 @@ describe GitPushService, services: true do
it { is_expected.to be_truthy } it { is_expected.to be_truthy }
it 'flushes the visible content cache' do it 'calls the after_push_commit hook' do
expect(project.repository).to receive(:expire_has_visible_content_cache) expect(project.repository).to receive(:after_push_commit).with('master')
subject
end
it 'flushes the branches cache' do
expect(project.repository).to receive(:expire_branches_cache)
subject
end
it 'flushes the branch count cache' do
expect(project.repository).to receive(:expire_branch_count_cache)
subject subject
end end
it 'flushes general cached data' do it 'calls the after_remove_branch hook' do
expect(project.repository).to receive(:expire_cache). expect(project.repository).to receive(:after_remove_branch)
with('master', newrev)
subject subject
end end
...@@ -598,6 +559,51 @@ describe GitPushService, services: true do ...@@ -598,6 +559,51 @@ describe GitPushService, services: true do
end end
end end
describe '#update_caches' do
let(:service) do
described_class.new(project,
user,
oldrev: sample_commit.parent_id,
newrev: sample_commit.id,
ref: 'refs/heads/master')
end
context 'on the default branch' do
before do
allow(service).to receive(:is_default_branch?).and_return(true)
end
it 'flushes the caches of any special files that have been changed' do
commit = double(:commit)
diff = double(:diff, new_path: 'README.md')
expect(commit).to receive(:raw_diffs).with(deltas_only: true).
and_return([diff])
service.push_commits = [commit]
expect(ProjectCacheWorker).to receive(:perform_async).
with(project.id, %i(readme))
service.update_caches
end
end
context 'on a non-default branch' do
before do
allow(service).to receive(:is_default_branch?).and_return(false)
end
it 'does not flush any conditional caches' do
expect(ProjectCacheWorker).to receive(:perform_async).
with(project.id, []).
and_call_original
service.update_caches
end
end
end
def execute_service(project, user, oldrev, newrev, ref) def execute_service(project, user, oldrev, newrev, ref)
service = described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref ) service = described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref )
service.execute service.execute
......
...@@ -18,7 +18,7 @@ describe GitTagPushService, services: true do ...@@ -18,7 +18,7 @@ describe GitTagPushService, services: true do
end end
it 'flushes general cached data' do it 'flushes general cached data' do
expect(project.repository).to receive(:expire_cache) expect(project.repository).to receive(:before_push_tag)
subject subject
end end
...@@ -28,12 +28,6 @@ describe GitTagPushService, services: true do ...@@ -28,12 +28,6 @@ describe GitTagPushService, services: true do
subject subject
end end
it 'flushes the tag count cache' do
expect(project.repository).to receive(:expire_tag_count_cache)
subject
end
end end
describe "Git Tag Push Data" do describe "Git Tag Push Data" do
......
...@@ -2,62 +2,78 @@ require 'spec_helper' ...@@ -2,62 +2,78 @@ require 'spec_helper'
describe ProjectCacheWorker do describe ProjectCacheWorker do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:worker) { described_class.new }
subject { described_class.new } describe '#perform' do
before do
describe '.perform_async' do allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).
it 'schedules the job when no lease exists' do and_return(true)
allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:exists?). end
and_return(false)
expect_any_instance_of(described_class).to receive(:perform) context 'with a non-existing project' do
it 'does nothing' do
expect(worker).not_to receive(:update_repository_size)
described_class.perform_async(project.id) worker.perform(-1)
end
end end
it 'does not schedule the job when a lease exists' do context 'with an existing project without a repository' do
allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:exists?). it 'does nothing' do
and_return(true) allow_any_instance_of(Repository).to receive(:exists?).and_return(false)
expect_any_instance_of(described_class).not_to receive(:perform) expect(worker).not_to receive(:update_repository_size)
described_class.perform_async(project.id) worker.perform(project.id)
end
end end
end
describe '#perform' do context 'with an existing project' do
context 'when an exclusive lease can be obtained' do it 'updates the repository size' do
before do expect(worker).to receive(:update_repository_size).and_call_original
allow(subject).to receive(:try_obtain_lease_for).with(project.id).
and_return(true)
end
it 'updates project cache data' do worker.perform(project.id)
expect_any_instance_of(Repository).to receive(:size) end
expect_any_instance_of(Repository).to receive(:commit_count)
expect_any_instance_of(Project).to receive(:update_repository_size) it 'updates the commit count' do
expect_any_instance_of(Project).to receive(:update_commit_count) expect_any_instance_of(Project).to receive(:update_commit_count).
and_call_original
subject.perform(project.id) worker.perform(project.id)
end end
it 'handles missing repository data' do it 'refreshes the method caches' do
expect_any_instance_of(Repository).to receive(:exists?).and_return(false) expect_any_instance_of(Repository).to receive(:refresh_method_caches).
expect_any_instance_of(Repository).not_to receive(:size) with(%i(readme)).
and_call_original
subject.perform(project.id) worker.perform(project.id, %i(readme))
end end
end end
end
context 'when an exclusive lease can not be obtained' do describe '#update_repository_size' do
it 'does nothing' do context 'when a lease could not be obtained' do
allow(subject).to receive(:try_obtain_lease_for).with(project.id). it 'does not update the repository size' do
allow(worker).to receive(:try_obtain_lease_for).
with(project.id, :update_repository_size).
and_return(false) and_return(false)
expect(subject).not_to receive(:update_caches) expect(project).not_to receive(:update_repository_size)
worker.update_repository_size(project)
end
end
context 'when a lease could be obtained' do
it 'updates the repository size' do
allow(worker).to receive(:try_obtain_lease_for).
with(project.id, :update_repository_size).
and_return(true)
expect(project).to receive(:update_repository_size).and_call_original
subject.perform(project.id) worker.update_repository_size(project)
end end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment