OPEN - issue BATCH-207: exit_message too short for holding long exceptions

http://opensource.atlassian.com/projects/spring/browse/BATCH-207

Added onFlushDirty to the entity interceptor in hibernate (and changed its name so not specific to entity name - BatchHibernateInterceptor).  Also truncating in Sql*Dao.  Only exit description is given special treatment for now, and the length is fixed at 250 - hard-coded and tightly coupled with the hibernate mapping and DDL).  Should be good enough for most scenarios.
This commit is contained in:
dsyer
2007-11-21 12:34:57 +00:00
parent 6d6b8a3005
commit edff6be9b3
16 changed files with 312 additions and 101 deletions

View File

@@ -0,0 +1,115 @@
package org.springframework.batch.execution.repository.dao;
import java.io.Serializable;
import org.hibernate.EmptyInterceptor;
import org.hibernate.type.Type;
import org.springframework.batch.core.domain.JobExecution;
import org.springframework.batch.core.domain.JobIdentifier;
import org.springframework.batch.core.domain.JobInstance;
import org.springframework.batch.core.domain.StepExecution;
import org.springframework.batch.repeat.ExitStatus;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
/**
* Hibernate interceptor for batch meta data. It can distinguish between the
* various {@link JobIdentifier} strategies in a {@link JobInstance}, and can
* truncate the exit descriptions. Its main task is to return the correct entity
* name for a {@link JobInstance} based on the type of {@link JobIdentifier}
* used. There is necessarily some tight coupling between this and the Hibernate
* mappings for {@link JobInstance} because the map from {@link JobIdentifier}
* type to entity name is in both places.
*
* @author Dave Syer
*
*/
public class BatchHibernateInterceptor extends EmptyInterceptor implements
InitializingBean {
/**
*
*/
private static final int EXIT_MESSAGE_LENGTH = 250;
private EntityNameLocator entityNameLocator;
/**
* Public setter for the {@link EntityNameLocator} property.
*
* @param entityNameLocator
* the entityNameLocator to set
*/
public void setEntityNameLocator(EntityNameLocator entityNameLocator) {
this.entityNameLocator = entityNameLocator;
}
/**
* Check mandatory properties ({@link #entityNameLocator}).
*
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
public void afterPropertiesSet() throws Exception {
Assert
.notNull(entityNameLocator,
"EntityNameLocator must be provided.");
}
/**
* If the object is a {@link JobInstance} search the identifier types for an
* entity name based on the {@link JobIdentifier} type. Fall back to
* SimpleJobIdentifier if the value is not found.
*
* @see org.hibernate.EmptyInterceptor#getEntityName(java.lang.Object)
*/
public String getEntityName(Object object) {
if (object instanceof JobInstance) {
JobInstance instance = (JobInstance) object;
return entityNameLocator
.locate(instance.getIdentifier().getClass());
}
return super.getEntityName(object);
}
/**
* Ensure that {@link JobExecution} and {@link StepExecution} have exit
* status with legal description (not too long).
*
* @see org.hibernate.EmptyInterceptor#onFlushDirty(java.lang.Object,
* java.io.Serializable, java.lang.Object[], java.lang.Object[],
* java.lang.String[], org.hibernate.type.Type[])
*/
public boolean onFlushDirty(Object entity, Serializable id,
Object[] currentState, Object[] previousState,
String[] propertyNames, Type[] types) {
if (entity instanceof StepExecution || entity instanceof JobExecution) {
int index = findExitStatus(propertyNames);
ExitStatus status = (ExitStatus) currentState[index];
String description = status == null ? "" : status
.getExitDescription();
if (description.length() > EXIT_MESSAGE_LENGTH) {
status = status.addExitDescription(description.substring(0,
EXIT_MESSAGE_LENGTH));
currentState[index] = status;
// state was modified...
return true;
}
}
return false;
}
/**
* @param propertyNames
* @return
*/
private int findExitStatus(String[] propertyNames) {
for (int i = 0; i < propertyNames.length; i++) {
if ("exitStatus".equals(propertyNames[i])) {
return i;
}
}
return -1;
}
}

View File

@@ -1,58 +0,0 @@
package org.springframework.batch.execution.repository.dao;
import org.hibernate.EmptyInterceptor;
import org.springframework.batch.core.domain.JobIdentifier;
import org.springframework.batch.core.domain.JobInstance;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
/**
* Hibernate interceptor that can distinguish between the various
* {@link JobIdentifier} strategies in a {@link JobInstance}. Its task is to
* return the correct entity name based on the type of {@link JobIdentifier}
* used. There is necessarily some tight coupling between this and the Hibernate
* mappings for {@link JobInstance} because the map from {@link JobIdentifier}
* type to entity name is in both places.
*
*
* @author Dave Syer
*
*/
public class EntityNameInterceptor extends EmptyInterceptor implements InitializingBean {
private EntityNameLocator entityNameLocator;
/**
* Public setter for the {@link EntityNameLocator} property.
*
* @param entityNameLocator the entityNameLocator to set
*/
public void setEntityNameLocator(EntityNameLocator entityNameLocator) {
this.entityNameLocator = entityNameLocator;
}
/**
* Check mandatory properties ({@link #entityNameLocator}).
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
public void afterPropertiesSet() throws Exception {
Assert.notNull(entityNameLocator, "EntityNameLocator must be provided.");
}
/**
* If the object is a {@link JobInstance} search the identifier types for an
* entity name based on the {@link JobIdentifier} type. Fall back to
* SimpleJobIdentifier if the value is not found.
*
* @see org.hibernate.EmptyInterceptor#getEntityName(java.lang.Object)
*/
public String getEntityName(Object object) {
if (object instanceof JobInstance) {
JobInstance instance = (JobInstance) object;
return entityNameLocator
.locate(instance.getIdentifier().getClass());
}
return super.getEntityName(object);
}
}

View File

@@ -21,6 +21,8 @@ import java.sql.SQLException;
import java.sql.Types;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.batch.core.domain.BatchStatus;
import org.springframework.batch.core.domain.JobExecution;
import org.springframework.batch.core.domain.JobIdentifier;
@@ -49,6 +51,8 @@ import org.springframework.util.StringUtils;
* @author Dave Syer
*/
public class SqlJobDao implements JobDao, InitializingBean {
protected static final Log logger = LogFactory.getLog(SqlJobDao.class);
/**
* Default value for the table prefix property.
@@ -78,6 +82,8 @@ public class SqlJobDao implements JobDao, InitializingBean {
private static final String CHECK_JOB_EXECUTION_EXISTS = "SELECT COUNT(*) FROM %PREFIX%JOB_EXECUTION WHERE ID=?";
private static final int EXIT_MESSAGE_LENGTH = 250;
private JdbcTemplate jdbcTemplate;
private DataFieldMaxValueIncrementer jobIncrementer;
@@ -207,11 +213,16 @@ public class SqlJobDao implements JobDao, InitializingBean {
validateJobExecution(jobExecution);
String exitDescription = jobExecution.getExitStatus().getExitDescription();
if (exitDescription!=null && exitDescription.length()>EXIT_MESSAGE_LENGTH) {
exitDescription = exitDescription.substring(0, EXIT_MESSAGE_LENGTH);
logger.debug("Truncating long message before update of JobExecution: "+jobExecution);
}
Object[] parameters = new Object[] { jobExecution.getStartTime(),
jobExecution.getEndTime(), jobExecution.getStatus().toString(),
jobExecution.getExitStatus().isContinuable() ? "Y" : "N",
jobExecution.getExitStatus().getExitCode(),
jobExecution.getExitStatus().getExitDescription(),
exitDescription,
jobExecution.getId() };
if (jobExecution.getId() == null) {

View File

@@ -22,6 +22,8 @@ import java.sql.Types;
import java.util.List;
import java.util.Properties;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.batch.core.domain.BatchStatus;
import org.springframework.batch.core.domain.JobExecution;
import org.springframework.batch.core.domain.JobInstance;
@@ -55,10 +57,13 @@ import org.springframework.util.StringUtils;
* on the step dao java docs as well.
*
* @author Lucas Ward
* @author Dave Syer
* @see StepDao
*/
public class SqlStepDao implements StepDao, InitializingBean {
protected static final Log logger = LogFactory.getLog(SqlStepDao.class);
// Step SQL statements
private static final String FIND_STEPS = "SELECT ID, STEP_NAME, STATUS, RESTART_DATA from %PREFIX%STEP where JOB_ID = ?";
@@ -84,6 +89,8 @@ public class SqlStepDao implements StepDao, InitializingBean {
private static final String FIND_STEP_EXECUTIONS = "SELECT ID, JOB_EXECUTION_ID, START_TIME, END_TIME, STATUS, COMMIT_COUNT,"
+ " TASK_COUNT, TASK_STATISTICS, CONTINUABLE, EXIT_CODE, EXIT_MESSAGE from %PREFIX%STEP_EXECUTION where STEP_ID = ?";
private static final int EXIT_MESSAGE_LENGTH = 250;
private JdbcOperations jdbcTemplate;
private JobDao jobDao;
@@ -314,6 +321,12 @@ public class SqlStepDao implements StepDao, InitializingBean {
// return; // throw exception?
// }
String exitDescription = stepExecution.getExitStatus().getExitDescription();
if (exitDescription!=null && exitDescription.length()>EXIT_MESSAGE_LENGTH) {
exitDescription = exitDescription.substring(0, EXIT_MESSAGE_LENGTH);
logger.debug("Truncating long message before update of StepExecution: "+stepExecution);
}
Object[] parameters = new Object[] {
stepExecution.getStartTime(),
stepExecution.getEndTime(),
@@ -324,7 +337,7 @@ public class SqlStepDao implements StepDao, InitializingBean {
.getStatistics()),
stepExecution.getExitStatus().isContinuable() ? "Y" : "N",
stepExecution.getExitStatus().getExitCode(),
stepExecution.getExitStatus().getExitDescription(),
exitDescription,
stepExecution.getId() };
jdbcTemplate
.update(getQuery(UPDATE_STEP_EXECUTION), parameters,

View File

@@ -15,11 +15,11 @@
<many-to-one name="job" cascade="all" update="false" column="JOB_ID" access="field"/>
<property name="startTime" column="START_TIME" />
<property name="endTime" column="END_TIME" />
<property name="status" type="org.springframework.batch.execution.repository.dao.BatchStatusUserType" column="STATUS" />
<property name="status" type="org.springframework.batch.execution.repository.dao.BatchStatusUserType" column="STATUS" length="10"/>
<property name="exitStatus" type="org.springframework.batch.execution.repository.dao.ExitStatusUserType">
<column name="CONTINUABLE"/>
<column name="EXIT_CODE"/>
<column name="EXIT_MESSAGE"/>
<column name="EXIT_CODE" length="20"/>
<column name="EXIT_MESSAGE" length="250"/>
</property>
</class>

View File

@@ -16,14 +16,14 @@
<many-to-one name="jobExecution" cascade="all" update="false" column="JOB_EXECUTION_ID" access="field"/>
<property name="startTime" column="START_TIME" />
<property name="endTime" column="END_TIME" />
<property name="status" type="org.springframework.batch.execution.repository.dao.BatchStatusUserType" column="STATUS" />
<property name="status" type="org.springframework.batch.execution.repository.dao.BatchStatusUserType" column="STATUS" length="10"/>
<property name="commitCount" column="COMMIT_COUNT" />
<property name="taskCount" column="TASK_COUNT" />
<property name="statistics" type="org.springframework.batch.execution.repository.dao.PropertiesUserType" column="TASK_STATISTICS" />
<property name="statistics" type="org.springframework.batch.execution.repository.dao.PropertiesUserType" column="TASK_STATISTICS" length="250"/>
<property name="exitStatus" type="org.springframework.batch.execution.repository.dao.ExitStatusUserType">
<column name="CONTINUABLE"/>
<column name="EXIT_CODE"/>
<column name="EXIT_MESSAGE"/>
<column name="EXIT_CODE" length="20"/>
<column name="EXIT_MESSAGE" length="250"/>
</property>
</class>

View File

@@ -16,8 +16,8 @@
<property name="name" update="false" column="STEP_NAME" access="field"/>
<property name="restartData"
type="org.springframework.batch.execution.repository.dao.RestartDataUserType" insert="false"
column="RESTART_DATA" />
column="RESTART_DATA" length="250"/>
<property name="status" type="org.springframework.batch.execution.repository.dao.BatchStatusUserType"
insert="false" column="STATUS" />
insert="false" column="STATUS" length="10"/>
</class>
</hibernate-mapping>

View File

@@ -0,0 +1,82 @@
package org.springframework.batch.execution.repository.dao;
import junit.framework.TestCase;
import org.hibernate.type.Type;
import org.springframework.batch.core.domain.JobExecution;
import org.springframework.batch.core.domain.JobInstance;
import org.springframework.batch.execution.runtime.ScheduledJobIdentifier;
import org.springframework.batch.repeat.ExitStatus;
public class BatchHibernateInterceptorTests extends TestCase {
public static final String LONG_STRING = "A very long description: \n" +
"Nov 16 20:10:42 util.JDBCExceptionReporter 78 - Data truncation: Data too long for column 'exit_message' at row 1\n" +
"Nov 16 20:10:43 def.AbstractFlushingEventListener 301 - Could not synchronize database state with session \n" +
"org.hibernate.exception.DataException: could not update: [org.springframework.batch.core.domain.JobExecution#67]\n" +
" at org.hibernate.exception.SQLStateConverter.convert(SQLStateConverter.java:77)\n";
private BatchHibernateInterceptor interceptor = new BatchHibernateInterceptor();
/* (non-Javadoc)
* @see junit.framework.TestCase#setUp()
*/
protected void setUp() throws Exception {
super.setUp();
interceptor.setEntityNameLocator(new EntityNameLocator() {
public String locate(Class clz) {
return "foo";
}
});
}
public void testAfterPropertiesSet() throws Exception {
interceptor = new BatchHibernateInterceptor();
try {
interceptor.afterPropertiesSet();
fail("Expected IllegalArgumentException");
} catch (IllegalArgumentException e) {
// expected
}
}
public void testGetEntityName() {
JobInstance job = new JobInstance(new ScheduledJobIdentifier("foo"));
assertEquals("foo", interceptor.getEntityName(job));
}
public void testGetEntityNameForNonJobInstance() {
Object job = new Object();
assertEquals(null, interceptor.getEntityName(job));
}
public void testOnFlushDirtyWithJobInstance() throws Exception {
JobInstance job = new JobInstance(new ScheduledJobIdentifier("foo"));
assertFalse(interceptor.onFlushDirty(job, null, new Object[1], new Object[1], new String[] {"exitStatus"}, new Type[1]));
}
public void testOnFlushDirtyWithShortDescription() throws Exception {
JobExecution execution = new JobExecution(new JobInstance(new ScheduledJobIdentifier("foo")));
execution.setExitStatus(ExitStatus.UNKNOWN.addExitDescription("bar"));
assertFalse(interceptor.onFlushDirty(execution, null, new Object[] {execution.getExitStatus()}, new Object[1], new String[] {"exitStatus"}, new Type[1]));
}
public void testOnFlushDirtyWithLongDescription() throws Exception {
JobExecution execution = new JobExecution(new JobInstance(new ScheduledJobIdentifier("foo")));
execution.setExitStatus(ExitStatus.UNKNOWN.addExitDescription(LONG_STRING));
Object[] state = new Object[] {execution.getExitStatus()};
assertTrue(interceptor.onFlushDirty(execution, null, state, new Object[1], new String[] {"exitStatus"}, new Type[1]));
assertEquals(250, ((ExitStatus) state[0]).getExitDescription().length());
}
public void testOnFlushDirtyWithMissingExitStatusProperty() throws Exception {
JobExecution execution = new JobExecution(new JobInstance(new ScheduledJobIdentifier("foo")));
execution.setExitStatus(ExitStatus.UNKNOWN.addExitDescription("bar"));
try {
interceptor.onFlushDirty(execution, null, new Object[] {execution.getExitStatus()}, new Object[1], new String[1], new Type[1]);
fail("Expected ArrayIndexOutOfBoundsException");
} catch (ArrayIndexOutOfBoundsException e) {
// expected;
}
}
}

View File

@@ -1,29 +0,0 @@
package org.springframework.batch.execution.repository.dao;
import junit.framework.TestCase;
import org.springframework.batch.core.domain.JobInstance;
import org.springframework.batch.execution.runtime.ScheduledJobIdentifier;
public class EntityNameInterceptorTests extends TestCase {
private EntityNameInterceptor interceptor = new EntityNameInterceptor();
/* (non-Javadoc)
* @see junit.framework.TestCase#setUp()
*/
protected void setUp() throws Exception {
super.setUp();
interceptor.setEntityNameLocator(new EntityNameLocator() {
public String locate(Class clz) {
return "foo";
}
});
}
public void testGetEntityName() {
JobInstance job = new JobInstance(new ScheduledJobIdentifier("foo"));
assertEquals("foo", interceptor.getEntityName(job));
}
}

View File

@@ -26,10 +26,13 @@ import org.springframework.batch.core.domain.JobIdentifier;
import org.springframework.batch.core.domain.JobInstance;
import org.springframework.batch.core.runtime.SimpleJobIdentifier;
import org.springframework.batch.execution.runtime.ScheduledJobIdentifier;
import org.springframework.batch.repeat.ExitStatus;
import org.springframework.util.ClassUtils;
public class HibernateJobDaoTests extends AbstractJobDaoTests {
private static final String LONG_STRING = BatchHibernateInterceptorTests.LONG_STRING;
private SessionFactory sessionFactory;
protected String[] getConfigLocations() {
@@ -89,6 +92,22 @@ public class HibernateJobDaoTests extends AbstractJobDaoTests {
assertEquals(simpleJob, testJob);
}
public void testUpdateJobExecutionWithLongExitCode() {
assertTrue(LONG_STRING.length()>250);
jobExecution.setExitStatus(ExitStatus.FINISHED.addExitDescription(LONG_STRING));
jobDao.update(jobExecution);
sessionFactory.getCurrentSession().flush();
List executions = jdbcTemplate.queryForList(
"SELECT * FROM BATCH_JOB_EXECUTION where JOB_ID=?",
new Object[] { job.getId() });
assertEquals(1, executions.size());
assertEquals(LONG_STRING.substring(0, 250), ((Map) executions.get(0))
.get("EXIT_MESSAGE"));
}
public void testNullIdentifierName() {
JobIdentifier simpleIdentifier = new SimpleJobIdentifier(null);

View File

@@ -26,11 +26,13 @@ import org.springframework.batch.core.domain.BatchStatus;
import org.springframework.batch.core.domain.JobExecution;
import org.springframework.batch.core.domain.StepExecution;
import org.springframework.batch.core.domain.StepInstance;
import org.springframework.batch.repeat.ExitStatus;
import org.springframework.batch.support.PropertiesConverter;
import org.springframework.util.ClassUtils;
public class HibernateStepDaoTests extends AbstractStepDaoTests {
private static final String LONG_STRING = BatchHibernateInterceptorTests.LONG_STRING;
private SessionFactory sessionFactory;
public void setSessionFactory(SessionFactory sessionFactory) {
@@ -78,5 +80,21 @@ public class HibernateStepDaoTests extends AbstractStepDaoTests {
}
public void testUpdateStepExecutionWithLongDescription() {
assertTrue(LONG_STRING.length()>250);
stepExecution.setExitStatus(ExitStatus.FINISHED.addExitDescription(LONG_STRING));
stepDao.update(stepExecution);
sessionFactory.getCurrentSession().flush();
List executions = jdbcTemplate.queryForList(
"SELECT * FROM BATCH_STEP_EXECUTION where STEP_ID=?",
new Object[] { step1.getId() });
assertEquals(1, executions.size());
assertEquals(LONG_STRING.substring(0, 250), ((Map) executions.get(0))
.get("EXIT_MESSAGE"));
}
}

View File

@@ -1,10 +1,16 @@
package org.springframework.batch.execution.repository.dao;
import java.util.List;
import java.util.Map;
import org.springframework.batch.repeat.ExitStatus;
import org.springframework.dao.DataAccessException;
public class SqlJobDaoTests extends AbstractJobDaoTests {
private static final String LONG_STRING = BatchHibernateInterceptorTests.LONG_STRING;
protected void onSetUpBeforeTransaction() throws Exception {
((SqlJobDao) jobDao).setTablePrefix(SqlJobDao.DEFAULT_TABLE_PREFIX);
}
@@ -19,4 +25,18 @@ public class SqlJobDaoTests extends AbstractJobDaoTests {
}
}
public void testUpdateJobExecutionWithLongExitCode() {
assertTrue(LONG_STRING.length()>250);
jobExecution.setExitStatus(ExitStatus.FINISHED.addExitDescription(LONG_STRING));
jobDao.update(jobExecution);
List executions = jdbcTemplate.queryForList(
"SELECT * FROM BATCH_JOB_EXECUTION where JOB_ID=?",
new Object[] { job.getId() });
assertEquals(1, executions.size());
assertEquals(LONG_STRING.substring(0, 250), ((Map) executions.get(0))
.get("EXIT_MESSAGE"));
}
}

View File

@@ -1,9 +1,15 @@
package org.springframework.batch.execution.repository.dao;
import java.util.List;
import java.util.Map;
import org.springframework.batch.repeat.ExitStatus;
import org.springframework.dao.DataAccessException;
public class SqlStepDaoTests extends AbstractStepDaoTests {
private static final String LONG_STRING = BatchHibernateInterceptorTests.LONG_STRING;
protected void onSetUpBeforeTransaction() throws Exception {
((SqlStepDao) stepDao).setTablePrefix(SqlJobDao.DEFAULT_TABLE_PREFIX);
}
@@ -18,4 +24,18 @@ public class SqlStepDaoTests extends AbstractStepDaoTests {
}
}
public void testUpdateStepExecutionWithLongExitCode() {
assertTrue(LONG_STRING.length()>250);
stepExecution.setExitStatus(ExitStatus.FINISHED.addExitDescription(LONG_STRING));
stepDao.update(stepExecution);
List executions = jdbcTemplate.queryForList(
"SELECT * FROM BATCH_STEP_EXECUTION where STEP_ID=?",
new Object[] { step1.getId() });
assertEquals(1, executions.size());
assertEquals(LONG_STRING.substring(0, 250), ((Map) executions.get(0))
.get("EXIT_MESSAGE"));
}
}

View File

@@ -6,7 +6,7 @@
<property name="dataSource">
<bean class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
<property name="url" value="jdbc:hsqldb:mem:testdb"/>
<property name="url" value="jdbc:hsqldb:mem:testdb;sql.enforce_strict_size=true"/>
</bean>
</property>
<property name="initScript" value="org/springframework/batch/execution/repository/dao/init.sql" />

View File

@@ -27,7 +27,7 @@
</property>
<property name="entityInterceptor">
<bean
class="org.springframework.batch.execution.repository.dao.EntityNameInterceptor">
class="org.springframework.batch.execution.repository.dao.BatchHibernateInterceptor">
<property name="entityNameLocator" ref="entityNameLocator"/>
</bean>
</property>

View File

@@ -1,7 +1,7 @@
# Placeholders batch.*
# for HSQLDB:
batch.jdbc.driver=org.hsqldb.jdbcDriver
batch.jdbc.url=jdbc:hsqldb:mem:testdb
batch.jdbc.url=jdbc:hsqldb:mem:testdb;sql.enforce_strict_size=true
# use this one for a separate server process (so you can inspect the results)
# batch.jdbc.url=jdbc:hsqldb:hsql://localhost:9005/samples
batch.jdbc.user=sa