Goldilocks DB - SQLCA scope 이슈

Sibeet·2021년 6월 8일
0

이거 때문에 일주일 고생했다..

SQLCA?

Object language bindings (SQL/OLB)라는 것이 있다. 다른 이름으론 embedded SQL이란 것인데(나는 Esql이라 부른다.), 일반적으로 디비를 접근하기 위해 개발자들은 ODBC JDBC..뭐 이런 것들을 사용하지만, 핸들을 다루는 커넥터 API기 때문에 사실 쓰기 좀 불편한 면이 있다.

단순히 SELECT 1 FROM DUAL; 하나 보자고 DBC핸들 만들고...CONNECT핸들 만들고...STATEMENT 핸들 만들고..

이런 과정은 좀 과하게 복잡하고, JDBC면 그나마 낫겠지만 ODBC는 저거 한 문장을 위해 수십 줄의 추가 코드가 필요할 수 있다.

이를 보완하기 위해 나온 것이 Embedded SQL으로, 특정 구문을 통해 그냥 SQL문만 나열해도 되는 것처럼 만들어 줄 수 있다. 기반은 c, cpp.

ex)

    EXEC SQL BEGIN DECLARE SECTION;
    int          sEmpNo;
    varchar      sEName[20 + 1];
    long         sSalary;
    EXEC SQL END DECLARE SECTION;
...
    EXEC SQL
        SELECT empno, ename, sal
        INTO   :sEmpNo, :sEName, :sSalary
        FROM   EMP
        ORDER BY SAL DESC
        LIMIT  1;

declare section에 Select절을 실행하는데, into 절에 있는 variable에 해당 데이터를 넣는다는 식으로, 매우 간단하게 사용 가능한 개념이다. 보통은 전용 컴파일러가 있어 DB별로 만들어 주기도 하는데..이걸 언어라고 해야 하나? api라고 해야 하나?

대부분의 DB에 있는 개념이고 오라클은 약간 변형하여 Pro*C라는 이름으로 사용하고 있다.(여긴 Esql과 조금 다르긴 함)

어쨌건 간에

이 Esql에서 에러 핸들링을 위한 구조체가 있는데, 바로 SQLCA라는 놈으로 사용하기가 매우 간편하다.
그냥 선언만 해주고, 쿼리를 실행 후에, sqlca에 무슨 값이 들어갔는지 확인해서 에러 핸들링을 하면 된다. 선언도 할 필요 없고 세팅도 할 필요가 없다. 자체 지원이니까.

struct sqlca
{
    char    sqlcaid[8];   
    int     sqlabc;       
    struct
    {
        unsigned short sqlerrml;
        char           sqlerrmc[SQLERRMC_LEN];
    } sqlerrm;
    char    sqlerrp[8];   
    int     sqlerrd[6];
    char    sqlwarn[8];
    char    sqlext[8];    
    char    sqlstate[8];  
    unsigned short  *rowstatus;  
}

(대략 뭐 이렇게 생김)
상세 참고 : https://www.ibm.com/docs/en/informix-servers/12.10?topic=structure-fields-sqlca

이번에 내가 달려간 이슈는 바로 이 에러 핸들링에서 시작한다.

이슈 사항

  1. 해당 AP 로직은 메인 프로세스가 존재하며, 기동하면 설정된 값 만큼의 Thread를 만들어 트랜잭션 발생 시 이 Thread들이 일을 하는 로직이다. 해당 쓰레드들은 트랜잭션 발생 시 각각의 트랜잭션 함수를 호출한다.
  2. 스트레스 테스트를 하는데, 트랜잭션을 호출하다 보면 'cursor not closed' 에러가 계속해서 발생한다. 문제는 cursor를 닫는 로직이 분명히 존재한다는 점이다. 어떤 케이스에서도 fetch시점에서 close가 안 될 일은 없다.

문제 분석 포인트

  • '진짜' Cursor가 닫히는지 아닌지 분석을 해야 한다. 골디락스의 Cursor scope는 File이다. 이말인즉슨 Function, Thread 내에서 cursor가 닫히지 않고 빠져나온다면, 그 스택 scope 내에서 발생한 cursor라도 여전히 존재할 수 있다는 것을 의미한다. 극단적으로는 cursor open 채로 thread가 죽어버린다면 남아 있을 수 있다. 일반적인 kill 등이면 당연 프로세스도 죽겠지만, 프로세스가 정리해 버릴 수도 있다는 점은 배제할 수 없음.
  • 에러가 발생하는 시점을 알아야 한다. thread 로그를 뒤지건 뭐건... 특정 thread에서만 발생하는지, 어떠한 쿼리를 실행했을 때 발생하는지, 혹은 그 이후에 발생하는지, 해당 쿼리를 실행했을 때 실제로 데이터가 어떻게 발생하는지. 해당 로직으로 재현했을 때 재현되는지...

분석 후 발견한 점

  1. 단순히 트랜잭션 로직으로는 아무런 문제점이 없다. thread가 마음대로 정리되는 로직도 없고, 트랜잭션 중 close되지 않고 빠져나갈 일이 없다. cursor는 declare-open-fetch의 순서대로 잘 이행되었고, open 이전의 cursor를 닫을 순 없기 때문에 close cursor는 fetch때만 처리되어야 한다.
  2. 가장 이상한 점은 cursor 1을 open하는 트랜잭션에서 cursor 2가 닫히지 않았다고 뜨는 점이다. 해당 트랜잭션 소스에서는 cursor 2는 언급조차 되지 않는데 어째서?? 이쯤되면 제품이 잘못된 건가 하는 의문마저 듬...thread 끼리 간섭을 할 수 있다고?
  3. Sql이 정상적으로 실행되면 sqlca의 에러코드는 0, 0이다. 그런데 아주아주 이상한 것은 특정 thread는 0,0이 반환되었음에도 성공이 아니라 에러로 판단하고, 특정 에러가 발생한 그 시점에 서로 다른 thread가 순차적으로 동일하게 에러를 반환한다는 것이다.

이쯤에서 꽤 확신이 들었다. 코드 문제도 커서가 안 닫힌 문제도 아니고 에러 핸들링 시에 에러가 아님에도 에러로 판단한다면 cursor는 open된 채로 정리되지 않고 끝날 수 있다는 점이다. open cursor(성공) -> Ap에선 에러로 판단(return) -> 당연히 close 로직을 타지 않음 -> cursor는 열려 있음.

즉 에러 핸들러를 모든 Thread가 공유하는 문제라면 상기한 문제점들을 설명 가능하다는 것이다. SQL_SUCCESS임에도 시간차로 에러가 발생할 수 있고, 내 에러가 아닌데도 핸들링하므로서 원하지 않는 방식의 루틴이 될 수 있다.

goldilocks에서 SQLCA의 scope

다른 제품은 모르겠으나(아마..thread safe하게 작동하는 옵션 등이 있는 것으로 알고 있다) goldilocks에서는 sqlca가 전역 변수로 취급된다. 선언할 필요도 없고 사용이 편한 대신에 생기는 trade-off인 셈이다.
즉...메인 프로세스에서 thread로 특정 함수, 코드를 호출하는 형식이라면, 해당 코드에 '모두' sqlca를 선언해 줘야 한다.

해결

struct sqlca sqlca;

이 한문장만 추가해 주면 각각 local scope에 구조체가 선언됨으로서 thread-safe하게 사용할 수 있게 된다.
추가해 주는게 좀 일이었지만, 어쨌든 넣고 나니 cursor 등 에러 없이 정상적으로 서비스 됨을 확인하였다.

thread도 많고 로그 정보도 많고 접근 자체를 로직 문제로 접근하다보니 시간이 꽤 걸렸다. 내 경력 중에 제일 풀기 어려운 문제였던 듯.

0개의 댓글