I had the pleasure of appearing on the Ruby Rogues podcast this week talking about how Pivotal Labs does Extreme Programing (XP). Accompanying me was ex-pivot Josh Susser, James Edward Gray, Avdi Grimm, and Charles Max Wood. I had more fun recording it than I would have imagined and the pivots here have enjoyed listening to it, I hope you will too!
Cucumber: When to Use It, When to Lose It
I had really poor experiences with Cucumber. The annoyance with Cucumber comes nearly all from a case of indirection. If devs are writing the Cucumber tests and then writing all the regular expressions to implement the steps, then you often wonder why the heck you’re not just writing tests in Ruby. SO WHY DON’T WE JUST WRITE RUBY AND SKIP THE INDIRECTION?!?!?!
On a recent project however, my anchor spent the first 3-4 weeks with a lot of hands-on time working with the PM to get the PM to write stories in Tracker with multiple scenarios in the “Given… When… Then…” syntax. We then paste it directly in to a .feature file and start defining the steps. We don’t try to reuse steps very much. This means steps.rb becomes a type of dumping ground and you just have to let that go. WHAT YOU HAVE TO DO is not put much in the steps.rb and build up a “language” that lives in your helper files which are well maintained and clean. This is the key. Some rules of thumb: steps should be short, one line is best. No Capybara in your steps, so don’t say “visit new_user_form” or “page.should have_content(…)” in your steps, put that in a helper method or custom RSpec matcher that is like “open_user_form” or “widget.should have_success_message” respectively. This way, when you look at the step because you are walking through the layers of indirection, it’s really clear what’s going on, and really straight forward to update when your UI changes – because it will.
You could (and should) do a lot of the same things in plain-Jane Capybara specs, it’s just good practice to be pulling up this domain specific language. The value of Cucumber comes in when a PM is writing the specs. It’s time consuming for that PM though. In an hour long story writing session, we get through writing about three stories. It works well for our team of 4 engineers and 1 designer. I don’t know that it would scale well to a team of 8 that needs more stories each week. We also take our Cucumber features and publish human readable documentation via Relish, so future devs or PMs could have some expectation about our intentions.
If only coders are reading and writing the specs, forget Cucumber. If non-coders are actually participating, then Cucumber starts to make sense. I find that we basically end up paying the specification cost up front. I know it works because in our team of 4, we rarely disagree on estimates (we always throw the same number). The payoff is that on this project we don’t have to turn to our PM for clarification nearly as much as I’ve had to on other projects.
The caveat though is that payment of cost up front. If you’re writing ten stories a week and throwing eight away, you’re wasting a lot of time and energy writing stories. Stories are fairly stable on projects where we’re doing re-writes and thus the feature set is known, or where the feature set is VERY well defined (as may be the case when cloning functionality inan existing product). In most other projects, the backlog gets shuffled around a good bit as things get released, users give feedback, and company priorities shift. In this later case, you want to put in some minimal effort upfront on story writing and pay the rest of the cost during IPMs and once the story is picked up by a pair. Cucumber has its place, it’s just not every place.
Org Chart Growth and Keeping Our “Flatness”
We’re growing. You may have heard that we were acquired in 2012. You might also have heard that we’ve been spun out again as a core part of a new company that shares part of our name. From when I started at Pivotal Labs back in 2009, we’ve grown by a factor of six just within ‘Labs and we’re now part of an organization that’s twelve hundred strong. If you were watching our org chart during this time you might have wondered where we got the magic beans for that growing bean stalk.
When I started four years ago we all “reported” to one person, and we talked a lot about being a “flat” organization. Then we got multiple offices, and added a local director layer. Then office head count grew beyond one director having a close relationship with all of the pivots and keeping up with director duties, so we added managers. Then we got enough managers and head count still growing that we needed associate directors. At the same time we got two more levels added on top of the head of our company when we were spun out. Given all of this change, you can relate to my astonishment when just last week I heard a pivot say, “We’re a flat organization.” Double woah.
For a pivot, one who is eight degrees of separation away from the head of the company, to say “we’re flat” means he’s feeling something that isn’t reflected in an org chart. As he talked more about it, he clarified that in terms of day-to-day individual, pair, and team activities there is still the same huge amount of autonomy. There’s no boss or manager in the traditional sense telling you what to do. You get to decide as a team what the right priorities are. You and your pair get to decide if you should do this refactoring now, or raise it up to the rest of the team as part of a larger effort. In terms of how you work on your project, you have as much influence as anyone else. The flatness is still the same in many ways as it was four years and a thousand people ago.
Moving up, let’s say you want to affect some change at the office level. Your first option is to go one step up the on-paper org chart, but really it’s a lateral move: Talk to your manager. At Pivotal Labs, your manager is more of a buddy or coach. Your manager is not on your project, your manager does not assign you work. He’s there to help you process feedback, help you find new ways to use your skills that also help the company, and navigate the company infrastructure so that you don’t have to keep it all in your head. Your manager might give you some advice on things to try, people to talk to, or green light some pizza budget so you can host an event after hours. Otherwise he might encourage you to talk directly to the office director who always has a door open for pivots who want to make their workplace better – and note that you’re skipping right over the manager and associate director levels.
From there, if you’re looking to make some cross office improvements, you’re just one door away from the COO of Pivotal Labs. At this point you probably can’t just drop in and instead you’ll have to schedule something on his calendar, but he’s the kind of guy that’s easy to trust. What I always find fascinating is that I have yet to surprise him – like you might expect a COO to do, he’s always thinking about making the company better and he’s good at finding ways to put passionate pivots to use. Similarly, you can get lunch or coffee with our founder who is a great example of an active listener.
Moving up to the company level I haven’t had any direct experience with those folks, but I know they expect the Pivotal Labs Way to become the Pivotal Way the same that we’ve always expected the Pivotal Labs Way to become the way our clients do software development. The way we’ve accomplished that in the past is by pairing with them on their projects. It is for this reason that I’ve asked to do a rotation on CloudFoundry, one of our core projects from Pivotal. By sharing my practices, I hope they carry them forward as they engage with other parts of Pivotal and those teams with the teams they overlap with, and so on.
Yes, the org chart is a lot deeper now than it once was. It’s clear though that pivots still have a voice and they still have all the autonomy to manage their own day-to-day. What’s interesting is that now my lever arm is potentially even longer than it was before because I’m now at the center of an organization that is designed around the way I work and I have an opportunity to shape the way more people make software than ever before.
Releasing When It’s Ugly
This is yet another post where the tl;dr is “SHIP IT!” but with a Pivotal perspective. It can be easy to hold back an initial launch, especially when you’re trying to attract customers, not offend them. I submit that with the right tools, you can not only minimize the impact of releasing early, but create customer delight from the pain whereas perfection out of the gates would have produced none.
If your software can enable users to do the core thing you want, no matter how tangled the experience is, you should ship today under one condition: You must be able to ship again tomorrow, and the next day, and then two hours after that, and then the following Friday. In his post, 1.0 is the Loneliest Number, Matt Mullenweg talks about how that first hurdle is a big psychological barrier. He speculates that Apple employees were embarrassed at the known shortcomings of the first iPod, anxious to get a better version out before the first one even shipped. A conversation I had with my own PM was reminiscent of this. He, along with myself and the rest of the team, has a lot of pride in his work, and this project in particular. Understandably, he wants the first user to have the best experience possible. And while I agree that I want the user to be happy, I know that his experience could always be improved.
You could rephrase this idea to the proverb, “Perfect is the enemy of good.”, meaning that you shouldn’t let perfection get in the way of doing good things. Another quote, said by one of our team members struck true form most of us: “If you waited until it was perfect, you waited too long.” But I think the thought that really bring it home for me is this, “Would you rather they have an imperfect experience now, or that they have no experience at all?” I like this because if the user has an experience, the user learns something about you, and if you’re lucky (or directed through user testing), the user gives you feedback and you learn something too.
When you learn something, you can take action on it. Before that learning happens, you’re just guessing, shooting wildly in the air hoping to hit something. Delivering perfection on the first try must be near impossible with this method. Thus, like all things Pivotal, we do what we can to shorten the feedback loop – getting feedback, making a decision to act, and delivering must take as little time as possible. To do that in software, that means a few things.
First, priorities must be flexible. If something important comes along, you shouldn’t have to wait six months to get it in the next release, and you shouldn’t have to worry about the implications of scuttling the sprint. We do this using our own tool, Pivotal Tracker which lets you easily drag the most important story to the top of the work queue.
Second, you must be able to respond quickly. If Joe is the only one who knows that code and he’s got six other high priority things to do today, you’re out of luck. That’s why we pair program and rotate, so that anyone on the team can pick up the next story that needs attention and everyone is familiar with all parts of the codebase that the team is responsible for.
Lastly, you need to be able to ship fast. There’re can’t be a two day release process on a thirty minute code change that only happens on Tuesday’s and Thursdays. It has to ship as close to now as possible, otherwise you’re wasting value by letting it sit on your virtual shelf. At Pivotal, we are always ready to ship because we don’t know any other way, our stories need reviewing, and that usually means shipping to some sort of prod-like environment, so we have to ship multiple times a day to keep the story feedback cycle small. If this process were anything other than dead simple, we’d need a pair on deployment full time just to keep up. Often times we configure our CI server to automatically deploy upon successful test runs, or in the worst case make it a manual one-click to deploy. The same technique is readily applied to production too. From there it’s up to the team to minimize downtime during deployments using whatever strategy is appropriate for the application.
But how does all this create customer delight from strife you ask? Picture the last time you withdrew cash from an ATM. You put your card in, punched some numbers, and hopefully the right amount of cash spits out. How would you rate that experience from these three choices? A) Meets Expectations, B) Excedes Expectations, C) Below Expectations. I’m guessing you didn’t get wowed by the ATM unless this was your first time taking cash out. It probably wasn’t below expectations since you got what you wanted. Rather, it was just a ho-hum experience.
What if instead, you put your card in, punch some numbers, and it buzzes for a while and shows you your receipt, but never gives you the expected cash money? Now you’re pissed. You wanted to use this service and it let you down. You really want cash, so you call the bank, planning to give them some strongly worded feedback. However you find yourself completely disarmed because the person on the other end of the line is really listening to you, not reading a script, and they quickly identify the problem, add back the money to your account, and tell you that they’re sending out a technician to fix the machine so that you and future customers have a much better experience in the future. WOW! I bet you’re thinking, “I would love to have a bank like that!” By shipping software that works in many cases, but has a compelling story to turn failures in to successes, you create a loyalty that’s a much better engine for your company than “Yeah, it meets my expectations.” Let them see you fix things. Let the users feel empowered to tell you what needs the most attention.
In the end, if you feel like you could ship, you should ship and feel good about it. You’re putting a valuable product in the hands of the users you want to help sooner. And if you’re doing it the Pivotal way, you can address their pain points quickly to change customer dissatisfaction into customer delight while being directed about how you spend your time and resources.
“Drinking the Kool-Aid™” How We Create Value Alignment
Over my four year tenure at Pivotal Labs I’ve heard it a lot: “You guys really drink the Kool-Aid around here, don’t you?” and I’d shake it off with a joke, “Yeah, you can grab anything from the fridge you want, so long as it’s powder-based fruit punch.” I thought I knew what they were getting at, we’re pretty rigorous about things that are radically different from the way most software developers work – TDD, pair programing, and a common start time to name a few. But it takes more than common practices to build a culture that is so strong that it consistently elicits cult analogies.
First off, I’m not excited to be called a cult member – the term is derogatory and carries a lot of baggage with it. But if we look at this definition from Wikipedia, the word cult is a ”term for a new religious movement or other group whose beliefs or practices are considered abnormal or bizarre by the larger society.” While we’re not “religious”, I would say we’re a “movement” to change the way software is made, and our practices intentionally push the boundaries of what most organizations are comfortable with. But what makes a cult is beyond practices.
I said I thought I knew what people were getting at, but I didn’t fully understand it until recently when I was sitting down with a candidate who was on the edge about accepting a job offer at Pivotal. She had been trough our usual rounds of interviews and had some really good questions. I did my best to answer them honestly. At the end she thanked me and offered, “If nothing else you’re all very consistent.” I indicated I wanted to know more and she shared that she had also talked to pivots at meetups and even overheard a couple of us on the train and apparently we “… all say the same things about [our] company.” She had to leave, so we said our goodbyes, but what she had said moments before was still washing over me.
How is it that we’re so consistent? Even when we think no one is listening we say the same things that we say when trying to attract new talent. For my specific situation, I wasn’t glossing over any of the parts of our company that are feeling growing pains; I felt confident that reality was not only good enough to stand on it’s own, but that it was the right thing to say. I wanted her to make an informed decision about coming to work with us – one of the hardest things to do is work with someone who doesn’t want to be there. I imagine school teachers feel this way a lot of the time, compared to other classroom settings where students opt-in.
Okay, being open is a pretty good thing, but it doesn’t fully explain how I was giving the same message that the rest of my coworkers were giving. Each person has a unique perspective on life, and there certainly isn’t someone coming over the speakers every morning telling us what to think. Upon reflection, I think the reason we have a common appreciation for our work place is because we pair program and rotate through projects. While I’m sharing my technical skills when at the keyboard, I’m also learning about what’s going on in the company, on the team, and with the individual between keyboarding times (see future post “Why Slow Tests Aren’t Always a Bad Thing”). Face-time, and pairing specifically, homogenates a workforce.
This constant sharing of ideas and perspectives quickly creates a value alignment without anyone ever having to say what those values are. We even pair when interviewing so we’re selecting for those values from day zero. And since we have this strong alignment, it’s easier to identify pivots as a group, or a “cult” if you must, rather than as individuals who works for a faceless brand. If you want your company values to be represented at all levels, you have to talk about and challenge those values every day so that there is shared understanding, and pairing is a great way to do just that.
As it turns out, that interview candidate accepted the job the day after we spoke. I also checked the fridge, and though it is well stocked with a variety of beverages including fancy tea, Kombucha, over thirty kinds of beer, and a plethora of juices, Kool-Aid is no where to be found. We understand why we’re at our job better than anyone else in the business, plus we can message those values even when we think no one is listening, and that’s a powerful thing to find in the workplace.
Get Personal, Get Feedback, Get Better
One of our directors shared this article with the managers here at Pivotal Labs about having a personal retrospective. It immediately sparked a healthy debate, and I latched on to the idea because I think it fills a very real hole in getting feedback. We do great at gathering, weighting, and aggregating feedback from a pivot’s peers and delivering that in a constructive way. However, we don’t have a great way to get direct feedback on a level above the day-to-day pairing feedback. To see if it would be useful at our company, we needed to try it out so I volunteered. In addition to learning about myself the personal retro had several unexpected, positive effects. My experience was very personal and I’d be happy to talk about it in more details in person – what follows here are my findings on how to run one successfully in the hopes that others will find this tool useful as well.
I think there were at least four key elements to my personal retro that made it work well:
- Be quiet and in the background. Have a facilitator, you can not host this yourself
- Pick the right people and set expectations
- Pick the right questions to ask
- Be in a frame of mind where you really want this feedback (introspective, courage)
I initially thought, “I’m good at leading project retros, I should be able to lead a retro about myself.” I’m glad someone else suggested that I have a facilitator and that I listened. It was intense enough listening to feedback, I can’t imagine trying to stay impartial and lead discussion while soaking it all in. I asked one of our pivots who has not only led retros, but also facilitated inceptions – I knew he could handle the job and be professional about it. He took my questions and prepared his own notes ahead of time and asked me clarifying questions. Since I was doing my best to be quiet to prevent influencing opinions, he also made sure that before we moved on that I had the clarity I needed. I was on the opposite side of the room from the facilitator, so most times I wasn’t in the field of view of the speaker and they addressed me in the third person “…he does this very well”, or “Will always…”. This went a long way to making me feel more comfortable and hopefully made the reviewers more comfortable too.
Picking the right people was tough. I wanted to get feedback on all parts of my job. I’m a developer, a team lead (anchor), a manger, and probably another two less official hats. I wanted to know about how I’m doing as a pivot in all these areas. My review team consisted of:
- My manager
- A fellow manager
- One of my reports
- One of my team mates
- and …
While I respect and value all four of these people’s opinions, I wanted to have someone in the room who saw me differently. Someone who I had a rough experience with. My experience has often been that you can learn a lot more when you disagree and so I wanted that person in the room too. She declined the retro so I invited a friend and rising pivot to be my fifth reviewer. I felt five was the right number because it was big enough for diversity, small enough to be productive, and odd numbered if any voting needed to take place.
In my invite I shared the above article. I shared my intended questions for discussion. I shared the invite list so that if someone didn’t feel comfortable reviewing me in front of the director of the office he or she could gracefully bow out. I made it clear that this was optional.
Not everyone was a match to give this kind of feedback in a group setting. As we do more personal retros I suspect we’ll get a better feel for who is well suited to giving feedback in a semi-public environment. The quantity of feedback I got seemed to be directly proportional to the seniority of the reviewer, but the less-senior pivots provided their own unique perspective and insights that I would keep if I was doing it over again; meaning that I appreciated the spread, but it would be folly to expect a greener employee to produce all the value, so set your expectations appropriately.
Picking the right questions for me was the most emotional part of this process. I wanted to be specific so as to provide direction to the group – “What do you like most/least about Will” was not going to cut it. I’ve gotten some feedback with my manager and I really wanted to focus in on what it would mean to make some of those areas better. Before I could do that, I had to share context and concrete examples with this group so they could paint me their own picture of how I am perceived through their eyes. If you want an exercise to get at these questions, you could probably stand at a whiteboard by yourself, create the three columns, and group them together and prioritize like you would in a normal retro. I wrote down my questions and sent out the invites at least two weeks ahead which gave me plenty of time to think about what kinds of answers I wanted from people, and what examples I had if they needed clarification. For me, I’m an introspective guy and these kinds of questions are always rolling around in my head, they’re what drive me to be better, but until now I only had myself to measure against. For most, it might be helpful to go through an annual review first to get some solid manager feedback and have a year of experience under your belt before embarking on a personal retro.
Once you’ve picked the right people, picked the right questions, the only thing to worry about is yourself. In order for this to be useful at all, you’ve got to want to know more about yourself, and you’ve got to be willing to listen to others even if you don’t agree with them. Humility goes hand and hand with the personal retro. I was called “brave” and “courageous”, which felt good, but I wasn’t there for praise, I was there to grow. It does take some steel-clad nerves to ask a peer to speak about you. It takes guts to show your boss where you and others think you can improve. And no one can call you chicken for shining this kind of light on some of the dusty parts of your personality, but I can assure you that I still had moments leading up to my retro where I wanted to smash the eject button and bail out. This. Is. Not. Easy. But the benefit far outweighs the cost for those who are willing to try.
How to do Google Apps SSO in Ruby
Google has a ton of APIs, and a fistful of authentication methods to match – everything from 3-legged OAuth2 to proprietary protocols like AuthSub. I’m in the middle of building out a One-click installable Google App for Enterprise/Education (ya know, when your company uses their domain but you get all the niceness of GMail and related Google tools) but I found the Ruby documentation lacking and even misleading. Furthermore, the existing gems that are available are too often focused on non-Google Apps development or return you an unparsed Atom feed, barely giving you any functionality beyond making a Net::HTTP request yourself. Despite all this, I’ve persevered, and what follows is the summation of my findings.
First off, make a new app in the Google Apps Marketplace. The documentation on this is pretty decent. It involves creating a couple of XML files. The openIdRealm is relevant. I set mine to my production URL and had no trouble in development (localhost:3000). What is important though is that your openIdRealm is different from the domain that you’re using to install the app. Initially, I was using loopb.ac as my openIdRealm (and thus my production URL), and it was the same domain I was using for GMail. I ran in to timeout issues when I went to production. I changed my openIdRealm and production URL to app.loopb.ac and things started working smoothly again.
Once you’ve got an app (no need to submit it for approval yet) you can get your OAuth credentials.
After you click the link, you’ll want to grab the “Consumer Key” and the “Consumer Key Secret”.
You’re going to be using 2-legged OAuth. This is a bit different than if you’ve used Twitter for SSO. In Twitter’s case you’re most likely using 3-legged OAuth to act on behalf of the individual user. In the case of Google Apps, you’re asking for privileges when the domain admin (aka the IT person at example.com) when s/he installs your app for all of the users in the domain. That way, you don’t have to make each user in the domain decide if it is okay for your app to read their calendar appointments.
You’re going to need two gems, oauth, and omniauth-google-apps. I didn’t seem to need the oauth gem till I deployed to Heroku.
gem 'oauth'
gem 'omniauth-google-apps'
Then, configure omniauth. Make a new file, config/initializers/omniauth.rb with the following contents:
require 'openid/store/filesystem'
Rails.application.config.middleware.use OmniAuth::Builder do
provider :google_apps, :store => OpenID::Store::Filesystem.new('./tmp')
end
The store option is… optional. But if you’re going out to Heroku you’ll want to include it and make sure that the path is ./tmp so as to work with Heroku’s read-only file system.
You can implement a pretty standard looking ApplicationController. See the gist at the end for an example. Basically to sign in you’ll need to redirect the user to /auth/google_apps. You’ll also want a callback endpoint for Google to hit. In your routes.rb:
match "/auth/:provider/callback" => "sessions#create"Fill in your sessions controller to handle the information in the params and sign the user in. There’s an example in the gist. Next, we’ll need a client class to talk to Google’s APIs. Nothing too unexpected here, but this is where the ID and Secret are used – since the domain admin approved your app, you use the app’s credentials, not the user’s to access information. All of Google’s APIs that I’ve needed return Atom feeds, so this’ll parse them in to REXML docs, or raise a 500 if something went wrong.
#lib/google/client.rb
require 'oauth'
require 'rexml/document'
module Google
class Client
attr_accessor :version
def self.get(base, query_parameters, version = '2.0')
make_request(:get, url(base, query_parameters), version)
end
private
def self.make_request(method, url, version)
oauth_consumer = OAuth::Consumer.new(GOOGLE_APP_ID, GOOGLE_APP_SECRET)
access_token = OAuth::AccessToken.new(oauth_consumer)
response = access_token.request(method, url, {'GData-Version' => version})
if response.is_a?(Net::HTTPFound)
return make_request(method, response['Location'], version)
end
return unless response.is_a?(Net::HTTPSuccess)
feed = REXML::Document.new(response.body)
throw :halt, [500, "Unable to query feed"] if feed.nil?
feed
end
def self.url(base, query_parameters={})
url = base
unless query_parameters.empty?
url += '?'
query_parameters.each { |key, value| url += "#{CGI::escape(key)}=#{CGI::escape(value)}&" }
url.chop!
end
url
end
end
end
Great, so how about we get a user’s private contacts? No problem! Here’s the Contact class that uses our client:
#lib/google/contact.rb
module Google
class Contact
attr_accessor :full_name, :first_name, :last_name, :email, :company, :title, :notes
def self.all(email)
feed = Google::Client.get("https://www.google.com/m8/feeds/contacts/default/full", {
'xoauth_requestor_id' => email
}, '3.0')
feed.elements.collect('//entry') do |entry|
new(
:full_name => entry.elements["gd:name"].elements["gd:fullName"].text,
:first_name => entry.elements["gd:name"].elements["gd:givenName"].text,
...
)
end
end
def initialize(options = {})
@full_name = options[:full_name]
@first_name = options[:first_name]
...
end
end
end
Modstly a lot of boilerplate to pull out the meaningful attributes form the XML. The one thing to notice is the query param we pass to the client,
'xoauth_requestor_id' => emailIn the case of a lot of the Google Data APIs, we need this parameter so the API knows who’s contacts we’re going after. For example, a Calendar class would look almost identical save the differing XML fields. Don’t get too comfortable though, what you probably want is to get a list of all users in this domain, not the personal contacts of a particular user. In that case you’ll want to use the Google Provisioning API. At first read of the docs, you see that it too supports the 2-legged auth that we used for contacts, but if you make a carbon copy you’re going to get an “Invalid Token” error. Uhhh, what? Turns out, the xoauth_requestor_id is not only not needed, it’ll cause problems if you include it. The email address isn’t required at all, which makes sense as you’re getting all the users in a domain and don’t really care who the current user is:
#lib/google/user.rb
module Google
class User
attr_accessor :login, :first_name, :last_name
def self.all(domain)
feed = Google::Client.get("https://apps-apis.google.com/a/feeds/#{domain}/user/2.0", {
'get' => 'all'
})
feed.elements.collect('//entry') { |e| new_from_entry(e) }
end
def initialize(options ={})
...
end
private
def self.new_from_entry(entry)
new(
...
)
end
end
end
There you have it. Hopefully I’ve saved you the countless hours of reading documentation, Googling for “Google”, and beating your head against any vertical surface as I did. Happy App development!
See the full gist here. Comments and pull requests welcome!
Experience Report: Engine Usage That Didn’t Work
On the project I’m currently working on we have a main portal that provides a user registration system and a generic billing mechanism. It also has several sub applications which need to know some information about the user and be able to publish billing events. With a fairly easy to articulate boundary, we thought it might make sense to be deliberate in how we organized our code – we came up with three main solutions:
- One big app, just use namespaces
- Create the portal and expose API endpoints over HTTP to get user data and set billing data.
- Create the portal, and have each sub application contained in a Rails Mountable Engine.
Our Boulder office had been making some noise about Rails Mountable Engines for some time, gave a presentation in the SF office, and I had experience working with engines both in the dark times of engines in Rails 2.x and the markedly improved days of Rails 3.x, and even better still in 3.1+. We had need to scale one sub-component of one of the applications independently, but not entire apps as the primary usage of the system would be low-volume. We set off down the engines path…
It worked pretty well. Two months in to the project we had a retrospective specifically about how engines were working. We agreed that we had felt some pain, but overall they drove out interesting decoupling and that the cost to pull them out could potentially outweigh the little pain we might encounter in the future.
Then our team size doubled. We had multiple epics that had high priority and deadlines associated with them. We also needed to ramp up four new people which led me to reassess our choice of engines once again, not to mention that we eventually will be handing over the code to developers who haven’t seen much Rails.
The conclusion I came to was that engines had to go. Here’s the list of why:
- Asset pipeline already feels magical, with engines you have to think even harder about what you’re doing and what to include in your application.js or application.scss.
- Testing. Writing an RSpec request spec didn’t make sense in the engines where certain styles were expected, or a specific layout – we found ourselves stubbing out a whole lot, or putting more and more into a gem that contained shared code between the portal and the engines.
- Running Tests – We had to have multiple instances of RubyMine open to get the correct load paths for the engine and the portal, constantly trying to remember “Is this in the shared gem, or in the engine?” or “Oh wait, this is specific to the engine, but it is a request spec so we need to go back to the portal to re-run that” was not unsolvable, but felt like a tax on our choice every time I fumbled.
- Migrations everywhere – You write the migration in the engine, then it has to be copied to the dummy app for testing, and also back up to the main app for running request specs, now you have three copies of the same migration, all with different timestamps. Not fun when you realize you also needed to change that string column to a text column.
- Everything still had to be namespaced anyway for the engines so we had the folder explosion we were trying to avoid from the One Big App solution.
- Confidence – It got to the point where I found myself asking “Is this broken because I wrote it wrong, or because there’s something I misunderstood about engines?”. I could never be sure of why something had gone wrong.
- Documentation – Rails Mountable Engines documentation is a small subset of the knowledge available on StackOverflow for example. If we want to make the ramp up and handoff as smooth as possible, we want to do the most vanilla thing possible so that things are intuitive or at least Googleable.
None of this was impossible, or even difficult to solve. It’s just that it wasn’t intuitive, not what a well seasoned Rails developer was expecting out of the box. It would have been a waste of our client’s time to bring a whole new team up to speed on all that we had learned about engines, rather than having one big application.
This is especially true since the only semi-real-payoff was that it made us isolate our sub-application code from the portal code. I say “semi-real” because the boundary was artificial – the reality was that the sub applications needed to know about the user and his account, and anything we built out for billing was really a dependency of each of the apps. This was different from a great engine like rails_admin that really is a drop in and has no domain-specific dependencies. Here we had nothing but domain-specific dependencies and now, by removing engines, our code and our domain are back where they belong: together.
UPDATE *
Engines really are great and there’s lots of situations where they can be really powerful. Boulder Pivot, Stephan Hagemann, had these additional tips that I wanted to share with you.Regarding RubyMine and running specs: there is a simple way to make RubyMine run all specs in all engines. It will make all engines modules with their own “RubMine root”, which fixes spec runs. http://pivotallabs.com/users/shagemann/blog/articles/2008-intellij-modules-in-rubymine-
Regarding migration duplication: You can have an engine or app that requires others run their migrations. Check out the migrations run by the main app in this sample app: https://github.com/shageman/the_next_big_thing
Regarding testing: If an engine is relying on some layout or style to be around, it should depend on it and include it (potentially by way of another engine).
Postgres features n’ bugs
Interestings
- Potential Heroku/PG/hstore bug in 9.1.3
One of our tables was using an hstore to collect data (a really big json object). We discovered that the primary database on staging was using PG 9.1.3 while our acceptance & production apps were using 9.1.4. Pushing this very large (58K) object into the hstore caused a Postgres failure:
ActiveRecord::StatementInvalid: PGError: ERROR: cannot execute INSERT in a read-only transaction : INSERT INTO “xxxx” (”created_at”, “data”, “updated_at”, “user_id”) VALUES ($1, $2, $3, $4) RETURNING “id”
while saving the same data into our acceptance app did not cause the error.
We suspect that there is a bug in PG 9.1.3 when inserting “big” strings into an hstore that is fixed in 9.1.4.
We promoted a new 9.1.4 db on our staging app. This cleared our problem.
- postgresql_cursor and streaming in rails
We were able to stream the contents of a table to the user without loading the whole dataset into memory.
- RubyConf CFP ends Friday
It’s in Denver this year.
https://rubyconf2012.busyconf.com/proposals/new
Hey there Qt
Interestings
- Keep your Qt up-to-date
Qt, the cross-platform system that Capybara-Webkit & PhantomJS use, does not get updated when you update the capybara-webkit gem.
So make sure you keep Qt up-to-date on your dev boxes and CI, and keep dev/CI in sync, for best performance and least pain.
E.g.,
$ brew install qt
$ gem uninstall capybara-webkit
$ bundle
Events
- SF Pair eXchange 6:15pm
