본문 바로가기

Coding, Testing, Challenge

AI 에이전트 예약 작업을 서버가 쥐게 한 이유, 도는 척하는 작업과 유령 lease 정리

 

예약 작업과 유령 lease 정리

"매주 월요일 주간 점검" 같은 반복 일은 머리로 기억하면 꼭 한 번씩 빠집니다.

그래서 기억 서버에 일정을 맡겼는데, 맡기는 과정에서 "도는 척하는 작업" 이라는 함정을 두 번 만났습니다.

이번 포스팅에서는 Claude Code 와 일하는 1인 운영에서 예약 일정을 서버가 쥐게 한 구조, 그리고 알림이 울리자마자 사라지던 문제와 유령 lease 를 정리한 방법을 풀어 보도록 하겠습니다.

서버는 알려 줄 뿐, 대신 일하지 않습니다

처음에 선택지를 셋 놓고 봤습니다.

1. 일정을 서버가 쥐고, 세션을 열면 오늘 할 일로 넣어 준다.

2. 백업 같은 정기 유지 작업은 서버가 직접 실행한다.

3. 사람 없이 서버에서 Claude 를 돌려 일까지 시킨다.

1번과 2번만 골랐습니다. 3번은 처음 설계 때부터 빼 둔 무인 실행이라 이번에도 넣지 않았습니다. 서버는 "오늘 이걸 하세요" 까지만 말하고, 실제 일은 제가 세션을 열어서 합니다.

반복 규칙은 cron 대신 읽히는 말로

예약 일정이 일감과 알림이 되는 길

반복 규칙을 cron 문자열로 두지 않았습니다. 대시보드에서 사람이 읽고 고칠 것이라, 0 9 * * 1 보다 weekly:MON 이 낫다고 봤습니다.

daily                 매일
weekly:MON,THU        매주 월·목
monthly:1             매월 1일   (1~28 만 허용)
every:3               3일마다

매월 규칙은 1~28일만 받습니다. 29~31일은 없는 달이 있어서 그달을 건너뛰게 됩니다. 그래서 monthly:31 은 이유를 말하고 거부합니다.

일정은 두 종류입니다. task 는 만기가 되면 백로그에 일감 한 건을 만들고, remind 는 일감을 만들지 않고 세션 컨텍스트에 알림으로만 보입니다. "기억만 하면 되지 일감은 아니다" 를 표현할 자리가 필요했습니다.

두 번 돌아도 한 번만 생기게

만기 처리는 서버의 별도 프로세스(jobs)가 1분마다 합니다. 프로세스는 다시 뜰 수도 있고, 같은 날을 두 번 훑을 수도 있습니다.

그래서 만든 일감에 출처 표시를 답니다.

source = f"sch:{일정ID}:{만기일}"     # 유니크 —— 같은 날 두 번 돌아도 한 건

출처가 유니크라서 같은 일정의 같은 날 몫은 두 번 생기지 않습니다. 재기동해서 다시 훑어도 안전합니다.

대시보드의 "지금" 버튼은 일감을 직접 만들지 않습니다. 만기일을 오늘로 당길 뿐이고, 만드는 일은 jobs 한 곳에서만 합니다. 화면이 같은 일을 한 벌 더 가지면 규칙이 두 갈래로 갈라지기 때문입니다.

함정 하나: 알림이 울리자마자 사라졌습니다

remind 일정이 만기가 됐는데 세션 컨텍스트에 보이지 않았습니다.

원인은 순서였습니다. jobs 가 만기를 처리하면서 다음 만기일을 바로 뒤로 밉니다. 그런데 컨텍스트를 만드는 쪽은 "다음 만기일이 오늘 이전인 것" 만 찾고 있었습니다.

처리하자마자 다음 만기가 미래로 가 버리니, 오늘 울린 알림을 컨텍스트가 찾을 수 없었던 것입니다.

그래서 컨텍스트 쪽에서 "마지막으로 울린 날이 오늘인 것" 도 함께 보도록 고쳤습니다. 실제로 돌려 보고 나서야 찾은 문제였습니다.

⚠️ 상태를 처리하는 쪽과 그 상태를 보여 주는 쪽이 따로 있으면, 처리하는 순간 보여 줄 근거가 사라질 수 있습니다. "언제 처리됐는지" 를 남겨 두세요.

함정 둘: 아무것도 안 도는데 "실행 중"

유령 lease 를 풀어 주는 순서

일감에는 실행 중(RUNNING) 상태가 있고, 일을 잡은 쪽이 lease(점유 기간)를 겁니다.

그런데 일을 잡은 쪽이 중간에 죽으면, 그 일감은 RUNNING 으로 영영 남습니다. 아무도 돌지 않는데 돌고 있다고 표시되고, 다른 쪽은 그 일을 잡지 못합니다.

그래서 jobs 가 1분마다 유효기간이 지난 lease 를 풀어 줍니다.

UPDATE tasks SET lease_owner=NULL, lease_until=NULL, status='READY'
 WHERE status='RUNNING' AND lease_until IS NOT NULL
   AND lease_until < now() - interval '6 hours'

점유 기간이 끝나고도 6시간이 지난 RUNNING 은 READY 로 되돌리고, 몇 건을 풀었는지 로그에 남깁니다.

제가 가장 쓰게 느낀 대목은 이것입니다. "잡았다" 를 기록하는 장치를 둘 때는, "잡은 쪽이 사라지면" 을 처리하는 장치도 반드시 함께 있어야 한다는 점입니다. 둘 중 하나만 있으면 상태 표시가 거짓말을 하기 시작합니다.

예약 일정을 붙일 때 제가 쓰는 기준

1. 서버는 알려 주는 데까지만 하고, 실제 일은 사람이 세션에서 합니다.

2. 반복 규칙은 사람이 읽을 수 있는 말로 쓰고, 애매한 날짜(29~31일)는 거부합니다.

3. 만기 처리는 두 번 돌아도 한 번만 생기게 출처를 유니크로 둡니다.

4. 만드는 코드는 한 곳에만 둡니다. 화면 버튼은 만기를 당길 뿐입니다.

5. 처리한 뒤에도 "오늘 처리됐다" 를 보여 줄 근거를 남깁니다.

6. 점유(lease)를 거는 곳이 있으면, 점유한 쪽이 사라졌을 때 풀어 주는 일도 함께 만듭니다.

예약 작업에서 무서운 건 실패보다 "도는 척" 이었습니다. 알림이 울렸는데 안 보이고, 아무도 안 도는데 실행 중이라고 나왔습니다.

혹시 운영 중인 작업 목록에 며칠째 "실행 중" 으로 멈춰 있는 항목이 있다면, 그게 정말 돌고 있는지 한번 확인해 보시는 건 어떨까요?

그러면 오늘도 모두 스테이블 하세요.


만든 것들