Monday, November 28, 2011
Some Lua trickery
In my previous post, I presented this bit of Lua code:
process = require("org.conman.process") -- -------------------------------------------------------------------- -- process limits added because an earlier version of the code actually -- crashed the server it was running on, due to resource exhaustion. -- -------------------------------------------------------------------- process.limits.hard.cpu = "10m" -- 10 minutes process.limits.hard.core = 0 -- no core file process.limits.hard.data = "20m" -- 20 MB
It looks like a simple assignment to set process limits, yet under Unix,
you need to call setrlimit()
. What's happening under the hood
(so to speak) is that it's easy to intercept assignments to tables (Lua
“go-to” data structure) and that's exactly what's going on here. During
the process of registering the module org.conman.process
(more
on the name later) we create some fake structures for the hard limits (and
soft limits, but since it's similar, I'll skip that part) and attach a
metatable, which contains code to intercept both reads and writes so we can
do a bit of magic:
#define SYS_LIMIT_HARD "rlimit_hard" #define SYS_LIMIT_SOFT "rlimit_soft" static const struct luaL_reg mhlimit_reg[] = { { "__index" , mhlimitlua___index } , { "__newindex" , mhlimitlua___newindex } , { NULL , NULL } }; static const struct luaL_reg mslimit_reg[] = { { "__index" , mslimitlua___index } , { "__newindex" , mslimitlua___newindex } , { NULL , NULL } }; int luaopen_org_conman_process(lua_State *const L) { void *udata; assert(L != NULL); luaL_newmetatable(L,SYS_LIMIT_HARD); luaL_register(L,NULL,mhlimit_reg); luaL_newmetatable(L,SYS_LIMIT_SOFT); luaL_register(L,NULL,mslimit_reg); luaL_register(L,"org.conman.process",mprocess_reg); lua_createtable(L,0,2); udata = lua_newuserdata(L,sizeof(int)); luaL_getmetatable(L,SYS_LIMIT_HARD); lua_setmetatable(L,-2); lua_setfield(L,-2,"hard"); udata = lua_newuserdata(L,sizeof(int)); luaL_getmetatable(L,SYS_LIMIT_SOFT); lua_setmetatable(L,-2); lua_setfield(L,-2,"soft"); lua_setfield(L,-2,"limits"); return 1; }
When Lua sees an assignment to the process.limits.hard
table, it calls mhlimit_lua___newindex()
, where the magic
happens:
static int mhlimitlua___newindex(lua_State *const L) { struct rlimit limit; void *ud; const char *tkey; int key; lua_Integer ival; assert(L != NULL); ud = luaL_checkudata(L,1,SYS_LIMIT_HARD); tkey = luaL_checkstring(L,2); if (!mlimit_trans(&key,tkey)) return luaL_error(L,"Illegal limit resource: %s",tkey); if (lua_isnumber(L,3)) ival = lua_tointeger(L,3); else if (lua_isstring(L,3)) { const char *tval; const char *unit; tval = lua_tostring(L,3); ival = strtoul(tval,(char **)&unit,10); if (!mlimit_valid_suffix(&ival,key,unit)) return luaL_error(L,"Illegal suffix: %c",*unit); } else return luaL_error(L,"Non-supported type"); limit.rlim_cur = ival; limit.rlim_max = ival; setrlimit(key,&limit); return 0; }
We basically take the key we're given, say, “cpu”, and translate it to
the appropriate value (which happens in
mlimit_trans()
—nothing terribly interesting, it just maps the
string to the appropriate constant value, in this example,
RLIMIT_CPU
) and the same for the value; if it's a number, we'll
use that and if it's a string, we'll convert it to a value and use any
suffix to modify the value. For our example, “cpu”, it's a meaure of
time, so the suffix “m” means “minutes.”
mlimit_valid_suffix()
handles this and again, it's pretty
straightforward code.
I think it's a pretty cool trick, but I can see why some might not like the idea of masking what amounts to a system call with what looks like a simple assignment, since it does have side effects outside of the simple assignment, but I like the way it looks, and it's a more “natural” or even “Luaish” way of specifying the intent of the code.
Now, on to the name of the module, org.conman.process
. When
I first started playing around with Lua I wrote a few modules that did
similar operations as existing modules, with the same names. One example is
syslog
. There's an existing Lua syslog module, but I don't like
how it works, so I wrote my own.
The problem now becomes, what if I want to use a module that uses the
existing Lua syslog module, but the rest of my code uses mine? If they both
have the same name, some code is going to get a nasty surprise. To work
around that, I decided to put all my modules under a “namespace” I control
and is not likely to cause any conflicts with any existing (or even future)
modules. Thus, the org.conman
namespace.