이놈의 서버가 자꾸 터진다. 사실 메모리 이슈인줄 알았다. 오늘 열어보니 그렇지 않았다.
3달동안 서버 재실행으로 돌려막기를 했는데 이번에 고쳐야겠다고 맘먹었다.
## sqlalchemy.exc.TimeoutError: QueuePool limit of size 5 overflow 10 reached, connection timed out, timeout 30.00 (Background on this error at: https://sqlalche.me/e/20/3o7r)
원인은 커넥션 풀에서 커넥션을 다 끌어다 쓰고 있으며 대기중인 요쳥이 30초 동안 대기했다가 에러를 내뿜은 것이였다.
해결책으로 커넥션을 늘리면 될까? 근데 뭔가 이상하다.
이 서비스는 분명 사용하는 사람이 적은데 커넥션이 부족하다고 하는 것은 어디선가 커넥션을 끊어야 하는데 끊지 않았다는 의미인 것 같다.
일단 이와 같은 에러가 발생하는 테스트를 작성해야 한다. (그래야 검증할 수 있으니까)
## 잘못된 통합 테스트
전에는 몰랐지만 다시와서 코드를 보니 통합테스트가 이상했다.
원래 통합 테스트는 프로그램의 모든 요소가 잘 짜맞춰져서 동작하는지 테스트 하는 것인데,
GithubApi를 모킹하고 있었다. 이러면 제대로 클라이언트 입장에서 테스트 할 수 없다.
때문에 모킹을 제거하고 시작했다.
```
# 기존에 존재했던 가짜 객체
class GithubAPIFake:
def __init__(self, contributions: dict[int, int]) -> None:
self.contribs = contributions
async def get_user_total_contributions(self, *args, **kwargs) -> dict[int, int]:
return self.contribs
async def get_user_contributions_by_year(self, *args, year: int, **kwargs) -> int:
return self.contribs[year]
```
## 예외 수정
이 프로젝트는 예외가 제대로 제공되지 않고 있었다.
가령 GithubAPIRequestFailedError 라는 예외가 있다. 근데 이 친구는 예외 핸들러에서 처리하기 어려운 타입으로 되어있다.
(응답 상태 코드를 어디선가 지정해주지 않는다.)
그리고 facade 계층에서 try except 문으로 잡아서 다시 다른 예외를 던지는 방법으로 되어있다.
하지만 이건 좀 실수를 유발 할 수 있다는 생각이 들었다.
파이썬의 예외는 자바와 같은 CheckedException이 없기 때문에 실수로 처리하지 않을 수 있다.
그럼 클라이언트는 500 에러를 받게 될것이다.
(근데.. 현재 모든 GithubAPIRequestFailedError를 503 Service Unavailable 로 응답하고 있었다.)
사실은 유저의 요청이 잘못된 것이거나 유저 정보가 없는 것인데 말이다.
때문에 이를 수정해야 했다.
Music in a balloon 프로젝트 처럼 예외는 큰 범위의 것으로 정의하고 (BadRequest 같은)
예외 메시지로 구분했다.
## 한번에 10개의 요청을 보내는 테스트
아래와 같은 테스트를 만들어서 실행했다.
동시에 같은 요청을 여러번 하는 방식이다. 사람이 몰리면 충분히 가능하다. 분명 실패할 줄 알았다.
async def test_get_pokemons__10_requests_at_same_time__responses_ok(client: AsyncClient):
# when
responses = await asyncio.gather(
client.get("/pokemons/2jun0"),
client.get("/pokemons/2jun0"),
client.get("/pokemons/2jun0"),
client.get("/pokemons/2jun0"),
client.get("/pokemons/2jun0"),
client.get("/pokemons/2jun0"),
client.get("/pokemons/2jun0"),
client.get("/pokemons/2jun0"),
client.get("/pokemons/2jun0"),
client.get("/pokemons/2jun0"),
)
# then
for response in responses:
assert response.status_code == status.HTTP_200_OK
예상대로 실패했지만 예상 외로 다른 부분에서 오류를 잡아낼 수 있었다;;
sqlalchemy.exc.IntegrityError: (sqlite3.IntegrityError) UNIQUE constraint failed: user.username
오 이럴수가! 이런 경우에는 어떻게 처리해야 하는거지? 싶었지만 아마도 이런 상황은 별로 없을 것 같다.
보통은 처음 자신의 유저를 등록할때 동시에 같은 요청을 보내는 경우는 없기 때문이다.
유저 생성 부분에서 생긴 오류이므로 유저를 미리 생성한 뒤 검증할 필요가 있었다.
그래서 이렇게 바꿨다.
async def test_get_pokemons__10_requests_at_same_time__responses_ok(client: AsyncClient, session: AsyncSession):
# given
username = "2jun0"
user = User(username=username)
user.set_commit_points(
{
2021: 50,
2022: 200,
2023: 0,
2024: 100,
}
)
session.add(user)
await session.commit()
# when
responses = await asyncio.gather(
client.get(f"/pokemons/{username}"),
client.get(f"/pokemons/{username}"),
client.get(f"/pokemons/{username}"),
client.get(f"/pokemons/{username}"),
client.get(f"/pokemons/{username}"),
client.get(f"/pokemons/{username}"),
client.get(f"/pokemons/{username}"),
client.get(f"/pokemons/{username}"),
client.get(f"/pokemons/{username}"),
client.get(f"/pokemons/{username}"),
)
# then
for response in responses:
assert response.status_code == status.HTTP_200_OK
통과했다…
## 커넥션 사용 범위 좁히기
원인을 잘 모르겠으니 아마도 db 커넥션 관리를 제대로 하지 않아서 그런 것 같다고 추측했다.
그래서 UserService단에서 SQLAchlemy의 AsyncSession은 close라는 메서드를 사용해 커넥션을 관리했다.
이름에서 오해를 살 수 있는데, 이건 영원히 커넥션을 잃어버리는 메서드가 아니다.
모든 orm model을 expired 하고 커넥션을 release 하는 것 뿐이다.
이러고 나면 아마 좀 괜찮을 것이다.
## 번외 - 아니 뭘 뒤지는 거예요?
위 문제를 해결하면서 로그를 열심히 보다보니 아래와 같은 로그들을 볼 수 있었다.
.env를 열심히 찾아보는 걸 볼 수 있는데 조심해야겠다.
INFO: 172.71.242.10:64314 - "GET / HTTP/1.1" 404 Not Found
INFO: 172.71.183.137:59050 - "GET / HTTP/1.1" 404 Not Found
INFO: 162.158.158.54:28562 - "GET /.env HTTP/1.1" 404 Not Found
INFO: 162.158.159.185:52014 - "GET /.env.www HTTP/1.1" 404 Not Found
INFO: 162.158.158.10:34242 - "GET /.env.save HTTP/1.1" 404 Not Found
INFO: 162.158.62.145:25220 - "GET /.env.bak HTTP/1.1" 404 Not Found
INFO: 162.158.155.201:52454 - "GET /.env.dev HTTP/1.1" 404 Not Found
INFO: 172.70.114.204:62340 - "GET /.env_1 HTTP/1.1" 404 Not Found
INFO: 162.158.158.84:17660 - "GET /.env.live HTTP/1.1" 404 Not Found
INFO: 162.158.159.72:39986 - "GET /.env.development.local HTTP/1.1" 404 Not Found
INFO: 172.70.114.123:26052 - "GET /.env.prod HTTP/1.1" 404 Not Found
INFO: 162.158.155.12:30864 - "GET /.env.dev.local HTTP/1.1" 404 Not Found
INFO: 162.158.155.202:48712 - "GET /.env_sample HTTP/1.1" 404 Not Found
INFO: 162.158.158.55:22750 - "GET /.env.prod.local HTTP/1.1" 404 Not Found
INFO: 172.70.115.152:37052 - "GET /.env.production.local HTTP/1.1" 404 Not Found
INFO: 162.158.158.243:32082 - "GET /env.test.js HTTP/1.1" 404 Not Found
INFO: 172.70.110.33:14472 - "GET /.env.local HTTP/1.1" 404 Not Found
INFO: 162.158.155.166:46230 - "GET /.env.production HTTP/1.1" 404 Not Found
INFO: 162.158.63.101:39614 - "GET /env.development.js HTTP/1.1" 404 Not Found
INFO: 162.158.155.85:65212 - "GET /env.js HTTP/1.1" 404 Not Found
INFO: 162.158.155.186:51708 - "GET /www/.env HTTP/1.1" 404 Not Found
INFO: 172.70.230.140:42558 - "GET /api/.env HTTP/1.1" 404 Not Found
INFO: 172.70.110.20:64930 - "GET /html/.env HTTP/1.1" 404 Not Found
INFO: 172.70.110.221:26168 - "GET /.env.backup HTTP/1.1" 404 Not Found
INFO: 172.70.231.87:21252 - "GET /.env.stage HTTP/1.1" 404 Not Found
INFO: 172.70.231.26:51210 - "GET /.env.old HTTP/1.1" 404 Not Found
INFO: 172.70.115.89:51746 - "GET /env.production.js HTTP/1.1" 404 Not Found
INFO: 162.158.159.66:46234 - "GET /var/.env HTTP/1.1" 404 Not Found
INFO: 162.158.63.27:63446 - "GET /env.dev.js HTTP/1.1" 404 Not Found
INFO: 172.70.111.103:64918 - "GET /env.prod.js HTTP/1.1" 404 Not Found
INFO: 172.69.194.50:47260 - "GET / HTTP/1.1" 404 Not Found
INFO: 172.70.131.215:35922 - "GET /robots.txt HTTP/1.1" 404 Not Found
'프로그래밍 > Github Pokemon Collection' 카테고리의 다른 글
# 6 유저이름에 대한 문제 (0) | 2024.07.30 |
---|---|
# 5 봇들아 가라~ 재미없다~ (0) | 2024.07.26 |
# 4 이제는 배경을 넣을때 (1) | 2024.07.13 |
# 2 업데이트 했음 (0) | 2024.06.01 |
# 1 깃허브에서 포켓몬을 수집하자! (0) | 2024.04.27 |