--- This class contains the ioloop implementation. -- -- In short: allowing you to work with several MQTT clients in one script, and allowing them to maintain -- a long-term connection to broker, using PINGs. This is the bundled alternative to Copas and Nginx. -- -- NOTE: this module will work only with MQTT clients using the `connector.luasocket` connector. -- -- Providing an IO loop instance dealing with efficient (as much as possible in limited lua IO) network communication -- for several MQTT clients in the same OS thread. -- The main idea is that you are creating an ioloop instance, then adding MQTT clients to it. -- Then ioloop is starting an endless loop trying to receive/send data for all added MQTT clients. -- You may add more or remove some MQTT clients to/from the ioloop after it has been created and started. -- -- Using an ioloop is allowing you to run a MQTT client for long time, through sending PINGREQ packets to broker -- in keepAlive interval to maintain long-term connection. -- -- Also, any function can be added to the ioloop instance, and it will be called in the same endless loop over and over -- alongside with added MQTT clients to provide you a piece of processor time to run your own logic (like running your own -- network communications or any other thing good working in an io-loop) -- @classmod Ioloop local _M = {} -- load required stuff local log = require "mqtt.log" local next = next local type = type local ipairs = ipairs local require = require local setmetatable = setmetatable local table = require("table") local tbl_remove = table.remove local math = require("math") local math_min = math.min --- ioloop instances metatable local Ioloop = {} Ioloop.__index = Ioloop --- Initialize ioloop instance. -- @tparam table opts ioloop creation options table -- @tparam[opt=0] number opts.sleep_min min sleep interval after each iteration -- @tparam[opt=0.002] number opts.sleep_step increase in sleep after every idle iteration -- @tparam[opt=0.030] number opts.sleep_max max sleep interval after each iteration -- @tparam[opt=luasocket.sleep] function opts.sleep_function custom sleep function to call after each iteration -- @treturn Ioloop ioloop instance function Ioloop:__init(opts) log:debug("initializing ioloop instance '%s'", tostring(self)) opts = opts or {} opts.sleep_min = opts.sleep_min or 0 opts.sleep_step = opts.sleep_step or 0.002 opts.sleep_max = opts.sleep_max or 0.030 opts.sleep_function = opts.sleep_function or require("socket").sleep self.opts = opts self.clients = {} self.timeouts = setmetatable({}, { __mode = "v" }) self.running = false --ioloop running flag, used by MQTT clients which are adding after this ioloop started to run end --- Add MQTT client or a loop function to the ioloop instance. -- When adding a function, the function should on each call return the time (in seconds) it wishes to sleep. The ioloop -- will sleep after each iteration based on what clients/functions returned. So the function may be called sooner than -- the requested time, but will not be called later. -- @tparam client_mt|function client MQTT client or a loop function to add to ioloop -- @return true on success or false and error message on failure -- @usage -- -- create a timer on a 1 second interval -- local timer do -- local interval = 1 -- local next_call = socket.gettime() + interval -- timer = function() -- if next_call >= socket.gettime() then -- -- -- do stuff here -- -- next_call = socket.gettime() + interval -- return interval -- else -- return next_call - socket.gettime() -- end -- end -- end -- -- loop:add(timer) function Ioloop:add(client) local clients = self.clients if clients[client] then if type(client) == "table" then log:warn("MQTT client '%s' was already added to ioloop '%s'", client.opts.id, tostring(self)) return false, "MQTT client was already added to this ioloop" else log:warn("MQTT loop function '%s' was already added to this ioloop '%s'", tostring(client), tostring(self)) return false, "MQTT loop function was already added to this ioloop" end end clients[#clients + 1] = client clients[client] = true self.timeouts[client] = self.opts.sleep_min if type(client) == "table" then log:info("adding client '%s' to ioloop '%s'", client.opts.id, tostring(self)) -- create and add function for PINGREQ local function f() if not clients[client] then -- the client were supposed to do keepalive for is gone, remove ourselves self:remove(f) end return client:check_keep_alive() end -- add it to start doing keepalive checks self:add(f) else log:info("adding function '%s' to ioloop '%s'", tostring(client), tostring(self)) end return true end --- Remove MQTT client or a loop function from the ioloop instance -- @tparam client_mt|function client MQTT client or a loop function to remove from ioloop -- @return true on success or false and error message on failure function Ioloop:remove(client) local clients = self.clients if not clients[client] then if type(client) == "table" then log:warn("MQTT client not found '%s' in ioloop '%s'", client.opts.id, tostring(self)) return false, "MQTT client not found" else log:warn("MQTT loop function not found '%s' in ioloop '%s'", tostring(client), tostring(self)) return false, "MQTT loop function not found" end end -- search an index of client to remove for i, item in ipairs(clients) do if item == client then -- found it, remove tbl_remove(clients, i) clients[client] = nil break end end if type(client) == "table" then log:info("removed client '%s' from ioloop '%s'", client.opts.id, tostring(self)) else log:info("removed loop function '%s' from ioloop '%s'", tostring(client), tostring(self)) end return true end --- Perform one ioloop iteration. -- TODO: make this smarter do not wake-up functions or clients returning a longer -- sleep delay. Currently they will be tried earlier if another returns a smaller delay. function Ioloop:iteration() local opts = self.opts local sleep = opts.sleep_max for _, client in ipairs(self.clients) do local t, err -- read data and handle events if type(client) ~= "function" then t, err = client:step() else t = client() or opts.sleep_max end if t == -1 then -- no data read, client is idle, step up timeout t = math_min(self.timeouts[client] + opts.sleep_step, opts.sleep_max) self.timeouts[client] = t elseif not t then -- an error from a client was returned if not client.opts.reconnect then -- error and not reconnecting, remove the client log:info("client '%s' returned '%s', no re-connect set, removing client", client.opts.id, err) self:remove(client) t = opts.sleep_max else -- error, but will reconnect log:error("client '%s' failed with '%s', will try re-connecting", client.opts.id, err) t = opts.sleep_min -- try asap end else -- a number of seconds was returned t = math_min(t, opts.sleep_max) self.timeouts[client] = opts.sleep_min end sleep = math_min(sleep, t) end -- sleep a bit if sleep > 0 then opts.sleep_function(sleep) end end --- Run the ioloop. -- While there is at least one client/function in the ioloop it will continue -- iterating. After all clients/functions are gone, it will return. function Ioloop:run_until_clients() log:info("ioloop started with %d clients/functions", #self.clients) self.running = true while next(self.clients) do self:iteration() end self.running = false log:info("ioloop finished with %d clients/functions", #self.clients) end --- Exported functions -- @section exported --- Create IO loop instance with given options -- @name ioloop.create -- @see Ioloop:__init -- @treturn Ioloop ioloop instance function _M.create(opts) local inst = setmetatable({}, Ioloop) inst:__init(opts) return inst end -- Default ioloop instance local ioloop_instance --- Returns default ioloop instance -- @name ioloop.get -- @tparam[opt=true] boolean autocreate Automatically create ioloop instance -- @tparam[opt] table opts Arguments for creating ioloop instance -- @treturn Ioloop ioloop instance function _M.get(autocreate, opts) if autocreate == nil then autocreate = true end if autocreate and not ioloop_instance then log:info("auto-creating default ioloop instance") ioloop_instance = _M.create(opts) end return ioloop_instance end ------- -- export module table return _M -- vim: ts=4 sts=4 sw=4 noet ft=lua