Skip to Content
Course content

200: Streaming RPCs in Ruby

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Up until now, our RPC calls have been pretty straightforward: you send a request, the server does some work, and it sends back a single response. That's "unary" RPC. But in the real world, you often have data that arrives in chunks—like a live feed of system logs or a stock ticker—and waiting for the entire dataset to be ready before sending it is a great way to crash your memory or time out your connection.

Today, we're going to build a Live System Metric Streamer. The goal is to have a server that pushes CPU and Memory usage updates to a client every second without the client having to poll the server repeatedly. This is where server-side streaming comes into play.

Defining our Metric Stream in Proto

First, we need to tell gRPC that this specific method isn't returning a single object, but a stream of them. In our .proto file, we do this by adding the stream keyword to the return type.

syntax = "proto3";

package metrics;

service MetricService {
  // The 'stream' keyword here is the magic part
  rpc StreamMetrics (MetricRequest) returns (stream MetricResponse);
}

message MetricRequest {
  string server_id = 1;
}

message MetricResponse {
  double cpu_usage = 1;
  double mem_usage = 2;
  int64 timestamp = 3;
}

I've kept the request simple. We just pass a server_id so the server knows which machine's stats we're interested in. The response, however, is marked as stream MetricResponse, which tells the Ruby gRPC runtime to expect multiple messages over a single long-lived connection.

Building the Server's Push Logic

Now we implement the service. In Ruby, a streaming RPC method doesn't just return a value; it needs to provide a way to yield multiple values over time. I'll start by creating a basic loop that simulates reading system stats.

class MetricServer < Metrics::MetricService::Service
  def stream_metrics(request, _call)
    puts "Client requested metrics for #{request.server_id}"
    
    # I want to send 10 updates and then close the stream
    10.times do
      # Simulating system data
      response = Metrics::MetricResponse.new(
        cpu_usage: rand(1.0..100.0),
        mem_usage: rand(1.0..100.0),
        timestamp: Time.now.to_i
      )
      
      # This is where the streaming happens
      yield response
      sleep 1
    end
  end
end

Fixing the "One-and-Done" Response

Here is where I usually trip up when I first switch from unary to streaming. In my first draft of this code, I tried to collect all the responses into an array and return that array at the end of the method. I thought, "Well, an array is just a collection of responses, right?"

Wrong. If you return an array, the gRPC library treats it as a single response object (or fails entirely because it doesn't match the proto definition). The server-side streaming implementation in the Ruby gRPC gem relies specifically on the yield keyword. By yielding, you're telling the server to push that specific message down the wire immediately and keep the connection open for the next one.

If you find your client only receiving one message and then closing, check your server method. If you see a return statement with a list, you've built a unary call in a streaming costume. Use yield.

Connecting the Client to the Flow

On the client side, the experience is different. Instead of getting a single response object back, the call to stream_metrics returns an Enumerator. You can't just assign it to a variable; you have to iterate over it.

stub = Metrics::MetricService::Stub.new('localhost:50051', :this_channel_is_insecure)
request = Metrics::MetricRequest.new(server_id: "prod-web-01")

puts "Listening for metrics..."
# The response is an enumerable stream
stub.stream_metrics(request).each do |metric|
  puts "[#{Time.at(metric.timestamp)}] CPU: #{metric.cpu_usage.round(2)}% | MEM: #{metric.mem_usage.round(2)}%"
end

puts "Stream closed by server."

I love this pattern because the client doesn't need to know how many messages are coming. It just stays in that .each block until the server decides to stop yielding or the connection drops. It's incredibly efficient for real-time dashboards.




📋 Practical Task

Exercise: Build a Live Order Status Tracker

Imagine you are building a food delivery app. Instead of the user refreshing the page to see where their burger is, you want the server to stream status updates (e.g., "Order Received", "Preparing", "Out for Delivery", "Delivered") as they happen.

Your Task:

  • Modify the .proto definition to create a DeliveryService with a method TrackOrder that returns a stream OrderStatus.
  • Implement the server-side logic using yield. To make it realistic, create an array of statuses and yield one every 2 seconds.
  • Implement a client that iterates over the stream and prints each status update to the console with a timestamp.
  • Challenge: Add a condition where the server stops streaming immediately once the "Delivered" status is sent.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.