日常辣鸡水文:一个关于 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 も提出されていますが、どうなるかは分かりません。。。
さて、これで終わりにしましょう。明日も仕事があるので、失礼します。