Hurry Up and Wait – TINN TimingPosted: July 31, 2013
Moving right along. First I needed to do basic networking. Starting at the lowest level of socket interaction, advancing up the stack through specific TCP and HTTP uses. Then back down to UDP.
With basic async networking covered, the next thing that comes up is timing. The general socket IO is covered. You can basically build an entire service using nothing more than asynchronous socket calls. But, most servers are more interesting than that. There are situations where you’ll want to cancel out an async operation if it’s taking too long, or you might want to perform some operation over time, repeatedly. So, clearly the TINN system needs some concept of time management.
Here’s the kind of code I would like to write in order to do something every 500 milliseconds:
require ("IOProcessor"); local test_wait = function(interval) while true do wait(interval); print(string.format("interval: %d", IOProcessor.Clock:Milliseconds())); end end run(test_wait)
Basically, there is a new ‘wait()’ function. You give it a number of milliseconds, and it will suspend the coroutine you’re currently in for the given amount of time. This capability comes courtesy of some changes to the base scheduler. The changes are the following:
wait = function(millis) local nextTime = IOProcessor.Clock:Milliseconds() + millis; return IOProcessor:yieldUntilTime(nextTime); end IOProcessor.yieldUntilTime = function(self, atime) if self.CurrentFiber ~= nil then self.CurrentFiber.DueTime = atime; tabutils.binsert(self.FibersAwaitingTime, self.CurrentFiber, compareTaskDueTime); return self:yield(); end return false; end
The yieldUntilTime() function will take the currently running fiber (coroutine) and put it into the list of FibersAwaitingTime. This is simply a table which is maintained in sorted order from lowest to highest. Once a fiber is placed on this list, it is no longer in the list of currently active fibers. it will sit on this list until it’s DueTime has passed.
The main scheduling loop will step through the fibers that are sitting in the AwaitingTime list using the following:
IOProcessor.stepTimeEvents = function(self) local currentTime = self.Clock:Milliseconds(); -- traverse through the fibers that are waiting -- on time local nAwaiting = #self.FibersAwaitingTime; for i=1,nAwaiting do local fiber = self.FibersAwaitingTime; if fiber.DueTime <= currentTime then -- put it back into circulation fiber.DueTime = 0; self:scheduleFiber(fiber); -- Remove the fiber from the list of fibers that are -- waiting on time table.remove(self.FibersAwaitingTime, 1); end end end
Basically, step through the list of fibers that are waiting for their wait time to expire. For all those that qualify, put them back into the list of active fibers by calling the ‘shcheduleFiber()’ function.
This begins to get very interesting I think. Of course once you create a timer, or even async i/o, you probably also want the ability to cancel such operations. But, that’s another matter.
Doing this little exercise, the scheduler begins to take on some more of the attributes of what you might find in the core OS. The big difference is that everything is done in the Lua space, and does not rely on OS primitives, so it is actually somewhat portable to other architectures. That’s a nice idea to have such a nice scheduler available across multiple LuaJIT environments.
While going through this exercise, I also started looking at other features of schedulers, thinking about priorities, and other factors that might go into a really good scheduler. So, at least one thing becomes apparent to me. Having this scheduler readily available and transformable in the Lua space makes it relatively easy to try out different scheduling techniques that will match the particular situation at hand. There is even the possibility of changing the scheduling algorithm dynamically based on attributes of the running system.