日常辣鸡水文:一个关于 Sanic 的小问题的思考#
眠れないので、API コピー&ペーストエンジニアとして日常的な辣鸡水文を書いてみる。
正文#
最近、グループ内のコードを Sanic に移行する際に、非常に面白い状況に遭遇した。
まず、標準的な流れは以下のようになるはずだ。
from sanic import Sanic,reponse
app=Sanic(__name__)
def return_value(controller_fun):
"""
パラメータを返すデコレーター
:param controller_fun: コントロール層関数
:return:
"""
async def __decorator(*args, **kwargs):
ret_value = {
"version": server_current_config.version,
"success": 0,
"message": u"失敗したクエリ"
}
ret_data, code = await controller_fun(*args, **kwargs)
if is_blank(ret_data):
ret_value["data"] = {}
else:
ret_value["success"] = 1
ret_value["message"] = u"成功したクエリ"
ret_value["data"] = ret_data
ret_value["update_time"] = convert_time_to_time_str(get_now())
print(ret_value)
return response.json(body=ret_value, status=code)
return __decorator
async def test1():
return {"a":1"}
@return_value
async def test2():
return await test1(),200
@app.route("/wtf")
async def test3():
return await test2()
標準的で、特に問題はない。
しかし、上記のコードが以下のように変わるとどうなるか。
from sanic import Sanic,reponse
app=Sanic(__name__)
async def test1():
return {"a":1"}
@return_value
async def test2():
return await test1()
@app.route("/wtf")
def test3():
return test2()
一般的には、これがエラーを引き起こすと思われる。なぜなら await test2()
がないため、直接 return test2()
すると、返されるのは Coroutine
オブジェクトであり、エラーが発生するはずだ。しかし、実際には正常に動作する。最初は混乱したが、後で Sanic の handle_request
に関する部分を見て、少し面白いことがわかった。
async def handle_request(self, request, write_callback, stream_callback):
"""HTTPサーバーからリクエストを受け取り、返すレスポンスオブジェクトを返す
HTTPサーバーはレスポンスオブジェクトのみを期待しているため、
例外処理はここで行う必要がある。
:param request: HTTPリクエストオブジェクト
:param write_callback: レスポンスを唯一の引数として呼び出す同期レスポンス関数
:param stream_callback: ハンドラーによって生成されたStreamingHTTPResponseを処理するコルーチン。
:return: 何も返さない
"""
try:
# -------------------------------------------- #
# リクエストミドルウェア
# -------------------------------------------- #
request.app = self
response = await self._run_request_middleware(request)
# ミドルウェアの結果がない場合
if not response:
# -------------------------------------------- #
# ハンドラーの実行
# -------------------------------------------- #
# ルーターからハンドラーを取得
handler, args, kwargs, uri = self.router.get(request)
request.uri_template = uri
if handler is None:
raise ServerError(
("ルーターからハンドラーをリクエストした際に 'None' が返されました"))
# レスポンスハンドラーを実行
response = handler(request, *args, **kwargs)
if isawaitable(response):
response = await response
except Exception as e:
# -------------------------------------------- #
# レスポンス生成に失敗
# -------------------------------------------- #
try:
response = self.error_handler.response(request, e)
if isawaitable(response):
response = await response
except Exception as e:
if self.debug:
response = HTTPResponse(
"エラー処理中にエラーが発生しました: {}\nスタック: {}".format(
e, format_exc()))
else:
response = HTTPResponse(
"エラー処理中にエラーが発生しました")
finally:
# -------------------------------------------- #
# レスポンスミドルウェア
# -------------------------------------------- #
try:
response = await self._run_response_middleware(request,
response)
except:
log.exception(
'レスポンスミドルウェアハンドラーの一つで例外が発生しました'
)
# 正しいコールバックにレスポンスを渡す
if isinstance(response, StreamingHTTPResponse):
await stream_callback(response)
else:
write_callback(response)
核心となるコードは以下の部分だ。
handler, args, kwargs, uri = self.router.get(request)
request.uri_template = uri
if handler is None:
raise ServerError(
("ルーターからハンドラーをリクエストした際に 'None' が返されました"))
# レスポンスハンドラーを実行
response = handler(request, *args, **kwargs)
if isawaitable(response):
response = await response
要するに、まず route->add_route
の順に対応する処理関数と URL をマッピングに登録し、リクエストが来たときに対応する handler
を取り出し、さらに処理を行う。
最初の標準的なやり方では、
@app.route("/wtf")
async def test3():
return await test2()
登録された handler
は test3
という関数であり、次に response = handler(request, *args, **kwargs)
を実行し、Coroutine
オブジェクトを初期化する。続いて、このオブジェクトは awaitable
であるため、次の response = await response
の流れに入る。
さて、非主流のやり方を見てみよう。
@app.route("/wtf")
def test3():
return test2()
従来通り、まず登録し、次に test3
という関数を handler
として取り出し、実行する。通常の関数であるため、response
の値は test3
で初期化された Coroutine
オブジェクトとなり、同様に awaitable
であるため、次の response = await response
の流れに入る。
二つの方法は異なる道を辿るが、同じ結果に至る。これが、なぜ二つ目の不正な方法でも正しい結果が得られるのかを説明している。
思考#
Sanic のこの処理方法は、フレームワーク全体の耐障害性を強化している。ユーザーが以前のような不正なコードを書くことを許す可能性もある。しかし、これが良いか悪いかは意見が分かれるだろう。ただ一つ確かなことは、debug
モードでユーザーが app.route
を使って非 async
の関数を追加した場合、警告を出す必要があるということだ。しかし、Sanic にはそのような機能があり、PR が既に提出されているが、どうなるかは分からない。。。
さて、これで終わりにしよう。明日も仕事があるので、失礼します。