Commit a2750882 authored by Douglas Barbosa Alexandre's avatar Douglas Barbosa Alexandre

Merge branch 'mk/add-secondary-lag-message-on-http-push-geo' into 'master'

Add secondary lag message on Git push over HTTP

See merge request gitlab-org/gitlab!15901
parents 0b623c08 517d6ab6
resources :projects, only: [:index, :new, :create]
Gitlab.ee do
scope "/-/push_from_secondary/:geo_node_id" do
draw :git_http
end
end
draw :git_http
get '/projects/:id' => 'projects#resolve'
......
......@@ -155,7 +155,13 @@ module EE
end
def primary_full_url
::Gitlab::Utils.append_path(::Gitlab::Geo.primary_node.internal_url, request_fullpath_for_primary)
path = File.join(secondary_referrer_path_prefix, request_fullpath_for_primary)
::Gitlab::Utils.append_path(::Gitlab::Geo.primary_node.internal_url, path)
end
def secondary_referrer_path_prefix
File.join(::Gitlab::Geo::GitPushHttp::PATH_PREFIX, ::Gitlab::Geo.current_node.id.to_s)
end
def redirect?
......
......@@ -13,6 +13,21 @@ module EE
render json: ::Gitlab::Workhorse.git_http_ok(repository, repo_type, user, action_name, show_all_refs: geo_request?)
end
override :git_receive_pack
def git_receive_pack
# Authentication/authorization already happened in `before_action`s
if ::Gitlab::Geo.primary?
# This ID is used by the /internal/post_receive API call
gl_id = ::Gitlab::GlId.gl_id(user)
gl_repository = repo_type.identifier_for_subject(project)
node_id = params["geo_node_id"]
::Gitlab::Geo::GitPushHttp.new(gl_id, gl_repository).cache_referrer_node(node_id)
end
super
end
private
def user
......
---
title: Add secondary lag message on Git push over HTTP
merge_request: 15901
author:
type: added
......@@ -14,6 +14,26 @@ module EE
def lfs_authentication_url(project)
project.lfs_http_url_to_repo(params[:operation])
end
override :ee_post_receive_response_hook
def ee_post_receive_response_hook(response)
response.add_basic_message(geo_secondary_lag_message) if ::Gitlab::Geo.primary?
end
def geo_secondary_lag_message
lag = current_replication_lag
return if lag.to_i <= 0
"Current replication lag: #{lag} seconds"
end
def current_replication_lag
fetch_geo_node_referrer&.status&.db_replication_lag_seconds
end
def fetch_geo_node_referrer
::Gitlab::Geo::GitPushHttp.new(params[:identifier], params[:gl_repository]).fetch_referrer_node
end
end
end
end
......
# frozen_string_literal: true
module Gitlab
module Geo
class GitPushHttp
PATH_PREFIX = '/-/push_from_secondary'
CACHE_KEY_PREFIX = 'git_receive_pack:geo_node_id'
EXPIRES_IN = 5.minutes
def initialize(gl_id, gl_repository)
@gl_id = gl_id
@gl_repository = gl_repository
end
def cache_referrer_node(geo_node_id)
geo_node_id = geo_node_id.to_i
return unless geo_node_id > 0
Rails.cache.write(cache_key, geo_node_id, expires_in: EXPIRES_IN)
end
def fetch_referrer_node
id = Rails.cache.read(cache_key)
if id
# There is a race condition but since this is only used to display a
# notice, it's ok. If we didn't delete it, then a subsequent push
# directly to the primary would inappropriately show the secondary lag
# notice again.
Rails.cache.delete(cache_key)
GeoNode.find_by_id(id)
end
end
private
def cache_key
[
CACHE_KEY_PREFIX,
@gl_id,
@gl_repository
].join(':')
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Geo::GitPushHttp, :geo, :use_clean_rails_memory_store_caching do
include EE::GeoHelpers
let(:gl_id) { 'user-1234' }
let(:gl_repository) { 'project-77777' }
let(:cache_key) { "#{described_class::CACHE_KEY_PREFIX}:#{gl_id}:#{gl_repository}" }
set(:secondary) { create(:geo_node) }
subject { described_class.new(gl_id, gl_repository) }
describe '#cache_referrer_node' do
context 'when geo_node_id is present' do
context 'when geo_node_id is an integer' do
it 'stores the ID in cache' do
subject.cache_referrer_node(secondary.id)
value = Rails.cache.read(cache_key)
expect(value).to eq(secondary.id)
end
it 'stores the ID with an expiration' do
Timecop.freeze do
subject.cache_referrer_node(secondary.id)
Timecop.travel(described_class::EXPIRES_IN + 20.seconds) do
value = Rails.cache.read(cache_key)
expect(value).to be_nil
end
end
end
end
context 'when geo_node_id is not an integer' do
it 'does not cache anything' do
subject.cache_referrer_node('bad input')
value = Rails.cache.read(cache_key)
expect(value).to be_nil
end
end
end
context 'when geo_node_id is blank' do
it 'does not cache anything' do
subject.cache_referrer_node(' ')
value = Rails.cache.read(cache_key)
expect(value).to be_nil
end
end
end
describe '#fetch_referrer_node' do
context 'when there is a cached ID' do
it 'deletes the key' do
Rails.cache.write(cache_key, secondary.id, expires_in: described_class::EXPIRES_IN)
subject.fetch_referrer_node
expect(subject.fetch_referrer_node).to be_nil
end
context 'when the GeoNode exists' do
it 'returns the GeoNode with the cached ID' do
Rails.cache.write(cache_key, secondary.id, expires_in: described_class::EXPIRES_IN)
expect(subject.fetch_referrer_node).to eq(secondary)
end
end
context 'when the GeoNode does not exist' do
it 'returns nil' do
Rails.cache.write(cache_key, 9999998, expires_in: described_class::EXPIRES_IN)
expect(subject.fetch_referrer_node).to be_nil
end
end
end
context 'when there is no cached ID' do
it 'returns nil' do
expect(subject.fetch_referrer_node).to be_nil
end
end
end
end
......@@ -4,6 +4,113 @@ require 'spec_helper'
describe API::Internal::Base do
include EE::GeoHelpers
set(:primary_node) { create(:geo_node, :primary) }
set(:secondary_node) { create(:geo_node) }
describe 'POST /internal/post_receive', :geo do
set(:user) { create(:user) }
let(:key) { create(:key, user: user) }
set(:project) { create(:project, :repository, :wiki_repo) }
let(:secret_token) { Gitlab::Shell.secret_token }
let(:gl_repository) { "project-#{project.id}" }
let(:reference_counter) { double('ReferenceCounter') }
let(:identifier) { 'key-123' }
let(:valid_params) do
{
gl_repository: gl_repository,
secret_token: secret_token,
identifier: identifier,
changes: changes,
push_options: {}
}
end
let(:branch_name) { 'feature' }
let(:changes) do
"#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{branch_name}"
end
let(:git_push_http) { double('GitPushHttp') }
before do
project.add_developer(user)
allow(described_class).to receive(:identify).and_return(user)
allow_any_instance_of(Gitlab::Identifier).to receive(:identify).and_return(user)
stub_current_geo_node(primary_node)
end
context 'when the push was redirected from a Geo secondary to the primary' do
before do
expect(Gitlab::Geo::GitPushHttp).to receive(:new).with(identifier, gl_repository).and_return(git_push_http)
expect(git_push_http).to receive(:fetch_referrer_node).and_return(secondary_node)
end
context 'when the secondary has a GeoNodeStatus' do
context 'when the GeoNodeStatus db_replication_lag_seconds is greater than 0' do
let!(:status) { create(:geo_node_status, geo_node: secondary_node, db_replication_lag_seconds: 17) }
it 'includes current Geo secondary lag in the output' do
post api('/internal/post_receive'), params: valid_params
expect(response).to have_gitlab_http_status(200)
expect(json_response['messages']).to include({
'type' => 'basic',
'message' => "Current replication lag: 17 seconds"
})
end
end
context 'when the GeoNodeStatus db_replication_lag_seconds is 0' do
let!(:status) { create(:geo_node_status, geo_node: secondary_node, db_replication_lag_seconds: 0) }
it 'does not include current Geo secondary lag in the output' do
post api('/internal/post_receive'), params: valid_params
expect(response).to have_gitlab_http_status(200)
expect(json_response['messages']).not_to include({ 'message' => a_string_matching('replication lag'), 'type' => anything })
end
end
context 'when the GeoNodeStatus db_replication_lag_seconds is nil' do
let!(:status) { create(:geo_node_status, geo_node: secondary_node, db_replication_lag_seconds: nil) }
it 'does not include current Geo secondary lag in the output' do
post api('/internal/post_receive'), params: valid_params
expect(response).to have_gitlab_http_status(200)
expect(json_response['messages']).not_to include({ 'message' => a_string_matching('replication lag'), 'type' => anything })
end
end
end
context 'when the secondary does not have a GeoNodeStatus' do
it 'does not include current Geo secondary lag in the output' do
post api('/internal/post_receive'), params: valid_params
expect(response).to have_gitlab_http_status(200)
expect(json_response['messages']).not_to include({ 'message' => a_string_matching('replication lag'), 'type' => anything })
end
end
end
context 'when the push was not redirected from a Geo secondary to the primary' do
before do
expect(Gitlab::Geo::GitPushHttp).to receive(:new).with(identifier, gl_repository).and_return(git_push_http)
expect(git_push_http).to receive(:fetch_referrer_node).and_return(nil)
end
it 'does not include current Geo secondary lag in the output' do
post api('/internal/post_receive'), params: valid_params
expect(response).to have_gitlab_http_status(200)
expect(json_response['messages']).not_to include({ 'message' => a_string_matching('replication lag'), 'type' => anything })
end
end
end
describe "POST /internal/allowed" do
set(:user) { create(:user) }
set(:key) { create(:key, user: user) }
......@@ -147,17 +254,14 @@ describe API::Internal::Base do
end
end
describe "POST /internal/lfs_authenticate" do
describe "POST /internal/lfs_authenticate", :geo do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:secret_token) { Gitlab::Shell.secret_token }
context 'for a secondary node' do
let!(:primary) { create(:geo_node, :primary) }
let!(:secondary) { create(:geo_node) }
before do
stub_current_geo_node(secondary)
stub_current_geo_node(secondary_node)
project.add_developer(user)
end
......
......@@ -113,7 +113,7 @@ describe "Git HTTP requests (Geo)", :geo do
it 'redirects to the primary' do
is_expected.to have_gitlab_http_status(:redirect)
redirect_location = "#{primary.url.chomp('/')}#{url}?service=git-receive-pack"
redirect_location = "#{redirected_primary_url}?service=git-receive-pack"
expect(subject.header['Location']).to eq(redirect_location)
end
end
......@@ -166,7 +166,7 @@ describe "Git HTTP requests (Geo)", :geo do
it 'redirects to the primary' do
is_expected.to have_gitlab_http_status(:redirect)
redirect_location = "#{primary.url.chomp('/')}#{url}"
redirect_location = "#{redirected_primary_url}"
expect(subject.header['Location']).to eq(redirect_location)
end
end
......@@ -254,19 +254,62 @@ describe "Git HTTP requests (Geo)", :geo do
it 'redirects to the primary' do
is_expected.to have_gitlab_http_status(:redirect)
redirect_location = "#{primary.url.chomp('/')}#{url}"
redirect_location = "#{redirected_primary_url}"
expect(subject.header['Location']).to eq(redirect_location)
end
end
end
end
end
def redirected_primary_url
"#{primary.url.chomp('/')}#{::Gitlab::Geo::GitPushHttp::PATH_PREFIX}/#{secondary.id}#{url}"
end
end
context 'when current node is the primary' do
context 'when current node is the primary', :use_clean_rails_memory_store_caching do
let(:current_node) { primary }
describe 'POST git_receive_pack' do
subject do
make_request
response
end
context 'when HTTP redirected from a secondary node' do
def make_request
post url, headers: auth_env(user.username, user.password, nil)
end
let(:identifier) { "user-#{user.id}" }
let(:gl_repository) { "project-#{project.id}" }
let(:url) { "#{::Gitlab::Geo::GitPushHttp::PATH_PREFIX}/#{secondary.id}/#{project.full_path}.git/git-receive-pack" }
# The bigger picture request flow relevant to this feature is:
#
# * The HTTP request hits NGINX
# * Then Workhorse
# * Then Rails (the scope of request tests is limited to this line item)
# * Rails responds OK to Workhorse
# * Workhorse connects to Gitaly: SmartHTTP Service, ReceivePack RPC
# * In a pre-receive hook, Gitaly makes a request to Rails' POST /api/v4/internal/allowed
# * Rails says OK
# * In a post-receive hook, Gitaly makes a request to Rails' POST /api/v4/internal/post_receive
# * Rails responds to Gitaly, including a collection of messages, which includes the replication lag message
# * Gitaly outputs the messages in the stream of Proto messages
# * Pipe the output through Workhorse and NGINX
#
# See https://gitlab.com/gitlab-org/gitlab-ee/issues/9195
#
it 'stores the secondary node ID so the internal API post_receive request can generate the replication lag message' do
is_expected.to have_gitlab_http_status(:ok)
stored_node = ::Gitlab::Geo::GitPushHttp.new(identifier, gl_repository).fetch_referrer_node
expect(stored_node).to eq(secondary)
end
end
context 'when proxying an SSH request from a secondary node' do
def make_request
post url, params: {}, headers: env
end
......@@ -277,11 +320,6 @@ describe "Git HTTP requests (Geo)", :geo do
env['Geo-GL-Id'] = geo_gl_id
end
subject do
make_request
response
end
context 'when gl_id is incorrectly provided via HTTP headers' do
where(:geo_gl_id) do
[
......@@ -350,6 +388,7 @@ describe "Git HTTP requests (Geo)", :geo do
end
end
end
end
context 'repository does not exist' do
subject do
......
......@@ -22,6 +22,10 @@ module API
# easily.
project.http_url_to_repo
end
def ee_post_receive_response_hook(response)
# Hook for EE to add messages
end
end
namespace 'internal' do
......@@ -265,6 +269,8 @@ module API
response.add_basic_message(project_created_message)
end
ee_post_receive_response_hook(response)
present response, with: Entities::InternalPostReceive::Response
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