Coroutines

Take advantage of coroutines for a huge amount of flexibility when it comes to managing Elements' API functionality.

Coroutines are an integral part of cloud functions in Elements. Every invoked function (even in endpoints) is a coroutine, which allows for a tremendous amount of flexibility.

There are two ways to control coroutines in Elements:

-- The native Lua coroutine module

local coroutine = require "coroutine"

-- Helper class for gathering info and controlling 
-- coroutines from outside of the current coroutine

local namazu_coroutine = require "namazu.coroutine"

coroutine

In this example, we can use the native coroutine module to yield the current coroutine with a variety of commands.

Indicates the coroutine should yield and be rescheduled as soon as absolutely possible:

IMMEDIATE

Indicates that the coroutine should yield and be rescheduled until the provided absolute time. The time is expressed in the time since the unix epoch: January 1, 1970. The units may be specified, but the default value is in seconds:

UNTIL_TIME

Indicates that the should parse the second argument as a cron string. The coroutine will resume when at the next time the cron expression will hold true:

UNTIL_NEXT

Indicates that the coroutine should yield for the specified amount of time. The units may be specified, but the default is is in seconds:

FOR

Indicates that the coroutine should yield indefinitely. Typically this means the the calling code will manually resume the coroutine at some point later. Note that under some circumstances, the container may opt to forcibly kill the running Resource:

INDEFINITELY

Indicates that the Resource should yield immediately just to save the state. This will not release the lock on the Resource and will ensure that the contents are written to disk before returning:

COMMIT

Valid time units that can be used for yielding are as follows:

"NANOSECONDS"
"MICROSECONDS"
"MILLISECONDS"
"SECONDS"
"MINUTES"
"HOURS"
"DAYS"

For example:

    print "Yielding immediately"
    reason, elapsed, units = coroutine.yield("IMMEDIATE")
    print(reason, " yielded for ", elapsed, " ", units)

    print "Yielding for one second"
    reason, elapsed, units = coroutine.yield("FOR", 1, "SECONDS")
    print(reason, " yielded for ", elapsed, " ", units)

    time = os.time() + 1
    print("Yielding until ", time, " (in seconds)")
    reason, elapsed, units = coroutine.yield("UNTIL_TIME", time, "SECONDS")
    print(reason, " yielded for ", elapsed, " ", units)

    print("Yielding until cron next cron for \"* * * ? * *\" (once every second)")
    reason, elapsed, units = coroutine.yield("UNTIL_NEXT", "* * * ? * *")
    print(reason, " yielded for ", elapsed, " ", units)

namazu.coroutine

The namazu_coroutine provides three functions:

  • current_task_id()

  • start(coroutine)

  • resume(task_id)

Knowing the current task id is useful primarily for managing the state of the task externally. For example:

local test_coroutine = {}

local coroutine = require "coroutine"
local namazu_coroutine = require "namazu.coroutine"

local task_id = nil

function test_coroutine.block()

    task_id = namazu_coroutine.current_task_id()

    -- Yielding indefinitely will suspend the current task until resume 
    -- is called (see awake below)

    print ("Blocking coroutine " .. tostring(task_id) .. " indefinitely.")
    coroutine.yield("INDEFINITELY")

    print ("Got wake signal.  Exiting.")
    return "OK"

end

function test_coroutine.awake()

    while task_id == nil do
        print("No corresponding task.  Waiting ...")
        coroutine.yield("FOR", 1, "SECONDS")
    end

    print ("Waking up " .. tostring(task_id))
    namazu_coroutine.resume(task_id)

    return "OK"

end

return test_coroutine

Sometimes, you might want to run several tasks asynchronously. To do this, you can create any number of other coroutines. Take this endpoint code for example:

local index = require ("namazu.index")
local resource = require ("namazu.resource")
local auth = require ("namazu.elements.auth")
local namazu_response = require ("namazu.response")

local test_endpoints = {}

function test_endpoint.update_resources(payload, request, session)

    -- In this example, this is an authenticated request, so we can use auth
    -- to get the profile info of the requestor.

    local profile = auth.profile()
    local profile_id = profile.id

    local update_info = payload

    -- This would be the path to all of the resources pertaining to the current
    -- profile id of the user making this request.
    -- Wildcards are useful for searching the entire contents of a directory.

    local resources_path = string.format("resources/%s/*", profile_id)  
    local resource_listings = index.list(path_pattern)
    local active_tasks = 0

    for path, resource_id in pairs(resource_listings) do

        -- Creates a coroutine to update each resource asynchronously

        local co = coroutine.create(function ()

            -- Coroutines accept upvalues, which can be very useful!

            active_tasks = active_tasks + 1

            local result, code = resource.invoke(resource_id, "update_resource", update_info)

            active_tasks = active_tasks - 1

        end)

        namazu_coroutine.start(co)
    end

    while(active_tasks > 0) do
        coroutine.yield("IMMEDIATE")
    end

    -- Just return okay for the purpose of this example

    return namazu_response.formulate(200)
end

return test_endpoints

Last updated