Created
September 15, 2012 02:32
-
-
Save mitchellh/3726130 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require "fiber" | |
module Middleware | |
# This is a basic runner for middleware stacks based on Ruby fibers. | |
# By using fibers rather than recursion, the stack is not deepened, and | |
# therefore cannot stack overflow. This allows middleware sequences of | |
# thousands to be used without crashing Ruby. | |
# | |
# This runner is much more complicated than the normal runner because it | |
# has to do a lot of work to _simulate_ a call stack. For example, one of | |
# the benefits of middleware sequences is how nicely exceptions can be used | |
# as an error handling mechanism since exceptions naturally propagate | |
# outwards. This doesn't work at all in fibers since there is no call | |
# stack. This runner does some hacky things to simulate this. Another | |
# example is return values. Middleware can technically use the return | |
# values of calling the next middleware. With fibers, this doesn't just | |
# naturally work and we have to work around it. | |
# | |
# That being said, this runner passes all the unit tests that the | |
# normal runner does and is therefore fully API compatible. | |
class FiberRunner | |
# Build a new middleware runner with the given middleware | |
# stack. | |
# | |
# Note: This class usually doesn't need to be used directly. | |
# Instead, take a look at using the {Builder} class, which is | |
# a much friendlier way to build up a middleware stack. | |
# | |
# @param [Array] stack An array of the middleware to run. | |
def initialize(stack) | |
# We take the stack and build a proper array of callables. | |
@callables = build_callables(stack) | |
end | |
# Run the middleware stack with the given state bag. | |
# | |
# @param [Object] env The state to pass into as the initial | |
# environment data. This is usual a hash of some sort. | |
def call(env) | |
# Turn the callables into fibers. | |
fibers = build_fibers(@callables) | |
# This will keep track of the fibers that we successfully executed | |
# during the first pass, up to the `yield` (if there is one) | |
called = [] | |
# This will hold some state so we can simulate call stacks. | |
state = nil | |
# Start each fiber in order. | |
fibers.each do |fib| | |
# Start the fiber with our environment. This will run it up to the | |
# point where the "@app.call" is called, which does a `Fiber.yield`. | |
# Or, if that is never called, it will just end. | |
result = fib.resume(env) | |
# If an exception is raised, we need to track that state and then | |
# halt the fiber chain, and immediately begin cleaning out the called | |
# fibers to simulate exception propagation. | |
if result && result[0] == :exception | |
state = result | |
break | |
end | |
# If the fiber ended, then we reached the end and we don't run | |
# the next middleware. | |
break if !fib.alive? | |
# Otherwise, mark that we called it so we'll properly resume later | |
called.unshift(fib) | |
end | |
# Go through every called fiber and finish it out | |
called.each do |fib| | |
state = fib.resume(state) | |
end | |
# If we STILL are tracking an exception, then it was raised OUT of | |
# the middleware stack, and we raise it here. | |
raise state[1] if state && state[0] == :exception | |
end | |
protected | |
# When middlewares call "@app.call" to call the next middleware, this | |
# is the actual method invoked. This yields the fiber and does other | |
# things to simulate call-stack behavior for fibers. | |
def next_middleware(env) | |
# TODO: Verify only called once | |
# Yield control. When the fiber is resumed, the runner will tell us | |
# some state so we can simulate a call stack. | |
state = Fiber.yield | |
if state | |
# If there is an exception then raise it | |
raise state[1] if state[0] == :exception | |
# If there is an upstream result then return that | |
return state[1] if state[0] == :result | |
end | |
end | |
# This takes a stack of middlewares and initializes them in a way | |
# that each middleware properly calls the next middleware. | |
def build_callables(stack) | |
next_middleware = self.method(:next_middleware) | |
# Go through the stack of middlewares and convert them into callable | |
# objects. | |
stack.map do |middleware| | |
# Unpack the actual item | |
klass, args, block = middleware | |
# Default the arguments to an empty array. Otherwise in Ruby 1.8 | |
# a `nil` args will actually pass `nil` into the class. Not what | |
# we want! | |
args ||= [] | |
if klass.is_a?(Class) | |
# If the klass actually is a class, then instantiate it with | |
# the app and any other arguments given. | |
klass.new(next_middleware, *args, &block) | |
elsif klass.respond_to?(:call) | |
# Make it a lambda which calls the item then forwards up | |
# the chain. | |
lambda do |env| | |
klass.call(env) | |
next_middleware.call(env) | |
end | |
else | |
raise "Invalid middleware, doesn't respond to `call`: #{action.inspect}" | |
end | |
end | |
end | |
# This takes an array of callables and turns them into fibers that | |
# are ready for execution. | |
# | |
# @param [Array] callables An array of middleware-compliant callables. | |
# @return [Array] An array of fibers which when started will call the | |
# respective callable. | |
def build_fibers(callables) | |
callables.map do |callable| | |
Fiber.new do |env| | |
begin | |
[:result, callable.call(env)] | |
rescue Exception => e | |
# Any exceptions raised in the called are raised here. Since | |
# we're using fibers, exceptions don't work as they do when | |
# recursing through functions, but we want to simulate that, | |
# so we do some fun things here. | |
[:exception, e] | |
end | |
end | |
end | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment