Orphaned records remain in the DB after process instance is deleted
-------------------------------------------------------------------
Key: JBPM-970
URL:
http://jira.jboss.com/jira/browse/JBPM-970
Project: JBoss jBPM
Issue Type: Bug
Components: Core Engine
Affects Versions: jBPM 3.1.2
Reporter: Tihomir Daskalov
Assigned To: Tom Baeyens
I have detected leak of orphaned records in the DB in the following cases:
1) deleting a process instance variable
2) changing the type of a process instance variable (for example from string to long)
3) updating a byte array (the leak appears only when the updates are performed in
different JBPM sessions - i.e. when the process instance execution is asynchronous)
Seems that for the first two cases the orphaned records are left because of the
cascade="save-update" in org.jbpm.context.log.VariableLog.hbm.xml. It causes the
variable instances that are referenced only from the process instance log to be not
deleted in the DB. As workaround I had to manually remove the orphaned records in the
following way:
// The logs map contains the logs per token.
Map tokensLogs = SessionContext.getJbpmContext().getLoggingSession()
.findLogsByProcessInstance(_processInstance.getId());
if (tokensLogs.size() != 1) {
// For now are supported only logs with one token.
throw new IllegalArgumentException("Expected exactly one token but found:
"
+ tokensLogs.size());
}
// The map is expected to have only one element mapping the root token
// to the list of its logs.
List logs = (List) tokensLogs.values().iterator().next();
Session session = SessionContext.getJbpmContext().getSession();
for (Iterator iter = logs.iterator(); iter.hasNext();) {
ProcessLog log = (ProcessLog) iter.next();
if (log instanceof VariableLog) {
VariableLog variableLog = ((VariableLog) log);
VariableInstance variableInstance = variableLog.getVariableInstance();
// Remove only the variable instances that are not associated with token.
if (variableInstance.getToken() == null) {
session.delete(variableInstance);
}
}
session.delete(log);
}
The third case seems to be caused from orphaned child entity (org.jbpm.bytes.ByteArray).
The new ByteArray value replaces the old ByteArray value in the
org.jbpm.context.exe.VariableInstance class, and thus the old ByteArray remains orphan.
This is no problem if the old ByteArray is still transient - it will be GC-ed, but if it
was already persisted then the persisted entity will remain orphan in the DB. This
explains why the 3-rd case can be reproduced only when the byte array is persisted between
the two states of the process instance.
I have used the following code to workaround the 3-rd case:
// Take reference to the old ByteArray entity.
ByteArray oldByteArray = null;
TokenVariableMap tokenVariableMap =
pProcessInstance.getContextInstance().getTokenVariableMap(pProcessInstance.getRootToken());
if (tokenVariableMap != null) {
VariableInstance variableInstance =
tokenVariableMap.getVariableInstance(pVariableName);
if (variableInstance != null) {
oldByteArray = (ByteArray) ((ByteArrayInstance)
variableInstance).getObject();
}
}
// Set the new value of the byte array.
pProcessInstance.getContextInstance().setVariable(pVariableName, pBytes);
// Manually remove the old byte array.
if (oldByteArray != null) {
Session session = _jbpmContext().getSession();
if (session != null && session.contains(oldByteArray)) {
SessionContext.getJbpmContext().getSession().delete(oldByteArray);
}
}
Follows the test case that reproduces (via failing) the three operations described above:
public class JbpmTest extends TestCase {
private JbpmContext _jbpmContext;
protected void setUp() throws Exception {
JbpmConfiguration.getInstance().createSchema();
_jbpmContext = JbpmConfiguration.getInstance().createJbpmContext();
}
protected void tearDown() throws Exception {
_jbpmContext.close();
JbpmConfiguration.getInstance().dropSchema();
}
/**
* Tests that deleting a JBPM variable leaks orphaned records.
*/
public void testOrphanRecords_1() throws HibernateException, SQLException {
ProcessDefinition processDefinition = ProcessDefinition
.parseXmlString("<process-definition name='test'>"
+ " <start-state>"
+ " <transition to='s' />"
+ " </start-state>"
+ " <state name='s'>"
+ " <transition to='end' />"
+ " </state>"
+ " <end-state name='end' />"
+ "</process-definition>");
_jbpmContext.deployProcessDefinition(processDefinition);
// Assure that the watched tables are empty.
assertEquals(0, getRecordCount("JBPM_VARIABLEINSTANCE"));
assertEquals(0, getRecordCount("JBPM_LOG"));
assertEquals(0, getRecordCount("JBPM_BYTEBLOCK"));
assertEquals(0, getRecordCount("JBPM_BYTEARRAY"));
// Execute a process instance and create and delete a variable.
ProcessInstance processInstance = new ProcessInstance(processDefinition);
processInstance.getContextInstance().setVariable("test",
"abc");
processInstance.signal();
assertFalse(processInstance.hasEnded());
processInstance.getContextInstance().deleteVariable("test");
processInstance.signal();
assertTrue(processInstance.hasEnded());
// Persist the process instance.
_jbpmContext.save(processInstance);
_jbpmContext.getSession().flush();
long processInstanceId = processInstance.getId();
// Assure that the log entries are really persisted.
assertTrue(0 < getRecordCount("JBPM_LOG"));
// Delete the process instance.
_jbpmContext.getGraphSession().deleteProcessInstance(processInstanceId);
// Assure that the watched tables are empty.
assertEquals(0, getRecordCount("JBPM_VARIABLEINSTANCE"));
assertEquals(0, getRecordCount("JBPM_LOG"));
assertEquals(0, getRecordCount("JBPM_BYTEBLOCK"));
assertEquals(0, getRecordCount("JBPM_BYTEARRAY"));
}
/**
* Tests that changing the type of a JBPM variable leaks orphaned records.
*/
public void testOrphanRecords_2() throws HibernateException, SQLException {
ProcessDefinition processDefinition = ProcessDefinition
.parseXmlString("<process-definition name='test'>"
+ " <start-state>"
+ " <transition to='s' />"
+ " </start-state>"
+ " <state name='s'>"
+ " <transition to='end' />"
+ " </state>"
+ " <end-state name='end' />"
+ "</process-definition>");
_jbpmContext.deployProcessDefinition(processDefinition);
// Assure that the watched tables are empty.
assertEquals(0, getRecordCount("JBPM_VARIABLEINSTANCE"));
assertEquals(0, getRecordCount("JBPM_LOG"));
assertEquals(0, getRecordCount("JBPM_BYTEBLOCK"));
assertEquals(0, getRecordCount("JBPM_BYTEARRAY"));
// Execute a process instance and create and then change the type of a variable.
ProcessInstance processInstance = new ProcessInstance(processDefinition);
processInstance.getContextInstance().setVariable("test",
"abc");
processInstance.signal();
assertFalse(processInstance.hasEnded());
processInstance.getContextInstance().setVariable("test", new Long(10));
processInstance.signal();
assertTrue(processInstance.hasEnded());
// Persist the process instance.
_jbpmContext.save(processInstance);
_jbpmContext.getSession().flush();
long processInstanceId = processInstance.getId();
// Assure that the log entries are really persisted.
assertTrue(0 < getRecordCount("JBPM_LOG"));
// Delete the process instance.
_jbpmContext.getGraphSession().deleteProcessInstance(processInstanceId);
// Assure that the watched tables are empty.
assertEquals(0, getRecordCount("JBPM_VARIABLEINSTANCE"));
assertEquals(0, getRecordCount("JBPM_LOG"));
assertEquals(0, getRecordCount("JBPM_BYTEBLOCK"));
assertEquals(0, getRecordCount("JBPM_BYTEARRAY"));
}
/**
* Tests that a JBPM variable of byte array type leaks orphaned records.
* Note that the leak appears only when the JBPM session is reopened between the
states.
*/
public void testOrphanRecords_3() throws HibernateException, SQLException {
ProcessDefinition processDefinition = ProcessDefinition
.parseXmlString("<process-definition name='test'>"
+ " <start-state>"
+ " <transition to='s' />"
+ " </start-state>"
+ " <state name='s'>"
+ " <transition to='end' />"
+ " </state>"
+ " <end-state name='end' />"
+ "</process-definition>");
_jbpmContext.deployProcessDefinition(processDefinition);
// Assure that the watched tables are empty.
assertEquals(0, getRecordCount("JBPM_VARIABLEINSTANCE"));
assertEquals(0, getRecordCount("JBPM_LOG"));
assertEquals(0, getRecordCount("JBPM_BYTEBLOCK"));
assertEquals(0, getRecordCount("JBPM_BYTEARRAY"));
// Execute a process instance and create a byte array variable.
// Note that the leak appears only when the JBPM session is reopened between the
states.
ProcessInstance processInstance = new ProcessInstance(processDefinition);
processInstance.getContextInstance().setVariable("bytes", new byte[] {1,
2});
_jbpmContext.save(processInstance);
_jbpmContext.getSession().flush();
long processInstanceId = processInstance.getId();
_jbpmContext.close();
// Reopen the session and continue the execution.
_jbpmContext = JbpmConfiguration.getInstance().createJbpmContext();
processInstance = _jbpmContext.loadProcessInstance(processInstanceId);
processInstance.signal();
assertFalse(processInstance.hasEnded());
processInstance.getContextInstance().setVariable("bytes", new byte[] {1,
2, 3, 4});
processInstance.signal();
assertTrue(processInstance.hasEnded());
_jbpmContext.save(processInstance);
_jbpmContext.close();
_jbpmContext = JbpmConfiguration.getInstance().createJbpmContext();
// Assure that the log entries are really persisted.
assertTrue(0 < getRecordCount("JBPM_LOG"));
// Delete the process instance.
_jbpmContext.getGraphSession().deleteProcessInstance(processInstanceId);
// Assure that the watched tables are empty.
assertEquals(0, getRecordCount("JBPM_VARIABLEINSTANCE"));
assertEquals(0, getRecordCount("JBPM_LOG"));
assertEquals(0, getRecordCount("JBPM_BYTEBLOCK"));
assertEquals(0, getRecordCount("JBPM_BYTEARRAY"));
}
private int getRecordCount(String pTableName) throws HibernateException, SQLException
{
Statement stmt = null;
ResultSet rs = null;
try {
stmt = _jbpmContext.getSession().connection().createStatement();
String sql = "SELECT count(*) FROM " + pTableName;
rs = stmt.executeQuery(sql);
rs.next();
return rs.getInt(1);
} finally {
if (rs != null) {
rs.close();
}
if (stmt != null) {
stmt.close();
}
}
}
}
--
This message is automatically generated by JIRA.
-
If you think it was sent incorrectly contact one of the administrators:
http://jira.jboss.com/jira/secure/Administrators.jspa
-
For more information on JIRA, see:
http://www.atlassian.com/software/jira