본문 바로가기
기록/Python

[Python] 파이썬 timeout 데코레이터 - API 실행 시간 5초 초과됐을 때 pass 처리하는 방법 기록

by 자임 2022. 8. 8.


목표 : 외부 API를 여러개 사용해 각 데이터를 취합해 return 해주는 방식인데, 외부 사이트 사정으로 무한로딩에 걸리는 경우가 있어서 5초 이상 걸리면 그냥 pass 해주기로 했다.


 

 



첫 번째 방법 : signal를 활용한 timeout 데코레이터

검색해보면 가장 먼저 뜨는 게 signal를 활용한 방법이다
참고 : 
https://growd.tistory.com/57
https://daeguowl.tistory.com/139


하지만 signal은 window에서 사용이 불가능하다고 해서 테스트가 불가. Unix 환경에서만 가능한 것 같다.

참고 :
https://stackoverflow.com/questions/52779920/why-is-signal-sigalrm-not-working-in-python-on-windows

윈도우에서, signal()은 SIGABRT, SIGFPE, SIGILL, SIGINT, SIGSEGV, SIGTERM 또는 SIGBREAK로만 호출 할 수 있습니다. 다른 경우에는 ValueError가 발생합니다.

=> 결국 이 방법은 포기







두 번째 방법 : requests.get의 timeout 사용하기

response = requests.get(xmlUrl + queryParams, timeout=5).text
results = xmltodict.parse(response)


=> api 데이터를 불러올 때 5초가 넘어가면 timeout이 돼서 null 값이 들어간다







세 번째 방법 : timeout 데코레이터, raise

requests.get를 사용할 수 없는 api가 있어서 다른 방법을 찾아야했다.


참고 :
https://stackoverflow.com/questions/21827874/timeout-a-function-windows

timeout 데코레이터를 설정해주고, 메서드 실행시간이 5초 지나면 raise를 통해 강제로 에러를 발생시켜 pass 한다.

테스트를 위해서 timeout을 0.0005로 둠

 


코드:

# _*_ coding: utf-8 _*_
from flask import Flask, request, jsonify
from kiprisJson import kipris
from manetJson import manet
from kciJson import kci
from WosLiteJson import wos
from ScienceOnSearchJson import ScienceOn
from threading import Thread
import functools
import time


app = Flask(__name__)
app.config['JSON_AS_ASCII'] = False



def timeout(timeout):
    def deco(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            res = [Exception('function [%s] timeout [%s seconds] exceeded!' % (func.__name__, timeout))]
            # 모든 timeout 데코레이터 사용한 메서드에 res 초기값을 error로 초기화
            def newFunc():
                try:
                    res[0] = func(*args, **kwargs)
                    #res[0] : api 데이터, 메서드를 실행시켜서 값을 저장
                except Exception as e:
                    print("오류발생")
                    res[0] = e
                    print("res[0] except", res[0]) #함수 자체가 실행이 안 되는 오류 처리
            t = Thread(target=newFunc)
            t.daemon = True
            try:
                t.start()
                t.join(timeout)
            except Exception as je:
                print ('error starting thread')
                raise je
            ret = res[0]
            # print("ret", ret)
            print("ret 타입", type(ret))
            if isinstance(ret, BaseException):
                print("오류 발생")
                raise ret
            return ret
        return wrapper
    return deco



@app.route("/search")
def search():
    start_time = time.time()

    data =[]
    title = request.args.get("title", "")
    target = request.args.get("target", "")
    page = request.args.get("page", "")

    try:
        data.append(kipris_call(title))
    except Exception as e:
        print(e)
        pass

    try:
        data.append(manet_call(title))
    except Exception as e:
        print(e)
        pass

    try:
        data.append(kci_call(title, page))
    except Exception as e:
        print(e)
        pass

    try:
        data.append(ScienceOn_call(title, target))
    except Exception as e:
        print(e)
        pass

    try:
        data.append(wos_call(title))
    except Exception as e:
        print(e)
        pass

    end_time = time.time()
    print("WorkingTime: {} sec".format(end_time - start_time))

    return jsonify(data)



@timeout(0.0005)
def kipris_call(title):
    result = kipris.get_all(title)
    return result

@timeout(5)
def manet_call(title):
    result = manet.get_all(title)
    return result

@timeout(5)
def kci_call(title, page):
    result = kci.get_all(title, page)
    return result

@timeout(5)
def ScienceOn_call(title, target):
    result = ScienceOn.get_all(title, target)
    return result

@timeout(0.0005)
def wos_call(title):
    result = wos.get_all(title)
    return result



if __name__ == "__main__":
    app.run(debug=True, host='0.0.0.0', port=5000)

 

 

class kipris():
    def get_all(title):
        xmlUrl = '[url]'
        ServiceKey = unquote('[ServiceKey]')
        queryParams = '?' + urlencode(
            {
                quote_plus('astrtCont'): title,  # 초록
                quote_plus('inventionTitle'): title,  # 발명(고안)의 명칭
                quote_plus('ServiceKey'): ServiceKey,
            }
        )

        response = requests.get(xmlUrl + queryParams).text
        results = xmltodict.parse(response)

        # 응답 오류 체크
        successYN = results['response']['header'].get('successYN')
        print("successYN", successYN)

        if successYN == 'Y' :
            finalResult = dict(index='patent_current', resultData=results)

            return finalResult
        else:
            pass

 




1) 데코레이터 deco 함수

데코레이터란, 함수를 받아 명령을 추가한 뒤 이를 다시 함수의 형태로 반환하는 함수이다.
함수의 내부를 수정하지 않고 기능에 변화를 주고 싶을 때 사용한다. 일반적으로 함수의 전처리나 후처리에 대한 필요가 있을 때 사용한다.

deco 함수는 입력으로 함수(fn)를 전달받고 해당 함수를 호출하는 새로운 함수 객체를 리턴해준다.

출처 : https://wikidocs.net/160127
출처 : https://hckcksrl.medium.com/python-%EB%8D%B0%EC%BD%94%EB%A0%88%EC%9D%B4%ED%84%B0-decorator-980fe8ca5276




2) (*args, **kwargs) : 가변 인자
*args는 임의의 갯수의 positional arguments를 받음을 의미하며, **kwargs는 임의의 갯수의 keyword arguments를 받음을 의미한다. 이때 *args, **kwargs 형태로 가변인자를 받는걸 packing이라고 한다.

positional 형태로 전달되는 인자들은 args라는 tuple에 저장되며, keyword 형태로 전달되는 인자들은 kwargs라는 dict에 저장된다.

keyword는 positional보다 앞에 선언할 수 없기 때문에 (**kwargs, *args) 이런식으로 순서를 바꾼 경우 에러를 발생시킨다.

출처 : https://mingrammer.com/understanding-the-asterisk-of-python/




3) @functools.wraps(func)

데코레이터를 사용하면 디버거와 같이 객체 내부를 조사하는 도구가 이상하게 동작할 수도 있음. 직접 데코레이터를 정의할 때 이런 문제를 피하려면 내장 모듈 functools의 wraps 데코레이터를 사용.
decorator를 이용하면 debugging이 어려워 지는데 @wraps를 사용하면 그 문제를 해결할 수 있다.
wraps는 데코레이터 작성을 돕는 데코레이터. 내가 활용하고 싶은 함수의 표준 attribute를 유지해준다. 데코레이터로 감싸진 함수에 대해서도 원하는 결과를 볼 수 있다.

출처 : 
https://brownbears.tistory.com/239

좋은 참고글 :
https://hongl.tistory.com/250





4) Thread.join(timeout=seconds)

It just tells join how long to wait for the thread to stop. If the thread is still running after the timeout expires, the join call ends, but the thread keeps running.

별도 스레드에서의 동작이 무한루프가 되어야 하므로 해당 스레드는 ‘데몬’으로서 동작해야 한다. 데몬은 그 수명이 메인 스레드에 의존적인 스레드로, 그 자신이 무한루프를 돌고있다 하더라도 메인스레드가 종료되면 (즉 프로세스가 종료되면) 자동으로 종료되면서 리소스를 반납한다. 스레드를 데몬으로 만들고 싶다면 Thread 를 생성할 때 daemon=True 옵션을 지정하면 된다.

Thread를 생성한 후 start()를 호출해야 thread가 실행된다. 만약 실행될 thread 객체가 없다면 RunTimeError가 발생하다.

Thread 종료를 기다리는 join()은 is_alive()를 사용해 thread가 동작중인 경우 사용해야 한다. Thread가 종료된 상태에 Join()을 호출하면 RuntimeError가 발생한다. Join의 timeout인자의 단위는 초(second)이며, timeout=None인 경우 Thread가 종료될 때 까지 기다린다.
join 으로 순서를 제어해준다

출처 : https://burningrizen.tistory.com/244




5) isinstance : 타입, 클래스, 객체 비교 함수
isinstance(확인하고자 하는 데이터 값, 확인하고자 하는 데이터 타입) 두 값을 비교한다.

출처: https://blockdmask.tistory.com/536 




6) BaseException
파이썬3에서는 BaseException 내장 클래스를 상속받는 클래스만이 예외 클래스로 인식된다.
타입이 BaseException 이거냐고 묻는 건, 결국 파이썬에서 동작하는 exception이냐고 체크하는 것.
오류일 때만 (정상적인 데이터가 아닐 때만) 오류를 일으킴 timeout error




7) 코드 흐름과 원리 최종 정리

res[0] = func(*args, **kwargs) 정상적인 데이터가 5초 이내에 저장되면 그 값을 바로 return 해준다. if isinstance(ret, BaseException) 해당 조건에도 걸리지 않음.
5초 이내에 정상적으로 데이터가 들어오지 않으면 Thread가 timeout 되고 정상적인 데이터가 ret에 저장되지 않음. ret에는 처음에 초기화해줫던 exception 값(res = [TimeoutError('function [%s] timeout [%s seconds] exceeded!' % (func.__name__, timeout))])이 들어있기 때문에 if문에서 걸린다.

if isinstance(ret, BaseException):
   raise ret

이렇게 강제로 오류가 발생됨. 오류가 발생하면 pass 되므로 아무런 값도 들어가지 않게 된다.