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 theargs
have been passed toresume
theargs
will be set tonil
. Ifargs
is set again, then the next time the thread is up to run, the new set ofargs
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 keysthread, 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 callingyield()
.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 thecopas.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 thecopas.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, otherwisecopas.cancelall()
will be called to properly cancel all running timers.
See also:
-
timeout: Timeout (in seconds) after which to forcefully exit the loop, abandoning any workerthreads still running.
- 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")
endReturn value:
the thread table (as earlier returned byaddworker()
) ornil
if it wasn't foundSee 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)
endReturn value:
nil
: the loop is not running,false
: the loop is running, ortrue
: 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 callingexitloop
.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 between0
andtimeout
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:
-
timeout: time out (in seconds) to be used. The timer list will be checked at least every
- 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 toyield
when their operation takes to long, but the timers run on the main loop, and hence the callbacks should neveryield()
, in those cases consider adding a worker throughcopas.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 orfalse
if it wasn't foundSee also:
-
t: thread table (as returned by
- 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 functionloop()
.
Return value:
time in seconds until the next timer in the list expires, 0 if there is a worker waiting for execution, ornil
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()
andhandler()
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 returntrue
orfalse
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, orfalse
if the operation timed-out, any additional parameters provided towaitforcondition()
will be passed after that. -
...: additional parameters passed on to both the
condition()
andhandler()
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, thecancel
handler will not be called in this case, but thearm
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 againReturn value:
the timert
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 againSee also: