wafflestudio/seminar-2020

test_user.py에서 TransactionManagementError발생

Opened this issue · 4 comments

제공해주신 test_user.py를 돌리는 중에 다음과 같은 에러가 계속 발생합니다.

django.db.transaction.TransactionManagementError: 
An error occurred in the current transaction. You can't execute queries until the end of the 'atomic' block.

에러는 아래 test_user.py 187번 line에서 발생함을 확인할 수 있었습니다.
image

즉 에러 메세지와 코드를 종합해보면, 187번 line에서 Token.objects.get이라는 쿼리가 날라가는데 이 쿼리가 'atomic' block 안에서 날라가기 때문에 이 에러가 발생하는 것으로 추측할 수 있습니다.

구글링해보니 다수의 사람들이 아래와 같이 transaction.atomic()을 이용해 atomic block을 함수 내부에서 나눠 이 에러를 해결하는 것을 확인할 수 있었습니다. (https://stackoverflow.com/questions/21458387/transactionmanagementerror-you-cant-execute-queries-until-the-end-of-the-atom)

from django.db import transaction
def test_constraint(self):
    try:
        # Duplicates should be prevented.
        with transaction.atomic():
            models.Question.objects.create(domain=self.domain, slug='barks')
        self.fail('Duplicate question allowed.')
    except IntegrityError:
        pass

그래서 저도 test_user.py에서 Token.objects.get에 transaction 하였으나 에러가 해결되지 않고 그대로입니다.

제가 궁금한 점은 어떤 기준으로 장고에서 atomic block이 정해지는지, 그리고 이 에러를 어떻게 해결할 수 있을지입니다.
제 코드와 test_user.py를 확인한 결과 데코레이터 등으로 transaction을 정의하지는 않은 것 같습니다.
구글링을 해봐도 transaction을 어떻게 지정하는지만 나오고 자동으로 transaction이 되는지 여부는 찾을 수 없었습니다.

이에 대해 알고 있는 분이 계시다면 알려주시면 감사하겠습니다.

참고로 저는 윈도우에서 아나콘다를 활용하고 있으며 장고 버전은 requirements.txt에 나와있는 그대로입니다.

asgiref==3.2.10
Django==3.1
django-debug-toolbar==2.2
djangorestframework==3.11.1
mysqlclient==2.0.1
pytz==2020.1
sqlparse==0.3.1

참고가 될까 싶어 traceback도 첨부합니다.

Traceback (most recent call last):
  File "C:\Users\SEOYOON_MOON\Desktop\Waffle\waffle-rookies-18.5-backend-2\waffle_backend\user\tests.py", line 189, in setUp
    self.instructor_token = 'Token ' + Token.objects.get(user__username='inst').key
  File "C:\Users\SEOYOON_MOON\anaconda3\envs\be383\lib\site-packages\django\db\models\manager.py", line 85, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "C:\Users\SEOYOON_MOON\anaconda3\envs\be383\lib\site-packages\django\db\models\query.py", line 425, in get
    num = len(clone)
  File "C:\Users\SEOYOON_MOON\anaconda3\envs\be383\lib\site-packages\django\db\models\query.py", line 269, in __len__
    self._fetch_all()
  File "C:\Users\SEOYOON_MOON\anaconda3\envs\be383\lib\site-packages\django\db\models\query.py", line 1303, in _fetch_all
    self._result_cache = list(self._iterable_class(self))
  File "C:\Users\SEOYOON_MOON\anaconda3\envs\be383\lib\site-packages\django\db\models\query.py", line 53, in __iter__
    results = compiler.execute_sql(chunked_fetch=self.chunked_fetch, chunk_size=self.chunk_size)
  File "C:\Users\SEOYOON_MOON\anaconda3\envs\be383\lib\site-packages\django\db\models\sql\compiler.py", line 1154, in execute_sql
    cursor.execute(sql, params)
  File "C:\Users\SEOYOON_MOON\anaconda3\envs\be383\lib\site-packages\django\db\backends\utils.py", line 66, in execute
    return self._execute_with_wrappers(sql, params, many=False, executor=self._execute)
  File "C:\Users\SEOYOON_MOON\anaconda3\envs\be383\lib\site-packages\django\db\backends\utils.py", line 75, in _execute_with_wrappers
    return executor(sql, params, many, context)
  File "C:\Users\SEOYOON_MOON\anaconda3\envs\be383\lib\site-packages\django\db\backends\utils.py", line 78, in _execute
    self.db.validate_no_broken_transaction()
  File "C:\Users\SEOYOON_MOON\anaconda3\envs\be383\lib\site-packages\django\db\backends\base\base.py", line 447, in validate_no_broken_transaction    raise TransactionManagementError(
django.db.transaction.TransactionManagementError: An error occurred in the current transaction. You can't execute queries until the end of the 'atomic' block.

@symoon9 이런 경우에는 어쨌든 test 코드가 실행시키는 본인의 코드와 합쳐져 의도치 않은 동작을 만들어내는 것이므로, 최대한 관련되어 있을 것으로 보이는 자신의 코드 내용을 제공해주면 좋습니다. 저나 다른 분들의 경우에는 발생하지 않는 것 같으니까요.

명시적으로 transaction.atomic을 decorator(@transaction.atomic)으로든 Python context(with transaction.atomic())로든 사용하지 않았다면, Django에서 transaction 단위는 일반적으로 ORM query 단위(그냥 get, create 같은 거 하나하나)가 됩니다. 그런데 TestCase 에 한해서는 성능상의 이유로 이러한 동작이 조금 다르게, test 단위로 transaction이 묶이는 것으로 알고 있습니다. 이 모든 내용이 Django transaction 문서의 https://docs.djangoproject.com/en/3.1/topics/db/transactions/#django-s-default-transaction-behavior 에 있습니다.

이런 에러는 주로 https://lee-seul.github.io/django/2019/02/02/django-transactionmanagementerror.html 글에서 언급되는 것처럼 하나의 transaction 단위 내에서 try-except 문을 사용할 때 등의 상황에 발생하는데(공식 문서에도 https://docs.djangoproject.com/en/3.1/topics/db/transactions/#django.db.transaction.atomic 내부에 그런 언급이 있습니다.) 현재의 상황만으로는 저는 재현이 안 되어서 파악에 조금 어려움이 있네요. 어쨌든 TestCase의 transaction.atomic이 만들어지는 상황과 맞물려, 187 line 근처에서 test 코드가 하고 싶어하는 일이 혹시 본인 코드로 정상적인 실행이 안 되는 것이 아닌지 한 번 시험에 보는 것도 좋을 것 같습니다. 예를 들면 의도치 않게 IntegrityError가 저 테스트 코드의 상황에서 발생하여, DB가 rollback하고 싶어하는 상황이 만들어진다거나.(atomic block 내에서 DB rollback이 일어나려는데 try-except 문이 사용되고 있으면 이런 식의 상황이 발생할 수 있습니다. 그리고 POST /api/v1/user/ 내에서 사용되는 except IntegrityError 등이 실행되는 상황이 의심스럽습니다.) https://docs.djangoproject.com/en/3.1/topics/db/transactions/#use-in-tests 을 참고해 TransactionTestCase 로 바꿔보는 것도 하나의 방법일 것 같습니다. 어쩌면 MySQL 설정이 뭔가 다른 걸까? 싶기도 하네요.

좀 더 구체적으로 TestCase의 transaction에 대해 말하자면, https://docs.djangoproject.com/en/3.1/topics/testing/tools/#testcase 에도 있듯 하나는 테스트 메소드마다, 하나는 TestCase class 자체를 두 개의 transaction.atomic block이 감싸게 됩니다.

@symoon9 혹시 해결되셨다면 상황 공유해주시면 좋을 것 같아요~

@davin111 자세한 답변 정말 감사합니다!!

말씀대로 user을 create하는 중에 에러가 발생하여 문제가 발생하는 것이었습니다.
제 코드 중 UserSerializer에서 create()할 때 ProfileSerializer에 user 정보를 넘겨주려고 initial_data를 임의로 변경하는 부분이 있었습니다(profile object를 ProfileSerializer에서 생성하려고 이렇게 데이터를 넘겼습니다). 그런데 여기서 immutable한 data를 변경하려고해 에러가 발생했습니다. 이 에러로 인해 아래 try-except에서 roll-back이 일어난 것 같습니다.

def create(self, request):
    ...
    try:
        user = serializer.save()
    except IntegrityError:
    ...

그래서 initial_data를 이용하는 부분을 완전히 삭제하고 profile object를 UserSerializer에서 생성하는 방식으로 변경했습니다.

다만 과제2를 제출하기 전 test를 했을 때는 이 부분이 문제가 되지 않았는데 왜 그때는 회원가입이 정상적으로 되었는지 조금 의문입니다😂