Commit ffb9b3ef authored by Yorick Peterse's avatar Yorick Peterse

Refactor cache refreshing/expiring

This refactors repository caching so it's possible to selectively
refresh certain caches, instead of just expiring and refreshing
everything.

To allow this the various methods that were cached (e.g. "tag_count" and
"readme") use a similar pattern that makes expiring and refreshing
their data much easier.

In this new setup caches are refreshed as follows:

1. After a commit (but before running ProjectCacheWorker) we expire some
   basic caches such as the commit count and repository size.

2. ProjectCacheWorker will recalculate the commit count, repository
   size, then refresh a specific set of caches based on the list of
   files changed in a push payload.

This requires a bunch of changes to the various methods that may be
cached. For one, data should not be cached if a branch used or the
entire repository does not exist. To prevent all these methods from
handling this manually this is taken care of in
Repository#cache_method_output. Some methods still manually check for
the existence of a repository but this result is also cached.

With selective flushing implemented ProjectCacheWorker no longer uses an
exclusive lease for all of its work. Instead this worker only uses a
lease to limit the number of times the repository size is updated as
this is a fairly expensive operation.
parent 6f393877
...@@ -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,20 +75,6 @@ class Repository ...@@ -37,20 +75,6 @@ class Repository
) )
end end
def exists?
return @exists unless @exists.nil?
@exists = cache.fetch(:exists?) do
refs_directory_exists?
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/*
...@@ -217,10 +241,6 @@ class Repository ...@@ -217,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
...@@ -270,34 +290,6 @@ class Repository ...@@ -270,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
...@@ -313,48 +305,55 @@ class Repository ...@@ -313,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 gitlab_ci_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)
...@@ -373,15 +372,14 @@ class Repository ...@@ -373,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
...@@ -390,51 +388,22 @@ class Repository ...@@ -390,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.
...@@ -449,9 +418,8 @@ class Repository ...@@ -449,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)
...@@ -468,9 +436,9 @@ class Repository ...@@ -468,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
...@@ -478,7 +446,7 @@ class Repository ...@@ -478,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
...@@ -490,12 +458,14 @@ class Repository ...@@ -490,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
...@@ -504,7 +474,6 @@ class Repository ...@@ -504,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
...@@ -519,7 +488,6 @@ class Repository ...@@ -519,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
...@@ -546,82 +514,127 @@ class Repository ...@@ -546,82 +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)
file_on_head(:version)
end
end end
cache_method :version
def contribution_guide def contribution_guide
cache.fetch(:contribution_guide) do file_on_head(:contributing)
file_on_head(:contributing)
end
end end
cache_method :contribution_guide
def changelog def changelog
cache.fetch(:changelog) do file_on_head(:changelog)
file_on_head(:changelog)
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(:license)
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(:gitignore)
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(:koding)
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 ||= cache.fetch(:gitlab_ci_yml) do
file_on_head(:gitlab_ci)
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
...@@ -771,10 +784,6 @@ class Repository ...@@ -771,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 = {
...@@ -1132,12 +1141,41 @@ class Repository ...@@ -1132,12 +1141,41 @@ 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)
if instance_variable_defined?(ivar)
instance_variable_get(ivar)
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
@avatar ||= cache.fetch(:avatar) do def cache_instance_variable_name(key)
AVATAR_FILES.find do |file| :"@#{key.to_s.tr('?!', '')}"
blob_at_branch(root_ref, file) end
def file_on_head(type)
if head = tree(:head)
head.blobs.find do |file|
Gitlab::FileDetector.type_of(file.name) == type
end end
end end
end end
...@@ -1154,16 +1192,6 @@ class Repository ...@@ -1154,16 +1192,6 @@ class Repository
@cache ||= RepositoryCache.new(path_with_namespace, @project.id) @cache ||= RepositoryCache.new(path_with_namespace, @project.id)
end end
def head_exists?
exists? && !empty? && !rugged.head_unborn?
end
def file_on_head(type)
tree(:head).blobs.find do |file|
Gitlab::FileDetector.type_of(file.name) == type
end
end
def tags_sorted_by_committed_date def tags_sorted_by_committed_date
tags.sort_by { |tag| tag.dereferenced_target.committed_date } tags.sort_by { |tag| tag.dereferenced_target.committed_date }
end end
......
...@@ -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
...@@ -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)
...@@ -915,34 +904,6 @@ describe Repository, models: true do ...@@ -915,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
...@@ -1002,12 +963,23 @@ describe Repository, models: true do ...@@ -1002,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
...@@ -1119,24 +1091,12 @@ describe Repository, models: true do ...@@ -1119,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)
...@@ -1161,36 +1121,18 @@ describe Repository, models: true do ...@@ -1161,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)
...@@ -1221,8 +1163,9 @@ describe Repository, models: true do ...@@ -1221,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
...@@ -1239,17 +1182,23 @@ describe Repository, models: true do ...@@ -1239,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
expect(repository).to receive(:expire_branch_cache).
with('master').
and_call_original
repository.after_push_commit('master', '123') repository.after_push_commit('master')
end end
end end
...@@ -1301,7 +1250,8 @@ describe Repository, models: true do ...@@ -1301,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
...@@ -1319,23 +1269,23 @@ describe Repository, models: true do ...@@ -1319,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
...@@ -1411,84 +1361,27 @@ describe Repository, models: true do ...@@ -1411,84 +1361,27 @@ 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)
expect(repository.avatar).to eq('logo.png')
end
end
describe '#expire_avatar_cache' do
let(:cache) { repository.send(:cache) }
before do
allow(repository).to receive(:cache).and_return(cache)
end
context 'without a branch or revision' do
it 'flushes the cache' do
expect(cache).to receive(:expire).with(:avatar)
repository.expire_avatar_cache
end
end
context 'with a branch' do
it 'does not flush the cache if the branch is not the default branch' do
expect(cache).not_to receive(:expire)
repository.expire_avatar_cache('cats')
end
it 'flushes the cache if the branch equals the default branch' do
expect(cache).to receive(:expire).with(:avatar)
repository.expire_avatar_cache(repository.root_ref)
end
end
context 'with a branch and revision' do
let(:commit) { double(:commit) }
before do
allow(repository).to receive(:commit).and_return(commit)
end
it 'does not flush the cache if the commit does not change any logos' do
diff = double(:diff, new_path: 'test.txt')
expect(commit).to receive(:raw_diffs).and_return([diff])
expect(cache).not_to receive(:expire)
repository.expire_avatar_cache(repository.root_ref, '123')
end
it 'flushes the cache if the commit changes any of the logos' do
diff = double(:diff, new_path: Repository::AVATAR_FILES[0])
expect(commit).to receive(:raw_diffs).and_return([diff])
expect(cache).to receive(:expire).with(:avatar)
repository.expire_avatar_cache(repository.root_ref, '123') 2.times { expect(repository.avatar).to eq('logo.png') }
end
end end
end end
...@@ -1502,40 +1395,6 @@ describe Repository, models: true do ...@@ -1502,40 +1395,6 @@ describe Repository, models: true do
end end
end end
describe '#build_cache' do
let(:cache) { repository.send(:cache) }
it 'builds the caches if they do not already exist' do
cache_keys = repository.cache_keys + repository.cache_keys_for_branches_and_tags
expect(cache).to receive(:exist?).
exactly(cache_keys.length).
times.
and_return(false)
cache_keys.each do |key|
expect(repository).to receive(key)
end
repository.build_cache
end
it 'does not build any caches that already exist' do
cache_keys = repository.cache_keys + repository.cache_keys_for_branches_and_tags
expect(cache).to receive(:exist?).
exactly(cache_keys.length).
times.
and_return(true)
cache_keys.each do |key|
expect(repository).not_to receive(key)
end
repository.build_cache
end
end
describe "#keep_around" do describe "#keep_around" do
it "does not fail if we attempt to reference bad commit" do it "does not fail if we attempt to reference bad commit" do
expect(repository.kept_around?('abc1234')).to be_falsey expect(repository.kept_around?('abc1234')).to be_falsey
...@@ -1592,83 +1451,226 @@ describe Repository, models: true do ...@@ -1592,83 +1451,226 @@ describe Repository, models: true do
end end
end end
describe '#changelog', caching: true do describe '#gitignore', caching: true do
it 'returns and caches the output' do it 'returns and caches the output' do
expect(repository).to receive(:file_on_head). expect(repository).to receive(:file_on_head).
with(:changelog). with(:gitignore).
and_return(Gitlab::Git::Tree.new(path: 'CHANGELOG')). and_return(Gitlab::Git::Tree.new(path: '.gitignore')).
once once
2.times do 2.times do
expect(repository.changelog).to be_an_instance_of(Gitlab::Git::Tree) expect(repository.gitignore).to be_an_instance_of(Gitlab::Git::Tree)
end end
end end
end end
describe '#license_blob', caching: true do describe '#koding_yml', caching: true do
it 'returns and caches the output' do it 'returns and caches the output' do
expect(repository).to receive(:file_on_head). expect(repository).to receive(:file_on_head).
with(:license). with(:koding).
and_return(Gitlab::Git::Tree.new(path: 'LICENSE')). and_return(Gitlab::Git::Tree.new(path: '.koding.yml')).
once once
2.times do 2.times do
expect(repository.license_blob).to be_an_instance_of(Gitlab::Git::Tree) expect(repository.koding_yml).to be_an_instance_of(Gitlab::Git::Tree)
end end
end end
end end
describe '#license_key', caching: true do describe '#readme', caching: true do
it 'returns and caches the output' do context 'with a non-existing repository' do
license = double(key: 'mit') it 'returns nil' do
expect(repository).to receive(:tree).with(:head).and_return(nil)
expect(Licensee).to receive(:license). expect(repository.readme).to be_nil
with(repository.path). end
and_return(license). end
once
2.times do context 'with an existing repository' do
expect(repository.license_key).to eq('mit') it 'returns the README' do
expect(repository.readme).to be_an_instance_of(Gitlab::Git::Blob)
end end
end end
end end
describe '#gitignore', caching: true do describe '#expire_statistics_caches' do
it 'returns and caches the output' do it 'expires the caches' do
expect(repository).to receive(:file_on_head). expect(repository).to receive(:expire_method_caches).
with(:gitignore). with(%i(size commit_count))
and_return(Gitlab::Git::Tree.new(path: '.gitignore')).
once
2.times do repository.expire_statistics_caches
expect(repository.gitignore).to be_an_instance_of(Gitlab::Git::Tree) end
end
describe '#expire_method_caches' do
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_method_caches(%i(readme gitignore))
end
end
describe '#expire_all_method_caches' do
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
expect(repository).to receive(:expire_method_caches).with(%i(avatar))
repository.expire_avatar_cache
end
end
describe '#file_on_head' do
context 'with a non-existing repository' do
it 'returns nil' do
expect(repository).to receive(:tree).with(:head).and_return(nil)
expect(repository.file_on_head(:readme)).to be_nil
end
end
context 'with a repository that has no blobs' do
it 'returns nil' do
expect_any_instance_of(Tree).to receive(:blobs).and_return([])
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 end
end end
describe '#koding_yml', caching: true do describe '#head_tree' do
it 'returns and caches the output' do context 'with an existing repository' do
expect(repository).to receive(:file_on_head). it 'returns a Tree' do
with(:koding). expect(repository.head_tree).to be_an_instance_of(Tree)
and_return(Gitlab::Git::Tree.new(path: '.koding.yml')). end
once end
2.times do context 'with a non-existing repository' do
expect(repository.koding_yml).to be_an_instance_of(Gitlab::Git::Tree) it 'returns nil' do
expect(repository).to receive(:head_commit).and_return(nil)
expect(repository.head_tree).to be_nil
end end
end end
end end
describe '#gitlab_ci_yml', caching: true do describe '#tree' do
it 'returns and caches the output' do context 'using a non-existing repository' do
expect(repository).to receive(:file_on_head). before do
with(:gitlab_ci). allow(repository).to receive(:head_commit).and_return(nil)
and_return(Gitlab::Git::Tree.new(path: '.gitlab-ci.yml')). end
once
2.times do it 'returns nil' do
expect(repository.gitlab_ci_yml).to be_an_instance_of(Gitlab::Git::Tree) expect(repository.tree(:head)).to be_nil
end
it 'returns nil when using a path' do
expect(repository.tree(:head, 'README.md')).to be_nil
end
end
context 'using an existing repository' do
it 'returns a Tree' do
expect(repository.tree(:head)).to be_an_instance_of(Tree)
end
end
end
describe '#size' do
context 'with a non-existing repository' do
it 'returns 0' do
expect(repository).to receive(:exists?).and_return(false)
expect(repository.size).to eq(0.0)
end
end
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
describe '#commit_count' do
context 'with a non-existing repository' do
it 'returns 0' do
expect(repository).to receive(:root_ref).and_return(nil)
expect(repository.commit_count).to eq(0)
end
end
context 'with an existing repository' do
it 'returns the commit count' do
expect(repository.commit_count).to be_an_instance_of(Fixnum)
end
end
end
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 end
context 'with an existing repository' do
it 'caches the output' do
object = double
expect(object).to receive(:number).once.and_return(10)
2.times do
val = repository.cache_method_output(:cats) { object.number }
expect(val).to eq(10)
end
expect(repository.send(:cache).exist?(:cats)).to eq(true)
expect(repository.instance_variable_get(:@cats)).to eq(10)
end
end
end
describe '#refresh_method_caches' do
it 'refreshes the caches of the given types' do
expect(repository).to receive(:expire_method_caches).
with(%i(readme license_blob license_key))
expect(repository).to receive(:readme)
expect(repository).to receive(:license_blob)
expect(repository).to receive(:license_key)
repository.refresh_method_caches(%i(readme license))
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