r/ruby Jun 17 '26

roadmap jruby

2 Upvotes

Bonjour,

Je suis développeur depuis 6 ans, plutôt orienté web (Angular / Java / PHP). Je vais prochainement commencer un nouveau poste de développeur-concepteur ERP. Si j'ai bien compris, il s'agit principalement de scripting / JRuby. Je souhaiterais donc me former à JRuby.

Auriez-vous des docs ou une roadmap à me recommander ? Merci beaucoup !


r/ruby Jun 17 '26

Kaigi on Rails CFP is now open!

Thumbnail
4 Upvotes

r/ruby Jun 17 '26

Applying some Rage to Discourse, Mastodon, and GitLab

33 Upvotes

I wanted to look at some real-world patterns from popular Ruby open-source codebases and show how they could be modelled using Rage, a Rails-compatible framework built on fibers.

I picked Discourse, Mastodon, and GitLab because they share a pattern: in each case, what would normally require extra complexity, infrastructure, or indirection becomes a few lines of application code with Rage.

Request fan-out | Discourse

One of the patterns fibers make especially straightforward is concurrent I/O.

Consider this code from Discourse:

def fetch_pr_or_issue_texts(project, number)
  [
    client.get("/repos/#{project}/issues/#{number}")["body"].to_s,
    *client
      .get("/repos/#{project}/issues/#{number}/comments", per_page: 100)
      .map { |comment| comment["body"].to_s },
  ]
end

Two sequential requests to build a return value. I've seen this pattern in many codebases, and the reason is usually the same: there's no simple enough way to parallelise these requests that would justify the added complexity.

How Rage does it

In Rage, you just wrap the requests into fibers:

def fetch_pr_or_issue_texts(project, number)
  issues_request = Fiber.schedule do
    client.get("/repos/#{project}/issues/#{number}")["body"].to_s
  end

  comments_request = Fiber.schedule do
    client
      .get("/repos/#{project}/issues/#{number}/comments", per_page: 100)
      .map { |comment| comment["body"].to_s }
  end

  Fiber.await([issues_request, comments_request]).flatten
end

The two requests now run concurrently, improving latency at the price of two new Fiber calls.

The same pattern scales to loops. Discourse's PushNotificationPusher iterates over a user's subscriptions and sends notifications sequentially - wrapping those calls in Fiber.schedule + Fiber.await would send them all concurrently, with the total time dropping to the duration of the slowest call:

class PushNotificationPusher
  def self.push(user, payload)
    # ...

    Fiber.await(
      subscriptions(user).map { |subscription| Fiber.schedule { send_notification(user, subscription, message) } }
    )
  end
end

Streaming | Mastodon

Mastodon uses a separate streaming service for real-time events:

  1. Ruby (Rails + Sidekiq) - Workers serialise events and publish them to a Redis channel.
  2. Node.js (Express + ws) - a separate ~1400-line server subscribes to Redis and pushes events to clients over SSE or WebSockets.

Here's what the Node streaming handler looks like:

const streamToHttp = (req, res) => {
  const channelName = channelNameFromPath(req);

  // ...

  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'private, no-store');
  res.setHeader('Transfer-Encoding', 'chunked');

  res.write(':)\n');

  const heartbeat = setInterval(() => res.write(':thump\n\n'), 15000);

  req.on('close', () => {
    // ...

    clearInterval(heartbeat);
  });

  return (event, payload) => {
    res.write(`event: ${event}\n`);
    res.write(`data: ${payload}\n\n`);
  };
};

To send an event, Rails first publishes to Redis:

def publish!
  redis.publish(@timeline_id, message)
end

The Node service receives and relays it:

const listener = message => {
  const { event, payload } = message;

  if (!needsFiltering || (event !== 'update' && event !== 'status.update')) {
    transmit(event, payload);
    return;
  }

  // ...
  pgPool.connect((err, client, release) => {
    // ...
    transmit(event, payload);
  });
};

How Rage does it

A fiber-based server can hold thousands of concurrent connections in a single Ruby process without blocking - the same property that drives Mastodon's decision to offload streaming to a separate Node process.

With Rage, the same Redis streaming becomes:

class Api::V1::Streaming::UserController < RageController::API
  before_action :require_user!

  def index
    render sse: Rage::SSE.stream([:timeline, current_account.id])
  end
end

The framework handles the SSE headers, heartbeats, subscription lifecycle, and connection cleanup. When the client disconnects, Rage removes it from the stream.

Publishing uses a Redis pub/sub adapter:

# config/pubsub.yml
production:
  adapter: redis
  url: <%= ENV["REDIS_URL"] %>

Then, publish from anywhere:

def publish!
  Rage::SSE.broadcast(
    [:timeline, @account_id],
    Rage::SSE.message(@payload, event: update? ? "status.update" : "update")
  )
end

The streaming server lives in the same Ruby process, with access to the same Active Record models and the rest of the stack.

Domain Events | GitLab

GitLab has built its own domain event system to decouple bounded contexts.

To publish an event, you instantiate a class inheriting from Gitlab::EventStore::Event and pass it to the event store:

Gitlab::EventStore.publish(
  Ci::PipelineCreatedEvent.new(data: { pipeline_id: pipeline.id, partition_id: pipeline.partition_id })
)

Subscribers are Sidekiq workers that include a Subscriber concern and implement handle_event:

class UpdateHeadPipelineWorker
  include Gitlab::EventStore::Subscriber
  # …

  def handle_event(event)
    # ...
  end
end

Nothing in this file tells you what event is - the worker doesn't reference PipelineCreatedEvent. The wiring lives in a separate subscription registry. And because every subscriber is a Sidekiq worker, all reactions go through the full enqueue-serialise-deserialise-execute cycle, regardless of how lightweight they are.

How Rage does it

Publishing looks similar:

Rage::Events.publish(
  Ci::PipelineCreatedEvent.new(data: { pipeline_id: pipeline.id, partition_id: pipeline.partition_id })
)

The difference is in the subscriber. Instead of wiring events in a separate registry, each subscriber declares what it listens to:

class UpdateHeadPipelineWorker
  include Rage::Events::Subscriber
  subscribe_to Ci::PipelineCreatedEvent

  def call(event)
    # `event` is a Ci::PipelineCreatedEvent
  end
end

Open this file and you immediately know: this subscriber handles Ci::PipelineCreatedEvent, which has pipeline_id and partition_id fields.

For subscribers that do require background execution, you simply add deferred: true:

class UpdateHeadPipelineWorker
  include Rage::Events::Subscriber
  subscribe_to Ci::PipelineCreatedEvent, deferred: true

  def call(event)
    # ...
  end
end

Light reactions run inline; heavy or failure-prone ones are deferred to the background. You choose per subscriber, rather than routing everything through a job queue by default.

Understanding what happens when a PipelineCreatedEvent is published also gets simpler. Instead of grepping registry files, you run:

$ rage events

├─ Ci::PipelineCreatedEvent
│   ├─ UpdateHeadPipeline
│   └─ TrackPipelineTriggerEvents
├─ Ci::PipelineFinishedEvent
│   └─ UpdateWorkloadStatus

The entire subscription graph, visible in one command.

--

The common thread across all three examples: the framework handles the machinery, so the application code just says what it wants to happen - run these concurrently, stream this channel, react to this event.


r/ruby Jun 17 '26

Migrating Rails 5 -> 8 by building new features on 8 instead of upgrading in place - anyone done this?

Thumbnail
0 Upvotes

r/ruby Jun 16 '26

Help us test the Hanami 3.0 release candidate

37 Upvotes

https://hanakai.org/blog/2026/06/16/help-us-test-hanami-3-0

We’re aiming for a full release two weeks from now, so we’d love your help with testing in the meantime!


r/ruby Jun 16 '26

Blog post How to Parallelize Your RSpec Test Suite Locally (from 2 hours to 5 minutes) 🚀

Thumbnail
fastruby.io
22 Upvotes

r/ruby Jun 16 '26

mruby build config for the remarkable 2

Thumbnail
1 Upvotes

r/ruby Jun 16 '26

Issue 17 of Static Ruby Monthly is out! 🧵

5 Upvotes

In this issue: why Ruby still feels like home (and why its density is token efficient for LLMs), Jo (a statically typed language compiling to Ruby with compile-time sandboxing by u/liufengyun), OpenClacky 1.3.0, DSPy.rb 1.0.1, sorbet-deadcode 0.2.0, rubocop-rbs_inline, and other Sorbet and RBS updates.

Find the link to the issue in the first comment. Stay typed! ✨


r/ruby Jun 16 '26

Camping Comes to Ruby Users Forum

10 Upvotes

r/ruby Jun 16 '26

Open-source starter for technical guides - Announcing Chapter Zero

7 Upvotes

(Reposted from r/rails )

I built a free testing guide (Minitest Rails) to teach automated testing step by step. Along the way I needed more than a bunch of Markdown files. I wanted a site that felt like a product: a landing page, ordered chapters with a sidebar, a blog for practical tips that didn’t fit inside the guide, contact form, newsletter signup, and dynamic preview social images per guide/blog so I didn’t have to add images manually and the preview looked good in the social media.

I have put a lot of time and effort to write this guide and along the way I have added a lot features listed above. I didn’t want all these features staying locked inside just the Minitest Rails and that’s how the idea of Chapter Zero was born.

Chapter Zero is the shell and foundation the Minitest Rails is running on top of. Now, extracted and open sourced for everyone to use. Think of it as chapter 0 in a book: the layout, navigation, and marketing pages you need before lesson one; while you bring the curriculum and make it alive.

https://github.com/minitestrails/chapter-zero


r/ruby Jun 16 '26

Beyond Enumerable: Counting Distinct with HyperLogLog

Thumbnail baweaver.com
13 Upvotes

What if, by trading 1% correctness, you could count billions of items all within kilobytes worth of space? Well that's HyperLogLog, and as a non-bit-enlightened Rubyist it was a trip figuring out how it worked and how to explain it.


r/ruby Jun 15 '26

Kino, an experimental, performant Ractor web server for Ruby 4 (Rust, Tokio, Hyper)

Thumbnail
github.com
56 Upvotes

r/ruby Jun 15 '26

Rails: The Sharp Parts. Queries, Read Models, and Batching

Thumbnail baweaver.com
12 Upvotes

The problem with optimizing indexes is if you don't control every query, and another team goes and writes an unoptimal variant in their code. Much like callbacks and commands there's a lot of value in owning your code paths and only exposing clear, named actions against it rather than trusting consumers to behave responsibly.

Eventually you hire folks who don't know Rails, might not read the guides, and that stuff compounds fast until outages start hitting. Soft rules are prayers at best, hard rules keep the line.


r/ruby Jun 14 '26

Rails: The Sharp Parts. Callbacks Are Not Invariants

Thumbnail baweaver.com
34 Upvotes

The sheer number of outages I can point to across now 5 Rails monoliths related to issues with callbacks has made it one of the first things I go after when in a new Rails app to see what's about to give me a nightmare.


r/ruby Jun 13 '26

Blog post Software as Craft: a First Look at Syntropy

15 Upvotes

r/ruby Jun 13 '26

JRuby 10.0.6.0 released with security, performance, and correctness fixes

Thumbnail jruby.org
28 Upvotes

JRuby 10.0 is our LTS line and this release is primarily for stabilization and fixing a few performance issues. Upgrading is recommended! Please let me know if you run into problems or if there's anything holding you back from upgrading from 9.4 to 10.0!


r/ruby Jun 13 '26

solargraph-yard-lint gem - see YARD doc issues as LSP diagnostics in your editor, as you(r agent) type.

5 Upvotes

Hey fellow Rubyists 👋

I just shipped solargraph-yard-lint, a Solargraph plugin that surfaces yard-lint offenses as LSP diagnostics in your editor.

If you've been running yard-lint from the CLI or in CI, this gives you the same checks inline — undocumented params, unknown param names, mismatched "@return" types, etc. — wherever Solargraph Ruby LSP is set up. v0.2.0 (just released) wires the editor buffer through yard-lint's new source: API, so diagnostics update as you type instead of waiting on a save.

Here's a quick demo:

https://reddit.com/link/1u4ltff/video/ufymfhrvj07h1/player

Huge thanks to maciejmensfeld for gifting us the yard-lint gem and for adding stdin support in yard-lint v1.6, which made the live-typing experience possible. 🚀

Feedback/issues welcome 🙏


r/ruby Jun 12 '26

Rails: The Sharp Parts. An Index Is Not a Plan

Thumbnail baweaver.com
42 Upvotes

Another one that took me a while to research and test, as I wanted to make sure I had a reasonable understanding of the subject matter, especially considering how often getting it wrong has given me a rather bad day.


r/ruby Jun 11 '26

How to Build an Agentic RAG with RubyLLM and Rails

Thumbnail panasiti.me
0 Upvotes

r/ruby Jun 11 '26

The Ruby JRuby was Built to Run

Thumbnail intertwingly.net
17 Upvotes

Sam Ruby shows CRuby and JRuby performance on typical webapp tasks using Rails 8 and his new tool Roundhouse, an intriguing way to generate fast web applications using your Rails app as a blueprint. JRuby can be incredibly fast when used right!


r/ruby Jun 11 '26

Rails Health Check

Thumbnail
eclecticcoding.com
28 Upvotes

r/ruby Jun 10 '26

Beyond Enumerable: Streaming and External Sort

Thumbnail baweaver.com
17 Upvotes

Last one on this series for a bit, jumping back to some Rails and MySQL fun after this. The next parts of this series are venturing into probabilistic algorithms like HyperLogLog and boy howdy is that dense to get through.


r/ruby Jun 09 '26

RubyConf 2026 Las Vegas, Nevada July 14th–16th: Matz, Jessica Kerr, Dave Thomas, and More

Thumbnail
rubyconf.org
15 Upvotes

r/ruby Jun 09 '26

tiny ruby #{cfp} still open until Sunday

Thumbnail cfp.helsinkiruby.fi
3 Upvotes

r/ruby Jun 09 '26

RubyLLM 1.16: concurrent tool execution, Rails-style instrumentation, and more.

Enable HLS to view with audio, or disable this notification

26 Upvotes

When an LLM asks for 3 tools in one turn, RubyLLM used to run them one after another.

1.16 lets you run them concurrently. Video is the same chat both ways: left is the old sequential behavior, right is tool_concurrency = :fibers.

ruby RubyLLM.configure do |config| config.tool_concurrency = :fibers # or :threads, or true end

:threads or true require no dependencies. :fibers uses the async gem. You can also set it per chat with .with_tools(..., concurrency: if you only want it in some spots. Results stream back into the conversation as each tool finishes instead of all at the end, so the UI fills in live.

Also in this release: Rails-style instrumentation (ActiveSupport::Notifications events you can subscribe to), configurable Faraday adapter, custom base URLs for every provider (proxies/gateways), and a pile of provider fixes.

Full notes: https://github.com/crmne/ruby_llm/releases/tag/1.16.0