首页
/ Ruby on Rails 8.1 发布说明详解:Active Job Continuations、结构化事件上报与本地 CI 等核心特性

Ruby on Rails 8.1 发布说明详解:Active Job Continuations、结构化事件上报与本地 CI 等核心特性

2026-09-06 21:27:06作者:宣聪麟

本文基于 Rails 8.1 官方发布说明(guides/source/8_1_release_notes.md),完整覆盖该版本的七大重点特性——Active Job 断点续跑、结构化事件上报、本地 CI、Markdown 渲染、命令行凭据获取、废弃关联报告与无注册表 Kamal 部署,并逐一解读 Railties、Action Pack、Active Record、Active Support、Active Job 等框架的移除、废弃与重要变更,适合正在评估或执行 8.0 到 8.1 升级的 Rails 开发者。

升级到 Rails 8.1 前的准备

官方文档给出的升级路径建议:

  • 升级前先保证良好的测试覆盖率;
  • 如果应用还没有升级,应先升级到 Rails 8.0,确认应用行为正常后再尝试 8.1;
  • 升级时需要留意的事项,汇总在升级指南中。

以下章节按“重大特性”与“各框架变更”两条主线展开,特性部分会结合当前仓库中的源码实现(Active Job、Active Support、Railties、Active Record)做纵深解读。

重大特性一:Active Job Continuations(作业断点续跑)

使用方式

长耗时作业现在可以被拆分为离散的步骤(step),在中断或重启后从最后完成的步骤继续执行,而不是从头重跑。这一点在使用 Kamal 部署时尤其有价值——Kamal 默认只给运行作业的容器 30 秒来关闭。

文档给出的标准示例:

class ProcessImportJob < ApplicationJob
  include ActiveJob::Continuable

  def perform(import_id)
    @import = Import.find(import_id)

    # block format
    step :initialize do
      @import.initialize
    end

    # step with cursor, the cursor is saved when the job is interrupted
    step :process do |step|
      @import.records.find_each(start: step.cursor) do |record|
        record.process
        step.advance! from: record.id
      end
    end

    # method format
    step :finalize
  end

  private
    def finalize
      @import.finalize
    end
end

step 支持 block 与方法名两种形式;带游标(cursor)的步骤在中断时会保存游标,恢复时从游标位置继续。

源码层面的实现细节

结合 activejob/lib/active_job/continuable.rbactivejob/lib/active_job/continuation.rb 的实现,可以补充几个文档示例之外的关键机制:

配置项(class_attribute),在 Continuable 模块中定义:

  • max_resumptions:最多恢复执行的次数,默认 nil(不限);超过上限时抛出 Continuation::ResumeLimitError
  • resume_options:恢复时传给 retry_job 的选项,默认 { wait: 5.seconds }
  • resume_errors_after_advancing:推进过进度后再发生错误是否自动重试,默认 true
class ProcessImportJob < ApplicationJob
  include ActiveJob::Continuable

  self.max_resumptions = 3
  self.resume_options = { wait: 1.seconds, queue: :resumed }
  self.resume_errors_after_advancing = false
end

检查点(checkpoint)机制:设置或推进游标(set!advance!)、显式调用 checkpoint! 都会产生一个检查点;每个步骤开始前(首个步骤除外)也有自动检查点。在检查点处,作业会调用 queue_adapter.stopping?,若返回真值则抛出 Continuation::Interrupt(注意它继承自 Exception 而非 StandardError,因此不会被常规异常处理捕获),由 Active Job 将带进度序列化的作业重新入队。进度数据写入 job data 的 continuation 键,包含已完成步骤列表和当前步骤的游标值(见 Continuation#to_h)。

步骤语义:步骤按序执行、遇到即运行;已完成步骤在恢复时直接跳过(发出 step_skipped 事件);不在步骤内的代码每次执行都会运行。对无法在关停宽限期内设置检查点的超长步骤,可以声明 isolated: true,强制该步骤独占一次执行、开始前先序列化进度。

错误处理:作业在已推进进度(完成过一个步骤或推进过游标)之后抛出错误时,会被自动重试;相关行为可在 activejob/test/cases/continuation_test.rbactivejob/test/cases/structured_event_subscriber_test.rb 等测试中验证。

重大特性二:Structured Event Reporting(结构化事件上报)

使用方式

Rails 默认 Logger 面向人类阅读,不利于程序化后处理。新的 Event Reporter 提供了产生结构化事件的统一接口:

Rails.event.notify("user.signup", user_id: 123, email: "user@example.com")

支持给事件打标签(tags):

Rails.event.tagged("graphql") do
  # Event includes tags: { graphql: true }
  Rails.event.notify("user.signup", user_id: 123, email: "user@example.com")
end

以及设置上下文(context):

# All events will contain context: {request_id: "abc123", shop_id: 456}
Rails.event.set_context(request_id: "abc123", shop_id: 456)

事件会分发给订阅者(subscriber)。应用通过注册订阅者来控制事件如何被序列化与输出;订阅者必须实现接收事件哈希的 #emit 方法:

class LogSubscriber
  def emit(event)
    payload = event[:payload].map { |key, value| "#{key}=#{value}" }.join(" ")
    source_location = event[:source_location]
    log = "[#{event[:name]}] #{payload} at #{source_location[:filepath]}:#{source_location[:lineno]}"
    Rails.logger.info(log)
  end
end

源码层面的事件结构与能力

实现位于 activesupport/lib/active_support/event_reporter.rb。从源码看,notify 产出的事件哈希包含以下键:

  • name:事件名(字符串或 symbol 会被转为字符串;若传入事件对象,则用其类名);
  • payload:payload 哈希或事件对象本身;
  • tags:由 tagged 块内累积的标签,基于 Fiber 存储,可嵌套;
  • context:由 set_context 设置的请求/作业级上下文,默认存储在 EventContext 中,可通过 config.active_support.event_reporter_context_store 换成自定义存储;
  • timestamp:纳秒级时间戳;
  • source_location:调用点信息,包含 filepathlinenolabel

此外源码中还确认了几个实用细节:

  • 过滤订阅subscribe 可传入 filter 过程,让订阅者只接收部分事件,但过滤器只能访问 :name 键;
  • debug 模式Rails.event.debug 仅在 debug 模式下发出事件,with_debug 块可临时开启;
  • 敏感数据过滤:基于 payload 的哈希会按 Rails.application.filter_parameters 自动脱敏;若传入事件对象,则需要订阅者自行过滤。

重大特性三:Local CI(本地 CI)

动机与默认 DSL

开发者机器的核心数与速度已经大幅提升,足以本地运行相当规模的测试套件。这使得许多中小型应用完全有理由抛弃云端 CI。为此 Rails 新增了默认 CI 声明 DSL,定义在 config/ci.rb 中,由 bin/ci 执行。文档给出的完整示例:

CI.run do
  step "Setup", "bin/setup --skip-server"
  step "Style: Ruby", "bin/rubocop"

  step "Security: Gem audit", "bin/bundler-audit"
  step "Security: Importmap vulnerability audit", "bin/importmap audit"
  step "Security: Brakeman code analysis", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error"
  step "Tests: Rails", "bin/rails test"
  step "Tests: Seeds", "env RAILS_ENV=test bin/rails db:seed:replant"

  # Requires the `gh` CLI and `gh extension install basecamp/gh-signoff`.
  if success?
    step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff"
  else
    failure "Signoff: CI failed. Do not merge or deploy.", "Fix the issues and try again."
  end
end

可选的 gh 集成确保 PR 必须被一次通过的 CI 运行“签收”(signoff)后才有资格合并,把“本地验证通过”变成了合并的前置条件。

仓库中的实际落点

当前仓库的 app 生成器会为新应用生成这套文件:模板 railties/lib/rails/generators/rails/app/templates/config/ci.rb.tt 开头即注释 # Run using bin/ci 并包含 CI.run do 块;railties/lib/rails/generators/rails/app/templates/bin/ci.tt 则是一行 require_relative "../config/ci.rb" 的可执行入口。测试 railties/test/application/bin_ci_test.rb 会断言生成的 bin/ci 存在且可执行,因此新建 Rails 应用天然带有一套可运行的本地 CI 声明。

重大特性四:Markdown Rendering(Markdown 渲染)

随着 AI 生态的普及,Markdown 正在成为通用交换格式。Rails 顺应这一趋势,让控制器更轻松地响应并直接渲染 markdown 请求。文档示例:

class Page
  def to_markdown
    body
  end
end

class PagesController < ActionController::Base
  def show
    @page = Page.find(params[:id])

    respond_to do |format|
      format.html
      format.md { render markdown: @page }
    end
  end
end

即模型实现 to_markdown、控制器在 respond_to 中声明 format.md 并用 render markdown: ... 渲染,即可让 Accept: text/markdown 之类的请求直接拿到 markdown 内容,无需手写响应体。

重大特性五:Command-line Credentials Fetching(命令行获取凭据)

Kamal 现在可以直接从 Rails 加密的 credentials 存储中提取部署所需的机密,作为外部 Secret Store 的“低保真”替代——前提是 master key 对部署环境可用:

# .kamal/secrets
KAMAL_REGISTRY_PASSWORD=$(rails credentials:fetch kamal.registry_password)

对应的实现位于 railties/lib/rails/commands/credentials/credentials_command.rb,其中 CredentialsCommand#fetch(path)(约 L61)负责按点分路径(如 kamal.registry_password)读取加密存储中的单个值并输出到 stdout,正是上面 shell 命令替换所调用的入口。这意味着部署脚本无需维护独立的 secret 文件,只需把机密写进 config/credentials.yml.enc 再引用即可。

重大特性六:Deprecated Associations(废弃关联报告)

Active Record 关联现在可以显式标记为废弃:

class Author < ApplicationRecord
  has_many :posts, deprecated: true
end

标记之后,任何对该关联的使用都会被报告。覆盖范围既包括显式 API 调用:

author.posts
author.posts = ...

也包括间接使用,例如:

author.preload(:posts)

嵌套属性(nested attributes)引发的使用等同样在列。

支持的报告模式有三种::warn:raise:notify;backtrace 可以开启或关闭,但无论哪种设置都会附带报告发生位置。默认是 :warn 模式且不输出 backtrace。

从源码看,入口在 activerecord/lib/active_record/associations.rbdeprecated_associations_api_guard(L92)与 report_deprecated_association(L96);各关联构建器(如 activerecord/lib/active_record/associations/builder/collection_association.rbactiverecord/lib/active_record/associations/builder/singular_association.rb)中的读取/赋值方法都会先经过该守卫。deprecated 也是 has_many 等关联构建器正式接受的选项之一(见 activerecord/lib/active_record/associations/builder/association.rb 的选项白名单)。这为“关联下线”提供了一种灰度路径:先报告、再决定收紧。

重大特性七:Registry-Free Kamal Deployments(免远程注册表部署)

Kamal 做基础部署不再强依赖 Docker Hub、GHCR 之类的远程镜像注册表:Kamal 2.8 起默认使用本地注册表完成简单部署。大规模部署仍建议使用远程注册表,但这一变化显著降低了上手门槛,让第一次部署 Hello World 应用成为一件更顺畅的事。

各框架变更清单

以下各节完整继承发布说明中按框架划分的 Removals / Deprecations / Notable changes,详细变更可分别参阅 railties/CHANGELOG.mdactionpack/CHANGELOG.mdactionview/CHANGELOG.mdactionmailer/CHANGELOG.mdactioncable/CHANGELOG.mdactiverecord/CHANGELOG.mdactivestorage/CHANGELOG.md、[activemodel/CHANGELOG.md)(activemodel/CHANGELOG.md)、activesupport/CHANGELOG.mdactivejob/CHANGELOG.mdactiontext/CHANGELOG.md 与各框架 CHANGELOG。

Railties

移除(Removals)

  • 移除已废弃的 rails/console/methods.rb 文件;
  • 移除已废弃的 bin/rake stats 命令;
  • 移除已废弃的 STATS_DIRECTORIES

Action Pack

移除(Removals)

  1. 移除参数解析器中“跳过参数名开头方括号”的废弃支持。行为变化对比:

    之前:

    ActionDispatch::ParamBuilder.from_query_string("[foo]=bar") # => { "foo" => "bar" }
    ActionDispatch::ParamBuilder.from_query_string("[foo][bar]=baz") # => { "foo" => { "bar" => "baz" } }
    

    之后:

    ActionDispatch::ParamBuilder.from_query_string("[foo]=bar") # => { "[foo]" => "bar" }
    ActionDispatch::ParamBuilder.from_query_string("[foo][bar]=baz") # => { "[foo]" => { "bar" => "baz" } }
    

    即前导 [ 不再被剥离,而是作为参数名的一部分保留——依赖旧行为的参数解析代码会受影响。

  2. 移除以分号作为查询串分隔符的废弃支持。行为对比:

    之前:

    ActionDispatch::QueryParser.each_pair("foo=bar;baz=quux").to_a
    # => [["foo", "bar"], ["baz", "quux"]]
    

    之后:

    ActionDispatch::QueryParser.each_pair("foo=bar;baz=quux").to_a
    # => [["foo", "bar;baz=quux"]]
    

    分号从此是普通字符,只有 & 作为分隔符。

  3. 移除路由指向多个路径(a route to multiple paths)的废弃支持。

废弃(Deprecations)

  • 废弃 Rails.application.config.action_dispatch.ignore_leading_brackets 配置项(与上面第一项移除对应)。

重要变更(Notable changes)

  • 在新建的 Rails 应用中,重定向(redirect)在 development 环境下会输出更详细的日志。存量应用如需开启,在 config/development.rb 中加入:

    config.action_dispatch.verbose_redirect_logs = true
    

Action View

废弃(Deprecations)

  • 废弃携带无关键字参数的 :renderable 对象(即响应 #render_in 的对象)的 render 调用方式。

Active Record

移除(Removals)

  • 移除 SQLite3 适配器已废弃的 :retries 选项;
  • 移除 MySQL 已废弃的 :unsigned_float:unsigned_decimal 列方法。

废弃(Deprecations)

  • 废弃在不指定 order 的情况下使用依赖顺序的查找方法(如 #first);
  • 废弃 ActiveRecord::Base.signed_id_verifier_secret,改用 Rails.application.message_verifiers(若密钥是特定于某模型的,用 Model.signed_id_verifier);
  • 废弃在关联中使用未持久化记录执行 insert_all / upsert_all
  • 废弃在 update_all 中使用 WITHWITH RECURSIVEDISTINCT

重要变更(Notable changes)

  • schema.rb 中的表列现在按字母顺序排序。

Active Storage

移除(Removals)

  • 移除已废弃的 :azure 存储服务。

Active Support

移除(Removals)

  • 移除向 Time#since 传入 Time 对象的废弃用法;
  • 移除已废弃的 Benchmark.ms 方法(该方法现由 benchmark gem 提供);
  • 移除 Time 实例与 ActiveSupport::TimeWithZone 相加的废弃支持;
  • 移除 to_time 保留系统本地时区的废弃支持——现在始终保留接收者的时区。

废弃(Deprecations)

  • 废弃 config.active_support.to_time_preserves_timezone 配置项(与上一条移除配套);
  • 废弃 String#mb_charsActiveSupport::Multibyte::Chars
  • 废弃 ActiveSupport::Configurable 模块。

Active Job

移除(Removals)

  • 移除将 ActiveJob::Base.enqueue_after_transaction_commit 设为 :never:always:default 的支持;
  • 移除已废弃的 Rails.application.config.active_job.enqueue_after_transaction_commit 配置项;
  • 移除内置的 SuckerPunch 适配器(内部实现),改用 sucker_punch gem 自带的适配器。

废弃(Deprecations)

  • 自定义 Active Job 序列化器必须提供公开的 #klass 方法;
  • 废弃内置 sidekiq 适配器(改由 sidekiq gem 提供)。

Action Text

废弃(Deprecations)

  • 废弃 ActionText::TrixAttachment 类;
  • 废弃 ActionText::Attachments::TrixConversion 模块;
  • 废弃 ActionText::Attachable#to_trix_content_attachment_partial_path,请改为覆写 to_editor_content_attachment_partial_path
  • 废弃 ActionText::RichText#to_trix_htmlActionText::Content#to_trix_html

其他框架

Action Cable、Action Mailer、Action Mailbox 与 Active Model 在本版本发布说明中没有列出需要额外注意的移除或废弃项,详细行为变化以各自 CHANGELOG 为准。

小结

Rails 8.1 的主题可以概括为“把长生命周期任务与观测性做扎实”:Active Job Continuations 让长作业在部署滚动中不再丢进度;Event Reporter 让应用事件以结构化形式进入日志与采集管线;本地 CI 把验证循环拉回开发者机器;废弃关联、Markdown 渲染与命令行凭据获取则分别服务于渐进式重构、AI 时代的内容交换与更轻量的部署密钥管理。升级前建议先按升级指南过一遍 Action Pack 参数解析与 Active Support 时间/多字节字符串相关的行为变化,这些是最可能在存量应用中触发的兼容性问题。

登录后查看全文
热门项目推荐
相关项目推荐