GitLab 유틸리티

개발을 보다 쉽게 돕기 위해 여러 유틸리티를 개발했습니다.

MergeHash

merge_hash.rb를 참조하세요:

  • 해시 배열을 깊이 Merge합니다:

    Gitlab::Utils::MergeHash.merge(
      [{ hello: ["world"] },
       { hello: "Everyone" },
       { hello: { greetings: ['Bonjour', 'Hello', 'Hallo', 'Dzien dobry'] } },
        "Goodbye", "Hallo"]
    )
    

    결과:

    [
      {
        hello:
          [
            "world",
            "Everyone",
            { greetings: ['Bonjour', 'Hello', 'Hallo', 'Dzien dobry'] }
          ]
      },
      "Goodbye"
    ]
    
  • 모든 키와 값을 해시에서 배열로 추출합니다:

    Gitlab::Utils::MergeHash.crush(
      { hello: "world", this: { crushes: ["an entire", "hash"] } }
    )
    

    결과:

    [:hello, "world", :this, :crushes, "an entire", "hash"]
    

Override

override.rb를 참조하세요:

  • 이 유틸리티는 한 메소드가 다른 메소드를 덮어쓸지 여부를 확인하는 데 도움을 줍니다. 이는 Java의 @Override 어노테이션이나 Scala의 override 키워드와 같은 개념입니다. 그러나 ENV['STATIC_VERIFICATION']이 설정되어 있는 경우에만 이 확인을 실행합니다. 다음과 같은 확인에 유용합니다:

    • 덮어쓰는 메소드에 오타가 있는지 확인할 때
    • 덮어쓰는 메소드의 이름이 바뀌어 원래의 덮어쓰기 메소드가 무의미해졌는지 확인할 때

    간단한 예제는 다음과 같습니다:

    class Base
      def execute
      end
    end
      
    class Derived < Base
      extend ::Gitlab::Utils::Override
        
      override :execute # 여기서 덮어쓰기 확인이 발생합니다
      def execute
      end
    end
    

    이는 모듈에서도 작동합니다:

    module Extension
      extend ::Gitlab::Utils::Override
        
      override :execute # 모듈에서는 즉시 이 확인을 수행하지 않습니다
      def execute
      end
    end
      
    class Derived < Base
      prepend Extension # 여기서 덮어쓰기 확인이 발생하며, 모듈에서는 확인하지 않습니다
    end
    

    이 확인은 다음 중 하나인 경우에만 발생합니다:

    • 덮어쓰는 메소드가 클래스 내에서 정의된 경우
    • 덮어쓰는 메소드가 모듈 내에서 정의되었으며, 클래스나 다른 모듈에 선행된 경우

    왜냐하면 클래스나 선행된 모듈만이 실제로 메소드를 덮어쓸 수 있기 때문입니다. 모듈을 다른 곳에 포함하거나 확장시키는 것은 아무것도 덮어쓸 수 없습니다.

ActiveSupport::Concern, prepend, class_methods와의 상호작용

클래스 메소드를 포함하는 ActiveSupport::Concern을 사용할 때 일반적인 Ruby 모듈과 다르게 작동하기 때문에 예상한 결과를 얻지 못할 수 있습니다.

우리는 이미 ActiveSupport::Concernprepend를 활성화하기 위해 Prependable을 패치했으므로, 이것이 overrideclass_methods와 어떻게 상호작용할지에 대한 결과가 있습니다. 이러한 우회 방법을 사용하여 지정된 문맥에서 override를 확인할 때 extendClassMethods에 정의된 Prependable 모듈 내부로 가져옵니다.

위의 내용을 보여주는 예제 코드:

module Base
  extend ActiveSupport::Concern
  
  class_methods do
    def f
    end
  end
end

module Derived
  include Base
end

# 우회 방법을 사용하지 않을 때
Base.f    # => NoMethodError
Derived.f # => nil

# 우회 방법을 사용할 때
Base.f    # => nil
Derived.f # => nil

StrongMemoize

strong_memoize.rb를 참조하세요:

  • 값이 nil 또는 false일 때도 값을 메모이제합니다.

    종종 @value ||= compute와 같은 작업을 합니다. 그러나 compute가 결국 nil을 반환하게 되어 다시 계산하고 싶지 않은 경우에는 defined?를 사용하여 값이 설정되었는지 확인해야 합니다. 이러한 패턴을 작성하는 것은 지루하고, StrongMemoize는 이러한 패턴을 사용하는 데 도움을 줍니다.

    다음과 같이 작성할 수 있습니다:

    class Find
      include Gitlab::Utils::StrongMemoize
        
      def result
        search
      end
      strong_memoize_attr :result
        
      def enabled?
        Feature.enabled?(:some_feature)
      end
      strong_memoize_attr :enabled?
    end
    

    매개변수가 있는 메소드에 strong_memoize_attr를 사용하는 것은 지원되지 않습니다. override와 결합하면 올바르지 않은 결과를 메모이제할 수 있습니다.

    대신 strong_memoize_with를 사용하십시오.

    # 나쁨
    def expensive_method(arg)
      # ...
    end
    strong_memoize_attr :expensive_method
      
    # 좋음
    def expensive_method(arg)
      strong_memoize_with(:expensive_method, arg) do
        # ...
      end
    end
    

    매개변수를 사용하는 메소드를 메모이제하는 데 도움이 되는 strong_memoize_with도 있습니다. 이는 매개변수가 작업의 일관된 반복적인 인수를 가지거나 작은 수의 가능한 값이 있는 메소드에 사용해야 합니다.

    class Find
      include Gitlab::Utils::StrongMemoize
        
      def result(basic: true)
        strong_memoize_with(:result, basic) do
          search(basic)
        end
      end
    
  • 메모이제 초기화

    class Find
      include Gitlab::Utils::StrongMemoize
    end
      
    Find.new.clear_memoization(:result)
    

RequestCache

request_cache.rb를 참조하세요.

이 모듈은 RequestStore에 값을 캐싱하는 간단한 방법을 제공하며, 캐시 키는 클래스 이름, 메소드 이름, 선택적으로 사용자 지정 인스턴스 레벨 값, 선택적으로 사용자 지정 메소드 레벨 값 및 선택적으로 메서드 인수에 기반합니다.

인스턴스 레벨 사용자 지정 값을 사용하는 간단한 예는 다음과 같습니다:

class UserAccess
  extend Gitlab::Cache::RequestCache
  
  request_cache_key do
    [user&.id, project&.id]
  end
  
  request_cache def can_push_to_branch?(ref)
    # ...
  end
end

이를 통해 can_push_to_branch?의 결과는 RequestStore.store에 캐시 키를 기반으로 캐싱됩니다. 현재 RequestStore가 활성화되어 있지 않으면 해시에 저장되고 캐시 논리가 같게 됩니다.

다른 메소드에 대해 서로 다른 전략을 설정할 수도 있습니다:

class Commit
  extend Gitlab::Cache::RequestCache
  
  def author
    User.find_by_any_email(author_email)
  end
  request_cache(:author) { author_email }
end

ReactiveCaching

ReactiveCaching 문서를 읽어보세요.

CircuitBreaker

Gitlab::CircuitBreaker는 회로 차단 보호 기능을 가진 코드를 실행해야 하는 모든 클래스 주변에 래핑할 수 있습니다. run_with_circuit 메소드를 제공하여 코드 블록을 회로 차단 기능으로 래핑하여 침투적인 실패를 방지하고 시스템 내구성을 향상할 수 있습니다. 회로 차단 패턴에 대한 자세한 내용은 아래 링크를 참조하세요:

CircuitBreaker 사용

CircuitBreaker 래퍼를 사용하려면:

class MyService
  def call_external_service
    Gitlab::CircuitBreaker.run_with_circuit('ServiceName') do
      # 외부 서비스와 상호 작용하는 코드를 여기에 작성합니다
      
      raise Gitlab::CircuitBreaker::InternalServerError # 이슈가 있는 경우
    end
  end
end

call_external_service 메서드는 외부 서비스와 상호 작용하는 예시 메서드입니다. 외부 서비스와 상호 작용하는 코드를 run_with_circuit으로 감싸면 해당 메서드는 회로 차단기 내에서 실행됩니다.

해당 메서드가 코드 블록 실행 중에 InternalServerError 오류를 발생시켜야 하며, 회로 차단기는 에러 수와 요청률을 추적하여 구성된 에러 임계값 또는 볼륨 임계값에 도달하면 회로를 엽니다. 회로가 열리면 후속 요청은 코드 블록을 실행하지 않고 빠르게 실패하며, 회로 차단기는 일정 수의 요청을 주기적으로 서비스의 가용성을 테스트하기 위해 잠시 통과시킨 후 다시 회로를 닫습니다.

Configuration

각 고유한 회로에 대해 캐시 키로 사용되는 서비스명을 지정해야 합니다. 이는 회로를 식별하는 CamelCase 문자열이어야 합니다.

회로 차단기에는 각 회로마다 오버라이드할 수 있는 기본값이 있습니다. 예를 들어:

Gitlab::CircuitBreaker.run_with_circuit('ServiceName', options = { volume_threshold: 5 }) do
  ...
end

기본값은 다음과 같습니다:

  • exceptions: [Gitlab::CircuitBreaker::InternalServerError]
  • error_threshold: 50
  • volume_threshold: 10
  • sleep_window: 90
  • time_window: 60