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 も提出されていますが、どうなるかは分かりません。。。

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

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