Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-05-17 19:05:49 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-05-17 19:05:49 +0300
commit43a25d93ebdabea52f99b05e15b06250cd8f07d7 (patch)
treedceebdc68925362117480a5d672bcff122fb625b /lib/gitlab/graphql
parent20c84b99005abd1c82101dfeff264ac50d2df211 (diff)
Add latest changes from gitlab-org/gitlab@16-0-stable-eev16.0.0-rc42
Diffstat (limited to 'lib/gitlab/graphql')
-rw-r--r--lib/gitlab/graphql/authorize/authorize_resource.rb4
-rw-r--r--lib/gitlab/graphql/deprecations/deprecation.rb4
-rw-r--r--lib/gitlab/graphql/loaders/lazy_relation_loader.rb74
-rw-r--r--lib/gitlab/graphql/loaders/lazy_relation_loader/registry.rb75
-rw-r--r--lib/gitlab/graphql/loaders/lazy_relation_loader/relation_proxy.rb52
-rw-r--r--lib/gitlab/graphql/loaders/lazy_relation_loader/top_n_loader.rb83
-rw-r--r--lib/gitlab/graphql/pagination/connections.rb4
-rw-r--r--lib/gitlab/graphql/project/dast_profile_connection_extension.rb9
-rw-r--r--lib/gitlab/graphql/subscriptions/action_cable_with_load_balancing.rb54
9 files changed, 352 insertions, 7 deletions
diff --git a/lib/gitlab/graphql/authorize/authorize_resource.rb b/lib/gitlab/graphql/authorize/authorize_resource.rb
index 983bdb9c0a2..e3548b97ebf 100644
--- a/lib/gitlab/graphql/authorize/authorize_resource.rb
+++ b/lib/gitlab/graphql/authorize/authorize_resource.rb
@@ -45,8 +45,8 @@ module Gitlab
end
end
- def find_object(*args)
- raise NotImplementedError, "Implement #find_object in #{self.class.name}"
+ def find_object(id:)
+ GitlabSchema.find_by_gid(id)
end
def authorized_find!(*args, **kwargs)
diff --git a/lib/gitlab/graphql/deprecations/deprecation.rb b/lib/gitlab/graphql/deprecations/deprecation.rb
index 7f4cea7c635..dfcca5ee75b 100644
--- a/lib/gitlab/graphql/deprecations/deprecation.rb
+++ b/lib/gitlab/graphql/deprecations/deprecation.rb
@@ -9,7 +9,7 @@ module Gitlab
REASONS = {
REASON_RENAMED => 'This was renamed.',
- REASON_ALPHA => 'This feature is in Alpha. It can be changed or removed at any time.'
+ REASON_ALPHA => 'This feature is an Experiment. It can be changed or removed at any time.'
}.freeze
include ActiveModel::Validations
@@ -27,7 +27,7 @@ module Gitlab
return unless options
if alpha
- raise ArgumentError, '`alpha` and `deprecated` arguments cannot be passed at the same time' \
+ raise ArgumentError, '`experiment` and `deprecated` arguments cannot be passed at the same time' \
if deprecated
options[:reason] = :alpha
diff --git a/lib/gitlab/graphql/loaders/lazy_relation_loader.rb b/lib/gitlab/graphql/loaders/lazy_relation_loader.rb
new file mode 100644
index 00000000000..69056e87091
--- /dev/null
+++ b/lib/gitlab/graphql/loaders/lazy_relation_loader.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ module Loaders
+ class LazyRelationLoader
+ class << self
+ attr_accessor :model, :association
+
+ # Automatically register the inheriting
+ # classes to GitlabSchema as lazy objects.
+ def inherited(klass)
+ GitlabSchema.lazy_resolve(klass, :load)
+ end
+ end
+
+ def initialize(query_ctx, object, **kwargs)
+ @query_ctx = query_ctx
+ @object = object
+ @kwargs = kwargs
+
+ query_ctx[loader_cache_key] ||= Registry.new(relation(**kwargs))
+ query_ctx[loader_cache_key].register(object)
+ end
+
+ # Returns an instance of `RelationProxy` for the object (parent model).
+ # The returned object behaves like an Active Record relation to support
+ # keyset pagination.
+ def load
+ case reflection.macro
+ when :has_many
+ relation_proxy
+ when :has_one
+ relation_proxy.last
+ else
+ raise 'Not supported association type!'
+ end
+ end
+
+ private
+
+ attr_reader :query_ctx, :object, :kwargs
+
+ delegate :model, :association, to: :"self.class"
+
+ # Implement this one if you want to filter the relation
+ def relation(**)
+ base_relation
+ end
+
+ def loader_cache_key
+ @loader_cache_key ||= self.class.name.to_s + kwargs.sort.to_s
+ end
+
+ def base_relation
+ placeholder_record.association(association).scope
+ end
+
+ # This will only work for HasMany and HasOne associations for now
+ def placeholder_record
+ model.new(reflection.active_record_primary_key => 0)
+ end
+
+ def reflection
+ model.reflections[association.to_s]
+ end
+
+ def relation_proxy
+ RelationProxy.new(object, query_ctx[loader_cache_key])
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/loaders/lazy_relation_loader/registry.rb b/lib/gitlab/graphql/loaders/lazy_relation_loader/registry.rb
new file mode 100644
index 00000000000..ab2b2bd4dc2
--- /dev/null
+++ b/lib/gitlab/graphql/loaders/lazy_relation_loader/registry.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ module Loaders
+ class LazyRelationLoader
+ class Registry
+ PrematureQueryExecutionTriggered = Class.new(RuntimeError)
+ # Following methods are Active Record kicker methods which fire SQL query.
+ # We can support some of them with TopNLoader but for now restricting their use
+ # as we don't have a use case.
+ PROHIBITED_METHODS = (
+ ActiveRecord::FinderMethods.instance_methods(false) +
+ ActiveRecord::Calculations.instance_methods(false)
+ ).to_set.freeze
+
+ def initialize(relation)
+ @parents = []
+ @relation = relation
+ @records = []
+ @loaded = false
+ end
+
+ def register(object)
+ @parents << object
+ end
+
+ def method_missing(method_name, ...)
+ raise PrematureQueryExecutionTriggered if PROHIBITED_METHODS.include?(method_name)
+
+ result = relation.public_send(method_name, ...) # rubocop:disable GitlabSecurity/PublicSend
+
+ if result.is_a?(ActiveRecord::Relation) # Spawn methods generate a new relation (e.g. where, limit)
+ @relation = result
+
+ return self
+ end
+
+ result
+ end
+
+ def respond_to_missing?(method_name, include_private = false)
+ relation.respond_to?(method_name, include_private)
+ end
+
+ def load
+ return records if loaded
+
+ @loaded = true
+ @records = TopNLoader.load(relation, parents)
+ end
+
+ def for(object)
+ load.select { |record| record[foreign_key] == object[active_record_primary_key] }
+ .tap { |records| set_inverse_of(object, records) }
+ end
+
+ private
+
+ attr_reader :parents, :relation, :records, :loaded
+
+ delegate :proxy_association, to: :relation, private: true
+ delegate :reflection, to: :proxy_association, private: true
+ delegate :active_record_primary_key, :foreign_key, to: :reflection, private: true
+
+ def set_inverse_of(object, records)
+ records.each do |record|
+ object.association(reflection.name).set_inverse_instance(record)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/loaders/lazy_relation_loader/relation_proxy.rb b/lib/gitlab/graphql/loaders/lazy_relation_loader/relation_proxy.rb
new file mode 100644
index 00000000000..bab2a272fb0
--- /dev/null
+++ b/lib/gitlab/graphql/loaders/lazy_relation_loader/relation_proxy.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ module Loaders
+ class LazyRelationLoader
+ # Proxies all the method calls to Registry instance.
+ # The main purpose of having this is that calling load
+ # on an instance of this class will only return the records
+ # associated with the main Active Record model.
+ class RelationProxy
+ def initialize(object, registry)
+ @object = object
+ @registry = registry
+ end
+
+ def load
+ registry.for(object)
+ end
+ alias_method :to_a, :load
+
+ def last(limit = 1)
+ result = registry.limit(limit)
+ .reverse_order!
+ .for(object)
+
+ return result.first if limit == 1 # This is the Active Record behavior
+
+ result
+ end
+
+ private
+
+ attr_reader :registry, :object
+
+ # Delegate everything to registry
+ def method_missing(method_name, ...)
+ result = registry.public_send(method_name, ...) # rubocop:disable GitlabSecurity/PublicSend
+
+ return self if result == registry
+
+ result
+ end
+
+ def respond_to_missing?(method_name, include_private = false)
+ registry.respond_to?(method_name, include_private)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/loaders/lazy_relation_loader/top_n_loader.rb b/lib/gitlab/graphql/loaders/lazy_relation_loader/top_n_loader.rb
new file mode 100644
index 00000000000..6404148832b
--- /dev/null
+++ b/lib/gitlab/graphql/loaders/lazy_relation_loader/top_n_loader.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+# rubocop:disable CodeReuse/ActiveRecord
+module Gitlab
+ module Graphql
+ module Loaders
+ class LazyRelationLoader
+ # Loads the top-n records for each given parent record.
+ # For example; if you want to load only 5 confidential issues ordered by
+ # their updated_at column per project for a list of projects by issuing only a single
+ # SQL query then this class can help you.
+ # Note that the limit applies per parent record which means that if you apply limit as 5
+ # for 10 projects, this loader will load 50 records in total.
+ class TopNLoader
+ def self.load(original_relation, parents)
+ new(original_relation, parents).load
+ end
+
+ def initialize(original_relation, parents)
+ @original_relation = original_relation
+ @parents = parents
+ end
+
+ def load
+ klass.select(klass.arel_table[Arel.star])
+ .from(from)
+ .joins("JOIN LATERAL (#{lateral_relation.to_sql}) AS #{klass.arel_table.name} ON true")
+ .includes(original_includes)
+ .preload(original_preload)
+ .eager_load(original_eager_load)
+ .load
+ end
+
+ private
+
+ attr_reader :original_relation, :parents
+
+ delegate :proxy_association, to: :original_relation, private: true
+ delegate :reflection, to: :proxy_association, private: true
+ delegate :klass, :foreign_key, :active_record, :active_record_primary_key,
+ to: :reflection, private: true
+
+ # This only works for HasMany and HasOne.
+ def lateral_relation
+ original_relation
+ .unscope(where: foreign_key) # unscoping the where condition generated for the placeholder_record.
+ .where(klass.arel_table[foreign_key].eq(active_record.arel_table[active_record_primary_key]))
+ end
+
+ def from
+ grouping_arel_node.as("#{active_record.arel_table.name}(#{active_record.primary_key})")
+ end
+
+ def grouping_arel_node
+ Arel::Nodes::Grouping.new(id_list_arel_node)
+ end
+
+ def id_list_arel_node
+ parent_ids.map { |id| [id] }
+ .then { |ids| Arel::Nodes::ValuesList.new(ids) }
+ end
+
+ def parent_ids
+ parents.pluck(active_record.primary_key)
+ end
+
+ def original_includes
+ original_relation.includes_values
+ end
+
+ def original_preload
+ original_relation.preload_values
+ end
+
+ def original_eager_load
+ original_relation.eager_load_values
+ end
+ end
+ end
+ end
+ end
+end
+# rubocop:enable CodeReuse/ActiveRecord
diff --git a/lib/gitlab/graphql/pagination/connections.rb b/lib/gitlab/graphql/pagination/connections.rb
index 965c01dd02f..df1231b005f 100644
--- a/lib/gitlab/graphql/pagination/connections.rb
+++ b/lib/gitlab/graphql/pagination/connections.rb
@@ -14,6 +14,10 @@ module Gitlab
Gitlab::Graphql::Pagination::Keyset::Connection)
schema.connections.add(
+ Gitlab::Graphql::Loaders::LazyRelationLoader::RelationProxy,
+ Gitlab::Graphql::Pagination::Keyset::Connection)
+
+ schema.connections.add(
Gitlab::Graphql::ExternallyPaginatedArray,
Gitlab::Graphql::Pagination::ExternallyPaginatedArrayConnection)
diff --git a/lib/gitlab/graphql/project/dast_profile_connection_extension.rb b/lib/gitlab/graphql/project/dast_profile_connection_extension.rb
index 45f90de2f17..1c21d286187 100644
--- a/lib/gitlab/graphql/project/dast_profile_connection_extension.rb
+++ b/lib/gitlab/graphql/project/dast_profile_connection_extension.rb
@@ -12,9 +12,12 @@ module Gitlab
def preload_authorizations(dast_profiles)
return unless dast_profiles
- projects = dast_profiles.map(&:project)
- users = dast_profiles.filter_map { |dast_profile| dast_profile.dast_profile_schedule&.owner }
- Preloaders::UsersMaxAccessLevelInProjectsPreloader.new(projects: projects, users: users).execute
+ project_users = dast_profiles.group_by(&:project).transform_values do |project_profiles|
+ project_profiles
+ .filter_map { |profile| profile.dast_profile_schedule&.owner }
+ .uniq
+ end
+ Preloaders::UsersMaxAccessLevelByProjectPreloader.new(project_users: project_users).execute
end
end
end
diff --git a/lib/gitlab/graphql/subscriptions/action_cable_with_load_balancing.rb b/lib/gitlab/graphql/subscriptions/action_cable_with_load_balancing.rb
new file mode 100644
index 00000000000..851750163af
--- /dev/null
+++ b/lib/gitlab/graphql/subscriptions/action_cable_with_load_balancing.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ module Subscriptions
+ class ActionCableWithLoadBalancing < ::GraphQL::Subscriptions::ActionCableSubscriptions
+ extend ::Gitlab::Utils::Override
+ include Gitlab::Database::LoadBalancing::WalTrackingSender
+ include Gitlab::Database::LoadBalancing::WalTrackingReceiver
+
+ KEY_PAYLOAD = 'gql_payload'
+ KEY_WAL_LOCATIONS = 'wal_locations'
+
+ override :execute_all
+ def execute_all(event, object)
+ super(event, {
+ KEY_WAL_LOCATIONS => current_wal_locations,
+ KEY_PAYLOAD => object
+ })
+ end
+
+ # We fall back to the primary in case no replica is sufficiently caught up.
+ override :execute_update
+ def execute_update(subscription_id, event, object)
+ # Make sure we do not accidentally try to unwrap messages that are not wrapped.
+ # This could in theory happen if workers roll over where some send wrapped payload
+ # and others expect the original payload.
+ return super(subscription_id, event, object) unless wrapped_payload?(object)
+
+ wal_locations = object[KEY_WAL_LOCATIONS]
+ ::Gitlab::Database::LoadBalancing::Session.current.use_primary! if use_primary?(wal_locations)
+
+ super(subscription_id, event, object[KEY_PAYLOAD])
+ end
+
+ private
+
+ def wrapped_payload?(object)
+ object.try(:key?, KEY_PAYLOAD)
+ end
+
+ def use_primary?(wal_locations)
+ wal_locations.blank? || !databases_in_sync?(wal_locations)
+ end
+
+ # We stringify keys since otherwise the graphql-ruby serializer will inject additional metadata
+ # to keep track of which keys used to be symbols.
+ def current_wal_locations
+ wal_locations_by_db_name&.stringify_keys
+ end
+ end
+ end
+ end
+end