Commit 971662e6 authored by Robert Speicher's avatar Robert Speicher

Merge branch 'stanhu/gitlab-ce-add-eager-load-lib' into 'master'

Add eager load paths to help prevent dependency load issues with Sidekiq workers

_Originally opened at !3545 by @stanhu._

- - -

Relevant resources:

- https://github.com/mperham/sidekiq/wiki/FAQ#why-doesnt-sidekiq-autoload-my-rails-application-code
- https://github.com/mperham/sidekiq/issues/1281#issuecomment-27129904
- http://blog.arkency.com/2014/11/dont-forget-about-eager-load-when-extending-autoload
- https://github.com/rails/rails/blob/52ce6ece8c8f74064bb64e0a0b1ddd83092718e1/railties/lib/rails/engine.rb#L472-L479
- https://github.com/rails/rails/blob/v4.2.6/railties/lib/rails/paths.rb

Attempts to address #3661, #11896, #12769, #13521, #14131, #14589, #14759, #14825.

See merge request !3724
parents 634f02b0 5589dcf8
...@@ -32,6 +32,7 @@ v 8.8.0 (unreleased) ...@@ -32,6 +32,7 @@ v 8.8.0 (unreleased)
- API: Expose Issue#user_notes_count. !3126 (Anton Popov) - API: Expose Issue#user_notes_count. !3126 (Anton Popov)
- Files over 5MB can only be viewed in their raw form, files over 1MB without highlighting !3718 - Files over 5MB can only be viewed in their raw form, files over 1MB without highlighting !3718
- Add support for supressing text diffs using .gitattributes on the default branch (Matt Oakes) - Add support for supressing text diffs using .gitattributes on the default branch (Matt Oakes)
- Add eager load paths to help prevent dependency load issues in Sidekiq workers. !3724
- Added multiple colors for labels in dropdowns when dups happen. - Added multiple colors for labels in dropdowns when dups happen.
- Improve description for the Two-factor Authentication sign-in screen. (Connor Shea) - Improve description for the Two-factor Authentication sign-in screen. (Connor Shea)
- API support for the 'since' and 'until' operators on commit requests (Paco Guzman) - API support for the 'since' and 'until' operators on commit requests (Paco Guzman)
......
...@@ -81,7 +81,7 @@ class Repository ...@@ -81,7 +81,7 @@ class Repository
def commit(id = 'HEAD') def commit(id = 'HEAD')
return nil unless exists? return nil unless exists?
commit = Gitlab::Git::Commit.find(raw_repository, id) commit = Gitlab::Git::Commit.find(raw_repository, id)
commit = Commit.new(commit, @project) if commit commit = ::Commit.new(commit, @project) if commit
commit commit
rescue Rugged::OdbError rescue Rugged::OdbError
nil nil
......
require File.expand_path('../boot', __FILE__) require File.expand_path('../boot', __FILE__)
require 'rails/all' require 'rails/all'
require 'devise'
I18n.config.enforce_available_locales = false
Bundler.require(:default, Rails.env) Bundler.require(:default, Rails.env)
require_relative '../lib/gitlab/redis'
module Gitlab module Gitlab
class Application < Rails::Application class Application < Rails::Application
require_dependency Rails.root.join('lib/gitlab/redis')
# Settings in config/environments/* take precedence over those specified here. # Settings in config/environments/* take precedence over those specified here.
# Application configuration should go into files in config/initializers # Application configuration should go into files in config/initializers
# -- all .rb files in that directory are automatically loaded. # -- all .rb files in that directory are automatically loaded.
# Custom directories with classes and modules you want to be autoloadable. # Sidekiq uses eager loading, but directories not in the standard Rails
config.autoload_paths.push(*%W(#{config.root}/lib # directories must be added to the eager load paths:
#{config.root}/app/models/hooks # https://github.com/mperham/sidekiq/wiki/FAQ#why-doesnt-sidekiq-autoload-my-rails-application-code
#{config.root}/app/models/concerns # Also, there is no need to add `lib` to autoload_paths since autoloading is
#{config.root}/app/models/project_services # configured to check for eager loaded paths:
#{config.root}/app/models/members)) # https://github.com/rails/rails/blob/v4.2.6/railties/lib/rails/engine.rb#L687
# This is a nice reference article on autoloading/eager loading:
# http://blog.arkency.com/2014/11/dont-forget-about-eager-load-when-extending-autoload
config.eager_load_paths.push(*%W(#{config.root}/lib
#{config.root}/app/models/ci
#{config.root}/app/models/hooks
#{config.root}/app/models/members
#{config.root}/app/models/project_services))
# Only load the plugins named here, in the order given (default is alphabetical). # Only load the plugins named here, in the order given (default is alphabetical).
# :all can be used as a placeholder for all plugins not explicitly named. # :all can be used as a placeholder for all plugins not explicitly named.
......
require 'gitlab' # Load lib/gitlab.rb as soon as possible require_dependency Rails.root.join('lib/gitlab') # Load Gitlab as soon as possible
class Settings < Settingslogic class Settings < Settingslogic
source ENV.fetch('GITLAB_CONFIG') { "#{Rails.root}/config/gitlab.yml" } source ENV.fetch('GITLAB_CONFIG') { "#{Rails.root}/config/gitlab.yml" }
......
# GIT over HTTP # GIT over HTTP
require Rails.root.join("lib", "gitlab", "backend", "grack_auth") require_dependency Rails.root.join('lib/gitlab/backend/grack_auth')
# GIT over SSH # GIT over SSH
require Rails.root.join("lib", "gitlab", "backend", "shell") require_dependency Rails.root.join('lib/gitlab/backend/shell')
# GitLab shell adapter # GitLab shell adapter
require Rails.root.join("lib", "gitlab", "backend", "shell_adapter") require_dependency Rails.root.join('lib/gitlab/backend/shell_adapter')
required_version = Gitlab::VersionInfo.parse(Gitlab::Shell.version_required) required_version = Gitlab::VersionInfo.parse(Gitlab::Shell.version_required)
current_version = Gitlab::VersionInfo.parse(Gitlab::Shell.new.version) current_version = Gitlab::VersionInfo.parse(Gitlab::Shell.new.version)
......
Dir["#{Rails.root}/lib/api/*.rb"].each {|file| require file}
module API module API
class API < Grape::API class API < Grape::API
include APIGuard include APIGuard
...@@ -25,38 +23,39 @@ module API ...@@ -25,38 +23,39 @@ module API
format :json format :json
content_type :txt, "text/plain" content_type :txt, "text/plain"
helpers Helpers # Ensure the namespace is right, otherwise we might load Grape::API::Helpers
helpers ::API::Helpers
mount Groups
mount GroupMembers mount ::API::Groups
mount Users mount ::API::GroupMembers
mount Projects mount ::API::Users
mount Repositories mount ::API::Projects
mount Issues mount ::API::Repositories
mount Milestones mount ::API::Issues
mount Session mount ::API::Milestones
mount MergeRequests mount ::API::Session
mount Notes mount ::API::MergeRequests
mount Internal mount ::API::Notes
mount SystemHooks mount ::API::Internal
mount ProjectSnippets mount ::API::SystemHooks
mount ProjectMembers mount ::API::ProjectSnippets
mount DeployKeys mount ::API::ProjectMembers
mount ProjectHooks mount ::API::DeployKeys
mount Services mount ::API::ProjectHooks
mount Files mount ::API::Services
mount Commits mount ::API::Files
mount CommitStatus mount ::API::Commits
mount Namespaces mount ::API::CommitStatuses
mount Branches mount ::API::Namespaces
mount Labels mount ::API::Branches
mount Settings mount ::API::Labels
mount Keys mount ::API::Settings
mount Tags mount ::API::Keys
mount Triggers mount ::API::Tags
mount Builds mount ::API::Triggers
mount Variables mount ::API::Builds
mount Runners mount ::API::Variables
mount Licenses mount ::API::Runners
mount ::API::Licenses
end end
end end
...@@ -2,171 +2,175 @@ ...@@ -2,171 +2,175 @@
require 'rack/oauth2' require 'rack/oauth2'
module APIGuard module API
extend ActiveSupport::Concern module APIGuard
extend ActiveSupport::Concern
included do |base| included do |base|
# OAuth2 Resource Server Authentication # OAuth2 Resource Server Authentication
use Rack::OAuth2::Server::Resource::Bearer, 'The API' do |request| use Rack::OAuth2::Server::Resource::Bearer, 'The API' do |request|
# The authenticator only fetches the raw token string # The authenticator only fetches the raw token string
# Must yield access token to store it in the env # Must yield access token to store it in the env
request.access_token request.access_token
end end
helpers HelperMethods helpers HelperMethods
install_error_responders(base) install_error_responders(base)
end end
# Helper Methods for Grape Endpoint # Helper Methods for Grape Endpoint
module HelperMethods module HelperMethods
# Invokes the doorkeeper guard. # Invokes the doorkeeper guard.
# #
# If token is presented and valid, then it sets @current_user. # If token is presented and valid, then it sets @current_user.
# #
# If the token does not have sufficient scopes to cover the requred scopes, # If the token does not have sufficient scopes to cover the requred scopes,
# then it raises InsufficientScopeError. # then it raises InsufficientScopeError.
# #
# If the token is expired, then it raises ExpiredError. # If the token is expired, then it raises ExpiredError.
# #
# If the token is revoked, then it raises RevokedError. # If the token is revoked, then it raises RevokedError.
# #
# If the token is not found (nil), then it raises TokenNotFoundError. # If the token is not found (nil), then it raises TokenNotFoundError.
# #
# Arguments: # Arguments:
# #
# scopes: (optional) scopes required for this guard. # scopes: (optional) scopes required for this guard.
# Defaults to empty array. # Defaults to empty array.
# #
def doorkeeper_guard!(scopes: []) def doorkeeper_guard!(scopes: [])
if (access_token = find_access_token).nil? if (access_token = find_access_token).nil?
raise TokenNotFoundError raise TokenNotFoundError
else else
case validate_access_token(access_token, scopes) case validate_access_token(access_token, scopes)
when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE
raise InsufficientScopeError.new(scopes) raise InsufficientScopeError.new(scopes)
when Oauth2::AccessTokenValidationService::EXPIRED when Oauth2::AccessTokenValidationService::EXPIRED
raise ExpiredError raise ExpiredError
when Oauth2::AccessTokenValidationService::REVOKED when Oauth2::AccessTokenValidationService::REVOKED
raise RevokedError raise RevokedError
when Oauth2::AccessTokenValidationService::VALID when Oauth2::AccessTokenValidationService::VALID
@current_user = User.find(access_token.resource_owner_id) @current_user = User.find(access_token.resource_owner_id)
end
end end
end end
end
def doorkeeper_guard(scopes: []) def doorkeeper_guard(scopes: [])
if access_token = find_access_token if access_token = find_access_token
case validate_access_token(access_token, scopes) case validate_access_token(access_token, scopes)
when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE
raise InsufficientScopeError.new(scopes) raise InsufficientScopeError.new(scopes)
when Oauth2::AccessTokenValidationService::EXPIRED when Oauth2::AccessTokenValidationService::EXPIRED
raise ExpiredError raise ExpiredError
when Oauth2::AccessTokenValidationService::REVOKED when Oauth2::AccessTokenValidationService::REVOKED
raise RevokedError raise RevokedError
when Oauth2::AccessTokenValidationService::VALID when Oauth2::AccessTokenValidationService::VALID
@current_user = User.find(access_token.resource_owner_id) @current_user = User.find(access_token.resource_owner_id)
end
end end
end end
end
def current_user def current_user
@current_user @current_user
end end
private private
def find_access_token
@access_token ||= Doorkeeper.authenticate(doorkeeper_request, Doorkeeper.configuration.access_token_methods)
end
def doorkeeper_request def find_access_token
@doorkeeper_request ||= ActionDispatch::Request.new(env) @access_token ||= Doorkeeper.authenticate(doorkeeper_request, Doorkeeper.configuration.access_token_methods)
end end
def validate_access_token(access_token, scopes) def doorkeeper_request
Oauth2::AccessTokenValidationService.validate(access_token, scopes: scopes) @doorkeeper_request ||= ActionDispatch::Request.new(env)
end end
end
module ClassMethods def validate_access_token(access_token, scopes)
# Installs the doorkeeper guard on the whole Grape API endpoint. Oauth2::AccessTokenValidationService.validate(access_token, scopes: scopes)
#
# Arguments:
#
# scopes: (optional) scopes required for this guard.
# Defaults to empty array.
#
def guard_all!(scopes: [])
before do
guard! scopes: scopes
end end
end end
private module ClassMethods
def install_error_responders(base) # Installs the doorkeeper guard on the whole Grape API endpoint.
error_classes = [ MissingTokenError, TokenNotFoundError, #
ExpiredError, RevokedError, InsufficientScopeError] # Arguments:
#
# scopes: (optional) scopes required for this guard.
# Defaults to empty array.
#
def guard_all!(scopes: [])
before do
guard! scopes: scopes
end
end
base.send :rescue_from, *error_classes, oauth2_bearer_token_error_handler private
end
def oauth2_bearer_token_error_handler def install_error_responders(base)
Proc.new do |e| error_classes = [ MissingTokenError, TokenNotFoundError,
response = ExpiredError, RevokedError, InsufficientScopeError]
case e
when MissingTokenError
Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new
when TokenNotFoundError
Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new(
:invalid_token,
"Bad Access Token.")
when ExpiredError
Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new(
:invalid_token,
"Token is expired. You can either do re-authorization or token refresh.")
when RevokedError
Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new(
:invalid_token,
"Token was revoked. You have to re-authorize from the user.")
when InsufficientScopeError
# FIXME: ForbiddenError (inherited from Bearer::Forbidden of Rack::Oauth2)
# does not include WWW-Authenticate header, which breaks the standard.
Rack::OAuth2::Server::Resource::Bearer::Forbidden.new(
:insufficient_scope,
Rack::OAuth2::Server::Resource::ErrorMethods::DEFAULT_DESCRIPTION[:insufficient_scope],
{ scope: e.scopes })
end
response.finish base.send :rescue_from, *error_classes, oauth2_bearer_token_error_handler
end
def oauth2_bearer_token_error_handler
Proc.new do |e|
response =
case e
when MissingTokenError
Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new
when TokenNotFoundError
Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new(
:invalid_token,
"Bad Access Token.")
when ExpiredError
Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new(
:invalid_token,
"Token is expired. You can either do re-authorization or token refresh.")
when RevokedError
Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new(
:invalid_token,
"Token was revoked. You have to re-authorize from the user.")
when InsufficientScopeError
# FIXME: ForbiddenError (inherited from Bearer::Forbidden of Rack::Oauth2)
# does not include WWW-Authenticate header, which breaks the standard.
Rack::OAuth2::Server::Resource::Bearer::Forbidden.new(
:insufficient_scope,
Rack::OAuth2::Server::Resource::ErrorMethods::DEFAULT_DESCRIPTION[:insufficient_scope],
{ scope: e.scopes })
end
response.finish
end
end end
end end
end
# #
# Exceptions # Exceptions
# #
class MissingTokenError < StandardError; end class MissingTokenError < StandardError; end
class TokenNotFoundError < StandardError; end class TokenNotFoundError < StandardError; end
class ExpiredError < StandardError; end class ExpiredError < StandardError; end
class RevokedError < StandardError; end class RevokedError < StandardError; end
class InsufficientScopeError < StandardError class InsufficientScopeError < StandardError
attr_reader :scopes attr_reader :scopes
def initialize(scopes) def initialize(scopes)
@scopes = scopes @scopes = scopes
end
end end
end end
end end
...@@ -2,7 +2,7 @@ require 'mime/types' ...@@ -2,7 +2,7 @@ require 'mime/types'
module API module API
# Project commit statuses API # Project commit statuses API
class CommitStatus < Grape::API class CommitStatuses < Grape::API
resource :projects do resource :projects do
before { authenticate! } before { authenticate! }
......
Dir["#{Rails.root}/lib/ci/api/*.rb"].each {|file| require file}
module Ci module Ci
module API module API
class API < Grape::API class API < Grape::API
include APIGuard include ::API::APIGuard
version 'v1', using: :path version 'v1', using: :path
rescue_from ActiveRecord::RecordNotFound do rescue_from ActiveRecord::RecordNotFound do
...@@ -31,9 +29,9 @@ module Ci ...@@ -31,9 +29,9 @@ module Ci
helpers ::API::Helpers helpers ::API::Helpers
helpers Gitlab::CurrentSettings helpers Gitlab::CurrentSettings
mount Builds mount ::Ci::API::Builds
mount Runners mount ::Ci::API::Runners
mount Triggers mount ::Ci::API::Triggers
end end
end end
end end
require 'gitlab/git' require_dependency 'gitlab/git'
module Gitlab module Gitlab
def self.com? def self.com?
......
require 'spec_helper' require 'spec_helper'
describe API::CommitStatus, api: true do describe API::CommitStatuses, api: true do
include ApiHelpers include ApiHelpers
let!(:project) { create(:project) } let!(:project) { create(:project) }
......
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