lua定時器與定時任務的接口設計

在全部的服務器編程當中,定時任務永遠是一個不可或缺的需求。
最直接的需求就是,天天凌晨0點0分的時候老是有一大堆的各類精力重置。
怎麼來設計這個接口呢,想了幾個方案:javascript

  • 每秒觸發
  • 每分鐘觸發
  • 每整點觸發
  • 天天觸發
  • 每月觸發

oh no!不靠譜啊,若是這接口真設計成這樣,得有多爛,靈光一現,unix下的crontab表達式很是完美的解決了這個問題。java

附上crontab表達式的語法說明以下:git

crontab特殊的符號說明:

"*"表明全部的取值範圍內的數字。特別要注意哦!
"/"表明每的意思,如"*/5"表示每5個單位
"-"表明從某個數字到某個數字
","分散的數字github

 

crontab文件的使用示例:

30 21 * * * 表示每晚的21:30 
45 4 1,10,22 * * 表示每個月一、十、22日的4 : 45
10 1 * * 6,0 表示每週6、週日的1 : 10
0,30 18-23 * * * 表示在天天18 : 00至23 : 00之間每隔30分鐘
0 23 * * 6 表示每星期六的11 : 00 pm
* */1 * * * 每一小時
* 23-7/1 * * * 晚上11點到早上7點之間,每隔一小時
* 8,13 * * 1-5 從週一到週五的上午8點和下午1點
0 11 4 * mon-wed 每個月的4號與每週一到週三的11點
0 4 1 jan * 一月一號的4點編程

 

看起來很複雜的樣子,但其實夠用就好,咱們也不須要實現所有特性。服務器

  • 實現一個毫秒級別的定時器Update
  • 根據這個update函數實現一個秒級別定時器
  • 而後每秒取得天然時間與表達式中 分、時、幾號、月份、星期幾 分別匹配就能夠實現了
  • 因爲定時器除了增長之外,可能還須要一個刪除功能,那就再提供一個定時器命名的功能,用於增刪改查定時器是自己
  • 再加個測試函數。。完美

直接上代碼:函數

--------------------------------------------
--任何一個記錄產生一個實例
local Clock = {}
local Clock_mt = {__index = Clock}

local function __checkPositiveInteger(name, value)
	if type(value) ~= "number" or value < 0 then
		error(name .. " must be a positive number")
	end
end

--驗證是否可執行
local function __isCallable(callback)
	local tc = type(callback)
	if tc == 'function' then return true end
	if tc == 'table' then
		local mt = getmetatable(callback)
		return type(mt) == 'table' and type(mt.__call) == 'function'
	end
	return false
end

local function newClock(cid, name, time, callback, update, args)
	assert(time)
	assert(callback)
	assert(__isCallable(callback), "callback must be a function")
	return setmetatable({
		cid		 = cid,
		name	 = name,
		time     = time,
		callback = callback,
		args     = args,
		running  = 0,
		update   = update
	}, Clock_mt)
end

function Clock:reset(running)
	running = running or 0
	__checkPositiveInteger('running', running)

	self.running = running
	self.deleted = nil			--若是已經刪除的,也要復活
end

local function updateEveryClock(self, dt)
	__checkPositiveInteger('dt', dt)
	self.running = self.running + dt

	while self.running >= self.time do
		self.callback(unpack(self.args))
		self.running = self.running - self.time
	end
	return false
end

local function updateAfterClock(self, dt) -- returns true if expired
	__checkPositiveInteger('dt', dt)
	if self.running >= self.time then return true end

	self.running = self.running + dt

	if self.running >= self.time then
		self.callback(unpack(self.args))
		return true
	end
	return false
end

local function match( left, right )
	if left == '*' then return true end

	--單整數的狀況
	if 'number' == type(left) and left == right then
		return true
	end

	--範圍的狀況 形如 1-12/5,算了,先不支持這種每隔幾分鐘的這種特性吧
	_,_,a,b = string.find(left, "(%d+)-(%d+)")
	if a and b then
		return (right >= tonumber(a) and right <= tonumber(b))
	end

	--多選項的狀況 形如 1,2,3,4,5
	--哎,luajit不支持gfind,	
	--for d in string.gfind(left, "%d+") do
	--其實也能夠for i in string.gmatch(left,'(%d+)') do
	local pos = 0
	for st,sp in function() return string.find(left, ',', pos, true) end do
		if tonumber(string.sub(left, pos, st - 1)) == right then
			return true
		end
		pos = sp + 1
	end
	return tonumber(string.sub(left, pos)) == right
end

local function updateCrontab( self, dt )
	local now = os.date('*t')
	local tm = self.time
	--print('updateCrontab/now:',	now.min, now.hour,	now.day,	now.month,	now.wday)
	--print('updateCrontab/tm', tm.mn, tm.hr, tm.day, tm.mon, tm.wkd)
	--print('match:',match(tm.mn, now.min), match(tm.hr, now.hour), match(tm.day, now.day), match(tm.mon, now.month), match(tm.wkd, now.wday))
	if match(tm.mn, now.min) and match(tm.hr, now.hour)
		and match(tm.day, now.day) and match(tm.mon, now.month)
		and match(tm.wkd, now.wday)
	then
		--print('matching',self.name,self.callback,self.running)
		self.callback(unpack(self.args))
		self.running = self.running + 1
	end
	return false
end

--遍歷並執行全部的定時器
local function updateClockTables( tbl )
	for i = #tbl, 1, -1 do
		local v = tbl[i]
		if v.deleted == true or v:update(1) then
			table.remove(tbl,i)
		end
	end
end

----------------------------------------------------------

local crontab = {}
crontab.__index = crontab

function crontab.new( obj )
	local obj = obj or {}
	setmetatable(obj, crontab)
	--執行一下構造函數
	if obj.ctor then
		obj.ctor(obj)
	end
 	return obj
end

function crontab:ctor(  )
	--全部的定時器
	self._clocks = self._clocks or {}
	self._crons = self._crons or {}
	--累積的時間差
	self._diff = self._diff or 0
	--已命名的定時器,設置爲弱引用表
	self._nameObj = {}
	setmetatable(self._nameObj, {__mode="k,v"})

	--取得如今的秒數,延遲到整點分鐘的時候啓動一個定時
	self:after("__delayUpdateCrontab", 60-os.time()%60, function ( )
		--在整點分鐘的時候,每隔一分鐘執行一次
		self:every("__updateCrontab", 60, function ( )					
			updateClockTables(self._crons)
		end)
	end)
end

function crontab:update( diff )
	self._diff = self._diff + diff
	while self._diff >= 1000 do
		--TODO:這裏真讓人糾結,要不要支持累積時間偏差呢?
		self._diff = self._diff - 1000
		--開始對全部的定時器心跳,若是返回true,則從列表中移除
		updateClockTables(self._clocks)
	end
end

function crontab:remove( name )
	if name and self._nameObj[name] then
		self._nameObj[name].deleted = true
	end
end

--經過判斷callback的真正位置,以及參數類型來支持可變參數
--返回值順序 number, string, number, function, args
--總的有以下5種狀況
--1) cid,name,time,callback,args
--2) name,cid,time,callback,args
--3) name,time,callback,args
--4) cid,time,callback,args
--5) time,callback,args
local function changeParamsName( p1, p2, p3, p4, p5 )
	if __isCallable(p4) then
		if type(p1) == 'string' then
			return p2,p1,p3,p4,p5
		else
			return p1,p2,p3,p4,p5
		end
	elseif __isCallable(p3) then
		if type(p1) == 'string' then
			return nil,p1,p2,p3,p4
		else
			return p1,nil,p2,p3,p4
		end
	else
		return nil,nil,p1,p2,p3
	end
end

function crontab:every( cid, name, time, callback, args )
	--支持可變參數
	cid, name, time, callback, args = changeParamsName(cid, name, time, callback,args)
	__checkPositiveInteger('time', time)
	local clock = newClock(cid, name, time, callback, updateEveryClock, args or {})
	table.insert(self._clocks,clock)
	if name and name ~= '' then
		self._nameObj[name] = clock
	end
	return clock
end

function crontab:after( cid, name, time, callback, args )
	cid, name, time, callback, args = changeParamsName(cid, name, time, callback,args)
	__checkPositiveInteger('time', time)
	local clock = newClock(cid, name, time, callback, updateAfterClock, args or {})
	table.insert(self._clocks,clock)
	if name and name ~= '' then
		self._nameObj[name] = clock
	end
	return clock
end

--增長計劃任務,精度到達分鐘級別
--表達式:分鐘[0-59] 小時[0-23] 每個月的幾號[1-31] 月份[1-12] 星期幾[1-7]
--			星期天爲1,
--			"*"表明全部的取值範圍內的數字
--			"-"表明從某個數字到某個數字
--			"/"表明每的意思,如"*/5"表示每5個單位,未實現
--			","分散的數字
--	如:"45 4-23/5 1,10,22 * *"
function crontab:addCron(cid, name, crontab_str, callback, args )
	cid, name, crontab_str, callback, args = changeParamsName(cid, name, crontab_str, callback, args)
	--print(cid, name, crontab_str, callback)
	local t = {}
	for v in string.gmatch(crontab_str,'[%w._/,%-*]+') do
		--若是能夠轉成整型直接轉了,等下直接對比
		local i = tonumber(v)
		table.insert(t, i and i or v)
	end
	if table.getn(t) ~= 5 then
		return error(string.format('crontab string,[%s] error!',crontab_str))
	end

	local time = {mn = t[1], hr = t[2], day = t[3], mon = t[4], wkd = t[5]}
	local clock = newClock(cid, name, time, callback, updateCrontab, args or {})
	table.insert(self._crons,clock)
	if name and name ~= '' then
		self._nameObj[name] = clock
	end
end

return crontab

  

再看看測試代碼:測試

--傳說中的測試代碼
local function RunTests()
	-- the following calls are equivalent:
	local function printMessage(a )
	  print('Hello',a)
	end

	local cron = crontab.new()

	local c1 = cron:after( 5, printMessage)
	local c2 = cron:after( 5, print, {'Hello'})

	c1:update(2) -- will print nothing, the action is not done yet
	c1:update(5) -- will print 'Hello' once

	c1:reset() -- reset the counter to 0

	-- prints 'hey' 5 times and then prints 'hello'
	while not c1:update(1) do
	  print('hey')
	end

	-- Create a periodical clock:
	local c3 = cron:every( 10, printMessage)

	c3:update(5) -- nothing (total time: 5)
	c3:update(4) -- nothing (total time: 9)
	c3:update(12) -- prints 'Hello' twice (total time is now 21)

	-------------------------------------
	c1.deleted = true
	c2.deleted = true
	c3.deleted = true

	------------------------------
	--測試一下match
	print('----------------------------------')
	assert(match('*',14) == true)
	assert(match('12-15',14) == true)
	assert(match('18-21',14) == false)
	assert(match('18,21',14) == false)
	assert(match('18,21,14',14) == true)

	--加一個定時器1分鐘後執行
	cron:update(1000)

	--加入一個定時器每分鐘執行
	cron:addCron('每秒執行', '* * * * *', print, {'.......... cron'})

	cron:update((60-os.time()%60)*1000)
	cron:update(30*1000)
	cron:update(31*1000)
	cron:update(1)
	cron:update(60*1000)		--打印兩次
end

  

也能夠直接到 https://github.com/linbc/crontab.lua  下載代碼ui

參考資料:lua

http://www.cise.ufl.edu/~cop4600/cgi-bin/lxr/http/source.cgi/commands/simple/cron.c

https://github.com/kikito/cron.lua

相關文章
相關標籤/搜索