Commit f1b444d3 authored by Robert Speicher's avatar Robert Speicher

Merge branch '4644-geo-selective-sync-by-shard' into 'master'

Resolve "Geo selective sync by repository shard"

Closes #4644

See merge request gitlab-org/gitlab-ee!4286
parents 7b60afd8 9398b481
......@@ -12,14 +12,25 @@ const onPrimaryCheckboxChange = function onPrimaryCheckboxChange(e, $namespaces)
$namespaces.toggleClass('hidden', e.currentTarget.checked);
};
const onSelectiveSyncTypeChange = function onSelectiveSyncTypeChange(e, $byNamespaces, $byShards) {
$byNamespaces.toggleClass('hidden', e.target.value !== 'namespaces');
$byShards.toggleClass('hidden', e.target.value !== 'shards');
};
export default function geoNodeForm($container) {
const $namespaces = $('.js-hide-if-geo-primary', $container);
const $primaryCheckbox = $('input[type="checkbox"]', $container);
const $selectiveSyncTypeSelect = $('.js-geo-node-selective-sync-type', $container);
const $select2Dropdown = $('.js-geo-node-namespaces', $container);
const $syncByNamespaces = $('.js-sync-by-namespace', $container);
const $syncByShards = $('.js-sync-by-shard', $container);
$primaryCheckbox.on('change', e =>
onPrimaryCheckboxChange(e, $namespaces));
$selectiveSyncTypeSelect.on('change', e =>
onSelectiveSyncTypeChange(e, $syncByNamespaces, $syncByShards));
$select2Dropdown.select2({
placeholder: s__('Geo|Select groups to replicate.'),
multiple: true,
......
......@@ -97,12 +97,12 @@ module ApplicationSettingsHelper
]
end
def repository_storages_options_for_select
def repository_storages_options_for_select(selected)
options = Gitlab.config.repositories.storages.map do |name, storage|
["#{name} - #{storage['path']}", name]
end
options_for_select(options, @application_setting.repository_storages)
options_for_select(options, selected)
end
def sidekiq_queue_options_for_select
......
......@@ -575,7 +575,8 @@
.form-group
= f.label :repository_storages, 'Storage paths for new projects', class: 'control-label col-sm-2'
.col-sm-10
= f.select :repository_storages, repository_storages_options_for_select, {include_hidden: false}, multiple: true, class: 'form-control'
= f.select :repository_storages, repository_storages_options_for_select(@application_setting.repository_storages),
{include_hidden: false}, multiple: true, class: 'form-control'
.help-block
Manage repository storage paths. Learn more in the
= succeed "." do
......
---
title: Implement selective synchronization by repository shard for Geo
merge_request: 4286
author:
type: added
......@@ -1014,6 +1014,8 @@ ActiveRecord::Schema.define(version: 20180201101405) do
t.integer "files_max_capacity", default: 10, null: false
t.integer "repos_max_capacity", default: 25, null: false
t.string "url", null: false
t.string "selective_sync_type"
t.text "selective_sync_shards"
end
add_index "geo_nodes", ["access_key"], name: "index_geo_nodes_on_access_key", using: :btree
......
......@@ -216,19 +216,27 @@ Currently, this is what is synced:
* Issues, merge requests, snippets, and comment attachments
* Users, groups, and project avatars
## Selective replication
GitLab Geo supports selective replication, which allows admins to choose which
groups should be replicated by secondary nodes.
It is important to note that selective replication:
1. Does not restrict permissions from secondary nodes.
1. Does not hide projects metadata from secondary nodes. Since Geo currently
relies on PostgreSQL replication, all project metadata gets replicated to
secondary nodes, but repositories that have not been selected will be empty.
1. Secondary nodes won't pull repositories that do not belong to the selected
groups to be replicated.
## Selective synchronization
GitLab Geo supports selective synchronization, which allows admins to choose
which projects should be synchronized by secondary nodes.
It is important to note that selective synchronization does not:
1. Restrict permissions from secondary nodes.
1. Hide project metadata from secondary nodes.
* Since Geo currently relies on PostgreSQL replication, all project metadata
gets replicated to secondary nodes, but repositories that have not been
selected will be empty.
1. Reduce the number of events generated for the Geo event log
* The primary generates events as long as any secondaries are present.
Selective synchronization restrictions are implemented on the secondaries,
not the primary.
A subset of projects can be chosen, either by group or by storage shard. The
former is ideal for replicating data belonging to a subset of users, while the
latter is more suited to progressively rolling out Geo to a large GitLab
instance.
## Upgrading Geo
......
......@@ -121,9 +121,9 @@ method to be enabled. Navigate to **Admin Area ➔ Settings**
Read [Verify proper functioning of the secondary node](configuration.md#step-6-verify-proper-functioning-of-the-secondary-node).
## Selective replication
## Selective synchronization
Read [Selective replication](configuration.md#selective-replication).
Read [Selective synchronization](configuration.md#selective-synchronization).
## Troubleshooting
......
......@@ -22,12 +22,11 @@ changes on the primary!
Secondaries have a number of additional settings available:
| Setting | Description|
|--------------------------|------------|
| Public Key | The SSH public key of the user that your GitLab instance runs on (unless changed, should be the user `git`). |
| Groups to replicate | Enable Geo selective sync for this secondary - only the selected groups will be synchronized. |
| Repository sync capacity | Number of concurrent requests this secondary will make to the primary when backfilling repositories. |
| File sync capacity | Number of concurrent requests this secondary will make to the primary when backfilling files. |
| Setting | Description |
|---------------------------|-------------|
| Selective synchronization | Enable Geo [selective sync](../../gitlab-geo/configuration.md#selective-synchronization) for this secondary. |
| Repository sync capacity | Number of concurrent requests this secondary will make to the primary when backfilling repositories. |
| File sync capacity | Number of concurrent requests this secondary will make to the primary when backfilling files. |
## Geo backfill
......
......@@ -106,7 +106,7 @@
/>
<geo-node-sync-settings
v-else-if="isCustomTypeSync"
:namespaces="itemValue.namespaces"
:selective-sync-type="itemValue.selectiveSyncType"
:last-event="itemValue.lastEvent"
:cursor-last-event="itemValue.cursorLastEvent"
/>
......
......@@ -150,7 +150,7 @@
},
syncSettings() {
return {
namespaces: this.nodeDetails.namespaces,
selectiveSyncType: this.nodeDetails.selectiveSyncType,
lastEvent: this.nodeDetails.lastEvent,
cursorLastEvent: this.nodeDetails.cursorLastEvent,
};
......
......@@ -14,9 +14,10 @@
icon,
},
props: {
namespaces: {
type: Array,
required: true,
selectiveSyncType: {
type: String,
required: false,
default: null,
},
lastEvent: {
type: Object,
......@@ -30,7 +31,11 @@
computed: {
syncType() {
return this.namespaces.length > 0 ? s__('GeoNodes|Selective') : s__('GeoNodes|Full');
if (this.selectiveSyncType === null || this.selectiveSyncType === '') {
return s__('GeoNodes|Full');
}
return `${s__('GeoNodes|Selective')} (${this.selectiveSyncType})`;
},
eventTimestampEmpty() {
return this.lastEvent.timeStamp === 0 || this.cursorLastEvent.timeStamp === 0;
......
......@@ -81,7 +81,7 @@ export default class GeoNodesStore {
id: rawNodeDetails.cursor_last_event_id || 0,
timeStamp: rawNodeDetails.cursor_last_event_timestamp,
},
namespaces: rawNodeDetails.namespaces,
selectiveSyncType: rawNodeDetails.selective_sync_type,
dbReplicationLag: rawNodeDetails.db_replication_lag_seconds,
};
}
......
......@@ -87,6 +87,8 @@ class Admin::GeoNodesController < Admin::ApplicationController
params.require(:geo_node).permit(
:url,
:primary,
:selective_sync_type,
:selective_sync_shards,
:namespace_ids,
:repos_max_capacity,
:files_max_capacity
......
......@@ -68,7 +68,16 @@ module Geo
private
def group_uploads
namespace_ids = Gitlab::GroupHierarchy.new(current_node.namespaces).base_and_descendants.select(:id)
namespace_ids =
if current_node.selective_sync_by_namespaces?
Gitlab::GroupHierarchy.new(current_node.namespaces).base_and_descendants.select(:id)
elsif current_node.selective_sync_by_shards?
leaf_groups = Namespace.where(id: current_node.projects.select(:namespace_id))
Gitlab::GroupHierarchy.new(leaf_groups).base_and_ancestors.select(:id)
else
Namespace.none
end
arel_namespace_ids = Arel::Nodes::SqlLiteral.new(namespace_ids.to_sql)
upload_table[:model_type].eq('Namespace').and(upload_table[:model_id].in(arel_namespace_ids))
......
......@@ -19,6 +19,17 @@ module EE
end
end
def selective_sync_type_options_for_select(geo_node)
options_for_select(
[
[s_('Geo|All projects'), ''],
[s_('Geo|Projects in certain groups'), 'namespaces'],
[s_('Geo|Projects in certain storage shards'), 'shards']
],
geo_node.selective_sync_type
)
end
def status_loading_icon
icon "spinner spin fw", class: 'js-geo-node-loading'
end
......
class GeoNode < ActiveRecord::Base
include Presentable
SELECTIVE_SYNC_TYPES = %w[namespaces shards].freeze
# Array of repository storages to synchronize for selective sync by shards
serialize :selective_sync_shards, Array # rubocop:disable Cop/ActiveRecordSerialize
belongs_to :oauth_application, class_name: 'Doorkeeper::Application', dependent: :destroy # rubocop: disable Cop/ActiveRecordDependent
has_many :geo_node_namespace_links
......@@ -18,6 +23,12 @@ class GeoNode < ActiveRecord::Base
validates :access_key, presence: true
validates :encrypted_secret_access_key, presence: true
validates :selective_sync_type, inclusion: {
in: SELECTIVE_SYNC_TYPES,
allow_blank: true,
allow_nil: true
}
validate :check_not_adding_primary_as_secondary, if: :secondary?
after_save :expire_cache!
......@@ -119,13 +130,26 @@ class GeoNode < ActiveRecord::Base
end
def projects
if selective_sync?
Project.where(namespace_id: Gitlab::GroupHierarchy.new(namespaces).base_and_descendants.select(:id))
return Project.all unless selective_sync?
if selective_sync_by_namespaces?
query = Gitlab::GroupHierarchy.new(namespaces).base_and_descendants
Project.where(namespace_id: query.select(:id))
elsif selective_sync_by_shards?
Project.where(repository_storage: selective_sync_shards)
else
Project.all
Project.none
end
end
def selective_sync_by_namespaces?
selective_sync_type == 'namespaces'
end
def selective_sync_by_shards?
selective_sync_type == 'shards'
end
def projects_include?(project_id)
return true unless selective_sync?
......@@ -133,7 +157,7 @@ class GeoNode < ActiveRecord::Base
end
def selective_sync?
namespaces.exists?
selective_sync_type.present?
end
def replication_slots_count
......
class GeoNodeStatus < ActiveRecord::Base
belongs_to :geo_node
delegate :selective_sync_type, to: :geo_node
# Whether we were successful in reaching this node
attr_accessor :success, :version, :revision
attr_writer :health_status
......
module Geo
class NodeUpdateService
attr_reader :geo_node, :old_namespace_ids, :params
attr_reader :geo_node, :old_namespace_ids, :old_shards, :params
def initialize(geo_node, params)
@geo_node = geo_node
@old_namespace_ids = geo_node.namespace_ids
@old_shards = geo_node.selective_sync_shards
@params = params.dup
@params[:namespace_ids] = @params[:namespace_ids].to_s.split(',')
end
def execute
return false unless geo_node.update(params)
if geo_node.secondary? && namespaces_changed?(geo_node)
Geo::RepositoriesChangedEventStore.new(geo_node).create
end
Geo::RepositoriesChangedEventStore.new(geo_node).create if selective_sync_changed?
true
end
private
def namespaces_changed?(geo_node)
def selective_sync_changed?
return false unless geo_node.secondary?
geo_node.selective_sync_type_changed? ||
selective_sync_by_namespaces_changed? ||
selective_sync_by_shards_changed?
end
def selective_sync_by_namespaces_changed?
return false unless geo_node.selective_sync_by_namespaces?
geo_node.namespace_ids.any? && geo_node.namespace_ids != old_namespace_ids
end
def selective_sync_by_shards_changed?
return false unless geo_node.selective_sync_by_shards?
geo_node.selective_sync_shards.any? && geo_node.selective_sync_shards != old_shards
end
end
end
......@@ -11,12 +11,27 @@
= form.check_box :primary
%strong This is a primary node
.form-group.js-hide-if-geo-primary{ class: ('hidden' unless geo_node.secondary?) }
= form.label :namespace_ids, s_('Geo|Groups to replicate'), class: 'control-label'
.col-sm-10
= hidden_field_tag "#{form.object_name}[namespace_ids]", geo_node.namespace_ids.join(","), class: 'js-geo-node-namespaces', data: { selected: node_namespaces_options(geo_node.namespaces).to_json }
.help-block
#{ s_("Choose which groups you wish to replicate to this secondary node. Leave blank to replicate all.") }
.js-hide-if-geo-primary{ class: ('hidden' unless geo_node.secondary?) }
.form-group
= form.label :selective_sync_type, s_('Selective synchronization'), class: 'control-label'
.col-sm-10
= form.select :selective_sync_type, selective_sync_type_options_for_select(geo_node),
{}, { class: "form-control js-geo-node-selective-sync-type" }
.form-group.js-sync-by-namespace{ class: ('hidden' unless geo_node.selective_sync_by_namespaces?) }
= form.label :namespace_ids, s_('Geo|Groups to synchronize'), class: 'control-label'
.col-sm-10
= hidden_field_tag "#{form.object_name}[namespace_ids]", geo_node.namespace_ids.join(","), class: 'js-geo-node-namespaces', data: { selected: node_namespaces_options(geo_node.namespaces).to_json }
.help-block
#{ s_("Choose which groups you wish to synchronize to this secondary node.") }
.form-group.js-sync-by-shard{ class: ('hidden' unless geo_node.selective_sync_by_shards?) }
= form.label :selective_sync_shards, s_('Geo|Shards to synchronize'), class: 'control-label'
.col-sm-10
= form.select :selective_sync_shards, repository_storages_options_for_select(geo_node.selective_sync_shards),
{ include_hidden: false }, multiple: true, class: 'form-control'
.help-block
#{ s_("Choose which shards you wish to synchronize to this secondary node.") }
.form-group.js-hide-if-geo-primary{ class: ('hidden' unless geo_node.secondary?) }
= form.label :repos_max_capacity, s_('Geo|Repository sync capacity'), class: 'control-label'
......
......@@ -12,7 +12,7 @@ module Geo
return unless Gitlab::Geo.geo_database_configured?
return unless Gitlab::Geo.secondary?
shards = healthy_shards
shards = selective_sync_filter(healthy_shards)
Gitlab::Geo::ShardHealthCache.update(shards)
......@@ -32,5 +32,11 @@ module Geo
.compact
.uniq
end
def selective_sync_filter(shards)
return shards unless ::Gitlab::Geo.current_node&.selective_sync_by_shards?
shards & ::Gitlab::Geo.current_node.selective_sync_shards
end
end
end
class GeoSelectiveSyncByShard < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
add_column :geo_nodes, :selective_sync_type, :string
add_column :geo_nodes, :selective_sync_shards, :text
# Nodes with associated namespaces should be set to 'namespaces'
connection.execute(<<~SQL)
UPDATE geo_nodes
SET selective_sync_type = 'namespaces'
WHERE id IN(
SELECT DISTINCT geo_node_id
FROM geo_node_namespace_links
)
SQL
end
def down
remove_column :geo_nodes, :selective_sync_type
remove_column :geo_nodes, :selective_sync_shards
end
end
......@@ -1226,6 +1226,9 @@ module API
expose :version
expose :revision
expose :selective_sync_type
# Deprecated: remove in API v5. We use selective_sync_type instead now.
expose :namespaces, using: NamespaceBasic
expose :updated_at
......
......@@ -30,6 +30,7 @@
"last_event_timestamp",
"cursor_last_event_id",
"cursor_last_event_timestamp",
"selective_sync_type",
"namespaces",
"version",
"revision",
......@@ -72,6 +73,7 @@
"cursor_last_event_id": { "type": ["integer", "null"] },
"cursor_last_event_timestamp": { "type": ["integer", "null"] },
"last_successful_status_check_timestamp": { "type": ["integer", "null"] },
"selective_sync_type": { "type": ["string", "null"] },
"namespaces": { "type": "array" },
"storage_shards": { "type": "array" },
"storage_shards_match": { "type": "boolean" },
......
......@@ -9,7 +9,7 @@ describe Geo::AttachmentRegistryFinder, :geo do
let(:synced_subgroup) { create(:group, parent: synced_group) }
let(:unsynced_group) { create(:group) }
let(:synced_project) { create(:project, group: synced_group) }
let(:unsynced_project) { create(:project, group: unsynced_group) }
let(:unsynced_project) { create(:project, group: unsynced_group, repository_storage: 'broken') }
let!(:upload_1) { create(:upload, model: synced_group) }
let!(:upload_2) { create(:upload, model: unsynced_group) }
......@@ -54,21 +54,12 @@ describe Geo::AttachmentRegistryFinder, :geo do
end
context 'with selective sync' do
it 'returns synced avatars, attachment, personal snippets and files' do
create(:geo_file_registry, :avatar, file_id: upload_1.id)
create(:geo_file_registry, :avatar, file_id: upload_2.id)
create(:geo_file_registry, :avatar, file_id: upload_3.id)
create(:geo_file_registry, :avatar, file_id: upload_4.id)
create(:geo_file_registry, :avatar, file_id: upload_5.id, success: false)
create(:geo_file_registry, :avatar, file_id: upload_6.id)
create(:geo_file_registry, :avatar, file_id: upload_7.id)
create(:geo_file_registry, :lfs, file_id: lfs_object.id)
it 'falls back to legacy queries' do
secondary.update!(selective_sync_type: 'namespaces', namespaces: [synced_group])
secondary.update_attribute(:namespaces, [synced_group])
synced_attachments = subject.find_synced_attachments
expect(subject).to receive(:legacy_find_synced_attachments)
expect(synced_attachments.pluck(:id)).to match_array([upload_1.id, upload_3.id, upload_6.id, upload_7.id])
subject.find_synced_attachments
end
end
end
......@@ -94,21 +85,12 @@ describe Geo::AttachmentRegistryFinder, :geo do
end
context 'with selective sync' do
it 'returns failed avatars, attachment, personal snippets and files' do
create(:geo_file_registry, :avatar, file_id: upload_1.id, success: false)
create(:geo_file_registry, :avatar, file_id: upload_2.id)
create(:geo_file_registry, :avatar, file_id: upload_3.id, success: false)
create(:geo_file_registry, :avatar, file_id: upload_4.id)
create(:geo_file_registry, :avatar, file_id: upload_5.id)
create(:geo_file_registry, :avatar, file_id: upload_6.id, success: false)
create(:geo_file_registry, :avatar, file_id: upload_7.id, success: false)
create(:geo_file_registry, :lfs, file_id: lfs_object.id, success: false)
secondary.update_attribute(:namespaces, [synced_group])
it 'falls back to legacy queries' do
secondary.update!(selective_sync_type: 'namespaces', namespaces: [synced_group])
failed_attachments = subject.find_failed_attachments
expect(subject).to receive(:legacy_find_failed_attachments)
expect(failed_attachments.pluck(:id)).to match_array([upload_1.id, upload_3.id, upload_6.id, upload_7.id])
subject.find_failed_attachments
end
end
end
......@@ -163,7 +145,7 @@ describe Geo::AttachmentRegistryFinder, :geo do
expect(synced_attachments).to match_array([upload_1, upload_2, upload_6, upload_7])
end
context 'with selective sync' do
context 'with selective sync by namespace' do
it 'returns synced avatars, attachment, personal snippets and files' do
create(:geo_file_registry, :avatar, file_id: upload_1.id)
create(:geo_file_registry, :avatar, file_id: upload_2.id)
......@@ -174,13 +156,32 @@ describe Geo::AttachmentRegistryFinder, :geo do
create(:geo_file_registry, :avatar, file_id: upload_7.id)
create(:geo_file_registry, :lfs, file_id: lfs_object.id)
secondary.update_attribute(:namespaces, [synced_group])
secondary.update!(selective_sync_type: 'namespaces', namespaces: [synced_group])
synced_attachments = subject.find_synced_attachments
expect(synced_attachments).to match_array([upload_1, upload_3, upload_6, upload_7])
end
end
context 'with selective sync by shard' do
it 'returns synced avatars, attachment, personal snippets and files' do
create(:geo_file_registry, :avatar, file_id: upload_1.id)
create(:geo_file_registry, :avatar, file_id: upload_2.id)
create(:geo_file_registry, :avatar, file_id: upload_3.id)
create(:geo_file_registry, :avatar, file_id: upload_4.id)
create(:geo_file_registry, :avatar, file_id: upload_5.id, success: false)
create(:geo_file_registry, :avatar, file_id: upload_6.id)
create(:geo_file_registry, :avatar, file_id: upload_7.id)
create(:geo_file_registry, :lfs, file_id: lfs_object.id)
secondary.update!(selective_sync_type: 'shards', selective_sync_shards: ['default'])
synced_attachments = subject.find_synced_attachments
expect(synced_attachments).to match_array([upload_1, upload_3, upload_6])
end
end
end
describe '#find_failed_attachments' do
......@@ -203,24 +204,43 @@ describe Geo::AttachmentRegistryFinder, :geo do
expect(failed_attachments).to match_array([upload_3, upload_6, upload_7])
end
context 'with selective sync' do
context 'with selective sync by namespace' do
it 'returns failed avatars, attachment, personal snippets and files' do
create(:geo_file_registry, :avatar, file_id: upload_1.id, success: false)
create(:geo_file_registry, :avatar, file_id: upload_2.id)
create(:geo_file_registry, :avatar, file_id: upload_3.id, success: false)
create(:geo_file_registry, :avatar, file_id: upload_4.id)
create(:geo_file_registry, :avatar, file_id: upload_4.id, success: false)
create(:geo_file_registry, :avatar, file_id: upload_5.id)
create(:geo_file_registry, :avatar, file_id: upload_6.id, success: false)
create(:geo_file_registry, :avatar, file_id: upload_7.id, success: false)
create(:geo_file_registry, :lfs, file_id: lfs_object.id, success: false)
secondary.update_attribute(:namespaces, [synced_group])
secondary.update!(selective_sync_type: 'namespaces', namespaces: [synced_group])
failed_attachments = subject.find_failed_attachments
expect(failed_attachments).to match_array([upload_1, upload_3, upload_6, upload_7])
end
end
context 'with selective sync by shard' do
it 'returns failed avatars, attachment, personal snippets and files' do
create(:geo_file_registry, :avatar, file_id: upload_1.id, success: false)
create(:geo_file_registry, :avatar, file_id: upload_2.id)
create(:geo_file_registry, :avatar, file_id: upload_3.id, success: false)
create(:geo_file_registry, :avatar, file_id: upload_4.id, success: false)
create(:geo_file_registry, :avatar, file_id: upload_5.id)
create(:geo_file_registry, :avatar, file_id: upload_6.id, success: false)
create(:geo_file_registry, :avatar, file_id: upload_7.id, success: false)
create(:geo_file_registry, :lfs, file_id: lfs_object.id, success: false)
secondary.update!(selective_sync_type: 'shards', selective_sync_shards: ['default'])
failed_attachments = subject.find_failed_attachments
expect(failed_attachments).to match_array([upload_1, upload_3, upload_6])
end
end
end
describe '#find_unsynced_attachments' do
......
......@@ -36,7 +36,7 @@ describe Geo::JobArtifactRegistryFinder, :geo do
context 'with selective sync' do
before do
secondary.update_attribute(:namespaces, [synced_group])
secondary.update!(selective_sync_type: 'namespaces', namespaces: [synced_group])
end
it 'delegates to #legacy_find_synced_job_artifacts' do
......@@ -72,7 +72,7 @@ describe Geo::JobArtifactRegistryFinder, :geo do
context 'with selective sync' do
before do
secondary.update_attribute(:namespaces, [synced_group])
secondary.update!(selective_sync_type: 'namespaces', namespaces: [synced_group])
end
it 'delegates to #legacy_find_failed_job_artifacts' do
......
......@@ -36,7 +36,7 @@ describe Geo::LfsObjectRegistryFinder, :geo do
context 'with selective sync' do
before do
secondary.update_attribute(:namespaces, [synced_group])
secondary.update!(selective_sync_type: 'namespaces', namespaces: [synced_group])
end
it 'delegates to #legacy_find_synced_lfs_objects' do
......@@ -78,7 +78,7 @@ describe Geo::LfsObjectRegistryFinder, :geo do
context 'with selective sync' do
before do
secondary.update_attribute(:namespaces, [synced_group])
secondary.update!(selective_sync_type: 'namespaces', namespaces: [synced_group])
end
it 'delegates to #legacy_find_failed_lfs_objects' do
......
......@@ -34,7 +34,7 @@ describe Geo::ProjectRegistryFinder, :geo do
context 'with selective sync' do
before do
secondary.update_attribute(:namespaces, [synced_group])
secondary.update!(selective_sync_type: 'namespaces', namespaces: [synced_group])
end
it 'delegates to #legacy_find_synced_repositories' do
......@@ -85,7 +85,7 @@ describe Geo::ProjectRegistryFinder, :geo do
context 'with selective sync' do
before do
secondary.update_attribute(:namespaces, [synced_group])
secondary.update!(selective_sync_type: 'namespaces', namespaces: [synced_group])
end
it 'delegates to #legacy_find_synced_wiki' do
......@@ -125,7 +125,7 @@ describe Geo::ProjectRegistryFinder, :geo do
context 'with selective sync' do
before do
secondary.update_attribute(:namespaces, [synced_group])
secondary.update!(selective_sync_type: 'namespaces', namespaces: [synced_group])
end
it 'delegates to #find_failed_repositories' do
......@@ -165,7 +165,7 @@ describe Geo::ProjectRegistryFinder, :geo do
context 'with selective sync' do
before do
secondary.update_attribute(:namespaces, [synced_group])
secondary.update!(selective_sync_type: 'namespaces', namespaces: [synced_group])
end
it 'delegates to #find_failed_wikis' do
......@@ -212,7 +212,7 @@ describe Geo::ProjectRegistryFinder, :geo do
context 'with selective sync' do
before do
secondary.update_attribute(:namespaces, [synced_group])
secondary.update!(selective_sync_type: 'namespaces', namespaces: [synced_group])
end
it 'delegates to #legacy_find_filtered_failed_projects' do
......@@ -265,7 +265,7 @@ describe Geo::ProjectRegistryFinder, :geo do
end
it 'delegates to #legacy_find_unsynced_projects when node has selective sync' do
secondary.update_attribute(:namespaces, [synced_group])
secondary.update!(selective_sync_type: 'namespaces', namespaces: [synced_group])
expect(subject).to receive(:legacy_find_unsynced_projects).and_call_original
......@@ -290,7 +290,7 @@ describe Geo::ProjectRegistryFinder, :geo do
end
it 'delegates to #legacy_find_projects_updated_recently when node has selective sync' do
secondary.update_attribute(:namespaces, [synced_group])
secondary.update!(selective_sync_type: 'namespaces', namespaces: [synced_group])
expect(subject).to receive(:legacy_find_projects_updated_recently).and_call_original
......
......@@ -204,7 +204,13 @@ describe Gitlab::Geo::LogCursor::Daemon, :postgresql, :clean_gitlab_redis_shared
end
it 'does not replay events for projects that do not belong to selected namespaces to replicate' do
secondary.update!(namespaces: [group_2])
secondary.update!(selective_sync_type: 'namespaces', namespaces: [group_2])
expect { daemon.run_once! }.not_to change(Geo::ProjectRegistry, :count)
end
it 'does not replay events for projects that do not belong to selected shards to replicate' do
secondary.update!(selective_sync_type: 'shards', selective_sync_shards: ['broken'])
expect { daemon.run_once! }.not_to change(Geo::ProjectRegistry, :count)
end
......
......@@ -21,6 +21,10 @@ describe GeoNode, type: :model do
it { is_expected.to have_many(:namespaces).through(:geo_node_namespace_links) }
end
context 'validations' do
it { is_expected.to validate_inclusion_of(:selective_sync_type).in_array([nil, *GeoNode::SELECTIVE_SYNC_TYPES]) }
end
context 'default values' do
where(:attribute, :value) do
:url | Gitlab::Routing.url_helpers.root_url
......@@ -254,27 +258,43 @@ describe GeoNode, type: :model do
end
describe '#projects_include?' do
let(:unsynced_project) { create(:project) }
let(:unsynced_project) { create(:project, repository_storage: 'broken') }
it 'returns true without namespace restrictions' do
it 'returns true without selective sync' do
expect(node.projects_include?(unsynced_project.id)).to eq true
end
context 'with namespace restrictions' do
context 'selective sync by namespaces' do
let(:synced_group) { create(:group) }
before do
node.update_attribute(:namespaces, [synced_group])
node.update!(selective_sync_type: 'namespaces', namespaces: [synced_group])
end
it 'returns true when project belongs to one of the namespaces' do
project_in_synced_group = create(:project, group: synced_group)
expect(node.projects_include?(project_in_synced_group.id)).to eq true
expect(node.projects_include?(project_in_synced_group.id)).to be_truthy
end
it 'returns false when project does not belong to one of the namespaces' do
expect(node.projects_include?(unsynced_project.id)).to be_falsy
end
end
context 'selective sync by shards' do
before do
node.update!(selective_sync_type: 'shards', selective_sync_shards: ['default'])
end
it 'returns true when project belongs to one of the namespaces' do
project_in_synced_shard = create(:project)
expect(node.projects_include?(project_in_synced_shard.id)).to be_truthy
end
it 'returns false when project does not belong to one of the namespaces' do
expect(node.projects_include?(unsynced_project.id)).to eq false
expect(node.projects_include?(unsynced_project.id)).to be_falsy
end
end
end
......@@ -285,28 +305,54 @@ describe GeoNode, type: :model do
let(:nested_group_1) { create(:group, parent: group_1) }
let!(:project_1) { create(:project, group: group_1) }
let!(:project_2) { create(:project, group: nested_group_1) }
let!(:project_3) { create(:project, group: group_2) }
let!(:project_3) { create(:project, group: group_2, repository_storage: 'broken') }
it 'returns all projects without selective sync' do
expect(node.projects).to match_array([project_1, project_2, project_3])
end
it 'returns projects that belong to the namespaces with selective sync' do
node.update_attribute(:namespaces, [group_1, nested_group_1])
it 'returns projects that belong to the namespaces with selective sync by namespace' do
node.update!(selective_sync_type: 'namespaces', namespaces: [group_1, nested_group_1])
expect(node.projects).to match_array([project_1, project_2])
end
it 'returns projects that belong to the shards with selective sync by shard' do
node.update!(selective_sync_type: 'shards', selective_sync_shards: ['default'])
expect(node.projects).to match_array([project_1, project_2])
end
it 'returns nothing if an unrecognised selective sync type is used' do
node.update_attribute(:selective_sync_type, 'unknown')
expect(node.projects).to be_empty
end
end
describe '#selective_sync?' do
it 'returns true when Geo node has namespace restrictions' do
node.update_attribute(:namespaces, [create(:group)])
subject { node.selective_sync? }
it 'returns true when selective sync is by namespaces' do
node.update!(selective_sync_type: 'namespaces')
is_expected.to be_truthy
end
it 'returns true when selective sync is by shards' do
node.update!(selective_sync_type: 'shards')
expect(node.selective_sync?).to be true
is_expected.to be_truthy
end
it 'returns false when Geo node does not have namespace restrictions' do
expect(node.selective_sync?).to be false
it 'returns false when selective sync is disabled' do
node.update!(
selective_sync_type: '',
namespaces: [create(:group)],
selective_sync_shards: ['default']
)
is_expected.to be_falsy
end
end
end
......@@ -132,7 +132,7 @@ describe GeoNodeStatus, :geo do
end
it 'returns the right percentage with group restrictions' do
secondary.update_attribute(:namespaces, [group])
secondary.update!(selective_sync_type: 'namespaces', namespaces: [group])
create(:geo_file_registry, :avatar, file_id: upload_1.id)
create(:geo_file_registry, :avatar, file_id: upload_2.id)
......@@ -191,7 +191,7 @@ describe GeoNodeStatus, :geo do
end
it 'returns the right percentage with group restrictions' do
secondary.update_attribute(:namespaces, [group])
secondary.update!(selective_sync_type: 'namespaces', namespaces: [group])
create(:geo_file_registry, :lfs, file_id: lfs_object_project.lfs_object_id, success: true)
expect(subject.lfs_objects_synced_in_percentage).to be_within(0.0001).of(50)
......@@ -266,7 +266,7 @@ describe GeoNodeStatus, :geo do
end
it 'returns the right number of failed repos with group restrictions' do
secondary.update_attribute(:namespaces, [group])
secondary.update!(selective_sync_type: 'namespaces', namespaces: [group])
expect(subject.repositories_failed_count).to eq(1)
end
......@@ -285,7 +285,7 @@ describe GeoNodeStatus, :geo do
end
it 'returns the right number of failed repos with group restrictions' do
secondary.update_attribute(:namespaces, [group])
secondary.update!(selective_sync_type: 'namespaces', namespaces: [group])
expect(subject.wikis_failed_count).to eq(1)
end
......@@ -309,7 +309,7 @@ describe GeoNodeStatus, :geo do
end
it 'returns the right percentage with group restrictions' do
secondary.update_attribute(:namespaces, [group])
secondary.update!(selective_sync_type: 'namespaces', namespaces: [group])
create(:geo_project_registry, :synced, project: project_1)
expect(subject.repositories_synced_in_percentage).to be_within(0.0001).of(50)
......@@ -336,7 +336,7 @@ describe GeoNodeStatus, :geo do
end
it 'returns the right percentage with group restrictions' do
secondary.update_attribute(:namespaces, [group])
secondary.update!(selective_sync_type: 'namespaces', namespaces: [group])
create(:geo_project_registry, :synced, project: project_1)
expect(subject.wikis_synced_in_percentage).to be_within(0.0001).of(50)
......
require 'spec_helper'
describe Geo::NodeUpdateService do
let(:groups) { create_list(:group, 2) }
let!(:primary) { create(:geo_node, :primary) }
set(:primary) { create(:geo_node, :primary) }
let(:geo_node) { create(:geo_node) }
let(:geo_node_with_restrictions) { create(:geo_node, namespace_ids: [groups.first.id]) }
let(:groups) { create_list(:group, 2) }
let(:namespace_ids) { groups.map(&:id).join(',') }
describe '#execute' do
it 'updates the node' do
......@@ -31,22 +32,58 @@ describe Geo::NodeUpdateService do
expect(service.execute).to eq false
end
it 'logs an event to the Geo event log when namespaces change' do
service = described_class.new(geo_node, namespace_ids: groups.map(&:id).join(','))
context 'selective sync disabled' do
it 'does not log an event to the Geo event log when adding restrictions' do
service = described_class.new(geo_node, namespace_ids: namespace_ids, selective_sync_shards: ['default'])
expect { service.execute }.to change(Geo::RepositoriesChangedEvent, :count).by(1)
expect { service.execute }.not_to change(Geo::RepositoriesChangedEvent, :count)
end
end
it 'does not log an event to the Geo event log when removing namespace restrictions' do
service = described_class.new(geo_node_with_restrictions, namespace_ids: '')
context 'selective sync by namespaces' do
let(:restricted_geo_node) { create(:geo_node, selective_sync_type: 'namespaces', namespaces: [create(:group)]) }
it 'logs an event to the Geo event log when adding namespace restrictions' do
service = described_class.new(restricted_geo_node, namespace_ids: namespace_ids)
expect { service.execute }.to change(Geo::RepositoriesChangedEvent, :count).by(1)
end
it 'does not log an event to the Geo event log when removing namespace restrictions' do
service = described_class.new(restricted_geo_node, namespace_ids: '')
expect { service.execute }.not_to change(Geo::RepositoriesChangedEvent, :count)
end
expect { service.execute }.not_to change(Geo::RepositoriesChangedEvent, :count)
it 'does not log an event to the Geo event log when node is a primary node' do
primary.update!(selective_sync_type: 'namespaces')
service = described_class.new(primary, namespace_ids: namespace_ids)
expect { service.execute }.not_to change(Geo::RepositoriesChangedEvent, :count)
end
end
it 'does not log an event to the Geo event log when node is a primary node' do
service = described_class.new(primary, namespace_ids: groups.map(&:id).join(','))
context 'selective sync by shards' do
let(:restricted_geo_node) { create(:geo_node, selective_sync_type: 'shards', selective_sync_shards: ['default']) }
it 'logs an event to the Geo event log when adding shard restrictions' do
service = described_class.new(restricted_geo_node, selective_sync_shards: %w[default broken])
expect { service.execute }.to change(Geo::RepositoriesChangedEvent, :count).by(1)
end
it 'does not log an event to the Geo event log when removing shard restrictions' do
service = described_class.new(restricted_geo_node, selective_sync_shards: [])
expect { service.execute }.not_to change(Geo::RepositoriesChangedEvent, :count)
end
it 'does not log an event to the Geo event log when node is a primary node' do
primary.update!(selective_sync_type: 'shards')
service = described_class.new(primary, selective_sync_shards: %w[default broken'])
expect { service.execute }.not_to change(Geo::RepositoriesChangedEvent, :count)
expect { service.execute }.not_to change(Geo::RepositoriesChangedEvent, :count)
end
end
end
end
......@@ -181,7 +181,7 @@ describe Geo::FileDownloadDispatchWorker, :geo do
before do
allow(ProjectCacheWorker).to receive(:perform_async).and_return(true)
secondary.update_attribute(:namespaces, [synced_group])
secondary.update!(selective_sync_type: 'namespaces', namespaces: [synced_group])
end
it 'does not perform Geo::FileDownloadWorker for LFS object that does not belong to selected namespaces to replicate' do
......
......@@ -10,7 +10,7 @@ describe Geo::RepositoriesCleanUpWorker do
context 'when node has namespace restrictions' do
let(:synced_group) { create(:group) }
let(:geo_node) { create(:geo_node, namespaces: [synced_group]) }
let(:geo_node) { create(:geo_node, selective_sync_type: 'namespaces', namespaces: [synced_group]) }
context 'legacy storage' do
it 'performs GeoRepositoryDestroyWorker for each project that does not belong to selected namespaces to replicate' do
......
......@@ -104,7 +104,7 @@ describe Geo::RepositoryShardSyncWorker, :geo, :delete, :clean_gitlab_redis_cach
context 'when node has namespace restrictions' do
before do
secondary.update_attribute(:namespaces, [synced_group])
secondary.update!(selective_sync_type: 'namespaces', namespaces: [synced_group])
end
it 'does not perform Geo::ProjectSyncWorker for projects that do not belong to selected namespaces to replicate' do
......
......@@ -9,6 +9,8 @@ describe Geo::RepositorySyncWorker, :geo, :clean_gitlab_redis_cache do
let!(:project_in_synced_group) { create(:project, group: synced_group) }
let!(:unsynced_project) { create(:project) }
let(:healthy_shard) { project_in_synced_group.repository.storage }
subject { described_class.new }
before do
......@@ -32,6 +34,21 @@ describe Geo::RepositorySyncWorker, :geo, :clean_gitlab_redis_cache do
Sidekiq::Testing.inline! { subject.perform }
end
it 'skips backfill for projects on shards excluded by selective sync' do
secondary.update!(selective_sync_type: 'shards', selective_sync_shards: [healthy_shard])
# Report both shards as healthy
expect(Gitlab::HealthChecks::FsShardsCheck).to receive(:readiness)
.and_return([result(true, healthy_shard), result(true, 'broken')])
expect(Gitlab::HealthChecks::GitalyCheck).to receive(:readiness)
.and_return([result(true, healthy_shard), result(true, 'broken')])
expect(Geo::RepositoryShardSyncWorker).to receive(:perform_async).with('default')
expect(Geo::RepositoryShardSyncWorker).not_to receive(:perform_async).with('broken')
Sidekiq::Testing.inline! { subject.perform }
end
it 'skips backfill for projects on missing shards' do
missing_not_synced = create(:project, group: synced_group)
missing_not_synced.update_column(:repository_storage, 'unknown')
......@@ -52,17 +69,14 @@ describe Geo::RepositorySyncWorker, :geo, :clean_gitlab_redis_cache do
it 'skips backfill for projects with downed Gitaly server' do
create(:project, group: synced_group, repository_storage: 'broken')
unhealthy_dirty = create(:project, group: synced_group, repository_storage: 'broken')
healthy_shard = project_in_synced_group.repository.storage
create(:geo_project_registry, :synced, :repository_dirty, project: unhealthy_dirty)
# Report only one healthy shard
allow(Gitlab::HealthChecks::FsShardsCheck).to receive(:readiness).and_return(
[Gitlab::HealthChecks::Result.new(true, nil, { shard: healthy_shard }),
Gitlab::HealthChecks::Result.new(true, nil, { shard: 'broken' })])
allow(Gitlab::HealthChecks::GitalyCheck).to receive(:readiness).and_return(
[Gitlab::HealthChecks::Result.new(true, nil, { shard: healthy_shard }),
Gitlab::HealthChecks::Result.new(false, nil, { shard: 'broken' })])
expect(Gitlab::HealthChecks::FsShardsCheck).to receive(:readiness)
.and_return([result(true, healthy_shard), result(true, 'broken')])
expect(Gitlab::HealthChecks::GitalyCheck).to receive(:readiness)
.and_return([result(true, healthy_shard), result(false, 'broken')])
expect(Geo::RepositoryShardSyncWorker).to receive(:perform_async).with(healthy_shard)
expect(Geo::RepositoryShardSyncWorker).not_to receive(:perform_async).with('broken')
......@@ -71,4 +85,8 @@ describe Geo::RepositorySyncWorker, :geo, :clean_gitlab_redis_cache do
end
end
end
def result(success, shard)
Gitlab::HealthChecks::Result.new(success, nil, { shard: shard })
end
end
......@@ -6,13 +6,13 @@ import { mockNodeDetails } from '../mock_data';
import mountComponent from '../../helpers/vue_mount_component_helper';
const createComponent = (
namespaces = mockNodeDetails.namespaces,
selectiveSyncType = mockNodeDetails.selectiveSyncType,
lastEvent = mockNodeDetails.lastEvent,
cursorLastEvent = mockNodeDetails.cursorLastEvent) => {
const Component = Vue.extend(geoNodeSyncSettingsComponent);
return mountComponent(Component, {
namespaces,
selectiveSyncType,
lastEvent,
cursorLastEvent,
});
......@@ -23,7 +23,7 @@ describe('GeoNodeSyncSettingsComponent', () => {
describe('syncType', () => {
it('returns string representing sync type', () => {
const vm = createComponent();
expect(vm.syncType).toBe('Selective');
expect(vm.syncType).toBe('Selective (namespaces)');
vm.$destroy();
});
});
......
......@@ -66,6 +66,7 @@ export const rawMockNodeDetails = {
last_successful_status_check_timestamp: 1515142330,
version: '10.4.0-pre',
revision: 'b93c51849b',
selective_sync_type: 'namespaces',
namespaces: [
{
id: 54,
......@@ -151,31 +152,6 @@ export const mockNodeDetails = {
id: 3,
timeStamp: 1511255200,
},
namespaces: [
{
id: 54,
name: 'platform',
path: 'platform',
kind: 'group',
full_path: 'platform',
parent_id: null,
},
{
id: 4,
name: 'Twitter',
path: 'twitter',
kind: 'group',
full_path: 'twitter',
parent_id: null,
},
{
id: 3,
name: 'Documentcloud',
path: 'documentcloud',
kind: 'group',
full_path: 'documentcloud',
parent_id: null,
},
],
selectiveSyncType: 'namespaces',
dbReplicationLag: 0,
};
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