Commit 43e3dc2f authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 2c89e169
...@@ -380,13 +380,13 @@ rspec-ee integration pg10: ...@@ -380,13 +380,13 @@ rspec-ee integration pg10:
extends: extends:
- .rspec-ee-base-pg10 - .rspec-ee-base-pg10
- .rails:rules:master-refs-code-backstage - .rails:rules:master-refs-code-backstage
parallel: 3 parallel: 4
rspec-ee system pg10: rspec-ee system pg10:
extends: extends:
- .rspec-ee-base-pg10 - .rspec-ee-base-pg10
- .rails:rules:master-refs-code-backstage - .rails:rules:master-refs-code-backstage
parallel: 5 parallel: 6
# ee + master-only jobs # # ee + master-only jobs #
######################### #########################
......
...@@ -8,11 +8,11 @@ module Types ...@@ -8,11 +8,11 @@ module Types
description 'Represents the snippet blob' description 'Represents the snippet blob'
present_using SnippetBlobPresenter present_using SnippetBlobPresenter
field :highlighted_data, GraphQL::STRING_TYPE, field :rich_data, GraphQL::STRING_TYPE,
description: 'Blob highlighted data', description: 'Blob highlighted data',
null: true null: true
field :plain_highlighted_data, GraphQL::STRING_TYPE, field :plain_data, GraphQL::STRING_TYPE,
description: 'Blob plain highlighted data', description: 'Blob plain highlighted data',
null: true null: true
......
...@@ -119,6 +119,17 @@ module ApplicationSettingsHelper ...@@ -119,6 +119,17 @@ module ApplicationSettingsHelper
options_for_select(options, selected) options_for_select(options, selected)
end end
def repository_storages_options_json
options = Gitlab.config.repositories.storages.map do |name, storage|
{
label: "#{name} - #{storage['gitaly_address']}",
value: name
}
end
options.to_json
end
def external_authorization_description def external_authorization_description
_("If enabled, access to projects will be validated on an external service"\ _("If enabled, access to projects will be validated on an external service"\
" using their classification label.") " using their classification label.")
......
...@@ -65,8 +65,6 @@ class Repository ...@@ -65,8 +65,6 @@ class Repository
xcode_config: :xcode_project? xcode_config: :xcode_project?
}.freeze }.freeze
MERGED_BRANCH_NAMES_CACHE_DURATION = 10.minutes
def initialize(full_path, container, disk_path: nil, repo_type: Gitlab::GlRepository::PROJECT) def initialize(full_path, container, disk_path: nil, repo_type: Gitlab::GlRepository::PROJECT)
@full_path = full_path @full_path = full_path
@disk_path = disk_path || full_path @disk_path = disk_path || full_path
...@@ -914,38 +912,28 @@ class Repository ...@@ -914,38 +912,28 @@ class Repository
@root_ref_sha ||= commit(root_ref).sha @root_ref_sha ||= commit(root_ref).sha
end end
# If this method is not provided a set of branch names to check merge status,
# it fetches all branches.
def merged_branch_names(branch_names = []) def merged_branch_names(branch_names = [])
# Currently we should skip caching if requesting all branch names # Currently we should skip caching if requesting all branch names
# This is only used in a few places, notably app/services/branches/delete_merged_service.rb, # This is only used in a few places, notably app/services/branches/delete_merged_service.rb,
# and it could potentially result in a very large cache/performance issues with the current # and it could potentially result in a very large cache/performance issues with the current
# implementation. # implementation.
skip_cache = branch_names.empty? || Feature.disabled?(:merged_branch_names_redis_caching) skip_cache = branch_names.empty? || Feature.disabled?(:merged_branch_names_redis_caching, default_enabled: true)
return raw_repository.merged_branch_names(branch_names) if skip_cache return raw_repository.merged_branch_names(branch_names) if skip_cache
cached_branch_names = cache.read(:merged_branch_names) cache = redis_hash_cache
merged_branch_names_hash = cached_branch_names || {}
missing_branch_names = branch_names.select { |bn| !merged_branch_names_hash.key?(bn) }
# Track some metrics here whilst feature flag is enabled
if cached_branch_names.present?
counter = Gitlab::Metrics.counter(
:gitlab_repository_merged_branch_names_cache_hit,
"Count of cache hits for Repository#merged_branch_names"
)
counter.increment(full_hit: missing_branch_names.empty?)
end
if missing_branch_names.any? merged_branch_names_hash = cache.fetch_and_add_missing(:merged_branch_names, branch_names) do |missing_branch_names, hash|
merged = raw_repository.merged_branch_names(missing_branch_names) merged = raw_repository.merged_branch_names(missing_branch_names)
missing_branch_names.each do |bn| missing_branch_names.each do |bn|
merged_branch_names_hash[bn] = merged.include?(bn) # Redis only stores strings in hset keys, use a fancy encoder
hash[bn] = Gitlab::Redis::Boolean.new(merged.include?(bn))
end end
cache.write(:merged_branch_names, merged_branch_names_hash, expires_in: MERGED_BRANCH_NAMES_CACHE_DURATION)
end end
Set.new(merged_branch_names_hash.select { |_, v| v }.keys) Set.new(merged_branch_names_hash.select { |_, v| Gitlab::Redis::Boolean.true?(v) }.keys)
end end
def merge_base(*commits_or_ids) def merge_base(*commits_or_ids)
...@@ -1168,6 +1156,10 @@ class Repository ...@@ -1168,6 +1156,10 @@ class Repository
@redis_set_cache ||= Gitlab::RepositorySetCache.new(self) @redis_set_cache ||= Gitlab::RepositorySetCache.new(self)
end end
def redis_hash_cache
@redis_hash_cache ||= Gitlab::RepositoryHashCache.new(self)
end
def request_store_cache def request_store_cache
@request_store_cache ||= Gitlab::RepositoryCache.new(self, backend: Gitlab::SafeRequestStore) @request_store_cache ||= Gitlab::RepositoryCache.new(self, backend: Gitlab::SafeRequestStore)
end end
......
# frozen_string_literal: true # frozen_string_literal: true
class SnippetBlobPresenter < BlobPresenter class SnippetBlobPresenter < BlobPresenter
def highlighted_data def rich_data
return if blob.binary? return if blob.binary?
highlight(plain: false) if markup?
blob.rendered_markup
else
highlight(plain: false)
end
end end
def plain_highlighted_data def plain_data
return if blob.binary? return if blob.binary?
highlight(plain: true) highlight(plain: !markup?)
end end
def raw_path def raw_path
...@@ -23,6 +27,10 @@ class SnippetBlobPresenter < BlobPresenter ...@@ -23,6 +27,10 @@ class SnippetBlobPresenter < BlobPresenter
private private
def markup?
blob.rich_viewer&.partial_name == 'markup'
end
def snippet def snippet
blob.snippet blob.snippet
end end
......
...@@ -56,12 +56,10 @@ module Users ...@@ -56,12 +56,10 @@ module Users
MigrateToGhostUserService.new(user).execute unless options[:hard_delete] MigrateToGhostUserService.new(user).execute unless options[:hard_delete]
if Feature.enabled?(:destroy_user_associations_in_batches) # Rails attempts to load all related records into memory before
# Rails attempts to load all related records into memory before # destroying: https://github.com/rails/rails/issues/22510
# destroying: https://github.com/rails/rails/issues/22510 # This ensures we delete records in batches.
# This ensures we delete records in batches. user.destroy_dependent_associations_in_batches
user.destroy_dependent_associations_in_batches
end
# Destroy the namespace after destroying the user since certain methods may depend on the namespace existing # Destroy the namespace after destroying the user since certain methods may depend on the namespace existing
user_data = user.destroy user_data = user.destroy
......
...@@ -25,6 +25,10 @@ class AvatarUploader < GitlabUploader ...@@ -25,6 +25,10 @@ class AvatarUploader < GitlabUploader
self.class.absolute_path(upload) self.class.absolute_path(upload)
end end
def mounted_as
super || 'avatar'
end
private private
def dynamic_segment def dynamic_segment
......
...@@ -958,7 +958,7 @@ ...@@ -958,7 +958,7 @@
:resource_boundary: :unknown :resource_boundary: :unknown
:weight: 1 :weight: 1
- :name: project_export - :name: project_export
:feature_category: :source_code_management :feature_category: :importers
:has_external_dependencies: :has_external_dependencies:
:latency_sensitive: :latency_sensitive:
:resource_boundary: :memory :resource_boundary: :memory
......
...@@ -5,7 +5,7 @@ class ProjectExportWorker ...@@ -5,7 +5,7 @@ class ProjectExportWorker
include ExceptionBacktrace include ExceptionBacktrace
sidekiq_options retry: 3 sidekiq_options retry: 3
feature_category :source_code_management feature_category :importers
worker_resource_boundary :memory worker_resource_boundary :memory
def perform(current_user_id, project_id, after_export_strategy = {}, params = {}) def perform(current_user_id, project_id, after_export_strategy = {}, params = {})
......
---
title: Destroy user associations in batches like we do with projects
merge_request: 24641
author:
type: performance
---
title: Fix bug rendering BlobType markdown data
merge_request: 24960
author:
type: fixed
---
title: Improvement to merged_branch_names cache
merge_request: 24504
author:
type: performance
---
title: Ensure a valid mount_point is used by the AvatarUploader
merge_request: 24800
author:
type: fixed
---
title: Cache repository merged branch names by default
merge_request: 24986
author:
type: performance
...@@ -6757,11 +6757,6 @@ type SnippetBlob { ...@@ -6757,11 +6757,6 @@ type SnippetBlob {
""" """
binary: Boolean! binary: Boolean!
"""
Blob highlighted data
"""
highlightedData: String
""" """
Blob mode Blob mode
""" """
...@@ -6780,13 +6775,18 @@ type SnippetBlob { ...@@ -6780,13 +6775,18 @@ type SnippetBlob {
""" """
Blob plain highlighted data Blob plain highlighted data
""" """
plainHighlightedData: String plainData: String
""" """
Blob raw content endpoint path Blob raw content endpoint path
""" """
rawPath: String! rawPath: String!
"""
Blob highlighted data
"""
richData: String
""" """
Blob content rich viewer Blob content rich viewer
""" """
......
...@@ -7556,20 +7556,6 @@ ...@@ -7556,20 +7556,6 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "highlightedData",
"description": "Blob highlighted data",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "mode", "name": "mode",
"description": "Blob mode", "description": "Blob mode",
...@@ -7613,7 +7599,7 @@ ...@@ -7613,7 +7599,7 @@
"deprecationReason": null "deprecationReason": null
}, },
{ {
"name": "plainHighlightedData", "name": "plainData",
"description": "Blob plain highlighted data", "description": "Blob plain highlighted data",
"args": [ "args": [
...@@ -7644,6 +7630,20 @@ ...@@ -7644,6 +7630,20 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "richData",
"description": "Blob highlighted data",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "richViewer", "name": "richViewer",
"description": "Blob content rich viewer", "description": "Blob content rich viewer",
......
...@@ -1067,12 +1067,12 @@ Represents the snippet blob ...@@ -1067,12 +1067,12 @@ Represents the snippet blob
| Name | Type | Description | | Name | Type | Description |
| --- | ---- | ---------- | | --- | ---- | ---------- |
| `binary` | Boolean! | Shows whether the blob is binary | | `binary` | Boolean! | Shows whether the blob is binary |
| `highlightedData` | String | Blob highlighted data |
| `mode` | String | Blob mode | | `mode` | String | Blob mode |
| `name` | String | Blob name | | `name` | String | Blob name |
| `path` | String | Blob path | | `path` | String | Blob path |
| `plainHighlightedData` | String | Blob plain highlighted data | | `plainData` | String | Blob plain highlighted data |
| `rawPath` | String! | Blob raw content endpoint path | | `rawPath` | String! | Blob raw content endpoint path |
| `richData` | String | Blob highlighted data |
| `richViewer` | SnippetBlobViewer | Blob content rich viewer | | `richViewer` | SnippetBlobViewer | Blob content rich viewer |
| `simpleViewer` | SnippetBlobViewer! | Blob content simple viewer | | `simpleViewer` | SnippetBlobViewer! | Blob content simple viewer |
| `size` | Int! | Blob size | | `size` | Int! | Blob size |
......
...@@ -7,6 +7,8 @@ are very appreciative of the work done by translators and proofreaders! ...@@ -7,6 +7,8 @@ are very appreciative of the work done by translators and proofreaders!
- Albanian - Albanian
- Proofreaders needed. - Proofreaders needed.
- Amharic
- Tsegaselassie Tadesse - [GitLab](https://gitlab.com/tsega), [Crowdin](https://crowdin.com/profile/tsegaselassi/activity)
- Arabic - Arabic
- Proofreaders needed. - Proofreaders needed.
- Bulgarian - Bulgarian
......
...@@ -65,13 +65,13 @@ you can search the content of your logs via a search bar. ...@@ -65,13 +65,13 @@ you can search the content of your logs via a search bar.
The search is passed on to Elasticsearch using the [simple_query_string](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-simple-query-string-query.html) The search is passed on to Elasticsearch using the [simple_query_string](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-simple-query-string-query.html)
Elasticsearch function, which supports the following operators: Elasticsearch function, which supports the following operators:
``` | Operator | Description |
+ signifies AND operation |----------------------------|------------------------------------------------------------|
| signifies OR operation | `\|` | An OR operation. |
- negates a single token | `-` | Negates a single token. |
" wraps a number of tokens to signify a phrase for searching | `+` | An AND operation. |
* at the end of a term signifies a prefix query | `"` | Wraps a number of tokens to signify a phrase for searching. |
( and ) signify precedence | `*` (at the end of a term) | A prefix query. |
~N after a word signifies edit distance (fuzziness) | `(` and `)` | Precedence. |
~N after a phrase signifies slop amount | `~N` (after a word) | Edit distance (fuzziness). |
``` | `~N` (after a phrase) | Slop amount. |
# frozen_string_literal: true
# A serializer for boolean values being stored in Redis.
#
# This is to ensure that booleans are stored in a consistent and
# testable way when being stored as strings in Redis.
#
# Examples:
#
# bool = Gitlab::Redis::Boolean.new(true)
# bool.to_s == "_b:1"
#
# Gitlab::Redis::Boolean.encode(true)
# => "_b:1"
#
# Gitlab::Redis::Boolean.decode("_b:1")
# => true
#
# Gitlab::Redis::Boolean.true?("_b:1")
# => true
#
# Gitlab::Redis::Boolean.true?("_b:0")
# => false
module Gitlab
module Redis
class Boolean
LABEL = "_b"
DELIMITER = ":"
TRUE_STR = "1"
FALSE_STR = "0"
BooleanError = Class.new(StandardError)
NotABooleanError = Class.new(BooleanError)
NotAnEncodedBooleanStringError = Class.new(BooleanError)
def initialize(value)
@value = value
end
# @return [String] the encoded boolean
def to_s
self.class.encode(@value)
end
class << self
# Turn a boolean into a string for storage in Redis
#
# @param value [Boolean] true or false
# @return [String] the encoded boolean
# @raise [NotABooleanError] if the value isn't true or false
def encode(value)
raise NotABooleanError.new(value) unless bool?(value)
[LABEL, to_string(value)].join(DELIMITER)
end
# Decode a boolean string
#
# @param value [String] the stored boolean string
# @return [Boolean] true or false
# @raise [NotAnEncodedBooleanStringError] if the provided value isn't an encoded boolean
def decode(value)
raise NotAnEncodedBooleanStringError.new(value.class) unless value.is_a?(String)
label, bool_str = *value.split(DELIMITER, 2)
raise NotAnEncodedBooleanStringError.new(label) unless label == LABEL
from_string(bool_str)
end
# Decode a boolean string, then test if it's true
#
# @param value [String] the stored boolean string
# @return [Boolean] is the value true?
# @raise [NotAnEncodedBooleanStringError] if the provided value isn't an encoded boolean
def true?(encoded_value)
decode(encoded_value)
end
# Decode a boolean string, then test if it's false
#
# @param value [String] the stored boolean string
# @return [Boolean] is the value false?
# @raise [NotAnEncodedBooleanStringError] if the provided value isn't an encoded boolean
def false?(encoded_value)
!true?(encoded_value)
end
private
def bool?(value)
[true, false].include?(value)
end
def to_string(bool)
bool ? TRUE_STR : FALSE_STR
end
def from_string(str)
raise NotAnEncodedBooleanStringError.new(str) unless [TRUE_STR, FALSE_STR].include?(str)
str == TRUE_STR
end
end
end
end
end
...@@ -132,6 +132,11 @@ module Gitlab ...@@ -132,6 +132,11 @@ module Gitlab
raise NotImplementedError raise NotImplementedError
end end
# RepositoryHashCache to be used. Should be overridden by the including class
def redis_hash_cache
raise NotImplementedError
end
# List of cached methods. Should be overridden by the including class # List of cached methods. Should be overridden by the including class
def cached_methods def cached_methods
raise NotImplementedError raise NotImplementedError
...@@ -215,6 +220,7 @@ module Gitlab ...@@ -215,6 +220,7 @@ module Gitlab
end end
expire_redis_set_method_caches(methods) expire_redis_set_method_caches(methods)
expire_redis_hash_method_caches(methods)
expire_request_store_method_caches(methods) expire_request_store_method_caches(methods)
end end
...@@ -234,6 +240,10 @@ module Gitlab ...@@ -234,6 +240,10 @@ module Gitlab
methods.each { |name| redis_set_cache.expire(name) } methods.each { |name| redis_set_cache.expire(name) }
end end
def expire_redis_hash_method_caches(methods)
methods.each { |name| redis_hash_cache.delete(name) }
end
# All cached repository methods depend on the existence of a Git repository, # All cached repository methods depend on the existence of a Git repository,
# so if the repository doesn't exist, we already know not to call it. # so if the repository doesn't exist, we already know not to call it.
def fallback_early?(method_name) def fallback_early?(method_name)
......
# frozen_string_literal: true
# Interface to the Redis-backed cache store for keys that use a Redis HSET.
# This is currently used as an incremental cache by the `Repository` model
# for `#merged_branch_names`. It works slightly differently to the other
# repository cache classes in that it is intended to work with partial
# caches which can be updated with new data, using the Redis hash system.
module Gitlab
class RepositoryHashCache
attr_reader :repository, :namespace, :expires_in
RepositoryHashCacheError = Class.new(StandardError)
InvalidKeysProvidedError = Class.new(RepositoryHashCacheError)
InvalidHashProvidedError = Class.new(RepositoryHashCacheError)
# @param repository [Repository]
# @param extra_namespace [String]
# @param expires_in [Integer] expiry time for hash store keys
def initialize(repository, extra_namespace: nil, expires_in: 1.day)
@repository = repository
@namespace = "#{repository.full_path}"
@namespace += ":#{repository.project.id}" if repository.project
@namespace = "#{@namespace}:#{extra_namespace}" if extra_namespace
@expires_in = expires_in
end
# @param type [String]
# @return [String]
def cache_key(type)
"#{type}:#{namespace}:hash"
end
# @param key [String]
# @return [Integer] 0 or 1 depending on success
def delete(key)
with { |redis| redis.del(cache_key(key)) }
end
# Check if the provided hash key exists in the hash.
#
# @param key [String]
# @param h_key [String] the key to check presence in Redis
# @return [True, False]
def key?(key, h_key)
with { |redis| redis.hexists(cache_key(key), h_key) }
end
# Read the values of a set of keys from the hash store, and return them
# as a hash of those keys and their values.
#
# @param key [String]
# @param hash_keys [Array<String>] an array of keys to retrieve from the store
# @return [Hash] a Ruby hash of the provided keys and their values from the store
def read_members(key, hash_keys = [])
raise InvalidKeysProvidedError unless hash_keys.is_a?(Array) && hash_keys.any?
with do |redis|
# Fetch an array of values for the supplied keys
values = redis.hmget(cache_key(key), hash_keys)
# Turn it back into a hash
hash_keys.zip(values).to_h
end
end
# Write a hash to the store. All keys and values will be strings when stored.
#
# @param key [String]
# @param hash [Hash] the hash to be written to Redis
# @return [Boolean] whether all operations were successful or not
def write(key, hash)
raise InvalidHashProvidedError unless hash.is_a?(Hash) && hash.any?
full_key = cache_key(key)
with do |redis|
results = redis.pipelined do
# Set each hash key to the provided value
hash.each do |h_key, h_value|
redis.hset(full_key, h_key, h_value)
end
# Update the expiry time for this hset
redis.expire(full_key, expires_in)
end
results.all?
end
end
# A variation on the `fetch` pattern of other cache stores. This method
# allows you to attempt to fetch a group of keys from the hash store, and
# for any keys that are missing values a block is then called to provide
# those values, which are then written back into Redis. Both sets of data
# are then combined and returned as one hash.
#
# @param key [String]
# @param h_keys [Array<String>] the keys to fetch or add to the cache
# @yieldparam missing_keys [Array<String>] the keys missing from the cache
# @yieldparam new_values [Hash] the hash to be populated by the block
# @return [Hash] the amalgamated hash of cached and uncached values
def fetch_and_add_missing(key, h_keys, &block)
# Check the cache for all supplied keys
cache_values = read_members(key, h_keys)
# Find the results which returned nil (meaning they're not in the cache)
missing = cache_values.select { |_, v| v.nil? }.keys
if missing.any?
new_values = {}
# Run the block, which updates the new_values hash
yield(missing, new_values)
# Ensure all values are converted to strings, to ensure merging hashes
# below returns standardised data.
new_values = standardize_hash(new_values)
# Write the new values to the hset
write(key, new_values)
# Merge the two sets of values to return a complete hash
cache_values.merge!(new_values)
end
record_metrics(key, cache_values, missing)
cache_values
end
private
def with(&blk)
Gitlab::Redis::Cache.with(&blk) # rubocop:disable CodeReuse/ActiveRecord
end
# Take a hash and convert both keys and values to strings, for insertion into Redis.
#
# @param hash [Hash]
# @return [Hash] the stringified hash
def standardize_hash(hash)
hash.map { |k, v| [k.to_s, v.to_s] }.to_h
end
# Record metrics in Prometheus.
#
# @param key [String] the basic key, e.g. :merged_branch_names. Not record-specific.
# @param cache_values [Hash] the hash response from the cache read
# @param missing_keys [Array<String>] the array of missing keys from the cache read
def record_metrics(key, cache_values, missing_keys)
cache_hits = cache_values.delete_if { |_, v| v.nil? }
# Increment the counter if we have hits
metrics_hit_counter.increment(full_hit: missing_keys.empty?, store_type: key) if cache_hits.any?
# Track the number of hits we got
metrics_hit_histogram.observe({ type: "hits", store_type: key }, cache_hits.size)
metrics_hit_histogram.observe({ type: "misses", store_type: key }, missing_keys.size)
end
def metrics_hit_counter
@counter ||= Gitlab::Metrics.counter(
:gitlab_repository_hash_cache_hit,
"Count of cache hits in Redis HSET"
)
end
def metrics_hit_histogram
@histogram ||= Gitlab::Metrics.histogram(
:gitlab_repository_hash_cache_size,
"Number of records in the HSET"
)
end
end
end
...@@ -33,6 +33,7 @@ module Quality ...@@ -33,6 +33,7 @@ module Quality
serializers serializers
services services
sidekiq sidekiq
support_specs
tasks tasks
uploaders uploaders
validators validators
......
...@@ -3578,6 +3578,9 @@ msgstr "" ...@@ -3578,6 +3578,9 @@ msgstr ""
msgid "Choose which repositories you want to connect and run CI/CD pipelines." msgid "Choose which repositories you want to connect and run CI/CD pipelines."
msgstr "" msgstr ""
msgid "Choose which shards you wish to synchronize to this secondary node"
msgstr ""
msgid "Choose which shards you wish to synchronize to this secondary node." msgid "Choose which shards you wish to synchronize to this secondary node."
msgstr "" msgstr ""
...@@ -12902,6 +12905,9 @@ msgstr "" ...@@ -12902,6 +12905,9 @@ msgstr ""
msgid "Notes|This comment has changed since you started editing, please review the %{open_link}updated comment%{close_link} to ensure information is not lost" msgid "Notes|This comment has changed since you started editing, please review the %{open_link}updated comment%{close_link} to ensure information is not lost"
msgstr "" msgstr ""
msgid "Nothing found…"
msgstr ""
msgid "Nothing to preview." msgid "Nothing to preview."
msgstr "" msgstr ""
...@@ -17025,6 +17031,9 @@ msgstr "" ...@@ -17025,6 +17031,9 @@ msgstr ""
msgid "Select projects you want to import." msgid "Select projects you want to import."
msgstr "" msgstr ""
msgid "Select shards to replicate"
msgstr ""
msgid "Select source branch" msgid "Select source branch"
msgstr "" msgstr ""
...@@ -17055,6 +17064,9 @@ msgstr "" ...@@ -17055,6 +17064,9 @@ msgstr ""
msgid "Selecting a GitLab user will add a link to the GitLab user in the descriptions of issues and comments (e.g. \"By <a href=\"#\">@johnsmith</a>\"). It will also associate and/or assign these issues and comments with the selected user." msgid "Selecting a GitLab user will add a link to the GitLab user in the descriptions of issues and comments (e.g. \"By <a href=\"#\">@johnsmith</a>\"). It will also associate and/or assign these issues and comments with the selected user."
msgstr "" msgstr ""
msgid "Selective synchronization"
msgstr ""
msgid "Self monitoring project does not exist" msgid "Self monitoring project does not exist"
msgstr "" msgstr ""
...@@ -17385,6 +17397,12 @@ msgstr "" ...@@ -17385,6 +17397,12 @@ msgstr ""
msgid "Severity: %{severity}" msgid "Severity: %{severity}"
msgstr "" msgstr ""
msgid "Shards selected: %{count}"
msgstr ""
msgid "Shards to synchronize"
msgstr ""
msgid "Share" msgid "Share"
msgstr "" msgstr ""
......
...@@ -52,10 +52,10 @@ gitlab: ...@@ -52,10 +52,10 @@ gitlab:
resources: resources:
requests: requests:
cpu: 650m cpu: 650m
memory: 970M memory: 1018M
limits: limits:
cpu: 975m cpu: 975m
memory: 1450M memory: 1527M
hpa: hpa:
targetAverageValue: 650m targetAverageValue: 650m
task-runner: task-runner:
...@@ -69,11 +69,11 @@ gitlab: ...@@ -69,11 +69,11 @@ gitlab:
unicorn: unicorn:
resources: resources:
requests: requests:
cpu: 500m cpu: 525m
memory: 1630M memory: 1711M
limits: limits:
cpu: 750m cpu: 787m
memory: 2450M memory: 2566M
deployment: deployment:
readinessProbe: readinessProbe:
initialDelaySeconds: 5 # Default is 0 initialDelaySeconds: 5 # Default is 0
......
...@@ -4,7 +4,7 @@ require 'spec_helper' ...@@ -4,7 +4,7 @@ require 'spec_helper'
describe GitlabSchema.types['SnippetBlob'] do describe GitlabSchema.types['SnippetBlob'] do
it 'has the correct fields' do it 'has the correct fields' do
expected_fields = [:highlighted_data, :plain_highlighted_data, expected_fields = [:rich_data, :plain_data,
:raw_path, :size, :binary, :name, :path, :raw_path, :size, :binary, :name, :path,
:simple_viewer, :rich_viewer, :mode] :simple_viewer, :rich_viewer, :mode]
......
# frozen_string_literal: true
require "spec_helper"
describe Gitlab::Redis::Boolean do
subject(:redis_boolean) { described_class.new(bool) }
let(:bool) { true }
let(:label_section) { "#{described_class::LABEL}#{described_class::DELIMITER}" }
describe "#to_s" do
subject { redis_boolean.to_s }
context "true" do
let(:bool) { true }
it { is_expected.to eq("#{label_section}#{described_class::TRUE_STR}") }
end
context "false" do
let(:bool) { false }
it { is_expected.to eq("#{label_section}#{described_class::FALSE_STR}") }
end
end
describe ".encode" do
subject { redis_boolean.class.encode(bool) }
context "true" do
let(:bool) { true }
it { is_expected.to eq("#{label_section}#{described_class::TRUE_STR}") }
end
context "false" do
let(:bool) { false }
it { is_expected.to eq("#{label_section}#{described_class::FALSE_STR}") }
end
end
describe ".decode" do
subject { redis_boolean.class.decode(str) }
context "valid encoded bool" do
let(:str) { "#{label_section}#{bool_str}" }
context "true" do
let(:bool_str) { described_class::TRUE_STR }
it { is_expected.to be(true) }
end
context "false" do
let(:bool_str) { described_class::FALSE_STR }
it { is_expected.to be(false) }
end
end
context "partially invalid bool" do
let(:str) { "#{label_section}whoops" }
it "raises an error" do
expect { subject }.to raise_error(described_class::NotAnEncodedBooleanStringError)
end
end
context "invalid encoded bool" do
let(:str) { "whoops" }
it "raises an error" do
expect { subject }.to raise_error(described_class::NotAnEncodedBooleanStringError)
end
end
end
describe ".true?" do
subject { redis_boolean.class.true?(str) }
context "valid encoded bool" do
let(:str) { "#{label_section}#{bool_str}" }
context "true" do
let(:bool_str) { described_class::TRUE_STR }
it { is_expected.to be(true) }
end
context "false" do
let(:bool_str) { described_class::FALSE_STR }
it { is_expected.to be(false) }
end
end
context "partially invalid bool" do
let(:str) { "#{label_section}whoops" }
it "raises an error" do
expect { subject }.to raise_error(described_class::NotAnEncodedBooleanStringError)
end
end
context "invalid encoded bool" do
let(:str) { "whoops" }
it "raises an error" do
expect { subject }.to raise_error(described_class::NotAnEncodedBooleanStringError)
end
end
end
describe ".false?" do
subject { redis_boolean.class.false?(str) }
context "valid encoded bool" do
let(:str) { "#{label_section}#{bool_str}" }
context "true" do
let(:bool_str) { described_class::TRUE_STR }
it { is_expected.to be(false) }
end
context "false" do
let(:bool_str) { described_class::FALSE_STR }
it { is_expected.to be(true) }
end
end
context "partially invalid bool" do
let(:str) { "#{label_section}whoops" }
it "raises an error" do
expect { subject }.to raise_error(described_class::NotAnEncodedBooleanStringError)
end
end
context "invalid encoded bool" do
let(:str) { "whoops" }
it "raises an error" do
expect { subject }.to raise_error(described_class::NotAnEncodedBooleanStringError)
end
end
end
end
...@@ -7,6 +7,7 @@ describe Gitlab::RepositoryCacheAdapter do ...@@ -7,6 +7,7 @@ describe Gitlab::RepositoryCacheAdapter do
let(:repository) { project.repository } let(:repository) { project.repository }
let(:cache) { repository.send(:cache) } let(:cache) { repository.send(:cache) }
let(:redis_set_cache) { repository.send(:redis_set_cache) } let(:redis_set_cache) { repository.send(:redis_set_cache) }
let(:redis_hash_cache) { repository.send(:redis_hash_cache) }
describe '#cache_method_output', :use_clean_rails_memory_store_caching do describe '#cache_method_output', :use_clean_rails_memory_store_caching do
let(:fallback) { 10 } let(:fallback) { 10 }
...@@ -212,6 +213,8 @@ describe Gitlab::RepositoryCacheAdapter do ...@@ -212,6 +213,8 @@ describe Gitlab::RepositoryCacheAdapter do
expect(cache).to receive(:expire).with(:branch_names) expect(cache).to receive(:expire).with(:branch_names)
expect(redis_set_cache).to receive(:expire).with(:rendered_readme) expect(redis_set_cache).to receive(:expire).with(:rendered_readme)
expect(redis_set_cache).to receive(:expire).with(:branch_names) expect(redis_set_cache).to receive(:expire).with(:branch_names)
expect(redis_hash_cache).to receive(:delete).with(:rendered_readme)
expect(redis_hash_cache).to receive(:delete).with(:branch_names)
repository.expire_method_caches(%i(rendered_readme branch_names)) repository.expire_method_caches(%i(rendered_readme branch_names))
end end
......
# frozen_string_literal: true
require "spec_helper"
describe Gitlab::RepositoryHashCache, :clean_gitlab_redis_cache do
let_it_be(:project) { create(:project) }
let(:repository) { project.repository }
let(:namespace) { "#{repository.full_path}:#{project.id}" }
let(:cache) { described_class.new(repository) }
let(:test_hash) do
{ "test" => "value" }
end
describe "#cache_key" do
subject { cache.cache_key(:example) }
it "includes the namespace" do
is_expected.to eq("example:#{namespace}:hash")
end
context "with a given namespace" do
let(:extra_namespace) { "my:data" }
let(:cache) { described_class.new(repository, extra_namespace: extra_namespace) }
it "includes the full namespace" do
is_expected.to eq("example:#{namespace}:#{extra_namespace}:hash")
end
end
end
describe "#delete" do
subject { cache.delete(:example) }
context "key exists" do
before do
cache.write(:example, test_hash)
end
it { is_expected.to eq(1) }
it "deletes the given key from the cache" do
subject
expect(cache.read_members(:example, ["test"])).to eq({ "test" => nil })
end
end
context "key doesn't exist" do
it { is_expected.to eq(0) }
end
end
describe "#key?" do
subject { cache.key?(:example, "test") }
context "key exists" do
before do
cache.write(:example, test_hash)
end
it { is_expected.to be(true) }
end
context "key doesn't exist" do
it { is_expected.to be(false) }
end
end
describe "#read_members" do
subject { cache.read_members(:example, keys) }
let(:keys) { %w(test missing) }
context "all data is cached" do
before do
cache.write(:example, test_hash.merge({ "missing" => false }))
end
it { is_expected.to eq({ "test" => "value", "missing" => "false" }) }
end
context "partial data is cached" do
before do
cache.write(:example, test_hash)
end
it { is_expected.to eq({ "test" => "value", "missing" => nil }) }
end
context "no data is cached" do
it { is_expected.to eq({ "test" => nil, "missing" => nil }) }
end
context "empty keys are passed for some reason" do
let(:keys) { [] }
it "raises an error" do
expect { subject }.to raise_error(Gitlab::RepositoryHashCache::InvalidKeysProvidedError)
end
end
end
describe "#write" do
subject { cache.write(:example, test_hash) }
it { is_expected.to be(true) }
it "actually writes stuff to Redis" do
subject
expect(cache.read_members(:example, ["test"])).to eq(test_hash)
end
end
describe "#fetch_and_add_missing" do
subject do
cache.fetch_and_add_missing(:example, keys) do |missing_keys, hash|
missing_keys.each do |key|
hash[key] = "was_missing"
end
end
end
let(:keys) { %w(test) }
it "records metrics" do
# Here we expect it to receive "test" as a missing key because we
# don't write to the cache before this test
expect(cache).to receive(:record_metrics).with(:example, { "test" => "was_missing" }, ["test"])
subject
end
context "fully cached" do
let(:keys) { %w(test another) }
before do
cache.write(:example, test_hash.merge({ "another" => "not_missing" }))
end
it "returns a hash" do
is_expected.to eq({ "test" => "value", "another" => "not_missing" })
end
it "doesn't write to the cache" do
expect(cache).not_to receive(:write)
subject
end
end
context "partially cached" do
let(:keys) { %w(test missing) }
before do
cache.write(:example, test_hash)
end
it "returns a hash" do
is_expected.to eq({ "test" => "value", "missing" => "was_missing" })
end
it "writes to the cache" do
expect(cache).to receive(:write).with(:example, { "missing" => "was_missing" })
subject
end
end
context "uncached" do
let(:keys) { %w(test missing) }
it "returns a hash" do
is_expected.to eq({ "test" => "was_missing", "missing" => "was_missing" })
end
it "writes to the cache" do
expect(cache).to receive(:write).with(:example, { "test" => "was_missing", "missing" => "was_missing" })
subject
end
end
end
end
...@@ -21,7 +21,7 @@ RSpec.describe Quality::TestLevel do ...@@ -21,7 +21,7 @@ RSpec.describe Quality::TestLevel do
context 'when level is unit' do context 'when level is unit' do
it 'returns a pattern' do it 'returns a pattern' do
expect(subject.pattern(:unit)) expect(subject.pattern(:unit))
.to eq("spec/{bin,config,db,dependencies,factories,finders,frontend,graphql,haml_lint,helpers,initializers,javascripts,lib,models,policies,presenters,rack_servers,replicators,routing,rubocop,serializers,services,sidekiq,tasks,uploaders,validators,views,workers,elastic_integration}{,/**/}*_spec.rb") .to eq("spec/{bin,config,db,dependencies,factories,finders,frontend,graphql,haml_lint,helpers,initializers,javascripts,lib,models,policies,presenters,rack_servers,replicators,routing,rubocop,serializers,services,sidekiq,support_specs,tasks,uploaders,validators,views,workers,elastic_integration}{,/**/}*_spec.rb")
end end
end end
...@@ -82,7 +82,7 @@ RSpec.describe Quality::TestLevel do ...@@ -82,7 +82,7 @@ RSpec.describe Quality::TestLevel do
context 'when level is unit' do context 'when level is unit' do
it 'returns a regexp' do it 'returns a regexp' do
expect(subject.regexp(:unit)) expect(subject.regexp(:unit))
.to eq(%r{spec/(bin|config|db|dependencies|factories|finders|frontend|graphql|haml_lint|helpers|initializers|javascripts|lib|models|policies|presenters|rack_servers|replicators|routing|rubocop|serializers|services|sidekiq|tasks|uploaders|validators|views|workers|elastic_integration)}) .to eq(%r{spec/(bin|config|db|dependencies|factories|finders|frontend|graphql|haml_lint|helpers|initializers|javascripts|lib|models|policies|presenters|rack_servers|replicators|routing|rubocop|serializers|services|sidekiq|support_specs|tasks|uploaders|validators|views|workers|elastic_integration)})
end end
end end
......
...@@ -500,45 +500,62 @@ describe Repository do ...@@ -500,45 +500,62 @@ describe Repository do
let(:branch_names) { %w(test beep boop definitely_merged) } let(:branch_names) { %w(test beep boop definitely_merged) }
let(:already_merged) { Set.new(["definitely_merged"]) } let(:already_merged) { Set.new(["definitely_merged"]) }
let(:merge_state_hash) do let(:write_hash) do
{ {
"test" => false, "test" => Gitlab::Redis::Boolean.new(false).to_s,
"beep" => false, "beep" => Gitlab::Redis::Boolean.new(false).to_s,
"boop" => false, "boop" => Gitlab::Redis::Boolean.new(false).to_s,
"definitely_merged" => true "definitely_merged" => Gitlab::Redis::Boolean.new(true).to_s
} }
end end
let_it_be(:cache) do let(:read_hash) do
caching_config_hash = Gitlab::Redis::Cache.params {
ActiveSupport::Cache.lookup_store(:redis_cache_store, caching_config_hash) "test" => Gitlab::Redis::Boolean.new(false).to_s,
end "beep" => Gitlab::Redis::Boolean.new(false).to_s,
"boop" => Gitlab::Redis::Boolean.new(false).to_s,
let(:repository_cache) do "definitely_merged" => Gitlab::Redis::Boolean.new(true).to_s
Gitlab::RepositoryCache.new(repository, backend: Rails.cache) }
end end
let(:cache_key) { repository_cache.cache_key(:merged_branch_names) } let(:cache) { repository.send(:redis_hash_cache) }
let(:cache_key) { cache.cache_key(:merged_branch_names) }
before do before do
allow(Rails).to receive(:cache) { cache }
allow(repository).to receive(:cache) { repository_cache }
allow(repository.raw_repository).to receive(:merged_branch_names).with(branch_names).and_return(already_merged) allow(repository.raw_repository).to receive(:merged_branch_names).with(branch_names).and_return(already_merged)
end end
it { is_expected.to eq(already_merged) } it { is_expected.to eq(already_merged) }
it { is_expected.to be_a(Set) } it { is_expected.to be_a(Set) }
describe "cache expiry" do
before do
allow(cache).to receive(:delete).with(anything)
end
it "is expired when the branches caches are expired" do
expect(cache).to receive(:delete).with(:merged_branch_names).at_least(:once)
repository.send(:expire_branches_cache)
end
it "is expired when the repository caches are expired" do
expect(cache).to receive(:delete).with(:merged_branch_names).at_least(:once)
repository.send(:expire_all_method_caches)
end
end
context "cache is empty" do context "cache is empty" do
before do before do
cache.delete(cache_key) cache.delete(:merged_branch_names)
end end
it { is_expected.to eq(already_merged) } it { is_expected.to eq(already_merged) }
describe "cache values" do describe "cache values" do
it "writes the values to redis" do it "writes the values to redis" do
expect(cache).to receive(:write).with(cache_key, merge_state_hash, expires_in: Repository::MERGED_BRANCH_NAMES_CACHE_DURATION) expect(cache).to receive(:write).with(:merged_branch_names, write_hash)
subject subject
end end
...@@ -546,14 +563,14 @@ describe Repository do ...@@ -546,14 +563,14 @@ describe Repository do
it "matches the supplied hash" do it "matches the supplied hash" do
subject subject
expect(cache.read(cache_key)).to eq(merge_state_hash) expect(cache.read_members(:merged_branch_names, branch_names)).to eq(read_hash)
end end
end end
end end
context "cache is not empty" do context "cache is not empty" do
before do before do
cache.write(cache_key, merge_state_hash) cache.write(:merged_branch_names, write_hash)
end end
it { is_expected.to eq(already_merged) } it { is_expected.to eq(already_merged) }
...@@ -568,8 +585,8 @@ describe Repository do ...@@ -568,8 +585,8 @@ describe Repository do
context "cache is partially complete" do context "cache is partially complete" do
before do before do
allow(repository.raw_repository).to receive(:merged_branch_names).with(["boop"]).and_return([]) allow(repository.raw_repository).to receive(:merged_branch_names).with(["boop"]).and_return([])
hash = merge_state_hash.except("boop") hash = write_hash.except("boop")
cache.write(cache_key, hash) cache.write(:merged_branch_names, hash)
end end
it { is_expected.to eq(already_merged) } it { is_expected.to eq(already_merged) }
......
...@@ -3,10 +3,10 @@ ...@@ -3,10 +3,10 @@
require 'spec_helper' require 'spec_helper'
describe SnippetBlobPresenter do describe SnippetBlobPresenter do
describe '#highlighted_data' do describe '#rich_data' do
let(:snippet) { build(:personal_snippet) } let(:snippet) { build(:personal_snippet) }
subject { described_class.new(snippet.blob).highlighted_data } subject { described_class.new(snippet.blob).rich_data }
it 'returns nil when the snippet blob is binary' do it 'returns nil when the snippet blob is binary' do
allow(snippet.blob).to receive(:binary?).and_return(true) allow(snippet.blob).to receive(:binary?).and_return(true)
...@@ -18,7 +18,7 @@ describe SnippetBlobPresenter do ...@@ -18,7 +18,7 @@ describe SnippetBlobPresenter do
snippet.file_name = 'test.md' snippet.file_name = 'test.md'
snippet.content = '*foo*' snippet.content = '*foo*'
expect(subject).to eq '<span id="LC1" class="line" lang="markdown"><span class="ge">*foo*</span></span>' expect(subject).to eq '<p data-sourcepos="1:1-1:5" dir="auto"><em>foo</em></p>'
end end
it 'returns syntax highlighted content' do it 'returns syntax highlighted content' do
...@@ -37,10 +37,10 @@ describe SnippetBlobPresenter do ...@@ -37,10 +37,10 @@ describe SnippetBlobPresenter do
end end
end end
describe '#plain_highlighted_data' do describe '#plain_data' do
let(:snippet) { build(:personal_snippet) } let(:snippet) { build(:personal_snippet) }
subject { described_class.new(snippet.blob).plain_highlighted_data } subject { described_class.new(snippet.blob).plain_data }
it 'returns nil when the snippet blob is binary' do it 'returns nil when the snippet blob is binary' do
allow(snippet.blob).to receive(:binary?).and_return(true) allow(snippet.blob).to receive(:binary?).and_return(true)
...@@ -52,7 +52,7 @@ describe SnippetBlobPresenter do ...@@ -52,7 +52,7 @@ describe SnippetBlobPresenter do
snippet.file_name = 'test.md' snippet.file_name = 'test.md'
snippet.content = '*foo*' snippet.content = '*foo*'
expect(subject).to eq '<span id="LC1" class="line" lang="">*foo*</span>' expect(subject).to eq '<span id="LC1" class="line" lang="markdown"><span class="ge">*foo*</span></span>'
end end
it 'returns plain syntax content' do it 'returns plain syntax content' do
......
...@@ -67,7 +67,8 @@ describe 'Creating a Snippet' do ...@@ -67,7 +67,8 @@ describe 'Creating a Snippet' do
it 'returns the created Snippet' do it 'returns the created Snippet' do
post_graphql_mutation(mutation, current_user: current_user) post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response['snippet']['blob']['highlightedData']).to match(content) expect(mutation_response['snippet']['blob']['richData']).to match(content)
expect(mutation_response['snippet']['blob']['plainData']).to match(content)
expect(mutation_response['snippet']['title']).to eq(title) expect(mutation_response['snippet']['title']).to eq(title)
expect(mutation_response['snippet']['description']).to eq(description) expect(mutation_response['snippet']['description']).to eq(description)
expect(mutation_response['snippet']['fileName']).to eq(file_name) expect(mutation_response['snippet']['fileName']).to eq(file_name)
...@@ -92,7 +93,8 @@ describe 'Creating a Snippet' do ...@@ -92,7 +93,8 @@ describe 'Creating a Snippet' do
it 'returns the created Snippet' do it 'returns the created Snippet' do
post_graphql_mutation(mutation, current_user: current_user) post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response['snippet']['blob']['highlightedData']).to match(content) expect(mutation_response['snippet']['blob']['richData']).to match(content)
expect(mutation_response['snippet']['blob']['plainData']).to match(content)
expect(mutation_response['snippet']['title']).to eq(title) expect(mutation_response['snippet']['title']).to eq(title)
expect(mutation_response['snippet']['description']).to eq(description) expect(mutation_response['snippet']['description']).to eq(description)
expect(mutation_response['snippet']['fileName']).to eq(file_name) expect(mutation_response['snippet']['fileName']).to eq(file_name)
......
...@@ -56,7 +56,8 @@ describe 'Updating a Snippet' do ...@@ -56,7 +56,8 @@ describe 'Updating a Snippet' do
it 'returns the updated Snippet' do it 'returns the updated Snippet' do
post_graphql_mutation(mutation, current_user: current_user) post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response['snippet']['blob']['highlightedData']).to match(updated_content) expect(mutation_response['snippet']['blob']['richData']).to match(updated_content)
expect(mutation_response['snippet']['blob']['plainData']).to match(updated_content)
expect(mutation_response['snippet']['title']).to eq(updated_title) expect(mutation_response['snippet']['title']).to eq(updated_title)
expect(mutation_response['snippet']['description']).to eq(updated_description) expect(mutation_response['snippet']['description']).to eq(updated_description)
expect(mutation_response['snippet']['fileName']).to eq(updated_file_name) expect(mutation_response['snippet']['fileName']).to eq(updated_file_name)
...@@ -77,7 +78,8 @@ describe 'Updating a Snippet' do ...@@ -77,7 +78,8 @@ describe 'Updating a Snippet' do
it 'returns the Snippet with its original values' do it 'returns the Snippet with its original values' do
post_graphql_mutation(mutation, current_user: current_user) post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response['snippet']['blob']['highlightedData']).to match(original_content) expect(mutation_response['snippet']['blob']['richData']).to match(original_content)
expect(mutation_response['snippet']['blob']['plainData']).to match(original_content)
expect(mutation_response['snippet']['title']).to eq(original_title) expect(mutation_response['snippet']['title']).to eq(original_title)
expect(mutation_response['snippet']['description']).to eq(original_description) expect(mutation_response['snippet']['description']).to eq(original_description)
expect(mutation_response['snippet']['fileName']).to eq(original_file_name) expect(mutation_response['snippet']['fileName']).to eq(original_file_name)
......
...@@ -26,16 +26,6 @@ describe Users::DestroyService do ...@@ -26,16 +26,6 @@ describe Users::DestroyService do
service.execute(user) service.execute(user)
end end
context 'when :destroy_user_associations_in_batches flag is disabled' do
it 'does not delete user associations in batches' do
stub_feature_flags(destroy_user_associations_in_batches: false)
expect(user).not_to receive(:destroy_dependent_associations_in_batches)
service.execute(user)
end
end
it 'will delete the project' do it 'will delete the project' do
expect_next_instance_of(Projects::DestroyService) do |destroy_service| expect_next_instance_of(Projects::DestroyService) do |destroy_service|
expect(destroy_service).to receive(:execute).once.and_return(true) expect(destroy_service).to receive(:execute).once.and_return(true)
......
...@@ -2,12 +2,15 @@ ...@@ -2,12 +2,15 @@
module ActiveRecord module ActiveRecord
class QueryRecorder class QueryRecorder
attr_reader :log, :skip_cached, :cached attr_reader :log, :skip_cached, :cached, :data
UNKNOWN = %w(unknown unknown).freeze
def initialize(skip_cached: true, &block) def initialize(skip_cached: true, query_recorder_debug: false, &block)
@data = Hash.new { |h, k| h[k] = { count: 0, occurrences: [], backtrace: [] } }
@log = [] @log = []
@cached = [] @cached = []
@skip_cached = skip_cached @skip_cached = skip_cached
@query_recorder_debug = query_recorder_debug
# force replacement of bind parameters to give tests the ability to check for ids # force replacement of bind parameters to give tests the ability to check for ids
ActiveRecord::Base.connection.unprepared_statement do ActiveRecord::Base.connection.unprepared_statement do
ActiveSupport::Notifications.subscribed(method(:callback), 'sql.active_record', &block) ActiveSupport::Notifications.subscribed(method(:callback), 'sql.active_record', &block)
...@@ -19,30 +22,62 @@ module ActiveRecord ...@@ -19,30 +22,62 @@ module ActiveRecord
Gitlab::BacktraceCleaner.clean_backtrace(caller).each { |line| Rails.logger.debug(" --> #{line}") } Gitlab::BacktraceCleaner.clean_backtrace(caller).each { |line| Rails.logger.debug(" --> #{line}") }
end end
def get_sql_source(sql)
matches = sql.match(/,line:(?<line>.*):in\s+`(?<method>.*)'\*\//)
matches ? [matches[:line], matches[:method]] : UNKNOWN
end
def store_sql_by_source(values: {}, backtrace: nil)
full_name = get_sql_source(values[:sql]).join(':')
@data[full_name][:count] += 1
@data[full_name][:occurrences] << values[:sql]
@data[full_name][:backtrace] << backtrace
end
def find_query(query_regexp, limit, first_only: false)
out = []
@data.each_pair do |k, v|
if v[:count] > limit && k.match(query_regexp)
out << [k, v[:count]]
break if first_only
end
end
out.flatten! if first_only
out
end
def occurrences_by_line_method
@occurrences_by_line_method ||= @data.sort_by { |_, v| v[:count] }
end
def callback(name, start, finish, message_id, values) def callback(name, start, finish, message_id, values)
show_backtrace(values) if ENV['QUERY_RECORDER_DEBUG'] store_backtrace = ENV['QUERY_RECORDER_DEBUG'] || @query_recorder_debug
backtrace = store_backtrace ? show_backtrace(values) : nil
if values[:cached] && skip_cached if values[:cached] && skip_cached
@cached << values[:sql] @cached << values[:sql]
elsif !values[:name]&.include?("SCHEMA") elsif !values[:name]&.include?("SCHEMA")
@log << values[:sql] @log << values[:sql]
store_sql_by_source(values: values, backtrace: backtrace)
end end
end end
def count def count
@log.count @count ||= @log.count
end end
def cached_count def cached_count
@cached.count @cached_count ||= @cached.count
end end
def log_message def log_message
@log.join("\n\n") @log_message ||= @log.join("\n\n")
end end
def occurrences def occurrences
@log.group_by(&:to_s).transform_values(&:count) @occurrences ||= @log.group_by(&:to_s).transform_values(&:count)
end end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
describe ActiveRecord::QueryRecorder do
class TestQueries < ActiveRecord::Base
self.table_name = 'schema_migrations'
end
describe 'detecting the right number of calls and their origin' do
it 'detects two separate queries' do
control = ActiveRecord::QueryRecorder.new query_recorder_debug: true do
2.times { TestQueries.count }
TestQueries.first
end
# Test first_only flag works as expected
expect(control.find_query(/.*query_recorder_spec.rb.*/, 0, first_only: true))
.to eq(control.find_query(/.*query_recorder_spec.rb.*/, 0).first)
# Check #find_query
expect(control.find_query(/.*/, 0).size)
.to eq(control.data.keys.size)
# Ensure exactly 2 COUNT queries were detected
expect(control.occurrences_by_line_method.last[1][:occurrences]
.find_all {|i| i.match(/SELECT COUNT/) }.count).to eq(2)
# Ensure exactly 1 LIMIT 1 (#first)
expect(control.occurrences_by_line_method.first[1][:occurrences]
.find_all { |i| i.match(/ORDER BY.*#{TestQueries.table_name}.*LIMIT 1/) }.count).to eq(1)
# Ensure 3 DB calls overall were executed
expect(control.log.size).to eq(3)
# Ensure memoization value match the raw value above
expect(control.count).to eq(control.log.size)
# Ensure we have only two sources of queries
expect(control.data.keys.size).to eq(2)
# Ensure we detect only queries from this file
expect(control.data.keys.find_all { |i| i.match(/query_recorder_spec.rb/) }.count).to eq(2)
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