Commit 5775bcdc authored by Douwe Maan's avatar Douwe Maan

Merge branch '99-support-configurable-sync-time' into 'master'

adds select field and sync time migration for mirror sync cron jobs

Closes #99

See merge request !1115
parents 1b273710 01333b96
...@@ -46,7 +46,8 @@ class Projects::MirrorsController < Projects::ApplicationController ...@@ -46,7 +46,8 @@ class Projects::MirrorsController < Projects::ApplicationController
end end
def mirror_params def mirror_params
params.require(:project).permit(:mirror, :import_url, :mirror_user_id, :mirror_trigger_builds, params.require(:project).permit(:mirror, :import_url, :mirror_user_id,
remote_mirrors_attributes: [:url, :id, :enabled]) :mirror_trigger_builds, :sync_time,
remote_mirrors_attributes: [:url, :id, :enabled, :sync_time])
end end
end end
...@@ -215,6 +215,10 @@ class Project < ActiveRecord::Base ...@@ -215,6 +215,10 @@ class Project < ActiveRecord::Base
validates :repository_size_limit, validates :repository_size_limit,
numericality: { only_integer: true, greater_than_or_equal_to: 0, allow_nil: true } numericality: { only_integer: true, greater_than_or_equal_to: 0, allow_nil: true }
validates :sync_time,
presence: true,
inclusion: { in: Gitlab::Mirror.sync_time_options.values }
with_options if: :mirror? do |project| with_options if: :mirror? do |project|
project.validates :import_url, presence: true project.validates :import_url, presence: true
project.validates :mirror_user, presence: true project.validates :mirror_user, presence: true
......
...@@ -12,6 +12,10 @@ class RemoteMirror < ActiveRecord::Base ...@@ -12,6 +12,10 @@ class RemoteMirror < ActiveRecord::Base
belongs_to :project, inverse_of: :remote_mirrors belongs_to :project, inverse_of: :remote_mirrors
validates :url, presence: true, url: { protocols: %w(ssh git http https), allow_blank: true } validates :url, presence: true, url: { protocols: %w(ssh git http https), allow_blank: true }
validates :sync_time,
presence: true,
inclusion: { in: Gitlab::Mirror.sync_time_options.values }
validate :url_availability, if: -> (mirror) { mirror.url_changed? || mirror.enabled? } validate :url_availability, if: -> (mirror) { mirror.url_changed? || mirror.enabled? }
after_save :refresh_remote, if: :mirror_url_changed? after_save :refresh_remote, if: :mirror_url_changed?
......
...@@ -45,6 +45,9 @@ ...@@ -45,6 +45,9 @@
They need to have at least master access to this project. They need to have at least master access to this project.
- if @project.builds_enabled? - if @project.builds_enabled?
= render "shared/mirror_trigger_builds_setting", f: f = render "shared/mirror_trigger_builds_setting", f: f
.form-group
= f.label :sync_time, "Synchronization time", class: "label-light append-bottom-0"
= f.select :sync_time, options_for_select(Gitlab::Mirror.sync_time_options, @project.sync_time), {}, class: 'form-control'
.col-sm-12 .col-sm-12
%hr %hr
.col-lg-3 .col-lg-3
...@@ -77,6 +80,9 @@ ...@@ -77,6 +80,9 @@
= rm_form.label :url, "Git repository URL", class: "label-light" = rm_form.label :url, "Git repository URL", class: "label-light"
= rm_form.text_field :url, class: "form-control", placeholder: 'https://username:password@gitlab.company.com/group/project.git' = rm_form.text_field :url, class: "form-control", placeholder: 'https://username:password@gitlab.company.com/group/project.git'
= render "instructions" = render "instructions"
.form-group
= rm_form.label :sync_time, "Synchronization time", class: "label-light append-bottom-0"
= rm_form.select :sync_time, options_for_select(Gitlab::Mirror.sync_time_options, @remote_mirror.sync_time), {}, class: 'form-control'
.col-sm-12.text-center .col-sm-12.text-center
%hr %hr
= f.submit 'Save changes', class: 'btn btn-create', name: 'update_remote_mirror' = f.submit 'Save changes', class: 'btn btn-create', name: 'update_remote_mirror'
...@@ -2,15 +2,15 @@ class UpdateAllMirrorsWorker ...@@ -2,15 +2,15 @@ class UpdateAllMirrorsWorker
include Sidekiq::Worker include Sidekiq::Worker
include CronjobQueue include CronjobQueue
LEASE_TIMEOUT = 3600 LEASE_TIMEOUT = 840
def perform def perform
return unless try_obtain_lease return unless try_obtain_lease
fail_stuck_mirrors! fail_stuck_mirrors!
Project.mirror.find_each(batch_size: 200) do |project| mirrors_to_sync.find_each(batch_size: 200) do |project|
RepositoryUpdateMirrorDispatchWorker.perform_in(rand(30.minutes), project.id) RepositoryUpdateMirrorDispatchWorker.perform_in(rand((project.sync_time / 2).minutes), project.id)
end end
end end
...@@ -26,8 +26,11 @@ class UpdateAllMirrorsWorker ...@@ -26,8 +26,11 @@ class UpdateAllMirrorsWorker
private private
def mirrors_to_sync
Project.mirror.where(sync_time: Gitlab::Mirror.sync_times)
end
def try_obtain_lease def try_obtain_lease
# Using 30 minutes timeout based on the 95th percent of timings (currently max of 10 minutes)
lease = ::Gitlab::ExclusiveLease.new("update_all_mirrors", timeout: LEASE_TIMEOUT) lease = ::Gitlab::ExclusiveLease.new("update_all_mirrors", timeout: LEASE_TIMEOUT)
lease.try_obtain lease.try_obtain
end end
......
...@@ -5,7 +5,7 @@ class UpdateAllRemoteMirrorsWorker ...@@ -5,7 +5,7 @@ class UpdateAllRemoteMirrorsWorker
def perform def perform
fail_stuck_mirrors! fail_stuck_mirrors!
RemoteMirror.find_each(batch_size: 50).each(&:sync) remote_mirrors_to_sync.find_each(batch_size: 50).each(&:sync)
end end
def fail_stuck_mirrors! def fail_stuck_mirrors!
...@@ -13,4 +13,10 @@ class UpdateAllRemoteMirrorsWorker ...@@ -13,4 +13,10 @@ class UpdateAllRemoteMirrorsWorker
remote_mirror.mark_as_failed('The mirror update took too long to complete.') remote_mirror.mark_as_failed('The mirror update took too long to complete.')
end end
end end
private
def remote_mirrors_to_sync
RemoteMirror.where(sync_time: Gitlab::Mirror.sync_times)
end
end end
...@@ -210,14 +210,6 @@ production: &base ...@@ -210,14 +210,6 @@ production: &base
historical_data_worker: historical_data_worker:
cron: "0 12 * * *" cron: "0 12 * * *"
# Update mirrored repositories
update_all_mirrors_worker:
cron: "0 * * * *"
# Update remote mirrors
update_all_remote_mirrors_worker:
cron: "30 * * * *"
# In addition to refreshing users when they log in, # In addition to refreshing users when they log in,
# periodically refresh LDAP users membership. # periodically refresh LDAP users membership.
# NOTE: This will only take effect if LDAP is enabled # NOTE: This will only take effect if LDAP is enabled
......
...@@ -370,12 +370,6 @@ Settings.cron_jobs['repository_archive_cache_worker']['job_class'] = 'Repository ...@@ -370,12 +370,6 @@ Settings.cron_jobs['repository_archive_cache_worker']['job_class'] = 'Repository
Settings.cron_jobs['historical_data_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['historical_data_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['historical_data_worker']['cron'] ||= '0 12 * * *' Settings.cron_jobs['historical_data_worker']['cron'] ||= '0 12 * * *'
Settings.cron_jobs['historical_data_worker']['job_class'] = 'HistoricalDataWorker' Settings.cron_jobs['historical_data_worker']['job_class'] = 'HistoricalDataWorker'
Settings.cron_jobs['update_all_mirrors_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['update_all_mirrors_worker']['cron'] ||= '0 * * * *'
Settings.cron_jobs['update_all_mirrors_worker']['job_class'] = 'UpdateAllMirrorsWorker'
Settings.cron_jobs['update_all_remote_mirrors_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['update_all_remote_mirrors_worker']['cron'] ||= '30 * * * *'
Settings.cron_jobs['update_all_remote_mirrors_worker']['job_class'] = 'UpdateAllRemoteMirrorsWorker'
Settings.cron_jobs['ldap_sync_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['ldap_sync_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['ldap_sync_worker']['cron'] ||= '30 1 * * *' Settings.cron_jobs['ldap_sync_worker']['cron'] ||= '30 1 * * *'
Settings.cron_jobs['ldap_sync_worker']['job_class'] = 'LdapSyncWorker' Settings.cron_jobs['ldap_sync_worker']['job_class'] = 'LdapSyncWorker'
......
...@@ -34,6 +34,10 @@ Sidekiq.configure_server do |config| ...@@ -34,6 +34,10 @@ Sidekiq.configure_server do |config|
end end
Sidekiq::Cron::Job.load_from_hash! cron_jobs Sidekiq::Cron::Job.load_from_hash! cron_jobs
# These jobs should not be allowed to be configured in gitlab.yml
Sidekiq::Cron::Job.create(name: 'update_all_remote_mirrors_worker', cron: '*/15 * * * *', class: 'UpdateAllRemoteMirrorsWorker')
Sidekiq::Cron::Job.create(name: 'update_all_mirrors_worker', cron: '*/15 * * * *', class: 'UpdateAllMirrorsWorker')
# Gitlab Geo: enable bulk notify job only on primary node # Gitlab Geo: enable bulk notify job only on primary node
Gitlab::Geo.bulk_notify_job.disable! unless Gitlab::Geo.primary? Gitlab::Geo.bulk_notify_job.disable! unless Gitlab::Geo.primary?
......
class AddSyncScheduleToProjectsAndRemoteProjects < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column_with_default(:remote_mirrors, :sync_time, :integer, default: 60)
add_column_with_default(:projects, :sync_time, :integer, default: 60)
end
def down
remove_column :projects, :sync_time
remove_column :remote_mirrors, :sync_time
end
end
...@@ -1112,6 +1112,7 @@ ActiveRecord::Schema.define(version: 20170204181513) do ...@@ -1112,6 +1112,7 @@ ActiveRecord::Schema.define(version: 20170204181513) do
t.text "description_html" t.text "description_html"
t.boolean "only_allow_merge_if_all_discussions_are_resolved" t.boolean "only_allow_merge_if_all_discussions_are_resolved"
t.integer "repository_size_limit", limit: 8 t.integer "repository_size_limit", limit: 8
t.integer "sync_time", default: 60, null: false
end end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
...@@ -1205,6 +1206,7 @@ ActiveRecord::Schema.define(version: 20170204181513) do ...@@ -1205,6 +1206,7 @@ ActiveRecord::Schema.define(version: 20170204181513) do
t.string "encrypted_credentials_salt" t.string "encrypted_credentials_salt"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.integer "sync_time", default: 60, null: false
end end
add_index "remote_mirrors", ["project_id"], name: "index_remote_mirrors_on_project_id", using: :btree add_index "remote_mirrors", ["project_id"], name: "index_remote_mirrors_on_project_id", using: :btree
......
# Cron jobs
## Adjusting synchronization times for repository mirroring
>**Notes:**
- This is an [Enterprise Edition][ee] only feature.
- For more information on the repository mirroring, see the
[user documentation](../workflow/repository_mirroring.md).
You can manually configure the repository synchronization times by setting the
following configuration values.
Please note that `update_all_mirrors_worker_cron` refers to the worker used for
pulling changes from a remote mirror while `update_all_remote_mirrors_worker_cron`
refers to the worker used for pushing changes to the remote mirror.
>**Note:**
These are cron formatted values. You can use a crontab generator to create these
values, for example http://www.crontabgenerator.com/.
**Omnibus installations**
```
gitlab_rails['update_all_mirrors_worker_cron'] = "0 * * * *"
gitlab_rails['update_all_remote_mirrors_worker_cron'] = "30 * * * *"
```
**Source installations**
```
cron_jobs:
update_all_mirrors_worker_cron:
cron: "0 * * * *"
update_all_remote_mirrors_worker_cron:
cron: "30 * * * *"
```
[ee]: https://about.gitlab.com/products
...@@ -7,7 +7,7 @@ There are two kinds of repository mirroring features supported by GitLab: ...@@ -7,7 +7,7 @@ There are two kinds of repository mirroring features supported by GitLab:
to another location, whereas the **pull** method mirrors an external repository to another location, whereas the **pull** method mirrors an external repository
in one in GitLab. in one in GitLab.
Mirror repositories are updated every hour, and all new branches, tags, and By default mirror repositories are updated every hour, and all new branches, tags, and
commits will be visible in the project's activity feed. commits will be visible in the project's activity feed.
Users with at least [developer access][perms] to the project can also force an Users with at least [developer access][perms] to the project can also force an
...@@ -51,8 +51,8 @@ whether you want to trigger builds for mirror updates. ...@@ -51,8 +51,8 @@ whether you want to trigger builds for mirror updates.
Since the repository on GitLab functions as a mirror of the upstream repository, Since the repository on GitLab functions as a mirror of the upstream repository,
you are advised not to push commits directly to the repository on GitLab. you are advised not to push commits directly to the repository on GitLab.
Instead, any commits should be pushed to the upstream repository, and will end Instead, any commits should be pushed to the upstream repository, and will end
up in the GitLab repository automatically within an hour, or when a up in the GitLab repository automatically within your project's configured
[forced update](#forcing-an-update) is initiated. synchronization time, or when a [forced update](#forcing-an-update) is initiated.
If you do manually update a branch in the GitLab repository, the branch will If you do manually update a branch in the GitLab repository, the branch will
become diverged from upstream, and GitLab will no longer automatically update become diverged from upstream, and GitLab will no longer automatically update
...@@ -72,8 +72,8 @@ repository to push to. Hit **Save changes** for the changes to take effect. ...@@ -72,8 +72,8 @@ repository to push to. Hit **Save changes** for the changes to take effect.
Similarly to the pull mirroring, since the upstream repository functions as a Similarly to the pull mirroring, since the upstream repository functions as a
mirror to the repository in GitLab, you are advised not to push commits directly mirror to the repository in GitLab, you are advised not to push commits directly
to the mirrored repository. Instead, any commits should be pushed to GitLab, to the mirrored repository. Instead, any commits should be pushed to GitLab,
and will end up in the mirrored repository automatically within an hour, or when and will end up in the mirrored repository automatically within the configured time,
a [forced update](#forcing-an-update) is initiated. or when a [forced update](#forcing-an-update) is initiated.
In case of a diverged branch, you will see an error indicated at the In case of a diverged branch, you will see an error indicated at the
**Mirror repository** settings. **Mirror repository** settings.
...@@ -82,7 +82,7 @@ In case of a diverged branch, you will see an error indicated at the ...@@ -82,7 +82,7 @@ In case of a diverged branch, you will see an error indicated at the
## Forcing an update ## Forcing an update
While mirrors update once an hour, you can force an update (either **push** or While mirrors update at a pre-configured time (hourly by default), you can always force an update (either **push** or
**pull**) by using the **Update now** button which is exposed in various places: **pull**) by using the **Update now** button which is exposed in various places:
- in the commits page - in the commits page
...@@ -92,22 +92,24 @@ While mirrors update once an hour, you can force an update (either **push** or ...@@ -92,22 +92,24 @@ While mirrors update once an hour, you can force an update (either **push** or
## Adjusting synchronization times ## Adjusting synchronization times
You can adjust the synchronization times for the repository mirroring if you Your repository's default synchronization time is hourly.
have access to the GitLab server. For more information, see However, you can adjust it by visiting the **Mirror repository** page
[the administration documentation][sync-times]. under the wheel icon in the upper right corner.
Check the Synchronization time section where you can choose to have your mirror
be updated once every fifteen minutes, hourly or daily and then hit **Save changes**
at the bottom.
## Using both mirroring methods at the same time ## Using both mirroring methods at the same time
Currently there is no bidirectional support without conflicts. That means that Currently there is no bidirectional support without conflicts. That means that
if you configure a repository to both pull and push to a second one, there is if you configure a repository to both pull and push to a second one, there is
no guarantee that it will update correctly on both remotes. You could no guarantee that it will update correctly on both remotes. You could
[adjust the synchronization times][sync-times] to a very low value and hope adjust the synchronization times on the mirror settings page
that no conflicts occur during the pull/push window time, but that is not a to a very low value and hope that no conflicts occur during
solution to consider on a production environment. Another thing you could try the pull/push window time, but that is not a solution to consider on a
is [configuring custom Git hooks][hooks] on the GitLab server. production environment. Another thing you could try is [configuring custom Git hooks][hooks] on the GitLab server.
[ee-51]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/51 [ee-51]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/51
[perms]: ../user/permissions.md [perms]: ../user/permissions.md
[sync-times]: ../administration/cron_jobs.md#adjusting-synchronization-times-for-repository-mirroring
[hooks]: https://docs.gitlab.com/ee/administration/custom_hooks.html [hooks]: https://docs.gitlab.com/ee/administration/custom_hooks.html
module Gitlab
module Mirror
FIFTEEN = 15
HOURLY = 60
DAILY = 1440
INTERVAL_BEFORE_FIFTEEN = 14.minutes
class << self
def sync_time_options
{
"Update every 15 minutes" => FIFTEEN,
"Update hourly" => HOURLY,
"Update every day" => DAILY,
}
end
def sync_times
sync_times = [FIFTEEN]
sync_times << DAILY if at_beginning_of_day?
sync_times << HOURLY if at_beginning_of_hour?
sync_times
end
def at_beginning_of_day?
start_at = DateTime.now.at_beginning_of_day
end_at = start_at + INTERVAL_BEFORE_FIFTEEN
DateTime.now.between?(start_at, end_at)
end
def at_beginning_of_hour?
start_at = DateTime.now.at_beginning_of_hour
end_at = start_at + INTERVAL_BEFORE_FIFTEEN
DateTime.now.between?(start_at, end_at)
end
end
end
end
require 'spec_helper' require 'spec_helper'
describe Projects::MirrorsController do describe Projects::MirrorsController do
let(:sync_times) { Gitlab::Mirror.sync_time_options.values }
describe 'setting up a mirror' do
context 'when the current project is a mirror' do
before do
@project = create(:project, :mirror)
sign_in(@project.owner)
end
context 'sync_time update' do
it 'allows sync_time update with valid time' do
sync_times.each do |sync_time|
expect do
do_put(@project, sync_time: sync_time)
end.to change { Project.mirror.where(sync_time: sync_time).count }.by(1)
end
end
it 'fails to update sync_time with invalid time' do
expect do
do_put(@project, sync_time: 1000)
end.not_to change { @project.sync_time }
end
end
end
end
describe 'setting up a remote mirror' do describe 'setting up a remote mirror' do
context 'when the current project is a mirror' do context 'when the current project is a mirror' do
before do before do
...@@ -14,6 +41,24 @@ describe Projects::MirrorsController do ...@@ -14,6 +41,24 @@ describe Projects::MirrorsController do
end.to change { RemoteMirror.count }.to(1) end.to change { RemoteMirror.count }.to(1)
end end
context 'sync_time update' do
it 'allows sync_time update with valid time' do
sync_times.each do |sync_time|
expect do
do_put(@project, remote_mirrors_attributes: { '0' => { 'enabled' => 1, 'url' => 'http://foo.com', 'sync_time' => sync_time } })
end.to change { RemoteMirror.where(sync_time: sync_time).count }.by(1)
end
end
it 'fails to update sync_time with invalid time' do
expect(@project.remote_mirrors.count).to eq(0)
expect do
do_put(@project, remote_mirrors_attributes: { '0' => { 'enabled' => 1, 'url' => 'http://foo.com', 'sync_time' => 1000 } })
end.not_to change { @project.remote_mirrors.count }
end
end
context 'when remote mirror has the same URL' do context 'when remote mirror has the same URL' do
it 'does not allow to create the remote mirror' do it 'does not allow to create the remote mirror' do
expect do expect do
......
...@@ -59,6 +59,18 @@ FactoryGirl.define do ...@@ -59,6 +59,18 @@ FactoryGirl.define do
end end
end end
trait :remote_mirror do
transient do
sync_time Gitlab::Mirror::HOURLY
url "http://foo.com"
enabled true
end
after(:create) do |project, evaluator|
project.remote_mirrors.create!(url: evaluator.url, enabled: evaluator.enabled, sync_time: evaluator.sync_time)
end
end
trait :read_only_repository do trait :read_only_repository do
repository_read_only true repository_read_only true
end end
......
...@@ -7,6 +7,16 @@ describe UpdateAllMirrorsWorker do ...@@ -7,6 +7,16 @@ describe UpdateAllMirrorsWorker do
end end
describe '#perform' do describe '#perform' do
project_count_with_time = { DateTime.now.beginning_of_hour + 15.minutes => 1,
DateTime.now.beginning_of_hour => 2,
DateTime.now.beginning_of_day => 3
}
let!(:mirror1) { create(:empty_project, :mirror, sync_time: Gitlab::Mirror::FIFTEEN) }
let!(:mirror2) { create(:empty_project, :mirror, sync_time: Gitlab::Mirror::HOURLY) }
let!(:mirror3) { create(:empty_project, :mirror, sync_time: Gitlab::Mirror::DAILY) }
let(:mirrors) { Project.mirror.where(sync_time: Gitlab::Mirror.sync_times) }
it 'fails stuck mirrors' do it 'fails stuck mirrors' do
worker = described_class.new worker = described_class.new
...@@ -15,16 +25,24 @@ describe UpdateAllMirrorsWorker do ...@@ -15,16 +25,24 @@ describe UpdateAllMirrorsWorker do
worker.perform worker.perform
end end
it 'enqueue a job on all mirrored Projects' do project_count_with_time.each do |time, project_count|
worker = described_class.new describe "at #{time}" do
before do
allow(DateTime).to receive(:now).and_return(time)
end
mirror = create(:empty_project, :mirror) it 'enqueues a job on mirrored Projects' do
create(:empty_project) worker = described_class.new
expect(worker).to receive(:rand).with(30.minutes).and_return(10) expect(mirrors.count).to eq(project_count)
expect(RepositoryUpdateMirrorDispatchWorker).to receive(:perform_in).with(10, mirror.id) mirrors.each do |mirror|
expect(worker).to receive(:rand).with((mirror.sync_time / 2).minutes).and_return(mirror.sync_time / 2)
expect(RepositoryUpdateMirrorDispatchWorker).to receive(:perform_in).with(mirror.sync_time / 2, mirror.id)
end
worker.perform worker.perform
end
end
end end
it 'does not execute if cannot get the lease' do it 'does not execute if cannot get the lease' do
......
require 'rails_helper'
describe UpdateAllRemoteMirrorsWorker do
describe "#perform" do
project_count_with_time = { DateTime.now.beginning_of_hour + 15.minutes => 1,
DateTime.now.beginning_of_hour => 2,
DateTime.now.beginning_of_day => 3
}
let!(:mirror1) { create(:project, :remote_mirror, sync_time: Gitlab::Mirror::FIFTEEN) }
let!(:mirror2) { create(:project, :remote_mirror, sync_time: Gitlab::Mirror::HOURLY) }
let!(:mirror3) { create(:project, :remote_mirror, sync_time: Gitlab::Mirror::DAILY) }
let(:mirrors) { RemoteMirror.where(sync_time: Gitlab::Mirror.sync_times) }
it 'fails stuck mirrors' do
worker = described_class.new
expect(worker).to receive(:fail_stuck_mirrors!)
worker.perform
end
project_count_with_time.each do |time, project_count|
describe "at #{time}" do
before do
allow(DateTime).to receive(:now).and_return(time)
end
it 'enqueues a job on mirrored Projects' do
worker = described_class.new
expect(mirrors.count).to eq(project_count)
mirrors.each do |mirror|
expect(RepositoryUpdateRemoteMirrorWorker).to receive(:perform_async).with(mirror.id)
end
worker.perform
end
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment