File copas/timer.lua

Copas Timer is a module that adds a timer capability to the Copas scheduler. It provides the same base functions step and loop as Copas (it actually replaces them) except that it will also check for (and run) timers expiring and run background workers if there is no IO or timer to handle. It also adds an exitloop method that allows for a controlled exit from the loop.
 
To use the module, make sure to 'require' copastimer before any other code 'requires' copas. This will make sure that the copas version in use will be updated before any other code uses it. The changes should be transparent to your existing code. It should be required as; local copas = require("copas.timer") because it returns the global copas table and not a separate timer table.
 
There is a difference between the 2 background mechanisms provided; the timers run on the main loop, they should never yield and return quickly, but they are precise. On the other hand the workers run in their own thread (coroutine) and can be yielded if they take too long, but are less precisely timed.
 
The workers are dispatched from a rotating queue, so when a worker is up to run it will be removed from the queue, resumed, and (if not finished) added at the end of the queue again.
Important: the workers should never wait for work to come in. They must exit when done. New work should create a new worker. The reason is that while there are worker threads available the luasocket select statement is called with a 0 timeout (non-blocking) to make sure background work is completed asap. If a worker waits for work (call yield() when it has nothing to do) it will create a busy-wait situation.
 
Copas Timer is free software under the MIT/X11 license.

Copyright ©2011-2012 Thijs Schreijer

Release: Version 0.4.2, Timer module to extend Copas with a timer and worker capability

Functions

copas.addworker (func, args, errhandler) Adds a worker thread.
copas.cancelall () Cancels all currently armed timers.
copas.delayedexecutioner (delay, func, ...) Calls a function delayed, after the specified amount of time.
copas.exitloop (timeout, keeptimers) Instructs Copas to exit the loop.
copas.getworker (t) Returns a background worker
copas.isexiting () Indicator of the loop running or exiting.
copas.loop (timeout, precision) Executes an endless loop handling Copas steps and timers (it replaces the original copas.loop()).
copas.newtimer (f_arm, f_expire, f_cancel, recurring, f_error) Creates a new timer.
copas.removeworker (t) Removes a worker thread
copas.step (timeout, precision) Executes a single Copas step followed by the execution of the first expired (if any) timer in the timers list (it replaces the original copas.step()) if there is no timer that expires then it will try to execute a worker step if available.
copas.waitforcondition (interval, timeout, condition, handler, ...) Executes a handler function after a specific condition has been met (non-blocking wait).
t:arm (self, interval) Arms a previously created timer.
t:cancel (self) Cancels a previously armed timer.


Functions

copas.addworker (func, args, errhandler)
Adds a worker thread. The threads will be executed when there is no IO nor any expiring timer to run. The args key of the returned table can be modified while the thread is still scheduled. After the args have been passed to resume the args will be set to nil. If args is set again, then the next time the thread is up to run, the new set of args will be passed on to the thread. This enables feeding the thread with data.

Parameters

  • func: function to execute in the coroutine
  • args: table with arguments for the function
  • errhandler: function to handle any errors returned

Usage:

copas.addworker(function(...)
        local t = {...}
        print(table.concat(t, " "))
    end, { "Hello", "world" })

Return value:

table with keys thread, args, errhandler

See also:

copas.cancelall ()
Cancels all currently armed timers.

See also:

copas.delayedexecutioner (delay, func, ...)
Calls a function delayed, after the specified amount of time. An example use is a call that requires communications to be running already, but if you start the Copas loop, it basically blocks; classic chicken-egg. In this case use the delayedexecutioner to call the method in 0.5 seconds, just before starting the CopasTimer loop. Now when the method actually executes, communications will be online already. The internals use a timer, so it is executed on the main loop and should not be suspended by calling yield().

Parameters

  • delay: delay in seconds before calling the function
  • func: function to call
  • ...: any arguments to be passed to the function

Usage:

local t = socket.gettime()
copas.delayedexecutioner(5, function(txt)
        print(txt .. " and it was " .. socket.gettime() - t .. " to be precise.")
    end, "This should display in 5 seconds from now.")

See also:

copas.exitloop (timeout, keeptimers)
Instructs Copas to exit the loop. It will wait for any background workers to complete. If the copas.eventer is used then the timeout will only start after the copas.events.loopstopping event has been completely handled.

Parameters

  • timeout: Timeout (in seconds) after which to forcefully exit the loop, abandoning any workerthreads still running.
    • nil or negative: no timeout, continue running until worker queue is empty
    • < 0: exit immediately after next loop iteration, do not wait for workers nor the copas.events.loopstopping/loopstopped events
    • to complete (timers will still be cancelled if set to do so)
  • keeptimers: (boolean) if true then the active timers will NOT be cancelled, otherwise copas.cancelall() will be called to properly cancel all running timers.

See also:

copas.getworker (t)
Returns a background worker

Parameters

  • t: thread (coroutine) to get from the list

Usage:

if copas.getworker(coroutine.running()) then
    print ("I'm running as a background worker")
else
    print ("No background worker found, so I'm on my own")
end

Return value:

the thread table (as earlier returned by addworker()) or nil if it wasn't found

See also:

copas.isexiting ()
Indicator of the loop running or exiting.

Usage:

if copas.isexiting() ~= nil then
    -- loop is currently running, make it exit after the worker queue is empty and cancel any timers
    copas.exitloop(nil, false)
end

Return value:

  • nil: the loop is not running,
  • false: the loop is running, or
  • true: the loop is scheduled to stop

See also:

copas.loop (timeout, precision)
Executes an endless loop handling Copas steps and timers (it replaces the original copas.loop()). The loop can be terminated by calling exitloop.

Parameters

  • timeout: time out (in seconds) to be used. The timer list will be checked at least every timeout period for expired timers. The actual interval will be between 0 and timeout based on the next timers expire time or worker threads being available. If not provided, it defaults to 5 seconds.
  • precision: the precision of the timer (in seconds). Whenever the timer list is checked for expired timers, a timer is considered expired when the exact expire time is in the past or up to precision seconds in the future. It defaults to 0.02 if not provided.

See also:

copas.newtimer (f_arm, f_expire, f_cancel, recurring, f_error)
Creates a new timer. After creating call the arm method of the new timer to actually schedule it. REMARK: the background workers run in their own thread (coroutine) and hence need to yield when their operation takes to long, but the timers run on the main loop, and hence the callbacks should never yield(), in those cases consider adding a worker through copas.addworker() from the timer callback.

Parameters

  • f_arm: callback function to execute when the timer is armed
  • f_expire: callback function to execute when the timer expires
  • f_cancel: callback function to execute when the timer is cancelled
  • recurring: (boolean) should the timer automatically be re-armed with the same interval after it expired
  • f_error: callback function to execute when any of the other callbacks generates an error

Usage:

-- Create a new timer
local t = copas.newtimer(nil, function () print("hello world") end, nil, false, nil)
 
-- Create timer and arm it immediately, to be run in 5 seconds
copas.newtimer(nil, function () print("hello world") end, nil, false, nil):arm(5)
 
-- Create timer and arm it immediately, to be run now (function f is provide twice!) and again every 5 seconds
local f = function () print("hello world") end
copas.newtimer(f, f, nil, true, nil):arm(5)

See also:

copas.removeworker (t)
Removes a worker thread

Parameters

  • t: thread table (as returned by copas.addworker()), or actual thread to be removed.

Return value:

true if success or false if it wasn't found

See also:

copas.step (timeout, precision)
Executes a single Copas step followed by the execution of the first expired (if any) timer in the timers list (it replaces the original copas.step()) if there is no timer that expires then it will try to execute a worker step if available.

Parameters

  • timeout: timeout value (in seconds) to pass to the Copas step handler
  • precision: see parameter precision at function loop().

Return value:

time in seconds until the next timer in the list expires, 0 if there is a worker waiting for execution, or nil if there is no timer nor any worker.

See also:

copas.waitforcondition (interval, timeout, condition, handler, ...)
Executes a handler function after a specific condition has been met (non-blocking wait). This is implemented using a timer, hence both the condition() and handler() functions run on the main thread and should return swiftly and should not yield.

Parameters

  • interval: interval (in seconds) for checking the condition
  • timeout: timeout value (in seconds) after which the operation fails (note that the handler() will still be called)
  • condition: a function that is called repeatedly. It will get the additional parameters specified to waitforcondition(). The function should return true or false depending on whether the condition was met.
  • handler: the handler function that will be executed. It will always be executed. The first argument to the handler will be true if the condition was met, or false if the operation timed-out, any additional parameters provided to waitforcondition() will be passed after that.
  • ...: additional parameters passed on to both the condition() and handler() functions.

Usage:

local count = 1
function check(param)
    print("Check count ", count, ". Called using param = ", param)
    count = count + 1
    return (count == 10)
end
 
function done(conditionmet, param)
    if conditionmet then
        print("The condition was met when count reached ", count - 1,". Called using param = ", param)
    else
        print("Failed, condition was not met. Called using param = ", param)
    end
end
 
copas.waitforcondition(0.1, 5, check, done, "1234567890")

Return value:

timer that verifies the condition.
t:arm (self, interval)
Arms a previously created timer. When arm() is called on an already armed timer then the timer will be rescheduled, the cancel handler will not be called in this case, but the arm handler will run.

Parameters

  • self:
  • interval: the interval after which the timer expires (in seconds). This must be set with the first call to arm() any additional calls will reuse the existing interval if no new interval is provided.

Usage:

-- Create a new timer
local t = copas.newtimer(nil, function () print("hello world") end, nil, false)
t:arm(5) -- arm it at 5 seconds
t:cancel() -- cancel it again

Return value:

the timer t

See also:

t:cancel (self)
Cancels a previously armed timer. This will run the cancel handler provided when creating the timer.

Parameters

  • self:

Usage:

-- Create a new timer
local t = copas.newtimer(nil, function () print("hello world") end, nil, false)
t:arm(5) -- arm it at 5 seconds
t:cancel() -- cancel it again

See also:

Valid XHTML 1.0!