Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									2c030942fd
								
							
						
					
					
						commit
						ea7202cfe3
					
				| 
						 | 
				
			
			@ -1 +1 @@
 | 
			
		|||
79a0dfb018671957bbc1a02e21684c8cc2858160
 | 
			
		||||
bccabad35ea7b90a2bfae196cf583611a0484c51
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,13 +1,11 @@
 | 
			
		|||
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-visibility-settings'), html: { class: 'fieldset-form', id: 'visibility-settings' } do |f|
 | 
			
		||||
= gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-visibility-settings'), html: { class: 'fieldset-form', id: 'visibility-settings' } do |f|
 | 
			
		||||
  = form_errors(@application_setting)
 | 
			
		||||
 | 
			
		||||
  %fieldset
 | 
			
		||||
    = render 'shared/default_branch_protection', f: f, selected_level: @application_setting.default_branch_protection
 | 
			
		||||
    = render 'shared/default_branch_protection', f: f
 | 
			
		||||
    = render_if_exists 'admin/application_settings/group_owners_can_manage_default_branch_protection_setting', form: f
 | 
			
		||||
 | 
			
		||||
    .form-group
 | 
			
		||||
      = f.label s_('ProjectCreationLevel|Default project creation protection'), class: 'label-bold'
 | 
			
		||||
      = f.select :default_project_creation, options_for_select(Gitlab::Access.project_creation_options, @application_setting.default_project_creation), {}, class: 'form-control'
 | 
			
		||||
    = render 'shared/project_creation_levels', f: f, method: :default_project_creation, legend: s_('ProjectCreationLevel|Default project creation protection')
 | 
			
		||||
    = render_if_exists 'admin/application_settings/default_project_deletion_protection_setting', form: f
 | 
			
		||||
    = render_if_exists 'admin/application_settings/default_delayed_project_deletion_setting', form: f
 | 
			
		||||
    = render_if_exists 'admin/application_settings/default_project_deletion_adjourned_period_setting', form: f
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,3 +1,4 @@
 | 
			
		|||
.form-group
 | 
			
		||||
  = f.label :default_branch_protection, class: 'label-bold'
 | 
			
		||||
  = f.select :default_branch_protection, options_for_select(Gitlab::Access.protection_options, selected_level), {}, class: 'form-control'
 | 
			
		||||
%fieldset.form-group
 | 
			
		||||
  %legend.h5.gl-border-none.gl-mt-0.gl-mb-3= _('Default branch protection')
 | 
			
		||||
  - Gitlab::Access.protection_options.each do |option|
 | 
			
		||||
    = f.gitlab_ui_radio_component :default_branch_protection, option[:value], option[:label], help_text: option[:help_text]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
- method = local_assigns.fetch(:method, nil)
 | 
			
		||||
- legend = local_assigns.fetch(:legend, nil)
 | 
			
		||||
 | 
			
		||||
%fieldset.form-group
 | 
			
		||||
  %legend.h5.gl-border-none.gl-mt-0.gl-mb-3= legend
 | 
			
		||||
  - Gitlab::Access.project_creation_options.each do |label, value|
 | 
			
		||||
    = f.gitlab_ui_radio_component method, value, label
 | 
			
		||||
| 
						 | 
				
			
			@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/339639
 | 
			
		|||
milestone: '14.3'
 | 
			
		||||
type: development
 | 
			
		||||
group: group::source code
 | 
			
		||||
default_enabled: false
 | 
			
		||||
default_enabled: true
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -572,6 +572,10 @@ or collapsing a section, don't include the word **section**.
 | 
			
		|||
Use **select** with buttons, links, menu items, and lists. **Select** applies to more devices,
 | 
			
		||||
while **click** is more specific to a mouse.
 | 
			
		||||
 | 
			
		||||
## Service Desk
 | 
			
		||||
 | 
			
		||||
Use title case for **Service Desk**.
 | 
			
		||||
 | 
			
		||||
## setup, set up
 | 
			
		||||
 | 
			
		||||
Use **setup** as a noun, and **set up** as a verb. For example:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,95 @@
 | 
			
		|||
---
 | 
			
		||||
comments: false
 | 
			
		||||
type: index, dev
 | 
			
		||||
stage: none
 | 
			
		||||
group: Development
 | 
			
		||||
info: "See the Technical Writers assigned to Development Guidelines: https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments-to-development-guidelines"
 | 
			
		||||
description: "Development Guidelines: learn about workspaces when developing GitLab."
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
# Workspaces
 | 
			
		||||
 | 
			
		||||
[Read more](../../user/workspace/index.md) about the workspaces initiative.
 | 
			
		||||
 | 
			
		||||
## Consolidate Groups and Projects
 | 
			
		||||
 | 
			
		||||
- [Architecture blueprint](../../architecture/blueprints/consolidating_groups_and_projects/index.md)
 | 
			
		||||
 | 
			
		||||
Consolidate Groups and Projects is one facet of the workspaces initiative, addressing the feature disparity between
 | 
			
		||||
groups and projects.
 | 
			
		||||
 | 
			
		||||
There is feature disparity between groups and projects. Some features only available at group level (for example epics).
 | 
			
		||||
Some features only available at project level (for example issues). And some features available at both levels
 | 
			
		||||
(for example labels, milestones).
 | 
			
		||||
 | 
			
		||||
We get more and more requests to get one feature or another added to either group or project level. This takes
 | 
			
		||||
engineering time, to just move features around to different levels. This also adds a UX overhead of keeping a mental
 | 
			
		||||
model of which features are available at which level.
 | 
			
		||||
 | 
			
		||||
This also creates lots of redundant code. Features get copied from project, to group to instance level with small
 | 
			
		||||
nuances between them. This also causes extra maintenance, when something needs to be fixed. The fix is copied to
 | 
			
		||||
several locations. This also creates different user experience for the same feature but in the different locations.
 | 
			
		||||
 | 
			
		||||
To solve this we are moving towards consolidating groups and projects into a single entity, namespace. The work on this
 | 
			
		||||
solution is split into several phases and is tracked in [epic 6473](https://gitlab.com/groups/gitlab-org/-/epics/6473).
 | 
			
		||||
 | 
			
		||||
### Phase 1
 | 
			
		||||
 | 
			
		||||
Epic tracking [Phase 1](https://gitlab.com/groups/gitlab-org/-/epics/6697)
 | 
			
		||||
 | 
			
		||||
Goal of Phase 1 is to ensure that every project gets a corresponding record in `namespaces` table with `type='Project'`.
 | 
			
		||||
For user namespaces, type changes from `NULL` to `User`.
 | 
			
		||||
 | 
			
		||||
Places where we should make sure project and project namespace go hand in hand:
 | 
			
		||||
 | 
			
		||||
- Create project.
 | 
			
		||||
  - Use Rails callbacks to ensure a new project namespace is created for each project.
 | 
			
		||||
    - In this case project namespace records should have `created_at`/`updated_at` attributes equal to project's `created_at`/`updated_at` attributes.
 | 
			
		||||
- Update Project.
 | 
			
		||||
  - Use Rails `after_save` callback to ensure some attributes are kept in sync between project and project namespaces,
 | 
			
		||||
  see [project#after_save](https://gitlab.com/gitlab-org/gitlab/blob/6d26634e864d7b748dda0e283eb2477362263bc3/app/models/project.rb#L101-L101).
 | 
			
		||||
- Delete project.
 | 
			
		||||
  - Use FKs cascade delete or Rails callbacks to ensure when either a `Project` or its `ProjectNamespace` is removed its
 | 
			
		||||
  corresponding `ProjectNamespace` or `Project` respectively is removed as well.
 | 
			
		||||
- Transfer project to a different group.
 | 
			
		||||
  - Make sure that when a project is transferred, its corresponding project namespace is transferred to the same group.
 | 
			
		||||
- Transfer group.
 | 
			
		||||
  - Make sure when transferring a group that all of its sub projects, either direct or through descendant groups, have their
 | 
			
		||||
  corresponding project namespaces transferred correctly as well.
 | 
			
		||||
- Export/import project.
 | 
			
		||||
  - Export project would continue to only export the project and not its project namespace in this phase. Project
 | 
			
		||||
  namespace does not contain any specific information that has to be exported at this point. Eventually we want the
 | 
			
		||||
  project namespace to be exported as well.
 | 
			
		||||
  - Import creates a new project, so project namespace is created through Rails `after_save` callback on the project model.
 | 
			
		||||
- Export/import group.
 | 
			
		||||
  - As of this writing, when importing or exporting a `Group`, `Project`s are not included in the operation. If that functionality is changed to include `Project` when its Group is imported/exported, the logic must be sure to include their corresponding project namespaces in the import/export.
 | 
			
		||||
 | 
			
		||||
After ensuring the above points, we plan to run a DB migration to create a `ProjectNamespace` record for every `Project`.
 | 
			
		||||
Project namespace records created during migration should have `created_at`/`updated_at` attributes set to migration
 | 
			
		||||
runtime. That means that project namespaces `created_at`/`updated_at` attributes don't match their corresponding
 | 
			
		||||
project's `created_at`/`updated_at` attributes. We want the different dates to help audit any of the created project
 | 
			
		||||
namespaces, in case we need it. We plan to run the back-filling migration in 14.5 milestone.
 | 
			
		||||
 | 
			
		||||
### Phase 2
 | 
			
		||||
 | 
			
		||||
Epic tracking [Phase 2](https://gitlab.com/groups/gitlab-org/-/epics/6768)
 | 
			
		||||
 | 
			
		||||
The focus of this phase is to make `ProjectNamespace` the front entity to interact with instead of `Project` .
 | 
			
		||||
Problems to solve as part of this phase:
 | 
			
		||||
 | 
			
		||||
- Routes handling through project namespace rather than project.
 | 
			
		||||
- Memberships handling.
 | 
			
		||||
- Policies handling.
 | 
			
		||||
- Import/export.
 | 
			
		||||
- Other interactions between project namespace and project models.
 | 
			
		||||
 | 
			
		||||
Communicate the changes company wide at the engineers level. We want engineers to be aware of the upcoming changes even
 | 
			
		||||
though active collaboration of teams is expected only in phase 3. Raise awareness to avoid regressions, conflicting or duplicate work
 | 
			
		||||
that can be taken care of even before phase 3.
 | 
			
		||||
 | 
			
		||||
### Phase 3
 | 
			
		||||
 | 
			
		||||
Epic tracking [Phase 3](https://gitlab.com/groups/gitlab-org/-/epics/6585)
 | 
			
		||||
 | 
			
		||||
The focus of this phase is to get feature parity between the namespace types. Phase 3 is when active migration
 | 
			
		||||
of features from `Project` to `ProjectNamespace` or directly to `Namespace` happens.
 | 
			
		||||
| 
						 | 
				
			
			@ -74,16 +74,32 @@ module Gitlab
 | 
			
		|||
      end
 | 
			
		||||
 | 
			
		||||
      def protection_options
 | 
			
		||||
        {
 | 
			
		||||
          "Not protected: Both developers and maintainers can push new commits and force push." => PROTECTION_NONE,
 | 
			
		||||
          "Protected against pushes: Developers cannot push new commits, but are allowed to accept merge requests to the branch. Maintainers can push to the branch." => PROTECTION_DEV_CAN_MERGE,
 | 
			
		||||
          "Partially protected: Both developers and maintainers can push new commits, but cannot force push." => PROTECTION_DEV_CAN_PUSH,
 | 
			
		||||
          "Fully protected: Developers cannot push new commits, but maintainers can. No one can force push." => PROTECTION_FULL
 | 
			
		||||
        }
 | 
			
		||||
        [
 | 
			
		||||
          {
 | 
			
		||||
            label: s_('DefaultBranchProtection|Not protected'),
 | 
			
		||||
            help_text: s_('DefaultBranchProtection|Both developers and maintainers can push new commits, force push, or delete the branch.'),
 | 
			
		||||
            value: PROTECTION_NONE
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            label: s_('DefaultBranchProtection|Protected against pushes'),
 | 
			
		||||
            help_text: s_('DefaultBranchProtection|Developers cannot push new commits, but are allowed to accept merge requests to the branch. Maintainers can push to the branch.'),
 | 
			
		||||
            value: PROTECTION_DEV_CAN_MERGE
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            label: s_('DefaultBranchProtection|Partially protected'),
 | 
			
		||||
            help_text: s_('DefaultBranchProtection|Both developers and maintainers can push new commits, but cannot force push.'),
 | 
			
		||||
            value: PROTECTION_DEV_CAN_PUSH
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            label: s_('DefaultBranchProtection|Fully protected'),
 | 
			
		||||
            help_text: s_('DefaultBranchProtection|Developers cannot push new commits, but maintainers can. No one can force push.'),
 | 
			
		||||
            value: PROTECTION_FULL
 | 
			
		||||
          }
 | 
			
		||||
        ]
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def protection_values
 | 
			
		||||
        protection_options.values
 | 
			
		||||
        protection_options.map { |option| option[:value] }
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def human_access(access)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,13 +9,10 @@ module Gitlab
 | 
			
		|||
          Gitlab::AppLogger.info(message: "Checking for previously detached partitions to drop")
 | 
			
		||||
 | 
			
		||||
          Postgresql::DetachedPartition.ready_to_drop.find_each do |detached_partition|
 | 
			
		||||
            connection.transaction do
 | 
			
		||||
              # Another process may have already dropped the table and deleted this entry
 | 
			
		||||
              next unless (detached_partition = Postgresql::DetachedPartition.lock.find_by(id: detached_partition.id))
 | 
			
		||||
 | 
			
		||||
              drop_detached_partition(detached_partition.table_name)
 | 
			
		||||
 | 
			
		||||
              detached_partition.destroy!
 | 
			
		||||
            if partition_attached?(qualify_partition_name(detached_partition.table_name))
 | 
			
		||||
              unmark_partition(detached_partition)
 | 
			
		||||
            else
 | 
			
		||||
              drop_partition(detached_partition)
 | 
			
		||||
            end
 | 
			
		||||
          rescue StandardError => e
 | 
			
		||||
            Gitlab::AppLogger.error(message: "Failed to drop previously detached partition",
 | 
			
		||||
| 
						 | 
				
			
			@ -27,31 +24,100 @@ module Gitlab
 | 
			
		|||
 | 
			
		||||
        private
 | 
			
		||||
 | 
			
		||||
        def unmark_partition(detached_partition)
 | 
			
		||||
          connection.transaction do
 | 
			
		||||
            # Another process may have already encountered this case and deleted this entry
 | 
			
		||||
            next unless try_lock_detached_partition(detached_partition.id)
 | 
			
		||||
 | 
			
		||||
            # The current partition was scheduled for deletion incorrectly
 | 
			
		||||
            # Dropping it now could delete in-use data and take locks that interrupt other database activity
 | 
			
		||||
            Gitlab::AppLogger.error(message: "Prevented an attempt to drop an attached database partition", partition_name: detached_partition.table_name)
 | 
			
		||||
            detached_partition.destroy!
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        def drop_partition(detached_partition)
 | 
			
		||||
          remove_foreign_keys(detached_partition)
 | 
			
		||||
 | 
			
		||||
          connection.transaction do
 | 
			
		||||
            # Another process may have already dropped the table and deleted this entry
 | 
			
		||||
            next unless try_lock_detached_partition(detached_partition.id)
 | 
			
		||||
 | 
			
		||||
            drop_detached_partition(detached_partition.table_name)
 | 
			
		||||
 | 
			
		||||
            detached_partition.destroy!
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        def remove_foreign_keys(detached_partition)
 | 
			
		||||
          partition_identifier = qualify_partition_name(detached_partition.table_name)
 | 
			
		||||
 | 
			
		||||
          # We want to load all of these into memory at once to get a consistent view to loop over,
 | 
			
		||||
          # since we'll be deleting from this list as we go
 | 
			
		||||
          fks_to_drop = PostgresForeignKey.by_constrained_table_identifier(partition_identifier).to_a
 | 
			
		||||
          fks_to_drop.each do |foreign_key|
 | 
			
		||||
            drop_foreign_key_if_present(detached_partition, foreign_key)
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        # Drops the given foreign key for the given detached partition, but only if another process has not already
 | 
			
		||||
        # detached the partition first. This method must be safe to call even if the associated partition table has already
 | 
			
		||||
        # been detached, as it could be called by multiple processes at once.
 | 
			
		||||
        def drop_foreign_key_if_present(detached_partition, foreign_key)
 | 
			
		||||
          # It is important to only drop one foreign key per transaction.
 | 
			
		||||
          # Dropping a foreign key takes an ACCESS EXCLUSIVE lock on both tables participating in the foreign key.
 | 
			
		||||
 | 
			
		||||
          partition_identifier = qualify_partition_name(detached_partition.table_name)
 | 
			
		||||
          with_lock_retries do
 | 
			
		||||
            connection.transaction(requires_new: false) do
 | 
			
		||||
              next unless try_lock_detached_partition(detached_partition.id)
 | 
			
		||||
 | 
			
		||||
              # Another process may have already dropped this foreign key
 | 
			
		||||
              next unless PostgresForeignKey.by_constrained_table_identifier(partition_identifier).where(name: foreign_key.name).exists?
 | 
			
		||||
 | 
			
		||||
              connection.execute("ALTER TABLE #{connection.quote_table_name(partition_identifier)} DROP CONSTRAINT #{connection.quote_table_name(foreign_key.name)}")
 | 
			
		||||
 | 
			
		||||
              Gitlab::AppLogger.info(message: "Dropped foreign key for previously detached partition",
 | 
			
		||||
                                     partition_name: detached_partition.table_name,
 | 
			
		||||
                                     referenced_table_name: foreign_key.referenced_table_identifier,
 | 
			
		||||
                                     foreign_key_name: foreign_key.name)
 | 
			
		||||
            end
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        def drop_detached_partition(partition_name)
 | 
			
		||||
          partition_identifier = qualify_partition_name(partition_name)
 | 
			
		||||
 | 
			
		||||
          if partition_detached?(partition_identifier)
 | 
			
		||||
            connection.drop_table(partition_identifier, if_exists: true)
 | 
			
		||||
          connection.drop_table(partition_identifier, if_exists: true)
 | 
			
		||||
 | 
			
		||||
            Gitlab::AppLogger.info(message: "Dropped previously detached partition", partition_name: partition_name)
 | 
			
		||||
          else
 | 
			
		||||
            Gitlab::AppLogger.error(message: "Attempt to drop attached database partition", partition_name: partition_name)
 | 
			
		||||
          end
 | 
			
		||||
          Gitlab::AppLogger.info(message: "Dropped previously detached partition", partition_name: partition_name)
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        def qualify_partition_name(table_name)
 | 
			
		||||
          "#{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.#{table_name}"
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        def partition_detached?(partition_identifier)
 | 
			
		||||
        def partition_attached?(partition_identifier)
 | 
			
		||||
          # PostgresPartition checks the pg_inherits view, so our partition will only show here if it's still attached
 | 
			
		||||
          # and thus should not be dropped
 | 
			
		||||
          !Gitlab::Database::PostgresPartition.for_identifier(partition_identifier).exists?
 | 
			
		||||
          Gitlab::Database::PostgresPartition.for_identifier(partition_identifier).exists?
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        def try_lock_detached_partition(id)
 | 
			
		||||
          Postgresql::DetachedPartition.lock.find_by(id: id).present?
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        def connection
 | 
			
		||||
          Postgresql::DetachedPartition.connection
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        def with_lock_retries(&block)
 | 
			
		||||
          Gitlab::Database::WithLockRetries.new(
 | 
			
		||||
            klass: self.class,
 | 
			
		||||
            logger: Gitlab::AppLogger,
 | 
			
		||||
            connection: connection
 | 
			
		||||
          ).run(raise_on_exhaustion: true, &block)
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,6 +10,12 @@ module Gitlab
 | 
			
		|||
 | 
			
		||||
        where(referenced_table_identifier: identifier)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      scope :by_constrained_table_identifier, ->(identifier) do
 | 
			
		||||
        raise ArgumentError, "Constrained table name is not fully qualified with a schema: #{identifier}" unless identifier =~ /^\w+\.\w+$/
 | 
			
		||||
 | 
			
		||||
        where(constrained_table_identifier: identifier)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10897,6 +10897,9 @@ msgstr ""
 | 
			
		|||
msgid "Default branch and protected branches"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "Default branch protection"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "Default classification label"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -10936,6 +10939,30 @@ msgstr ""
 | 
			
		|||
msgid "DefaultBranchLabel|default"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "DefaultBranchProtection|Both developers and maintainers can push new commits, but cannot force push."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "DefaultBranchProtection|Both developers and maintainers can push new commits, force push, or delete the branch."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "DefaultBranchProtection|Developers cannot push new commits, but are allowed to accept merge requests to the branch. Maintainers can push to the branch."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "DefaultBranchProtection|Developers cannot push new commits, but maintainers can. No one can force push."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "DefaultBranchProtection|Fully protected"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "DefaultBranchProtection|Not protected"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "DefaultBranchProtection|Partially protected"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "DefaultBranchProtection|Protected against pushes"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "Define a custom deploy freeze pattern with %{cronSyntaxStart}cron syntax%{cronSyntaxEnd}"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,6 +5,8 @@ require 'spec_helper'
 | 
			
		|||
RSpec.describe Gitlab::Database::Partitioning::DetachedPartitionDropper do
 | 
			
		||||
  include Database::TableSchemaHelpers
 | 
			
		||||
 | 
			
		||||
  subject(:dropper) { described_class.new }
 | 
			
		||||
 | 
			
		||||
  let(:connection) { ActiveRecord::Base.connection }
 | 
			
		||||
 | 
			
		||||
  def expect_partition_present(name)
 | 
			
		||||
| 
						 | 
				
			
			@ -23,10 +25,18 @@ RSpec.describe Gitlab::Database::Partitioning::DetachedPartitionDropper do
 | 
			
		|||
 | 
			
		||||
  before do
 | 
			
		||||
    connection.execute(<<~SQL)
 | 
			
		||||
      CREATE TABLE referenced_table (
 | 
			
		||||
        id bigserial primary key not null
 | 
			
		||||
      )
 | 
			
		||||
    SQL
 | 
			
		||||
    connection.execute(<<~SQL)
 | 
			
		||||
 | 
			
		||||
      CREATE TABLE parent_table (
 | 
			
		||||
         id bigserial not null,
 | 
			
		||||
         referenced_id bigint not null,
 | 
			
		||||
         created_at timestamptz not null,
 | 
			
		||||
         primary key (id, created_at)
 | 
			
		||||
         primary key (id, created_at),
 | 
			
		||||
        constraint fk_referenced foreign key (referenced_id) references referenced_table(id)
 | 
			
		||||
       ) PARTITION BY RANGE(created_at)
 | 
			
		||||
    SQL
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			@ -59,7 +69,7 @@ RSpec.describe Gitlab::Database::Partitioning::DetachedPartitionDropper do
 | 
			
		|||
                         attached: false,
 | 
			
		||||
                         drop_after: 1.day.from_now)
 | 
			
		||||
 | 
			
		||||
        subject.perform
 | 
			
		||||
        dropper.perform
 | 
			
		||||
 | 
			
		||||
        expect_partition_present('test_partition')
 | 
			
		||||
      end
 | 
			
		||||
| 
						 | 
				
			
			@ -75,7 +85,7 @@ RSpec.describe Gitlab::Database::Partitioning::DetachedPartitionDropper do
 | 
			
		|||
      end
 | 
			
		||||
 | 
			
		||||
      it 'drops the partition' do
 | 
			
		||||
        subject.perform
 | 
			
		||||
        dropper.perform
 | 
			
		||||
 | 
			
		||||
        expect(table_oid('test_partition')).to be_nil
 | 
			
		||||
      end
 | 
			
		||||
| 
						 | 
				
			
			@ -86,16 +96,62 @@ RSpec.describe Gitlab::Database::Partitioning::DetachedPartitionDropper do
 | 
			
		|||
        end
 | 
			
		||||
 | 
			
		||||
        it 'does not drop the partition' do
 | 
			
		||||
          subject.perform
 | 
			
		||||
          dropper.perform
 | 
			
		||||
 | 
			
		||||
          expect(table_oid('test_partition')).not_to be_nil
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      context 'removing foreign keys' do
 | 
			
		||||
        it 'removes foreign keys from the table before dropping it' do
 | 
			
		||||
          expect(dropper).to receive(:drop_detached_partition).and_wrap_original do |drop_method, partition_name|
 | 
			
		||||
            expect(partition_name).to eq('test_partition')
 | 
			
		||||
            expect(foreign_key_exists_by_name(partition_name, 'fk_referenced', schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA)).to be_falsey
 | 
			
		||||
 | 
			
		||||
            drop_method.call(partition_name)
 | 
			
		||||
          end
 | 
			
		||||
 | 
			
		||||
          expect(foreign_key_exists_by_name('test_partition', 'fk_referenced', schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA)).to be_truthy
 | 
			
		||||
 | 
			
		||||
          dropper.perform
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        it 'does not remove foreign keys from the parent table' do
 | 
			
		||||
          expect { dropper.perform }.not_to change { foreign_key_exists_by_name('parent_table', 'fk_referenced') }.from(true)
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        context 'when another process drops the foreign key' do
 | 
			
		||||
          it 'skips dropping that foreign key' do
 | 
			
		||||
            expect(dropper).to receive(:drop_foreign_key_if_present).and_wrap_original do |drop_meth, *args|
 | 
			
		||||
              connection.execute('alter table gitlab_partitions_dynamic.test_partition drop constraint fk_referenced;')
 | 
			
		||||
              drop_meth.call(*args)
 | 
			
		||||
            end
 | 
			
		||||
 | 
			
		||||
            dropper.perform
 | 
			
		||||
 | 
			
		||||
            expect_partition_removed('test_partition')
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        context 'when another process drops the partition' do
 | 
			
		||||
          it 'skips dropping the foreign key' do
 | 
			
		||||
            expect(dropper).to receive(:drop_foreign_key_if_present).and_wrap_original do |drop_meth, *args|
 | 
			
		||||
              connection.execute('drop table gitlab_partitions_dynamic.test_partition')
 | 
			
		||||
              Postgresql::DetachedPartition.where(table_name: 'test_partition').delete_all
 | 
			
		||||
            end
 | 
			
		||||
 | 
			
		||||
            expect(Gitlab::AppLogger).not_to receive(:error)
 | 
			
		||||
            dropper.perform
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      context 'when another process drops the table while the first waits for a lock' do
 | 
			
		||||
        it 'skips the table' do
 | 
			
		||||
          # First call to .lock is for removing foreign keys
 | 
			
		||||
          expect(Postgresql::DetachedPartition).to receive(:lock).once.ordered.and_call_original
 | 
			
		||||
          # Rspec's receive_method_chain does not support .and_wrap_original, so we need to nest here.
 | 
			
		||||
          expect(Postgresql::DetachedPartition).to receive(:lock).and_wrap_original do |lock_meth|
 | 
			
		||||
          expect(Postgresql::DetachedPartition).to receive(:lock).once.ordered.and_wrap_original do |lock_meth|
 | 
			
		||||
            locked = lock_meth.call
 | 
			
		||||
            expect(locked).to receive(:find_by).and_wrap_original do |find_meth, *find_args|
 | 
			
		||||
              # Another process drops the table then deletes this entry
 | 
			
		||||
| 
						 | 
				
			
			@ -106,9 +162,9 @@ RSpec.describe Gitlab::Database::Partitioning::DetachedPartitionDropper do
 | 
			
		|||
            locked
 | 
			
		||||
          end
 | 
			
		||||
 | 
			
		||||
          expect(subject).not_to receive(:drop_one)
 | 
			
		||||
          expect(dropper).not_to receive(:drop_one)
 | 
			
		||||
 | 
			
		||||
          subject.perform
 | 
			
		||||
          dropper.perform
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
| 
						 | 
				
			
			@ -123,19 +179,26 @@ RSpec.describe Gitlab::Database::Partitioning::DetachedPartitionDropper do
 | 
			
		|||
      end
 | 
			
		||||
 | 
			
		||||
      it 'does not drop the partition, but does remove the DetachedPartition entry' do
 | 
			
		||||
        subject.perform
 | 
			
		||||
        dropper.perform
 | 
			
		||||
        aggregate_failures do
 | 
			
		||||
          expect(table_oid('test_partition')).not_to be_nil
 | 
			
		||||
          expect(Postgresql::DetachedPartition.find_by(table_name: 'test_partition')).to be_nil
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'removes the detached_partition entry' do
 | 
			
		||||
        detached_partition = Postgresql::DetachedPartition.find_by!(table_name: 'test_partition')
 | 
			
		||||
      context 'when another process removes the entry before this process' do
 | 
			
		||||
        it 'does nothing' do
 | 
			
		||||
          expect(Postgresql::DetachedPartition).to receive(:lock).and_wrap_original do |lock_meth|
 | 
			
		||||
            Postgresql::DetachedPartition.delete_all
 | 
			
		||||
            lock_meth.call
 | 
			
		||||
          end
 | 
			
		||||
 | 
			
		||||
        subject.perform
 | 
			
		||||
          expect(Gitlab::AppLogger).not_to receive(:error)
 | 
			
		||||
 | 
			
		||||
        expect(Postgresql::DetachedPartition.exists?(id: detached_partition.id)).to be_falsey
 | 
			
		||||
          dropper.perform
 | 
			
		||||
 | 
			
		||||
          expect(table_oid('test_partition')).not_to be_nil
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -155,7 +218,7 @@ RSpec.describe Gitlab::Database::Partitioning::DetachedPartitionDropper do
 | 
			
		|||
      end
 | 
			
		||||
 | 
			
		||||
      it 'drops both partitions' do
 | 
			
		||||
        subject.perform
 | 
			
		||||
        dropper.perform
 | 
			
		||||
 | 
			
		||||
        expect_partition_removed('partition_1')
 | 
			
		||||
        expect_partition_removed('partition_2')
 | 
			
		||||
| 
						 | 
				
			
			@ -163,10 +226,10 @@ RSpec.describe Gitlab::Database::Partitioning::DetachedPartitionDropper do
 | 
			
		|||
 | 
			
		||||
      context 'when the first drop returns an error' do
 | 
			
		||||
        it 'still drops the second partition' do
 | 
			
		||||
          expect(subject).to receive(:drop_detached_partition).ordered.and_raise('injected error')
 | 
			
		||||
          expect(subject).to receive(:drop_detached_partition).ordered.and_call_original
 | 
			
		||||
          expect(dropper).to receive(:drop_detached_partition).ordered.and_raise('injected error')
 | 
			
		||||
          expect(dropper).to receive(:drop_detached_partition).ordered.and_call_original
 | 
			
		||||
 | 
			
		||||
          subject.perform
 | 
			
		||||
          dropper.perform
 | 
			
		||||
 | 
			
		||||
          # We don't know which partition we tried to drop first, so the tests here have to work with either one
 | 
			
		||||
          expect(Postgresql::DetachedPartition.count).to eq(1)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -38,4 +38,16 @@ RSpec.describe Gitlab::Database::PostgresForeignKey, type: :model do
 | 
			
		|||
      expect(described_class.by_referenced_table_identifier('public.referenced_table')).to contain_exactly(expected)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe '#by_constrained_table_identifier' do
 | 
			
		||||
    it 'throws an error when the identifier name is not fully qualified' do
 | 
			
		||||
      expect { described_class.by_constrained_table_identifier('constrained_table') }.to raise_error(ArgumentError, /not fully qualified/)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'finds the foreign keys for the constrained table' do
 | 
			
		||||
      expected = described_class.where(name: %w[fk_constrained_to_referenced fk_constrained_to_other_referenced]).to_a
 | 
			
		||||
 | 
			
		||||
      expect(described_class.by_constrained_table_identifier('public.constrained_table')).to match_array(expected)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue