Torna al blog

Rails 8.1: Novità e Miglioramenti - Release Ottobre 2025

Scopri Rails 8.1: Job Continuations, Event Reporter, Local CI DSL, Kamal Secrets, Association Deprecations e tutte le novità di ottobre 2025.

Edoardo Midali

Edoardo Midali

Developer · Content Creator

8 min di lettura
Rails 8.1: Novità e Miglioramenti - Release Ottobre 2025

Rails 8.1 è stato rilasciato il 22 ottobre 2025 con oltre 2500 commits e 500+ contributori. Questa release introduce Job Continuations per long-running jobs interrompibili, Event Reporter per structured logging, Local CI DSL, integrazione Kamal Secrets, Association Deprecations e supporto Markdown response.

🎯 Novità Principali

Job Continuations - Long-Running Jobs Interrompibili

La feature più richiesta: jobs che riprendono dall'ultimo step completato!

❌ Prima - Jobs fail on restart:

class ProcessImportJob < ApplicationJob
  def perform(import_id)
    import = Import.find(import_id)

    # ❌ 50,000 records - 30 minuti
    # Se server restart a 20 minuti → RESTART DA ZERO!
    import.records.each do |record|
      record.process
    end

    import.finalize
  end
end

✅ Rails 8.1 - Job Continuations:

class ProcessImportJob < ApplicationJob
  include ActiveJob::Continuable

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

    # ✅ Step 1 - Initialize
    step :initialize do
      @import.initialize
    end

    # ✅ Step 2 - Process con cursor
    step :process do |step|
      @import.records.find_each(start: step.cursor) do |record|
        record.process
        step.advance!(from: record.id) # Save progress!
      end
    end

    # ✅ Step 3 - Finalize (method format)
    step :finalize
  end

  private

  def finalize
    @import.finalize
  end
end

Come funziona:

# Job starts
ProcessImportJob.perform_later(123)

# Progress:
# - Step :initialize → Complete
# - Step :process → Processing... (cursor: 15,000/50,000)
# 🔄 SERVER RESTART!

# Job auto-resume!
# - Step :initialize → SKIPPED (completed)
# - Step :process → RESUMES from cursor 15,000!
# - Step :finalize → Execute dopo process

# ✅ No perdita di progresso!

Real-world example - Batch processing:

class ProcessVideosJob < ApplicationJob
  include ActiveJob::Continuable

  def perform(video_ids)
    step :download do |step|
      video_ids.each do |id|
        download_video(id)
        step.advance!(from: id)
      end
    end

    step :transcode do |step|
      video_ids.each do |id|
        transcode_video(id, format: :hd)
        step.advance!(from: id)
      end
    end

    step :upload do |step|
      video_ids.each do |id|
        upload_to_cdn(id)
        step.advance!(from: id)
      end
    end

    step :notify
  end

  private

  def notify
    VideoChannel.broadcast_to(user, { status: 'complete' })
  end
end

Vantaggi:

  • Zero perdita: Jobs riprendono da ultimo checkpoint
  • Kamal-ready: Perfect per deploy con 30s shutdown
  • Granular: Step-by-step progress tracking
  • Production-safe: Restart senza impatto

Event Reporter - Structured Logging

Structured events invece di log testuali!

❌ Prima - Text logs:

# Traditional logger
Rails.logger.info "User signup: id=#{user.id}, email=#{user.email}"
# ❌ Hard to parse, no structure

✅ Rails 8.1 - Event Reporter:

# ✅ Structured event
Rails.event.notify("user.signup",
  user_id: user.id,
  email: user.email,
  plan: "premium"
)

Tagged events:

# Add context tags
Rails.event.tagged("graphql") do
  Rails.event.notify("query.executed",
    query: gql_query,
    duration_ms: 45
  )
end

# Event includes: { graphql: true, query: "...", duration_ms: 45 }

Persistent context:

# Set context for ALL subsequent events
Rails.event.set_context(
  request_id: "abc123",
  shop_id: 456,
  user_id: 789
)

# Every event now includes this context!
Rails.event.notify("order.created", order_id: 999)
# → { order_id: 999, request_id: "abc123", shop_id: 456, user_id: 789 }

Custom subscribers:

# config/initializers/event_reporter.rb
class LogSubscriber
  def emit(event)
    payload = event[:payload].map { |key, value| "#{key}=#{value}" }.join(" ")
    source = event[:source_location]

    log = "[#{event[:name]}] #{payload} at #{source[:filepath]}:#{source[:line]}"
    Rails.logger.info(log)
  end
end

class DatadogSubscriber
  def emit(event)
    Datadog::Statsd.new.event(
      event[:name],
      event[:payload].to_json,
      tags: event[:tags]
    )
  end
end

# Register subscribers
Rails.event.subscribe(LogSubscriber.new)
Rails.event.subscribe(DatadogSubscriber.new)

Built-in events:

# Rails emits structured events for:
# - user.signup
# - order.created
# - payment.processed
# - job.enqueued
# - job.performed
# - cache.hit
# - cache.miss
# - sql.query

# Subscribe to specific events
Rails.event.subscribe_to("sql.query") do |event|
  if event[:duration_ms] > 100
    SlowQueryAlert.notify(event[:sql])
  end
end

Vantaggi:

  • Structured: JSON-friendly, parseable
  • Context-aware: Tags e context automatici
  • Multi-output: Log + Datadog + CloudWatch
  • Traceable: Source location inclusa

Local CI DSL

CI locale senza GitHub Actions/CircleCI!

Setup:

# config/ci.rb - New file!
CI.run do
  step "Setup", "bin/setup --skip-server"
  step "Style: Ruby", "bin/rubocop"
  step "Security: Gem audit", "bin/bundler-audit"
  step "Security: Importmap audit", "bin/importmap audit"
  step "Security: Brakeman", "bin/brakeman --quiet --exit-on-error"
  step "Tests: Rails", "bin/rails test"
  step "Tests: System", "bin/rails test:system"
  step "Tests: Seeds", "env RAILS_ENV=test bin/rails db:seed:replant"

  # Conditional steps
  if success?
    step "Signoff: Ready to merge!", "gh signoff"
  else
    failure "Signoff: CI failed. Fix issues before merge."
  end
end

Run CI:

# Execute full CI pipeline locally
bin/ci

# Output:
# ✓ Setup (2.1s)
# ✓ Style: Ruby (1.5s)
# ✓ Security: Gem audit (0.8s)
# ✓ Security: Importmap audit (0.5s)
# ✓ Security: Brakeman (3.2s)
# ✓ Tests: Rails (12.4s)
# ✓ Tests: System (8.9s)
# ✓ Tests: Seeds (1.2s)
# ✓ Signoff: Ready to merge! (0.3s)
#
# ✅ CI passed in 31.0s

Advanced configuration:

# config/ci.rb
CI.run do
  # Parallel execution
  parallel do
    step "Rubocop", "bin/rubocop"
    step "Tests: Models", "bin/rails test test/models"
    step "Tests: Controllers", "bin/rails test test/controllers"
  end

  # Conditional steps
  step "Deploy: Staging" if ENV['BRANCH'] == 'develop'

  # Custom failure handling
  on_failure do |failed_step|
    SlackNotifier.ping("CI failed at: #{failed_step}")
  end
end

Vantaggi:

  • No cloud: CI completamente locale
  • Fast feedback: No push/wait cycle
  • Cost-free: No CI minutes billing
  • Same env: Dev = CI environment

Kamal Secrets Integration

Kamal ora legge secrets da Rails credentials!

Setup:

# .kamal/secrets - New integration!
KAMAL_REGISTRY_PASSWORD=$(rails credentials:fetch kamal.registry_password)
DATABASE_URL=$(rails credentials:fetch production.database_url)
REDIS_URL=$(rails credentials:fetch production.redis_url)
SECRET_KEY_BASE=$(rails credentials:fetch secret_key_base)

Rails credentials:

# config/credentials.yml.enc
kamal:
  registry_password: ghp_xxxxxxxxxxxx

production:
  database_url: postgresql://...
  redis_url: redis://...

secret_key_base: xxxxx

Kamal config:

# config/deploy.yml
service: myapp

image: myapp/production

servers:
  web:
    - 192.168.1.10
    - 192.168.1.11

registry:
  username: myapp
  password:
    - KAMAL_REGISTRY_PASSWORD

env:
  secret:
    - DATABASE_URL
    - REDIS_URL
    - SECRET_KEY_BASE

Deploy:

# Kamal legge secrets da Rails credentials!
kamal deploy

# ✅ No più .env files
# ✅ No più external secret stores
# ✅ Tutto in Rails credentials (encrypted)

Vantaggi:

  • Low-fi: No AWS Secrets Manager/Vault
  • Encrypted: Rails credentials già secure
  • Simple: Single source of truth
  • Git-safe: Encrypted credentials in repo

Association Deprecations

Depreca associazioni per smooth migrations!

class Author < ApplicationRecord
  # ✅ Marca come deprecated
  has_many :posts, deprecated: true
  has_many :articles # New association
end

# Usage
author.posts
# DEPRECATION WARNING: Author#posts is deprecated. Use #articles instead.
# Backtrace: app/controllers/authors_controller.rb:15

Deprecation modes:

# Mode: :warn (default) - Logs warning
has_many :posts, deprecated: true

# Mode: :raise - Raises error
has_many :posts, deprecated: { mode: :raise }

# Mode: :notify - Sends to error tracker
has_many :posts, deprecated: { mode: :notify }

# Custom message
has_many :posts,
  deprecated: {
    message: "Use #articles instead. Will be removed in v2.0"
  }

Configuration:

# config/environments/production.rb
config.active_record.deprecation_mode = :notify

# Enable/disable backtraces
config.active_record.deprecation_backtrace = false # Default

Real-world migration:

# Step 1: Deprecate old association
class User < ApplicationRecord
  has_many :subscriptions, deprecated: true
  has_many :active_subscriptions # New
end

# Step 2: Fix all usages (warnings help find them)
# user.subscriptions → user.active_subscriptions

# Step 3: Remove deprecated association (next major version)
class User < ApplicationRecord
  has_many :active_subscriptions
end

Vantaggi:

  • Smooth migration: Gradual refactor
  • Traceable: Backtrace mostra dove usato
  • Flexible: Warn, raise, o notify
  • Safe: No breaking changes immediate

🚀 Additional Features

Kamal Local Registry

Deploy senza Docker Hub/GHCR!

# config/deploy.yml
registry:
  local: true # ✅ No remote registry needed!

servers:
  web:
    - 192.168.1.10
# Build e deploy usando registry locale
kamal deploy
# ✅ Image pushed to local registry on servers

Markdown Response Format

Supporto nativo per markdown responses!

class Page < ApplicationRecord
  def to_markdown
    body # Returns markdown content
  end
end

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

    respond_to do |format|
      format.html
      format.md # ✅ Nuovo formato!
    end
  end
end
# Request
GET /pages/1.md

# Response
Content-Type: text/markdown

# My Page Title

This is **bold** and this is *italic*.

dom_id Helper Globally Available

# Prima - solo in views
include ActionView::RecordIdentifier

# ✅ Rails 8.1 - disponibile ovunque!
user = User.new(id: 42)
dom_id(user) # => "user_42"

# Utile in Turbo Streams
turbo_stream.replace dom_id(@user), partial: "users/card"

PostgreSQL Nil Primary Key Handling

# ❌ Prima - Postgres error
user = User.new(id: nil)
user.save # PostgreSQL::NotNullViolation!

# ✅ Rails 8.1 - handled automatically
user = User.new(id: nil)
user.save # PostgreSQL usa DEFAULT value (sequence)

📊 Performance & Stability

Production-ready:

  • Shopify: Rails 8.1 in prod da mesi
  • HEY: Running stable in production
  • 500+ contributors: Community-driven
  • 2500+ commits: Extensive testing

🔄 Migration Guide

Upgrade

# Upgrade to Rails 8.1
bundle update rails

# Run migrations
rails app:update

# Check for deprecations
rails db:migrate

Enable Features

# config/application.rb

# Enable Job Continuations
config.active_job.continuations_enabled = true

# Enable Event Reporter
config.event_reporter.enabled = true

# Configure CI
# Create config/ci.rb (see examples above)

Requirements

  • Ruby: 3.2+
  • Rails: Upgrade from 8.0
  • PostgreSQL: 13+ (for continuations)

⚠️ Deprecations

  • ⚠️ Rails 7.0: End of Life (EOL)
  • ⚠️ Rails 7.1: End of Life (EOL)
  • Rails 8.0: Extended support (+6 months)

💡 Conclusioni

Rails 8.1 è una release production-focused:

Job Continuations - Long-running jobs interrompibili e resumable ✅ Event Reporter - Structured logging per observability ✅ Local CI - CI senza cloud vendor lock-in ✅ Kamal Secrets - Deploy semplificato con credentials ✅ Association Deprecations - Smooth API migrations ✅ Markdown Support - Native markdown responses ✅ 500+ contributors - Community-driven improvements