Help us learn about your current experience with the documentation. Take the survey.

GitLab 工具

我们开发了一些工具来帮助简化开发工作:

MergeHash

参考 merge_hash.rb

  • 深度合并一个包含哈希、数组或其他对象的元素数组:

    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

      请注意,只有当以下情况之一发生时才会进行检查:

      • 覆盖方法在类中定义,或:
      • 覆盖方法在模块中定义,并且它被前置(prepend)到类或模块中。

      因为只有类或前置的模块才能真正覆盖方法。包含或扩展模块到另一个模块中无法覆盖任何内容。

ActiveSupport::Concernprependclass_methods 的交互

当你使用包含类方法的 ActiveSupport::Concern 时,你不会得到预期的结果,因为 ActiveSupport::Concern 不像普通的 Ruby 模块那样工作。

由于我们已经有了 Prependable 作为 ActiveSupport::Concern 的补丁来启用 prepend,这会影响它与 overrideclass_methods 的交互方式。作为一种变通方法,将 ClassMethods 扩展到定义的 Prependable 模块中。

这允许我们在上述上下文中使用 override 来验证 class_methods。这个变通方法只在我们运行验证时适用,而不是在运行应用程序本身时适用。

以下是展示此变通方法效果的示例代码块:

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

  • 即使值为 nilfalse 也进行缓存。

    我们经常使用 @value ||= compute。但是,如果 compute 最终可能返回 nil 并且你不想再次计算,这种方法效果不佳。你可以使用 defined? 来检查值是否已设置。编写这样的模式很繁琐,而 StrongMemoize 会帮助你使用这种模式。

    与其编写这样的模式:

    class Find
      def result
        return @result if defined?(@result)
    
        @result = search
      end
    end

    你可以这样写:

    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
    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 的文档。

TokenAuthenticatable

阅读 TokenAuthenticatable 的文档。

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 错误,如果在代码块执行期间抛出,该错误将被计入错误阈值。 断路器跟踪错误数量和请求速率, 并在达到配置的错误阈值或容量阈值时打开断路器。 如果断路器打开,后续请求将快速失败而不执行代码块,断路器会定期允许少量请求通过,以测试服务的可用性,然后再关闭断路器。

配置

你需要为每个用作缓存键的唯一断路器指定一个服务名称。这应该是一个标识断路器的 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