Matt Cholick's Bloghttp://www.cholick.com/2020-10-04T17:51:56ZMatt Cholick
Modern CI/CD Is a Directed Graph of Containers
tag:cholick.com,2016:2882020-10-04T17:51:56Z
<p>
I had quite a difficult time figuring out how exactly GCP's Cloud Build works from reading docs and articles.
The marketing material describes its technical functionality poorly; I needed to write code and dive in to figure out how it really behaves. I found the lack of good examples frustrating enough that I decided to write up a post and the code to hopefully save someone else a bit of time.
My confusion came from the fact that I thought it was more than it actually is. Cloud Build really just boils down to a triggered chain of containers that are executed with persistent state mounted across steps. It does include a few nice convenience integrations into the larger platform, like auth, but there really is no magic.
</p>
<p>
My personal stuff has been running on VMs managed by Ansible for quite a few years now. This paradigm made sense when I set it up, but a lot has changed in the intervening years. My playbooks are feature rich and include both blue-green deployment and fully deploying software from a newly provisioned VM. Not working in that space, though, has atrophied the skills I need to maintain a large playbook.
</p>
<p>
Containers make a lot more sense today anyway, especially with all the layers of sugar that various clouds have built on top of basic orchestration. One of the features I've missed for a while is full automation post-commit: I'd like to be able make a small edit directly on Github and have that change automatically built, tested, and deployed. It's been possible for quite a while. The latest full server rebuild from playbooks I barely remember finally motived me to invest.
</p>
<p>
I've been using
<a href="https://concourse-ci.org">Concourse</a>
for quite a few years, and I have become quite fond of it (once I made peace with its statelessness). For something small, though, it's a bit heavy. I also don't like having the engine I need to restore software from scratch in the same cluster for disaster recovery reasons (my budget for toy projects is a single cluster). There are quite a few of hosted solutions outside of Concourse that would address that need, but starting with GCP's native offering is a pretty low friction choice, especially when the free tier would completely cover my needs.
</p>
<p>
Enter Cloud Build. I've skimmed the docs and once attended a talk, but I hadn't understood the core of it. I think I must have skipped over the key sentence that summarized the tool: Cloud Build just executes any standard container and does so in a context with some shared state. This model absolutely makes sense, but I had a different impression. Having used Concourse for so many years, as well as briefly testing out Drone, Jenkins X, and CircleCI, this is definitely the paradigm that modern CI/CD systems have settled on. The containers are run sequentially (with parallelization possible), steps return a non-zero exit code to indicate failure, and state is piped to the next container or otherwise stored. That's it; all the the modern systems boil down to that and differentiation is just UI and various convenience features.
</p>
<p>
While trying to make a complete pipeline example work, I found two aspects of the tool confusing. The first of those is
<a href="https://cloud.google.com/cloud-build/docs/cloud-builders">Cloud Builders</a>.
I came away from docs and examples assuming these were a first class concept, that there is a specific contract between a builder and the system executing it. There isn't; there is no special special sauce in the builders. My suggestion is to mostly stick to other containers. For a lot of functionality, there is likely a better maintained and documented container out there.
</p>
<p>
Images were the second misleading concept for me. The cloudbuild.yaml file can have an "images" key.
Using that, though, doesn't make the image available in GCR until the completion of the pipeline run. That's too late for the pipelines I want to build: I expect a pipeline to unit test, build, and deploy into a cluster. The deploy step doesn't work in this scenario, though, because images aren't available in GCR for a pull by the target cluster. Pipelines have to perform their own push when a pipeline step drive that pull.
</p>
<p>
The full
<a href="https://github.com/cholick/cloud-build-sample">Cloud Build example pipeline</a> code is up on Github.
The pipeline unit-tests, compiles, lints, builds the image, pushes the image, deploys to a Kubernetes cluster, and tests the deployed workload. The "images" key in the build yaml doesn't affect how the pipeline works, but it does add a link to the built image in the GCP UI.
</p>
<dl>
<dt>unit-test</dt>
<dd>Runs unit tests using the official Golang image</dd>
<dt>build-binary</dt>
<dd>
Builds the linux binary, which will be available to subsequent steps. This and the next few steps
are executed in parallel using the waitFor value of "-"
</dd>
<dt>helm-lint</dt>
<dd>Lints the Helm chart</dd>
<dt>go-lint</dt>
<dd>Lints the Go code</dd>
<dt>build-image</dt>
<dd>Builds the docker image. Subsequent docker commands will have access to the image</dd>
<dt>push-image</dt>
<dd>Pushes the image to GCR</dd>
<dt>install-dev</dt>
<dd>Installs the software (via Helm) into a cluster controlled by the step's environment variables. This is a one step where a builder did prove useful</dd>
<dt>prep-e2e</dt>
<dd>Installs the end-to-end Python tests' pre-requisites. The target flag coordinates with PYTHONPATH in the subsequent step</dd>
<dt>e2e</dt>
<dd>Performs end-to-end tests via Python</dd>
</dl>
<p>
The full build file follows.
</p>
<pre class="code">
<code data-language="yaml" class="hljs">
images: [ 'gcr.io/${PROJECT_ID}/cbt:${REVISION_ID}' ]
steps:
- id: unit-test
name: "golang:1.15"
env: [ 'GO111MODULE=on' ]
args: [ 'make', 'test' ]
- id: build-binary
name: "golang:1.15"
env: [ 'GO111MODULE=on' ]
args: [ 'make', 'build-linux' ]
waitFor: [ '-' ]
- id: helm-lint
name: 'gcr.io/$PROJECT_ID/helm-builder'
args: [ 'lint', 'deployment/cbt', '--strict' ]
waitFor: [ '-' ]
env: [ 'SKIP_CLUSTER_CONFIG=true' ]
- id: go-lint
name: "golangci/golangci-lint:v1.31"
args: [ 'golangci-lint', 'run', './...','--enable', 'gocritic,testpackage' ]
waitFor: [ '-' ]
- id: build-image
name: 'docker'
args: [
'build', 'deployment/docker',
'-t', 'gcr.io/$PROJECT_ID/cbt:$REVISION_ID',
"--label", "org.opencontainers.image.revision=${REVISION_ID}",
]
- id: push-image
name: 'gcr.io/cloud-builders/docker'
args: [ 'push', 'gcr.io/${PROJECT_ID}/cbt:${REVISION_ID}' ]
- id: install-dev
name: 'gcr.io/$PROJECT_ID/helm-builder'
args: [
'upgrade', 'cbt-dev', 'deployment/cbt', '--install',
'--wait', '--timeout', '1m',
'--namespace', 'dev', '--create-namespace',
'-f', 'deployment/values-staging.yaml',
'--set', 'image.repository=gcr.io/$PROJECT_ID/cbt',
'--set', 'image.tag=${REVISION_ID}',
]
env: [
'CLOUDSDK_COMPUTE_ZONE=us-central1-b',
'CLOUDSDK_CONTAINER_CLUSTER=hello-cloudbuild'
]
- id: prep-e2e
name: 'python:3.8-slim'
args: [
'pip', 'install',
'--target', '/workspace/lib',
'--requirement', '/workspace/test/requirements.txt'
]
- id: e2e
name: 'python:3.8-slim'
args: [
'python', '-m', 'unittest', 'discover',
'--start-directory', 'test',
'--pattern', '*_test.py'
]
env: [ "PYTHONPATH=/workspace/lib" ]
</code>
</pre>
<p>
Finally, these are the two references that I found the most useful
</p>
<ul>
<li>
The full build file
<a href="https://cloud.google.com/cloud-build/docs/build-config">syntax specification</a>
</li>
<li>
The documentation that describes substitution and lists
<a href="https://cloud.google.com/cloud-build/docs/configuring-builds/substitute-variable-values#using_default_substitutions">all the platform-provided values</a>
</li>
</ul>
Nine Years of 140 Characters
tag:cholick.com,2016:2872017-10-01T00:19:58Z
<p>
Twitter recently announced they're
<a href="https://blog.twitter.com/official/en_us/topics/product/2017/Giving-you-more-characters-to-express-yourself.html">testing a 280 character limit</a>. There's a graph in the post that shows 9% of English Tweets have 140 characters. This was a surprisingly high percentage to me; it takes me several edits to hit the limit exactly, and I didn't think that many people went to the effort. I recall a few instances of looking up things like unicode ellipsis so that I could sneak in under the limit.
</p>
<p>
Curious how my own content stacked up against that average, I downloaded my
<a href="https://support.twitter.com/articles/20170160">Twitter archive</a>
and then wrote
<a href="https://gist.github.com/cholick/5ba94d54ce4307b36c995f746cf33b75">some Python to parse my tweets</a>.
Here are a few simple stats (excluding @replies and retweets):
</p>
<table class="table table-bordered table-striped" style="width: auto">
<thead>
<tr><th>Year</th><th>Tweets</th><th>Mean Length</th><th>Length ≥ 135</th><th>Length = 140</th>
</thead>
<tbody>
<tr><td>2017</td><td>63</td><td>113</td><td>41%</td><td>21%</td></tr>
<tr><td>2016</td><td>109</td><td>113</td><td>36%</td><td>18%</td></tr>
<tr><td>2015</td><td>108</td><td>112</td><td>34%</td><td>12%</td></tr>
<tr><td>2014</td><td>123</td><td>111</td><td>35%</td><td>18%</td></tr>
<tr><td>2013</td><td>139</td><td>108</td><td>29%</td><td>15%</td></tr>
<tr><td>2012</td><td>252</td><td>95</td><td>20%</td><td>11%</td></tr>
<tr><td>2011</td><td>281</td><td>78</td><td>10%</td><td>6%</td></tr>
<tr><td>2010</td><td>211</td><td>71</td><td>3%</td><td>1%</td></tr>
<tr><td>2009</td><td>457</td><td>71</td><td>2%</td><td>0%</td></tr>
<tr><td>2008</td><td>358</td><td>78</td><td>8%</td><td>3%</td></tr>
</tbody>
</table>
<p>
2008 was a <em>long</em> time ago, and I definitely used Twitter differently (as the table shows). Here are a few representative tweets:
</p>
<ul>
<li><q>I'm about to watch the best 30 minutes of animation ever created: Futurama's Roswell That Ends Well.</q></li>
<li><q>Interesting wired article:
<a href="https://www.wired.com/2008/04/ff-wozniak/">https://www.wired.com/2008/04/ff-wozniak/</a></q></li>
<li><q>The dude abides.</q></li>
<li><q>Is it obsessive compulsive to dump out a bag of skittles and sort them by color?</q></li>
<li><q>Java has tainted me - I like xml now... it almost doesn't seem too verbose.</q></li>
</ul>
<p>
They content is very random. Reading back over them, I can empathize with people advocating for the
<a href="https://en.wikipedia.org/wiki/Right_to_be_forgotten">right to be forgotten</a>. Tools like Twitter came along when I was old enough to know to not act too stupidly online, but still.... those early Tweets are so banal. They're thoughts that should be ephemeral, but instead sit there, frozen there in amber, until the end of time. Those early years had a very low signal-to-noise ratio.
</p>
<p>
Wind the clock forward nearly a decade, and I'm definitely making much better use of all those characters. Here are a few recent and representative tweets:
<p>
<ul>
<li><q>Deep neural network generated text-to-speech:
<a href="https://deepmind.com/blog/wavenet-generative-model-raw-audio/">https://deepmind.com/blog/wavenet-generative-model-raw-audio/</a>
The audio samples near the end are amazing.</q></li>
<li><q>A sobering post by
<a href="https://twitter.com/briankrebs">@briankrebs</a>,
'The Democratization of Censorship'
<a href="https://krebsonsecurity.com/2016/09/the-democratization-of-censorship/">https://krebsonsecurity.com/2016/09/the-democratization-of-censorship/</a>
Great to see
<a href="https://twitter.com/google">@google</a>
step in w/ Project Shield.</q></li>
<li><q>Sad that I discovered
<a href="https://twitter.com/LuckyPeach">@LuckyPeach</a>
only months before its demise.</q></li>
<li><q>Vendoring & GOPATH create enough pain & annoyance that every time I step away from Go & return, I seriously consider dropping the language.</q></li>
<li><q>Crazy bimodal distribution of reviews:
<a href="https://fivethirtyeight.com/features/al-gores-new-movie-exposes-the-big-flaw-in-online-movie-ratings/">https://fivethirtyeight.com/features/al-gores-new-movie-exposes-the-big-flaw-in-online-movie-ratings/</a>
Wonder how often distilling movie to mean score obscures this kind of detail?</q></li>
</ul>
<p>
Nine years later, I think those examples are about as good as the medium can get in 140 characters. It's enough to let a folding magazine's writers know that they'll be missed. It can give enough context to a link so that a reader knows it's worth clicking. It's possible to complain about a single, concrete thing (GOPATH...) in hope of soliciting links to a solution. It's enough to broadcast a position on some contemporary issue.
</p>
<p>
I don't think
<a href="http://www.politico.com/magazine/story/2017/01/why-we-cant-fix-twitter-214607">Twitter is broken</a>. I just think it's not a medium
that can ever facilitate discussion: it's a mistake to think that discussion there is even possible. Nor is nuance possible. It's a broadcast medium for simple thoughts, links, and photos.
280 characters won't change that.
</p>
Slack Overload: A Frustrated User Rant
tag:cholick.com,2016:2862017-01-22T17:14:13Z
<p>
Slack
<a href="https://slackhq.com/threaded-messaging-comes-to-slack-417ffba054bd">introduced threading</a>.
I had such high hopes for this feature. But... it misses the mark and doesn't solve the problem that
Slack creates.
</p>
<p>
First, some background:
</p>
<dl>
<dt>Slack is not optional</dt>
<dd>
I'm on a distributed team but, even when I haven't been, using Slack is a requirement.
</dd>
<dt>People say important things in Slack</dt>
<dd>
This shouldn't come as a surprise, but given a communication channel, people say important things.
Since slack is not optional, people expect others to read the messages they write. This is a pretty reasonable perspective.
</dd>
<dt>People say unimportant (to me) things in Slack</dt>
<dd>
Again, no surprise here. Assuming what they're saying is relevant to the channel, it still might not be specifically relevant to me.
In the context of a single team this is true, but it's even more applicable in a larger organization.
Even after quickly leaving low-relevance channels and muting others, I still keep tabs on more than a dozen channels.
</dd>
</dl>
<p>
Taken together, all these things result in an information filtering problem.
Across all these channels are a subset of messages that I do want to read, but any given message has
<strong>no context</strong>. Over and over again, I have to evaluate and dismiss messages that I could have
ignored. Context is critically missing.
</p>
<p>
For a contrasting example, every email message has a subject line. I can quickly evaluate any message and ignore those not important to me.
To manage email further, one can do things like add filters or mute discussions.
For example, my credit cards automatically send me an email for every transaction. I tag these emails and
trigger a specific notification on my phone, so I know immediately any time my cards are charged. I archive
an email immediately if it's a charge I'm expecting. That single tag provides complete context.
</p>
<p>
<em>Managing email is a solved problem</em>. Anyone who says otherwise hasn't put in the work to route and filter incoming information.
</p>
<p>
Email is, of course, not Slack. Chat requires different solutions. What's frustrating, though,
is that other tools similar to Slack have solved (or at least helped to mitigate) the filtering problems they introduce.
<a href="https://www.flowdock.com/">Flowdock</a> is what I used before Slack (and no, Flowdock isn't a Slack clone: it came first).
They built a
<a href="http://blog.flowdock.com/2014/04/30/beware-of-private-conversations/">workable solution to this problem</a>
years ago. Here's a screenshot:
</p>
<img src="/entry/resource/2017-01-22.png" class="img-rounded" alt="Flowdock threading"/>
<p>
The colored bar on the left gives enough context that the message flow can be chunked. At a glance,
I can evaluate a given thread and dismiss it. In real-world usage, often the last several messages would be a single thread.
This style of threading drastically reduced the problem of information overload; it facilitated
quickly viewing a channel and categorizing entire blocks of messages as either dismissible or something to be read.
</p>
<p>
Slack's solution fails to solve the problem. It takes messages completely out of the channel body and thus will only be used on a small
subset of any channel's messages. Once a team embraced Flowdock's threading, <em>every single message</em> had existing context or established
a new one. Slack's threading doesn't support this, because a thread hides replies behind additional clicks.
</p>
<p>
With a different implementation, Slack could drastically reduce the amount of time I'm forced to spend reading
and dismissing irrelevant information. I had such hopes that they would get threading right. Slack, please give me the context and tools
I need to filter information.
</p>
Mourning the Open API
tag:cholick.com,2016:2852016-10-02T14:38:07Z
<p>
A few weeks ago, I got an email from Rotten Tomatoes letting me know that their API is going private.
I should "re-apply via the Business Proposal Form" to get continued access. This is actually the third and final major API to close
that I used to build my
<a href="/entry/show/278">master's project</a>
several years back. That software, or something like it, would be impossible to build today.
</p>
<p>
For the project, I built a collaborative recommender system for movies based on tweets.
The system had two large components. First, I built a classifier to decide if a tweet was positive or negative. To build the
training set, I attached to Twitter's firehose and searched for tweets containing expressions like :) or :(, using them as a noisy label.
Today, the firehose now requires special permission; developers can no longer just start exploring this data or building something.
<p>
<p>
Once I'd built a classifier, I needed a collection of accounts that had tweeted about several movies. Tospy was my source here. Even then, Twitter
didn't offer historical access. Topys provided a freemium API and search that let me build a dataset
about older movies, which I needed to build a large enough collection of different items for recommendation. Topsy was
purchased by Apple in 2013 and shut down in 2015. Today, there is no free source of historic tweets.
</p>
<p>
To build an informational page for my recommendations (links to reviews, poster art, and other information), I used Rotten Tomatoes. This let
me put together a page for each movie without manual data entry. This API is now private.
</p>
<p>
Finally, as a new user entered the system, I read their entire timeline to find tweets about movies. This is what let me calculate similar users
for collaborative recommendation. I also read the entire timelines of users surfaced by Topsy in an effort to build a larger dataset.
Twitter's
<a href="https://blog.twitter.com/2012/changes-coming-in-version-11-of-the-twitter-api">API changes in 2012</a>
would have made this part much harder (specifically
the rate-limiting). I likely wouldn't have been able to get sufficient data in time, as I ran
data collection for weeks at the higher rate-limit to build my recommender.
</p>
<p>
Running through this list, I'm reminded of Anil Dash's
<a href="https://www.youtube.com/watch?v=9KKMnoTTHJk">The Web We Lost</a>.
He builds a fantastic parallel between
<a href="http://www.nytimes.com/2011/10/20/opinion/zuccotti-park-and-the-private-plaza-problem.html">privately owned public spaces</a>
and technology platforms. There's a lot to that topic, which is worth visiting, but it's tangential to this discussion.
More to my point, he talks extensively about the drive toward a consolidation of a diverse
ecosystem into a few massive, non-interoperable giants that view their platforms as a walled garden.
He also contrasts Flickr and Instagram. The former cares about metadata, and that
is what makes so many things possible.
</p>
<p>
I really can see a stark contrast between Flickr and Instagram. Built years apart, the former embraces concepts like
metadata, creative commons licensing, an API, and all the things that it possible to pull its photos and make them a part of something else.
I even found a
<a href="https://amzn.com/0470097744">2007 book dedicated to Flickr mashups</a>.
In contrast, Instagram requires pre-approval of apps. It
<a href="http://www.theverge.com/2012/11/5/3605316/instagram-web-profiles">took years</a>
for Instagram to come to the web from mobile and
<a href="http://www.theverge.com/2015/7/20/9003521/instagram-web-search">years more</a>
before even basic things like web search were in place. Instagram's content is locked away,
reflecting the walled garden the app was born in.
</p>
<p>
I miss the perspective of "Here's access to something that's uniquely our users' via an API; go build something we can't imagine."
I hope that isn't a luxury that disappears as soon as a stock is public or growth slows
down. Platforms need to make money; as a developer, Twitter and Rotten Tomatoes don't <em>owe</em> me anything. But... they do
owe their users. These platforms are stewards and aggregators. Locking away this information does deprive their community.
Whether it's something as silly as
<a href="http://thenextweb.com/shareables/2012/04/27/klouchebag-is-a-wake-up-call-for-people-who-care-about-their-klout-score/">Klouchebag</a>
or something more profound, like
<a href="http://www.reuters.com/article/us-chicago-twitter-food-poisoning-idUSKBN0GQ25820140826">Chicago tracking food poisoning</a>,
the web is a better place when we share.
</p>
<p>
It's sad to see all this interesting data disappearing behind walls.
</p>
Quantified Self Meets IDE: A Year of Data
tag:cholick.com,2016:2842016-04-11T03:44:09Z
<p>
More than a year ago, I started tracking exactly what code I was working on using
<a href="https://wakatime.com">WakaTime</a>. As I've moved from specialized to more generalized as a developer, I wanted some real data to know where I'm focusing; data I could use to drive decisions. Am I getting a picture of the full stack? Was our team too focused on operations this sprint? Am I learning what I want to be learning?
</p>
<h4>The Data</h4>
<p>
Now that I have a solid year's worth of data, some analysis is in order.
The question I want to explore today is "How does that time spend coding breakdown by language?"
We do mostly
<a href="/entry/show/281">pair programming</a>, so the actual numbers I've captured aren't accurate, but I believe the percentages are. WakaTime hooks into Sublime, Visual Studio, and the JetBrains tools, so it captures nearly all my source edits. It likely does under represent operational work, as it has no hooks into terminal.
I've only put it on my work machine (that's what I was interested in measuring), so side projects aren't in the mix. One final thing to note is that the data included 8% of my time bucketed into "other", which was a grab bag of scratch buffers in various technologies, config files, and other random things. Anyway, here's a year of data broken down by language:
</p>
<p style="text-align: center">
<img src="/entry/resource/20160410a.png" class="img-rounded" alt="Ruby: 2.3%, HTML: 2.5%, JSON: 4.2%, C#: 4.5%, Markdown: 4.9%, Bash: 5.1%, TypeScript: 9.6%, YAML: 13.1%, Go: 24.8%, JavaScript: 29.2%"/>
</p>
<p>
Several couple things jump out for me:
</p>
<dl>
<dt>18% infrastructure</dt>
<dd>
Though operations work in general is under represented, infrastructure as code isn't. All the Bash and YAML (Ansible and <a href="http://bosh.io/">Bosh</a>) fall in this bucket. So, I've spent ~18% of my time coding up our infrastructure.
</dd>
<dt>12% front-end</dt>
<dd>
The division between TypeScript and JavaScript is useful, because for historical reasons the UI is TypeScript while JavaScript represents server side Node.js code. So, toss in the HTML and that comes to ~12% front-end work.
</dd>
<dt>60% back-end</dt>
<dd>
Add up the JavaScript, Go, and C# to arrive at ~60% back-end work.
</dd>
<dt>5% documentation</dt>
<dd>
Markdown is either documentation, notes, or knowledge base articles. It's a higher pecentage than I would have guessed.
</dd>
<dt>4% data</dt>
<dd>
~4% JSON is pretty interesting, in that this is time just spent looking at data. There's a little bit of config files in the mix there, but when I looked over the data, it was basically time spent studying API requests and responses.
</dd>
<dt>2% reading oss<dt>
<dd>
Ruby is an outlier, as I don't actually write Ruby code. This fraction of my time was studying the underlying open source software to figure out exactly how or why it's behaving a particular way. This wasn't even in an effort to change things, just reason about the platform we're building upon.
</dd>
</dl>
<p>
I have buzzword aversion to the terms "full-stack" or "devops", so I'm just going to call our team a collection of generalists. But, at least for this sample size of n=1, the graph is concretely what it looks like being a generalist working with in a team of other generalists.
</p>
<h4>Thoughts</h4>
<p>
I've spent quite a bit of time now both in specialist and generalist roles. One thing I really like with my current model, where each team members works with all the technology, is that folks have the understanding and mandate to solve problems anywhere. When very specialized, I've been frustrated at times not being able to solve issues outside my area.
I actually <a href="/entry/show/233">wrote a little on this</a>
five years ago when I was much more specialized:
</p>
<blockquote>
...to gain some experience. I'm a web developer, but at our shop we're very specialized. The developer's don't deal with server maintenance much. This specialization allows groups to be more productive, but it also makes troubleshooting issues on the border between the application and the server (class loading issues, for example) more difficult to deal with. It's frustrating not having the experience to deal with these kinds of issues.
</blockquote>
<p>
My team, down to the individual, is empowered and expected to solve problems and improve all aspects of our product. That's a powerful concept.
</p>
<p>
I also think incentives are well aligned working this way. "<a href="https://queue.acm.org/detail.cfm?id=1142065">You build it, you run it</a>" means that we're on call so we *do* spend the time to build monitoring and automation; one can see it
above as I spent 1/5 of my coding time on automating infrastructure.
I have been pulled from sleep in the middle of the night by my software and it really does change one's perspective. Delivering something
reliable becomes more important. There's a very real moral hazard when reliability is divorced from feature delivery.
</p>
<p>
This applies to other areas as well. We're also both building and consuming our APIs, so we strike a balance between pragmatism and hardcore HATEOAS.
When our customers are confused and generate support tickets, we have the incentive to
<a href="https://github.com/CenturyLinkCloud/PublicKB/commit/510418af833f79e3704a07cc92bcef954e800f02">write up examples</a> and improve the user experience.
There's neither handoff, nor transition, nor gaps.
</p>
<p>
There are disadvantages to generalization as well.
The 10,000 hour rule isn't actually a thing (the original source for the number in Outliers, in fact, wrote an amusingly titled rebuttal
"<a href="https://scholar.google.com/scholar?hl=en&q=The+Danger+of+Delegating+Education+to+Journalists">The Danger of Delegating Education to Journalists</a>"), but it is convenient shorthand for a lot of time invested in something. I've spent that many hours and more working on the JVM.
</p>
<p>
In contrast, looking at the graph above, I've spent my more recent time spread across many languages and even more frameworks. Despite not really touching Java or Groovy for the past year and a half, it still can feel more familiar then Go or TypeScript.
Never focusing long enough to gain real expertise, I sometimes find myself googling the most fundamental bits of language syntax. What isn't visible in the graph above are the context switches. We might go multiple sprints without touching any Node.js or Go code. With these switches, I'll forget how to receive on a Go channel or Typescript's delimiting character for multiline strings.
</p>
<p>
Another disadvantage is making decisions about technologies and frameworks in an absence of deep expertise. Choosing among upstart/monit/runit/systemd/daemonize is a recent concrete example. This is the sort of choice someone with years of Linux system administration experience would have a very informed opinion.
</p>
<p>
Sadly, I don't have a satisfying conclusion to this post. Some days, I miss that deep expertise and language facility that comes from working with the same language and framework for months on end: to know a thing fully. On the other hand, I really do love that I can build, maintain, monitor, update, and deploy full solutions with confidence.
</p>
SaaS and the Psychology of Ownership
tag:cholick.com,2016:2832015-09-07T13:44:52Z
<p>
Recently,
<a href="http://blog.jetbrains.com/blog/2015/09/03/introducing-jetbrains-toolbox/">JetBrains announced</a>
they're changing their licensing to a SaaS style subscription. The reaction was... less than positive.
</p>
<p>
I was curious how negative the response actually was. Paging though comments it did seem overwhelming, but I wanted something less subjective. I took four posts with a large number of comments and fed them through <a href="http://text-processing.com">text-processing.com</a>'s sentiment analysis API. The posts:
</p>
<ul>
<li>
<a href="http://blog.jetbrains.com/blog/2015/09/03/introducing-jetbrains-toolbox/">JetBrains post announcing Toolbox (640 comments)</a>
</li>
<li>
<a href="https://news.ycombinator.com/item?id=10170089">Hacker News post (454 comments)</a>
</li>
<li>
<a href="https://www.reddit.com/r/programming/comments/3ji148/jetbrains_toolbox_monthly_yearly_subscription_for/">Reddit post (200 comments)</a>
</li>
<li>
<a href="http://blog.jetbrains.com/blog/2015/09/04/we-are-listening/">JetBrains "We are listening" followup post (355 comments)</a>
</li>
</ul>
<p>
The code for analysis and raw data are all in <a href="https://github.com/cholick/blog-post-2015-09">this github repository</a>.
</p>
<p>
Analysis Results:
</p>
<table class="table table-bordered table-striped" style="width: auto">
<thead>
<tr>
<th>Source</th>
<th>Positive</th>
<th>Negative</th>
<th>Neutral</th>
</tr>
</thead>
<tbody>
<tr>
<td>JetBrains Post1 (Announcement)</td>
<td>183</td>
<td>338</td>
<td>119</td>
</tr>
<tr>
<td>Hacker News</td>
<td>98</td>
<td>240</td>
<td>116</td>
</tr>
<tr>
<td>Reddit</td>
<td>42</td>
<td>131</td>
<td>27</td>
</tr>
<tr>
<td>JetBrains Post2 (Listening)</td>
<td>100</td>
<td>197</td>
<td>58</td>
</tr>
<tr>
<td>Total</td>
<td>423</td>
<td>906</td>
<td>320</td>
</tr>
</tbody>
</table>
<p>
Negative comments outnumbered positive a little more than 2:1. Spot checking the analysis, I think the positive sentiment is overrepresented due to two types of comments: JetBrains staff responding on their own blog were almost always flagged positive and many commenters talked about how much they liked JetBrains products for the bulk of a post but then concluded they didn't like this change. The numbers are graphed below.
</p>
<p style="text-align: center">
<img src="/entry/resource/20150907.png" alt="Comment Sentiment Graph"/>
</p>
<p>
I'm absolutely an IntelliJ apologist. In teams, if someone complains about the tool, I speak up and defend it. I'll offer alternative ways of accomplishing something,
<a href="https://youtrack.jetbrains.com/issues?q=by%3A+cholick">submit a bug report</a>, or even
<a href="https://plugins.jetbrains.com/plugin/7114">write a plugin</a>.
I've had a personal license for more than five years, which I originally purchased simply because work didn't upgrade on the first day of a major release.
All that said, I do still feel uneasy about this change.
</p>
<p>
For me, I know it's not about the money. I would pay double what they charge for their software and not think twice about the purchase. Paging through the comments, I do see at least some others in a similar situation; some commenters acknowledge it would be cheaper for them, but they still don't like it. Many other commenters are complaining about the price, but I don't believe for a moment that's the core issue: a price increase would not generate this kind of furor.
</p>
<p>
I can't answer the question as to why everyone else is upset, but I can answer the question as to why I'm uncomfortable with the change. It's not the concept of SaaS in general that I have a problem with. In the last company I worked for, that's the type of software I actually wrote. Much of the music I listen to uses a similar pricing model: I pay a recurring fee where, if my subscription lapses, I'd lose access. Thinking more broadly, I pay a recurring "subscription fee" to live in my current house and am fine with that.
</p>
<p>
I tried to come up with a category of software where I would have similar issues. The one that immediately came to mind is the operating system. An analogous situation might be if OSX was subscription based and, if lapsed, I would still have all my files and data but no longer be able to use the operating system (the same way I'd still have source code if my IntelliJ subscription lapsed). I would likely switch back to Linux, similar to how many users are threatening to move to Eclipse.
</p>
<p>
So, what's makes the OS special? What makes me uncomfortable with a subscription model for this and for my IDE? The key common aspect I believe is how critical these software products are to getting work done. I watched a Cutthroat Kitchen episode the other day and one of the hinderances they added was making the chef cook with a single arm. That's how I feel when I'm using Sublime or Eclipse. And for something so important, I'm just flat out uncomfortable not owning it. It's not about the money. It's not about software phoning home (all my games are in Steam). It's not about a recurring fee (I upgrade IntelliJ every year). It's about renting the critical tools I use to get things done.
</p>
<p>
Everybody wants to be a SaaS, but JetBrains made a mistake. They just didn't understand their customers' relationship to the software they sell. It was avoidable though. Two years ago, they made
<a href="http://blog.jetbrains.com/idea/2013/12/intellij-idea-personal-licensing-changes/">a small step in this direction</a> with the personal license change to a subscription for upgrade model. The strongly negative customer reaction should have predicted the current storm. They also could have surveyed customers. As a big fan and someone willing to give their company the benefit of the doubt, I still would have reacted with "Don't do this". The takeaway? As it is so often the case in software development,
<a href="http://www.agilemanifesto.org/">talk to your customers</a>.
</p>
Ansible 101
tag:cholick.com,2016:2822015-04-08T22:50:38Z
<p>
I like Chef. I think it's a reasonable solution to a very real set of problems. I've worked with the tool enough to know how all its pieces fit together: Chef itself,
the nodes and environments, Chef-vault, Berkshelf, test-kitchen, and other elements of the ecosystem. I'm confident that I can modify a recipe to suite my needs or spin things up from
scratch. I like their overall model, and I like that the tooling supports a test-driven flow for developing cookbooks.
</p>
<p>
Where I run into trouble with Chef is coupling its high complexity with infrequent use. Complexity by itself isn't necessarily bad: difficult
problems can require complex solutions. My trouble is rooted in the fact that I'm a developer, not an operations engineer. I deal with Chef once every month
or two. In that time, some piece of the Chef
stack has inevitably drifted. Maybe I upgraded Vagrant. Or, more likely, some Ruby gem no longer works. Or I've forgotten some important detail about Berkshelf that's
critical to getting a recipe all the way through to production. There's enough to the stack that,
<strong>without fail</strong>, I'm debugging something broken in the tool or process itself rather than the server I'm trying to provision.
</p>
<p>
I've been on two teams now where a developer, frustrated by Chef, has started playing with Ansible and had nothing but praise for the tool. I finally
decided to give Ansible a shot and adapted part of my EC2 vm's Chef recipes.
</p>
<p>
I decided to write up my experience, as I didn't find any articles covering what I'd call a complete flow: touching everything from laying out a new
repository to setting up and running tests against a Vagrant virtual machine. For my sample cookbook, I'm installing a few packages,
adding some configuration files, installing the HotSpot JVM from Oracle, and setting the hostname.
For the full working example, clone my <a href="https://github.com/cholick/ansible_spike">Github repository</a>.
</p>
<p>
Ansible's <a href="http://docs.ansible.com/playbooks_best_practices.html">best practices</a> had some advice on directory layout, but it didn't
break up the environments cleanly. <a href="http://www.geedew.com/setting-up-ansible-for-multiple-environment-deployments/">@geedew's post here</a> has a
layout I prefer, as it better separates the environment specific configuration.
</p>
<pre style="font-family: Menlo, monospace; line-height: 1.3">
.
├── README.md
├── environments
│ ├── dev # development environment directory
│ │ ├── group_vars # group variables for an environment
│ │ ├── host_vars # host specific variable files
│ │ │ └── site_vm.yml
│ │ └── inventory
│ └── prod # production environment directory
│ ├── group_vars
│ ├── host_vars
│ └── inventory
├── roles # each subdirectory is a role
│ ├── common
│ │ ├── files
│ │ │ └── default.el # files for the role
│ │ └── tasks
│ │ └── main.yml # tasks, a main.yml is required
│ └── java
│ ├── files
│ │ └── install_jdk.sh
│ └── tasks
│ └── main.yml
├── server.yml # the master playbook
└── test # test directory
├── Gemfile
├── Rakefile # rakefile to run serverspec
├── Vagrantfile
├── spec
│ ├── default # serverspec tests
│ │ ├── common_spec.rb
│ │ └── java_spec.rb
│ └── spec_helper.rb
└── test.sh # test runner script
</pre>
<p>
At the top level is an environments directory, where each subdirectory contains
<a href="https://docs.ansible.com/playbooks_best_practices.html#group-and-host-variables">group and host variables</a> and an inventory file. The
<a href="http://docs.ansible.com/intro_inventory.html">inventory file</a> describes the hosts to run playbooks against. Below is the dev inventory file. I
specify a host, give it an alias, and configure the ssh user/key pair.
</p>
<pre class="code"><code class="ruby">192.168.33.100 ansible_ssh_user=vagrant ansible_ssh_private_key_file=~/.vagrant.d/insecure_private_key</code></pre>
<p>
My example targets a single server. To test things out, I picked something simple that varied per environment: hostname. The file host_vars/site_vm.yml
specifies all the host specific values for site_vm (192.168.33.100).
</p>
<pre class="code"><code class="ruby">hostname: cholick.com.dev</code></pre>
<p>
Next are the
<a href="https://docs.ansible.com/playbooks_roles.html#roles">roles</a>. Below is my common role's main.yml.
Like Chef, there are facilities for common things like installing packages, copying files, and setting the hostname. The install
packages block makes use of Ansible's
<a href="http://docs.ansible.com/playbooks_loops.html">loops</a>. I found the end result quite readable.
</p>
<pre class="code"><code>---
- name: update cache
apt: update_cache=yes cache_valid_time=3600
sudo: yes
- name: install common packages
apt: pkg={{ item }} state=present
sudo: yes
with_items:
- emacs23-nox
- htop
- copy: >
src=../files/default.el
dest=/usr/local/share/emacs/site-lisp/default.el
mode=0644 owner=root group=root
sudo: yes
- hostname: name={{ hostname }}
sudo: yes
</code></pre>
<p>
The second role installs Java. Unfortunately, Oracle's JVM isn't in the Ubuntu repositories (a licensing thing if I remember correctly), so I scripted
this
part of the install (which makes for a better
<a href="http://agiledictionary.com/209/spike/">spike</a>
anyway). I know there are
<a href="https://launchpad.net/~webupd8team/+archive/ubuntu/java">PPA</a> that offer this, but I haven't had good luck in the past with PPA staying current.
<a href="https://packagecloud.io/">packagecloud.io</a> could be a solution to setting up my own, but that's for another day. Here is Java's main.yml:
</p>
<pre class="code"><code>---
- name: Check java
shell: java -version || echo "undefined"
register: java_version
changed_when: False
- name: Run install script
script: ../files/install_jdk.sh
sudo: yes
when: "'Java HotSpot' not in java_version.stderr or '1.8' not in java_version.stderr"
</code></pre>
<p>
The script is slow (downloading, uncompressing, and installing Java), so I protected it with a check to only run if the JVM isn't on the box. Output of the first
task feeds into the second. install_jdk.sh is available in the
<a href="https://github.com/cholick/ansible_spike">repo for this playbook</a>
</p>
<p>
Now we come to testing. Here is where I disagree most with the Ansible authors philosophically. Their
<a href="http://docs.ansible.com/test_strategies.html">documentation says</a>:
</p>
<blockquote>
"[...] it should not be necessary to test that services are running, packages are installed, or other such things [...] so when there is an error creating
that user, it will stop the playbook run. You do not have to check up behind it."
</blockquote>
<p>
Their perspective really misses the point and misses many things that unit tests touch:
</p>
<ul>
<li>A role might be perfectly written, but it might not be on the right hosts (or any)</li>
<li>Variables consumed by tasks and roles might have the wrong values</li>
<li>A valid package is installed, but not the correct wrong one</li>
<li>
Tests help to describe the intent of the code. A test that checks that emacs is installed isn't necessarily checking up on Ansible, it's
explicitly documenting that I expect the machine to have Emacs
</li>
<li>They're a chance to fail fast, before the overhead of running in staging environments</li>
<li>Refactoring: changes to playbooks that successfully run, but no longer do the correct thing</li>
<li>
TDD: I'm sure anyone reading this already has an opinion about TDD; mine is that
it's the Right Thing™ to do
</li>
</ul>
<p>
So, Ansible playbook test support isn't as integrated out-of-the-box as I would have liked. When I investigated how Chef did its testing, though,
I found that <a href="http://serverspec.org/">Serverspec</a> does much of what I thought was actually
<a href="https://github.com/test-kitchen/test-kitchen">Test Kitchen</a>. Serverspec was also quite simple to setup. After installing the gem,
running "serverspec-init" asks a series of questions that generates a test harness.
</p>
<p>
First in the test stack is a simple <a href="http://docs.vagrantup.com/v2/vagrantfile/">Vagrant file</a>, shown below. The file specifies an
IP address (matching the dev inventory file) as well as an Ubuntu version.
</p>
<pre class="code"><code class="ruby">VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.box = "ubuntu/trusty64"
config.vm.network :private_network, ip: "192.168.33.100"
end
</code></pre>
<p>
Rakefile, spec_helper.rb, and .rspec in the test tree were generated by Serverspec. The Gemfile, shown below, simply specifies
that I want to install Serverspec.
</p>
<pre class="code"><code class="ruby">gem 'serverspec'</code></pre>
<p>
Below I've included a simple test runner script (based on
<a href="https://servercheck.in/blog/testing-ansible-roles-travis-ci-github">a post by servercheck.in</a>). It
ensures the vm is up and then runs the tests. Optionally, the script will also start from scratch
and check for <a href="http://docs.ansible.com/glossary.html#idempotency">idepmpotence</a>.
</p>
<pre class="code"><code data-language="shell">#!/bin/bash -e
if [ "$1" == "--full" ]; then
vagrant destroy --force
fi
vagrant up
ansible-playbook -i ../environments/dev/inventory ../site.yml
if [ "$1" == "--full" ]; then
ansible-playbook -i hosts ../server.yml \
| grep -qE "changed=0\s+unreachable=0" \
&& (echo -e "Idempotence test: ${green}pass${clear}" && exit 0) \
|| (echo -e "Idempotence test: ${red}fail${clear}" && exit 1)
fi
rake
</code></pre>
<p>
Finally, below are a few tests over the "common" role. They check for the existence of a package, ensure that the default emacs config
has been copied over, and verify that the hostname is correctly set per the host_vars/site_vm.yml file.
</p>
<pre class="code"><code class="ruby">require 'spec_helper'
describe package('emacs23-nox') do
it { should be_installed }
end
describe file('/usr/local/share/emacs/site-lisp/default.el') do
it { should be_file }
it { should contain /backup-by-copying/ }
end
describe command('hostname') do
its(:stdout) { should match /cholick\.com\.dev/ }
end
</code></pre>
<p>
I was quite impressed with how quickly I was able to get up and running with Ansible. This simple start to exploring Ansible, though, didn't touch two areas
that have caused me headaches while using Chef.
I didn't learn how Ansible's manages community playbooks (How are they versioned? What sort of quality are they? Does the Ansible
ecosystem have something analogous to <a href="http://berkshelf.com/">Berkshelf</a>?). I also didn't learn how difficult it will be to work
on a living cookbook a months from now. I do like enough of what I saw, though, to start using Ansible in personal projects.
It's a slick tool.
</p>
Two Years of Pair Programming
tag:cholick.com,2016:2812014-12-21T20:34:41Z
<p>
For the last two years, I've built software using pair programming. I recently switched jobs; during this process, I talked to quite a few colleagues and researched practices at many companies. I came to realize that, as rare as pair programming is, rarer still is the way in which we practiced it. When many developers are discussing pair programming, they mean something much less intense than what I have in mind.
</p>
<p>
My team generally paired for the entire working day during a sprint. Small amounts of code were written by solo developers (for example, people came in during different times in the morning or someone took a vacation day), but this was the exception. Our physical space and technology setup also supported this style of work. Each workstation drove two 27” monitors, two mice, and two keyboards. Our desks had room for a laptop on the side, which we used for tasks like email and research. At the beginning of most sprints, we switched the pairs so that each team members had the chance to work with every other member over time.
</p>
<p>
After working this way for two years, I want to reflect on the practice and share my thoughts. In part, I simply want to evangelize pair programming; I very much believe this is a great way of working as a team.
</p>
<p>
There are many perspectives on team building and team cohesion. I like
<a href="http://en.wikipedia.org/wiki/Tuckman's_stages_of_group_development">Tuckman's stages of group development</a>: Forming → Storming → Norming → Performing. Performing is a great place to be as a team: we work together without (unneeded) conflict, we're motivated, we believe in our own skills and those of our teammates, and we feel like the team as a whole is greater than the sum of its parts.
</p>
<p>
The question is: how does a team reach the performing stage? Many practices, such as
<a href="http://amzn.com/0977616649">retrospectives</a>, contribute to this growth.
But for our team, I think pairing is the biggest answer to how we successfully got there.
</p>
<p>
Performing requires that team members communicate well. The act of working together all day, every day, teaches this. Effective pairing requires a continual discussion. Through sheer practice, team members learn to communicate effectively. I would know, for example, that while working with one team member a concept might take a white board discussion, while with a different member the same thing might instead require sketching out interface signatures.
</p>
<p>
Another aspect of a performing team is understanding and appreciating each member's abilities. Here, too, pair programming excels for the same reason. Writing code together, line by line, each developer learns very quickly the strengths and weaknesses of the other team members (as well as their own). Writing software as a cross functional team requires many things: programming language and library knowledge, experience with protocols, algorithm knowledge, building a continuous integration pipeline, writing and learning build tooling, operating system knowledge, writing deployment scripts and a myriad of other skills. Working directly together on each of these problems, I quickly learned my other team members' strengths.
</p>
<p>
<a href="http://www.jamesshore.com/Agile-Book/trust.html">Trust is implicit</a>
in the definition of a performing agile team. A development team is always working toward a shared goal. Pair programming, though, takes this to another level. Every day, each developer is working directly with a second person to accomplish a specific goal. Over time, this shared experience built trust much more quickly than in other contexts I've experienced.
</p>
<p>
There are other advantages to this style of working outside of team building.
<a href="https://twitter.com/cholick/status/384816643370532864">One experience</a>
that I recall clearly is teaching something to the person I was pairing with. The next week, he taught it to the developer he was pairing with. Shortly thereafter, I heard the thing I initially taught spread to a fourth team member. Knowledge spreads very quickly among team members practicing pair programming.
</p>
<p>
Pairing spreads other types of knowledge too. In any code base, there will be examples of both the wrong and the right way to accomplish something. Chances are greatly increased when two developers write code together that at least one developer understands the proper pattern to use.
</p>
<p>
Code quality is a tricky thing to quantify, especially on a young product with a lot of churn. I believe pair programming greatly improved quality, but my evidence here is more anecdotal.
</p>
<p>
A quote by Phil Karlton comes to mind:
</p>
<blockquote>
There are only two hard things in Computer Science: cache invalidation and naming things.
</blockquote>
<p>
One common experience that jumps out at me over the last couple years of pairing is having a discussion around naming variables, methods, and classes. Over and over again, naming generated a genuine conversation. Does that method name actually convey what it does? Does that name match common patterns? Would extracting that expression to a named variable add clarity? Naming is very important to a codebase, and, in my experience, pairing helped to give it the attention it deserves.
</p>
<p>
Good tests are an attribute of quality code; going a step further,
I do believe that
<a href="http://research.microsoft.com/en-us/groups/ese/nagappan_tdd.pdf">test driven development produces better code</a>.
It can, however, be easy to slip out of the habit of writing a test first. Sometimes one can know the right thing but rationalize not doing it. It's easy to tell yourself "I'll write the test after", or, on an especially bad day, "I can see the code is working, I'll skip the test for this particular case." Pairing helps to mitigate these kinds of things. Working with another developer, it's harder to rationalize not doing the right thing.
</p>
<p>
Finally, pair programming helped me to maintain focus. While working in a more open space, distractions can abound. It can be also be easy to lose mental context when an interruption inevitably comes up and then have to spend minutes reloading the right elements into working memory (working memory in the human sense). I found that pairing helped me focus and, when I do lose focuse, to more quickly rebuild context. Pairing is a constant discussion, and this discussion forms a bubble that blocks out distractions. My auditory sense was actively engaged during development. I felt much less prone to distraction. Upon losing focus, rebuilding context went more smoothly; across two developers, we much more quickly picked something back up.
</p>
<p>
Pair programming helped our team build cohesion, it taught us to trust and communicate, and the practice disseminated knowledge and improved code quality. I firmly believe in the practice.
</p>
Eliminating Development Redeploys using Gradle
tag:cholick.com,2016:2802014-09-06T18:27:44Z
<p>
For service development, my team recently moved away from Grails to the
<a href="https://dropwizard.github.io/dropwizard/">Dropwizard</a>
framework. One of the things I really missed from the Grails stack, though, was auto-reloading: any changes to source files appear in the running app moments after saving, without a restart. It proved feasible to pull this functionality into Gradle builds as well.
</p>
<p>
<a href="https://github.com/spring-projects/spring-loaded">Spring Loaded</a> is the library that Grails uses under its hood. It supports reloading quite a few types of changes without restarting the JVM:
</p>
<ul>
<li>Add/modify/delete methods/fields/constructors</li>
<li>Change annotations on types/methods/fields/constructors</li>
<li>Add/remove/change values in enum types</li>
</ul>
<p>
The other piece I needed was
<a href="https://github.com/bluepapa32/gradle-watch-plugin">a watch plugin</a>:
something to trigger Gradle tasks when source files change.
</p>
<p>
For the full working example, clone my <a href="https://github.com/cholick/gradle_reloading_demo">demo Github repository</a>.
</p>
<p>
The first piece of setup is adding an additional
<a href="http://www.gradle.org/docs/current/dsl/org.gradle.api.artifacts.Configuration.html">configuration</a>.
This isolates the spring-loaded.jar (which is only needed during development) from the standard configurations such as compile:
</p>
<pre class="code"><code data-language="groovy">configurations {
agent
}
</code></pre>
<p>
The dependency block reads as follows:
</p>
<pre class="code"><code data-language="groovy">configurations {
dependencies {
compile 'org.codehaus.groovy:groovy-all:2.3.4'
compile 'io.dropwizard:dropwizard-core:0.7.1'
compile 'com.sun.jersey:jersey-client:1.18'
agent "org.springframework:springloaded:${springloadedVersion}"
}
</code></pre>
<p>
The compile dependencies are the standard set one would expect in a Dropwizard project. The line starting with "agent" adds the Spring Loaded dependency to the agent configuration defined earlier. The build script uses this dependency to get the spring-loaded.jar onto the file system. <em>springloadedVersion</em> is a constant defined earlier in the build file.
</p>
<pre class="code"><code data-language="groovy">task copyAgent(type: Copy) {
from configurations.agent
into "$buildDir/agent"
}
run.mustRunAfter copyAgent
</code></pre>
<p>
The above <em>copyAgent</em> task will take the spring-loaded.jar file and copy it to the build directory for later use as a
<a href="http://www.captechconsulting.com/blog/david-tiller/not-so-secret-java-agents-part-1">javaagent</a>. <em>run</em> is also configured to follow <em>copyAgent</em> in the chain.
</p>
<pre class="code"><code data-language="groovy">buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.bluepapa32:gradle-watch-plugin:0.1.3'
}
}
apply plugin: 'watch'
watch {
groovy {
files files('src/main/groovy')
tasks 'compileGroovy'
}
}
task watchThread() << {
Thread.start {
project.tasks.watch.execute()
}
}
run.mustRunAfter watchThread
</code></pre>
<p>
The above script block adds and configures watch. The <em>buildscript</em> block adds the proper repository and the
<a href="https://github.com/bluepapa32/gradle-watch-plugin">the watch plugin</a> as a dependency.
The <em>watch</em> block configures the plugin; whenever there are changes in <em>src/main/groovy</em>, the Groovy source will be recompiled. The <em>watchThread</em> task executes watch parallely. This is needed because the final job will execute two tasks which both run continuously: watch and run. <em>watch</em> would normally block <em>run</em>. Finally, the <em>run</em> task is configured to follow <em>watchThread</em> when both are part of the chain.
</p>
<pre class="code"><code data-language="groovy">run {
args = ['server', 'app.yaml']
jvmArgs = ["-javaagent:${new File("$buildDir/agent/springloaded-${springloadedVersion}.jar").absolutePath}", '-noverify']
}
task reloading(dependsOn: [watchThread, copyAgent, run])
</code></pre>
<p>
This final bit of code configures the run command with a <em>javaagent</em> flag. This tells the JVM to use Spring Loaded and let it do its magic. Spring Loaded also needs the <em>noverify</em> flag. The <em>reloading</em> task is the actual task to run during development. It strings the tasks to copy the agent, spin up a thread watching for source changes, and running Dropwizard's main method.
</p>
<p>
This configuration structure would also support frameworks outside of Dropwizard: anything with a main method, really. Though it doesn't work for all kinds of code changes, it can eliminate a great many application restarts during development.
</p>
Runtime Class Loading to Support a Changing API
tag:cholick.com,2016:2792014-09-06T18:28:32Z
<p>
I maintain an <a href="http://plugins.jetbrains.com/plugin?pluginId=7114">IntelliJ plugin</a>
that improves the experience of writing
<a href="https://code.google.com/p/spock/">Spock specifications</a>.
A challenge of this project is supporting multiple & incompatible IntelliJ API versions in a single codebase.
The solution is simple in retrospect (it's an example of the
<a href="http://en.wikipedia.org/wiki/Adapter_pattern">adapter pattern</a>
in the wild), but it originally took a bit of thought and example hunting.
I was in the code again today to
<a href="https://github.com/cholick/idea-spock-enhancements/issues/19">fix support for a new version</a>,
and I decided to document how I originally solved the problem.
</p>
<p>
The fundamental issue is that my compiled code could be loaded in a JVM runtime environment with any of several different API versions present. My solution was to break up the project into four parts:
</p>
<ul>
<li>
A main project that doesn't depend on any varying API calls and is therefore compatible across all API versions. The main project also has code that loads the appropriate adapter implementation based on the runtime environment it finds itself in. In this case, I'm able to take advantage of the IntelliJ PicoContainer for service lookup, but the reflection API or dependency injection also have what's needed.
</li>
<li>
A set of abstract adapters that provide an API for the main project to use. This project also doesn't depend on any code that varies across API versions.
</li>
<li>
Sets of classes that implement the abstract adapters for each supported API versions. Each set of adapters wraps changing API calls and is compiled against a specific API version.
</li>
</ul>
<p>
The simplest case to deal with is a refactor where something in the API moves. This is also what actually broke this last version. My main code needs the Groovy instance of com.intellij.lang.Language. This instance moved in IntelliJ 14.
<p>
<p>
This code was constant until 14, so in this case I'm adding a new adapter. In the adapter module, I have an abstract class
<a href="https://github.com/cholick/idea-spock-enhancements/blob/master/intellij-adapter-api/src/com/cholick/idea/spock/LanguageLookup.java">LanguageLookup.java</a>:
</p>
<pre class="code"><code data-language="java">package com.cholick.idea.spock;
import com.intellij.lang.Language;
import com.intellij.openapi.components.ServiceManager;
public abstract class LanguageLookup {
public static LanguageLookup getInstance() {
return ServiceManager.getService(LanguageLookup.class);
}
public abstract Language groovy();
}
</code></pre>
<p>
The lowest IntelliJ API version that I support is 11. Looking up the Groovy language instance is constant across 11-13, so the first concrete adapter lives in the module compiled against the IntelliJ 11 API.
<a href="https://github.com/cholick/idea-spock-enhancements/blob/master/intellij-adapter-11/src/com/cholick/idea/spock/LanguageLookup11.java">LanguageLookup11.java</a>:
</p>
<pre class="code"><code data-language="java">package com.cholick.idea.spock;
import com.intellij.lang.Language;
import org.jetbrains.plugins.groovy.GroovyFileType;
public class LanguageLookup11 extends LanguageLookup {
public Language groovy() {
return GroovyFileType.GROOVY_LANGUAGE;
}
}
</code></pre>
<p>
The newest API introduced the breaking change, so a second concrete adapter lives in a module compiled against
version 14 of their API.
<a href="https://github.com/cholick/idea-spock-enhancements/blob/master/intellij-adapter-14/src/com/cholick/idea/spock/LanguageLookup14.java">LanguageLookup14.java</a>
</p>
<pre class="code"><code data-language="java">package com.cholick.idea.spock;
import com.intellij.lang.Language;
import org.jetbrains.plugins.groovy.GroovyLanguage;
public class LanguageLookup14 extends LanguageLookup {
public Language groovy() {
return GroovyLanguage.INSTANCE;
}
}
</code></pre>
<p>
Finally, the main project has a class <a href="https://github.com/cholick/idea-spock-enhancements/blob/master/src/main/com/cholick/idea/spock/adapter/SpockPluginLoader.java">SpockPluginLoader.java</a>
that registers the proper adapter class based on the runtime API that's loaded (I omitted several methods not specifically relevant to the example):
<p>
<pre class="code"><code data-language="java">package com.cholick.idea.spock.adapter;
import com.cholick.idea.spock.LanguageLookup;
import com.cholick.idea.spock.LanguageLookup11;
import com.cholick.idea.spock.LanguageLookup14;
import com.intellij.openapi.application.ApplicationInfo;
import com.intellij.openapi.components.ApplicationComponent;
import com.intellij.openapi.components.impl.ComponentManagerImpl;
import org.jetbrains.annotations.NotNull;
import org.picocontainer.MutablePicoContainer;
public class SpockPluginLoader implements ApplicationComponent {
private ComponentManagerImpl componentManager;
SpockPluginLoader(@NotNull ComponentManagerImpl componentManager) {
this.componentManager = componentManager;
}
@Override
public void initComponent() {
MutablePicoContainer picoContainer = componentManager.getPicoContainer();
registerLanguageLookup(picoContainer);
}
private void registerLanguageLookup(MutablePicoContainer picoContainer) {
if(isAtLeast14()) {
picoContainer.registerComponentInstance(LanguageLookup.class.getName(), new LanguageLookup14());
} else {
picoContainer.registerComponentInstance(LanguageLookup.class.getName(), new LanguageLookup11());
}
}
private IntelliJVersion getVersion() {
int version = ApplicationInfo.getInstance().getBuild().getBaselineVersion();
if (version >= 138) {
return IntelliJVersion.V14;
} else if (version >= 130) {
return IntelliJVersion.V13;
} else if (version >= 120) {
return IntelliJVersion.V12;
}
return IntelliJVersion.V11;
}
private boolean isAtLeast14() {
return getVersion().compareTo(IntelliJVersion.V14) >= 0;
}
enum IntelliJVersion {
V11, V12, V13, V14
}
}
</code></pre>
<p>
Finally, in code where I need the Groovy com.intellij.lang.Language, I get a hold of the LanguageLookup service and call its groovy method
</p>
<pre class="code"><code data-language="java">...
Language groovy = LanguageLookup.getInstance().groovy();
if (PsiUtilBase.getLanguageAtOffset(file, offset).isKindOf(groovy)) {
...
</code></pre>
<p>
This solution allows the same compiled plugin JAR to support IntelliJ's varying API across versions 11-14. I imagine that Android developers commonly implement solutions like this, but it's something I'd never had to write as a web application developer.
<p>