티스토리 뷰

728x90
반응형

본 포스팅은 WildFly의 2PC Commit 수행 테스트 시나리오에 대해 알아보겠습니다.


2PC Commit은 Transaction에서 매우 중요한 일부입니다. 해당 요청이 정상 수행이 진행되는지 여부를 판단할 수 있고 문제가 발생 시 롤백을 수행 모든 요청이 완료시 Commit을 수행하여 종료를 알립니다.

이번 포스팅에서는 WildFly에서 2PC Commit이 어떠한 방식으로 이루어 지는지 알아보겠습니다.

해당 자료는 이후 WildFly 또는 JBoss POC를 수행하는데 있어 사용할 수 있는 자료입니다.

 

1. Application 작성

먼저 2PC Commit에 사용될 application을 작성해 보겠습니다.


[OracleXARun.java]

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.sql.*;
import javax.sql.*;
import java.util.*;
import oracle.jdbc.driver.*;
import oracle.jdbc.pool.*;
import oracle.jdbc.xa.OracleXid;
import oracle.jdbc.xa.client.*;
import javax.transaction.xa.*;
import java.io.*;

/**
 * Servlet implementation class OracleXARun
 */
@WebServlet("/OracleXARun")
public class OracleXARun extends HttpServlet {

// 1
        private static final long serialVersionUID = 1L;
        final static String DB_FROM_ADDR = "172.21.70.24";
        final static int DB_FROM_PORT = 1521;
        final static String DB_FROM_SID = "orcl";
        final static String DB_FROM_USER = "system";
        final static String DB_FROM_PASSWORD = "oracle";
        // 아래 한번 더 입력하는이유는, 원래 이기종간에서 2PC를 사용하기 때문에 각각 다른 정보를 입력하게 되기때문인데, 현재 TEST에서는
        // 동일기종간 2PC 테스트기 때문에 동일하게 입력한다.
        final static String DB_TO_ADDR = "172.21.70.24";
        final static int DB_TO_PORT = 1521;
        final static String DB_TO_SID = "orcl";
        final static String DB_TO_USER = "system";
        final static String DB_TO_PASSWORD = "oracle";

        /**
         * @see HttpServlet#HttpServlet()
         */
        public OracleXARun() {
                super();
                // TODO Auto-generated constructor stub
        }

        /**
         * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse
         *      response)
         */
        protected void doGet(HttpServletRequest request, HttpServletResponse response)
                        throws ServletException, IOException {
                System.out.println("ssss");
                OracleXARun oxa = new OracleXARun();
                try {
                        oxa.XARunning();
                } catch (Exception e) {
                        e.printStackTrace();
                }
        }

        /**
         * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse
         *      response)
         */
        protected void doPost(HttpServletRequest request, HttpServletResponse response)
                        throws ServletException, IOException {
                // TODO Auto-generated method stub
                doGet(request, response);
        }

        void XARunning() throws XAException, SQLException {

// 2
                XADataSource xds1 = getXADataSource(DB_FROM_ADDR, DB_FROM_PORT, DB_FROM_SID, DB_FROM_USER, DB_FROM_PASSWORD);
                XADataSource xds2 = getXADataSource(DB_TO_ADDR, DB_TO_PORT, DB_TO_SID, DB_TO_USER, DB_TO_PASSWORD);
                System.out.println("Datasource from :" + xds1);
                System.out.println("Datasource to :" + xds2);

// 3
                XAConnection xaconn1 = xds1.getXAConnection();
                XAConnection xaconn2 = xds2.getXAConnection();
                Connection conn1 = xaconn1.getConnection();
                Connection conn2 = xaconn2.getConnection();
                System.out.println("Connection from :" + conn1);
                System.out.println("Connection to :" + conn2);

// 4
                XAResource xar1 = xaconn1.getXAResource();
                XAResource xar2 = xaconn2.getXAResource();
                Xid xid1 = createXid(1);
                Xid xid2 = createXid(2);
                xar1.start(xid1, XAResource.TMNOFLAGS);
                xar2.start(xid2, XAResource.TMNOFLAGS);
                String sql;
                Statement stmt1 = conn1.createStatement();
                sql = "update accountfrom set balance=balance-10 where accountno=3000";
                stmt1.executeUpdate(sql);
                sql = " insert into accountto values(100,100)";
                System.out.println("Statement 1 passed");
                Statement stmt2 = conn2.createStatement();
                stmt2.executeUpdate(sql);
                System.out.println("Statement 2 passed");

// 5
                xar1.end(xid1, XAResource.TMSUCCESS);
                xar2.end(xid2, XAResource.TMSUCCESS);

                int prep1 = xar1.prepare(xid1);
                int prep2 = xar2.prepare(xid2);
                boolean docommit = false;
                if ((prep1 == XAResource.XA_OK || prep1 == XAResource.XA_RDONLY)
                                && (prep2 == XAResource.XA_OK || prep2 == XAResource.XA_RDONLY)) {
                        docommit = true;
                }

// 6
                if (docommit) {
                        if (prep1 == XAResource.XA_OK)
                                xar1.commit(xid1, false);
                        System.out.println("Transaction Commited1");
                        if (prep2 == XAResource.XA_OK)
                                xar2.commit(xid2, false);
                        System.out.println("Transaction Commited2");
                } else {
                        if (prep1 != XAResource.XA_RDONLY)
                                xar1.rollback(xid1);
                        if (prep2 != XAResource.XA_RDONLY)
                                xar2.rollback(xid2);
                }

// 7
                conn1.close();
                conn2.close();
                xaconn1.close();
                xaconn2.close();

                System.out.println("connection closed");
                conn1 = null;
                conn2 = null;
                xaconn1 = null;
                xaconn2 = null;
        }

        Xid createXid(int bids) throws XAException {
                byte[] gid = new byte[1];
                gid[0] = (byte) 9;
                byte[] bid = new byte[1];
                bid[0] = (byte) bids;
                byte[] gtrid = new byte[64];
                byte[] bqual = new byte[64];
                System.arraycopy(gid, 0, gtrid, 0, 1);
                System.arraycopy(bid, 0, bqual, 0, 1);
                Xid xid = new OracleXid(0x1234, gtrid, bqual);
                return xid;
        }

        XADataSource getXADataSource(String dbAddr, int port, String sid, String userId, String password)
                        throws SQLException, XAException {
                OracleDataSource oxds = new OracleXADataSource();
                String url = "jdbc:oracle:thin:@172.21.70.24:1521:orcl";
                oxds.setURL(url);
                oxds.setUser(userId);
                oxds.setPassword(password);
                return (XADataSource) oxds;
        }
}


총 7가지 스탭으로 나뉘어 Server단에서 수행 될 application을 작성합니다.

1번에서는 Database 연결 정보를 static으로 선언하는 부분입니다.

2번에서는 1번에서 선언한 정보를 기반으로 datasource xds1, xds2번을 생성합니다.

3번에서는 생성한 datasource를 getConnection합니다.

4번에서는 이를 기반으로 XAResource를 생성하여 Transaction Start를 수행합니다.

4번과 5번 사이에 실제 원하는 Query가 날라가게 됩니다.

5번에서는 Transaction End를 수행합니다.

6번에서는 Transaction 수행 결과에 따라 Commit 또는 Rollback을 수행합니다.

7번에서는 연결 Connection과 XAConnection을 close합니다.

 

다음으로 해당 OracleXARun.java를 호출할 jsp입니다.


[XATest.jsp]

<%@ page import="test.OracleXARun" language="java" contentType="text/html; charset=EUC-KR"
    pageEncoding="EUC-KR"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=EUC-KR">
<title>XA Test Page</title>
</head>
<body>

// 1
<form action="http://172.21.70.24:8180/1/OracleXARun" method="get">
        <input type="submit" value="Call Page">
</form>

</form>
</body>
</html>


위 jsp에서는 1번 구문에 내용이 담겨 있습니다. Call Page를 클릭하면 action submit이 수행되어 action에 등록된 page로 get방식 메소드를 호출합니다.

여기서 유의 할 점은 action탭에 LocalCall 즉 같은 JVM에 있는 페이지를 호출할 경우에는 /1/OracleXARun만 기입해도 무방하며, RemoteCall을 할 경우 http://172.21.70.24:8180/1/OracleXARun를 넣어주어야 합니다.

 

자 그럼 위 두가지 application Source가 컴파일 된 war package를 첨부하겠습니다.

12PCCommit.war

위 파일을 다운로드 받아 특정 경로에 압축해제한 후 WildFly에 deploy하면됩니다.

 

2. WildFly 구성

WildFly 구성은 별도로 셋팅할 부분은 없습니다.

일반적인 설정에 위에 첨부한 application Deploy 설정만 반영하면 됩니다.

standalone directory를 압축하여 첨부합니다.

standalone.jar

 

3. Database 구성


accountfrom table
SQL> create table accountfrom(accountno number, balance number, primary key(accountno));
Table created.
SQL> insert into accountfrom values(3000,30000);
1 row created.
SQL> select * from accountfrom;
 ACCOUNTNO    BALANCE
---------- ----------
      3000      30000
      1000      20000
 
accountto table
SQL> create table accountto (accountno number, balance number, primary key(accountno));
Table created.


위와 같이 테이블 구성 후 2pc test를 진행합니다.

 

Test Senario


1. OracleXARun이 호출 되도록 XATest.jsp를 호출합니다.

- 먼저 accountfrom table의 accountno가 3000인 row를 balance - 10을 update합니다.

- 이후 accountto table에 insert를 수행합니다.

2. 다시 OracleXARun이 호출 되도록 XATest.jsp를 재 호출합니다.

-  먼저 accountfrom table의 accountno가 3000인 row를 balance - 10을 update합니다.

- 이후 accountto table에 insert를 수행합니다.

- 이시점에 accountto table의 accountno가 primary key로 등록되어 PK 충돌에 의한 java.sql.SQLIntegrityConstraintViolationException 발생 여부를 확인합니다.

- 이후 accountfrom table에도 Rollback이 수행되어 최초 상태가 유지되는지 여부를 확인합니다.


 

자 그럼 테스트 결과를 확인해 보겠습니다.

먼저 첫번째 XATest.jsp 호출 결과입니다.

확인 과정은 Standard Out으로 출력한 서버 로그와 Oracle Table의 변경 여부를 가지고 판단하겠습니다.



위에서 보는 것과 같이 Context-path가 jboss-web.xml에 1로 등록되어 있어 https://ip:httpport/context-path/calljsp 순으로 이어져 있음을 확인할 수 있습니다.

Call Page를 클릭하면 function이 summit되어 java 파일이 수행됩니다.

결과는 다음과 같습니다.


2018-08-06 14:35:20,779 ERROR [io.undertow] (default I/O-4) UT005017: Unknown variable %{User-Agent}. For the literal percent character use two percent characters: '%'
2018-08-06 14:35:20,784 INFO  [stdout] (default task-1) ssss
2018-08-06 14:35:20,803 INFO  [stdout] (default task-1) Datasource from :oracle.jdbc.xa.client.OracleXADataSource@3817024c
2018-08-06 14:35:20,803 INFO  [stdout] (default task-1) Datasource to :oracle.jdbc.xa.client.OracleXADataSource@6188d4d0
2018-08-06 14:35:20,986 INFO  [stdout] (default task-1) Connection from :oracle.jdbc.driver.LogicalConnection@3629f54d
2018-08-06 14:35:20,986 INFO  [stdout] (default task-1) Connection to :oracle.jdbc.driver.LogicalConnection@47ad50d9
2018-08-06 14:35:21,018 INFO  [stdout] (default task-1) Statement 1 passed


중간 중간에 debug message를 심어 실제로 Transaction이 성공적으로 수행될 경우 Statement 1 passed라는 메시지를 찍도록 유도하였습니다.

Table Select 결과입니다.


SQL> select * from accountfrom;
 
 ACCOUNTNO    BALANCE
---------- ----------
      3000      29900

      1000      20000
 
SQL> select * from accountto;
 
 ACCOUNTNO    BALANCE
---------- ----------
       100        100


 

자 그럼 다음스탭으로 동일한 XATest.jsp 파일을 재 호출해 보도록 하겠습니다.

호출 결과입니다.


2018-08-06 13:41:13,410 ERROR [stderr] (default task-1) java.sql.SQLIntegrityConstraintViolationException: ORA-00001: ¿¿¿ ¿¿ ¿¿(SYSTEM.SYS_C0010169)¿ ¿¿¿¿¿
2018-08-06 13:41:13,410 ERROR [stderr] (default task-1)
2018-08-06 13:41:13,410 ERROR [stderr] (default task-1)         at oracle.jdbc.driver.T4CTTIoer.processError(T4CTTIoer.java:439)
2018-08-06 13:41:13,410 ERROR [stderr] (default task-1)         at oracle.jdbc.driver.T4CTTIoer.processError(T4CTTIoer.java:395)
2018-08-06 13:41:13,410 ERROR [stderr] (default task-1)         at oracle.jdbc.driver.T4C8Oall.processError(T4C8Oall.java:802)
2018-08-06 13:41:13,411 ERROR [stderr] (default task-1)         at oracle.jdbc.driver.T4CTTIfun.receive(T4CTTIfun.java:436)
2018-08-06 13:41:13,411 ERROR [stderr] (default task-1)         at oracle.jdbc.driver.T4CTTIfun.doRPC(T4CTTIfun.java:186)
2018-08-06 13:41:13,411 ERROR [stderr] (default task-1)         at oracle.jdbc.driver.T4C8Oall.doOALL(T4C8Oall.java:521)
2018-08-06 13:41:13,411 ERROR [stderr] (default task-1)         at oracle.jdbc.driver.T4CStatement.doOall8(T4CStatement.java:194)
2018-08-06 13:41:13,411 ERROR [stderr] (default task-1)         at oracle.jdbc.driver.T4CStatement.executeForRows(T4CStatement.java:1000)
2018-08-06 13:41:13,411 ERROR [stderr] (default task-1)         at oracle.jdbc.driver.OracleStatement.doExecuteWithTimeout(OracleStatement.java:1307)
2018-08-06 13:41:13,411 ERROR [stderr] (default task-1)         at oracle.jdbc.driver.OracleStatement.executeUpdateInternal(OracleStatement.java:1814)
2018-08-06 13:41:13,411 ERROR [stderr] (default task-1)         at oracle.jdbc.driver.OracleStatement.executeUpdate(OracleStatement.java:1779)
2018-08-06 13:41:13,411 ERROR [stderr] (default task-1)         at oracle.jdbc.driver.OracleStatementWrapper.executeUpdate(OracleStatementWrapper.java:277)
2018-08-06 13:41:13,411 ERROR [stderr] (default task-1)         at test.OracleXARun.XARunning(OracleXARun.java:95)
2018-08-06 13:41:13,412 ERROR [stderr] (default task-1)         at test.OracleXARun.doGet(OracleXARun.java:55)
2018-08-06 13:41:13,412 ERROR [stderr] (default task-1)         at javax.servlet.http.HttpServlet.service(HttpServlet.java:687)
2018-08-06 13:41:13,412 ERROR [stderr] (default task-1)         at javax.servlet.http.HttpServlet.service(HttpServlet.java:790)
2018-08-06 13:41:13,412 ERROR [stderr] (default task-1)         at io.undertow.servlet.handlers.ServletHandler.handleRequest(ServletHandler.java:74)

...

...


위와 같이 SQLIntegrityConstraintViolationException이 발생합니다.

이에 대한 결과를 다시한번 db table에서 조회해 보겠습니다.

Table Select 결과입니다.


SQL> select * from accountfrom;
 
 ACCOUNTNO    BALANCE
---------- ----------
      3000      29900

      1000      20000
 
SQL> select * from accountto;
 
 ACCOUNTNO    BALANCE
---------- ----------
       100        100


롤백이 수행되어 당연히 수행되어야 할 accountfrom까지 기존 호출상태로 변경된 것을 확인할 수 있습니다.

 

4. 결론 및 정리
일반적으로 transaction은 아래 와 같은 logic으로 처리 됩니다.


Begin Transaction
If(error) then rollback
Prepare transaction
If(prepare transaction failed) then rollback

Else commit transaction
End Transaction


이런 transaction 의 기본 logic을 이해한 상태에서, 본 sample test 문서를 활용하면 XADatasource 와 transaction의의 기본적인 관계와 개념을 이해하기 쉽습니다.  XA는 ejb모듈이나 tp에서 Global Transaction 을 통한 transaction 보장을 위해 주로 사용됩니다. 이와 연관된 부수적인 개념 부분은 본 문서에 많이 생략되었으나, 소스코드를 한번 실행해서 실제 어떻게 처리되는지를 확인 하는데 큰 도움이 될 것이라고 생각합니다.

고맙습니다.

728x90
반응형