我有一个有趣的用例我想知道是否有人有想法。
基本上,数据库中有一个基本上具有以下结构的通知表:
DROP TABLE IF EXISTS "notifications";
CREATE TABLE "notifications" (
"process_id" INT(11) NOT NULL DEFAULT '0',
"table_name" VARCHAR(40) NOT NULL,
"last_updated" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ,
"last_processed" TIMESTAMP NOT NULL,
PRIMARY KEY ("process_id")
);
INSERT INTO notifications (process_id, table_name, last_updated, last_processed) VALUES
(1, 'CASES_A', '2013-10-23 08:01:15+00:00','2013-10-22 08:30:22+00:00'),
(2, 'CASES_B', '2013-10-23 08:05:15+00:00','2013-10-22 08:05:15+00:00');
DROP TABLE IF EXISTS "CASES_A";
CREATE TABLE "CASES_A" (
"case_id" VARCHAR(25),
"case_date" TIMESTAMP NOT NULL,
PRIMARY KEY ("case_id")
);
INSERT into CASES_A (case_id, case_date) VALUES
('5000NQLj451NJ1', '2013-10-22 18:33:25+00:00'),
('5000NQLj4992F1', '2013-11-05 17:19:02+00:00'),
('5000NQLj8N9J11', '2013-11-06 08:03:08+00:00');
DROP TABLE IF EXISTS "CASES_B";
CREATE TABLE "CASES_B" (
"case_id" VARCHAR(25),
"case_date" TIMESTAMP NOT NULL,
PRIMARY KEY ("case_id")
);
INSERT into CASES_B (case_id, case_date) VALUES
('5000NQLk451NX4', '2013-10-21 10:23:26+00:00'),
('5000NQLk451NX5', '2013-10-20 11:10:25+00:00');
我们的想法是,我们调用通知表的检查(使用quartz完成生产),然后当有一个记录的最后更新日期大于上次处理日期时,我们读取table_name中的数据并处理记录,将它们路由到适当的队列,当我们记录了所有记录时,我们更新last_processed日期。
用例具有以下目标:
为了尝试实施此交易策略,我有以下路线和测试用例(注意我的缩写项目与问题无关。
package com.ea.wwce.camel.test.utilities;
import com.ea.wwce.camel.utilities.data.Record;
import com.ea.wwce.camel.utilities.data.RecordList;
import com.ea.wwce.camel.utilities.expressions.JodaDateTimeNow;
import com.ea.wwce.camel.utilities.jackson.RecordSerialization;
import com.ea.wwce.camel.utilities.transactions.TxnHelper;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import java.sql.Connection;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.jackson.JacksonDataFormat;
import org.apache.camel.component.mock.MockEndpoint;
import org.apache.camel.component.sql.SqlConstants;
import org.apache.camel.impl.JndiRegistry;
import org.testng.annotations.Test;
import static com.ea.wwce.camel.test.utilities.TransactionTestTools.*;
import static com.ea.wwce.camel.utilities.activemq.ActiveMQHelper.endpointAMQ;
import static com.ea.wwce.camel.utilities.jackson.RecordSerialization.toListOfJsonStrings;
import static org.apache.camel.ExchangePattern.InOnly;
/** Test Composite Transactions. */
public class CompositeTransactionTest extends AMQRouteTestSupport {
public static final String MOCK_NOTIFICATION_END = "mock:notification_end";
public static final String MOCK_AFTER_NOTIFICATION_SPLIT = "mock:after_notification_split";
public static final String MOCK_CASE_SPLIT_END = "mock:case_split_end";
public static final String MOCK_BEFORE_CASE_END = "mock:before_case_end";
public static final String MOCK_ROUTED = "mock:routed";
public static final String MOCK_AFTER_CASE_END = "mock:after_case_end";
public static final String QUEUE_DEAD = endpointAMQ("dead");
public static final String QUEUE_ROUTING = endpointAMQ("routing");
public static final String QUEUE_TRIGGER = endpointAMQ("trigger");
public static final String DIRECT_DO_CASE = "direct:do_case";
public static final String DIRECT_DO_NOTIFICATION = "direct:do_notification";
/** The database support object. */
private H2DatabaseSupport dbSupport = createDBSupport(DBOnlyTransactionTest.class.getSimpleName());
/** Jackson data format. */
private JacksonDataFormat df = new JacksonDataFormat(createMapper(), Record.class);
/** Jackson Mapper. */
private ObjectMapper mapper = createMapper();
/** Mock Endpoints. */
private MockEndpoint mockDead, mockBeforeCaseEnd, mockAfterNotificationSplit, mockNotificationEnd, mockCaseSplitEnd, mockRouted, mockAfterCaseEnd;
/** Creates a jackson mapper. */
private static ObjectMapper createMapper() {
final JsonFactory factory = new JsonFactory();
factory.enable(JsonParser.Feature.ALLOW_COMMENTS);
final ObjectMapper mapper = new ObjectMapper(factory);
mapper.registerModule(RecordSerialization.createJacksonModule(Record.class, null));
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
mapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
return mapper;
}
public void initMocks() {
mockDead = assertAndGetMockEndpoint(MOCK_DEAD);
mockRouted = assertAndGetMockEndpoint(MOCK_ROUTED);
mockBeforeCaseEnd = assertAndGetMockEndpoint(MOCK_BEFORE_CASE_END);
mockAfterCaseEnd = assertAndGetMockEndpoint(MOCK_AFTER_CASE_END);
mockAfterNotificationSplit = assertAndGetMockEndpoint(MOCK_AFTER_NOTIFICATION_SPLIT);
mockNotificationEnd = assertAndGetMockEndpoint(MOCK_NOTIFICATION_END);
mockCaseSplitEnd = assertAndGetMockEndpoint(MOCK_CASE_SPLIT_END);
}
@Override
protected void afterRegistryCreation(final JndiRegistry registry) throws Exception {
dbSupport.registerNewDatasource(registry, DS_JNDI_KEY);
}
@Override
protected RouteBuilder createRouteBuilder() {
System.out.println("createRouteBuilder");
return new RouteBuilder(this.context) {
@Override
public void configure() {
context.setTracing(true);
from(DIRECT_START).to(QUEUE_TRIGGER);
from(QUEUE_TRIGGER).routeId(ROUTE_ID_TEST_ROUTE)
.onException(RuntimeException.class).handled(true).useOriginalMessage().marshal(df).to(QUEUE_DEAD).markRollbackOnly().end()
.transacted(TxnHelper.KEY_TXNPOLICY_REQUIRED)
.setHeader(SqlConstants.SQL_QUERY, simple(SQL_CHECK))
.to("sql:?dataSource=" + DS_JNDI_KEY)
.convertBodyTo(RecordList.class)
.split(body()) // Split checks
.to(DIRECT_DO_NOTIFICATION)
.end(); // end spit of checks;
from(DIRECT_DO_NOTIFICATION)
.onException(RuntimeException.class).handled(true).useOriginalMessage().marshal(df).to(QUEUE_DEAD).markRollbackOnly().end()
.transacted(TxnHelper.KEY_TXNPOLICY_REQUIRED)
.to(MOCK_AFTER_NOTIFICATION_SPLIT)
.setHeader(HDR_PROC, simple("${body[process_id]}"))
.setHeader(HDR_NOW, new JodaDateTimeNow("GMT", "yyyy-MM-dd HH:mm:ss+00:00"))
.setHeader(HDR_TABLE, simple("${body[table_name]}"))
.setHeader(SqlConstants.SQL_QUERY, simple(SQL_CASES))
.to("sql:?dataSource=" + DS_JNDI_KEY)
.convertBodyTo(RecordList.class)
.split(body()) // split cases
.to(DIRECT_DO_CASE)
.end() // end split of process cases
.to(MOCK_CASE_SPLIT_END)
.setHeader(SqlConstants.SQL_QUERY, simple(SQL_UPDATE))
.to("sql:?dataSource=" + DS_JNDI_KEY)
.to(MOCK_NOTIFICATION_END);
from(DIRECT_DO_CASE)
.onException(RuntimeException.class).handled(true).useOriginalMessage().marshal(df).to(QUEUE_DEAD).end()
.transacted(TxnHelper.KEY_TXNPOLICY_REQUIRED)
.marshal(df)
.to(MOCK_BEFORE_CASE_END)
.to(QUEUE_ROUTING)
.to(MOCK_AFTER_CASE_END);
from(QUEUE_ROUTING).to(MOCK_ROUTED);
from(QUEUE_DEAD).to(MOCK_DEAD);
}
};
}
@Test
public void testSingleCaseFailIsIsolatedAfterEnqueue() throws Exception {
try (final Connection connection = dbSupport.connectH2WithInitScripts()) {
startCamelContext();
initMocks();
final RecordList notifications = notifications();
final RecordList casesA = casesA();
final RecordList casesB = casesB();
mockAfterNotificationSplit.expectedBodiesReceivedInAnyOrder(notifications);
//noinspection RedundantCast
mockCaseSplitEnd.expectedBodiesReceivedInAnyOrder((Object) casesA, (Object) casesB);
mockRouted.expectedBodiesReceivedInAnyOrder(toListOfJsonStrings(mapper, casesA.get(1), casesA.get(2), casesB.get(0), casesB.get(1)));
mockBeforeCaseEnd.expectedMessageCount(5);
mockAfterCaseEnd.whenExchangeReceived(1, EXCEPTION_PROCESSOR);
//noinspection RedundantCast
mockNotificationEnd.expectedBodiesReceivedInAnyOrder((Object) casesA, (Object) casesB);
mockDead.expectedMessageCount(1);
mockDead.expectedBodiesReceived(toListOfJsonStrings(mapper, casesA().get(0)));
template.sendBody(DIRECT_START, ""); // trigger process
mockBeforeCaseEnd.assertIsSatisfied();
mockAfterNotificationSplit.assertIsSatisfied();
mockDead.assertIsSatisfied();
mockRouted.assertIsSatisfied();
mockNotificationEnd.assertIsSatisfied();
assertFalse(isProcessTimeUnchanged(connection, notifications().get(0)));
assertFalse(isProcessTimeUnchanged(connection, notifications().get(1)));
}
}
}
问题是测试用例testSingleCaseFailIsIsolatedAfterEnqueue不起作用。测试无法回滚入队的消息,似乎消息在事务提交之前已经路由。有没有人知道如何解决这个问题?我一直在为这个测试案例投入大量精力,我想知道AMQ似乎没有参与交易,但是只有AMQ的测试用例,而不是数据库正常工作。
非常感谢您的意见或建议。
修改
有趣的是,似乎AMQ没有参与JTA交易。我不知道为什么会发生这种情况。以下是事务管理器的设置:
final Properties txnSvcProps = new Properties();
txnSvcProps.setProperty("com.atomikos.icatch.service", "com.atomikos.icatch.standalone.UserTransactionServiceFactory");
txnSvcProps.setProperty("com.atomikos.icatch.output_dir", "./target/atomikos/");
txnSvcProps.setProperty("com.atomikos.icatch.log_base_dir", "./target/atomikos/");
txnSvcProps.setProperty("com.atomikos.icatch.console_log_level", "DEBUG");
txnSvcProps.setProperty("com.atomikos.icatch.tm_unique_name", this.txnManagerServiceName);
/* The Atomikos user transaction service. */
atomikosUserTxnService = new UserTransactionServiceImp(txnSvcProps);
atomikosUserTxnService.init();
this.atomikosTxMgr = new UserTransactionManager();
this.atomikosTxMgr.setStartupTransactionService(false); // already started above.
this.atomikosTxMgr.setForceShutdown(false);
try {
this.atomikosTxMgr.init();
} catch (final SystemException ex) {
throw new AssertionError("Error initializing JTA transaction manager", ex);
}
this.userTxn = new UserTransactionImp();
这是AMQ设置:
final ActiveMQXAConnectionFactory amqcf = registry.lookupByNameAndType(amqCFJndiName, ActiveMQXAConnectionFactory.class);
final ActiveMQComponent amq = (ActiveMQComponent) context.getComponent("activemq");
amq.setConnectionFactory(amqcf);
// -- Set Transaction manager because we will be using transacted(); note that this will find the correct one on the host platform.
amq.setTransactionManager(txnMgr);
// -- If we are using a JMSTransactionManager instance we have to set the connection factory in the manager.
if (txnMgr instanceof JmsTransactionManager) ((JmsTransactionManager) txnMgr).setConnectionFactory(amqcf);
答案 0 :(得分:0)
尝试在processEvent路由中交换transacted / onException的顺序。有一条规则说(Camel inAction§9.2.2):
在Java DSL中使用transacted()时,必须立即添加它 from()以确保正确配置路由以使用 交易。
您是否使用JTA事务管理器并且ActiveMQ也参与该事务?
答案 1 :(得分:0)
尝试按照指定here将.end()
添加到onException()
子句。
答案 2 :(得分:0)
我发现了问题。使用Atomikos时,您必须将连接工厂包装在自己的bean中。添加这样的代码可以解决问题:
@Override
protected CamelContext createCamelContext() throws Exception {
final CamelContext camelContext = super.createCamelContext();
if (log.isDebugEnabled()) log.debug("Registering AMQ connection factory using key: {}", CONNECTION_FACTORY_JNDI_NAME);
// -- create a connection factory proxy for atomikos.
final AtomikosConnectionFactoryBean acfb = new AtomikosConnectionFactoryBean();
acfb.setUniqueResourceName("amq-embedded");
acfb.setXaConnectionFactory(brokerSupport.amqcf);
acfb.setMaxPoolSize(10);
acfb.init();
registry().bind(CONNECTION_FACTORY_JNDI_NAME, acfb);
ActiveMQHelper.configureActiveMQ(camelContext, registry(), fetchPlatformTxnMgr());
return camelContext;
}
此代码将连接工厂包装在atomikos bean中,从而解决了现在当路由查找连接工厂时它们获取bean并且所有内容都已排序的问题。