Luaをタイムアウト付きで実行する

環境

  • Lua 5.1.4
  • LuaJIT 2.0.0-beta9

動機

MySQLのUDFの中でLuaを実行したい。
このとき、無限ループするようなコードを実行すると、killクエリを投げても止まらないスレッドになってしまい、mysqldをkill -9するしかなくなる。
そのため、無限ループしてても時間がたてば強制終了されるように、タイムアウトを設定したい。

実装

Luaのフックを使えば、タイムアウトを実現できる。

startclock = os.clock()
timeout = startclock + 3
debug.sethook(function ()
  if os.clock() >= timeout then 
    error('timeout')
  end
end, '', 100000)
while true do end

debug.sethookの第2引数に'c', 'r', 'l'以外を渡して、第3引数を渡すと、カウントフックを設定できる。
第3引数に渡した値がカウント値として設定される。
カウント値はLuaが命令を実行するたびにデクリメントされ、0になったときにカウントフックが呼ばれる。
一度カウントフックが呼ばれると、カウント値は第3引数の値にリセットされ、再度0になるまでデクリメントされる。

リファレンスマニュアルに書いてある、

count がゼロでなければ、フックは count 命令が実行されるたびに、その直後に呼ばれる。
With a count different from zero, the hook is called after every count instructions.

この説明は間違っているので注意。

パフォーマンス

カウント値が0でないときは、命令実行のたびにデクリメントが走るだけなので、それほど重くない。
debug.sethook関数に渡す第3引数を十分大きな値にしておけば、数%程度の速度低下ですむはず。(厳密には測ってないけど。)
MySQLのHandler APIを使ってタイムラインを取得するサンプルコードでは、1%~2%程度の速度低下で済んだ。
ただし、カウント値を大きくすればするほど、タイムアウト時刻の精度が落ちるので、注意。

制限

LuaJITの場合、タイムアウトされないことがあった。
JITコンパイルされたコードの中ではカウントフックが呼ばれないのかも。

最後に

実際に動くコードはここにあります。
https://github.com/atsumu/mylua