00:00:10.480
Hi everyone. Oh, hi! Hi, Wayne. Okay, um, my name is Konstantin and I'm going to talk to you about real-time Rack.
00:00:15.880
But first, I want to thank Engine Yard for sponsoring my trip here. I'm really grateful for that; otherwise, I couldn't be here to talk to you.
00:00:29.800
A bit about me: I'm on GitHub and on Twitter, and I have a blog like probably many of you. Back in Germany, where I'm originally from, I'm a student at the Hasso Plattner Institute of IT Systems Engineering, which is in Potsdam, close to Berlin. If you ever want to visit, I'd be happy to show you around.
00:00:49.920
In my free time, I'm also maintaining Sinatra. I don't know if you've heard of it; it's quite popular. I'm also maintaining a fork or a clone called Almost Sinatra, which is probably going to be more popular than Sinatra soon because it's only eight lines.
00:01:33.920
I recently started working on a book about Sinatra, which is supposed to be released next month if I can continue writing on time. If you want to learn more about Sinatra, please get that book at 'Sinatra Up and Running.' I'm also a member of the Rack core team, and I'm going to talk a bit more about what Rack is in case you're not fully aware of that.
00:02:03.680
Currently, I'm doing an internship in Portland, also sponsored by Engine Yard, working full-time on Rinas, an alternative Ruby implementation, in case you haven't heard of that.
00:02:11.400
Okay, this talk is about breaking the laws of Rack. But why do we want to break the laws of Rack? Back in the day, the web worked like this: you put in a URL, requested a page, and got a response back from the site, which your fancy web browser displayed. But that's pretty boring.
00:02:25.519
So, along came Ajax, doing all those fancy requests where you don't really notice data being fetched while you're on the page. Ajax is really the client requesting data from the server, but in some cases, you want the server to push data to the client. People came up with something called Comet, which is basically Ajax but keeps polling the server to get messages.
00:03:06.000
However, this isn't what I mean when I talk about real-time. Real-time means streaming from the server to the client and pushing data at any time without the client having to request it each time. The basic idea is to decide what to send while streaming, not upfront. This is particularly useful for streaming APIs, like what Twitter does. If you've written a Twitter client, you've probably come across this, or Service-Sent Events.
00:03:45.400
You've probably heard about WebSockets, which are basically the foundations for implementing web circuits over Rack. But I'll start with a demo. You've all probably seen this prompt before, which can calculate stuff or write out code. This is basically a clone of IRB, and the cool thing is, it's running in my web browser.
00:04:00.960
What makes it really fancy is that it's sending the data to the server—the string to evaluate—and then the server evaluates it and returns the result to the client using Server-Sent Events, which is secure, but for a demo, it's fine. The real fun begins when I fire up a second browser, which could be on another machine. When I run some code here, it actually sends it out to both clients—or all clients if you'd connect to that port. I have a firewall on, so don't even try.
00:04:51.479
So, this is basically what I'm talking about when I mention real-time Rack, and I'm going to show you how to do this. First, let’s ensure we’re all on the same page regarding how Rack works internally. Rack is essentially a Ruby to HTTP to Ruby bridge, serving as a specification for translating HTTP requests into Ruby objects and back into HTTP responses.
00:05:41.400
It's also a middleware API and the foundation for basically every Ruby web framework out there, including Rails, Sinatra, and so on. The basic structure looks like this: you have the HTTP client, which is usually your browser but can be anything else, talking to what we call the Rack handler, essentially a web server like Thin or Mongrel.
00:06:01.680
The handler is responsible for translating from HTTP and then using the Rack protocol to communicate with any middleware. The so-called endpoint is what you typically define when writing your Rails application or Sinatra application, where Rails and Sinatra set up some middleware in front for all the routing and filtering.
00:06:36.039
What a basic Rack application looks like, if you're not using any framework, is essentially a proc that takes an ENV (an hash of the past HTTP headers and some environment information) and then returns an array with three elements: the first is the status code, the second their headers as a hash, and the last one is an object that loops through strings that will be sent to the client.
00:07:04.919
The object returned doesn't have to be a proc; it can be any object that responds to 'call' and takes in ENV. I'll use a bit of Sinatra during my talk to cut down on boilerplate, but Sinatra really embraces Rack. You could just return the response here for clarity.
00:07:31.759
Now, what the handler does is pause the HTTP response into an ENV and then calls 'call' on that. After that, it sends out the headers and calls each on the body object, then closes the connection. This overview reflects the general code structure you'll find in most Rack handlers.
00:08:11.560
Before I discuss streaming, let's take a look at middleware. This is a sample middleware that takes the body of your Rack endpoint application and converts everything into uppercase. Who doesn't want that, right?
00:08:51.480
What it does is store the endpoint or the next middleware in the middleware chain since you can use more than one middleware. When it gets a call, it will send that call to the application. It could modify the ENV or keep a list of applications to perform other nice actions, but that doesn’t matter for streaming.
00:09:33.040
Then it calls each on the body object, uppercases everything, and returns the result. Note that I'm not using 'map' here as that would violate the Rack specification since it's not guaranteed that the body object responds to 'map'.
00:09:36.480
This is how you would set up this middleware using a 'config.ru' file. If you have a Rack application, it's usually already there, or you could use some Rack or Rails interfaces to add middleware. The server's responsibility, instead of using the previous line from the Rack handler code, is now to call 'new' on the middleware, passing the endpoint to it, and then calls 'call'.
00:10:06.839
If you have any questions about this setup, please ask now. I want to ensure we're all on the same page. I once gave this talk at RubyConf and people mentioned they lost the thread halfway.
00:10:59.680
So let's dive into streaming. The body object must respond to 'each', but other than that, it can be anything. This allows us to implement some streaming. A simple application could send the current time 20 times, always waiting one second in between each send. If you use the browser or Curl, you can observe that the response comes in bit by bit with a one-second delay.
00:11:53.000
Once again, I’m using Sinatra for simplicity here. But let’s do something fancier: a messaging service where you have a list of clients to whom you can send data.
00:12:09.760
The goal is to maintain a list of subscribers. Whenever someone opens a connection, we keep it open, and whenever you post to the application, it should send a message to that connection.
00:12:32.240
Here's a sample approach: The subscriber will wait for new messages to come in using a sleep, which blocks the current thread. Then, whenever we send out data, we wake up that thread which we had stored when 'each' was called, and send the data.
00:13:03.760
This works fine on some servers, but there are issues. It can block the current thread with the sleep, which becomes problematic if you don't have a separate thread for every request.
00:13:50.120
Typically, if you have a server using a thread pool, it will handle requests simultaneously only based on the size of that thread pool, which can limit scalability.
00:14:11.840
It does not work well with certain middleware; for example, if we chain the uppercase middleware in front of it, it wouldn't work because the uppercase middleware waits until each completes before it sends its result to the handler.
00:14:56.120
This means you would never see any result. Additionally, it doesn't perform well with evented servers like EventMachine or Rainbows. So, let's explore how we can improve this scenario.
00:15:17.440
My presentation is actually running on Thin, so it must work somehow. The solution is to adopt evented streaming, avoiding a thread per request.
00:15:54.080
In this context, 'evented' means implementing an event loop, typically run by EventMachine. This is essentially a continuous loop that checks for registered callbacks triggered whenever new data or connections come in.
00:16:14.360
Data that needs to be sent out is registered, and you perform all actions in callbacks registered throughout the stream. This approach allows for scalability to handle a large number of connections.
00:16:45.920
If you take this non-evented application, you'll notice it waits 10 seconds, only firing the request to Rack afterward. In contrast, when using EventMachine, you can set up an event loop to trigger callbacks independently of delaying responses.
00:17:30.960
You can use timers to trigger callbacks after certain intervals, managing both the event loop and incoming requests seamlessly, allowing for concurrent processing in a single-threaded environment.
00:18:18.560
Now, bringing this back to our Rack application, we might leverage a nifty trick called the Async callback, where we send out our Rack response (the status code, headers, and body) whenever our timer callback is fired.
00:19:15.320
This requires a little magic to signal the Rack handler that we're postponing the response. The handler will be informed to proceed with the event loop while we handle other functionalities.
00:20:07.680
There are multiple ways to implement this: one is using 'throw' for skipping middleware and directly reaching the Rack handler or returning a status code of -1, which is generally cleaner.
00:20:41.600
Also, there's a nice library called Async Sinatra, which includes the necessary async methods (for GET, POST, etc.) to avoid manual signaling to the Rack handler for asynchronous execution.
00:21:56.080
When using Async callbacks, we can make calls to other systems such as querying a database or making HTTP requests while still processing requests concurrently.
00:22:39.560
Now, consider an application designed for service and events, which are essentially a new standard for HTTP from W3C. It's a one-way channel, allowing you to implement a socket-like functionality without the complexities of websocket settings.
00:23:21.680
The benefit of service and events over websockets is the simplicity of the implementation, especially in browsers like Firefox, Chrome, and even older ones like Internet Explorer 7, with automatic reconnections.
00:24:00.960
For client implementation, you can easily add JavaScript calls that react to data being sent from the server using HTTP headers, ensuring a straightforward stream. The connection remains open long enough for the server to decide when to send data, prefixed correctly to avoid confusion.
00:25:21.320
For tracking of sent data, you can also manage IDs which indicate the position in a stream for a reconnection process, besides accommodating multiline messages seamlessly.
00:26:09.200
Lastly, I want to touch on websockets, which provide a two-way communication channel effectively resembling event sources with an added send method. However, this creates additional complexity in the Rack implementation due to their nature and the need for browser support.
00:27:27.920
Remember, if your main goal is to send data to a client, service and events present a much simpler and more manageable solution.
00:28:00.720
Lastly, there's a new protocol called HTTP/2, which is spearheaded by Google. It offers multiple request channels over a single SSL connection, allowing for simultaneous data transfers back to clients.
00:28:34.760
Okay, that's it! Thank you!
00:28:37.279
My slides are up on GitHub.