DBMS_SCHEDULER 잡이 실패했는데 로그가 비어 있을 때

DBMS_SCHEDULER로 등록한 잡이 실패했는데, 애플리케이션 로그에도 아무것도 없고 프로시저 안에 넣어 둔 로그 테이블도 비어 있는 경우가 있다. 잡은 분명 실패 상태인데 왜 실패했는지가 어디에도 없는 상황이다.

이때 봐야 할 곳은 정해져 있다. 스케줄러는 자기 이력을 딕셔너리 뷰에 남기기 때문에, 애플리케이션 로그가 비어 있어도 실패 원인은 대부분 거기 남아 있다.

먼저 볼 뷰

실행 이력의 출발점은 이 뷰다.

select job_name, log_date, status, error#, run_duration, actual_start_date
from user_scheduler_job_run_details
where job_name = 'JOB_DAILY_STAT'
order by log_date desc
fetch first 20 rows only;

status가 FAILED면 error# 컬럼에 오라클 에러 번호가 들어 있다. 그리고 이 뷰에는 additional_info 컬럼이 있는데, 여기에 에러 메시지 전문이 담기는 경우가 많다. 화면이 좁아 잘려 보이기 쉬우니 따로 뽑아 본다.

select log_date, error#, additional_info
from user_scheduler_job_run_details
where job_name = 'JOB_DAILY_STAT'
and status = 'FAILED'
order by log_date desc
fetch first 5 rows only;

대부분의 경우 여기서 끝난다. ORA 번호와 메시지가 나오면 그다음은 일반적인 오류 추적이다.

실행 이력 자체가 없을 때

더 난감한 경우는 job_run_details에 아무 행도 없는 상황이다. 이건 잡이 실패한 게 아니라 애초에 시작되지 않았다는 뜻이다. 확인할 것이 몇 가지 있다.

select job_name, enabled, state, failure_count, run_count,
last_start_date, next_run_date
from user_scheduler_jobs
where job_name = 'JOB_DAILY_STAT';
  • enabled가 FALSE — 누군가 비활성화했거나, 연속 실패로 자동 비활성화됐다. 뒤에서 다시 다룬다.
  • state가 BROKEN — 잡 정의 자체에 문제가 있다.
  • next_run_date가 NULL — 반복 일정이 끝났거나 end_date를 지났다.
  • next_run_date가 과거 — 스케줄러 자체가 안 도는 상태일 수 있다.

일정 표현식이 의도대로 해석됐는지는 직접 계산해 볼 수 있다. 눈으로 읽어서 맞다고 생각한 것과 실제 해석이 다른 경우가 종종 있다.

declare
v_next timestamp;
begin
dbms_scheduler.evaluate_calendar_string(
'FREQ=DAILY; BYHOUR=2; BYMINUTE=30',
systimestamp, null, v_next);
dbms_output.put_line(v_next);
end;
/

연속 실패로 잡이 꺼진 경우

DBMS_SCHEDULER에는 max_failures 속성이 있다. 지정된 횟수만큼 연속 실패하면 스케줄러가 잡을 비활성화한다. 아무도 끄지 않았는데 enabled가 FALSE인 경우 대개 이것이다.

select attribute_name, value
from user_scheduler_job_attributes
where job_name = 'JOB_DAILY_STAT'
and attribute_name in ('MAX_FAILURES','MAX_RUN_DURATION','RESTARTABLE','AUTO_DROP');

여기서 함께 확인할 것이 auto_drop이다. 일회성 잡은 기본적으로 실행 후 자동 삭제되기 때문에, 잡이 통째로 사라져서 조회 자체가 안 되는 경우가 있다. 이력만 남고 잡은 없는 상태다.

중간에 멈춰 있는 경우

실패도 성공도 아니고 계속 RUNNING인 잡도 있다. 실행 중인 잡은 별도 뷰에서 본다.

select job_name, session_id, running_instance, elapsed_time
from user_scheduler_running_jobs;

session_id를 얻었으면 그 세션이 무엇을 기다리고 있는지 본다. 락 대기인지, 디스크 I/O인지, 아니면 그냥 오래 걸리는 작업인지가 갈린다.

select s.sid, s.serial#, s.status, s.event, s.seconds_in_wait,
s.blocking_session, q.sql_text
from v$session s
left join v$sql q on q.sql_id = s.sql_id
where s.sid = &session_id;

blocking_session에 값이 있으면 다른 세션에 막혀 있다는 뜻이다. 야간 배치가 끝나지 않는 원인 중 흔한 쪽이다.

이런 상황을 반복해서 겪는다면 잡에 최대 수행 시간을 걸어 두는 편이 낫다. 지정 시간을 넘기면 스케줄러가 중단시키고 실패로 기록한다. 아침에 와서야 “아직도 돌고 있었다”를 발견하는 것보다 낫다.

begin
dbms_scheduler.set_attribute(
name => 'JOB_DAILY_STAT',
attribute => 'max_run_duration',
value => interval '90' minute);
end;
/

로그가 비는 근본 원인

여기까지 와도 원인이 안 잡히는 경우, 프로시저 안의 예외 처리를 의심할 차례다. 다음 패턴은 스케줄러 쪽 이력까지 깨끗하게 만든다.

exception
when others then
null;
end;

예외를 삼키고 정상 종료하기 때문에 스케줄러 입장에서는 성공한 잡이다. status는 SUCCEEDED로 남고, 결과만 아무것도 안 만들어져 있다. “잡은 돌았다는데 데이터가 없다”의 대표적인 원인이다.

로그를 남기고 예외를 다시 올리는 형태가 기본이 되어야 한다.

exception
when others then
insert into job_err_log (job_nm, err_dt, err_cd, err_msg, err_stack)
values ('JOB_DAILY_STAT', systimestamp, sqlcode, sqlerrm,
dbms_utility.format_error_backtrace);
commit;
raise;
end;

로그를 남기는 INSERT는 별도 트랜잭션으로 처리해야 본문 롤백과 함께 사라지지 않는다. 로그 전용 프로시저를 PRAGMA AUTONOMOUS_TRANSACTION으로 만들어 두고 호출하는 방식이 일반적이다.

정리

스케줄러 잡 문제는 세 갈래로 나뉜다. 시작이 안 된 경우, 시작했는데 실패한 경우, 시작했는데 끝나지 않는 경우. 확인해야 할 뷰가 각각 다르므로, 어느 쪽인지부터 가르는 것이 순서다.

  • 시작 여부 — user_scheduler_jobs의 enabled, state, next_run_date
  • 실패 원인 — user_scheduler_job_run_details의 error#, additional_info
  • 진행 중 상태 — user_scheduler_running_jobs와 v$session

그리고 애플리케이션 로그가 비어 있다는 사실 자체가 정보다. 스케줄러 이력에는 실패가 찍혔는데 내 로그가 비었다면, 로그를 남기기 전 단계에서 죽은 것이다. 양쪽 다 깨끗한데 결과만 없다면, 예외를 어딘가에서 삼키고 있는 것이다.

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

위로 스크롤