From f4777ff9727bb6bb289cd87c09e1024850034971 Mon Sep 17 00:00:00 2001 From: Katherine Richards Date: Fri, 5 Sep 2025 12:08:37 -0600 Subject: [PATCH] Add seat assignment service for seat management This change adds an event, publisher, and subscriber to upsert seat assignments triggered by a callback on the Member model. --- config/sidekiq_queues.yml | 2 + ee/app/events/members/updated_event.rb | 18 ++++++++ ee/app/models/ee/member.rb | 12 +++++ .../seat_assignment_service.rb | 46 +++++++++++++++++++ ee/app/workers/all_queues.yml | 10 ++++ .../members/updated_worker.rb | 29 ++++++++++++ ee/lib/ee/gitlab/event_store.rb | 1 + 7 files changed, 118 insertions(+) create mode 100644 ee/app/events/members/updated_event.rb create mode 100644 ee/app/services/gitlab_subscriptions/seat_assignment_service.rb create mode 100644 ee/app/workers/gitlab_subscriptions/members/updated_worker.rb diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 84cbf015a16e96..6f04bb70a6ba2b 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -479,6 +479,8 @@ - 1 - - gitlab_subscriptions_members_record_last_activity - 1 +- - gitlab_subscriptions_members_updated + - 1 - - gitlab_subscriptions_refresh_seats - 1 - - gitlab_subscriptions_seat_assignments_group_links_create_or_update_seats diff --git a/ee/app/events/members/updated_event.rb b/ee/app/events/members/updated_event.rb new file mode 100644 index 00000000000000..608f3e1a13d7e0 --- /dev/null +++ b/ee/app/events/members/updated_event.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Members + class UpdatedEvent < ::Gitlab::EventStore::Event + def schema + { + 'type' => 'object', + 'required' => %w[source_id source_type user_id], + 'properties' => { + 'root_namespace_id' => { 'type' => 'integer' }, + 'source_id' => { 'type' => 'integer' }, + 'source_type' => { 'type' => 'string' }, + 'user_id' => { 'type' => %w[integer null] } + } + } + end + end +end diff --git a/ee/app/models/ee/member.rb b/ee/app/models/ee/member.rb index e5d1cd7523c9ad..523c18a94b8311 100644 --- a/ee/app/models/ee/member.rb +++ b/ee/app/models/ee/member.rb @@ -77,6 +77,8 @@ module Member end scope :order_access_level_desc, -> { order(access_level: :desc) } + + after_commit :publish_member_updated_event, on: [:create, :update], if: -> { user_id.present? } end class_methods do @@ -328,5 +330,15 @@ def after_accept_request update_user_group_member_roles end + + def publish_member_updated_event + ::Gitlab::EventStore.publish( + ::Members::UpdatedEvent.new(data: { + source_id: source.id, + source_type: source.class.name, + user_id: user_id + }) + ) + end end end diff --git a/ee/app/services/gitlab_subscriptions/seat_assignment_service.rb b/ee/app/services/gitlab_subscriptions/seat_assignment_service.rb new file mode 100644 index 00000000000000..3089cfc5dbc670 --- /dev/null +++ b/ee/app/services/gitlab_subscriptions/seat_assignment_service.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module GitlabSubscriptions + class SeatAssignmentService + def initialize(user, namespace) + @user = user + @namespace = namespace&.root_ancestor + end + + def execute + return ServiceResponse.error(message: 'Invalid params') unless user && namespace&.group_namespace? + return ServiceResponse.error(message: 'User is not a member') unless user_is_a_member? + + seat_assignment = find_seat_assignment || build_seat_assignment + + seat_type = GitlabSubscriptions::SeatTypeCalculator.execute(user, namespace) + seat_assignment.update(seat_type: seat_type) + + if seat_assignment.save + ServiceResponse.success(message: 'Seat assignment updated') + else + ServiceResponse.error(message: 'Unable to update seat assignment') + end + end + + private + + attr_reader :user, :namespace + + def find_seat_assignment + GitlabSubscriptions::SeatAssignment.find_by_namespace_and_user(namespace, user) + end + + def build_seat_assignment + GitlabSubscriptions::SeatAssignment.new( + namespace: namespace, + user: user, + organization_id: namespace.organization_id || Organizations::Organization::DEFAULT_ORGANIZATION_ID + ) + end + + def user_is_a_member? + ::Member.in_hierarchy(namespace).with_user(user).exists? + end + end +end diff --git a/ee/app/workers/all_queues.yml b/ee/app/workers/all_queues.yml index 4e687b14e2ccb3..152241fdeb1400 100644 --- a/ee/app/workers/all_queues.yml +++ b/ee/app/workers/all_queues.yml @@ -2443,6 +2443,16 @@ :idempotent: true :tags: [] :queue_namespace: +- :name: gitlab_subscriptions_members_updated + :worker_name: GitlabSubscriptions::Members::UpdatedWorker + :feature_category: :seat_cost_management + :has_external_dependencies: false + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] + :queue_namespace: - :name: gitlab_subscriptions_refresh_seats :worker_name: GitlabSubscriptions::RefreshSeatsWorker :feature_category: :seat_cost_management diff --git a/ee/app/workers/gitlab_subscriptions/members/updated_worker.rb b/ee/app/workers/gitlab_subscriptions/members/updated_worker.rb new file mode 100644 index 00000000000000..a1e759237eb167 --- /dev/null +++ b/ee/app/workers/gitlab_subscriptions/members/updated_worker.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module GitlabSubscriptions + module Members + class UpdatedWorker + include Gitlab::EventStore::Subscriber + + DEFER_ON_HEALTH_DELAY = 1.minute + + data_consistency :delayed + feature_category :seat_cost_management + urgency :low + + defer_on_database_health_signal :gitlab_main, [:users, :namespaces, :members], DEFER_ON_HEALTH_DELAY + + idempotent! + deduplicate :until_executing, including_scheduled: true + + def handle_event(event) + user = ::User.find_by_id(event.data[:user_id]) + namespace = ::Namespace.find_by_id(event.data[:root_namespace_id]) + + return unless user && namespace&.group_namespace? + + ::GitlabSubscriptions::SeatAssignmentService.new(user, namespace).execute + end + end + end +end diff --git a/ee/lib/ee/gitlab/event_store.rb b/ee/lib/ee/gitlab/event_store.rb index 6e7dd3fca0fc57..0938896f4b1d41 100644 --- a/ee/lib/ee/gitlab/event_store.rb +++ b/ee/lib/ee/gitlab/event_store.rb @@ -69,6 +69,7 @@ def configure!(store) if: ->(_) { ::Gitlab::CurrentSettings.enable_member_promotion_management? } + store.subscribe ::GitlabSubscriptions::Members::UpdatedWorker, to: ::Members::UpdatedEvent register_threat_insights_subscribers(store) register_security_policy_subscribers(store) -- GitLab