copy-edit viget posts
This commit is contained in:
@@ -2,13 +2,12 @@
|
||||
title: "First-Class Failure"
|
||||
date: 2014-07-22T00:00:00+00:00
|
||||
draft: false
|
||||
needs_review: true
|
||||
canonical_url: https://www.viget.com/articles/first-class-failure/
|
||||
---
|
||||
|
||||
As a developer, nothing makes me more nervous than third-party
|
||||
dependencies and things that can fail in unpredictable
|
||||
ways^[1](%7Bfn:1:url%7D "see footnote"){#fnref:1 .footnote}^. More often
|
||||
ways[^1]. More often
|
||||
than not, these two go hand-in-hand, taking our elegant, robust
|
||||
applications and dragging them down to the lowest common denominator of
|
||||
the services they depend upon. A recent internal project called for
|
||||
@@ -22,7 +21,7 @@ something more meaningful than a 500 page, and our developers have a
|
||||
fighting chance at tracking and fixing the problem? Here's the approach
|
||||
we took.
|
||||
|
||||
## Step 1: Model the processes {#step1:modeltheprocesses}
|
||||
## Step 1: Model the processes
|
||||
|
||||
Rather than importing the data or generating the report with procedural
|
||||
code, create ActiveRecord models for them. In our case, the models are
|
||||
@@ -30,7 +29,7 @@ code, create ActiveRecord models for them. In our case, the models are
|
||||
report generation, save a new record to the database *immediately*,
|
||||
before doing any work.
|
||||
|
||||
## Step 2: Give 'em status {#step2:giveemstatus}
|
||||
## Step 2: Give 'em status
|
||||
|
||||
These models have a `status` column. We default it to "queued," since we
|
||||
offload most of the work to a series of [Resque](http://resquework.org/)
|
||||
@@ -38,33 +37,35 @@ tasks, but you can use "pending" or somesuch if that's more your speed.
|
||||
They also have an `error` field for reasons that will become apparent
|
||||
shortly.
|
||||
|
||||
## Step 3: Define an interface {#step3:defineaninterface}
|
||||
## Step 3: Define an interface
|
||||
|
||||
Into both of these models, we include the following module:
|
||||
|
||||
module ProcessingStatus
|
||||
def mark_processing
|
||||
update_attributes(status: "processing")
|
||||
end
|
||||
```ruby
|
||||
module ProcessingStatus
|
||||
def mark_processing
|
||||
update_attributes(status: "processing")
|
||||
end
|
||||
|
||||
def mark_successful
|
||||
update_attributes(status: "success", error: nil)
|
||||
end
|
||||
def mark_successful
|
||||
update_attributes(status: "success", error: nil)
|
||||
end
|
||||
|
||||
def mark_failure(error)
|
||||
update_attributes(status: "failed", error: error.to_s)
|
||||
end
|
||||
def mark_failure(error)
|
||||
update_attributes(status: "failed", error: error.to_s)
|
||||
end
|
||||
|
||||
def process(cleanup = nil)
|
||||
mark_processing
|
||||
yield
|
||||
mark_successful
|
||||
rescue => ex
|
||||
mark_failure(ex)
|
||||
ensure
|
||||
cleanup.try(:call)
|
||||
end
|
||||
end
|
||||
def process(cleanup = nil)
|
||||
mark_processing
|
||||
yield
|
||||
mark_successful
|
||||
rescue => ex
|
||||
mark_failure(ex)
|
||||
ensure
|
||||
cleanup.try(:call)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Lines 2--12 should be self-explanatory: methods for setting the object's
|
||||
status. The `mark_failure` method takes an exception object, which it
|
||||
@@ -74,29 +75,30 @@ error.
|
||||
Line 14 (the `process` method) is where things get interesting. Calling
|
||||
this method immediately marks the object "processing," and then yields
|
||||
to the provided block. If the block executes without error, the object
|
||||
is marked "success." If any^[2](#fn:2 "see footnote"){#fnref:2
|
||||
.footnote}^ exception is thrown, the object marked "failure" and the
|
||||
is marked "success." If any[^2] exception is thrown, the object marked "failure" and the
|
||||
error message is logged. Either way, if a `cleanup` lambda is provided,
|
||||
we call it (courtesy of Ruby's
|
||||
[`ensure`](http://ruby.activeventure.com/usersguide/rg/ensure.html)
|
||||
keyword).
|
||||
|
||||
## Step 4: Wrap it up {#step4:wrapitup}
|
||||
## Step 4: Wrap it up
|
||||
|
||||
Now we can wrap our nasty, fail-prone reporting code in a `process` call
|
||||
for great justice.
|
||||
|
||||
class ReportGenerator
|
||||
attr_accessor :report
|
||||
```ruby
|
||||
class ReportGenerator
|
||||
attr_accessor :report
|
||||
|
||||
def generate_report
|
||||
report.process -> { File.delete(file_path) } do
|
||||
# do some fail-prone work
|
||||
end
|
||||
end
|
||||
|
||||
# ...
|
||||
def generate_report
|
||||
report.process -> { File.delete(file_path) } do
|
||||
# do some fail-prone work
|
||||
end
|
||||
end
|
||||
|
||||
# ...
|
||||
end
|
||||
```
|
||||
|
||||
The benefits are almost too numerous to count: 1) no 500 pages, 2)
|
||||
meaningful feedback for users, and 3) super detailed diagnostic info for
|
||||
@@ -105,7 +107,7 @@ developers -- better than something like
|
||||
the same level of context. (`-> { File.delete(file_path) }` is just a
|
||||
little bit of file cleanup that should happen regardless of outcome.)
|
||||
|
||||
\* \* \*
|
||||
***
|
||||
|
||||
I've always found it an exercise in futility to try to predict all the
|
||||
ways a system can fail when integrating with an external dependency.
|
||||
@@ -115,17 +117,5 @@ contributed to a seriously robust platform. This technique may not be
|
||||
applicable in every case, but when it fits, [it's
|
||||
good](https://www.youtube.com/watch?v=HNfciDzZTNM&t=1m40s).
|
||||
|
||||
|
||||
------------------------------------------------------------------------
|
||||
|
||||
1. ::: {#fn:1}
|
||||
Well, [almost
|
||||
nothing](https://github.com/github/hubot/blob/master/src/scripts/google-images.coffee#L5).
|
||||
[ ↩](#fnref:1 "return to article"){.reversefootnote}
|
||||
:::
|
||||
|
||||
2. ::: {#fn:2}
|
||||
[Any descendent of
|
||||
`StandardError`](http://stackoverflow.com/a/10048406), in any event.
|
||||
[ ↩](#fnref:2 "return to article"){.reversefootnote}
|
||||
:::
|
||||
[^1]: Well, [almost nothing](https://github.com/github/hubot/blob/master/src/scripts/google-images.coffee#L5).
|
||||
[^2]: [Any descendent of `StandardError`](http://stackoverflow.com/a/10048406), in any event.
|
||||
|
||||
Reference in New Issue
Block a user