Thursday, October 28, 2010

Well, this got way more complicated than I intended

So the situation is that our app is a lot like PowerPoint. Sorry, the Microsoft Office Presentation Platform View Preferred Platform of View, or MOPPVPPV. Something like that. Or maybe that's my most recent architectural pattern, I forget.

Aaaaanyway, there are some things that make maintaining consistency of mental model with MOPPPVPPV interesting:

1) It's like PowerPoint, so you'd expect to see thumbnails of the slides, to help you decide where to go next.
2) The content was originally drawn from a PowerPoint presentation.
3) All the content in the application is added to the slides after creation time - even the original PowerPoint content.
4) Once the slides have been imported you can add more content.
5) Other people can add content.
6) Some of the content is private.
7) What the HELL does a thumbnail LOOK like?

I know, it seems obvious: "The thumbnail looks like what the slide looks like". But it looks different to everybody. "Well then, it looks the way it last looked to me". Sure, but what if you're wrong? I mean, what if it was white the last time you were there but it isn't now because 200 people went and added content to it while you weren't watching? "Well then make it look how it really is". But a lot of the content isn't visible to you, either because you prefer to ignore that person or because it's private to someone else. "Well then make an arbitrary determination about what it looks like and then move on."

Okay, so. Here's what I'd like:
1) It should represent the content of the slide well enough that I can decide to go there.
2) It should not impose a high cost on the client machine.
3) It should not destabilize the architecture or introduce too much code complexity.
4) It should springboard another piece of development, rather than being a dead end.

So. It looks like the collective public view, accurate up to fairly recently, and invalidated when the teacher moves away from the slide or after enough time has passed. Fair enough? The presumption is that the teacher will be the dominant author, and that content creation will slow in the absence of active presenting.

I wanted to build a little server - just enough to be able to fire up an InkCanvas, paint it with the right stuff, take a picture of it and deliver that to the client. That's 1, 2 and 4 definitely addressed. As to code complexity? Heh.

It's good for not destabilizing because even if it crashes horribly the thumbs just stop rendering. But let's talk about that crashing. No, let's talk first about design.

I want it to be very simple. So, since this is Microsoft land (Mono doesn't have the InkCanvas unfortunately) we've got two options:
ASP and IIS, or...
Well, ASP and IIS.

Shit. And the only Windows server we've got is running 2003 and isn't going to get upgraded anytime soon, and is probably running IIS and ASP about 10 years old. I'm lucky that it's even got .Net 3.

So I desperately dig around and it turns out that HttpListener is basically the perfect answer. It has to run as Administrator, unfortunately, which I really do try to avoid doing. But whatever. We'll figure something out there.

So first take on the problem:


public static void Main(string[] _args)
{
ServiceBase.Run(new ServiceBase[]{new ThumbService()});
}
protected override void OnStart(string[] _args){
listener = new HttpListener();
listener.Prefixes.Add("http://*:8080/");
listener.Start();
listener.GetContext(Route, listener);
}


Look good? Guess what it does.

Nothing? Or were you just yawning? Either way, yeah, you're right. Nothing whatsoever. That first GetContext is blocking. I was cool with that, I figured it doesn't have to go that fast on its first go and since we tend to have really high contention around the caching dictionary I would probably end up taking Write locks all the time anyway. But no.

You can't even START this service. Maybe there's a trick, I don't know. The whole Windows Service development and deployment cycle is pretty much like eating a glass rattlesnake at the best of times. So yeah, Windows never works out that the service has started because OnStart never completes, and we never get to see our pretty thumbnails.

So man the fuck up and go asynchronous. Yes, I know you were burnt so severely by Enumerations in multithreaded code that you still don't have full motion in your right arm. Yes, I know that when you write most of your code in Scala and Erlang handling multithreaded code in any other language feels like fucking a corpse on camera. Deal with it.

Orright, so it's not actually a huge difference:


public static void Main(string[] _args)
{
ServiceBase.Run(new ServiceBase[]{new ThumbService()});
}
protected override void OnStart(string[] _args){
listener = new HttpListener();
listener.Prefixes.Add("http://*:8080/");
listener.Start();
listener.BeginGetContext(Route, listener);
}
public void Route(IAsyncResult result) {
HttpListenerContext context = listener.EndGetContext(result);
if (q(context, "invalidate") == "true")
Forget(context);
Thumb(context);
listener.BeginGetContext(Route, listener);
}


Well that seems fine. I was even feeling brave so I actually included the business logic. Not madly complicated, hey. Sometimes we invalidate the cache. Otherwise we thumb, which means either retrieving from the cache or calculating and then caching.

And that one actually starts! But so does the pain. Here are some snippets:

public void Forget(HttpListenerContext context)
{//Guaranteed to crash horribly once more than one person attempts to touch the cache dictionary. Guaranteed to crash horribly once YOU try to use the enumeration of memoKeys and delete its members at the same time.
int slide = Int32.Parse(q(context, "slide"));
var memoKeys = cache.Keys.Where(k => k.slide == slide);
foreach (var key in memoKeys)
cache.Remove(key);
}//Solution to both problems: Add locking. YAY THREADS! At least a ReaderWriterLockSlim is reasonably simple to use. But now our threading code is starting to outweigh the rest of the app.



public void Thumb(HttpListenerContext context){
var requestInfo = new RequestInfo
{
slide = Int32.Parse(q(context, "slide")),
width = Int32.Parse(q(context, "width")),
height = Int32.Parse(q(context, "height")),
server = q(context, "server")
};
byte[] image;
if (cache.ContainsKey(requestInfo))
{//Will crash if someone else is modifying the cache - there's a hidden loop in there. Yay!
image = cache[requestInfo];
}
else
{
image = createImage(requestInfo);
cache[requestInfo] = image;
}
context.Response.ContentType = "image/png";
context.Response.ContentLength64 = image.Count();
context.Response.OutputStream.Write(image, 0, image.Count());
context.Response.OutputStream.Close();
}//Will crash if the client has closed the connection. Or if we've used them all up. Or basically ANYTHING.


So 10 minutes of erlang twiddling at the command line and we've got this little beauty:


-module(bench).
-compile(export_all).
go(HowManyTimes)->
inets:start(),
lists:map(fun(I)->
spawn_link(fun()->
Invalidate = case I rem 10 of
0 -> true;
_ -> false
end,
Uri = io_lib:format("http://localhost:8080/?
slide=101&width=720&height=540&invalidate=~p&server=madam", [Invalidate]),
{ok,{_,[_,_,{_,Length},{_,Type}],_}} = httpc:request(Uri),
end)
end,lists:seq(1,HowManyTimes)).


I know you know, but I sure do love Erlang. Inexcusable syntax and all. So in case you don't read Awful, here's what that code says:

"Get a camera and take a picture of your nice webserver. Because you are NOT GOING TO RECOGNIZE IT AFTER THIS." You will note that I have called my little module "bench". This is because "rapekit" seemed harder to google at work.

So, fling that into action. And yes, the code breaks over and over in the most hilarious and unexpected ways. To cut a long story short (about 1000 words too late, I know), here's the final code for a (moderately) survivable quick (ha!) C# web server:

Oh. My. God.


Damn. That's a lot of plumbing for a quickie web service. Maybe I should have gone ahead with IIS 4.

Or maybe that's just crazy.

No comments:

Post a Comment