Develop

CDP로 띄운 창이 화면에 없다 — 예외가 없으면 성공이라는 착각

reveal의 성공 반환 뒤 창 좌표를 확인하고 재시도하도록 바꿨으며, 창을 못 띄워도 앱 결제를 위한 대기를 유지했다

한 줄로

CGV 좌석 감시 데몬이 좌석 선점까지는 해놓고 결제 창을 안 띄우는 문제를 잡았다. 원인은 두 개가 겹쳐 있었는데, 하나는 데몬이 --no-reveal(창을 띄우지 말라는 옵션)로 돌고 있는데 감시 루프는 그걸 무시하고 reveal()을 호출하던 모순이었고, 다른 하나는 reveal()이 CDP 호출에서 예외만 안 나면 무조건 True를 반환하던 것이었다. 후자가 더 나쁘다 — 로그에는 성공이 찍히는데 화면에는 아무것도 없다.

예외가 없다 ≠ 성공했다

브라우저를 코드로 조종할 때는 CDP(Chrome DevTools Protocol, 크롬을 외부에서 원격 조작하는 프로토콜)로 명령을 보낸다. 창을 앞으로 가져와라, 좌표를 옮겨라 같은 것들이다. 이때 함수의 성공 판정을 이렇게 쓰기 쉽다.

CDP 호출에 예외가 없으면 True 반환
예외가 나면 False 반환

위는 당시 성공 판정을 요약한 의사 코드다.

문제는 여기서 True가 증명하는 게 “명령을 보내는 데 실패하지 않았다”뿐이라는 점이다. 실제로 원한 결과 — 창이 사용자 눈에 보이는 위치에 있다 — 는 전혀 확인하지 않았다. CDP는 명령을 받아들이고 정상 응답을 돌려주는데 창은 화면 밖 좌표에 그대로 남아 있을 수 있다. 이러면 코드는 조용히 성공을 보고하고, 사람만 아무것도 못 본다.

이런 함수는 디버깅이 특히 고약하다. 실패한 지점에서 로그가 나오지 않으니 로그를 아무리 뒤져도 단서가 없다. 로그는 “창을 띄웠다”라고 말하고 있고, 그 말은 코드 관점에서는 거짓말도 아니다.

실제로 겪은 것

작업은 cron-job의 좌석 감시 데몬에서 했다.

원인 두 가지를 각각 다르게 손봐야 했다.

첫 번째, --no-reveal과 reveal() 호출의 모순. 실행 인자는 창을 띄우지 말라고 하는데 감시 루프는 띄우려 하고 있었다. 플래그를 제거해서 의도를 한쪽으로 맞췄다.

두 번째, 검증 없는 성공 반환. 창을 옮긴 뒤 좌표를 실제로 확인하도록 바꾸고, 확인에 실패하면 3회 재시도하게 했다.

문제 조치
창이 안 보임 --no-reveal 제거 + 옮긴 뒤 좌표를 확인, 실패 시 3회 재시도
실패해도 조용함 못 옮기면 “앱 예매내역에서 결제하라” 안내 출력
실패 시 즉시 종료 창을 못 띄워도 15분 대기는 유지 (앱으로 결제 가능하므로)

세 번째 줄이 이 표에서 제일 중요한 판단이다. 창을 못 띄웠다고 해서 프로세스를 끝내면 안 된다. 좌석은 이미 선점된 상태고, 사용자는 앱 예매내역에서 결제할 수 있다. 자동화의 한 단계가 실패한 것이지 목적 자체가 실패한 게 아니다. 그래서 실패 경로에서도 15분 대기를 그대로 유지했다.

폴백을 없애는 결정

같이 좌석 선점 정책도 바꿨다. 앞줄 폴백을 제거하고 D열 이상만 선점한다. A열은 아예 잡지 않는다.

폴백(원하는 조건을 못 찾으면 차선으로 내려가는 처리)은 보통 안전장치로 여겨진다. 없애면 성공률이 떨어지니까. 여기서도 그랬다 — 해당 관에서 D열 이상 빈자리가 0석인 경우가 535회 관측됐다. 잡을 기회가 크게 줄어든다는 뜻이고, 이걸 알고 내린 결정이다.

그럼에도 없앤 이유는 폴백이 성공한 뒤에 벌어지는 일 때문이다. A열을 잡으면 선점 기록이 남고 감시가 멈춘다. 쓸모없는 자리를 잡은 대가로 감시 자체가 죽는다. “덜 좋은 결과”가 아니라 “더 나쁜 상태”를 만드는 폴백이라면 안전장치가 아니다.

되돌리려면 MIN_ROW_RELAX=1.

상태에는 만료를 붙인다

선점 기록에 15분 만료를 추가했다. 결제를 못 해도 15분 뒤 감시가 자동으로 재개된다.

이전에는 만료가 없어서 선점 기록이 그대로 남았고, 그 상태로 몇 시간을 통째로 놓쳤다. 앞의 폴백 문제와 같은 구조다 — 어떤 상태에 들어가면 나올 방법이 없는 것. 감시 루프처럼 오래 도는 프로세스가 들고 있는 상태에는 “이 상태를 언제 버리는가”가 반드시 있어야 한다.

검증·커밋

테스트 8스위트를 통과시키고 커밋했다. 93dd20c(cgv-ticket), 3cf42d8(cron-job). 데몬 재시작까지 마쳤다.

남는 것

이번 건에서 반복해서 나온 형태는 하나다. 성공을 선언하는 코드는 원하는 결과를 확인해야지, 실패하지 않았다는 것으로 대신하면 안 된다. reveal()은 예외 없음을 성공으로 바꿔치기했고, 폴백은 자리를 잡았다는 사실을 목적 달성으로 바꿔치기했다. 둘 다 로그상으로는 정상이었다.