00:00:00.030
Let's get started a bit early so that we know you are all here. Please gather at the front so we can swiftly do the switcheroo.
00:00:06.240
This is a reminder for everyone who has coached or organized the Rails Girls workshop before; please join me in the front during the break for a photo.
00:00:12.889
Now, I'm excited to introduce our next speaker, Pan Thomakos. Pan leads the productivity engineering team for Ruby on Rails and the JavaScript platform. He loves developing automation to reduce manual tasks.
00:00:25.320
Recently, he encountered a rather weird bug while trying to solve an issue, which led him into the depths of Rack. So, without further ado, let's give a warm welcome to Pan!
00:00:40.860
Thank you! I’m excited to share a little bit about this adventure I went on, and through that, I hope to help you learn about Rack just as I did along the way. For those of you sitting in the back who can’t see me well, I look like this, but my beard is a little longer than in this picture, and I’m not wearing a button-down shirt.
00:01:04.780
I work for a company called Strava, which is a social network and training log site for athletes. Basically, you might go on a run or a bike ride, upload a bunch of GPS and power meter data, as well as heart rate data. We allow you to delve into that data to uncover cool insights about yourself and compare them with your friends and past performances.
00:01:23.710
Today, I want to discuss a double refresh bug that I encountered in our Rails application, which I refer to as the "double refresh bug." Let me explain why. In our application, there's a screen that enables you to change your display preferences. For instance, as an athlete in Europe, you might want to see metrics in kilometers and kilograms. Meanwhile, if you live in the United States, you'd want to see those in miles and pounds.
00:01:49.660
So, here's how this bug works: I go to a drop-down menu and select "miles" and "pounds" instead of the default "kilometers" and "kilograms." After clicking save, there's an AJAX action that hits the server to persist that preference. However, when I refresh the page, the value flips back to kilometers and kilograms. After refreshing again - hence the "double refresh" - it finally updates to miles and pounds.
00:02:28.420
At this point, my initial reaction was to determine if this issue was impacting the database. Am I persisting the correct value, or is it simply a code issue? I needed to narrow down the possibilities.
00:02:40.299
I wrote a query to hit the database, and luckily, it confirmed that the value is actually being persisted correctly. This action gave me some insight that I needed to investigate further. Our settings controller handles all these display preference views, containing an athlete method. This method takes the current athlete since you're only allowed to modify your own preferences and wraps it with a permissions decorator.
00:03:15.500
It's not vital, but it exposes that athlete to all the controller methods. So, I dug into my debugging toolbox and pulled out Pry. For those unfamiliar, Pry is like IRB but comes with numerous cool additional features.
00:03:28.370
One of my favorite features is that it allows you to invoke Pry on a Ruby binding object, acting as a breakpoint to interactively engage with the code.
00:03:33.620
I inserted a binding.pry call into my athlete method, refreshed the page, and suddenly I was in an interactive console. This console informed me of precisely which file I was in and the specific line of code running. Moreover, it provided a neat visual representation of where I was in that method.
00:03:56.120
Excitedly, I asked, "What is the measurement preference for the current athlete?" and it responded with "meters," which was incorrect since I was trying to switch to feet. I thought, let's use Active Record instead and query the database directly using the same athlete ID to see its value. Interestingly, it returned "feet." This discrepancy was puzzling.
00:04:29.330
To investigate further, I looked at the method definition for the current athlete, which employs a find method from the application controller. There are two ways to log in: through a remember cookie, which is a long-lived session, or through the current session cookie.
00:05:08.750
I confirmed I was using the current session since I was actively engaging with the website. Diving deeper into this method revealed additional code, but the key point is that I'm using the session ID to determine my athlete ID.
00:05:21.770
Next, I thought it would be apt to explore how the athlete.fetch behaves. Unfortunately, when I did this, I still received "meters," indicating it was the same as the current athlete's value.
00:05:39.949
Thus, I needed to analyze the method definition for fetch. The fetch method has a simple API: it takes an ID like the find method and hands off to this entity map.
00:06:07.620
As it turns out, the entity map is a request-level cache that saves time when requesting an object by its primary key, allowing you to skip hitting the database.
00:06:31.369
This approach means you don't need to go back to the database if that object is already cached in memory. When you issue requests, the method generates a cache if one doesn’t exist and retrieves requested primary keys.
00:06:43.879
However, when I tried fetching a preferred value again, I still got the incorrect meter value. When I cleared the cache and requested it again, I finally got the correct value.
00:07:11.929
Having established this, I pondered: how do I clear this cache during web requests? I immediately thought about Rack middleware, knowing each request requires a fresh cache.
00:07:31.459
For those unaware, Rack serves as a minimal interface between web or application servers and your Ruby application that communicates via HTTP.
00:07:53.290
You might have web servers like Puma or Unicorn on one end and Ruby frameworks like Rails, Hanami, or Sinatra on the other, all speaking Rack and thus interchangeable.
00:08:12.070
So what does Rack need to generate an HTTP response? Essentially, we need a status code, headers indicating the content type, and the actual response body.
00:08:43.310
A Rack response is represented as an array containing three main elements: the status, the headers (a hash), and the body (the actual content such as JSON or HTML).
00:09:09.230
However, the Rack API allows me to send back multiple bodies as an enumerable. This can be useful for buffering and streaming, so while I generate the response, I can send preliminary parts to the browser.
00:09:34.320
Middleware is where it gets interesting. Rack handlers talk to servers while your Ruby application, on the other end, does its length according to the middleware.
00:10:35.069
You'll understand soon that timing management in middleware can leach a lot of resources in real-time configurations. Let's create a sample middleware to keep track of requests.
00:10:50.100
After processing the response through the middleware stack, I can measure CPU time. Here's an example of a 'Timer' middleware to measure request durations.
00:11:11.220
I'll track the time at the beginning and return the original response at the end. However, there's the catch that the middleware handler iterates after returning, which might not reflect accurate timings for slower responses.
00:11:30.420
So how do we know when our response is truly complete? Rack provides the 'close' method: when the request has gone through the entire response pipeline, it can call close, ensuring completion of all tasks.
00:11:44.430
To adjust my middleware to accommodate this, I create a body proxy. The proxy wraps the original body and runs additional code when it closes, allowing me to account for various lifecycle events in the middleware.
00:12:05.170
This is an effective technique for separating calling concerns. The body can handle the response tasks, while the close method manages the closure aspect rightly.
00:12:25.050
In this middleware system, we can leverage Rack's body proxy. By returning original status and headers alongside my new proxy body, I account for all processes.
00:12:45.110
Now, I managed to fix timing issues successfully by updating how I tag my middleware. After testing, I can determine where the hanging issue lies.
00:13:02.660
I ran into this peculiar display preferences problem; oddly, I have to refresh the page twice to get the accurate response from the database. After pinpointing the issue to the entity map...
00:13:36.850
...I learned that we cleared our entity cache when migrating from Rails 3 to Rails 4 and transferred functionality to the entity map.
00:13:50.830
Before it cleared, we had a middleware piece that cleared that entity map at the end of the request cycle - this behavior is essential for accurate data interpretation.
00:14:09.100
When I added in my 'bind pry' calls to observe close calls, I refreshed and found my console, reassuring myself about middleware interactions.
00:14:31.650
As I scrutinize my requests, I map out proxies, but I realize my entity map middleware proxy does not exist in the expected places.
00:14:45.220
After validation, I reviewed the sequence: where's my entity map, and what happens in different HTTP requests? Once identified, I noticed the middleware flow.
00:15:01.999
Now, I hypothesize a connection between an unwanted middleware layer reforming requests and returning responses lacking a proper pipeline closure.
00:15:23.160
After rigorous debugging, I discover the root cause to stem from an unexpected caching mechanism inside the e-tag middleware.
00:15:48.599
The conditional handle did not enact closure calls to the subsequent middlewares, demonstrating how core functionality can falter.
00:16:02.440
After addressing the flaw, it became clear the patch was essential for synchronous execution. By directly addressing these lifecycle prompts, we can better inform prospective middleware.
00:16:16.859
In the aftermath, I think of restoring the coding guidelines; maintaining clean integrity in middleware tracking is vital for all workflows.
00:16:41.150
If you're wondering how to enhance the Rack API, I propose adjustments: clarifications around lifecycle management offer better user insights.
00:16:57.329
Upon close confronted by the concern about user programming errors, we can rethink descriptive programming practices or offer insightful automation on our execution responses.
00:17:18.650
Summarizing this adventure: we faced a dubbing bug, improved middleware coherence, and encountered reducing latency in associated requests.
00:17:39.300
Although we resolved our original double-refresh issue, the overall experience taught me invaluable lessons concerning request lifecycle nuance and middleware coordination.
00:18:01.329
In summary, whilst addressing the code dependencies, we now have better implementations ensuring integrity with the additional edge case coverage.
00:18:20.580
I'm appreciative of these explorations into the workings of Rack and how minor adjustments can streamline complex request-response behaviors.
00:18:43.230
Thank you, everyone, for being here today! I'm Pan Thomakos, and I hope this talk inspired you in some way as well!
00:19:10.330
If you are a lightning talk speaker, please come to the front as we prepare for the next session.