2021년 10월 3일 일요일

python flask 의 자동 리로드 기능 구현 원리(How to implement flask's automatic reload function)

 

Flask 처음 실행해보고 느낌이 팍 온것은 flask 기능이 아니라 실행중인 auto reload하는 기능이었다. 이 얼마나 아름다운 기능인가!

실행 소스가 아닌 config/parameter 라면 쉽게 reload하는것은 구현이 간단하다. 그렇지만 flask테스트시 그건 단순 paramer reload가 아닌 소스코드 재시작임을 알 수 있었다.

일반적인 언어라면 구현이 까다롭다. 특히 컴파일 언어의 경우 컴파일 하는것도 힘들고 자기자신이 자기자신 process를 죽이고 다시 살아나는건 좀 더 어렵다고 볼 수 있다.

그렇다면 스크립트 언어라면 쉬울까? 딱히 그렇지도 않다. 하지만 python언어라면 python 언어 내부에서 다른 python 스크립트를 실행하는것도 가능하니 불가능할것은 없다고 생각된다.

Flask 소스코드를 탐험해보고 구현원리를 파헤치는 모험이 지금 시작 되고 있다.

소스 코드는  https://github.com/pallets/flask github에 친절히 있다. 실행하는곳을 따라가 보기로 했다.


기본 소스의 위치는 아래 링크에 있다.

https://github.com/pallets/flask/blob/main/src/flask/app.py


여기 class에서 부터 출발 하면 되는데

class Flask(Scaffold): 

당연히 기본함수는 run이 된다. flask실행시 run() 함수를 실행 시키므로

    def run(
        self,
        host: t.Optional[str] = None,
        port: t.Optional[int] = None,
        debug: t.Optional[bool] = None,
        load_dotenv: bool = True,
        **options: t.Any,
    ) -> None:

run함수 내부에 이런부분이 있다. 

        try:
            run_simple(t.cast(str, host), port, self, **options)

run_simple로 실행시키는가본데... 

        from werkzeug.serving import run_simple


이런... Flask가 본체가 아니었던 것이다. 

werkzeug 얘가 본체인것이다.

그럼 werkzeug 이걸 찾아들어가보자.

소스는 역시 github 형님이 가지고 있고, https://github.com/pallets/werkzeug


소스 여기에서 run_simple 본체를 찾을 수 있었다.

https://github.com/pallets/werkzeug/blob/main/src/werkzeug/serving.py


def run_simple(

더 깊은 곳으로

def run_with_reloader(*args: t.Any, **kwargs: t.Any) -> None:
    """Run a process with the reloader. This is not a public API, do
    not use this function.
    .. deprecated:: 2.0
        Will be removed in Werkzeug 2.1.
    """
    from ._reloader import run_with_reloader as _rwr


    warnings.warn(
        (
            "'run_with_reloader' is a private API, it will no longer be"
            " accessible in Werkzeug 2.1. Use 'run_simple' instead."
        ),
        DeprecationWarning,
        stacklevel=2,
    )
    _rwr(*args, **kwargs)


https://github.com/pallets/werkzeug/blob/main/src/werkzeug/_reloader.py


그렇다. ReloaderLoop 클래스를 찾을 수 있었다. 얘기 대마왕인것이다.

class ReloaderLoop:

여기가 그중에서도 핵심이 되겠다.

    def restart_with_reloader(self) -> int:
        """Spawn a new Python interpreter with the same arguments as the
        current one, but running the reloader thread.
        """
        while True:
            _log("info", f" * Restarting with {self.name}")
            args = _get_args_for_reloading()
            new_environ = os.environ.copy()
            new_environ["WERKZEUG_RUN_MAIN"] = "true"
            exit_code = subprocess.call(args, env=new_environ, close_fds=False)

            if exit_code != 3:
                return exit_code

def _get_args_for_reloading() -> t.List[str]: =>실행키켰던 exe 파일이나 인자들을 구해와서 subprocess 로 똑같이 재시작 하고 있던 것이었다.


그리고 아래 부분도 주의해서 봐야할 부분이다. 직접 실행했을때와 reload로 실행했을때 차이를 환경변수 environ["WERKZEUG_RUN_MAIN"] = "true" 에 값을 넣어서 그 조건일때만 다른 처리를 하도록 구현하였다.

def run_with_reloader(
    main_func: t.Callable[[], None],
    extra_files: t.Optional[t.Iterable[str]] = None,
    exclude_patterns: t.Optional[t.Iterable[str]] = None,
    interval: t.Union[int, float] = 1,
    reloader_type: str = "auto",
) -> None:
    """Run the given function in an independent Python interpreter."""
    import signal

    signal.signal(signal.SIGTERM, lambda *args: sys.exit(0))
    reloader = reloader_loops[reloader_type](
        extra_files=extra_files, exclude_patterns=exclude_patterns, interval=interval
    )

    try:
        if os.environ.get("WERKZEUG_RUN_MAIN") == "true":
            ensure_echo_on()
            t = threading.Thread(target=main_func, args=())
            t.daemon = True

            # Enter the reloader to set up initial state, then start
            # the app thread and reloader update loop.
            with reloader:
                t.start()
                reloader.run()
        else:
            sys.exit(reloader.restart_with_reloader())
    except KeyboardInterrupt:
        pass


소스 따라가기는 여기서 마치도록 하고 실제 간단한 샘플로 구현은 나중으로 미루기로 한다.

정리하자면 재시작 하는 방법은 

1. 실행을 어떻게 했는지 정보를 파악하고

2. subprocess.call 로 동일하게 재시작한다.

3. 자신은 종료

4. 필요시 환경 변수를 이용해 처리


댓글 없음:

댓글 쓰기