Geo self-service framework (alpha)

原文:https://docs.gitlab.com/ee/development/geo/framework.html

Geo self-service framework (alpha)

注意:本文档可能会随时更改. 这是我们正在研究的建议,一旦实施完成,此文档将得到更新. 跟随史诗般的进度.注意: Geo 自助服务框架当前处于 Alpha 状态. 如果您需要复制新的数据类型,请与 Geo 小组联系以讨论选项. 您可以在 Slack 的#g_geo与他们联系,或在问题或合并请求中提及@geo-team .

Geo 提供了一个 API,使跨 Geo 节点轻松复制数据类型成为可能. 该 API 以 Ruby 域特定语言(DSL)的形式呈现,旨在使创建数据类型的工程师只需花费很少的精力即可复制数据.

Nomenclature

在深入研究 API 之前,开发人员需要了解一些特定于地理位置的命名约定.

Model

模型是活动模型,在整个 Rails 代码库中都是如此. 它通常与数据库表绑定. 从地理角度来看,模型可以具有一个或多个资源.

Resource

资源是属于模型的一条数据,由 GitLab 功能生成. 使用存储机制将其持久化. 默认情况下,资源不可复制.

Data type

Data type is how a resource is stored. Each resource should fit in one of the data types Geo supports: :- Git repository :- Blob :- Database

有关更多详细信息,请参见数据类型 .

Geo Replicable

可复制资源是 Geo 希望在 Geo 节点之间同步的资源. 受支持的可复制数据类型有限. 实现属于已知数据类型之一的资源的复制所需的工作量很小.

Geo Replicator

地理复制器是知道如何复制可复制对象的对象. 它负责::-触发事件(生产者):-消费事件(消费者)

它与 Geo Replicable 数据类型相关. 所有复制器都有一个公共接口,可用于处理(即产生和使用)事件. 它负责主节点(产生事件的地方)和次节点(消耗事件的地方)之间的通信. 想要将 Geo 纳入其功能的工程师将使用复制器的 API 来实现这一目标.

Geo Domain-Specific Language

语法糖使工程师可以轻松指定应复制哪些资源以及如何复制.

Geo Domain-Specific Language

The replicator

首先,您需要编写一个复制器. 复制器位于ee/app/replicators/geo . 对于每个需要复制的资源,即使多个资源绑定到同一模型,也应指定一个单独的复制器.

例如,以下复制器复制软件包文件:

  1. module Geo
  2. class PackageFileReplicator < Gitlab::Geo::Replicator
  3. # Include one of the strategies your resource needs
  4. include ::Geo::BlobReplicatorStrategy
  5. # Specify the CarrierWave uploader needed by the used strategy
  6. def carrierwave_uploader
  7. model_record.file
  8. end
  9. # Specify the model this replicator belongs to
  10. def self.model
  11. ::Packages::PackageFile
  12. end
  13. end
  14. end

类名应该是唯一的. 它还与注册表的表名紧密相关,因此在此示例中,注册表表将为package_file_registry .

对于不同的数据类型,Geo 支持包括不同的策略. 选择一个适合您的需求.

Linking to a model

要将此复制器绑定到模型,需要在模型代码中添加以下内容:

  1. class Packages::PackageFile < ApplicationRecord
  2. include ::Gitlab::Geo::ReplicableModel
  3. with_replicator Geo::PackageFileReplicator
  4. end

API

设置好后,可以通过模型轻松访问复制器:

  1. package_file = Packages::PackageFile.find(4) # just a random id as example
  2. replicator = package_file.replicator

或者从复制器取回模型:

  1. replicator.model_record
  2. => <Packages::PackageFile id:4>

复制器可用于生成事件,例如在 ActiveRecord 挂钩中:

  1. after_create_commit -> { replicator.publish_created_event }

Library

所有这些背后的框架位于ee/lib/gitlab/geo/ .

Existing Replicator Strategies

在编写一种新的复制器策略之前,请检查以下内容,以查看现有策略之一是否已经可以处理您的资源. 如果不确定,请咨询地理团队.

Blob Replicator Strategy

使用Geo::BlobReplicatorStrategy模块,Geo 可以轻松支持使用CarrierWave 的 Uploader::Base模型.

首先,每个文件应具有其自己的主要 ID 和模型. Geo 强烈建议将每个文件都视为头等公民,因为根据我们的经验,这大大简化了跟踪复制和验证状态.

例如,要添加对具有Widget widgets表的Widget模型引用的文件的支持,您将执行以下步骤:

Replication

  1. Widget类中包含Gitlab::Geo::ReplicableModel ,并使用with_replicator Geo::WidgetReplicator指定 Replicator 类.

    此时, Widget类应如下所示:

    1. # frozen_string_literal: true
    2. class Widget < ApplicationRecord
    3. include ::Gitlab::Geo::ReplicableModel
    4. with_replicator Geo::WidgetReplicator
    5. mount_uploader :file, WidgetUploader
    6. def self.replicables_for_geo_node
    7. # Should be implemented. The idea of the method is to restrict
    8. # the set of synced items depending on synchronization settings
    9. end
    10. ...
    11. end
  2. 创建ee/app/replicators/geo/widget_replicator.rb . 实现#carrierwave_uploader方法,该方法应返回CarrierWave::Uploader . 并实现类方法.model以返回Widget类.

    1. # frozen_string_literal: true
    2. module Geo
    3. class WidgetReplicator < Gitlab::Geo::Replicator
    4. include ::Geo::BlobReplicatorStrategy
    5. def self.model
    6. ::Widget
    7. end
    8. def carrierwave_uploader
    9. model_record.file
    10. end
    11. end
    12. end
  3. 创建ee/spec/replicators/geo/widget_replicator_spec.rb并执行必要的设置,以定义共享示例的model_record变量.

    1. # frozen_string_literal: true
    2. require 'spec_helper'
    3. RSpec.describe Geo::WidgetReplicator do
    4. let(:model_record) { build(:widget) }
    5. it_behaves_like 'a blob replicator'
    6. end
  4. 创建widget_registry表,以便 Geo 次要对象可以跟踪每个 Widget 文件的同步和验证状态:

    1. # frozen_string_literal: true
    2. class CreateWidgetRegistry < ActiveRecord::Migration[6.0]
    3. DOWNTIME = false
    4. disable_ddl_transaction!
    5. def up
    6. unless table_exists?(:widget_registry)
    7. ActiveRecord::Base.transaction do
    8. create_table :widget_registry, id: :bigserial, force: :cascade do |t|
    9. t.integer :widget_id, null: false
    10. t.integer :state, default: 0, null: false, limit: 2
    11. t.integer :retry_count, default: 0, limit: 2
    12. t.text :last_sync_failure
    13. t.datetime_with_timezone :retry_at
    14. t.datetime_with_timezone :last_synced_at
    15. t.datetime_with_timezone :created_at, null: false
    16. t.index :widget_id
    17. t.index :retry_at
    18. t.index :state
    19. end
    20. end
    21. end
    22. add_text_limit :widget_registry, :last_sync_failure, 255
    23. end
    24. def down
    25. drop_table :widget_registry
    26. end
    27. end
  5. Create ee/app/models/geo/widget_registry.rb:

    1. # frozen_string_literal: true
    2. class Geo::WidgetRegistry < Geo::BaseRegistry
    3. include Geo::ReplicableRegistry
    4. MODEL_CLASS = ::Widget
    5. MODEL_FOREIGN_KEY = :widget_id
    6. belongs_to :widget, class_name: 'Widget'
    7. end

    方法has_create_events? 在大多数情况下应该返回true . 但是,如果您添加的实体没有创建事件,则根本不要添加该方法.

  6. Update REGISTRY_CLASSES in ee/app/workers/geo/secondary/registry_consistency_worker.rb.

  7. Create ee/spec/factories/geo/widget_registry.rb:

    1. # frozen_string_literal: true
    2. FactoryBot.define do
    3. factory :geo_widget_registry, class: 'Geo::WidgetRegistry' do
    4. widget
    5. state { Geo::WidgetRegistry.state_value(:pending) }
    6. trait :synced do
    7. state { Geo::WidgetRegistry.state_value(:synced) }
    8. last_synced_at { 5.days.ago }
    9. end
    10. trait :failed do
    11. state { Geo::WidgetRegistry.state_value(:failed) }
    12. last_synced_at { 1.day.ago }
    13. retry_count { 2 }
    14. last_sync_failure { 'Random error' }
    15. end
    16. trait :started do
    17. state { Geo::WidgetRegistry.state_value(:started) }
    18. last_synced_at { 1.day.ago }
    19. retry_count { 0 }
    20. end
    21. end
    22. end
  8. Create ee/spec/models/geo/widget_registry_spec.rb:

    1. # frozen_string_literal: true
    2. require 'spec_helper'
    3. RSpec.describe Geo::WidgetRegistry, :geo, type: :model do
    4. let_it_be(:registry) { create(:geo_widget_registry) }
    5. specify 'factory is valid' do
    6. expect(registry).to be_valid
    7. end
    8. include_examples 'a Geo framework registry'
    9. describe '.find_registry_differences' do
    10. ... # To be implemented
    11. end
    12. end

小部件现在应该由 Geo 复制!

Verification

  1. 将验证状态字段添加到widgets表中,以便 Geo 主数据库可以跟踪验证状态:

    1. # frozen_string_literal: true
    2. class AddVerificationStateToWidgets < ActiveRecord::Migration[6.0]
    3. DOWNTIME = false
    4. def change
    5. add_column :widgets, :verification_retry_at, :datetime_with_timezone
    6. add_column :widgets, :verified_at, :datetime_with_timezone
    7. add_column :widgets, :verification_checksum, :binary, using: 'verification_checksum::bytea'
    8. add_column :widgets, :verification_failure, :string
    9. add_column :widgets, :verification_retry_count, :integer
    10. end
    11. end
  2. verification_failureverification_checksum上添加部分索引,以确保可以高效执行重新验证:

    1. # frozen_string_literal: true
    2. class AddVerificationFailureIndexToWidgets < ActiveRecord::Migration[6.0]
    3. include Gitlab::Database::MigrationHelpers
    4. DOWNTIME = false
    5. disable_ddl_transaction!
    6. def up
    7. add_concurrent_index :widgets, :verification_failure, where: "(verification_failure IS NOT NULL)", name: "widgets_verification_failure_partial"
    8. add_concurrent_index :widgets, :verification_checksum, where: "(verification_checksum IS NOT NULL)", name: "widgets_verification_checksum_partial"
    9. end
    10. def down
    11. remove_concurrent_index :widgets, :verification_failure
    12. remove_concurrent_index :widgets, :verification_checksum
    13. end
    14. end

要做的事情:在二级服务器上添加验证. 这应作为以下内容的一部分完成:Geo:自助服务框架-包文件验证的首次实现

小部件现在应由 Geo 验证!

Metrics

指标由Geo::MetricsUpdateWorker收集,保存在GeoNodeStatus以显示在 UI 中,然后发送给 Prometheus.

  1. 将字段widget_countwidget_checksummed_countwidget_checksum_failed_countwidget_synced_countwidget_failed_countwidget_registry_countee/app/models/geo_node_status.rb GeoNodeStatus#RESOURCE_STATUS_FIELDS数组中.
  2. 将相同的字段添加到ee/app/models/geo_node_status.rb GeoNodeStatus#PROMETHEUS_METRICS哈希中.
  3. 将相同字段添加到doc/administration/monitoring/prometheus/gitlab_metrics.md Sidekiq metrics表中.
  4. 将相同的字段添加到doc/api/geo_nodes.md GET /geo_nodes/status示例响应中.
  5. 将相同的字段添加到ee/spec/models/geo_node_status_spec.rbee/spec/factories/geo_node_statuses.rb .
  6. Set widget_count in GeoNodeStatus#load_data_from_current_node:

    1. self.widget_count = Geo::WidgetReplicator.primary_total_count
  7. 添加GeoNodeStatus#load_widgets_data来设置widget_synced_countwidget_failed_countwidget_registry_count

    1. def load_widget_data
    2. self.widget_synced_count = Geo::WidgetReplicator.synced_count
    3. self.widget_failed_count = Geo::WidgetReplicator.failed_count
    4. self.widget_registry_count = Geo::WidgetReplicator.registry_count
    5. end
  8. Call GeoNodeStatus#load_widgets_data in GeoNodeStatus#load_secondary_data.

  9. Set widget_checksummed_count and widget_checksum_failed_count in GeoNodeStatus#load_verification_data:

    1. self.widget_checksummed_count = Geo::WidgetReplicator.checksummed_count self.widget_checksum_failed_count = Geo::WidgetReplicator.checksum_failed_count

小部件复制和验证指标现在应该可以在 API,管理区域 UI 和 Prometheus 中使用!

GraphQL API

  1. ee/app/graphql/types/geo/geo_node_type.rbGeoNodeType添加一个新字段:

    1. field :widget_registries, ::Types::Geo::WidgetRegistryType.connection_type,
    2. null: true,
    3. resolver: ::Resolvers::Geo::WidgetRegistriesResolver,
    4. description: 'Find widget registries on this Geo node',
    5. feature_flag: :geo_self_service_framework
  2. 新添加widget_registries字段名的expected_fields在阵列ee/spec/graphql/types/geo/geo_node_type_spec.rb .

  3. Create ee/app/graphql/resolvers/geo/widget_registries_resolver.rb:

    1. # frozen_string_literal: true
    2. module Resolvers
    3. module Geo
    4. class WidgetRegistriesResolver < BaseResolver
    5. include RegistriesResolver
    6. end
    7. end
    8. end
  4. Create ee/spec/graphql/resolvers/geo/widget_registries_resolver_spec.rb:

    1. # frozen_string_literal: true
    2. require 'spec_helper'
    3. RSpec.describe Resolvers::Geo::WidgetRegistriesResolver do
    4. it_behaves_like 'a Geo registries resolver', :geo_widget_registry
    5. end
  5. Create ee/app/finders/geo/widget_registry_finder.rb:

    1. # frozen_string_literal: true
    2. module Geo
    3. class WidgetRegistryFinder
    4. include FrameworkRegistryFinder
    5. end
    6. end
  6. Create ee/spec/finders/geo/widget_registry_finder_spec.rb:

    1. # frozen_string_literal: true
    2. require 'spec_helper'
    3. RSpec.describe Geo::WidgetRegistryFinder do
    4. it_behaves_like 'a framework registry finder', :geo_widget_registry
    5. end
  7. Create ee/app/graphql/types/geo/widget_registry_type.rb:

    1. # frozen_string_literal: true
    2. module Types
    3. module Geo
    4. # rubocop:disable Graphql/AuthorizeTypes because it is included
    5. class WidgetRegistryType < BaseObject
    6. include ::Types::Geo::RegistryType
    7. graphql_name 'WidgetRegistry'
    8. description 'Represents the sync and verification state of a widget'
    9. field :widget_id, GraphQL::ID_TYPE, null: false, description: 'ID of the Widget'
    10. end
    11. end
    12. end
  8. Create ee/spec/graphql/types/geo/widget_registry_type_spec.rb:

    1. # frozen_string_literal: true
    2. require 'spec_helper'
    3. RSpec.describe GitlabSchema.types['WidgetRegistry'] do
    4. it_behaves_like 'a Geo registry type'
    5. it 'has the expected fields (other than those included in RegistryType)' do
    6. expected_fields = %i[widget_id]
    7. expect(described_class).to have_graphql_fields(*expected_fields).at_least
    8. end
    9. end
  9. Add integration tests for providing Widget registry data to the frontend via the GraphQL API, by duplicating and modifying the following shared examples in ee/spec/requests/api/graphql/geo/registries_spec.rb:

    1. it_behaves_like 'gets registries for', {
    2. field_name: 'widgetRegistries',
    3. registry_class_name: 'WidgetRegistry',
    4. registry_factory: :geo_widget_registry,
    5. registry_foreign_key_field_name: 'widgetId'
    6. }

现在应该可以通过 GraphQL API 获得各个小部件同步和验证数据!

  1. 注意复制”更新”事件. Geo Framework 目前不支持复制”更新”事件,因为此时添加到框架的所有实体都是不可变的. 如果您要添加的实体属于这种情况,请遵循https://gitlab.com/gitlab-org/gitlab/-/issues/118743https://gitlab.com/gitlab-org/gitlab /// issues / 118745作为添加新事件类型的示例. 添加通知后,请同时删除它.

Admin UI

要做的事情:这应该作为《 地理手册》的一部分完成:实现自助服务框架可复制的前端

窗口小部件同步和验证数据(总计和个人)现在应该在管理界面中可用!