public
Last active

  • Download Gist
QueryTimeouts.scala
Scala
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
/* This is when a refactoring really pays off.
*
* In order to make your code more modular, avoid hard-coding assumptions (or refactor them away).
* The most fundamental, anti-modular assumption in Object-Oriented software is the concrete type of objects.
* Any time you write "new MyClass" in your code (or in Ruby MyClass.new) you've hardcoded
* an assumption about the concrete class of the object you're allocating. These makes it impossible, for example,
* for someone to later add logging around method invocations of that object, or timeouts, or whatever.
*
* In a very dynamic language like Ruby, open classes and method aliasing mitigate this problem, but
* they don't solve it. If you manipulate a class to add logging, all instances of that class will have
* logging; you can't take a surgical approach and say "just objects instantiated in this context".
*
* There are standard design patterns to mitigate this, namely Dependency Injection and Factories.
* By taking a Factory (a function that manufactures objects) as a parameter to a function (or
* a constructor) you allow a programmer to later change his mind about what Factory to provide; and
* this means the programmer can change the concrete types of objects as his heart desires.
*
* There is another pattern, the Decorator pattern, that makes this even more powerful. Instead of
* instantiating class A instead of class B, you instantiate an A with a B, where A delegates all
* of its methods to B except where it needs to add "decorations" such as logging.
*/
 
// In this example, I have a Query object, with methods like #execute(). I want to add timeouts around all queries. I start
// by creating a QueryProxy that routes all method invocations through an over-ridable method: #delegate:
 
abstract class QueryProxy(query: Query) extends Query {
def select[A](f: ResultSet => A) = delegate(query.select(f))
def execute() = delegate(query.execute())
def cancel() = query.cancel()
 
protected def delegate[A](f: => A) = f
}
 
// Then, to implement timeouts, I create a Query Decorator:
 
class TimingOutQuery(timeout: Duration, query: Query) extends QueryProxy(query) {
override def delegate[A](f: => A) = {
try {
Timeout(timeout) {
f
} {
cancel()
}
} catch {
case e: TimeoutException =>
throw new SqlTimeoutException
}
}
}
 
// As an aside, it is worthwhile to note that the Decorator pattern is just the Object-Oriented equivalent of function composition
// in a functional language. Scala makes this especially explicit since everything is both an Object and a Function (if it
// responds to #apply). A Decorator around an object that only implements #apply is pure Function-composition as you would see in
// Haskell, ML, etc. (e.g., `f . g`). I might phrase this as: function composition is a degenerate case of the Decorator pattern.
 
// The implementation of the Timeout function is provided here for the sake of curiosities. It uses threads and is weird but cool.
 
object Timeout {
val timer = new Timer("Timer thread", true)
 
def apply[T](timeout: Duration)(f: => T)(onTimeout: => Unit): T = {
@volatile var cancelled = false
val task = if (timeout.inMillis > 0) Some(schedule(timeout, { cancelled = true; onTimeout })) else None
try {
f
} finally {
task map { t =>
t.cancel()
timer.purge()
}
if (cancelled) throw new TimeoutException
}
}
 
private def schedule(timeout: Duration, f: => Unit) = {
val task = new TimerTask() {
override def run() { f }
}
timer.schedule(task, timeout.inMillis)
task
}
}
 
// The thing that ties this all together is to make sure that everybody that needs to instantiate a Query object
// never ever calls "new Query" directly. Provide instead a Factory:
 
class TimingOutQueryFactory(queryFactory: QueryFactory, timeout: Duration) extends QueryFactory {
def apply(connection: Connection, query: String, params: Any*) = {
new TimingOutQuery(timeout, queryFactory(connection, query, params: _*))
}
}
 
// Of course, this Factory takes another Factory, allowing Factories to be composed (so we have Factory Decorators that
// take Decorated Factories that make Decorated Queries! So meta!
 
// Actually, "meta" in Greek means nothing like "meta" in English. "Meta" plus
// the accusative means "after" so Aristotle's Metaphysics is actually just a book "after [the book on] physics". Anyway.
 
// The "root" QueryFactory in this onion is the simplest QueryFactory and it just calls "new Query" etc.
 
// The thing that made me excited tonight, though, is that I had to add a new feature: per-query timeouts. We had a global
// 3-second timeout and this was proving to be stupid given that our most common query has a latency of 0.5ms and a standard
// deviation of 2ms. If you have a global timeout you must set your timeout around your most expensive query not your most
// common query (otherwise your most expensive query will always timeout!). But for a production system, cheap frequent queries,
// if they start exceeding 2 standard deviations, can take down your site. So a sensible timeout is like 5ms. But we had it
// set to 3,000 ms!! Yikes. Fortunately, this has not yet caused a problem but it's a time bomb.
 
// So anyway, how many lines of code is it to make per-query timeouts??
 
class PerQueryTimeoutTimingOutQueryFactory(queryFactory: QueryFactory, timeouts: Map[String, Duration]) extends QueryFactory {
def apply(connection: Connection, query: String, params: Any*) = {
new TimingOutQuery(timeouts(query), queryFactory(connection, query, params: _*)) // YAY!
}
}
 
// BOOM. 1 LOC. That's what modularity means in practice.
// We just look some shit up in a hashtable and we're done. By injecting the timeout value itself into the constructor of the
// TimingOutQuery, we can actually programatically modify it. Contrast this to the previous implementation, which effectively
// reached up into the air and snatched out a global variable. NOT modular.
 
// What's more, it's even more amazing not how short this code is but that it could be
// added by any programmer anywhere, regardless of whether they have access to the source code that actually instantiates and
// executes queries. And they can add it "surgically" -- just in this context or that -- not globally as the
// open classes / alias_method_chain pattern in Ruby entails.

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.