From 2ee76c86ec46cd9fafef89d97a63f18349b5ef3d Mon Sep 17 00:00:00 2001 From: Surabhi Suman Date: Fri, 5 Sep 2025 00:33:36 +0530 Subject: [PATCH 1/6] chore: Adds sidekiq workers to cascade ai settings This adds sidekiq workers to cascade application_settings and namespace_settings when they are updated via application code EE: true Changelog: added --- .../rspec/redundant_metatag_type.yml | 1 - config/sidekiq_queues.yml | 4 +++ .../controllers/concerns/ee/groups/params.rb | 2 +- ...ice.rb => cascade_duo_settings_service.rb} | 18 ++++++---- .../ee/application_settings/update_service.rb | 17 +++++----- ee/app/services/ee/groups/update_service.rb | 12 +++++-- ee/app/workers/all_queues.yml | 20 +++++++++++ .../cascade_duo_features_enabled_worker.rb | 3 +- .../app_config/cascade_duo_settings_worker.rb | 23 +++++++++++++ .../cascade_duo_features_enabled_worker.rb | 7 ++-- .../namespaces/cascade_duo_settings_worker.rb | 24 +++++++++++++ ...b => cascade_duo_settings_service_spec.rb} | 18 +++++----- .../update_service_spec.rb | 34 +++++++++++++------ .../services/groups/update_service_spec.rb | 20 +++++++---- ...ascade_duo_features_enabled_worker_spec.rb | 3 +- .../cascade_duo_settings_worker_spec.rb | 19 +++++++++++ ...ascade_duo_features_enabled_worker_spec.rb | 3 +- .../cascade_duo_settings_worker_spec.rb | 20 +++++++++++ 18 files changed, 198 insertions(+), 50 deletions(-) rename ee/app/services/ai/{cascade_duo_features_enabled_service.rb => cascade_duo_settings_service.rb} (64%) create mode 100644 ee/app/workers/app_config/cascade_duo_settings_worker.rb create mode 100644 ee/app/workers/namespaces/cascade_duo_settings_worker.rb rename ee/spec/services/ai/{cascade_duo_features_enabled_service_spec.rb => cascade_duo_settings_service_spec.rb} (88%) create mode 100644 ee/spec/workers/app_config/cascade_duo_settings_worker_spec.rb create mode 100644 ee/spec/workers/namespaces/cascade_duo_settings_worker_spec.rb diff --git a/.rubocop_todo/rspec/redundant_metatag_type.yml b/.rubocop_todo/rspec/redundant_metatag_type.yml index f95eb41c4791f4..63232effb1b48c 100644 --- a/.rubocop_todo/rspec/redundant_metatag_type.yml +++ b/.rubocop_todo/rspec/redundant_metatag_type.yml @@ -216,7 +216,6 @@ RSpec/RedundantMetatagType: - 'ee/spec/requests/users/targeted_message_dismissals_controller_spec.rb' - 'ee/spec/routing/project_routing_spec.rb' - 'ee/spec/routing/webhook_routes_spec.rb' - - 'ee/spec/services/ai/cascade_duo_features_enabled_service_spec.rb' - 'ee/spec/services/ai/duo_workflows/create_event_service_spec.rb' - 'ee/spec/services/ai/duo_workflows/enablement_check_service_spec.rb' - 'ee/spec/services/ai/duo_workflows/onboarding_service_spec.rb' diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 651fff252d4f2d..f390b5b7d752cc 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -67,6 +67,8 @@ - 1 - - app_config_cascade_duo_features_enabled - 1 +- - app_config_cascade_duo_settings + - 1 - - app_sec_container_scanning_scan_image - 1 - - app_sec_dast_scanner_profiles_builds_consistency @@ -685,6 +687,8 @@ - 1 - - namespaces_cascade_duo_features_enabled - 1 +- - namespaces_cascade_duo_settings + - 1 - - namespaces_cascade_web_based_commit_signing_enabled - 1 - - namespaces_free_user_cap_group_over_limit_notification diff --git a/ee/app/controllers/concerns/ee/groups/params.rb b/ee/app/controllers/concerns/ee/groups/params.rb index 20c4e6c521e509..694d5a7b86df51 100644 --- a/ee/app/controllers/concerns/ee/groups/params.rb +++ b/ee/app/controllers/concerns/ee/groups/params.rb @@ -69,7 +69,7 @@ def group_params_ee if licensed_ai_features_available? params_ee.push(%i[duo_features_enabled duo_core_features_enabled lock_duo_features_enabled - duo_availability prompt_cache_enabled duo_remote_flows_availability]) + duo_availability prompt_cache_enabled duo_remote_flows_enabled lock_duo_remote_flows_enabled]) end params_ee << :disable_personal_access_tokens if current_group&.disable_personal_access_tokens_available? diff --git a/ee/app/services/ai/cascade_duo_features_enabled_service.rb b/ee/app/services/ai/cascade_duo_settings_service.rb similarity index 64% rename from ee/app/services/ai/cascade_duo_features_enabled_service.rb rename to ee/app/services/ai/cascade_duo_settings_service.rb index c763fbfe06dcd2..89993269bbb025 100644 --- a/ee/app/services/ai/cascade_duo_features_enabled_service.rb +++ b/ee/app/services/ai/cascade_duo_settings_service.rb @@ -1,23 +1,27 @@ # frozen_string_literal: true module Ai - class CascadeDuoFeaturesEnabledService - def initialize(duo_features_enabled) - @duo_features_enabled = duo_features_enabled + class CascadeDuoSettingsService + def initialize(setting_attributes) + @setting_attributes = setting_attributes.stringify_keys end def cascade_for_group(group) + return if @setting_attributes.empty? + update_subgroups(group) update_projects(group) end def cascade_for_instance + return if @setting_attributes.empty? + ProjectSetting.each_batch(of: 25000) do |batch| - batch.update_all(duo_features_enabled: @duo_features_enabled) + batch.update_all(@setting_attributes) end ::NamespaceSetting.each_batch(of: 25000) do |batch| - batch.update_all(duo_features_enabled: @duo_features_enabled) + batch.update_all(@setting_attributes) end end @@ -27,7 +31,7 @@ def update_subgroups(group) group.self_and_descendants.each_batch do |batch| namespace_ids = batch.pluck_primary_key ::NamespaceSetting.for_namespaces(namespace_ids) - .update_all(duo_features_enabled: @duo_features_enabled) + .update_all(@setting_attributes) end end @@ -35,7 +39,7 @@ def update_projects(group) group.all_projects.each_batch do |batch| project_ids_to_update = batch.pluck_primary_key ProjectSetting.for_projects(project_ids_to_update) - .update_all(duo_features_enabled: @duo_features_enabled) + .update_all(@setting_attributes) end end end diff --git a/ee/app/services/ee/application_settings/update_service.rb b/ee/app/services/ee/application_settings/update_service.rb index a989edc4d8427b..1962781af9852f 100644 --- a/ee/app/services/ee/application_settings/update_service.rb +++ b/ee/app/services/ee/application_settings/update_service.rb @@ -33,7 +33,7 @@ def execute update_elasticsearch_containers(ElasticsearchIndexedProject, elasticsearch_project_ids) update_elasticsearch_index_settings(number_of_replicas: elasticsearch_replicas, number_of_shards: elasticsearch_shards) - cascade_duo_features_settings if duo_features_changed? + cascade_duo_features_settings # There are cases when current user is passed as nil like in elastic.rake # we should not log audit events in such cases @@ -94,14 +94,15 @@ def filter_auto_duo_code_review_param params.delete(:auto_duo_code_review_enabled) end - def duo_features_changed? - application_setting.previous_changes.include?(:duo_features_enabled) - end - def cascade_duo_features_settings - duo_features_enabled = application_setting.duo_features_enabled - - ::AppConfig::CascadeDuoFeaturesEnabledWorker.perform_async(duo_features_enabled) + previous_changes = application_setting.previous_changes + cascading_ai_settings = [:duo_features_enabled, :duo_remote_flows_enabled] + + # Collect all changed AI settings and their values + changed_ai_settings = cascading_ai_settings.filter_map do |setting| + [setting, application_setting.attributes[setting.to_s]] if previous_changes.include?(setting) + end.to_h + ::AppConfig::CascadeDuoSettingsWorker.perform_async(changed_ai_settings) end def should_auto_approve_blocked_users? diff --git a/ee/app/services/ee/groups/update_service.rb b/ee/app/services/ee/groups/update_service.rb index 971935431ed630..1a74059210a588 100644 --- a/ee/app/services/ee/groups/update_service.rb +++ b/ee/app/services/ee/groups/update_service.rb @@ -166,8 +166,16 @@ def log_audit_events def update_cascading_settings previous_changes = group.namespace_settings.previous_changes - if previous_changes.include?(:duo_features_enabled) - ::Namespaces::CascadeDuoFeaturesEnabledWorker.perform_async(group.id) + cascading_ai_settings = [:duo_features_enabled, :duo_remote_flows_enabled] + # Collect all changed AI settings and their values + changed_ai_settings = cascading_ai_settings.filter_map do |setting| + if previous_changes.include?(setting) + [setting, group.namespace_settings.attributes[setting.to_s]] + end + end.to_h + + if changed_ai_settings.any? + ::Namespaces::CascadeDuoSettingsWorker.perform_async(group.id, changed_ai_settings) end if previous_changes.include?(:web_based_commit_signing_enabled) diff --git a/ee/app/workers/all_queues.yml b/ee/app/workers/all_queues.yml index cd36929a578dfa..5e52ecae608c52 100644 --- a/ee/app/workers/all_queues.yml +++ b/ee/app/workers/all_queues.yml @@ -1543,6 +1543,16 @@ :idempotent: true :tags: [] :queue_namespace: +- :name: app_config_cascade_duo_settings + :worker_name: AppConfig::CascadeDuoSettingsWorker + :feature_category: :ai_abstraction_layer + :has_external_dependencies: false + :urgency: :low + :resource_boundary: :memory + :weight: 1 + :idempotent: true + :tags: [] + :queue_namespace: - :name: app_sec_container_scanning_scan_image :worker_name: AppSec::ContainerScanning::ScanImageWorker :feature_category: :software_composition_analysis @@ -2863,6 +2873,16 @@ :idempotent: true :tags: [] :queue_namespace: +- :name: namespaces_cascade_duo_settings + :worker_name: Namespaces::CascadeDuoSettingsWorker + :feature_category: :ai_abstraction_layer + :has_external_dependencies: false + :urgency: :low + :resource_boundary: :memory + :weight: 1 + :idempotent: true + :tags: [] + :queue_namespace: - :name: namespaces_cascade_web_based_commit_signing_enabled :worker_name: Namespaces::CascadeWebBasedCommitSigningEnabledWorker :feature_category: :source_code_management diff --git a/ee/app/workers/app_config/cascade_duo_features_enabled_worker.rb b/ee/app/workers/app_config/cascade_duo_features_enabled_worker.rb index 0eb66edd2264a9..99a8688068c5e6 100644 --- a/ee/app/workers/app_config/cascade_duo_features_enabled_worker.rb +++ b/ee/app/workers/app_config/cascade_duo_features_enabled_worker.rb @@ -15,7 +15,8 @@ class CascadeDuoFeaturesEnabledWorker worker_resource_boundary :memory def perform(duo_features_enabled) - ::Ai::CascadeDuoFeaturesEnabledService.new(duo_features_enabled).cascade_for_instance + setting_attributes = { duo_features_enabled: duo_features_enabled } + ::Ai::CascadeDuoSettingsService.new(setting_attributes).cascade_for_instance end end end diff --git a/ee/app/workers/app_config/cascade_duo_settings_worker.rb b/ee/app/workers/app_config/cascade_duo_settings_worker.rb new file mode 100644 index 00000000000000..e90a68811a501b --- /dev/null +++ b/ee/app/workers/app_config/cascade_duo_settings_worker.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module AppConfig + class CascadeDuoSettingsWorker + include ApplicationWorker + extend ActiveSupport::Concern + + feature_category :ai_abstraction_layer + + idempotent! + deduplicate :until_executed, if_deduplicated: :reschedule_once + urgency :low + data_consistency :delayed + loggable_arguments 0 + worker_resource_boundary :memory + defer_on_database_health_signal :gitlab_main, + [:application_settings, :namespace_settings, :project_settings], 1.minute + + def perform(setting_attributes) + ::Ai::CascadeDuoSettingsService.new(setting_attributes).cascade_for_instance + end + end +end diff --git a/ee/app/workers/namespaces/cascade_duo_features_enabled_worker.rb b/ee/app/workers/namespaces/cascade_duo_features_enabled_worker.rb index 5b562b1cd7c2fc..ca4b57d12e2a02 100644 --- a/ee/app/workers/namespaces/cascade_duo_features_enabled_worker.rb +++ b/ee/app/workers/namespaces/cascade_duo_features_enabled_worker.rb @@ -17,9 +17,12 @@ class CascadeDuoFeaturesEnabledWorker def perform(*args) group_id = args[0] group = Group.find(group_id) - duo_features_enabled = group.namespace_settings.duo_features_enabled + setting_attribute_name = :duo_features_enabled + setting_attribute_value = group.namespace_settings.attributes[setting_attribute_name.to_s] - ::Ai::CascadeDuoFeaturesEnabledService.new(duo_features_enabled).cascade_for_group(group) + ::Ai::CascadeDuoSettingsService.new({ + setting_attribute_name => setting_attribute_value + }).cascade_for_group(group) end end end diff --git a/ee/app/workers/namespaces/cascade_duo_settings_worker.rb b/ee/app/workers/namespaces/cascade_duo_settings_worker.rb new file mode 100644 index 00000000000000..f3fe15dfadb478 --- /dev/null +++ b/ee/app/workers/namespaces/cascade_duo_settings_worker.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Namespaces + class CascadeDuoSettingsWorker + include ApplicationWorker + extend ActiveSupport::Concern + + feature_category :ai_abstraction_layer + + idempotent! + deduplicate :until_executed, if_deduplicated: :reschedule_once + urgency :low + data_consistency :delayed + loggable_arguments 0 + worker_resource_boundary :memory + defer_on_database_health_signal :gitlab_main, [:namespace_settings, :project_settings], 1.minute + + def perform(group_id, setting_attributes) + group = Group.find_by_id(group_id) + + ::Ai::CascadeDuoSettingsService.new(setting_attributes).cascade_for_group(group) + end + end +end diff --git a/ee/spec/services/ai/cascade_duo_features_enabled_service_spec.rb b/ee/spec/services/ai/cascade_duo_settings_service_spec.rb similarity index 88% rename from ee/spec/services/ai/cascade_duo_features_enabled_service_spec.rb rename to ee/spec/services/ai/cascade_duo_settings_service_spec.rb index 7c05f09bd1e312..3856c25fe73126 100644 --- a/ee/spec/services/ai/cascade_duo_features_enabled_service_spec.rb +++ b/ee/spec/services/ai/cascade_duo_settings_service_spec.rb @@ -2,8 +2,8 @@ require 'spec_helper' -RSpec.describe Ai::CascadeDuoFeaturesEnabledService, type: :service, feature_category: :ai_abstraction_layer do - let(:duo_features_enabled) { true } +RSpec.describe Ai::CascadeDuoSettingsService, feature_category: :ai_abstraction_layer do + let(:setting_attributes) { { duo_features_enabled: true } } let_it_be_with_reload(:group) { create(:group) } let_it_be_with_reload(:subgroup) { create(:group, parent: group) } let_it_be_with_reload(:project) { create(:project, :repository, group: group) } @@ -11,7 +11,7 @@ let_it_be_with_reload(:subgroup2) { create(:group, parent: group2) } let_it_be_with_reload(:project2) { create(:project, :repository, group: group2) } - subject(:service) { described_class.new(duo_features_enabled) } + subject(:service) { described_class.new(setting_attributes) } describe '#cascade_for_group' do context 'when duo_features_enabled is true' do @@ -36,9 +36,9 @@ end context 'when duo_features_enabled is false' do - let(:duo_features_enabled) { false } + let(:setting_attributes) { { duo_features_enabled: false } } - subject(:service) { described_class.new(duo_features_enabled) } + subject(:service) { described_class.new(setting_attributes) } it 'updates subgroups and projects for given groups to false' do # Initialize with duo_features_enabled: true @@ -62,9 +62,9 @@ describe '#cascade_for_instance' do context 'when duo_features_enabled is true' do - let(:duo_features_enabled) { true } + let(:setting_attributes) { { duo_features_enabled: true } } - subject(:service) { described_class.new(duo_features_enabled) } + subject(:service) { described_class.new(setting_attributes) } it 'updates all root groups, subgroups, and projects' do # Initialize with duo_features_enabled: false @@ -86,9 +86,9 @@ end context 'when duo_features_enabled is false' do - let(:duo_features_enabled) { false } + let(:setting_attributes) { { duo_features_enabled: false } } - subject(:service) { described_class.new(duo_features_enabled) } + subject(:service) { described_class.new(setting_attributes) } it 'updates all root groups, subgroups, and projects' do # Initialize with duo_features_enabled: true diff --git a/ee/spec/services/application_settings/update_service_spec.rb b/ee/spec/services/application_settings/update_service_spec.rb index 6af5dbc1f4ce99..27bdb4e0a9eb60 100644 --- a/ee/spec/services/application_settings/update_service_spec.rb +++ b/ee/spec/services/application_settings/update_service_spec.rb @@ -331,30 +331,42 @@ end end - context 'when updating duo_features_enabled' do - let(:params) { { duo_features_enabled: true } } + shared_examples 'when updating duo settings' do |setting_key, setting_val| + let(:params) { { setting_key => setting_val } } let(:service) { described_class.new(setting, user, params) } - before do - setting.update!(duo_features_enabled: false) - end - - it 'triggers the CascadeDuoFeaturesEnabledWorker with correct arguments' do - expect(AppConfig::CascadeDuoFeaturesEnabledWorker).to receive(:perform_async) - .with(params[:duo_features_enabled]) + it 'triggers the CascadeDuoSettingsWorker with correct arguments' do + expect(AppConfig::CascadeDuoSettingsWorker).to receive(:perform_async) + .with(params) service.execute end - it 'updates the duo_features_enabled setting' do + it 'updates the setting' do result = service.execute expect(result).to be_truthy - expect(setting.reload.duo_features_enabled).to be(true) + expect(setting.reload.send(setting_key)).to be(setting_val) end end + context 'when updating duo_features_enabled' do + before do + setting.update!(duo_features_enabled: false) + end + + it_behaves_like 'when updating duo settings', :duo_features_enabled, true + end + + context 'when updating duo_remote_flows_enabled' do + before do + setting.update!(duo_remote_flows_enabled: true) + end + + it_behaves_like 'when updating duo settings', :duo_remote_flows_enabled, false + end + context 'when updating auto_duo_code_review_enabled' do let(:params) { { auto_duo_code_review_enabled: true } } let(:service) { described_class.new(setting, user, params) } diff --git a/ee/spec/services/groups/update_service_spec.rb b/ee/spec/services/groups/update_service_spec.rb index 646fd27156b4aa..0e5e4e680f32aa 100644 --- a/ee/spec/services/groups/update_service_spec.rb +++ b/ee/spec/services/groups/update_service_spec.rb @@ -580,10 +580,10 @@ def update_file_template_project_id(id) end end - context 'when updating duo_features_enabled' do + shared_examples 'when updating duo settings' do |setting_key, setting_val| let_it_be_with_reload(:user) { create(:user) } let_it_be_with_reload(:group) { create(:group, :public) } - let(:params) { { duo_features_enabled: false } } + let(:params) { { setting_key => setting_val } } context 'as a normal user' do before_all do @@ -592,7 +592,7 @@ def update_file_template_project_id(id) it 'does not change settings' do expect { update_group(group, user, params) } - .not_to(change { group.namespace_settings.duo_features_enabled }) + .not_to(change { group.namespace_settings.public_send(setting_key) }) end end @@ -603,23 +603,31 @@ def update_file_template_project_id(id) it 'changes settings' do expect { update_group(group, user, params) } - .to(change { group.namespace_settings.duo_features_enabled }.to(false)) + .to(change { group.namespace_settings.public_send(setting_key) }.to(setting_val)) end context 'group has subgroups' do let(:subgroup) { create(:group, parent: group) } it 'runs worker that sets subgroup duo_features_enabled to match group', :sidekiq_inline do - subgroup.namespace_settings.update!(duo_features_enabled: true) + subgroup.namespace_settings.update!(setting_key => setting_val) update_group(group, user, params) - expect(subgroup.reload.namespace_settings.reload.duo_features_enabled).to eq false + expect(subgroup.reload.namespace_settings.reload.send(setting_key)).to eq(setting_val) end end end end + context 'when updating duo_features_enabled' do + it_behaves_like 'when updating duo settings', :duo_features_enabled, false + end + + context 'when updating duo_remote_flows_enabled' do + it_behaves_like 'when updating duo settings', :duo_remote_flows_enabled, false + end + context 'when updating lock_duo_features_enabled' do let_it_be_with_reload(:user) { create(:user) } let_it_be_with_reload(:group) { create(:group, :public) } diff --git a/ee/spec/workers/app_config/cascade_duo_features_enabled_worker_spec.rb b/ee/spec/workers/app_config/cascade_duo_features_enabled_worker_spec.rb index 8ba0edf7da8bfa..1b9f390c531336 100644 --- a/ee/spec/workers/app_config/cascade_duo_features_enabled_worker_spec.rb +++ b/ee/spec/workers/app_config/cascade_duo_features_enabled_worker_spec.rb @@ -9,7 +9,8 @@ describe '#perform' do it 'calls cascade_for_instance on service with the correct argument' do - expect_next_instance_of(Ai::CascadeDuoFeaturesEnabledService, duo_features_enabled) do |service| + expect_next_instance_of(Ai::CascadeDuoSettingsService, + { duo_features_enabled: duo_features_enabled }) do |service| expect(service).to receive(:cascade_for_instance) end diff --git a/ee/spec/workers/app_config/cascade_duo_settings_worker_spec.rb b/ee/spec/workers/app_config/cascade_duo_settings_worker_spec.rb new file mode 100644 index 00000000000000..037ba4cae919eb --- /dev/null +++ b/ee/spec/workers/app_config/cascade_duo_settings_worker_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe AppConfig::CascadeDuoSettingsWorker, feature_category: :ai_abstraction_layer do + let(:setting_attributes) { { duo_features_enabled: true } } + + subject(:worker) { described_class.new } + + describe '#perform' do + it 'calls cascade_for_instance on service with the correct argument' do + expect_next_instance_of(Ai::CascadeDuoSettingsService, setting_attributes) do |service| + expect(service).to receive(:cascade_for_instance) + end + + worker.perform(setting_attributes) + end + end +end diff --git a/ee/spec/workers/namespaces/cascade_duo_features_enabled_worker_spec.rb b/ee/spec/workers/namespaces/cascade_duo_features_enabled_worker_spec.rb index 27be525debc76e..705983b052676f 100644 --- a/ee/spec/workers/namespaces/cascade_duo_features_enabled_worker_spec.rb +++ b/ee/spec/workers/namespaces/cascade_duo_features_enabled_worker_spec.rb @@ -10,7 +10,8 @@ describe '#perform' do it 'calls cascade_for_group on service with the correct argument' do - expect_next_instance_of(Ai::CascadeDuoFeaturesEnabledService, duo_features_enabled) do |service| + expect_next_instance_of(Ai::CascadeDuoSettingsService, + { duo_features_enabled: duo_features_enabled }) do |service| expect(service).to receive(:cascade_for_group).with(group) end diff --git a/ee/spec/workers/namespaces/cascade_duo_settings_worker_spec.rb b/ee/spec/workers/namespaces/cascade_duo_settings_worker_spec.rb new file mode 100644 index 00000000000000..b240022ea06a70 --- /dev/null +++ b/ee/spec/workers/namespaces/cascade_duo_settings_worker_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Namespaces::CascadeDuoSettingsWorker, feature_category: :ai_abstraction_layer do + let(:group) { create(:group) } + let(:setting_attributes) { { duo_features_enabled: true } } + + subject(:worker) { described_class.new } + + describe '#perform' do + it 'calls cascade_for_group on service with the correct argument' do + expect_next_instance_of(Ai::CascadeDuoSettingsService, setting_attributes) do |service| + expect(service).to receive(:cascade_for_group).with(group) + end + + worker.perform(group.id, setting_attributes) + end + end +end -- GitLab From f27b814c86f23b8102a51c3e1a47c4eeda0b8de1 Mon Sep 17 00:00:00 2001 From: Surabhi Suman Date: Thu, 11 Sep 2025 20:44:12 +0530 Subject: [PATCH 2/6] chore: Adds validations for group setting --- ee/app/controllers/concerns/ee/groups/params.rb | 3 ++- .../ee/namespace_settings/assign_attributes_service.rb | 8 ++++++++ .../namespaces/cascade_duo_features_enabled_worker.rb | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/ee/app/controllers/concerns/ee/groups/params.rb b/ee/app/controllers/concerns/ee/groups/params.rb index 694d5a7b86df51..2e9379d59dc237 100644 --- a/ee/app/controllers/concerns/ee/groups/params.rb +++ b/ee/app/controllers/concerns/ee/groups/params.rb @@ -69,7 +69,8 @@ def group_params_ee if licensed_ai_features_available? params_ee.push(%i[duo_features_enabled duo_core_features_enabled lock_duo_features_enabled - duo_availability prompt_cache_enabled duo_remote_flows_enabled lock_duo_remote_flows_enabled]) + duo_availability duo_remote_flows_availability prompt_cache_enabled duo_remote_flows_enabled + lock_duo_remote_flows_enabled]) end params_ee << :disable_personal_access_tokens if current_group&.disable_personal_access_tokens_available? diff --git a/ee/app/services/ee/namespace_settings/assign_attributes_service.rb b/ee/app/services/ee/namespace_settings/assign_attributes_service.rb index 9c09efce1d6a65..f59306a955e92d 100644 --- a/ee/app/services/ee/namespace_settings/assign_attributes_service.rb +++ b/ee/app/services/ee/namespace_settings/assign_attributes_service.rb @@ -31,6 +31,14 @@ def execute param_key: :lock_duo_features_enabled, user_policy: :admin_group ) + validate_settings_param_for_admin( + param_key: :duo_remote_flows_enabled, + user_policy: :admin_group + ) + validate_settings_param_for_admin( + param_key: :lock_duo_remote_flows_enabled, + user_policy: :admin_group + ) validate_settings_param_for_root_group( param_key: :disable_invite_members, user_policy: :owner_access diff --git a/ee/app/workers/namespaces/cascade_duo_features_enabled_worker.rb b/ee/app/workers/namespaces/cascade_duo_features_enabled_worker.rb index ca4b57d12e2a02..8e3124ae2639be 100644 --- a/ee/app/workers/namespaces/cascade_duo_features_enabled_worker.rb +++ b/ee/app/workers/namespaces/cascade_duo_features_enabled_worker.rb @@ -18,7 +18,7 @@ def perform(*args) group_id = args[0] group = Group.find(group_id) setting_attribute_name = :duo_features_enabled - setting_attribute_value = group.namespace_settings.attributes[setting_attribute_name.to_s] + setting_attribute_value = group.namespace_settings.duo_features_enabled ::Ai::CascadeDuoSettingsService.new({ setting_attribute_name => setting_attribute_value -- GitLab From db4509d6d7cc6c064dc37468514e6eb157aecae5 Mon Sep 17 00:00:00 2001 From: Surabhi Suman Date: Wed, 1 Oct 2025 11:07:26 +0530 Subject: [PATCH 3/6] fix: Fixes failing spec --- ee/app/services/ee/application_settings/update_service.rb | 2 ++ ee/spec/services/ai/amazon_q/update_service_spec.rb | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ee/app/services/ee/application_settings/update_service.rb b/ee/app/services/ee/application_settings/update_service.rb index 1962781af9852f..7568f0ed99b610 100644 --- a/ee/app/services/ee/application_settings/update_service.rb +++ b/ee/app/services/ee/application_settings/update_service.rb @@ -102,6 +102,8 @@ def cascade_duo_features_settings changed_ai_settings = cascading_ai_settings.filter_map do |setting| [setting, application_setting.attributes[setting.to_s]] if previous_changes.include?(setting) end.to_h + return if changed_ai_settings.empty? + ::AppConfig::CascadeDuoSettingsWorker.perform_async(changed_ai_settings) end diff --git a/ee/spec/services/ai/amazon_q/update_service_spec.rb b/ee/spec/services/ai/amazon_q/update_service_spec.rb index 429ca3e87f2c41..446bcb2136e94c 100644 --- a/ee/spec/services/ai/amazon_q/update_service_spec.rb +++ b/ee/spec/services/ai/amazon_q/update_service_spec.rb @@ -212,7 +212,7 @@ end it 'triggers cascading worker to update all groups and projects', :sidekiq_inline do - expect(AppConfig::CascadeDuoFeaturesEnabledWorker).to receive(:perform_async).with(true) + expect(AppConfig::CascadeDuoSettingsWorker).to receive(:perform_async).with({ duo_features_enabled: true }) instance.execute end -- GitLab From e811f9f6106226616229ef7b62fc186a69a8bac0 Mon Sep 17 00:00:00 2001 From: Surabhi Suman Date: Tue, 7 Oct 2025 11:39:42 +0530 Subject: [PATCH 4/6] chore: Use fixed settings list for cascading --- ee/app/services/ai/cascade_duo_settings_service.rb | 10 ++++++++++ .../services/ai/cascade_duo_settings_service_spec.rb | 8 ++++++++ 2 files changed, 18 insertions(+) diff --git a/ee/app/services/ai/cascade_duo_settings_service.rb b/ee/app/services/ai/cascade_duo_settings_service.rb index 89993269bbb025..6644c869096459 100644 --- a/ee/app/services/ai/cascade_duo_settings_service.rb +++ b/ee/app/services/ai/cascade_duo_settings_service.rb @@ -2,8 +2,12 @@ module Ai class CascadeDuoSettingsService + DUO_SETTINGS = %w[duo_features_enabled duo_remote_flows_enabled].freeze + def initialize(setting_attributes) @setting_attributes = setting_attributes.stringify_keys + + check_setting_attributes! end def cascade_for_group(group) @@ -27,6 +31,12 @@ def cascade_for_instance private + def check_setting_attributes! + return if @setting_attributes.keys.all? { |key| DUO_SETTINGS.include?(key) } + + raise ArgumentError, "Invalid key used" + end + def update_subgroups(group) group.self_and_descendants.each_batch do |batch| namespace_ids = batch.pluck_primary_key diff --git a/ee/spec/services/ai/cascade_duo_settings_service_spec.rb b/ee/spec/services/ai/cascade_duo_settings_service_spec.rb index 3856c25fe73126..e683c223caabf5 100644 --- a/ee/spec/services/ai/cascade_duo_settings_service_spec.rb +++ b/ee/spec/services/ai/cascade_duo_settings_service_spec.rb @@ -14,6 +14,14 @@ subject(:service) { described_class.new(setting_attributes) } describe '#cascade_for_group' do + context 'when updating invalid setting value' do + let(:setting_attributes) { { invalid_setting_key: 'invalid_setting_value' } } + + it 'raises ArgumentError' do + expect { service }.to raise_error(ArgumentError) + end + end + context 'when duo_features_enabled is true' do it 'updates subgroups and projects for given group to true' do # Initialize with duo_features_enabled: false -- GitLab From 8bca8a9ec4392d6a11bdf1690ba06650c0efce90 Mon Sep 17 00:00:00 2001 From: Surabhi Suman Date: Fri, 10 Oct 2025 13:55:30 +0530 Subject: [PATCH 5/6] chore: Use string keys for sidekiq job args --- ee/app/services/ai/cascade_duo_settings_service.rb | 3 ++- .../services/ai/cascade_duo_settings_service_spec.rb | 10 ++++++---- .../app_config/cascade_duo_settings_worker_spec.rb | 2 +- .../namespaces/cascade_duo_settings_worker_spec.rb | 2 +- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/ee/app/services/ai/cascade_duo_settings_service.rb b/ee/app/services/ai/cascade_duo_settings_service.rb index 6644c869096459..32008f18e5e866 100644 --- a/ee/app/services/ai/cascade_duo_settings_service.rb +++ b/ee/app/services/ai/cascade_duo_settings_service.rb @@ -34,7 +34,8 @@ def cascade_for_instance def check_setting_attributes! return if @setting_attributes.keys.all? { |key| DUO_SETTINGS.include?(key) } - raise ArgumentError, "Invalid key used" + raise ArgumentError, + "Invalid key in #{@setting_attributes}. Only the following Duo Settings can cascade: #{DUO_SETTINGS}" end def update_subgroups(group) diff --git a/ee/spec/services/ai/cascade_duo_settings_service_spec.rb b/ee/spec/services/ai/cascade_duo_settings_service_spec.rb index e683c223caabf5..40411ee216cfb3 100644 --- a/ee/spec/services/ai/cascade_duo_settings_service_spec.rb +++ b/ee/spec/services/ai/cascade_duo_settings_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Ai::CascadeDuoSettingsService, feature_category: :ai_abstraction_layer do - let(:setting_attributes) { { duo_features_enabled: true } } + let(:setting_attributes) { { 'duo_features_enabled' => true } } let_it_be_with_reload(:group) { create(:group) } let_it_be_with_reload(:subgroup) { create(:group, parent: group) } let_it_be_with_reload(:project) { create(:project, :repository, group: group) } @@ -15,7 +15,9 @@ describe '#cascade_for_group' do context 'when updating invalid setting value' do - let(:setting_attributes) { { invalid_setting_key: 'invalid_setting_value' } } + let(:setting_attributes) do + { 'invalid_setting_key' => 'invalid_setting_value', 'duo_remote_flows_enabled' => true } + end it 'raises ArgumentError' do expect { service }.to raise_error(ArgumentError) @@ -70,7 +72,7 @@ describe '#cascade_for_instance' do context 'when duo_features_enabled is true' do - let(:setting_attributes) { { duo_features_enabled: true } } + let(:setting_attributes) { { 'duo_features_enabled' => true } } subject(:service) { described_class.new(setting_attributes) } @@ -94,7 +96,7 @@ end context 'when duo_features_enabled is false' do - let(:setting_attributes) { { duo_features_enabled: false } } + let(:setting_attributes) { { 'duo_features_enabled' => false } } subject(:service) { described_class.new(setting_attributes) } diff --git a/ee/spec/workers/app_config/cascade_duo_settings_worker_spec.rb b/ee/spec/workers/app_config/cascade_duo_settings_worker_spec.rb index 037ba4cae919eb..2950a3914491bf 100644 --- a/ee/spec/workers/app_config/cascade_duo_settings_worker_spec.rb +++ b/ee/spec/workers/app_config/cascade_duo_settings_worker_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe AppConfig::CascadeDuoSettingsWorker, feature_category: :ai_abstraction_layer do - let(:setting_attributes) { { duo_features_enabled: true } } + let(:setting_attributes) { { "duo_features_enabled" => true } } subject(:worker) { described_class.new } diff --git a/ee/spec/workers/namespaces/cascade_duo_settings_worker_spec.rb b/ee/spec/workers/namespaces/cascade_duo_settings_worker_spec.rb index b240022ea06a70..7f20e531fd33bd 100644 --- a/ee/spec/workers/namespaces/cascade_duo_settings_worker_spec.rb +++ b/ee/spec/workers/namespaces/cascade_duo_settings_worker_spec.rb @@ -4,7 +4,7 @@ RSpec.describe Namespaces::CascadeDuoSettingsWorker, feature_category: :ai_abstraction_layer do let(:group) { create(:group) } - let(:setting_attributes) { { duo_features_enabled: true } } + let(:setting_attributes) { { "duo_features_enabled" => true } } subject(:worker) { described_class.new } -- GitLab From 12a21db06ccef24f25d7dd8564536dfc67f7008b Mon Sep 17 00:00:00 2001 From: Surabhi Suman Date: Fri, 10 Oct 2025 17:53:35 +0530 Subject: [PATCH 6/6] Update cascading settings docs --- doc/development/cascading_settings.md | 35 +++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/doc/development/cascading_settings.md b/doc/development/cascading_settings.md index 60caf06108ffbd..8c9e971a39fd01 100644 --- a/doc/development/cascading_settings.md +++ b/doc/development/cascading_settings.md @@ -130,7 +130,12 @@ An example of adding a cascading setting to a project is in [MR 149931](https:// ## Cascading setting values on write -The only cascading setting that actually cascades values at the database level in the new recommended way is `duo_features_enabled`. That setting cascades from groups to projects. [Issue 505335](https://gitlab.com/gitlab-org/gitlab/-/issues/505335) describes adding this cascading from the application level to groups as well. +The cascading settings that implement database-level value propagation using the current recommended approach are `duo_features_enabled` and `duo_remote_flows_enabled`. These settings follow a hierarchical cascade pattern: + +- **Group to Project cascading**: Implemented via `Namespaces::CascadeDuoSettingsWorker` +- **Application to Group and Project cascading**: Implemented via `AppConfig::CascadeDuoSettingsWorker` + +This architecture ensures consistent setting inheritance throughout the organizational hierarchy while maintaining optimal performance through asynchronous processing. ### Legacy cascading settings writes @@ -158,7 +163,33 @@ In addition to the confusing logic, this also creates a performance problem when ### Recommendation for cascading settings writes going forward -To provide a clearer logic chain and improve performance, you should be adding default values to newly-added cascading settings and doing a write on all child objects in the hierarchy when the setting value is updated. This requires kicking off a job so that the update happens asynchronously. An example of how to do this is in [MR 145876](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/145876). +To provide a clearer logic chain and improve performance, you should be adding default values to newly-added cascading settings and doing a write on all child objects in the hierarchy when the setting value is updated. This requires kicking off a job so that the update happens asynchronously. +The system currently employs two dedicated Sidekiq workers for this purpose: + +- `AppConfig::CascadeDuoSettingsWorker` - handles application-level setting propagation +- `Namespaces::CascadeDuoSettingsWorker` - manages namespace-level updates to child groups and projects + +This asynchronous approach ensures that setting modifications are efficiently distributed throughout the organizational hierarchy without impacting system performance. + +### Adding New Cascading AI Settings + +To implement a new cascading AI setting that propagates values through database writes, follow the procedures outlined below based on the setting scope: + +#### Namespace Setting Configuration + +When implementing namespace-level cascading settings: + +1. Integrate the setting into the `update_cascading_settings` method located in `ee/app/services/ee/groups/update_service.rb` +1. Register the setting within the allowed settings configuration in `ee/app/services/ai/cascade_duo_settings_service.rb` + +#### Application Setting Configuration + +When implementing application-level cascading settings: + +1. Incorporate the setting into the `cascade_duo_features_settings` method found in `ee/app/services/ee/application_settings/update_service.rb` +1. Register the setting within the allowed settings configuration in `ee/app/services/ai/cascade_duo_settings_service.rb` + +These configurations ensure proper validation and cascading behavior throughout the system hierarchy. Cascading settings that were added previously still have default `nil` values and read the ancestor hierarchy to find inherited settings values. But to minimize confusion we should update those to cascade on write. [Issue 483143](https://gitlab.com/gitlab-org/gitlab/-/issues/483143) describes this maintenance task. -- GitLab