Ruby
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Methods and Blocks
-
Section 4: Object-Oriented Ruby
-
Section 5: Metaprogramming
-
Section 6: Working with Data and Files
-
Section 7: Ruby Frameworks Overview
-
Section 8: Ecosystem and Testing
-
Section 9: Practical Projects
-
Section 10: Interview Practice
-
Section 11: Data Structures and Algorithms in Ruby
-
Section 12: More Practice Exercises
-
Section 13: Enumerable and Functional Style
-
Section 14: More OOP Practice
-
Section 15: More Testing
-
Section 16: Enumerable and Comparable Modules In Depth
-
Section 17: Ruby Standard Library: Core Utilities
-
Section 18: Ruby Standard Library: Data and Security
-
Section 19: Ruby Standard Library: CLI and Text
-
Section 20: Ruby Networking
-
Section 21: Ruby on Rails Deep Dive
-
Section 22: Ruby Metaprogramming Deep Dive
-
Section 23: Ruby Design Patterns
-
Section 24: Ruby Concurrency
-
Section 25: Ruby Testing Deep Dive
-
Section 26: Ruby Gems and Packaging
-
Section 27: Ruby Performance
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Rails API Development
-
Section 31: Rails Authentication and Authorization
-
Section 32: Rails Testing Deep Dive
-
Section 33: Rails Performance
-
Section 34: Rails Deployment
-
Section 35: Sinatra and Lightweight Ruby Web Apps
-
Section 36: More Ruby Language Deep Dive
-
Section 37: Ruby 3.x Modern Features
-
Section 38: More Data Structures in Ruby
-
Section 39: More Practical Projects
-
Section 40: Ruby Ecosystem Tools
-
Section 41: More Practice and Review
-
Section 42: Final Practice and Mastery
-
Section 43: Ruby Interview Deep Dive
-
Section 44: Ruby Background Processing Deep Dive
-
Section 45: Ruby GraphQL
-
Section 46: Ruby Object Model Deep Dive
-
Section 47: Ruby Hanami Framework Overview
-
Section 48: Ruby gRPC and Protocol Buffers
-
Section 49: Ruby Data Processing
-
Section 50: Ruby Search Integration
-
Section 51: Ruby File Upload and Media
-
Section 52: Ruby Email and Notifications
-
Section 53: Ruby Admin Panels
-
Section 54: Ruby Feature Flags and Experimentation
-
Section 55: Ruby Monitoring and Observability
-
Section 56: Ruby Docker and Deployment Deep Dive
-
Section 57: Ruby Security Deep Dive
-
Section 58: More Advanced Metaprogramming
-
Section 59: More Final Projects
200: Streaming RPCs in Ruby
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
.protodefinition to create aDeliveryServicewith a methodTrackOrderthat returns astream 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.
There are no comments for now.