Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									533d1ccc98
								
							
						
					
					
						commit
						61ae9de492
					
				|  | @ -93,6 +93,8 @@ class InvitesController < ApplicationController | |||
|     store_location_for(:user, invite_landing_url) if member | ||||
| 
 | ||||
|     if user_sign_up? | ||||
|       session[:invite_email] = member.invite_email | ||||
| 
 | ||||
|       redirect_to new_user_registration_path(invite_email: member.invite_email), notice: _("To accept this invitation, create an account or sign in.") | ||||
|     else | ||||
|       redirect_to new_user_session_path(sign_in_redirect_params), notice: sign_in_notice | ||||
|  |  | |||
|  | @ -155,13 +155,21 @@ class RegistrationsController < Devise::RegistrationsController | |||
|   end | ||||
| 
 | ||||
|   def resource | ||||
|     @resource ||= Users::BuildService.new(current_user, sign_up_params).execute | ||||
|     @resource ||= Users::RegistrationsBuildService | ||||
|                     .new(current_user, sign_up_params.merge({ skip_confirmation: skip_email_confirmation? })) | ||||
|                     .execute | ||||
|   end | ||||
| 
 | ||||
|   def devise_mapping | ||||
|     @devise_mapping ||= Devise.mappings[:user] | ||||
|   end | ||||
| 
 | ||||
|   def skip_email_confirmation? | ||||
|     invite_email = session.delete(:invite_email) | ||||
| 
 | ||||
|     sign_up_params[:email] == invite_email | ||||
|   end | ||||
| 
 | ||||
|   def load_recaptcha | ||||
|     Gitlab::Recaptcha.load_configurations! | ||||
|   end | ||||
|  |  | |||
|  | @ -39,10 +39,12 @@ class DeploymentsFinder | |||
|   private | ||||
| 
 | ||||
|   def init_collection | ||||
|     if params[:project] | ||||
|     if params[:project].present? | ||||
|       params[:project].deployments | ||||
|     elsif params[:group].present? | ||||
|       ::Deployment.for_projects(params[:group].all_projects) | ||||
|     else | ||||
|       Deployment.none | ||||
|       ::Deployment.none | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|  | @ -113,5 +115,3 @@ class DeploymentsFinder | |||
|   end | ||||
|   # rubocop: enable CodeReuse/ActiveRecord | ||||
| end | ||||
| 
 | ||||
| DeploymentsFinder.prepend_if_ee('EE::DeploymentsFinder') | ||||
|  |  | |||
|  | @ -14,9 +14,11 @@ module Users | |||
|     end | ||||
| 
 | ||||
|     def execute(skip_authorization: false) | ||||
|       @skip_authorization = skip_authorization | ||||
| 
 | ||||
|       raise Gitlab::Access::AccessDeniedError unless skip_authorization || can_create_user? | ||||
| 
 | ||||
|       user_params = build_user_params(skip_authorization: skip_authorization) | ||||
|       user_params = build_user_params | ||||
|       user = User.new(user_params) | ||||
| 
 | ||||
|       if current_user&.admin? | ||||
|  | @ -37,6 +39,8 @@ module Users | |||
| 
 | ||||
|     private | ||||
| 
 | ||||
|     attr_reader :skip_authorization | ||||
| 
 | ||||
|     def identity_attributes | ||||
|       [:extern_uid, :provider] | ||||
|     end | ||||
|  | @ -102,7 +106,7 @@ module Users | |||
|       ] | ||||
|     end | ||||
| 
 | ||||
|     def build_user_params(skip_authorization:) | ||||
|     def build_user_params | ||||
|       if current_user&.admin? | ||||
|         user_params = params.slice(*admin_create_params) | ||||
| 
 | ||||
|  | @ -111,10 +115,10 @@ module Users | |||
|         end | ||||
|       else | ||||
|         allowed_signup_params = signup_params | ||||
|         allowed_signup_params << :skip_confirmation if skip_authorization | ||||
|         allowed_signup_params << :skip_confirmation if allow_caller_to_request_skip_confirmation? | ||||
| 
 | ||||
|         user_params = params.slice(*allowed_signup_params) | ||||
|         if user_params[:skip_confirmation].nil? | ||||
|         if assign_skip_confirmation_from_settings?(user_params) | ||||
|           user_params[:skip_confirmation] = skip_user_confirmation_email_from_setting | ||||
|         end | ||||
| 
 | ||||
|  | @ -136,6 +140,14 @@ module Users | |||
|       user_params | ||||
|     end | ||||
| 
 | ||||
|     def allow_caller_to_request_skip_confirmation? | ||||
|       skip_authorization | ||||
|     end | ||||
| 
 | ||||
|     def assign_skip_confirmation_from_settings?(user_params) | ||||
|       user_params[:skip_confirmation].nil? | ||||
|     end | ||||
| 
 | ||||
|     def skip_user_confirmation_email_from_setting | ||||
|       !Gitlab::CurrentSettings.send_user_confirmation_email | ||||
|     end | ||||
|  |  | |||
|  | @ -0,0 +1,19 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module Users | ||||
|   class RegistrationsBuildService < BuildService | ||||
|     extend ::Gitlab::Utils::Override | ||||
| 
 | ||||
|     private | ||||
| 
 | ||||
|     override :allow_caller_to_request_skip_confirmation? | ||||
|     def allow_caller_to_request_skip_confirmation? | ||||
|       true | ||||
|     end | ||||
| 
 | ||||
|     override :assign_skip_confirmation_from_settings? | ||||
|     def assign_skip_confirmation_from_settings?(user_params) | ||||
|       user_params[:skip_confirmation].blank? | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -10,7 +10,7 @@ | |||
|   .controls.d-flex.align-items-center | ||||
|     %a.btn.gl-button.btn-default.gl-mr-3{ href: template.preview, rel: 'noopener noreferrer', target: '_blank', data: { track_label: "template_preview", track_property: template.name, track_event: "click_button", track_value: "" } } | ||||
|       = _("Preview") | ||||
|     %label.btn.gl-button.btn-success.template-button.choose-template.gl-mb-0{ for: template.name } | ||||
|     %label.btn.gl-button.btn-confirm.template-button.choose-template.gl-mb-0{ for: template.name } | ||||
|       %input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name, data: { track_label: "template_use", track_property: template.name, track_event: "click_button", track_value: "" } } | ||||
|       %span{ data: { qa_selector: 'use_template_button' } } | ||||
|         = _("Use template") | ||||
|  |  | |||
|  | @ -0,0 +1,5 @@ | |||
| --- | ||||
| title: Do not require invited users to confirm their email address | ||||
| merge_request: 59790 | ||||
| author: | ||||
| type: other | ||||
|  | @ -0,0 +1,5 @@ | |||
| --- | ||||
| title: Add index to support execution time order for batched migration jobs | ||||
| merge_request: 60133 | ||||
| author: | ||||
| type: other | ||||
|  | @ -0,0 +1,5 @@ | |||
| --- | ||||
| title: Move to btn-confirm from btn-success in create from template page | ||||
| merge_request: 58303 | ||||
| author: Yogi (@yo) | ||||
| type: changed | ||||
|  | @ -0,0 +1,5 @@ | |||
| --- | ||||
| title: Remove Legacy Group-Level DORA metrics API | ||||
| merge_request: 59858 | ||||
| author: | ||||
| type: removed | ||||
|  | @ -0,0 +1,8 @@ | |||
| --- | ||||
| name: optimize_batched_migrations | ||||
| introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60133 | ||||
| rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/328817 | ||||
| milestone: '13.12' | ||||
| type: ops | ||||
| group: group::database | ||||
| default_enabled: false | ||||
|  | @ -0,0 +1,17 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class AddExecutionOrderIndexToBatchedBackgroundMigrationJobs < ActiveRecord::Migration[6.0] | ||||
|   include Gitlab::Database::MigrationHelpers | ||||
| 
 | ||||
|   disable_ddl_transaction! | ||||
| 
 | ||||
|   INDEX_NAME = 'index_migration_jobs_on_migration_id_and_finished_at' | ||||
| 
 | ||||
|   def up | ||||
|     add_concurrent_index :batched_background_migration_jobs, %i(batched_background_migration_id finished_at), name: INDEX_NAME | ||||
|   end | ||||
| 
 | ||||
|   def down | ||||
|     remove_concurrent_index_by_name :batched_background_migration_jobs, INDEX_NAME | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1 @@ | |||
| aa0ae491a7f94d99ea0c42250434245a4f23b0084657b709b0aaad0317dfd6b1 | ||||
|  | @ -23256,6 +23256,8 @@ CREATE INDEX index_metrics_dashboard_annotations_on_timespan_end ON metrics_dash | |||
| 
 | ||||
| CREATE INDEX index_metrics_users_starred_dashboards_on_project_id ON metrics_users_starred_dashboards USING btree (project_id); | ||||
| 
 | ||||
| CREATE INDEX index_migration_jobs_on_migration_id_and_finished_at ON batched_background_migration_jobs USING btree (batched_background_migration_id, finished_at); | ||||
| 
 | ||||
| CREATE INDEX index_milestone_releases_on_release_id ON milestone_releases USING btree (release_id); | ||||
| 
 | ||||
| CREATE INDEX index_milestones_on_description_trigram ON milestones USING gin (description gin_trgm_ops); | ||||
|  |  | |||
|  | @ -1445,3 +1445,16 @@ Here are common errors and potential causes: | |||
|   - **GRPC::Unavailable (14:all SubCons are in TransientFailure...)** | ||||
|     - Praefect cannot reach one or more of its child Gitaly nodes. Try running | ||||
|       the Praefect connection checker to diagnose. | ||||
| 
 | ||||
| ### Determine primary Gitaly node | ||||
| 
 | ||||
| To determine the current primary Gitaly node for a specific Praefect node: | ||||
| 
 | ||||
| - Use the `Shard Primary Election` [Grafana chart](#grafana) on the [`Gitlab Omnibus - Praefect` dashboard](https://gitlab.com/gitlab-org/grafana-dashboards/-/blob/master/omnibus/praefect.json). | ||||
|   This is recommended. | ||||
| - If you do not have Grafana set up, use the following command on each host of each | ||||
|   Praefect node:  | ||||
| 
 | ||||
|   ```shell | ||||
|   curl localhost:9652/metrics | grep gitaly_praefect_primaries` | ||||
|   ``` | ||||
|  |  | |||
|  | @ -1,87 +1,8 @@ | |||
| --- | ||||
| stage: Release | ||||
| group: Release | ||||
| info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments | ||||
| type: reference, api | ||||
| redirect_to: 'dora/metrics.md' | ||||
| --- | ||||
| 
 | ||||
| # DORA4 Analytics Group API **(ULTIMATE SELF)** | ||||
| This document was moved to [another location](dora/metrics.md). | ||||
| 
 | ||||
| > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/291747) in GitLab 13.9. | ||||
| > - [Deployed behind a feature flag](../user/feature_flags.md), disabled by default. | ||||
| > - Disabled on GitLab.com. | ||||
| > - Not recommended for production use. | ||||
| > - To use in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-dora4-analytics-group-api). | ||||
| 
 | ||||
| WARNING: | ||||
| These endpoints are deprecated and will be removed in GitLab 14.0. Use the [DORA metrics API](dora/metrics.md) instead. | ||||
| 
 | ||||
| WARNING: | ||||
| This feature might not be available to you. Check the **version history** note above for details. | ||||
| 
 | ||||
| All methods require reporter authorization. | ||||
| 
 | ||||
| ## List group deployment frequencies | ||||
| 
 | ||||
| Get a list of all group deployment frequencies: | ||||
| 
 | ||||
| ```plaintext | ||||
| GET /groups/:id/analytics/deployment_frequency?environment=:environment&from=:from&to=:to&interval=:interval | ||||
| ``` | ||||
| 
 | ||||
| Attributes: | ||||
| 
 | ||||
| | Attribute    | Type   | Required | Description           | | ||||
| |--------------|--------|----------|-----------------------| | ||||
| | `id`         | string | yes      | The ID of the group. | | ||||
| 
 | ||||
| Parameters: | ||||
| 
 | ||||
| | Parameter    | Type   | Required | Description           | | ||||
| |--------------|--------|----------|-----------------------| | ||||
| | `environment`| string | yes      | The name of the environment to filter by. | | ||||
| | `from`       | string | yes      | Datetime range to start from. Inclusive, ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`). | | ||||
| | `to`         | string | no       | Datetime range to end at. Exclusive, ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`). | | ||||
| | `interval`   | string | no       | The bucketing interval (`all`, `monthly`, `daily`). | | ||||
| 
 | ||||
| Example request: | ||||
| 
 | ||||
| ```shell | ||||
| curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/:id/analytics/deployment_frequency?environment=:environment&from=:from&to=:to&interval=:interval" | ||||
| ``` | ||||
| 
 | ||||
| Example response: | ||||
| 
 | ||||
| ```json | ||||
| [ | ||||
|   { | ||||
|     "from": "2017-01-01", | ||||
|     "to": "2017-01-02", | ||||
|     "value": 106 | ||||
|   }, | ||||
|   { | ||||
|     "from": "2017-01-02", | ||||
|     "to": "2017-01-03", | ||||
|     "value": 55 | ||||
|   } | ||||
| ] | ||||
| ``` | ||||
| 
 | ||||
| ## Enable or disable DORA4 Analytics Group API **(ULTIMATE SELF)** | ||||
| 
 | ||||
| DORA4 Analytics Group API is under development and not ready for production use. It is | ||||
| deployed behind a feature flag that is **disabled by default**. | ||||
| [GitLab administrators with access to the GitLab Rails console](../administration/feature_flags.md) | ||||
| can enable it. | ||||
| 
 | ||||
| To enable it: | ||||
| 
 | ||||
| ```ruby | ||||
| Feature.enable(:dora4_group_deployment_frequency_api) | ||||
| ``` | ||||
| 
 | ||||
| To disable it: | ||||
| 
 | ||||
| ```ruby | ||||
| Feature.disable(:dora4_group_deployment_frequency_api) | ||||
| ``` | ||||
| <!-- This redirect file can be deleted after <2021-07-25>. --> | ||||
| <!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page --> | ||||
|  |  | |||
|  | @ -676,6 +676,7 @@ fetch = +refs/environments/*:refs/remotes/origin/environments/* | |||
| 
 | ||||
| > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/2112) in [GitLab Premium](https://about.gitlab.com/pricing/) 9.4. | ||||
| > - [Environment scoping for CI/CD variables was moved to all tiers](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/30779) in GitLab 12.2. | ||||
| > - [Environment scoping for Group CI/CD variables](https://gitlab.com/gitlab-org/gitlab/-/issues/2874) added to GitLab Premium in 13.11 | ||||
| 
 | ||||
| You can limit the environment scope of a CI/CD variable by | ||||
| defining which environments it can be available for. | ||||
|  |  | |||
|  | @ -43,7 +43,7 @@ predefined variable: | |||
| test_variable: | ||||
|   stage: test | ||||
|   script: | ||||
|     - echo $CI_JOB_STAGE | ||||
|     - echo "$CI_JOB_STAGE" | ||||
| ``` | ||||
| 
 | ||||
| The script outputs the `stage` for the `test_variable`, which is `test`: | ||||
|  | @ -88,7 +88,7 @@ job1: | |||
|   variables: | ||||
|     TEST_VAR_JOB: "Only job1 can use this variable's value" | ||||
|   script: | ||||
|     - echo $TEST_VAR and $TEST_VAR_JOB | ||||
|     - echo "$TEST_VAR" and "$TEST_VAR_JOB" | ||||
| ``` | ||||
| 
 | ||||
| Variables saved in the `.gitlab-ci.yml` file should store only non-sensitive project | ||||
|  | @ -114,9 +114,9 @@ name inside another variable: | |||
| ```yaml | ||||
| variables: | ||||
|   FLAGS: '-al' | ||||
|   LS_CMD: 'ls $FLAGS $$TMP_DIR' | ||||
|   LS_CMD: 'ls "$FLAGS" $$TMP_DIR' | ||||
| script: | ||||
|   - 'eval $LS_CMD'  # Executes 'ls -al $TMP_DIR' | ||||
|   - 'eval "$LS_CMD"'  # Executes 'ls -al $TMP_DIR' | ||||
| ``` | ||||
| 
 | ||||
| Use the [`value` and `description`](../yaml/README.md#prefill-variables-in-manual-pipelines) | ||||
|  | @ -151,10 +151,10 @@ After you create a variable, you can use it in the `.gitlab-ci.yml` file: | |||
| test_variable: | ||||
|   stage: test | ||||
|   script: | ||||
|     - echo $CI_JOB_STAGE  # calls a predefined variable | ||||
|     - echo $TEST          # calls a custom variable of type `env_var` | ||||
|     - echo $GREETING      # calls a custom variable of type `file` that contains the path to the temp file | ||||
|     - cat $GREETING       # the temp file itself contains the variable value | ||||
|     - echo "$CI_JOB_STAGE"  # calls a predefined variable | ||||
|     - echo "$TEST"          # calls a custom variable of type `env_var` | ||||
|     - echo "$GREETING"      # calls a custom variable of type `file` that contains the path to the temp file | ||||
|     - cat "$GREETING"       # the temp file itself contains the variable value | ||||
| ``` | ||||
| 
 | ||||
| The output is: | ||||
|  | @ -181,7 +181,7 @@ To add a group variable: | |||
|    - **Key**: Must be one line, with no spaces, using only letters, numbers, or `_`. | ||||
|    - **Value**: No limitations. | ||||
|    - **Type**: [`File` or `Variable`](#cicd-variable-types). | ||||
|    - **Environment scope** (optional): `All`, or specific [environments](#limit-the-environment-scope-of-a-cicd-variable). | ||||
|    - **Environment scope** (optional): `All`, or specific [environments](#limit-the-environment-scope-of-a-cicd-variable). **PREMIUM** | ||||
|    - **Protect variable** (Optional): If selected, the variable is only available | ||||
|      in pipelines that run on protected branches or tags. | ||||
|    - **Mask variable** (Optional): If selected, the variable's **Value** is masked | ||||
|  | @ -366,7 +366,7 @@ CI/CD variable with (`$`): | |||
| ```yaml | ||||
| job_name: | ||||
|   script: | ||||
|     - echo $CI_JOB_ID | ||||
|     - echo "$CI_JOB_ID" | ||||
| ``` | ||||
| 
 | ||||
| ### Use variables with PowerShell | ||||
|  | @ -506,7 +506,7 @@ build: | |||
| deploy: | ||||
|   stage: deploy | ||||
|   script: | ||||
|     - echo $BUILD_VERSION  # Output is: 'hello' | ||||
|     - echo "$BUILD_VERSION"  # Output is: 'hello' | ||||
|   dependencies: | ||||
|     - build | ||||
| ``` | ||||
|  | @ -525,7 +525,7 @@ build: | |||
| deploy: | ||||
|   stage: deploy | ||||
|   script: | ||||
|     - echo $BUILD_VERSION  # Output is: 'hello' | ||||
|     - echo "$BUILD_VERSION"  # Output is: 'hello' | ||||
|   needs: | ||||
|     - job: build | ||||
|       artifacts: true | ||||
|  |  | |||
|  | @ -77,6 +77,7 @@ The following languages and dependency managers are supported: | |||
| 1. Support for [sbt](https://www.scala-sbt.org/) 1.3 and above was added in GitLab 13.9. | ||||
| 
 | ||||
| Plans are underway for supporting the following languages, dependency managers, and dependency files. For details, see the issue link for each. | ||||
| For workarounds, see the [Troubleshooting section](#troubleshooting) | ||||
| 
 | ||||
| | Package Managers    | Languages | Supported files | Scan tools | Issue | | ||||
| | ------------------- | --------- | --------------- | ---------- | ----- | | ||||
|  | @ -568,6 +569,53 @@ As a workaround, remove the [`retire.js`](analyzers.md#selecting-specific-analyz | |||
| 
 | ||||
| ## Troubleshooting | ||||
| 
 | ||||
| ### Working around missing support for certain languages or package managers | ||||
| 
 | ||||
| As noted in the ["Supported languages" section](#supported-languages-and-package-managers) | ||||
| some dependency definition files are not yet supported. | ||||
| However, Dependency Scanning can be achieved if | ||||
| the language, a package manager, or a third-party tool | ||||
| can convert the definition file | ||||
| into a supported format. | ||||
| 
 | ||||
| Generally, the approach is the following: | ||||
| 
 | ||||
| 1. Define a dedicated converter job in your `.gitlab-ci.yml` file. | ||||
|    Use a suitable Docker image, script, or both to facilitate the conversion. | ||||
| 1. Let that job upload the converted, supported file as an artifact. | ||||
| 1. Add [`dependencies: [<your-converter-job>]`](../../../ci/yaml/README.md#dependencies) | ||||
|    to your `dependency_scanning` job to make use of the converted definitions files. | ||||
| 
 | ||||
| For example, the currently unsupported `poetry.lock` file can be | ||||
| [converted](https://python-poetry.org/docs/cli/#export) | ||||
| to the supported `requirements.txt` as follows. | ||||
| 
 | ||||
| ```yaml | ||||
| include: | ||||
|   - template: Dependency-Scanning.gitlab-ci.yml | ||||
| 
 | ||||
| stages: | ||||
|   - .pre | ||||
|   - test | ||||
| 
 | ||||
| variables: | ||||
|   PIP_REQUIREMENTS_FILE: "requirements-converted.txt" | ||||
| 
 | ||||
| convert-poetry: | ||||
|   stage: .pre | ||||
|   image: python:3-slim | ||||
|   script: | ||||
|     - pip install poetry  # Or via another method: https://python-poetry.org/docs/#installation | ||||
|     - poetry export --output "$PIP_REQUIREMENTS_FILE" | ||||
|   artifacts: | ||||
|     paths: | ||||
|       - "$PIP_REQUIREMENTS_FILE" | ||||
| 
 | ||||
| dependency_scanning: | ||||
|   stage: test | ||||
|   dependencies: ["convert-poetry"] | ||||
| ``` | ||||
| 
 | ||||
| ### `Error response from daemon: error processing tar file: docker-tar: relocation error` | ||||
| 
 | ||||
| This error occurs when the Docker version that runs the dependency scanning job is `19.03.0`. | ||||
|  |  | |||
|  | @ -645,9 +645,6 @@ The group's new subgroups have push rules set for them based on either: | |||
|   and issues) of group members. **(PREMIUM)** | ||||
| - [Issue analytics](issues_analytics/index.md): View a bar chart of your group's number of issues per month. **(PREMIUM)** | ||||
| - Use GitLab as a [dependency proxy](../packages/dependency_proxy/index.md) for upstream Docker images. | ||||
| - [DORA4 Project Analytics API](../../api/dora4_group_analytics.md): View deployment frequency analytics. | ||||
|   [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/291747) in GitLab Ultimate 13.9 as a | ||||
|   [Beta feature](https://about.gitlab.com/handbook/product/gitlab-the-product/#beta). **(ULTIMATE SELF)** | ||||
| - [Epics](epics/index.md): Track groups of issues that share a theme. **(ULTIMATE)** | ||||
| - [Security Dashboard](../application_security/security_dashboard/index.md): View the vulnerabilities of all | ||||
|   the projects in a group and its subgroups. **(ULTIMATE)** | ||||
|  |  | |||
|  | @ -0,0 +1,68 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module Gitlab | ||||
|   module Database | ||||
|     module BackgroundMigration | ||||
|       # This is an optimizer for throughput of batched migration jobs | ||||
|       # | ||||
|       # The underyling mechanic is based on the concept of time efficiency: | ||||
|       #     time efficiency = job duration / interval | ||||
|       # Ideally, this is close but lower than 1 - so we're using time efficiently. | ||||
|       # | ||||
|       # We aim to land in the 90%-98% range, which gives the database a little breathing room | ||||
|       # in between. | ||||
|       # | ||||
|       # The optimizer is based on calculating the exponential moving average of time efficiencies | ||||
|       # for the last N jobs. If we're outside the range, we add 10% to or decrease by 20% of the batch size. | ||||
|       class BatchOptimizer | ||||
|         # Target time efficiency for a job | ||||
|         # Time efficiency is defined as: job duration / interval | ||||
|         TARGET_EFFICIENCY = (0.8..0.98).freeze | ||||
| 
 | ||||
|         # Lower and upper bound for the batch size | ||||
|         ALLOWED_BATCH_SIZE = (1_000..1_000_000).freeze | ||||
| 
 | ||||
|         # Use this batch_size multiplier to increase batch size | ||||
|         INCREASE_MULTIPLIER = 1.1 | ||||
| 
 | ||||
|         # Use this batch_size multiplier to decrease batch size | ||||
|         DECREASE_MULTIPLIER = 0.8 | ||||
| 
 | ||||
|         attr_reader :migration, :number_of_jobs | ||||
| 
 | ||||
|         def initialize(migration, number_of_jobs: 10) | ||||
|           @migration = migration | ||||
|           @number_of_jobs = number_of_jobs | ||||
|         end | ||||
| 
 | ||||
|         def optimize! | ||||
|           return unless Feature.enabled?(:optimize_batched_migrations, type: :ops) | ||||
| 
 | ||||
|           if multiplier = batch_size_multiplier | ||||
|             migration.batch_size = (migration.batch_size * multiplier).to_i.clamp(ALLOWED_BATCH_SIZE) | ||||
|             migration.save! | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         private | ||||
| 
 | ||||
|         def batch_size_multiplier | ||||
|           efficiency = migration.smoothed_time_efficiency(number_of_jobs: number_of_jobs) | ||||
| 
 | ||||
|           return unless efficiency | ||||
| 
 | ||||
|           if TARGET_EFFICIENCY.include?(efficiency) | ||||
|             # We hit the range - no change | ||||
|             nil | ||||
|           elsif efficiency > TARGET_EFFICIENCY.max | ||||
|             # We're above the range - decrease by 20% | ||||
|             DECREASE_MULTIPLIER | ||||
|           else | ||||
|             # We're below the range - increase by 10% | ||||
|             INCREASE_MULTIPLIER | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -15,10 +15,22 @@ module Gitlab | |||
|           succeeded: 3 | ||||
|         } | ||||
| 
 | ||||
|         scope :successful_in_execution_order, -> { where.not(finished_at: nil).succeeded.order(:finished_at) } | ||||
| 
 | ||||
|         delegate :aborted?, :job_class, :table_name, :column_name, :job_arguments, | ||||
|           to: :batched_migration, prefix: :migration | ||||
| 
 | ||||
|         attribute :pause_ms, :integer, default: 100 | ||||
| 
 | ||||
|         def time_efficiency | ||||
|           return unless succeeded? | ||||
|           return unless finished_at && started_at | ||||
| 
 | ||||
|           duration = finished_at - started_at | ||||
| 
 | ||||
|           # TODO: Switch to individual job interval (prereq: https://gitlab.com/gitlab-org/gitlab/-/issues/328801) | ||||
|           duration.to_f / batched_migration.interval | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  |  | |||
|  | @ -76,6 +76,30 @@ module Gitlab | |||
|             migration_identifier: "%s/%s.%s" % [job_class_name, table_name, column_name] | ||||
|           } | ||||
|         end | ||||
| 
 | ||||
|         def smoothed_time_efficiency(number_of_jobs: 10, alpha: 0.2) | ||||
|           jobs = batched_jobs.successful_in_execution_order.reverse_order.limit(number_of_jobs) | ||||
| 
 | ||||
|           return if jobs.size < number_of_jobs | ||||
| 
 | ||||
|           efficiencies = jobs.map(&:time_efficiency).reject(&:nil?).each_with_index | ||||
| 
 | ||||
|           dividend = efficiencies.reduce(0) do |total, (job_eff, i)| | ||||
|             total + job_eff * (1 - alpha)**i | ||||
|           end | ||||
| 
 | ||||
|           divisor = efficiencies.reduce(0) do |total, (job_eff, i)| | ||||
|             total + (1 - alpha)**i | ||||
|           end | ||||
| 
 | ||||
|           return if divisor == 0 | ||||
| 
 | ||||
|           (dividend / divisor).round(2) | ||||
|         end | ||||
| 
 | ||||
|         def optimize! | ||||
|           BatchOptimizer.new(self).optimize! | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  |  | |||
|  | @ -21,6 +21,8 @@ module Gitlab | |||
|         def run_migration_job(active_migration) | ||||
|           if next_batched_job = create_next_batched_job!(active_migration) | ||||
|             migration_wrapper.perform(next_batched_job) | ||||
| 
 | ||||
|             active_migration.optimize! | ||||
|           else | ||||
|             finish_active_migration(active_migration) | ||||
|           end | ||||
|  |  | |||
|  | @ -10354,9 +10354,6 @@ msgstr "" | |||
| msgid "Date range cannot exceed %{maxDateRange} days." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Date range is greater than %{quarter_days} days" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Date range must be shorter than %{max_range} days." | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -23257,15 +23254,6 @@ msgstr "" | |||
| msgid "Parameter \"job_id\" cannot exceed length of %{job_id_max_size}" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Parameter `from` must be specified" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Parameter `interval` must be one of (\"%{valid_intervals}\")" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Parameter `to` is before the `from` date" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Parent" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -36566,9 +36554,6 @@ msgstr "" | |||
| msgid "You do not have any subscriptions yet" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "You do not have permission to access deployment frequencies" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "You do not have permission to access dora metrics." | ||||
| msgstr "" | ||||
| 
 | ||||
|  |  | |||
|  | @ -22,8 +22,9 @@ RSpec.describe RegistrationsController do | |||
|   describe '#create' do | ||||
|     let(:base_user_params) { { first_name: 'first', last_name: 'last', username: 'new_username', email: 'new@user.com', password: 'Any_password' } } | ||||
|     let(:user_params) { { user: base_user_params } } | ||||
|     let(:session_params) { {} } | ||||
| 
 | ||||
|     subject { post(:create, params: user_params) } | ||||
|     subject { post(:create, params: user_params, session: session_params) } | ||||
| 
 | ||||
|     context '`blocked_pending_approval` state' do | ||||
|       context 'when the `require_admin_approval_after_user_signup` setting is turned on' do | ||||
|  | @ -148,6 +149,26 @@ RSpec.describe RegistrationsController do | |||
|             expect { subject }.to have_enqueued_mail(DeviseMailer, :confirmation_instructions) | ||||
|             expect(controller.current_user).to be_nil | ||||
|           end | ||||
| 
 | ||||
|           context 'when registration is triggered from an accepted invite' do | ||||
|             context 'when invite email matches email used on registration' do | ||||
|               let(:session_params) { { invite_email: user_params.dig(:user, :email) } } | ||||
| 
 | ||||
|               it 'signs the user in without sending a confirmation email', :aggregate_failures do | ||||
|                 expect { subject }.not_to have_enqueued_mail(DeviseMailer, :confirmation_instructions) | ||||
|                 expect(controller.current_user).to be_confirmed | ||||
|               end | ||||
|             end | ||||
| 
 | ||||
|             context 'when invite email does not match the email used on registration' do | ||||
|               let(:session_params) { { invite_email: 'bogus@email.com' } } | ||||
| 
 | ||||
|               it 'does not authenticate the user and sends a confirmation email', :aggregate_failures do | ||||
|                 expect { subject }.to have_enqueued_mail(DeviseMailer, :confirmation_instructions) | ||||
|                 expect(controller.current_user).to be_nil | ||||
|               end | ||||
|             end | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         context 'when soft email confirmation is enabled' do | ||||
|  | @ -161,6 +182,24 @@ RSpec.describe RegistrationsController do | |||
|             expect(controller.current_user).to be_present | ||||
|             expect(response).to redirect_to(users_sign_up_welcome_path) | ||||
|           end | ||||
| 
 | ||||
|           context 'when invite email matches email used on registration' do | ||||
|             let(:session_params) { { invite_email: user_params.dig(:user, :email) } } | ||||
| 
 | ||||
|             it 'signs the user in without sending a confirmation email', :aggregate_failures do | ||||
|               expect { subject }.not_to have_enqueued_mail(DeviseMailer, :confirmation_instructions) | ||||
|               expect(controller.current_user).to be_confirmed | ||||
|             end | ||||
|           end | ||||
| 
 | ||||
|           context 'when invite email does not match the email used on registration' do | ||||
|             let(:session_params) { { invite_email: 'bogus@email.com' } } | ||||
| 
 | ||||
|             it 'authenticates the user and sends a confirmation email without confirming', :aggregate_failures do | ||||
|               expect { subject }.to have_enqueued_mail(DeviseMailer, :confirmation_instructions) | ||||
|               expect(controller.current_user).not_to be_confirmed | ||||
|             end | ||||
|           end | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|  |  | |||
|  | @ -90,7 +90,6 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do | |||
|     let(:new_user) { build_stubbed(:user) } | ||||
|     let(:invite_email) { new_user.email } | ||||
|     let(:group_invite) { create(:group_member, :invited, group: group, invite_email: invite_email, created_by: owner) } | ||||
|     let!(:project_invite) { create(:project_member, :invited, project: project, invite_email: invite_email) } | ||||
| 
 | ||||
|     context 'when registering using invitation email' do | ||||
|       before do | ||||
|  | @ -122,12 +121,6 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do | |||
| 
 | ||||
|           expect(current_path).to eq(activity_group_path(group)) | ||||
|           expect(page).to have_content('You have been granted Owner access to group Owned.') | ||||
| 
 | ||||
|           visit group_path(group) | ||||
|           expect(page).to have_content(group.full_name) | ||||
| 
 | ||||
|           visit project_path(project) | ||||
|           expect(page).to have_content(project.name) | ||||
|         end | ||||
| 
 | ||||
|         context 'the user sign-up using a different email address' do | ||||
|  | @ -150,18 +143,11 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do | |||
|             allow(User).to receive(:allow_unconfirmed_access_for).and_return 0 | ||||
|           end | ||||
| 
 | ||||
|           it 'signs up and redirects to root page with all the project/groups invitation automatically accepted' do | ||||
|           it 'signs up and redirects to the group activity page with all the project/groups invitation automatically accepted' do | ||||
|             fill_in_sign_up_form(new_user) | ||||
|             confirm_email(new_user) | ||||
|             fill_in_sign_in_form(new_user) | ||||
|             fill_in_welcome_form | ||||
| 
 | ||||
|             expect(current_path).to eq(root_path) | ||||
|             expect(page).to have_content(project.full_name) | ||||
| 
 | ||||
|             visit group_path(group) | ||||
| 
 | ||||
|             expect(page).to have_content(group.full_name) | ||||
|             expect(current_path).to eq(activity_group_path(group)) | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|  | @ -170,29 +156,14 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do | |||
|             allow(User).to receive(:allow_unconfirmed_access_for).and_return 2.days | ||||
|           end | ||||
| 
 | ||||
|           it 'signs up and redirects to root page with all the project/groups invitation automatically accepted' do | ||||
|           it 'signs up and redirects to to the group activity page with all the project/groups invitation automatically accepted' do | ||||
|             fill_in_sign_up_form(new_user) | ||||
|             fill_in_welcome_form | ||||
|             confirm_email(new_user) | ||||
| 
 | ||||
|             expect(current_path).to eq(root_path) | ||||
|             expect(page).to have_content(project.full_name) | ||||
| 
 | ||||
|             visit group_path(group) | ||||
| 
 | ||||
|             expect(page).to have_content(group.full_name) | ||||
|             expect(current_path).to eq(activity_group_path(group)) | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         it "doesn't accept invitations until the user confirms their email" do | ||||
|           fill_in_sign_up_form(new_user) | ||||
|           fill_in_welcome_form | ||||
|           sign_in(owner) | ||||
| 
 | ||||
|           visit project_project_members_path(project) | ||||
|           expect(page).to have_content 'Invited' | ||||
|         end | ||||
| 
 | ||||
|         context 'the user sign-up using a different email address' do | ||||
|           let(:invite_email) { build_stubbed(:user).email } | ||||
| 
 | ||||
|  | @ -202,7 +173,7 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do | |||
|               allow(User).to receive(:allow_unconfirmed_access_for).and_return 0 | ||||
|             end | ||||
| 
 | ||||
|             it 'signs up and redirects to the invitation page' do | ||||
|             it 'signs up and redirects to the group activity page' do | ||||
|               fill_in_sign_up_form(new_user) | ||||
|               confirm_email(new_user) | ||||
|               fill_in_sign_in_form(new_user) | ||||
|  | @ -218,7 +189,7 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do | |||
|               allow(User).to receive(:allow_unconfirmed_access_for).and_return 2.days | ||||
|             end | ||||
| 
 | ||||
|             it 'signs up and redirects to the invitation page' do | ||||
|             it 'signs up and redirects to the group activity page' do | ||||
|               fill_in_sign_up_form(new_user) | ||||
|               fill_in_welcome_form | ||||
| 
 | ||||
|  | @ -282,7 +253,7 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do | |||
|         visit invite_path(group_invite.raw_invite_token) | ||||
|       end | ||||
| 
 | ||||
|       it 'grants access and redirects to group page' do | ||||
|       it 'grants access and redirects to the group activity page' do | ||||
|         expect(group.users.include?(user)).to be false | ||||
| 
 | ||||
|         page.click_link 'Accept invitation' | ||||
|  |  | |||
|  | @ -125,6 +125,7 @@ RSpec.describe 'File blob', :js do | |||
| 
 | ||||
|         page.within '.project-refs-form' do | ||||
|           click_link ref_name | ||||
|           wait_for_requests | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|  | @ -170,6 +171,27 @@ RSpec.describe 'File blob', :js do | |||
|           expect(page).not_to have_css('.hll') | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'sucessfully change ref of similar name' do | ||||
|         before do | ||||
|           project.repository.create_branch('dev') | ||||
|           project.repository.create_branch('development') | ||||
|         end | ||||
| 
 | ||||
|         it 'switch ref from longer to shorter ref name' do | ||||
|           visit_blob('files/js/application.js', ref: 'development') | ||||
|           switch_ref_to('dev') | ||||
| 
 | ||||
|           expect(page.find('.file-title-name').text).to eq('application.js') | ||||
|         end | ||||
| 
 | ||||
|         it 'switch ref from shorter to longer ref name' do | ||||
|           visit_blob('files/js/application.js', ref: 'dev') | ||||
|           switch_ref_to('development') | ||||
| 
 | ||||
|           expect(page.find('.file-title-name').text).to eq('application.js') | ||||
|         end | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'visiting with a line number anchor' do | ||||
|  |  | |||
|  | @ -160,5 +160,62 @@ RSpec.describe DeploymentsFinder do | |||
|         end | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'at group scope' do | ||||
|       let_it_be(:group) { create(:group) } | ||||
|       let_it_be(:subgroup) { create(:group, parent: group) } | ||||
| 
 | ||||
|       let_it_be(:group_project_1) { create(:project, :public, :test_repo, group: group) } | ||||
|       let_it_be(:group_project_2) { create(:project, :public, :test_repo, group: group) } | ||||
|       let_it_be(:subgroup_project_1) { create(:project, :public, :test_repo, group: subgroup) } | ||||
|       let(:base_params) { { group: group } } | ||||
| 
 | ||||
|       describe 'ordering' do | ||||
|         using RSpec::Parameterized::TableSyntax | ||||
| 
 | ||||
|         let(:params) { { **base_params, order_by: order_by, sort: sort } } | ||||
| 
 | ||||
|         let!(:group_project_1_deployment) { create(:deployment, :success, project: group_project_1, iid: 11, ref: 'master', created_at: 2.days.ago, updated_at: Time.now, finished_at: Time.now) } | ||||
|         let!(:group_project_2_deployment) { create(:deployment, :success, project: group_project_2, iid: 12, ref: 'feature', created_at: 1.day.ago, updated_at: 2.hours.ago, finished_at: 2.hours.ago) } | ||||
|         let!(:subgroup_project_1_deployment) { create(:deployment, :success, project: subgroup_project_1, iid: 8, ref: 'video', created_at: Time.now, updated_at: 1.hour.ago, finished_at: 1.hour.ago) } | ||||
| 
 | ||||
|         where(:order_by, :sort) do | ||||
|           'created_at'  | 'asc' | ||||
|           'created_at'  | 'desc' | ||||
|           'id'          | 'asc' | ||||
|           'id'          | 'desc' | ||||
|           'iid'         | 'asc' | ||||
|           'iid'         | 'desc' | ||||
|           'ref'         | 'asc' | ||||
|           'ref'         | 'desc' | ||||
|           'updated_at'  | 'asc' | ||||
|           'updated_at'  | 'desc' | ||||
|           'finished_at' | 'asc' | ||||
|           'finished_at' | 'desc' | ||||
|           'invalid'     | 'asc' | ||||
|           'iid'         | 'err' | ||||
|         end | ||||
| 
 | ||||
|         with_them do | ||||
|           it 'returns the deployments unordered' do | ||||
|             expect(subject.to_a).to contain_exactly(group_project_1_deployment, | ||||
|                                                     group_project_2_deployment, | ||||
|                                                     subgroup_project_1_deployment) | ||||
|           end | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       it 'avoids N+1 queries' do | ||||
|         execute_queries = -> { described_class.new({ group: group }).execute.first } | ||||
|         control_count = ActiveRecord::QueryRecorder.new { execute_queries }.count | ||||
| 
 | ||||
|         new_project = create(:project, :repository, group: group) | ||||
|         new_env = create(:environment, project: new_project, name: "production") | ||||
|         create_list(:deployment, 2, status: :success, project: new_project, environment: new_env) | ||||
|         group.reload | ||||
| 
 | ||||
|         expect { execute_queries }.not_to exceed_query_limit(control_count) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -0,0 +1,69 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe Gitlab::Database::BackgroundMigration::BatchOptimizer do | ||||
|   describe '#optimize' do | ||||
|     subject { described_class.new(migration, number_of_jobs: number_of_jobs).optimize! } | ||||
| 
 | ||||
|     let(:migration) { create(:batched_background_migration, batch_size: batch_size, sub_batch_size: 100, interval: 120) } | ||||
| 
 | ||||
|     let(:batch_size) { 10_000 } | ||||
| 
 | ||||
|     let_it_be(:number_of_jobs) { 5 } | ||||
| 
 | ||||
|     def mock_efficiency(eff) | ||||
|       expect(migration).to receive(:smoothed_time_efficiency).with(number_of_jobs: number_of_jobs).and_return(eff) | ||||
|     end | ||||
| 
 | ||||
|     it 'with unknown time efficiency, it keeps the batch size' do | ||||
|       mock_efficiency(nil) | ||||
| 
 | ||||
|       expect { subject }.not_to change { migration.reload.batch_size } | ||||
|     end | ||||
| 
 | ||||
|     it 'with a time efficiency of 95%, it keeps the batch size' do | ||||
|       mock_efficiency(0.95) | ||||
| 
 | ||||
|       expect { subject }.not_to change { migration.reload.batch_size } | ||||
|     end | ||||
| 
 | ||||
|     it 'with a time efficiency of 90%, it keeps the batch size' do | ||||
|       mock_efficiency(0.9) | ||||
| 
 | ||||
|       expect { subject }.not_to change { migration.reload.batch_size } | ||||
|     end | ||||
| 
 | ||||
|     it 'with a time efficiency of 70%, it increases the batch size by 10%' do | ||||
|       mock_efficiency(0.7) | ||||
| 
 | ||||
|       expect { subject }.to change { migration.reload.batch_size }.from(10_000).to(11_000) | ||||
|     end | ||||
| 
 | ||||
|     it 'with a time efficiency of 110%, it decreases the batch size by 20%' do | ||||
|       mock_efficiency(1.1) | ||||
| 
 | ||||
|       expect { subject }.to change { migration.reload.batch_size }.from(10_000).to(8_000) | ||||
|     end | ||||
| 
 | ||||
|     context 'reaching the upper limit for the batch size' do | ||||
|       let(:batch_size) { 950_000 } | ||||
| 
 | ||||
|       it 'caps the batch size at 10M' do | ||||
|         mock_efficiency(0.7) | ||||
| 
 | ||||
|         expect { subject }.to change { migration.reload.batch_size }.to(1_000_000) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'reaching the lower limit for the batch size' do | ||||
|       let(:batch_size) { 1_050 } | ||||
| 
 | ||||
|       it 'caps the batch size at 1k' do | ||||
|         mock_efficiency(1.1) | ||||
| 
 | ||||
|         expect { subject }.to change { migration.reload.batch_size }.to(1_000) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -47,4 +47,55 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d | |||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#time_efficiency' do | ||||
|     subject { job.time_efficiency } | ||||
| 
 | ||||
|     let(:migration) { build(:batched_background_migration, interval: 120.seconds) } | ||||
|     let(:job) { build(:batched_background_migration_job, status: :succeeded, batched_migration: migration) } | ||||
| 
 | ||||
|     context 'when job has not yet succeeded' do | ||||
|       let(:job) { build(:batched_background_migration_job, status: :running) } | ||||
| 
 | ||||
|       it 'returns nil' do | ||||
|         expect(subject).to be_nil | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when finished_at is not set' do | ||||
|       it 'returns nil' do | ||||
|         job.started_at = Time.zone.now | ||||
| 
 | ||||
|         expect(subject).to be_nil | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when started_at is not set' do | ||||
|       it 'returns nil' do | ||||
|         job.finished_at = Time.zone.now | ||||
| 
 | ||||
|         expect(subject).to be_nil | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when job has finished' do | ||||
|       it 'returns ratio of duration to interval, here: 0.5' do | ||||
|         freeze_time do | ||||
|           job.started_at = Time.zone.now - migration.interval / 2 | ||||
|           job.finished_at = Time.zone.now | ||||
| 
 | ||||
|           expect(subject).to eq(0.5) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       it 'returns ratio of duration to interval, here: 1' do | ||||
|         freeze_time do | ||||
|           job.started_at = Time.zone.now - migration.interval | ||||
|           job.finished_at = Time.zone.now | ||||
| 
 | ||||
|           expect(subject).to eq(1) | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -50,6 +50,15 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do | |||
|             batch_size: migration.batch_size, | ||||
|             sub_batch_size: migration.sub_batch_size) | ||||
|         end | ||||
| 
 | ||||
|         it 'optimizes the migration after executing the job' do | ||||
|           migration.update!(min_value: event1.id, max_value: event2.id) | ||||
| 
 | ||||
|           expect(migration_wrapper).to receive(:perform).ordered | ||||
|           expect(migration).to receive(:optimize!).ordered | ||||
| 
 | ||||
|           runner.run_migration_job(migration) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'when the batch maximum exceeds the migration maximum' do | ||||
|  |  | |||
|  | @ -232,4 +232,96 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m | |||
|       expect(batched_migration.prometheus_labels).to eq(labels) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#smoothed_time_efficiency' do | ||||
|     let(:migration) { create(:batched_background_migration, interval: 120.seconds) } | ||||
|     let(:end_time) { Time.zone.now } | ||||
| 
 | ||||
|     around do |example| | ||||
|       freeze_time do | ||||
|         example.run | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     let(:common_attrs) do | ||||
|       { | ||||
|         status: :succeeded, | ||||
|         batched_migration: migration, | ||||
|         finished_at: end_time | ||||
|       } | ||||
|     end | ||||
| 
 | ||||
|     context 'when there are not enough jobs' do | ||||
|       subject { migration.smoothed_time_efficiency(number_of_jobs: 10) } | ||||
| 
 | ||||
|       it 'returns nil' do | ||||
|         create_list(:batched_background_migration_job, 9, **common_attrs) | ||||
| 
 | ||||
|         expect(subject).to be_nil | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when there are enough jobs' do | ||||
|       subject { migration.smoothed_time_efficiency(number_of_jobs: number_of_jobs) } | ||||
| 
 | ||||
|       let!(:jobs) { create_list(:batched_background_migration_job, number_of_jobs, **common_attrs.merge(batched_migration: migration)) } | ||||
|       let(:number_of_jobs) { 10 } | ||||
| 
 | ||||
|       before do | ||||
|         expect(migration).to receive_message_chain(:batched_jobs, :successful_in_execution_order, :reverse_order, :limit).with(no_args).with(no_args).with(number_of_jobs).and_return(jobs) | ||||
|       end | ||||
| 
 | ||||
|       def mock_efficiencies(*effs) | ||||
|         effs.each_with_index do |eff, i| | ||||
|           expect(jobs[i]).to receive(:time_efficiency).and_return(eff) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'example 1: increasing trend, but only recently crossed threshold' do | ||||
|         it 'returns the smoothed time efficiency' do | ||||
|           mock_efficiencies(1.1, 1, 0.95, 0.9, 0.8, 0.95, 0.9, 0.8, 0.9, 0.95) | ||||
| 
 | ||||
|           expect(subject).to be_within(0.05).of(0.95) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'example 2: increasing trend, crossed threshold a while ago' do | ||||
|         it 'returns the smoothed time efficiency' do | ||||
|           mock_efficiencies(1.2, 1.1, 1, 1, 1.1, 1, 0.95, 0.9, 0.95, 0.9) | ||||
| 
 | ||||
|           expect(subject).to be_within(0.05).of(1.1) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'example 3: decreasing trend, but only recently crossed threshold' do | ||||
|         it 'returns the smoothed time efficiency' do | ||||
|           mock_efficiencies(0.9, 0.95, 1, 1.2, 1.1, 1.2, 1.1, 1.0, 1.1, 1.0) | ||||
| 
 | ||||
|           expect(subject).to be_within(0.05).of(1.0) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'example 4: latest run spiked' do | ||||
|         it 'returns the smoothed time efficiency' do | ||||
|           mock_efficiencies(1.2, 0.9, 0.8, 0.9, 0.95, 0.9, 0.92, 0.9, 0.95, 0.9) | ||||
| 
 | ||||
|           expect(subject).to be_within(0.02).of(0.96) | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#optimize!' do | ||||
|     subject { batched_migration.optimize! } | ||||
| 
 | ||||
|     let(:batched_migration) { create(:batched_background_migration) } | ||||
|     let(:optimizer) { instance_double('Gitlab::Database::BackgroundMigration::BatchOptimizer') } | ||||
| 
 | ||||
|     it 'calls the BatchOptimizer' do | ||||
|       expect(Gitlab::Database::BackgroundMigration::BatchOptimizer).to receive(:new).with(batched_migration).and_return(optimizer) | ||||
|       expect(optimizer).to receive(:optimize!) | ||||
| 
 | ||||
|       subject | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -0,0 +1,73 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe Users::RegistrationsBuildService do | ||||
|   describe '#execute' do | ||||
|     let(:base_params) { build_stubbed(:user).slice(:first_name, :last_name, :username, :email, :password) } | ||||
|     let(:skip_param) { {} } | ||||
|     let(:params) { base_params.merge(skip_param) } | ||||
| 
 | ||||
|     subject(:service) { described_class.new(nil, params) } | ||||
| 
 | ||||
|     before do | ||||
|       stub_application_setting(signup_enabled?: true) | ||||
|     end | ||||
| 
 | ||||
|     context 'when automatic user confirmation is not enabled' do | ||||
|       before do | ||||
|         stub_application_setting(send_user_confirmation_email: true) | ||||
|       end | ||||
| 
 | ||||
|       context 'when skip_confirmation is true' do | ||||
|         let(:skip_param) { { skip_confirmation: true } } | ||||
| 
 | ||||
|         it 'confirms the user' do | ||||
|           expect(service.execute).to be_confirmed | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'when skip_confirmation is not set' do | ||||
|         it 'does not confirm the user' do | ||||
|           expect(service.execute).not_to be_confirmed | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'when skip_confirmation is false' do | ||||
|         let(:skip_param) { { skip_confirmation: false } } | ||||
| 
 | ||||
|         it 'does not confirm the user' do | ||||
|           expect(service.execute).not_to be_confirmed | ||||
|         end | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when automatic user confirmation is enabled' do | ||||
|       before do | ||||
|         stub_application_setting(send_user_confirmation_email: false) | ||||
|       end | ||||
| 
 | ||||
|       context 'when skip_confirmation is true' do | ||||
|         let(:skip_param) { { skip_confirmation: true } } | ||||
| 
 | ||||
|         it 'confirms the user' do | ||||
|           expect(service.execute).to be_confirmed | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'when skip_confirmation is not set the application setting takes precedence' do | ||||
|         it 'confirms the user' do | ||||
|           expect(service.execute).to be_confirmed | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'when skip_confirmation is false the application setting takes precedence' do | ||||
|         let(:skip_param) { { skip_confirmation: false } } | ||||
| 
 | ||||
|         it 'confirms the user' do | ||||
|           expect(service.execute).to be_confirmed | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
		Loading…
	
		Reference in New Issue