javatransactionswebspherexa

If in a transaction we do write only on one data source, is it possibile to avoid 2PC or handle commint manually? (J2CA0030E)


We are migrating an application from IBM WebSphere Traditiona 8.5 to OpenLiberty 19.0.0.12 (Both with IBM JDK 8).

In the process we want to get rid of XA transactions, are there is only one method that is actually using two different data sources, but only write to second data source (first one is used only for reading).

So imagine something like that :

@Path("/path_to_my_service")
@Stateless
public class MyService extends ByBaseService {

  private static final long serialVersionUID = 3470660101451196317L;

  @POST
  @Consumes(MediaType.APPLICATION_JSON)
  @Produces(MediaType.APPLICATION_JSON)
  public Response create(Model model) {
    try ( Connection connectionFromDataSourceOne = ...;
         Connection connectionFromDataSourceTwo = ... ) {
    // performs some reading from connectionFromDataSourceOne
    // try to performe writing on connectionFromDataSourceTwo
    ) catch () {
        ...
    } finally {
        ...
    }
  }

}

Keep in mind that there are various read (from ds1) and write (to ds2) but they are mixed in a complex method, so splitting the transaction would need a deep refactoring that we would avoid right now.

But we get this error :

J2CA0030E: Method enlist caught java.lang.IllegalStateException: Illegal attempt to enlist multiple 1PC XAResources

Is there any way to telle transaction manager 2PC is not needed without any major refactoring of the code?

Thanks in advance.

Edit after solution was found (2020-01-11):

We created before a little POC of what we needed and then we tried to solution of just changing transaction management :

@TransactionManagement(TransactionManagementType.BEAN)

Then we managed to apply it to the real usecase and seems all is warking properly. We post the POC in case could be useful to some for testing :

@Stateless
@Path("/test/transaction")
@TransactionManagement(TransactionManagementType.BEAN)
public class TransactionTestRest implements Serializable {

private static final long serialVersionUID = -2963030487875284408L;

private static final Logger logger = LoggerFactory.getLogger( TransactionTestRest.class );

@Resource(lookup = DataConsts.DS1_JNDI, name = DataConsts.DS1_NAME )
private DataSource ds1;

@Resource(lookup = DataConsts.DS2_JNDI, name = DataConsts.DS2_NAME )
private DataSource ds2;

private Properties baseProperties() {
    Properties props = new Properties();
    props.setProperty( "testVersion" , "3" );
    return props;
}

@GET
@Path("/conntest_all")
@Produces(MediaType.APPLICATION_JSON)
public Response testAll() {
    Response res = null;
    try ( Connection conn1 = this.ds1.getConnection();
            Connection conn2 = this.ds2.getConnection() ) {
        Properties props = this.baseProperties();
        props.setProperty( "testConn1" , conn1.getMetaData().getUserName() );
        props.setProperty( "testConn2" , conn2.getMetaData().getUserName() );
        conn2.setAutoCommit( false );   
        try ( Statement stm1 = conn1.createStatement();
                ResultSet rs1 = stm1.executeQuery( "SELECT * FROM test_ds1" );
                PreparedStatement pstm2 = conn2.prepareStatement( "INSERT INTO test_ds2 ( ID ) VALUES ( ? )" ) ) {
            while ( rs1.next() ) {
                BigDecimal id = rs1.getBigDecimal( "ID" );
                pstm2.setBigDecimal( 1 , id );
                pstm2.executeUpdate();
            }
            conn2.commit();
            props.setProperty( "result" , "OK!");
        } catch (Exception ie)  {
            props.setProperty( "result" , "Error:"+ie );
            conn2.rollback();
        } finally {
            conn2.setAutoCommit( true );
        }
        res =  Response.ok( props ).build();
    } catch (Exception e) {
        logger.error( "Error on conntest_all "+e, e );
        res = Response.status( Response.Status.INTERNAL_SERVER_ERROR ).build();
    }

    return res;
}

@GET
@Path("/conntest_1")
@Produces(MediaType.APPLICATION_JSON)
public Response test1() {
    Response res = null;
    try ( Connection conn1 = this.ds1.getConnection();) {
        Properties props = this.baseProperties();
        props.setProperty( "testConn1" , conn1.getMetaData().getUserName() );
        res =  Response.ok( props ).build();
    } catch (Exception e) {
        logger.error( "Error on conntest_1 "+e, e );
        res = Response.status( Response.Status.INTERNAL_SERVER_ERROR ).build();
    }
    return res;
}

@GET
@Path("/conntest_2")
@Produces(MediaType.APPLICATION_JSON)
public Response test2() {
    Response res = null;
    try ( Connection conn2 = this.ds2.getConnection();) {
        Properties props = this.baseProperties();
        props.setProperty( "testConn2" , conn2.getMetaData().getUserName() );
        res =  Response.ok( props ).build();
    } catch (Exception e) {
        logger.error( "Error on conntest_2 "+e, e );
        res = Response.status( Response.Status.INTERNAL_SERVER_ERROR ).build();
    }
    return res;
}   

}


Solution

  • Based on the @Stateless annotation, it looks like you are using a stateless session EJB and likely ending up with a container managed transaction around both connections, causing there to be two resources within a single transaction, which requires two phase commit.

    If having these connections within the same transaction is not desired, consider switching from container managed transactions to bean managed transactions, in which case your application gets to decide when and where to begin/commit.

    import javax.ejb.TransactionManagement;
    import javax.ejb.TransactionManagementType;
    ...
    @Stateless
    @TransactionManagement(TransactionManagementType.BEAN)
    public class MyService extends ByBaseService {
    ...
    

    After doing this, the remaining code might be fine as is (relying on autocommit from the JDBC driver) or you might wish to decide where to manually begin and end transactions as needed in the app code (not shown).