Rails Development Guideline

太长不读

本文档关注 GitLab Rails 后端开发的相关内容,希望能做到以下内容:

  1. 帮助新人快速了解后端开发的必备知识
  2. 面对后端需求时,能够快速找到对应代码位置,并且 follow 项目内的最佳实践
  3. 筛选 GitLab Docs 数量庞大的文档,列出必读精华内容

GitLab Upstream Handbook 中关于极狐的内容

JH Support

GitLab 整体架构

一定要先了解 GitLab 的整体架构,避免陷入开发细节

  • 推荐先看张乾介绍架构的视频,有一个宏观的认识

Meet Google Drive - One place for all your files

  • GitLab 文档介绍系统架构和重要组件
[GitLab architecture overview GitLab](https://docs.gitlab.com/ee/development/architecture.html)

prepend_mod

利用 Ruby 巧妙的实现了 prepend_mod 方法,正确的使用 prepend_mod 可以在一套代码库管理多个发行版,GitLab 大量使用 prepend_mod ,使得 EE 逻辑覆盖 CE 逻辑,极狐同样使用 prepend_mod 覆盖 GitLab 的逻辑,所以理解 prepend_mod 才能正确的开发

  • 张乾和曹宝栋在 RailsConf 的 session,介绍使用 prepend_mod 的场景

Meet Google Drive - One place for all your files

  • 曹宝栋 prepend_mod 详细介绍

GitLab EE与JH的加载

Devise

GitLab 大量使用了 devise,实现灵活多变的 Authentication 方案,如果要做有关 登录/登出 部分的逻辑。一定要学会 devise,devise 实在太方便了,但同样也太隐晦了,如果不了解的话,真的很难做逻辑修改。

  • 官方文档,强烈推荐看

GitHub - heartcombo/devise: Flexible authentication solution for Rails with Warden.

  • Devise how-to articles,里面介绍了大量有用的代码案例

How Tos · heartcombo/devise Wiki

Rails Routes

不同于普通 Rails 项目简单的 route 定义,GitLab 有以下不同

  • 定义了 routing concern,方便后面复用

Rails Routing from the Outside In - Ruby on Rails Guides

  • GitLab::Patch::DrawRoute定义了 draw 方法,用来加载 config/routesee/config/routes 文件夹中定义的各种子路由文件,同时极狐也覆盖了 draw 方法,加载极狐特有的路由
  • 可以在 gitlab 文件夹中运行 bundle exec rails routes 列出所有定义的路由,但需要注意,结果列表不包括 Rest API 和 GraphQL API

REST API

GitLab 使用 Grape Gem 来定义 OpenAPI,一方面提供给 public 使用,另一方面也会在组件之间使用,比如 gitlab runner 通过 API 上传 job log,或者 GitLab 前端调用

  • REST API 定义在 config/routes/api.rb, 通过 mount ::API::API => '/' 加载 lib/api/api.rb, 并在 API::API 中给所有 API path 定义加载 /api 作为 prefix
  • grape 官方文档

GitHub - ruby-grape/grape: An opinionated framework for creating REST-like APIs in Ruby.

  • 现存 REST API Authentication 方式
[API Docs GitLab](https://docs.gitlab.com/ee/api/#authentication)
  • 现存 Rest API 列表,但不是特别全,极狐可以考虑修改 upstream 的 api 定义文件,贡献一波
[REST API resources GitLab](https://docs.gitlab.com/ee/api/api_resources.html)

GraphQL API

与 REST API 类似,GraphQL API 也可以对外提供 API 能力

  • Graphql 中文文档,可以快速了解相关知识

GraphQL

  • GraphQL 入口在 config/routes/api.rb, 通过 match '/api/graphql', via: [:get, :post], to: 'graphql#execute' 定义路由。同时你也可以通过访问 127.0.0.1:3000/-/graphql-explorer 或者 https://gitlab.com/-/graphql-explorer 进入 GraphQL 调试页面
  • GitLab 使用 GraphQL 帮助文档
[Get started with GitLab GraphQL API GitLab](https://docs.gitlab.com/ee/api/graphql/getting_started.html)
  • GitLab GraphQL APL list,可以查看目前定义的所有 GraphQL endpoint
[GraphQL API Resources GitLab](https://docs.gitlab.com/ee/api/graphql/reference/)

Rails Controller

  • 需要理解 prepend_mod,因为极狐经常需要覆盖 upstream 的 controller 逻辑
  • 不要写过多的逻辑在 Controller 层,从现在 GitLab 的做法看,重要的逻辑都在 Model 或者 Service 中
  • upstream 已经不推荐写 controller 测试了,推荐写 requests 测试,具体看下面的MR

Transition from controller specs to request specs (&5076) · Epics · GitLab.org

  • 修改 controller 代码时,最烦人的就是被藏在各处的 before_action 干扰,这时可以在 rails console 中通过命令 XXXController.new._process_action_callbacks.map { |c| c.filter if c.kind == :before } 列出某一个 XXXController 有多少个 before_action,方便我们查看。或者通过在 controller 文件的最底部添加下面的代码,这样在运行测试时,某一个 action 的before_action 运行之前都会运行 binding.pry,方便大家调试
class XXXController
...
	_process_action_callbacks.each do |callback|
		define_method(callback.filter.to_s) do |*args|
			puts "Running before_action: #{callback.filter}"
			require'pry'
	    binding.pry
	    super(*args)
	  end if callback.kind == :before
	end
end

请参考下面的文章

Debugging Action Callbacks (aka Filters) in Rails

Rails Model

具体内容待总结

  • 刘文举同学分享的 user, group 的视频

Meet Google Drive - One place for all your files

  • zhu shuang同学分享的 project 的视频

Meet Google Drive - One place for all your files

  • 邓加红同学分享 issue 的视频

Meet Google Drive - One place for all your files

Meet Google Drive - One place for all your files

Rails Service

待总结

Rails Lib

待总结

Rails Initializer:

  • GitLab 有非常多的 initializer,目前通过文件名最前面的数字前缀保证加载顺序
  • application.rb 中,通过 initializer :load_jh_config_initializers, after: :load_config_initializers 确保极狐初始化文件在 upstream 之后加载

DB Migration:

目前极狐需要做任何数据库的修改,都需要在 upstream repo 上面进行修改,而且还需要注意以下几点:

  • 需要区分 migration 的类型,目前分成 Regular Schema Migrations,Post-deployment Migration,Background Migrations
[Migration Style Guide GitLab](https://docs.gitlab.com/ee/development/migration_style_guide.html#choose-an-appropriate-migration-type)
  • Migration 文件开发指南
[Migration Style Guide GitLab](https://docs.gitlab.com/ee/development/migration_style_guide.html#schema-changes)
  • 所有的 migration 都需要写测试
[Testing Rails migrations at GitLab GitLab](https://docs.gitlab.com/ee/development/testing_guide/testing_migrations_guide.html#testing-rails-migrations-at-gitlab)
  • 注意 migration 的运行时间,regular migrations 要小于 3分钟,post-deployment migrations 小于 10分钟,background migrations 大于 10分钟
  • pipeline 中 check-migrations 会对比 db/structure.sql 结构,如果 fail 记得 rebase 最新 master 分支

Rails View

在 repo 中一共有使用 3 个 application_helper.rb,分别保证将 CE,EE 和 JH 文件夹添加进入 ActionView::LookupContext,这样 CE,EE 和 JH 的 controller 中可以定位到对应的 view 文件

Feature Flag

Feature Flag 是一种编程技巧,通过它可以让我们在程序运行时动态打开或关闭某些功能

  • 目前 GitLab 推荐,根据下面的场景,在开发前判断是否需要 feature flag

Feature flags in development of GitLab

  • Feature Flag的生命周期大概是
    • 需要先添加 feature flag,但要保证现有功能不变
    • 适当时候打开 feature flag,验证新功能的可靠性
    • 最后删除 feature flag
[Feature flags in the development of GitLab GitLab](https://docs.gitlab.com/ee/development/feature_flags/#feature-flags-in-gitlab-development)
  • 目前 GitLab 的 Feature Flag 存在三种,development type(short-lived feature flag),用于开发,ops type(long-lived), 用于运维,experiment type , 用于 A/B testing
# 默认type就是development
Feature.enable(:<dev_flag_name>)  Feature.disable(:<dev_flag_name>)
# 第二个参数叫做 actor, 目前只接受 User, Project 和 Group 三种
Feature.enable(:<dev_flag_name>, Project.find(<project id>))
# 检查 Feature flag 是否打开
Feature.enabled?(:dev_flag_name) Feature.disabled?(:dev_flag_name)
# 检查 ops feature flag是否打开
Feature.enabled?(:my_ops_flag, type: :ops)
# 如果要强制从yaml文件中读取默认值,则使用 default_enabled: :yaml
Feature.enabled?(:feature_flag, project, default_enabled: :yaml)

#上面的方法实现在 lib/feature.rb 中
  • 在运行非 End to End 测试时,通过在 spec_helper.rb 中设置,几乎打开了所有的 development feature flag
config.before(:suite) do
	stub_all_feature_flags #开启feature flag
  # 上面的方法实现在 spec/support/helpers/stub_feature_flags.rb 
end

config.before do |example|
  # 除非测试加上了 stub_feature_flags: false 的 tag,否则只关闭了特定的 flag
	if example.metadata.fetch(:stub_feature_flags, true)
     ...
	else
end
  • 对于 End to end 测试,upstream 推荐使用 public api 打开或关闭某些 feature flag 达到测试的目的
[Feature flags in the development of GitLab GitLab](https://docs.gitlab.com/ee/development/feature_flags/#end-to-end-qa-tests)

Testing

GitLab 非常重视测试,强烈推荐阅读下面的文档

[Testing levels GitLab](https://docs.gitlab.com/ee/development/testing_guide/testing_levels.html#about-controller-tests)
  • 目前有 4 类测试
    • 单元测试,比如 model 测试或者 service 测试,数量最多,覆盖最全
    • 集成测试,一般指 controller 或者 request 测试,目前 upstream 推荐写 request 测试,不在新写 controller 测试
    • 白盒功能测试,放在 spec/features 文件夹下
    • 黑盒功能测试,放在 qa/ 文件夹下
  • 我推荐的测试策略,这部分大家可以一起讨论
    • 写自动化测试就像投资,写测试花费的时间就是成本,节省下手工测试的时间和验证功能的重要程度就是收益,既然是投资,那就要追求最大投资回报率。也就是说写出来的测试,验证的功能越重要,验证的次数越多,回报率越高。对于不同类型的测试,“投资策略”也不一样
    • 单元测试:dev 一定要写,测试成本低,覆盖全分支
    • 集成测试:dev 一定要写,测试成本低,相对可以少覆盖一些分支
    • 白盒功能测试:dev 或者 QA 写
      • 由 QA 给出 重要功能列表,写测试覆盖列表中所有的功能
      • 对于在迭代中碰到需求,要新加/修改功能,团队成员决定是否写对应测试,如果多数人觉得没必要写,可以不写
    • 黑盒功能测试:QA 写
      • 黑盒测试更偏向于验证各种线上环境的用户流程,而非某个特定功能,所以可以精简 重要功能列表 ,然后写测试将功能串起来
      • 有些功能或场景在产品环境很难自动化验证,比如发送短信验证码(因为要过人机识别),可以考虑使用别的方法验证功能,比如监控发送短信的成功率,创建定期任务检查短信余额等。换句话说,写这类测试成本太高,有点亏本,可以考虑更换投资方式。
  • 极狐有时候会覆盖 upstream 的某些逻辑,导致 upstream 测试失败,经与 upstream 团队讨论过后,初步的方案是
    • 优先考虑通过修改 assertion 来使 upstream 测试通过
    • 如果极狐修改了流程,比如注册时添加手机号验证,而导致大量 upstream 测试失败,可以通过在运行 upstream 测试时关闭相关功能通过测试
    • 如果失败的 upstream 测试使用了国内无法访问的资源,比如 google 服务等,直接跳过即可
      • 请尽可能少的跳过测试
      • 测试跳过配置在:jh/spec/skip_specs/skip_list.yml
      • skip_list.yml 配置文件通过文件分组,description 可以通过执行测试失败信息里取到
      • 请在新增时,通过注释加上原因以及 Issue/MR 链接,方便追溯原因

I18N

GitLab 支持多国语言,开发人员可以直接在代码中使用 I18N 相关的方法完成对 I18N 的支持,具体开发流程如下:

  1. 在代码中调用 I18N 的相关方法,比如 s__('Namespace|Label')
  2. 运行 bundle exec rake gettext:regenerate, 自动扫描代码中所有需要翻译的文本,更新到 locale/gitlab.pot 文件中
  3. 提交 locale/gitlab.pot 文件的修改到代码库中,程序员的工作基本上就完成了
  4. 后续翻译工作,GitLab 使用 Crowdin 系统进行
  • GitLab I18N 介绍
[Internationalization for GitLab GitLab](https://docs.gitlab.com/ee/development/i18n/externalization.html#always-pass-string-literals-to-the-translation-helpers)
  • GitLab I18N 在代码库中的运作原理

Meet Google Drive - One place for all your files

Sidekiq

GitLab 使用 Sidekiq 作为后台 worker,异步运行一些任务,比如发送邮件,清理数据之类的

  • GitLab Sidekiq Development
[Sidekiq guides GitLab](https://docs.gitlab.com/ee/development/sidekiq/#sidekiq-queues)

具体内容待总结

Application Setting & gitlab.yml

GitLab 有非常非常多的配置项,目前存放在两个地方,一个是 gitlab.yml ,另一个是 Application Setting Model。在 gitlab.yml 中有注释说新配置都放在 Application Setting 中,所以可以推断出,GitLab 先把配置项放在 yaml 文件中,然后又放到了数据库 application_setting 表中。

  • 代码库中使用 settingslogic gem 方便的读取 gitlab.yml,比如 Settings.db.username 就在读取 yaml 中 db → username 的相关配置
  • Application Setting 是一个 Active Record Model,有大约 100 个字段保存着各种各样的配置,同时完成发布之后的 Feature Toggle,如果是全局开关,理论上也应该变成 Application Setting 的一个字段
  • 如果开发过程中需要对本地配置进行修改,需要明确相关配置是在 yaml 中还是数据库中。另外,也可以在 gitlab-development-kit 下对 gdk.yml 进行修改,然后 gdk reconfigure 修改 gitlab 的相关配置
  • 遗留问题:没有找到 self manage GitLab 如何修改 Application Setting 内容的相关方法或者底层原理,期待有人解答:)

GitLab Integration

具体内容待总结