/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jbpm.memory;
import org.drools.core.event.KieBaseEventSupport;
import org.drools.core.impl.KnowledgeBaseImpl;
import org.drools.persistence.PersistableRunner;
import org.drools.persistence.jpa.processinstance.JPAWorkItemManagerFactory;
import org.jbpm.bpmn2.objects.TestWorkItemHandler;
import org.jbpm.workflow.instance.WorkflowProcessInstance;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.kie.api.KieBase;
import org.kie.api.event.process.DefaultProcessEventListener;
import org.kie.api.event.process.ProcessStartedEvent;
import org.kie.api.io.ResourceType;
import org.kie.api.runtime.Environment;
import org.kie.api.runtime.KieSessionConfiguration;
import org.kie.api.runtime.process.ProcessInstance;
import org.kie.internal.KnowledgeBase;
import org.kie.internal.KnowledgeBaseFactory;
import org.kie.internal.builder.KnowledgeBuilder;
import org.kie.internal.builder.KnowledgeBuilderError;
import org.kie.internal.builder.KnowledgeBuilderFactory;
import org.kie.internal.io.ResourceFactory;
import org.kie.internal.persistence.jpa.JPAKnowledgeService;
import org.kie.internal.runtime.StatefulKnowledgeSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import static org.jbpm.persistence.util.PersistenceUtil.*;
import static org.junit.Assert.*;
public class MemoryLeakTest {
private static final Logger logger = LoggerFactory.getLogger(MemoryLeakTest.class);
private static HashMap<String, Object> testContext;
private Environment env = null;
private static final String PROCESS_NAME = "RuleTaskWithProcessInstance";
@BeforeClass
public static void beforeClass() {
testContext = setupWithPoolingDataSource(JBPM_PERSISTENCE_UNIT_NAME);
}
@AfterClass
public static void afterClass() {
cleanUp(testContext);
}
@Before
public void before() {
env = createEnvironment(testContext);
}
@Test
public void findEventSupportRegisteredInstancesTest() {
// setup
KieBase kbase = createKnowledgeBase();
for( int i = 0; i < 3; ++i ) {
createKnowledgeSessionStartProcessEtc(kbase);
}
KieBaseEventSupport eventSupport = (KieBaseEventSupport) getValueOfField("eventSupport", KnowledgeBaseImpl.class, kbase);
assertEquals( "Event listeners should have been detached", 0, eventSupport.getEventListeners().size());
}
private KieBase createKnowledgeBase() {
KnowledgeBuilder kbuilder = KnowledgeBuilderFactory.newKnowledgeBuilder();
kbuilder.add(ResourceFactory.newClassPathResource("memory/BPMN2-RuleTaskWithInsertProcessInstance.bpmn2"), ResourceType.BPMN2);
kbuilder.add(ResourceFactory.newClassPathResource("memory/ProcessInstanceRule.drl"), ResourceType.DRL);
if (!kbuilder.getErrors().isEmpty()) {
Iterator<KnowledgeBuilderError> errIter = kbuilder.getErrors().iterator();
while( errIter.hasNext() ) {
KnowledgeBuilderError err = errIter.next();
StringBuilder lines = new StringBuilder("");
if( err.getLines().length > 0 ) {
lines.append(err.getLines()[0]);
for( int i = 1; i < err.getLines().length; ++i ) {
lines.append(", " + err.getLines()[i]);
}
}
logger.warn( err.getMessage() + " (" + lines.toString() + ")" );
}
throw new IllegalArgumentException("Errors while parsing knowledge base");
}
KnowledgeBase kbase = KnowledgeBaseFactory.newKnowledgeBase();
kbase.addKnowledgePackages(kbuilder.getKnowledgePackages());
return kbase;
}
private void createKnowledgeSessionStartProcessEtc(KieBase kbase) {
logger.info("session count=" + kbase.getKieSessions().size());
StatefulKnowledgeSession ksession = JPAKnowledgeService.newStatefulKnowledgeSession(kbase, getKnowledgeSessionConfiguration(), env);
addEventListenersToSession(ksession);
/**
* The following log line caused the memory leak.
* The specific (reverse-ordered) stack trace is the following:
*
* MemoryLeakTest.createKnowledgeSessionStartProcessEtc(KnowledgeBase) calls kbase.getKieSessions()
* ..
* KnowledgeBaseImpl.getStatefulKnowledgeSessions() line: 186
* StatefulKnowledgeSessionImpl.<init>(ReteooWorkingMemory, KnowledgeBase) line: 121
* ReteooStatefulSession(AbstractWorkingMemory).setKnowledgeRuntime(InternalKnowledgeRuntime) line: 1268
* ReteooStatefulSession(AbstractWorkingMemory).createProcessRuntime() line: 342
* ProcessRuntimeFactory.newProcessRuntime(AbstractWorkingMemory) line: 12
* ProcessRuntimeFactoryServiceImpl.newProcessRuntime(AbstractWorkingMemory) line: 1
* ProcessRuntimeFactoryServiceImpl.newProcessRuntime(AbstractWorkingMemory) line: 10
* ProcessRuntimeImpl.<init>(AbstractWorkingMemory) line: 84
* ProcessRuntimeImpl.initProcessEventListeners() line: 215
*
* And ProcessRuntimeImpl.initProcessEventListeners() is what adds a new listener
* to AbstractRuleBase.eventSupport.listeners via this line (235):
* kruntime.getKnowledgeBase().addEventListener(knowledgeBaseListener);
*
* The StatefulKnowledgeSessionImpl instance created in this .getStatefulKnowledgeSessions()
* method is obviously never disposed, which means that the listener is never removed.
* The listener then contains a link to a field (signalManager) of the ProcessRuntimeImpl,
* which contains a link to the StatefulKnowledgeSessionImpl instance created here. etc..
*/
logger.info("session count=" + kbase.getKieSessions().size());
TestWorkItemHandler handler = new TestWorkItemHandler();
ksession.getWorkItemManager().registerWorkItemHandler("Human Task", handler);
try {
// create process instance, insert into session and start process
Map<String, Object> processParams = new HashMap<String, Object>();
String [] fireballVarHolder = new String[1];
processParams.put("fireball", fireballVarHolder);
ProcessInstance processInstance = ksession.createProcessInstance(PROCESS_NAME, processParams);
ksession.insert(processInstance);
ksession.startProcessInstance(processInstance.getId());
// after the log line has been added, the DefaultProcessEventListener registered
// in the addEventListenersToSession() method no longer works?!?
ksession.fireAllRules();
// test process variables
String [] procVar = (String []) ((WorkflowProcessInstance) processInstance).getVariable("fireball");
assertEquals( "Rule task did NOT fire or complete.", "boom!", procVar[0] );
// complete task and process
Map<String, Object> results = new HashMap<String, Object>();
results.put( "chaerg", new SerializableResult("zhrini", 302l, "F", "A", "T"));
ksession.getWorkItemManager().completeWorkItem(handler.getWorkItem().getId(), results);
assertNull( ksession.getProcessInstance(processInstance.getId()));
} finally {
// This should clean up all listeners, but doesn't -> see docs above
ksession.dispose();
}
}
private KieSessionConfiguration getKnowledgeSessionConfiguration() {
Properties ksessionProperties;
ksessionProperties = new Properties();
ksessionProperties.put("drools.commandService", PersistableRunner.class.getName() );
ksessionProperties.put("drools.processInstanceManagerFactory",
"org.jbpm.persistence.processinstance.JPAProcessInstanceManagerFactory");
ksessionProperties.setProperty("drools.workItemManagerFactory", JPAWorkItemManagerFactory.class.getName());
ksessionProperties
.put("drools.processSignalManagerFactory", "org.jbpm.persistence.processinstance.JPASignalManagerFactory");
return KnowledgeBaseFactory.newKnowledgeSessionConfiguration(ksessionProperties);
}
private void addEventListenersToSession(StatefulKnowledgeSession session) {
session.addEventListener(new DefaultProcessEventListener() {
@Override
public void afterProcessStarted(ProcessStartedEvent event) {
logger.info(">>> Firing All the Rules after process started! " + event);
((StatefulKnowledgeSession) event.getKieRuntime()).fireAllRules();
}
});
}
private Object getValueOfField(String fieldname, Class<?> sourceClass, Object source ) {
String sourceClassName = sourceClass.getName();
Field field = null;
try {
field = sourceClass.getDeclaredField(fieldname);
field.setAccessible(true);
} catch (SecurityException e) {
fail("Unable to retrieve " + fieldname + " field from " + sourceClassName + ": " + e.getCause());
} catch (NoSuchFieldException e) {
fail("Unable to retrieve " + fieldname + " field from " + sourceClassName + ": " + e.getCause());
}
assertNotNull("." + fieldname + " field is null!?!", field);
Object fieldValue = null;
try {
fieldValue = field.get(source);
} catch (IllegalArgumentException e) {
fail("Unable to retrieve value of " + fieldname + " from " + sourceClassName + ": " + e.getCause());
} catch (IllegalAccessException e) {
fail("Unable to retrieve value of " + fieldname + " from " + sourceClassName + ": " + e.getCause());
}
return fieldValue;
}
}