sonumb

Shell script를 이용한 DBMS Test에서 client 실행 순서 제어 본문

개발자 이야기/Software Engineering

Shell script를 이용한 DBMS Test에서 client 실행 순서 제어

sonumb 2021. 3. 13. 13:51

목차

  • 1. 문제상황 및 요구사항
    • 1.1. 문제상황
    • 1.2. 요구사항
  • 2. 설계
    • 2.1. 전체 구조
    • 2.2. 쉘 스크립트 구현
      • 2.2.1. 함수 목록 및 설명
      • 2.2.2. 함수 명세
  • 3. 구현
    • 3.1. 코드
  • 4. 테스트
    • 4.1. 예제: 트랜잭션 제어
      • 4.1.1. 코드
      • 4.1.2. 실행 및 결과

1. 문제상황 및 요구사항

1.1. 문제상황

여러 클라이언트가 각자의 SQL을 실행하는 테스트가 있다고 가정하자. 이때, 각 클라이언트의 SQL 실행 순서를 제어해야 하는 경우가 있다.

예를 들어,

  Client 1 Client 2
1 create test database & test table 'tbl'  
2   transaction start;
3 transaction start;  
4 insert into tbl values ( 1 );  
5   select * from tbl;
6 select * from tbl;  
7   insert into tbl values ( 2 );
8 commit;  
9   select * from tbl;
10   rollback;
11   select * from tbl;

의 순서로 꼭 실행해야 한다.

그러나, 'mysql'   과 같은 대부분의 CLI 기반 클라이언트 프로그램으로는 client간 실행 순서 제어가 불가능하다.

1.2. 요구사항

  1. 여러 클라이언트를 동시에 실행되고, SQL을 포함한 명령어를 수행함에 있어 각 실행의 순서를 제어할 수 있어야 한다.
    1. 클라이언 실행시 옵션 인자를 제어할 수 있어야 한다.
  2. 실행 후 결과를 클라이언트마다 따로 확인해야함
    1. 입력과 결과의 출력이 혼동되면 안된다.
  3. 클라이언트 별로 signal 처리도 가능해야 한다.

2. 설계

2.1. 전체 구조

사용자는 클라이언트 프로그램의 실행을 제어하고 싶어한다. 그러나 배치파일로 실행하면 이러한 순서를 제어할 수 없다.

따라서 C API 혹은 ODBC와 같은 다른 인터페이스를 호출하는 프로그램을 작성하여 이러한 순서를 제어하여 테스트를 수행한다.

그러나 이러한 방법에는 생산성이 낮아지는 문제가 있으므로, mysql 과 같이 기존의 사용자 CLI(Command Line Interface) 클라이언트 프로그램('CLIC' 라 정의)를 이용하여 순서를 제어하는 것이 더욱 생산적일 것이다.

이러한 실행 순서를 제어하게 위해서, 실행되고 있는 CLIC에게 따로 명령어 실행하여도 종료가 되지 않으며 지속적으로 명령어를 전달할 수 있는 구조가 제안되어야 한다.

그리고 이러한 전체 구조는 위 그림과 같다.

사용자 입력을 받을 수 있는 파일(cmd.in.*)이 있으며, CLIC은 그 파일에 명령어가 유입되기를 기다리고 있다.

만일 사용자가 입력파일에 SQL 혹은 명령어를 기록하면, CLIC은 그것을 읽고 실행한다.

CLIC는 그 실행 결과를 stdout에 출력하지만, 실제로는 redirection 하였으므로, 화면 출력물은 파일(cmd.out.*)에 저장된다.

in/out 파일들은 CLIC마다 다른 파일로 열기/생성되므로, 각 CLIC은 다른 명령어를 읽을 수 있으며, 이에 대한 결과도 다른 파일에 저장할 수 있다.

 

이런 구조는 명령어의 실행이 끝나도 CLIC은 종료되지 않게 한다. 종료를 원한다면 종료 명령어를 사용자가 cmd.in.* 파일에 기록하고 CLIC은 이를 읽고 종료한다.

2.2. 쉘 스크립트 구현

아래 함수는 mysql 클라이언트 프로그램에 종속적이다.

다른 DBMS의 클라이언트 프로그램에 적용하고자 한다면, 적절히 변경해야 한다.

변경해야 할 포인트는 아래에서 기술한다.

 

2.2.1. 함수 목록 및 설명

함수의 이름과 간단한 설명은 아래와 같다.

  • tf_sleep(): 입력한 초 만큼 sleep한다.
  • echo_stage() : 실행할 내용을 설명할 필요가 있으며, 이 설명을 출력한다.
  • clt_launch_CLI(): 새로운 mysql 클라이언트를 실행
  • clt_get_session_id(): 특정 클라이언트의 세션ID를 조회
  • clt_exec_cmd(): 클라이언트에서 SQL이나 명령어를 실행하는 함수 (실행하여도 종료되지 않음)
  • clt_send_signal(): client에게 signal을 보낸다
  • clt_wait(): client의 실행이 완료될 때까지 대기한다.
  • clt_terminate(): client를 종료시킨다.
  • clt_cat_in_file(): 사용자가 client에게 입력한 명령어들을 출력한다.
  • clt_cat_out_file(): client의 실행 결과을 출력한다.
  • clt_rm_all_files(): 모든 client 의 in/out 파일을 삭제한다.

2.2.2. 함수 명세

⚠️client_id

아래에서 기술하는 client_id 는 공백을 제외한 하나 이상의 영문자, 숫자, 대시('-'), 언더바('_')의 조합이다.

정규표현식으로 나타내면 "[\-_0-9a-zA-Z]+" 이다. (하지만 코드내에서 이를 검사하지 않는다.)

(⛔️client_id는 파일명에 이용되므로, 특수 문자를 사용할 경우 문제가 발생한다. 따라서 숫자와 영문자만 조합할 것을 권장한다.)

 

함수명 인자 설명
tf_sleep() $1: <second>  입력한 값만큼 sleep한다. 입력값은 양의 부동소수를 입력해야 한다.
  • tf는 test framework의 약자다.
  • macOS는 sleep만 있으므로, sec 단위 이하로 sleep할 수 없다. 따라서, perl로 구현하여 이를 제어한다.
echo_stage() $1: <description> <description> 은 기술할 내용을 의미하며, 문자열이다.
이 함수를 호출하면 자동으로 증가되는 숫자와 <description>을 출력할때, 위아래로 '#'으로 구성된 라인도 함께 출력한다.
결과는 아래와 같다.

clt_launch_CLI()
  • $1: <client_id>
  • $2: dbname
  • "$3": <mysql options>
CLI 클라이언트를 실행할 때, cmd.in.<client_id> 파일과 cmd.out.<client_id> 파일을 각각 사용자 입력/출력결과를 저장하는데 사용한다.
mysql의 경우 아래와 같이 실행한다.
mysql은 기본적으로 -fvqn 옵션과 함께 실행되며,추가적인 실행 옵션을 넘겨주고 싶다면, 싱글 혹은 더블쿼테이션으로 감싸야한다. (파이프로 연결되어 실행된다면, 기본적으로 배치모드(-B)가 활성화됨을 알아두자)
기본으로 들어가는 옵션의 설명은 아래 소스코드에 기입하였다.

  1. 특이하게도 client 프로세스의 pid와 tail 프로세스의 pid를 저장한다. client pid는 시그널 전송에 이용된다. tail pid 저장의 이유는 아래 clt_terminate에서 설명한다.
clt_get_session_id()
  • $1: <client_id>
클라이언트 $1의 세션아이디를 조회한다.
<ex>
sessid_1=$(clt_get_session_id 1);

echo $session_1;
clt_exec_cmd()
  • $1: <client_id>
  • "$2": SQL 혹은 명령어
  1. $2의 끝은 항상 ';'로 끝나야 한다.
  2. "select * from a; select * from b;"와 같이 복수의 SQL로 구성될 수 있으나, 권장하지 않는다.
  3. clt_exec_cmd() 함수의 구현을 보면, SQL 수행 후, "mysql> $"를 출력해주도록 하였고, 이로 인해 사용자 입력 대기 상태와 트랜잭션 처리 대기상태"를 구별할 수 있게 한다. 자세한 이유는 clt_wait() 에서 설명한다.
clt_send_signal()
  • $1: <client_id>
  • $2: <Signal_id>
<signal_id>는 signal -l 명령어로 확인할 수 있다.
clt_send_signal() 로 SIGKILL을 하여 강제종료하였다하더라도, clt_terminate()를 호출하여 정리하여야 한다.
clt_wait()
  • $1: <client_id>
내부적인 구현이 조금 복잡하다.
mysql은 출력물을 redirection하면, 프롬프트가 출력되지 않는다. 프롬프트 대기상태이면, "^$"가 출력된다.
그런데, 이는 트랜잭션 처리중이라서 대기하는 상태와 구별이 불가능하다. 그러므로 clt_exec_cmd()에서 SQL 수행 후, "^mysql> $"를 출력한다. 이것으로 "사용자 입력 대기 상태"와 "트랜잭션 처리 대기상태"를 구별할 수 있게 한다.
그러면 clt_wait()는 0.5초간 sleep 힌 다음, 출력 파일에 "^mysql> $"(정규표현식)이 출력된 것을 확인하면 대기를 종료하며, 아니라면 프롬프트가 출력될 때까지 이를 반복한다. 

이때 발생할 수 있는 문제점은 클라이언트의 비정상 종료로 인해 프롬프트가 출력 안된다면 무한대기 할 가능성이 있다.
개선 방안으로 "시작시간 이후 일정시간 이상 대기하였다면 종료"하는 코드를 삽입하여 종료를 유도하는 것이다.
clt_terminate() $1: <client_id> client 를 종료한다.
tail의 pid를 저장하는 이유:
일반적으로 "tail -f" 프로세스는 사용자가 종료하기 전까지 무한대기한다.
'tail -f a.sql | mysql -uroot' 와 같이 파이프로 연결되었을 때, 연결된 mysql 프로세스가 종료되면 tail도 같이 종료된다.
그러나, 불행하게도 쉘스크립트로 실행할 때, mysql를 종료해도 tail 프로세스가 종료되지 않는다.
따라서 tail의 pid를 저장하고, 이후에 저장된 id를 이용하여 kill로 종료한다.
clt_send_signal() 로 SIGKILL을 하여 강제종료 하더라도, clt_terminate()를 호출하여 정리하여야 한다.
clt_cat_in_file() $1: <client_id> 사용자 입력명령어의 파일인 cmd.in.<client_id> 파일의 내용을 출력한다.
clt_cat_out_file() $1: <client_id> 사용자명령어의 결과 파일인 cmd.out.<client_id> 파일의 내용을 출력한다.
clt_rm_all_files() $1: <client_id> 모든 cmd.* 파일을 삭제한다.

 

3. 구현

3.1. 코드

client_control.sh

#!/bin/sh
 
##########################################################
# 이 파일을 다른 DBMS를 테스트하는데 이용하고 싶다면,
#  1. 아래 변수들
#  2. clt_launch_CLI() 에서 세션 ID를 획득하는 부분
# 을 수정해야 한다.

CLI_CLIENT=`which mysql`
CLI_BINARY=`basename $CLI_CLIENT`
CLI_PROMPT="mysql> "
CLI_PROMPT_PATTERN="^${CLI_PROMPT}$" # for grep
CLI_DEFAULT_OPTS="-fvnq"
# need to get session id,
CLI_ADMIN_CMD="$CLI_CLIENT -uroot -N" # no column header
##########################################################

if [[ -z $CLI_CLIENT ]]; then
  echo "ERROR: check path of CLI program!"
  exit;
fi
 
# macOS doesn't have 'usleep'
function tf_sleep()
{
  if [[ ! $# -eq 1 ]]; then
    echo "${FUNCNAME[0]} <sleep_interval>"
    exit;
  fi
 
  perl -e "select(undef,undef,undef,$1);"
}
 
echo_stage()
{
  let stage+=1;
  printf "\
################################################################\n\
${stage}. $1
################################################################\n"
}
export -f echo_stage
 
clt_launch_CLI()
{
  # $1: client_id
  # $2: CLI's options
 
  if [[ $# -gt 2 ]]; then
    echo "error: Args are invalid"
    echo " - usage: ${FUNCNAME[0]} <client_id> \"<CLI client's options>\""
    echo " - example ${FUNCNAME[0]}  clt1 \"testdb -u root -ppasswd\""
    echo "   (default option are \"-fvnq\". "
    echo "    If give options, they would be appended.)"
    exit 1;
  fi
 
# -B, --batch         Don't use history file. Disable interactive behavior.
#                     (Enables --silent.)
# -f, --force         Continue even if we get an SQL error.
# -v, --verbose       Write more. (-v -v -v gives the table output format).
#   (사용자가 입력한 SQL 혹은 명령어를 출력한다.)
# -n, --unbuffered    Flush buffer after each query.
#  -q, --quick         Don't cache result, print it row by row. This may slow
#                      down the server if the output is suspended. Doesn't use
#                      history file.
 
  opts=
  if [[ $# -eq 2 ]]; then
    opts=$2
  fi
 
  touch cmd.in.$1;
  touch cmd.out.$1;
 
  printf "[client:$1] launch CLI cient with \"$opts\"\n";
  (tail -f cmd.in.$1 | $CLI_CLIENT $opts $CLI_DEFAULT_OPTS >> cmd.out.$1 2>&1 ) &
 
  # Generally, the 'tail -f' process forked in a shell scripts
  # won't terminate by end of parent process.
  # Therefore, it must be terminated by kill command.
 
  # get child bash process pid
  clt_child_pid=$!
 
  # get tail's pid of child bash process
  export __clt_$1_tail_pid=`ps -ef | grep ${clt_child_pid} | \
    grep "tail -f cmd.in.$1" | awk '{print $2}'`
 
  # get CLI client's pid of child bash process
  export __clt_$1_pid=`ps -ef | grep ${clt_child_pid} | \
    grep $CLI_BINARY | awk '{print $2}'`
 
  clt_pid="__clt_$1_pid"
  export __clt_$1_sessid="$(echo "select conn_id from sys.x\$session where pid = ${!clt_pid};" | $CLI_ADMIN_CMD )"
 
  ## DEBUGGING
  # clt_sid="__clt_$1_sessid"
  # printf "CLI client: ${!clt_pid} ${!clt_sid}\n"
}
export -f clt_launch_CLI
 
clt_get_session_id()
{
  # $1: client_id
  if [[ $# -ne 1 ]]; then
    echo "error: Args are invalid"
    echo " - usage: ${FUNCNAME[0]} <client_id>"
    exit 1;
  fi
 
  clt_sessid="__clt_$1_sessid"
  echo ${!clt_sessid}
}
export -f clt_get_session_id
 
clt_exec_cmd()
{
  if [[ $# -ne 2 ]]; then
    echo "error: args are invalid: $@"
    echo " usage: ${FUNCNAME[0]} <client_id> \"<cmd or SQL string>\""
    exit 1;
  fi
 
  printf "[client:$1] ";
  printf "$2\n" | tee -a cmd.out.$1;
  printf "$2\n" >> cmd.in.$1;
  echo "\! printf '\nmysql> '" >> cmd.in.$1;
}
export -f clt_exec_cmd
 
clt_send_signal()
{
  # $1 : mclt id
  # $2 : signal id
  if [[ $# -ne 2 ]]; then
    echo "error: args are invalid"
    echo " usage: ${FUNCNAME[0]} <client_id> <signal>"
    "<signal> is one of belows:"
    kill -l
    exit 1;
  fi
 
  clt_pid="__clt_$1_pid"
  kill -s $2 ${!clt_pid}
}
export -f clt_send_signal
 
clt_wait()
{
  if [[ $# -ne 1 ]]; then
    echo "error: client id is invalid"
    echo " usage: ${FUNCNAME[0]} <client_id>"
    exit 1;
  fi
 
  # echo "" | watch -t -n 1 -e \'! tail cmd.out.$1 | grep -m 1 "^`echo $PROMPT`$"\'
  printf "[client:$1] waiting for end of command\n"
 
  until tail -1 cmd.out.$1 | grep -m 1 "$CLI_PROMPT_PATTERN";
  do
    tf_sleep 0.5;
  done;
}
export -f clt_wait
 
clt_terminate()
{
  if [[ $# -ne 1 ]]; then
    echo "error: client id is invalid"
    echo " usage: ${FUNCNAME[0]} <client_id>"
    exit 1;
  fi
 
  printf "[client:$1] try to terminate\n"
  #clt_exec_cmd $1 "exit;"
  clt_exec_cmd $1 "\q"
 
  clt_tail_pid="__clt_$1_tail_pid"
  # sh:   kill -s TERM ${!clt_tail_pid}
  # bash: kill -s SIGTERM ${!clt_tail_pid}
  kill -15 ${!clt_tail_pid} # 15: SIGTERM
 
  # wait for fflush in/out files
  tf_sleep 0.1;
}
export -f clt_terminate
 
clt_cat_in_file()
{
  if [[ $# -ne 1 ]]; then
    echo "error: client id is invalid"
    exit 1;
  fi
 
  printf "[client:$1] print input file\n"
  cat cmd.in.$1
  echo   "----------------------------------------------------------------"
}
export -f clt_cat_in_file
 
clt_cat_out_file()
{
  if [[ $# -ne 1 ]]; then
    echo "error: client id is invalid"
    exit 1;
  fi
 
  printf "[client:$1] print output file\n"
  cat cmd.out.$1
  echo   "----------------------------------------------------------------"
}
export -f clt_cat_out_file
 
clt_rm_all_files()
{
  printf "remove all in/out files of clients\n"
  rm cmd.in.*
  rm cmd.out.*
}
export -f clt_cat_out_file

 

4. 테스트

4.1. 예제: 트랜잭션 제어

이 문서 제일 위에서 언급한 내용을 테스트 코드로 옮긴 것이다.

4.1.1. 코드

test_tx.sh

#!/bin/bash
 
# 위에서 작성한 client_control 라이브러리를 가지고 온다.
source client_control.sh
 
TEST()
{
  echo_stage "remove all client's in/out files"
  clt_rm_all_files;
 
  echo_stage "launch client #1"
  clt_launch_CLI 1 "-uroot";
 
  echo_stage "create testdb database & tbl table"
  clt_exec_cmd 1 "drop database if exists testdb;"
  clt_exec_cmd 1 "create database testdb;"
  clt_exec_cmd 1 "use testdb;"
  clt_exec_cmd 1 "drop table if exists tbl;"
  clt_exec_cmd 1 "create table tbl( col1 int );"
 
  echo_stage "launch client #2"
  clt_launch_CLI 2 "testdb -uroot";
 
  echo_stage "Session ID of client #1"
  clt_get_session_id 1
 
  echo_stage "Session ID of client #2"
  clt_get_session_id 2
 
  echo_stage "mixed insert & select with transaction"
  clt_exec_cmd 2 "begin;"
  clt_exec_cmd 1 "begin;"
  clt_exec_cmd 1 "insert into tbl values (1);"
  clt_wait 1;
  clt_exec_cmd 2 "select * from tbl;"
  clt_wait 2;
  clt_exec_cmd 1 "select * from tbl;"
  clt_exec_cmd 2 "insert into tbl values (2);"
  clt_wait 2;
  clt_exec_cmd 1 "commit;"
  clt_wait 1;
  clt_exec_cmd 2 "select * from tbl;"
  clt_exec_cmd 2 "rollback;"
  clt_exec_cmd 2 "select * from tbl;"
 
  echo_stage "termination"
  clt_terminate 1;
  clt_terminate 2;
 
  echo_stage "wait for fflush stdout buffer"
  tf_sleep 1;
 
  echo_stage "print client 1's in/out files"
  clt_cat_in_file 1;
  clt_cat_out_file 1;
 
  echo_stage "print client 2's in/out files"
  clt_cat_in_file 2;
  clt_cat_out_file 2;
 
  exit 0;
}
 
TEST

4.1.2. 실행 및 결과

$ bash test.sh
################################################################
1. remove all client's in/out files
################################################################
remove all in/out files of clients
################################################################
2. launch client #1
################################################################
[client:1] launch CLI cient with "-uroot"
################################################################
3. create testdb database & tbl table
################################################################
[client:1] drop database if exists testdb;
[client:1] create database testdb;
[client:1] use testdb;
[client:1] drop table if exists tbl;
[client:1] create table tbl( col1 int );
################################################################
4. launch client #2
################################################################
[client:2] launch CLI cient with "testdb -uroot"
################################################################
5. Session ID of client #1
################################################################
95
################################################################
6. Session ID of client #2
################################################################
97
################################################################
7. mixed insert & select with transaction
################################################################
[client:2] begin;
[client:1] begin;
[client:1] insert into tbl values (1);
[client:1] waiting for end of command
mysql>
[client:2] select * from tbl;
[client:2] waiting for end of command
mysql>
[client:1] select * from tbl;
[client:2] insert into tbl values (2);
[client:2] waiting for end of command
mysql>
[client:1] commit;
[client:1] waiting for end of command
mysql>
[client:2] select * from tbl;
[client:2] rollback;
[client:2] select * from tbl;
################################################################
8. termination
################################################################
[client:1] try to terminate
[client:1] \q
[client:2] try to terminate
[client:2] \q
################################################################
9. wait for fflush stdout buffer
################################################################
################################################################
10. print client 1's in/out files
################################################################
[client:1] print input file
drop database if exists testdb;
\! printf '\nmysql> '
create database testdb;
\! printf '\nmysql> '
use testdb;
\! printf '\nmysql> '
drop table if exists tbl;
\! printf '\nmysql> '
create table tbl( col1 int );
\! printf '\nmysql> '
begin;
\! printf '\nmysql> '
insert into tbl values (1);
\! printf '\nmysql> '
select * from tbl;
\! printf '\nmysql> '
commit;
\! printf '\nmysql> '
\q
\! printf '\nmysql> '
----------------------------------------------------------------
[client:1] print output file
drop database if exists testdb;
create database testdb;
use testdb;
drop table if exists tbl;
create table tbl( col1 int );
--------------
drop database if exists testdb
--------------


mysql> --------------
create database testdb
--------------


mysql>
mysql> --------------
drop table if exists tbl
--------------


mysql> --------------
create table tbl( col1 int )
--------------


mysql> begin;
--------------
begin
--------------

insert into tbl values (1);

mysql> --------------
insert into tbl values (1)
--------------


mysql> select * from tbl;
--------------
select * from tbl
--------------

col1
1

mysql> commit;
--------------
commit
--------------


mysql> \q
----------------------------------------------------------------
################################################################
11. print client 2's in/out files
################################################################
[client:2] print input file
begin;
\! printf '\nmysql> '
select * from tbl;
\! printf '\nmysql> '
insert into tbl values (2);
\! printf '\nmysql> '
select * from tbl;
\! printf '\nmysql> '
rollback;
\! printf '\nmysql> '
select * from tbl;
\! printf '\nmysql> '
\q
\! printf '\nmysql> '
----------------------------------------------------------------
[client:2] print output file
begin;
--------------
begin
--------------


mysql> select * from tbl;
--------------
select * from tbl
--------------

col1

mysql> insert into tbl values (2);
--------------
insert into tbl values (2)
--------------


mysql> select * from tbl;
--------------
select * from tbl
--------------

col1
2
rollback;

mysql> select * from tbl;
--------------
rollback
--------------


mysql> --------------
select * from tbl
--------------

col1
1

mysql> \q
----------------------------------------------------------------
반응형