自作競プロジャッジサーバの構成について
簡単な自作競プロジャッジサーバーを作った話をします。(競プロとは?:こちらを参照) デプロイしてから結構経つのに、詳細な構成に関して何も記事を書いていなかったので、ここらで文章に落とそうと思います。 サーバは一時期まで公開していましたが、Dockerで実行環境を隔離しているとはいえ任意のコードが実行できてしまうのは怖いので最近は公開していません。
1. ジャッジサーバの構成に関して
ジャッジサーバ内のコンポーネント間の構成とデータフローを示します。
回答提出から正誤判定までのジャッジサーバ内での処理の流れ
- 回答者からコードが提出されると、一旦Djangoがそれを受け取ります。
- DjangoはこれをRedis+Celeryで実装された非同期処理ジャッジキューに突っ込みます。その後、ユーザに対しジャッジ処理中である旨を示します。
- ジャッジキューは順に提出コードの正誤判定を行ない、判定終了したコードに対し完了フラグを立てます。
- ジャッジ完了後、各コードの正誤判定の詳細をユーザに返します。
2. 難しかったところ
非同期処理の実装
コードを提出してから、ジャッジが完了するまでにある程度時間がかかります。 実行に時間がかかる場合やテストケースが多い場合は、POSTしてからページが表示されるまで何も表示されない状態になってしまいます。 また、実行時間があまりに長い場合はブラウザにリクエストタイムアウトと判定されてしまいま す。 そのため、今回は下記のようなフローで非同期処理を実装することにしました。
バックエンドではジャッジがcelery+redisで実装されたFIFOキューを保持し、提出されたコードに順次正誤判定を行なっています。 ジャッジ未完了の状態ではブラウザ更新を促し、ジャッジ完了の状態では結果を通知します。
TLEの検知
競技プログラミングにおいては、コードの実行時間
が非常に重要です。多くの場合、2秒程度の実行時間内に問題を解けるコードを書く必要があります。
制限時間内に解けなかったコードにはTLE
(Time Limit Exceeded)判定が与えられ、不正解扱いとなります。
最初は簡単なジャッジサーバができれば良い程度に考えていたので、コードの正解・不正解しか判定しないロジックを書いていました。
ところが、段々と完成度を追求したくなってくるもので、やはりTLE
の判定がしたくなってきます。
色々実行時間を計測する術を検討しました。例えば、下記のようにコード実行前後で時刻を計測し、その差分を測るようなやり方です。
start = time.time()
ret = subprocess.run(cmd, shell=True, ...) #提出コードを実行
end = time.time()
timeTaken = end - start # 実行時間を計測
しかし、このやり方ではwhile True: pass
のような無限ループが発生した場合にTLE判定を出せなくなってしまいます。
一定時間以上実行している場合には途中で実行を打ち切るような動作が必要になるわけです。
最終的にはコードを実行しているsubprocess
モジュールで実行時間を計測し、オーバーした場合にTLE
判定を出すようにしました。
subprocess
モジュールには、実行時間を制限するオプションtimeout
があるため、ここに実行制限時間の秒数を渡せば上記の問題に対応しつつTLE
を判定できます。
# subprocessモジュールによるコードの実行
try:
ret = subprocess.run(
cmd, timeout=2, shell=True, stdin=fin, # timeoutに制限時間を指定
stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
except (CalledProcessError, TimeoutExpired) as e:
if TimeoutExpired:
tle += 1 # 実行時間が超過した場合はTLE判定を出す
continue
elif CalledProcessError:
...
3 今後の予定
- フロントエンドをVue.jsで作り直す。
- ajaxによる非同期通信を実装し、ユーザがブラウザ更新をする必要をなくす
- 対応言語を増やす(現在Java, C++, Python3, PyPy3に対応)
- etc....