Manjusaka

Manjusaka

日常辣鸡水文: Sanic に関する小さな問題についての考察

日常辣鸡水文:一个关于 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()

登録された handlertest3 という関数であり、次に 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 が既に提出されているが、どうなるかは分からない。。。

さて、これで終わりにしよう。明日も仕事があるので、失礼します。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。