manywaypark's Blog
개발, 검색, 함수

synopsis - the bug
mysql 데이터를 oracle로 옮기는 아주 단순한 모듈을 clsql을 사용해서 만들었다.
처음에는 oracle 서버 설정이 잘못되어 oci를 통해 oracle에 접근하는 것이 불가능했다.
ORA-12705: Cannot access NLS data files or invalid environment specified.
서버 설정은 다음과 같이 하면 별 무리가 없을 듯했다.
# in .bash_profile of user oracle
export NLS_LANG=(KOREAN_KOREA|AMERICAN_AMERICA).UTF8
export ORA_NLS33=$ORACLE_HOME/ocommon/nls/admin/data      ## 9i  이하 버전
export ORA_NLS10=$ORACLE_HOME/nls/data                    ## 10g 이상 버전

클라이언트 쪽은 utf-8 환경의 우분투이므로,
export NLS_LANG=.UTF8

서버/클라이언트 설정을 맞추고 나자 다음과 같은 서버 에러를 내면서 레코드 삽입이 멈추는 심각한 상태가 되어 버렸다. 운좋게 들어간 몇몇 레코드들은 한글이 모조리 깨져있었다.
ORA-01756 quoted string not properly terminated
처음에는 인코딩 문제인것같아 환경변수 설정을 바꿔가면서 해봤다. 테스트용 모듈이 가끔 정상적인 한글을 insert할 때가 있었는데, 그 경우는 lisp 소스에 한글이 static하게 박혀있고 그 모듈을 (load "test.lisp")의 형태로 읽어들일 경우에만 한글이 제대로 오라클 DB로 전송되었다(지금도 이 부분이 정확히 어떻게 해서 성공적으로 그렇게 되었는지는 잘 이해가 안가는 상태다. lisp reader의 농간으로 추측되긴한다). 즉, 같은 소스를 emacs에서 읽어들여 function을 eval하면 깨진 한글이 전송되는... 신기하긴 하지만 돌아버릴 것같은 상황이 연출된 것이다. Orz...

이것이 눈물겨운 hacking의 시작이었다.

problem solving - the patch
clsql-oracle 및 uffi의 소스를 뒤졌다. oracle-sql.lisp에서 일단 실마리를 찾았다. sql-stmt-exec에서 oci interfact를 통해 sql statement를 실행하는데, uffi를 사용해 foreign string으로 변환 후에 oci interface를 통해 서버측으로 전달되게 되어있었다.
인코딩의 문제라는 강력한 심증이 있었으므로, lisp string 에서 변환된 foreign string을 lisp string으로 다시 변환(convert-from-foreign-string)해서 로그를 찍어보았다. 로그는 출력되지 않고 encoding error가 났다.
debugger invoked on a SB-INT:C-STRING-DECODING-ERROR in thread
#<THREAD "initial thread" {10025DEED1}>:
c-string decoding error (:external-format :UTF-8):
the octet sequence 3 cannot be decoded.

문제의 근원 (root cause)이 파악되는 순간이었다 (반이나 왔다고 긍정적으로 생각할 수도, 반이나 남았다고 비관적으로 생각할 수도 있는... go냐 stop이냐를 정말 갈등하게 하는 순간이다). '그냥 bug report하고 기다릴까? 테스트 케이스까지만 만들어 주고 report and wait??' 이런 유혹에 잠시 빠질려는 순간 어디선지 모르게 승부욕이 용솟음쳤다. 내나라의 아름다운 글을 바르게 표시하는 것은 내손으로 하자. 막판에는 사명감까지 생겼다. --;;

이제, 지금 이순간 필요한 것은 확실한 test case의 작성!!
(defparameter *tests* '("english" "한글"))
(dolist (test *tests*)
  (let* ((f    (uffi:convert-to-foreign-string test))
         (back (uffi:convert-from-foreign-string f)))
    (format t "~%test=~a, back=~a" test back)
    (assert (string= test back))))

역시 한글에서만 error가 났다. 용의자는 convert-to-foreign-string 아니면 convert-from-foreign-string 으로 압축되었다.

테스트 코드를 좀 끼워넣어보고, 함수를 재정의(lisp이 정말 강력한 부분)해보고 해서 찾은 진범은 바로,
uffi:convert-to-foreign-string

unicode에 대한 고려가 안되어있었다.

sbcl의 unicode관련 코드를 좀 살펴보고, uffi쪽 코드도 좀 살펴보고 해서 uffi:convert-to-foreign-string를 수정해서 상기 테스트 코드가 통과되게 만들었다.
이젠 DB쪽 삽입을 테스트할 차례. 자신감으로 충만해서 잠시 잊었던 나의 원래 목적인 (제일 처음 작성했던!!) mysql2oracle migration 모듈을 실행했다.
그런데 또 위에서 나왔던 ORA-01756에러가 났다. Orz...

이상하게 이번에는 전혀 짜증나지도 않았고, 해결할 수 없을지도 모른다는 생각도 들지 않았다. 이미 uffi와 clsql-oracle의 내부를 어느정도 파악하고 있었기 때문에 할 수 있다는 생각이 들었다.
잠시 살펴본 결과 sql-stmt-exec에서 oci interface로 전달하는 파라미터 중에 sql statememt의 길이를 전달하는 부분이 있었는데 여기에 원래 lisp string의 길이를 전달하고 있었다 (ascii string은 utf-8로 인코딩해도 길이가 같기 때문에 문제가 안되지만 한글등의 non-ascii는 당연히 짤려버려서 에러가 난다).
그래서 간단히 uffi:foreign-string-length를 불러서 해결하려했으나, 이 함수는 아무도 안쓰는지 완전히 잘못되어 있었다. macro의 body를 defun으로 정의해놓았고, 그나마 그것도 문법오류를 포함하고 있었고, unicode 고려도 안되어있었다. 악의 축같은 함수(매크로) 였지만, 이미 익숙해져 있는 모듈이었으므로 간단히 고칠 수 있었다.

이제 남은 것은 기존 테스트 케이스 돌려보기 및 upstream 반영을 위해 maintainer에게 patch 전달하기.



happy hackin'

댓글을 달아 주세요

sqlite3 backend로 clsql을 쓸때 다음과 같은 에러가 나는 경우가 있다.
"Error 21 / library routine called out of sequence"
기본적으로 라이브러리를 잘못 사용하고 있다는 말인데, 현재 상황에 비추어 검색한 결과 thread safe 문제인듯했다. 아래 참고 링크에서 SQLite 사이트의 FAQ를 인용해 놓은 것을 보면,
It is never safe to use the same sqlite3 structure pointer in two or more threads.
라고 되어 있으나 현재의 FAQ에서는 찾아 볼 수 없다 (FAQ.6에 비슷한 말이 나온다. sqlite 3.3.1이전 버전은 sqlite3 structure를 같은 thread에서만 열어야하며 thread safe를 위해서는 3.3.1 이후 버전을 SQLITE_THREADSAFE = 1로 설정하고 컴파일을 해야만한다. 아쉽게도 dapper에 깔리는 sqlite3는 버전이 3.2.8-1ubuntu1로 좀 낮다).
개발 중인 프로그램이 multithread인데 이전 버전들(sbcl 1:0.9.14.0-2 + cl-sql 3.6.3-1 on edgy)의 조합에서는 나타나지 않던 문제였으나 얼마전 내가 만든 backport들 (sbcl 1.0 + cl-sql 3.7.8 on dapper)의 조합에서는 나타났다.

DB 연결부분을 다음과 같이 기존 연결이 있을 때는 경고를 내고 그것을 재사용하도록 한 것이 문제일 수도 있겠지만, 문제의 여지를 남겨두기 싫어서 필요할 때마다 연결하고 끊는 매크로를 사용하고, thread간에는 mutex를 사용해서 동기화 해버렸다.
(clsql:connect (list *db-file*) :database-type +DB-TYPE+ :if-exists :warn-old)

(기존 연결 재사용은  thread safe 버전의 sqlite3를 사용하면 간단히 해결될 수도 있을 것같다.)

참고:
http://trac.edgewall.org/ticket/2196

happy hackin'

TAG clsql, err, sqlite3

댓글을 달아 주세요

cl-sql 사용하기

함수형 언어/Lisp 2007. 5. 29. 14:01 by manywaypark
cl-sql은 common lisp에서 여러 종류의 DB backend에 접근하는데 사용할 수 있는 package이다.
간단히 (asdf-install:install 'clsql)로 될줄 았았는데, 잘 안됐다. library 로딩에 실패하고 어쩌구...
혹시나 하는 마음에 ubuntu(debian) packaging system apt를 이용해보기로 했다.
$ apt-cache search clsql
cl-sql - SQL Interface for Common Lisp
cl-sql-aodbc - CLSQL database backend, AODBC
cl-sql-mysql - CLSQL database backend, MySQL
cl-sql-odbc - CLSQL database backend, ODBC
cl-sql-postgresql - CLSQL database backend, PostgreSQL
cl-sql-postgresql-socket - CLSQL database backend, PostgreSQL
cl-sql-sqlite - CLSQL database backend, SQLite
cl-sql-sqlite3 - CLSQL database backend, SQLite3
cl-sql-tests - Testing suite for CLSQL
cl-sql-uffi - Common UFFI functions for CLSQL database backends
cl-sql-oracle - CLSQL database backend, Oracle
오호라, 필요한건 다 있는 거같다.
$ sudo apt-get install cl-sql cl-sql-mysql
......
sbcl을 실행하고 테스트해봤다.
$ sbcl
......
* (require 'clsql)

; loading system definition from /home/USER/.sbcl/systems/uffi.asd into
; #<PACKAGE "ASDF1">
; registering #<SYSTEM UFFI {10028CA001}> as UFFI
NIL
* (require 'clsql-mysql)
......
Couldn't load foreign libraries "libmysqlclient", "libmysql". (searched CLSQL-SYS:*FOREIGN-LIBRARY-SEARCH-PATHS*)
......
엥? 에러 컨디션?
사실 (require 'clsql-mysql)은 직접 호출하지 않아도 connect류 등의 관련 함수를 부르면 자동으로 올라오는데 그것들도 다 실패했다. 시스템에는 libmysql*이 전부 설치되어있었다.
이 문제는 다음 두가지 방법 중 하나를 선택해서 해결할 수 있다.
해결책1: 직접 uffi를 이용해서 library를 로딩. (참고:http://common-lisp.net/pipermail/cl-debian/2006-May/001287.html)
CL-USER> (uffi:load-foreign-library "/usr/lib/libmysqlclient.so.15")
해결책2: symbolic link 생성.
$ sudo ln -s /usr/lib/libmysqlclient.so.15.0.0 /usr/lib/libmysqlclient.so
(library 버전 번호는 필요한 것으로 선택한다.)

테스트:
CL-USER> (require 'clsql)
......
;;; connect params: host, db, user, password.
CL-USER> (clsql:connect '("localhost" "mysql" "root" "") :database-type :mysql)
#<CLSQL-MYSQL:MYSQL-DATABASE localhost/mysql/root OPEN {1003707EF1}>
CL-USER> (clsql:status)
CLSQL STATUS: 2007-05-28 21:21:31,,0
-----------------------------------------
   DATABASE              TYPE   RECORDING 
-----------------------------------------
*  localhost/mysql/root  mysql  nil       
-----------------------------------------
; No value
CL-USER> (clsql:list-databases '("localhost" "mysql" "root" "") :database-type :mysql)
("information_schema" "mysql")
CL-USER> (clsql:query "select * from user")
......
CL-USER> (clsql:disconnect :database (clsql:find-database "localhost/mysql/root"))
T
CL-USER> (clsql:status)
CLSQL STATUS: 2007-05-28 21:48:23,,0
; No value

참고: apt를 이용해서 lisp 모듈을 설치하면, /usr/share/common-lisp/ 아래에 설치된다. 소스 참고시 유용하다.

2008-04-15 :
oracle을 backend로 쓰려면 instant client download 페이지에서 필요한 파일을 다운로드 받아 특정 디렉토리에 압축을 풀어놓고 그 디렉토리를 LD_LIBRARY_PATH 환경변수에 추가하면 된다. 물론 위의 해결책2에서 처럼 symbolic link를 걸어주는 작업(버전 번호를 떼주는...)이 필요하다.

happy hackin'

댓글을 달아 주세요

1 
분류 전체보기 (306)
잡담 (20)
함수형 언어 (65)
emacs (16)
java (18)
tips & tricks (154)
사랑 (1)
가사 (0)
독서 (4)
mobile (6)
비함수형 언어 (2)

공지사항

최근에 받은 트랙백

02-25 12:30