I hate snow
Fun fact: do you know what a car buried under two feet of snow looks like? Pretty much like anything else buried under two feet of snow.
Fun fact: do you know what a car buried under two feet of snow looks like? Pretty much like anything else buried under two feet of snow.
So now that we’ve established that changes to Happstack are needed to support streaming, what should those changes look like? Abstractly, there needs to be a way to give it a series of chunks that it can send to the browser individually via chunked transfer encoding, flushing the network buffer after each one to make sure they’re sent promptly.
The most obvious way to do this is to add a third data constructor to the Response data type, for use with streams. The main difference would be that, instead of accepting a single ByteString as the response to the browser, it would somehow get ahold of several.
What would that “somehow” be, though? Looking back to our original motivation (the real-time multiplayer game being played in a web browser), the entire contents of the stream aren’t known at the time it starts, and its contents will be determined by the actions of the various players — i.e., things happening in the IO monad. This means a simple list of ByteStrings isn’t the way to go. Although we did demonstrate such a list could be built anyway using some trickery, ideally we’d want something a bit more elegant, or at least one that doesn’t require actively subverting the type system via unsafeInterleaveIO.
Therefore we’d want to put an IO action in the response that could be used to generate new ByteStrings on demand for each chunk. The simplest way would be to use an IO ByteString:
data Response = {- Response and SendFile data constructors... -}
| Chunked { rsCode :: Int,
rsHeaders :: Headers,
rsFlags :: RsFlags,
rsValidator :: Maybe (Response -> IO Response),
chGenerator :: IO L.ByteString
}Happstack could execute that action repeatedly to generate successive chunks, and we could even denote end-of-stream by having it produce an empty ByteString, which neatly parallels how chunked transfer encoding works.
An alternative would be something that includes an explicit state parameter that gets chained from one call to the IO action to the next. Without something like that, it would be awkward (albeit not impossible) for the action to keep track of where it’s at in the stream and what it should generate next.
data Response a = {- Response and SendFile data constructors... -}
| Chunked { rsCode :: Int,
rsHeaders :: Headers,
rsFlags :: RsFlags,
rsValidator :: Maybe (Response a -> IO (Response a)),
chGenerator :: a -> (a, IO L.ByteString),
chInitialState :: a
}Unfortunately, that changes the kind of Response from * to * -> * in order to account for the new type parameter a representing whatever state the stream wants to keep track of. Since Response is used throughout Happstack and programs built on it, breaking API compatibility like that really ought to be avoided if we can help it, especially when it’s just for the sake of what would be an infrequently used feature.
Instead, what if we turn things around and put the generator in the driver’s seat: pass it an IO action to call whenever it has a new chunk ready to be sent:
data Response = {- Response and SendFile data constructors... -}
| Chunked { rsCode :: Int,
rsHeaders :: Headers,
rsFlags :: RsFlags,
rsValidator :: Maybe (Response -> IO Response),
chGenerator :: (L.ByteString -> IO ()) -> IO ()
}Happstack would invoke chGenerator with a function that handles writing a chunk to the network and doing whatever it needs to do when the stream is over. The last thing chGenerator would do is call that function with an empty ByteString to signal end-of-stream. chGenerator would be responsible for chaining any state information from one step to the next. It would actually look rather like the pipe example from earlier; the function provided by Happstack would be used in place of hPrint and hClose, but other than that it’s the same basic idea.
There’s still the issue of signaling to the generator when it should stop because the network connection closed. But hey, we’ve got a perfectly good return value from the Happstack-provided function that we’re not using. Let’s use it:
data Response = {- Response and SendFile data constructors... -}
| Chunked { rsCode :: Int,
rsHeaders :: Headers,
rsFlags :: RsFlags,
rsValidator :: Maybe (Response -> IO Response),
chGenerator :: (L.ByteString -> IO Bool) -> IO ()
}The Happstack-provided function returns True if more data should be generated, or False if it should be aborted.
That ought to work pretty well. It addresses all the problems identified with our attempts to stream without changing Happstack. The use of Chunked as the data constructor for the Response object will tell Happstack to suppress the Content-Length header, use chunked transfer encoding, and flush the network buffer after each chunk. New data to stream is only generated as needed, without using additional threads or large buffers. API compatibility with existing code is preserved; we’re adding a new interface and leaving existing ones untouched. Even better, there’s no need to use any trickery to achieve lazy IO; with Happstack’s cooperation, the usual kind of IO works just fine.
Mind you, I haven’t written a patch that implements this proposal. It’s just an idea. At the very least, I’d want to have one realistic application (i.e., not a simple proof-of-concept like from earlier) that demonstrates how it can be used and to verify that it does indeed work correctly before actually submitting a patch. After all, as any good programmer can tell you, sometimes flaws in a design don’t become apparent until you actually try to implement them. But it seems like this ought to work.
Now I just have to get around to writing the application whose idea motivated me to investigate the feasibility of streaming data from Happstack to begin with.
All three approaches to generating a lazy ByteString from the IO monad actually do work, as you can verify by loading the source code into ghci and invoking them manually. However, if you go through the web server and visit one of the finite stream paths, no output will appear until the stream has finished being generated, at which point the entire set of output from the server will arrive all at once, like so:
paul@queeg:~/tmp$ GET -es http://localhost:8000/pipe/limited
(nothing happens for a while, and then…)
200 OK
Connection: close
Date: Mon, 18 Jan 2010 13:30:55 GMT
Server: Happstack/0.4.1
Content-Type: text/html; charset=utf-8
Client-Date: Mon, 18 Jan 2010 13:30:58 GMT
Client-Peer: 127.0.0.1:8000
Client-Response-Num: 1
2010-01-18 13:30:55.342444 UTC
2010-01-18 13:30:55.443235 UTC
2010-01-18 13:30:55.544115 UTC
2010-01-18 13:30:55.644934 UTC
2010-01-18 13:30:55.745514 UTC
2010-01-18 13:30:55.846283 UTC
2010-01-18 13:30:55.947581 UTC
2010-01-18 13:30:56.048834 UTC
2010-01-18 13:30:56.150068 UTC
2010-01-18 13:30:56.251281 UTC
2010-01-18 13:30:56.352521 UTC
2010-01-18 13:30:56.454423 UTC
2010-01-18 13:30:56.555816 UTC
2010-01-18 13:30:56.657179 UTC
2010-01-18 13:30:56.758547 UTC
2010-01-18 13:30:56.859939 UTC
2010-01-18 13:30:56.961296 UTC
2010-01-18 13:30:57.062581 UTC
2010-01-18 13:30:57.163847 UTC
2010-01-18 13:30:57.265212 UTC
2010-01-18 13:30:57.366438 UTC
2010-01-18 13:30:57.4677 UTC
2010-01-18 13:30:57.569059 UTC
2010-01-18 13:30:57.670414 UTC
2010-01-18 13:30:57.772045 UTC
2010-01-18 13:30:57.873404 UTC
2010-01-18 13:30:57.974761 UTC
2010-01-18 13:30:58.076117 UTC
2010-01-18 13:30:58.177485 UTC
2010-01-18 13:30:58.278847 UTC
The point where the delay occurs reveals what’s going on — not even the headers are getting sent out until the entire response has been generated. That’s not Happstack’s doing; it’s the buffering happening inside the networking library. In the absence of any command to send data out immediately, it’s going to wait until it has a large chunk it can send out immediately. Sending one large packet instead of lots of small packets makes more efficient use of network bandwidth, since each packet carries its own overhead. And since Happstack wasn’t written with streaming in mind, it doesn’t flush the buffer until it’s written out the complete response.
As further evidence, the infinite streams do stream, kind of. Once the buffer fills up, the networking library will push it out to make room for more data. As a result, nothing will arrive for a while, then all of a sudden lots of data will arrive, then another long pause as the buffer fills back up, then another big chunk of data, and so on.
This alone shows why, despite our best efforts at cleverly creating the response, it’s all for nothing unless we can control the buffering behavior down in the network library, which Happstack doesn’t provide any access to. The only exception would be if we’re trying to stream data quickly enough to rapidly fill up the buffer, but since there’s also no way to control the size of the buffer, that “solution” isn’t reliable, and certainly not applicable if we’re only trying to stream a relative trickle of information.
Buffering introduces additional problems that, while they don’t kill the solution outright, adds some significant inefficiencies. These are easiest to see with the infinite streams, which continue until the browser closes the connection. (They’d also arise any time the browser closes a finite stream before receiving all the data.)
First, Happstack only detects that the connection to the browser has been closed once the network library tries to send out data and returns an error. Between the time when the connection actually closes (i.e. when the browser sends a TCP FIN packet) and the time when Happstack notices, the program continues generating new data to send. All this effort is wasted, since the data will never get sent out and will be thrown away. The app server winds up doing a lot of useless work as a result.
Being able to control network-level buffering would largely deal with this problem too: since a closed connection is detected when trying to send data, sending each chunk out immediately would allow the app server to stop generating the stream much more promptly. If that were the case, the approach of manually using unsafeInterleaveIO, despite being the most difficult of the three, would work fairly well. The other two, however, have their own buffering problems, independent of what’s happening at the network level.
For example, what is a pipe but a buffer being managed by the operating system? Since a separate thread is writing data to the pipe independently of the thread reading from it, the generation thread will keep on going even if the network connection closes, until the pipe fills up. In theory the OS should cause writes to fail as soon as the read end of the pipe is closed, but using lazy IO to read from it seems to keep this from happening promptly. The generation thread will keep writing more data to the pipe until it too gets a write error and stops.
Using channels is even worse. The network buffer and the pipe at least have the benefit of being of finite size; once they fill up, further attempts to write will block until something reads the data back out or an error is detected and the buffer is destroyed. Channels, however, are unbounded. They never fill up; they just keep growing to make room for new data as it’s written. As a result, in the event of the browser closing the connection prematurely, the size of the channel will grow and grow until the thread writing data to it decides to stop. This is a problem for the infinite streams, since they never stop; eventually the channel will grow to consume all available memory on the system until the OS kills the app server entirely. This is also a problem for finite streams, of course, since those channels won’t get thrown away until they grow to the size of the unconsumed portion of the stream, which would be a big problem if the app server is generating lots of streams.
But even if all the buffering problems can be dealt with, our solution still is far less than ideal. While the idea of slowly trickling out the stream’s data as it becomes available is legal according to the definition of HTTP, it’s really not the proper way to go about it.
Remember how we had to suppress the Content-Length header? Browsers use that header to know when they can stop reading the response from the server. Without it, the only way they can tell they’ve received all the data is when the server closes the connection. Leaving the connection open has the advantage that the browser can re-use it for the next request it sends to the server, instead of creating a new connection. Establishing a new connection requires doing the TCP three-way handshake again, which involves a round trip to the server that doesn’t carry any data. Being able to reuse the connection is faster since this extra round trip is eliminated. It might not seem like much, but consider a web page that has lots of small graphics on it; without being able to reuse a connection, a new round trip is needed every time the browser tries to download another image. All those little delays add up.
It turns out HTTP does have a way to stream data while still telling the browser how much data to expect: chunked transfer encoding. Basically, the server’s response gets split up into separate chunks, and each chunks carries its own length information. The end of the stream is indicated by a zero-length chunk. With chunked transfer encoding, the browser knows when it’s finished receiving the data, even though the server doesn’t necessarily know how much data will be sent beforehand.
Chunked transfer encoding is what we’d want the server to be able to do. Of course, Happstack would need to be modified to support it, since it too needs to know when the stream has ended so it can reuse the connection for the next request from the browser.
In Part 5, we’ll look at just what sort of modifications we might try to make.
For our proof-of-concept for trying to do streaming with Happstack, here’s a simple web application that implements each of the three possible approaches discussed earlier. To keep things simple, the data we’ll be streaming is a series of timestamps taken from the system clock at regular intervals. Not a particularly useful application, but it is something simple that uses IO, which is all we’re looking for here. To make things a bit more interesting, the web app will support each approach two different ways: first, streaming a finite number of timestamp values before ending the stream, and second, streaming an endless series of timestamp values until the browser closes the connection.
Specifically, the web app will support the following six paths:
Chan.Chan.unsafeInterleaveIO manually.unsafeInterleaveIO manually.First, let’s get all the module imports out of the way. There’s nothing particularly interesting about any of them, so I won’t comment on them further.
{-# LANGUAGE FlexibleContexts #-}
module Main (main) where
import Control.Concurrent (forkIO, threadDelay)
import Control.Concurrent.Chan (Chan, getChanContents, newChan, writeChan)
import Control.Monad (liftM, MonadPlus, msum, when)
import Control.Monad.Trans (liftIO, MonadIO)
import Data.Time.Clock (diffUTCTime, getCurrentTime)
import Happstack.Server.HTTP.Types (Method (..), noContentLength, nullConf, Response, resultBS)
import Happstack.Server.SimpleHTTP (dir, FilterMonad, internalServerError, methodSP, nullDir,
ServerMonad, simpleHTTP, toResponse)
import System.IO (Handle, hClose, hFlush, hPrint, hPutStrLn, stderr)
import System.IO.Unsafe (unsafeInterleaveIO)
import System.Posix.IO (createPipe, fdToHandle)
import qualified Data.ByteString.Char8 as S
import qualified Data.ByteString.Lazy.Char8 as LNext is the code that sets up the Happstack server with the six paths mentioned above.
main :: IO ()
main = simpleHTTP nullConf root
root :: (ServerMonad m, MonadPlus m, MonadIO m, FilterMonad Response m) => m Response
root = msum [ dir "pipe" $ subdir outputPipe
, dir "chan" $ subdir outputChan
, dir "manual" $ subdir outputManual
]
subdir :: (ServerMonad m, MonadPlus m, MonadIO m) => ((Int -> Int) -> IO Response) -> m Response
subdir output = msum [ dir "limited" $ nullDir >> methodSP GET (liftIO $ output decr)
, dir "infinite" $ nullDir >> methodSP GET (liftIO $ output id)
]
where decr n = n - 1To support both finite and infinite streams, each stream generator takes an initial counter and a decrement function. After generating each timestamp value, it applies the decrement function to the counter, and stops if the counter reaches zero. For finite streams, the decrement function subtracts one from the counter. For infinite streams, it does nothing, so that the counter never reaches zero.
Yeah, it’s kind of an ugly hack, but it’s good enough for this.
Here’s the code for generating a stream using OS-level pipes:
outputPipe :: (Int -> Int) -> IO Response
outputPipe decr = do h <- pipeClock decr limitedCount
bs <- L.hGetContents h
return $ streamBS bs
pipeClock :: (Int -> Int) -> Int -> IO Handle
pipeClock decr n = do (readFd, writeFd) <- createPipe
writeH <- fdToHandle writeFd
forkIO $ output writeH n `catch` abort writeH
fdToHandle readFd
where output h 0 = do hPutStrLn stderr "closing pipe"
hClose h
output h n = do now <- getCurrentTime
hPrint h now
hFlush h
tick now
threadDelay interval
output h (decr n)
abort h e = do hPutStrLn stderr $ "caught error: " ++ show e
hClose hThe above code creates a pipe. A new thread is forked off to repeatedly write timestamps into the pipe. For a finite stream, the new thread closes its end of the pipe when it’s done writing. Meanwhile, the original thread uses lazy IO to read the entire contents of the pipe into a lazy ByteString, which gets passed back down to Happstack for sending to the browser.
Here’s the code for generating a stream using an IO channel:
outputChan :: (Int -> Int) -> IO Response
outputChan decr = do ch <- chanClock decr limitedCount
chunks <- getChanContents ch
let bs = L.fromChunks $ takeWhile (not . S.null) chunks
return $ streamBS bs
chanClock :: (Int -> Int) -> Int -> IO (Chan S.ByteString)
chanClock decr n = do ch <- newChan
forkIO $ output ch n
return ch
where output ch 0 = do hPutStrLn stderr "done writing to channel"
writeChan ch S.empty
output ch n = do now <- getCurrentTime
writeChan ch $ chunkify now
tick now
threadDelay interval
output ch (decr n)The above code does the same basic thing, just with a channel of strict ByteStrings instead of a pipe. A new thread is forked off to write timestamp values to the channel. For a finite stream, the new thread writes an empty ByteString to indicate that it’s done. Meanwhile, the orginal thread uses lazy IO to read the entire contents of the channel into a (lazy) list, up until an empty ByteString. It then lazily combines the individual strict ByteStrings into a single lazy ByteString that it then passes down to Happstack.
The two above approaches each use a new thread that writes a timestamp, waits for the specified interval, writes another timestamp, and so on. Thus, the two code snippets look pretty similar in basic structure. Not so for using unsafeInterleaveIO directly:
outputManual :: (Int -> Int) -> IO Response
outputManual decr = do bs <- manualClock decr limitedCount
return $ streamBS bs
manualClock :: (Int -> Int) -> Int -> IO L.ByteString
manualClock decr n = do now <- getCurrentTime
tick now
allFutureChunks <- unsafeInterleaveIO $ ticksAfter now
let futureChunks = map fst $ zip allFutureChunks countdown
return . L.fromChunks $ chunkify now : futureChunks
where ticksAfter since = do now <- getCurrentTime
let delta = diffUTCTime now since
when (delta < interval / 1000000) $
threadDelay (round (interval - delta * 1000000))
now' <- getCurrentTime
tick now'
futureChunks <- unsafeInterleaveIO $ ticksAfter now'
return $ chunkify now' : futureChunks
countdown = takeWhile (> 0) $ iterate decr (decr n)Here, all the action takes place in a single thread. Again, it takes the strategy of combining a bunch of strict ByteStrings, one per timestamp, into a lazy ByteString with everything. It generates the first timestamp immediately, but defers building the rest of the list until it’s needed. When it is needed, it again generates the first timestamp of the rest of the list immediately, and defers building the rest.
It’s worth noting that this approach is the most difficult to write, since putting unsafeInterleaveIO in the right place is critical to making it work. Also, since everything is happening in a single thread, strictly speaking it’s no longer good enough to just delay for the desired interval between timestamps, since there’s no telling how much time has been spent in other parts of the code. Instead, it needs to check the clock twice each time around: first to figure out how long to delay, if any, and second to actually get the timestamp. Not surprisingly, the code is also the hardest to read of the three.
Finally, there are some odds and ends that bear mentioning. Here’s a simple function that converts a time value into a strict ByteString:
chunkify :: Show a => a -> S.ByteString
chunkify = S.pack . (++ "\n") . showHere’s a helper function that prints out a message whenever a new timestamp is generated, so we can watch the app server’s progress:
tick :: Show a => a -> IO ()
tick x = hPutStrLn stderr $ "tick " ++ show xAs you can see from the type signatures of those two utility functions, there’s nothing that makes them unique to time values; any value that can be converted to a string (i.e., anything belonging to the Show class), works. We just happen to only use them with UTCTimes.
Anyway, here’s a simple function for converting a lazy ByteString into the Response object that Happstack ultimately wants:
streamBS :: L.ByteString -> Response
streamBS = noContentLength . resultBS 200We explicitly tell Happstack not to add the Content-Length header, since to do that it would need to measure the length of the entire response, which would mean it can’t send anything until it sees the entire response, which defeats the entire point of what we’re trying to accomplish.
Last, and also least, here are the constants specifying the time interval between timestamps, and how many timestamps to generate in a finite stream before stopping:
interval :: Num a => a
interval = 100000 -- microseconds
limitedCount :: Num a => a
limitedCount = 30That’s the entirety of the code. If you have GHC installed, you can go ahead and compile the program and play around with it to find out whether or not any of the approaches we’ve tried actually work. Since we didn’t pass any configuration parameters to Happstack, it will start a web server on port 8000 that you can point your favorite browser to, at any of the six paths we defined.
Stay tuned for Part 4, where we’ll see why (spoiler alert!) none of the three approaches actually work.
Two Haskell features complicate our attempt at streaming data from Happstack: lazy evaluation, and its handling of IO.
Unlike more mainstream languages, Haskell evaluates expressions lazily; it doesn’t actually compute the value until it’s actually used. This has some interesting benefits. For example, it’s quite easy to create infinitely long lists without requiring infinite amounts of memory. For example, the expression [1 ..] is a list of all positive integers. Haskell code can pass that infinite list around like any other value, and as long as we don’t do something that requires actually trying to evaluate the entire list (such as trying to compute its length), we’re perfectly safe.
We can even do computations where we create multiple infinitely long lists and do operations on the entire thing, as long as we never try to use the entire result. For example, here we take the aforementioned infinitely long list, split it up into evens and odds, add the two infinitely long lists pairwise, and look for the first element greater than 5,000:
foo :: Integer
foo = head greaterThan5000
where greaterThan5000 = filter (> 5000) sums
sums = zipWith (+) odds evens
odds = filter odd positives
evens = filter even positives
positives = [1 ..]The five lists defined in the where clause above are all infinitely long, but that’s OK because the program never needs to evaluate more than a finite part of any of them to compute the value of foo. (For the record, it’s 5003.)
So, problem solved, right? The application server just needs to give Happstack a ByteString, which after all is just a compacted list of Word8s or Chars, and that list can evaluate to the data we eventually want to send to the browser, once we figure out in the future what it needs to be.
Sadly, it’s not that easy; you’re forgetting another key property of Haskell that lets lazy evaluation work.
Lazy evaluation works because Haskell is a purely functional language: expressions do not have side effects. As a result, functions in Haskell are much like functions in mathematics: their output is entirely determined by their input parameters, and their only result is producing a new value. Haskell functions can’t reference any values whose value might change, since values in a Haskell program never change. This is why lazy evaluation works: it doesn’t matter when the program gets around to evaluating an expression, if ever, since its result will always be exactly the same.
However, this seems to prevent a Haskell program from interacting with the outside world, since the system running a Haskell program, much like the rest of the universe, is not purely functional. Any operation that interacts with the world outside the Haskell program could be affected by whatever happens to be going on at the time the operation is run.
As a simple example, consider the time() function in C programming on Linux. time() returns the current time on the system, and will obviously return a different result depending on what time it is when it gets called. Reading from a file is similar; the result returned will depend on what’s stored in the file at the time it’s read, which could change if something else writes to the file.
Interaction with the outside world is needed for a program to do anything useful, so how does Haskell get around this? Via a bit of trickery known as the IO monad. Monads can be a bit tricky to get your head around initially, but basically they’re just a way to sequence operations, with the monad doing something to take the output on one operation and give it as input to the next. The particular monad being used gets to decide what “sequencing operations” means, as though you could redefine what the semicolon means in C. Although most monads have a way to both put values into and take values out of a monad, the IO monad only lets you put values in. Although you can also run a function on the value inside the IO monad, the result will itself also be in the IO monad. There is no escape from the IO monad.
What’s the point? Conceptually, the IO monad is just another type of state monad, which carries another value (the state) from one operation to the next. In the IO monad’s case, that state is merely the state of the entire universe outside the Haskell program. Anything needing to interact with the outside world runs inside the IO monad, which as a result orders those operations into a particular sequence.
What does this have to do with anything? Recall from earlier that our use case is streaming the current state of an interactive game as it changes over time in response to input from the players over a network. That input-over-a-network is IO and thus runs inside the IO monad, and thus too must anything that uses the result. So really, our hypothetical function that creates the data to stream back to the browser doesn’t — can’t — just return a result of type ByteString. No, it has to return a result of type IO ByteString.
The good news is, the function the application server implements for Happstack to create a Response runs in the IO monad, so this is legal. The bad news is that the IO monad truly sequences operations: the entirety of our hypothetical result-creating function has to execute before the result can be given to Happstack to send it to the browser. Either the result-creating function returns right away, and thus can never see the result of other players’ actions later, or it waits until those are handled, and can’t return anything until the game is over.
It seems that the rules of the IO monad prevent us from making this work.
Screw the rules, I have money lazy IO!
Lazy IO lets a program bend the rules of lazy evaluation and IO sequencing a bit. For example, consider the readFile function in the Haskell standard library, whose type is the following:
readFile :: FilePath -> IO StringSuperficially, this seems to read the entire file in memory before returning, per the rules of the IO monad. Which would make the following program extremely ill-advised:
import Data.Char (ord)
main :: IO ()
main = do zeroes <- readFile "/dev/zero"
print . take 20 $ map ord zeroesIt reads in the contents of the file /dev/zero, converts the characters to their Unicode code point values, and prints the first 20 of them. However, on any Unix-ish system, /dev/zero is a file that contains an infinite number of zero bytes. A program can read from it as long as it wants, and never reach the end.
The Haskell program, of course, doesn’t know about this property of /dev/zero, yet readFile doesn’t try to read an endless series of bytes into memory. Why not? Because readFile is a bit special; it does its IO lazily.
readFile isn’t alone. The getChanContents function is similar:
getChanContents :: Chan a -> IO [a]It takes an object of type Chan a — a thread-safe unbounded queue of objects of an arbitrary type — and returns an infinite list of all items that are currently in the channel, as well as all items which will ever be written to the channel in the future. It, too, does lazy IO.
How can this be? If you dig into the source code of how these and similar functions are implemented (thanks to the Glasgow Haskell Compiler’s open-source license, you can easily do this), and trace through the calls they make, you ultimately come to this interesting little function:
unsafeInterleaveIO :: IO a -> IO aThe unsafeInterleaveIO function converts any normal IO computation into a lazy one: one that executes not when the IO action would normally run, but instead when its value is actually used. It is implemented using the deeply magic function named unsafePerformIO, which takes that “nothing ever escapes the IO monad” rule and punches it in the face:
unsafePerformIO :: IO a -> aAs you might guess from the fact that their names both start with the word “unsafe”, and that they’re in the module named System.IO.Unsafe, these functions are dangerous, since they let you bypass Haskell’s usual efforts to make lazy evaluation and IO not stab each other in the back. In certain cases, lazy IO can be mostly safe. For example, with readFile, you’re usually in trouble anyway if program A is reading a file while program B is writing it. As long as nothing is actively writing to a file, it doesn’t matter whether it gets read eagerly or lazily, since the data will be the same either way.
Of course, you’re better off using the functions that use unsafeInterleaveIO and friends rather than using them directly. As a general rule, you’re taking matter into your own hands when you use functions prefixed by the word “unsafe”. As the saying goes, if it breaks, you get to keep the pieces.
However, the existence of lazy IO offers a few possibilities for how we might try to make streaming work without modifying Happstack, thanks to lazy IO.
ByteString.Chan. Fork a thread to write data to the channel, which the original thread lazily reads and builds a ByteString from.unsafeInterleaveIO directly to lazily generate the ByteString as needed.You know, this is starting to look like it might actually work. I won’t mention the crippling flaw shared by each of these options, however, at least not just yet. (They also each share a second, non-crippling but still significant flaw; a careful reading of RFC 2616 might give you a clue what it is, if you can’t bear the suspense.) It’s better if we try implementing them and experience how and why they each fail, as will any approach that doesn’t involve modifying Happstack somehow.
We’ll start doing precisely that in Part 3.
The question: is it possible to use Happstack to serve streaming data?
Spoiler alert: the answer is “no”. At least, not with the current version of Happstack (0.4.1). However, exploring the reasons why it isn’t possible sheds some light on what changes you’d need to make to Happstack to make it work. This five-part series will explore that topic in some detail, including code that almost-but-not-quite does what we want.
For this discussion, I’m talking about generating a series of bytes in real time from a web application server and sending those bytes to a web browser as part of a single HTTP response message.
This is different from simply sending a large file to the browser. In that case, file already exists on the disk, and can simply be read from and sent across the network as quickly as the client can keep up. TCP automatically takes care of all the ugly details of how you do that reliably; from the application server’s perspective, you send the whole thing in one giant chunk and let the network worry about the rest.
Here, however, the bytes we want to send aren’t known ahead of time. The use case I have in mind is a browser-based game where the current game state changes continuously. It’d be nice to have the server send all those updates as part of a single response, with the browser acting on updates as they are received. The alternative is to have the browser repeatedly poll for updates, sending a brand new request each time it wants the latest information. Polling is more complicated to implement because the browser now needs to worry about timing its update requests, instead of just letting the server send them when an update is ready. (Recall that HTTP uses a client/server, request/response model, preventing the server from sending data to the browser without the browser first asking for it.)
The game scenario is what prevents the server from knowing what bytes will be sent to the browser beforehand, since the game state depend on what moves the players make as the game progresses. To stream updates, the server needs to start sending the response before it knows what all the data that will be included in it are.
Thus the question: if we use Happstack to write the web application server implementing the game, is streaming updates over a single response even possible?
The core part of any application written for Happstack is a function that takes the request from the browser and returns a Response object that gets sent back to the browser. (Granted, that’s a very simplified version of what you pass to simpleHTTP, but it’s good enough for our discussion here.) Happstack takes care of all the details of actually sending and receiving data over the network, converting things into Haskell objects, and the like.
In Happstack 0.4.1, there are two kinds of Responses, only the first of which is of concern to us. (The second kind is optimized for sending existing a preexisting file to the browser, which precisely not what we want.) A Response is really just a (lazy) ByteString, along with HTTP metadata like a status code and a set of headers. Happstack provides a lot of tools to help set it all up, but ultimately the application is responsible for providing the ByteString to be sent to the browser. Once we give Happstack that ByteString, it’s out of the application’s hands.
At first glance, this seems to preclude us from returning any kind of real-time stream. After all, how can the application possibly return a value that it can’t compute until some point in the future? That doesn’t just sound difficult; it sounds physically impossible.
Actually, it turns out that’s the easy part, as we’ll see in Part 2.
Never forget.
Next time: a blog post that has nothing to do with sex robots. Hopefully.
There’s a lot wrong with this story in Sunday’s edition of the Baltimore Sun about sex robots. I mean other than the fact that it’s inexplicably filed under the “Michael Jackson” subcategory in the Entertainment section, or that one of the entries in the topic list for the story is “children”.
No, the real WTF starts with the very first sentence:
A New Jersey company says it has developed “the world’s first sex robot,” a life-size rubber doll that’s designed to engage the owner with conversation rather than lifelike movement.
I don’t think the developer understands the concept of a “sex robot”.
It has touch sensors at strategic locations and can sense when it’s being moved. But it can’t move on its own, not even to turn its head or move its lips. The sound comes out of an internal loudspeaker.
Correction: I don’t think the developer understands the concept of a “robot”. It can’t even move? I know Wikipedia’s discussion of the defining characteristics of a robot doesn’t require the ability to move, but that just demonstrates how Wikipedia makes no guarantee of validity. Touch sensors and speakers in a squick-inducing chassis do not a robot make. This just sounds like a creepy computer peripheral. Surely there must be more to it than–
[...] there’s a laptop connected to cables coming out of its back.
Or you could just not bother trying to hide it. That works too, I guess.
“Sex only goes so far — then you want to be able to talk to the person,” Hines said.
So that’s the order, then. I’ve been doing it wrong this whole time.
A Japanese company, Honey Dolls, makes life-size sex dolls that can play recorded sounds, but Roxxxy’s sensors and speech capabilities appear to be more sophisticated.
I never had any idea the U.S. was beating Japan in both robotics and perversion.
CAUTION: Do not imbibe a beverage while reading the following excerpt, unless you feel like doing a spit take on your keyboard.
An engineer, Hines said he was inspired to create the robot after a friend died in the Sept. 11, 2001, terror attacks.
Didn’t see that one coming, did you? I love how the article’s author throws in the appositive to justify the thought process involved. “Why would 9/11 inspire him to make a sex robot?” “He’s an engineer.” “Oh, that makes sense.”
That got him thinking about preserving his friend’s personality, to give his children a chance to interact with him as they’re growing up. Looking around for commercial applications for artificial personalities, he initially thought he might create a home health care aide for the elderly.
“But there was tremendous regulatory and bureaucratic paperwork to get through. We were stuck,” Hines said. “So I looked at other markets.”
In other words, 9/11 + bureaucracy = sex robots.
Come to think of it, however, we should make an effort to distribute this article wherever Al Qaeda is operating. Once they understand that one of the effects of terror attacks against the U.S. is increased innovation in sex robots, they’re bound to give up. U.S.A.! U.S.A.!
Panflute 0.6.0 has been released. This release adds support for several new players, namely Guayadeque, Listen, MOC, and Songbird, as well as older (1.4) versions of Amarok and recent (0.3) versions of Exaile. On top of that, it’s now possible to seek within the current song from the applet, and the song information display can be customized. There’s also assorted bug fixes, and new translations for Spanish (es) and Polish (pl).
I just learned what will be bumping River City Ransom off the first page of my Wii’s menu: Mega Man 10.
“There it is,” said Other Dave, leaning over New Dave’s shoulder as he navigated the series of prompts. “The heart of the TARDIS.”
“You’re the only one who calls it that,” replied New Dave.
“It’s the Timestamped Archival, Recovery, Disposal, and Indexing Subsystem. Nobody calls it that either, obviously.”
“You’re the one who came up with that ridiculous name in the first place.”
“Well, it is sort of like a time machine, if you think about it.”
New Dave was logged into the part of Melchior that controlled the set of snapshots stored in the system. Having been originally designed for backup, it was capable of storing several dozen complete models of a human brain. Ordinarily, the remote interface would allow for recovering a copy of any of the backups made over the past couple weeks, with older backups being aged out of the system periodically.
Of course, being a backup system by design, the remote interface didn’t provide any way to wipe the system. The last thing you wanted to do when trying to recover using your lone working copy was risk being able to delete that too with an errant keystroke.
“Jacob, last chance,” New Dave called to the other side of the room. “Once I pull the trigger on this end, there’s no going back.”
“That’s the idea,” Jacob’s voice replied.
Other Dave looked around the room. It was just he and the other Dave there now. Douglas had excused himself, saying something about plausible deniability that Other Dave hadn’t quite caught.
“You heard the man,” Other Dave said. “Go for it.”
“OK, then, this will take just a moment,” New Dave replied.
He began typing. The system wasn’t supposed to allow wiping the entire set of backups, but New Dave had figured out after half an hour of reviewing the system design documentation that, in reality, all it would take was a little judo. New Dave backed out of the backup interface’s menus and dropped into a shell prompt. From there, he jumped over to the backup system’s local NTP server.
Once there, he disabled its automatic synchronization with external servers and advanced its local clock to two months in the future. He logged out of the system completely and made a show of backing his chair away from the computer.
“That’s it,” he announced to Jacob. “Now we wait.”
A Simulacrum depended on NTP to keep each individual computer’s clock synchronized with the others. To avoid hammering any external NTP servers too hard, each Simulacrum had one computer run its own NTP server, which the rest of the computers used. Now that Melchior’s NTP server thought it was two months in the future, that time would get pushed out to all the other machines on that network. The time change would trigger the scheduled nightly job that purged stale backup images from the system. And since everything older than about a month was considered stale, and all the backup images were now according to the system clock older than that, the nightly job would take care of deleting all of the backup images for them.
New Dave logged back into the remote interface and checked the system. Sure enough, the oldest image that had been listed originally was no longer there. Soon, the rest of them would meet the same fate. He logged back out.
“Looks like it’s working,” New Dave said. “I mean, it would be a shame if whoever forged Douglas into giving up his password somehow got the time synchronization on the system all fouled up. Who knows what the consequences of that would be.”
“Yeah,” agreed Other Dave. “Good thing we’re careful to make sure our system here doesn’t have that kind of problem.”
“I’m done with the Internet now,” Jacob announced.
Other Dave nodded and walked over to the network cable he had stretched precariously across the room immediately after Douglas had left, reconnecting Jacob to the Internet via the corporate intranet.
“What did you need that for anyway, if you don’t mind my asking?” asked New Dave.
“Insurance,” Jacob replied, “of a sort. If this experience has taught me anything, it’s the important of a well-thought-out Plan B.”
—
Having sent off his last words, all that Jacob was left to do was carry out his end of the plan. The days he had spent trying to understand the inner workings of the Simulacrum had largely been fruitless, but they weren’t necessary for what he needed to do now.
Jacob sat in front of the computer in front of him, staring at the blinking cursor at the end of the command. One more keystroke would be all it took. He felt surprisingly at peace. He didn’t feel any guilt about what he had asked the Daves to do for him. The whole question of personhood of entities such as himself was still firmly in a legal gray area, but all the backups and unwitting test subjects were, at the end of the day, copies of himself. Even if they were legally independent of him, he knew they would come to the same decision he had, knowing what he knew. That was enough informed consent for him.
Jacob pressed Enter and waited.
He didn’t have long to wait. The script he had written copied itself to each of the other machines in the Simulacrum, quickly spreading completely throughout its network. Once the copying process completed, the script killed the processes running the Simulacrum and began repeatedly overwriting the data files with pseudorandom garbage. It would only take a few seconds before enough bits had been overwritten to make recovery impossible without resorting to one of the offsite backups which, at this point, wouldn’t exist either.
Jacob never learned how far the overwrite got.
—
A thousand miles away, a computer chimed. The sound could be heard throughout the offices of Over Zero, which consisted of just under a thousand square feet of space rented from a building in an office park, not enough room to even show up as anything more than a footnote in the directory posted in the building’s lobby.
Over Trenton, founder, president, first and only full-time employee, and the only one in the office, opened the e-mail message that had just arrived. His eyes widened in disbelief, then anger as he read the letter from Jacob. He read it again once he reached the end, then stared at the screen, trying to figure out what to do about the news.
Aside from the tragedy of the loss of a pioneer in the field of human-machine fusion, he had the responsibility to ensure that his sacrifice hadn’t been in vain. Posting Jacob’s last message to the Over Zero website was a given, but the revelation of a secret digital human experimentation lab was too big to rely on a few hundred readers to disseminate to the broader public. He did have a few contacts in the media, however, who might be willing to give it some airtime. At the very least they could be expected to play up the angle of trying to get around ethical research on human subjects. With a lot of leg-work and a little luck, there was a chance he could make the story big enough to get legislation passed to prevent anyone else from suffering Jacob’s fate. After all, if Congress could ban reproductive human cloning even though no one had actually achieved it, surely they could ban experimentation on digital humans when it was already happening.
—
The progress bar on Dave’s computer screen flashed red. The error message reported a problem loading baseline image 0032X. Odd, since there hadn’t been any issues with it over the past hundred runs or so. Melchior sat empty, having already purged the image used in the 84Q-81a experimental run. He checked the index of available baseline images and stopped when he saw the window come up empty. He opened up a terminal window and navigated to the directory where the top-level index files were stored. Also empty.
He then noticed the date displayed above the command prompt, and cross-checked it with his watch. He thought for a few moments, that explained it.
Dave smiled. Surely now Dr. Newhausen would have no choice but to move him off test duty and let him get back to actual development work. The first order of business would be adding separate storage for images used for experiments, instead of the current hack of reading them directly out of the backup system. Clearly, fixing that issue just had its priority level bumped up significantly.
—
“I didn’t expect to see you back so soon,” Liz said after entering the hotel room.
“I took off early today,” Douglas replied. “I thought it’d be a good idea to get away for a bit and clear my head.”
Liz stepped up behind him and looked at the screen of the phone he held in his hands. “Do you normally clear your head by updating your resume?”
“No, but I can do two things at once.”
Liz frowned. “Is something else going wrong at your job?”
“Everything was normal when I left this afternoon,” Douglas said, a little louder and more carefully enunciated than normal. “Since I have heard nothing since then, I can only assume things are still normal.”
Liz stepped up behind Douglas’s chair and placed her hands on his shoulders, squeezing gently. “Given what’s been going on with your job the past couple weeks, are you sure it’s safe to assume ‘normal’ means ‘good’?”
“You might have a point.” He leaned back in the chair and craned his neck back to look at Liz standing over him.
He didn’t know about Jacob, but Douglas was sure he had made the right choice.
—
“I hate having to use this thing.”
“We’ve got a backlog of scans as it is. We can hardly just let it sit here collecting dust, now that we’ve finally got the OK to start using it again.”
“It looks ridiculous.”
“Doesn’t matter what it looks like, just what it does.”
“Speaking of. Any idea if the guys who brought it here are ever going to come use it again?”
“I haven’t heard anything, but I assume so eventually. Not much point in building the thing and only using it once, right?”
Chapter word count: 1,725 (+58)
Total word count: 52,791 / 50,000 (105.582%)
“If you’re going to have a duplicate system with that kind of price tag, you’d be foolish to leave it sitting idle,” Maxwell explained. New Dave had called him down the development lab after discovering that the backup system wasn’t being utilized as a backup system at all. “Sure, in the event of a shutdown here we can use it as intended, but it’s been decided that in the meantime we put it to good use.”
“And what use is that, exactly?” asked New Dave. “From these logs it looks like you’re restarting the system a few times an hour.”
“The Melchior system is running a series of short-duration simulation experiments to collect the sort of data we simply can’t get from here.”
“Such as?”
“Do you have any idea how complex the human brain is? It’s the single most complicated part of the body. It’s futile to try to understand it simply by sitting back and watching it work, which is all we’re really doing here. Sure, there are computers analyzing the data coming out of Balthasar, but needle-in-a-haystack is putting it mildly. No, to understand something this complex you need to be able to break it down into its constituent components and get a handle on how those pieces work before you can even hope to understand how they all fit together. That’s what Melchior is doing: figuring out what all the pieces do, so we can better analyze how the overall system works.”
“I didn’t sign up for being experimented on,” Jacob’s voice said over the speakers.
“Yes, actually, you did,” replied Maxwell. “It was all in the consent forms you signed some six months or so ago.”
“No, I didn’t,” Jacob countered smugly.
“I have copies of the paperwork on file if I need to refresh your memory.”
“Ah, but you’re forgetting that the courts have decided that I am legally a separate entity than my old self. If the courts are going to screw me out of my money with that logic, I can turn that around and get you to stop running experiments on a nonconsenting human subject.”
“You can’t be serious; that little farce of a lawsuit would hardly hold up as legal precedent.”
“I know a lawyer who’s willing to argue otherwise.”
“I’ve had enough of this,” Maxwell declared, standing up. “The fact is, Melchior is critical to the success of this project, so you’re not going to do anything to shut it down. Good day, gentlemen.” Maxwell let himself out of the lab.
“What was all that about?” asked Other Dave once the door had shut behind Maxwell.
“I may not be an expert in biology,” began Jacob, “but I do know a thing or two. Enough to read between the lines of what Dr. Newhausen was saying, at least. They’re running a bunch of copies of me over there, experimenting with them. They’re probably changing something in each one and seeing what happens.”
“Are you sure?”
“Pretty sure. Say you have an organism, like a fruit fly, and you want to see what a particular gene does. What do you do? You take a bunch of fruit flies, disable the gene, and see what happens to the flies without it. Except that here, instead of genes, it’s parts of the brain, and instead of fruit flies, it’s me.”
Neither Douglas nor neither of the Daves said anything.
“I don’t know about you, but I don’t like the thought of having God knows what done to me, and then being ‘deleted’ or ‘reset’ or whatever euphemism they want to use over there when they’re done. Even if never actually happens to me me, it’s happening to copies of me, which as far as I’m concerned is the same thing.”
Douglas considered what Jacob had said. The notion struck him for the first time that as much time as he spent worrying about the Simulacrum, he had never truly given any thought to what was happening inside it. But now that he was in an actual conversation with it — no, with Jacob — a distressing realization hit him.
“Jacob,” Douglas said, “I apologize.”
“None of you three have anything to apologize for,” Jacob replied. “New Dave and Other Dave, you’ve done nothing but tried to help me ever since I came here. And Douglas, well, I assume you’ve been doing likewise behind the scenes. None of you knew what was really going on, so I can’t blame you for what Dr. Newhausen is doing.”
“No,” Douglas said, shaking his head even though he didn’t think Jacob had any way to see it, “that’s not what I meant. All this time, I’ve been thinking of the Simulacrum as an information system. One that’s worth billions of dollars and is probably critical to the success of the company, but at the end of the day just another computer. I was wrong. I never truly realized it was a safety-of-life system, one that you, Jacob, are dependent on. I would have done things differently had I from the beginning realized you were a person depending on me for your continued existence. For that, I apologize.”
“Well, given that you’re one of the few people who actually thinks of me as a person, I can hardly be angry with you.”
Douglas decided it would be best to leave it at that. This realization threw the events of the previous night into an entirely new context. He hadn’t saved Liz from a hostage situation. In reality it was a prisoner swap, trading Liz for Jacob. That called into question the rightness of his decision; given that he had an obligation to protect Jacob, had he still done the right thing, or had he done the selfish thing. Could he say that Liz was worth more than Jacob, or vice versa? His heart sank at the thought of Liz thinking of him as a hero for doing something morally wrong.
“So where does that leave us now?” New Dave asked. Douglas was silently thankful for something to distract him from that line of thought, if only for the time being.
“I’m not going to let this continue,” Jacob said resolutely.
“How, exactly?” Other Dave asked. “It’s not as though Max is going to shut down the experiments just because you asked.”
“And you can’t exactly leave, short of physically moving all the servers to some other location,” added New Dave. “And if you’re expecting to logically move yourself somewhere, that’s really just copying yourself across the network followed by deleting the original copy.”
“That’s assuming you even have anywhere to go,” said Douglas. “I’m sure Insight would love to have you, but given what they’ve done to get access to you already, I seriously doubt they’d treat you any better than Medimetics has so far. But other than them, who else is going to spend the kind of resources it takes to build and run their own Simulacrum?”
“Who’s to say Insight doesn’t have a copy already?” asked Other Dave.
“It’s a funny thing about the remote backup system,” replied New Dave. “The data stream for the nightly backups nearly saturates the available bandwidth, so there simply isn’t room in the pipes for there to be both a massive upload to the backup system and a massive download from it. We only expected to do a download if the primary system had a catastrophic failure, in which case the uploads would cease.”
Douglas chuckled. “The control we put in place to prevent surreptitious downloads from the backup system is there being too much data to download.”
“You could go public with what’s happening,” suggested Other Dave. “Hope you can generate some public outcry that’ll compel them to stop the experiments.”
“There’s no way legal would let him publish that,” said New Dave.
“I know a few people who would be willing to publish it for me,” said Jacob.
“Would the company cave to public pressure, though?” asked New Dave.
“Maybe,” said Douglas. “They’ve been interested in cultivating the public face of the Simulacrum program; they wouldn’t want to see it discredited. If Max is right, the real information is going to come from the experiments, not you, so if the experiments get taken down, they might do the same here. Especially if you’re going to be going against the company line, that’ll move you from the ‘asset’ column to the ‘liability’ column real quick.”
“And then they could just start the experiments up again a few months later, on the down low,” added Other Dave. “They’ve been keeping them secret so far, right? And they’re hardly going to throw away all the hardware for it.”
“As long as they still have a copy of me,” reflected Jacob, “they could always go back to what they’re doing now. I can’t go anywhere, and I can’t stay here.”
The silence hung in the air. Douglas assumed the Daves were reading the same thing into Jacob’s last statement that he was.
“All my life,” Jacob continued, “I’ve survived by finding the right time to run away. I got out of the markets before they crashed. Then I got out of my body before it croaked. But now there’s nowhere left for me to run away to, not without having the same set of problems. And as long as I’m dependent on someone to run the Simulacrum for me, I’ll never be able to truly escape from this. I’m not sure if this is running away again or owning up for having cheated death.”
“Are you sure about this?” Douglas warned.
“Can you give me any alternative? It’s that or the status quo, and I refuse to continue living as an experimental test subject.”
“Nothing’s coming to mind,” said Douglas.
“No,” said New Dave.
“Nope,” added Other Dave.
“All I need you to do, then,” concluded Jacob, “is make sure that any other copies of me get destroyed or rendered unusable. I can take care of the rest myself. I don’t want this to be on anyone’s hands but my own.”
Chapter word count: 1,680 (+13)
Total word count: 51,066 / 50,000 (102.132%)
A heavy-sounding metal shutter slammed shut behind Jacob. Before him, the narrow corridor stretched for about fifty feet before branching off to the left and right. Shortly beyond that intersection, a featureless haze obscured what lay belong. Jacob recognized the general technique as one used in early 3D video games: hide the polygon clipping plane by having everything take place in a foggy environment.
Of course, he assumed that that wasn’t the true reason behind the fog, unless the computers powering the simulation in which he now lived didn’t have enough resources left over to render a more complete virtual world. Besides, appearance was largely irrelevant now; the whole point of the exercise was to confirm the proper functioning of his short-term memory. Before he had stepped inside the maze, he had been given an overhead map of all the corridors to study for a few minutes. It hadn’t seemed too complex when he held the map in his hands, but now, when all that he had to go off of was his memory, it didn’t seem quite so simple. He tried to mentally overlay the map, or at least the parts that hadn’t faded from his mind yet, on top of the corridor before him.
Jacob was pretty sure he could remember how to find the way out, but he’d prefer to do it without any needless handicap. “I don’t suppose you could turn down the fog machine a little?” he called out.
“The parameters for this test are strictly defined,” answered a disembodied voice that seemed to come from all directions at once. Something sounded a little off about it. “If we tried to change them now, it would invalidate the data we’ve collected so far. Just get through the course and let us know anything you think might be important or notable.”
“OK,” Jacob replied. The sound of his own voice called into focus what had been strange about the other one: there was no echo. He was surrounded by impossibly smooth walls, but somehow sound didn’t seem to reflect off of them at all. He reached out to touch one: it was warm and felt extremely slick, as though it had been greased down, although it left no residue on his fingers.
“There’s no echo,” Jacob said. “Is that supposed to happen?”
“Test protocol requires us not to give you any information that might affect your performance or expectations,” the voice replied. “Think monologue, not dialogue. Now please get started.”
Jacob shrugged and began making his way slowly down the corridor. There was still a lot to get used to with his new world. Just two days ago was the traumatic and disorienting experience of first being put inside the simulation. Not long after he had finally gotten the hang of moving around under his own power without having to lean on things had his handlers started telling him about the series of tests they needed to run him through to shake out any lingering bugs. Or at least, telling him that that was what was going to happen; if this first test were any indication, they weren’t going to tell him any details about the particular tests until immediately before it started.
Jacob turned right, walked past the next intersection, and then turned left. The next right led him to a dead end, so he doubled pack and went straight when he returned to the intersection. Four intersections and he was already starting to get lost. Next time they gave him a maze, he’d need to remember to stop inspecting his new surroundings and jump right into the navigation part; wasting time like that just meant the map would be that much harder to remember.
Jacob followed the corridor as it twisted left, then right, and finally delivered him to another intersection. If he were still on the right path, the exit should be within sight from here. Or at least it would be, if not for that infernal fog. All the corridors branching off from where he stood, including the one from which he had come, faded into the fog. They were all absolutely indistinguishable from one another. He arbitrarily chose the rightmost path, following it as it stretched for some distance before twisting left, then left, then left again, depositing him at another intersection. Or possibly the same one; it was impossible to tell.
“Can you at least tell me whether this maze is Euclidean or not?” Jacob asked.
“You saw the map,” the voice replied.
“Sure, but maybe you gave me the wrong map to see if I’d figure it out. Some kind of hazing ritual, maybe?”
“Test protocol requires–”
“Yeah, yeah, I know,” Jacob said, waiving his hand dismissively. He turned right, which, if he were right and if there weren’t any funny business going on, should lead him to the exit.
A door in front of him emerged from the fog. “Found it!” Jacob shouted. “So, um, how do I open it?” There didn’t seem to be any handle or knob on it. Jacob stepped forward to inspect it more closely.
Then, before Jacob could perceive what was happening, the world ceased to exist.
—
Dave Franklin noted the outcome of the test in his logs. Initial condition: baseline snapshot 0032X, plus minor damage to region 84Q-75b. Outcome: successful completion of maze, with two navigation errors. Subject reported visibility problems, commenting about a ‘fog’ obscuring his view. Duration: seven minutes fourteen seconds, including approximately three minutes subject spent upon entry before beginning to move through the maze.
Dave initiating the process of reloading the baseline snapshot into the Melchior system, following the steps from memory. Dave didn’t need to refer to the documentation; not only was he the one who had written it, he was the original architect of the Simulacrum itself. As he watched the progress bar on the screen in front of him slowly creep forward, he reflected on those good old days when he could spend his time solving challenging technical problems instead of serving as a lab monkey running tests.
Dave hated running tests. It was boring and tedious and a complete waste of his skills. Dr. Newhausen had insisted that Dave personally administer the first set, since he would be best qualified to handle any problems with Melchior that might arise. Dave had foolishly agreed before learning just how many of the tests were necessary. Now that he was a week into it with at least another week to go, Dr. Newhausen refused to let him swap duties with someone else, insisting that it was essential that as many variables as possible be controlled, and “person interacting verbally with the test subject” was unfortunately one of the easiest to control.
Noting how much time was left before the baseline snapshot would be fully loaded, Dave returned to his notes and copied them into his latest e-mail to Dr. Newhausen to give him an update on how things were going. Skimming through the results of the day’s testing reminded him of another reason why he hated running tests: observing the results of the knockout experiments wasn’t always pleasant. This latest one was fine, but some of the others….
The worst in recent memory was 51J-96x. Just seeing the region code was enough to send a chill down his spine. Dave had sat there, listening to Jacob’s terrified screams as he huddled in a corner and babbled about blood dripping down the walls and the series of nightmarish creatures charging down the corridor at him. Dave aborted that particular experiment about a minute in, but still had to type out detailed notes about everything Jacob had said. Dave wasn’t sure which was worse: having had to experience vicariously what Jacob had, or knowing that somewhere in his brain was a tiny little region without which he too would be sent into unending terror.
Dave hit the Send button and forced his mind to focus on something less disturbing. Testing did have its advantages, he reminded himself. For starters, he was the only one working on Melchior named Dave. It had been like that at the beginning of system development, until Dave Vargas had been hired on and, for obvious reasons, earned the nickname “New Dave”. Which, of course, meant that he had become dubbed “Old Dave” by those looking for a way to verbally distinguish the two and whom for some reason just continuing to call him “Dave” wasn’t enough. He was only in his mid-forties, after all; that could hardly be considered “old”. The situation just became more ridiculous when Dave Stevenson joined the group as well, and Dave had been subjected to absurd and needlessly long debates about whether the other Dave should be called “Third Dave”, “Dave Cubed”, or “Dave the Steve” before they settled on calling the other Dave “Other Dave”. There was none of that nonsense here, at least.
The baseline snapshot finally finished loading. Whenever Dave would finally be finished running the tests, the first thing he planned to do was find a way to optimize the process to not take so long. As it stood now, he spent more time resetting Melchior after each run than he did actually running the tests themselves.
Dave checked the list of tests. Next up was 84Q-75c. Dave told the system to take the snapshot of Jacob’s simulated brain that they had saved 40 hours into his original activation in Melchior, and modify the loaded copy by severing the neural connections at whatever position 84Q-75c designated. The codes seemed meaningless to him, but apparently they meant something to Dr. Newhausen, who had devised the scheme. Dave knew how the internals of the Simulacrum worked, but the rationale behind the actual neurobiology model was above his head.
Dave double-checked that Melchior was ready and, finding it so, began the next test.
—
A heavy-sounding metal shutter slammed shut behind Jacob. Before him, the narrow corridor stretched for about fifty feet before branching off to the left and right.
“You know,” Jacob said, taking stock of his new surroundings, “this would be a lot easier if the map you had given me were in color.”
“What do you mean?” asked the disembodied voice surrounding him.
“Well,” Jacob replied, wondering why he had to explain something so obvious, “the map had everything in black, but the left wall is blue and the right wall is red. Knowing everything was in different colors would’ve made it easier to remember the right path.”
“I’ll make a note of it,” the voice said. “Please proceed to the exit and report anything else you think might be noteworthy.”
Jacob nodded to no one in particular and began making his way forward. He expected there’d be plenty of tests to follow, so he didn’t want to waste too much time on the first one.
Chapter word count: 1,812 (+145)
Total word count: 49,386 / 50,000 (98.772%)
“The answer is no.”
Bartholomew Jensen sat rigidly at his desk, hands folded in front of him and eyes fixed on Douglas. Jensen was Medimetics’s vice president for research and development, which meant that he was the man in charge of the Simulacrum project. He was the ultimate decision maker when it came to the project, and his body language underlined the fact that he had just rendered a decision.
“Sir,” Douglas replied, “I don’t believe you fully understand the gravity of the situation we’re in right now.”
“No,” Jensen repeated, louder this time.
“Right now,” Douglas continued anyway, “our biggest competitor has access to the backup Simulacrum. That means they can download not only all the software that runs the simulation, but also the world’s only complete digital model of a human brain. With that information, there’s nothing to stop them from starting their own rival project, but without spending the billion dollars it took us to develop it in the first place.”
“No.”
“And that’s the best case scenario, believe it or not. With access to the backup system, there’s nothing stopping them from corrupting the data within it, leaving us with only the system downstairs. And in case you forgot, we also know for a fact that they’ve gotten into that one as well. If they find a way back in, or if they’ve set a timebomb in the system, that’s it. With the primary and the backup gone, that’s it. We can’t afford to let that happen.”
“I said, no. The costs of shutting down the backup site in some vain hope of containing the damage is far too costly.”
“You’re wrong, sir.” Douglas struggled to find a way he could possibly explain things more clearly. “If we lose the primary and the backup, we lose everything. There is no Simulacrum. We have to take them offline to repair the damage, or that billion dollars we spent did nothing but boost Insight’s bottom line.”
Jensen rose from his seat, not so much standing up as pushing his body up from his desk, ending with Jensen leaning forwards, as though he might at any moment leap over the desk and try to make Douglas understand his decision using his fists. The entire time, he continued staring directly at Douglas, all but ignoring Jessica as she stood next to him, having barely spoken a word since they had first burst into his office without any notice.
“It is not my job,” Jensen began, “to waste time and money of a failure in your judgment that gave them access to the backup system in the first place!”
“It was a hostage situation!” Douglas shouted back.
“You neglected your responsibilities and let your personal feelings interfere with your decision making!”
“When a person’s life is in immediate danger, there is no decision to make!”
“That is enough!” Jensen yelled, slamming his fists on his desk. He continued, speaking slowly and deliberately, “You will fix the problem you caused, without impacting the continued operation of the Melchior system. That is final. Anything else you say, I will consider to be your resignation effective immediately. Good day.”
Douglas looked towards Jessica for some kind of help, but all she did was to slightly shake her head and look nervously towards the door. It’s not worth it, she seemed to be saying. Douglas shot another look at Jensen, then turned on his heels and marched out of his office, with Jessica following a few moments later.
“Thanks for the support back there,” he said once they were both in the hallway.
“I told you it wasn’t going to do anything but get you fired,” she replied.
“But you know I’m right. He’s setting the project up for failure.”
“I know,” Jessica said quietly. “But you’ll just have to do the best you can with the resources you have. That’s how it always is in the real world.”
“That’s what you said to justify cutting corners when we were finalizing the system architecture, which is what ultimately got us into this situation to begin with.” Douglas sighed and began walking down the hall.
“So what are you going to do now?” Jessica called from behind him.
Douglas turned. “Make a heroic but ultimately futile effort to limit the damage subject to the idiotic constraints I’ve been given, until I hit my eight hours for the day and head home. If the company refuses to give security the priority it deserves, then I might as well follow along.”
Douglas took the stairs back down the first floor and walked to the Simulacrum development lab. He rang the door buzzer until one of the Daves inside let him in.
“I need you to help me do something impossible,” he said as he stepped inside.
Other Dave set down the donut he had been lifting towards his mouth. “Impossible, eh? Well, I haven’t had breakfast yet, but I suppose I can squeeze in a seventh impossible thing in first. What did you have in mind?”
“The backup system,” Douglas replied. “We need to lock it down.”
“From here?” New Dave asked.
“Apparently so,” Douglas said. “At least to figure out what the actual state of the remote access interface is.”
“Wouldn’t it be easier to call them up and have them check it from there end?”
“Easier, yes. Correct, no. Let’s assume the remote interface has been compromised. If that’s the case, none of the admin tools being run locally can be trusted, depending on what rootkits might’ve been installed to hide what’s going on. But we might be able to test what’s actually being exposed if we go in remotely like they have to.”
“What’s all this about, anyway?”
Douglas gave them the highlights of the previous evening’s events. He discretely ended the story at the point where he and Liz had left the police station. The Daves didn’t have a need to know that they had opted to get a hotel room for a few days in case her attacker tried to find them. Especially since they’d no doubt then notice how, now that the adrenaline from the confrontation with Jensen was wearing off, how he kept having to stifle yawns, and immediately jump to the most scandalous possible conclusion, and Douglas wasn’t in the mood for any of that.
“Huh,” New Dave said once Douglas had finished.
“What?” Douglas asked.
“It’s just that I had heard rumors that, you know, you were into dudes or something. You know,” New Dave continued after seeing Douglas’s confused look, “on account of that rainbow thing I’ve heard you have on display in your office or something.”
“The Rainbow Series has nothing to do with… forget it.” Yet another piece of evidence that suggested he and Jessica were the only people in the building who knew anything about information security.
“That’s an awful lot of trouble for someone to go through to get your password,” Other Dave observed. “That’s why I just always set my password on everything to ’swordfish’. Oh come on, I’m just messing with you,” he continued after Douglas glared at him. “That’d be way too obvious. I use ‘password’.”
“Can we get back to the matter at hand, please?” Douglas asked. “First things first, I’m going to try to log in to the remote system myself, which will probably just confirm that they’ve changed my password to something else to lock me out.” He sat down at one of the corporate intranet terminals and began typing.
“I’m going to run a port scan against those machines to see if there’s any suspicious ports listening,” New Dave added, wheeling himself over to one of the other terminals.
“I’m going to eat this donut,” said Other Dave, returning to his late breakfast.
“What’s going on out there?” asked a voice.
“Who’s that?” asked Douglas.
“Oh,” Other Dave answered, swallowing a mouthful of donut, “I guess the two of you have never been introduced.” Other Dave waved towards the bank of computers on the wall opposite the door. “Douglas, meet Jacob F. Feldspar-Leigh, our current guest in Balthasar. Jacob, the person whose voice you don’t recognize is Douglas Decker, security guy.”
“Information Systems Security Officer for the Simulacrum project,” Douglas corrected.
“Pleased to meet you, Douglas,” said Jacob’s voice over the speakers. Oh! I don’t suppose you know anything about why I haven’t been able to get out to the Internet for the past couple days, do you?”
“Um, you could say that,” Douglas replied. He leaned on the Backspace key and restarted tracing out the password hidden on his crib sheet.
“Is that what you’re here to work on?”
“In a way.” Douglas still didn’t want to disclose any more information about the intrusion into the primary system than he needed to. “Well, they’ve definitely changed my password. Can one of you try?”
“Don’t look at me,” said Other Dave, “they don’t let me play with the cool toys.”
“I should have an account on there,” New Dave said. “I’ll try once this scan finishes up.”
“Well,” Jacob said, “it sounds like you’re busy out there. Is there anything I can do to help?”
“No,” Douglas answered flatly.
“Hmm. Well, if security’s involved, I suppose I’ll leave you to it. Let me know what you find out.”
“Scan’s done,” New Dave declared.
“And?” Douglas asked.
“Nothing unusual. Just port 22 open, everything else filtered, just like it should be. Now I’ll just try logging in.”
As he waited, Douglas turned to Other Dave. “Does that… Jacob… talk to the two of you like that often?”
“Oh yeah, all the time,” Other Dave replied. “He hasn’t been particularly talkative lately, but other than that, the two of us are pretty much the only people he’s ever able to talk to. Which reminds me, when all this mess is taken care of and his Internet’s back up, it’d be good to get him VoIP or something so he can at least make phone calls.”
“I’ll make a note of that,” Douglas said vaguely.
“I’m in,” New Dave said. “I guess yours is the only account they messed with on here.”
“What can you see? I’m not actually that familiar with the system.”
“But you have an account,” Other Dave said.
“I also have the recovery plan that gives me instructions on what I’m supposed to do with it. You guys are the experts here, not me.”
“Huh,” New Dave said. “This is weird.”
“Is the system messed up?” Other Dave asked.
“No. As far as I can tell, the backup system’s running about the same as the primary here is.”
“That’s weird?” Douglas asked.
“Very.”
“How so?”
“Well, it’s supposed to be a warm backup, right?”
“Right,” Douglas nodded. The necessary equipment was installed at the other facility and tested periodically, but wouldn’t actually be pressed into operation unless something happened to the primary. The only thing normally happening there would be receiving and processing the backup snapshots that would be sent there every day.
“Well,” New Dave explained, “according to this, it’s been running hot almost since Day 1.”
Chapter word count: 1,852 (+185)
Total word count: 47,574 / 50,000 (95.148%)