[jbpm]SyncWSHumanTaskHandler との戦い

SyncWSHumanTaskHandler&LocalTaskServiceが一番手堅い実装だと信じてスタート。後で意見を翻す可能性あり。

とりあえず Servlet/EJB で動かす

staticにksession、localTaskServiceを持ち、プロセスを通して使いまわす。JUnitのテストに近いやりかた。現実的ではない。staticにしてるのはもちろん手抜き。EJBのメソッド毎にトランザクションは切れる。

LocalTaskService.complete()でタスクをcomplete。

Daemon Thread [http-127.0.0.1-8080-1] (Suspended (breakpoint at line 316 in SyncWSHumanTaskHandler$TaskCompletedHandler))	
	SyncWSHumanTaskHandler$TaskCompletedHandler.execute(Payload) line: 316	
	LocalTaskService$SimpleEventTransport.trigger(Payload) line: 287	
	MessagingTaskEventListener.taskCompleted(TaskCompletedEvent) line: 73	
	TaskEventSupport.fireTaskCompleted(long, String) line: 47	
	TaskServiceSession.postTaskCompleteOperation(Task) line: 502	
	TaskServiceSession.taskOperation(Operation, long, String, String, ContentData, List<String>) line: 469	
	LocalTaskService.complete(long, String, ContentData) line: 76	
	HelloBean.retrieveTaskByJohn(int) line: 98	
...

abortWorkItem??ともかく、(completeWorkItemでも同様の動きになるはず)

processInstance.signalEvent("workItemAborted", workItem);

が飛び、

HumanTaskNodeInstance.triggerCompleted()からプロセスを継続する。よって次のノードのHumanTaskNodeInstance.createWorkItem()へ進む。

Daemon Thread [http-127.0.0.1-8080-1] (Suspended (breakpoint at line 36 in HumanTaskNodeInstance))	
	HumanTaskNodeInstance.createWorkItem(WorkItemNode) line: 36	
	HumanTaskNodeInstance(WorkItemNodeInstance).internalTrigger(NodeInstance, String) line: 96	
	HumanTaskNodeInstance(NodeInstanceImpl).trigger(NodeInstance, String) line: 122	
	HumanTaskNodeInstance(NodeInstanceImpl).triggerNodeInstance(NodeInstance, String) line: 196	
	HumanTaskNodeInstance(NodeInstanceImpl).triggerCompleted(String, boolean) line: 155	
	HumanTaskNodeInstance(ExtendedNodeInstanceImpl).triggerCompleted(String, boolean) line: 47	
	HumanTaskNodeInstance(StateBasedNodeInstance).triggerCompleted(String, boolean) line: 162	
	HumanTaskNodeInstance(StateBasedNodeInstance).triggerCompleted() line: 143	
	HumanTaskNodeInstance(WorkItemNodeInstance).triggerCompleted(WorkItem) line: 239	
	HumanTaskNodeInstance.triggerCompleted(WorkItem) line: 90	
	HumanTaskNodeInstance(WorkItemNodeInstance).workItemAborted(WorkItem) line: 293	
	HumanTaskNodeInstance(WorkItemNodeInstance).signalEvent(String, Object) line: 279	
	RuleFlowProcessInstance(WorkflowProcessInstanceImpl).signalEvent(String, Object) line: 338	
	JPAWorkItemManager.abortWorkItem(long) line: 154	
	AbortWorkItemCommand.execute(Context) line: 56	
	AbortWorkItemCommand.execute(Context) line: 29	
	DefaultCommandService.execute(Command<T>) line: 36	
	SingleSessionCommandService.execute(Command<T>) line: 355	
	CommandBasedStatefulKnowledgeSession$1.abortWorkItem(long) line: 156	
	SyncWSHumanTaskHandler$TaskCompletedHandler.handleCompletedTask(long) line: 378	
	SyncWSHumanTaskHandler$TaskCompletedHandler.execute(Payload) line: 319	
	LocalTaskService$SimpleEventTransport.trigger(Payload) line: 287	
	MessagingTaskEventListener.taskCompleted(TaskCompletedEvent) line: 73	
	TaskEventSupport.fireTaskCompleted(long, String) line: 47	
	TaskServiceSession.postTaskCompleteOperation(Task) line: 502	
	TaskServiceSession.taskOperation(Operation, long, String, String, ContentData, List<String>) line: 469	
	LocalTaskService.complete(long, String, ContentData) line: 76	
	HelloBean.retrieveTaskByJohn(int) line: 98	
...

JPAKnowledgeService.loadStatefulKnowledgeSession()

JPAKnowledgeService.loadStatefulKnowledgeSession()を使って、セッションIDからksessionをリストアする。SyncWSHumanTaskHandlerも毎回設定。org.jbpm.task.service.TaskServiceはstaticにキャッシュ。LocalTaskServiceも毎回作成。

    public TaskService getTaskService(StatefulKnowledgeSession ksession) {
    	if (taskService == null) {
    		taskService = new org.jbpm.task.service.TaskService(
				emf, SystemEventListenerFactory.getSystemEventListener());
    	}
		SyncWSHumanTaskHandler humanTaskHandler = new SyncWSHumanTaskHandler(
			new LocalTaskService(taskService), ksession);
		humanTaskHandler.setLocal(true);
		humanTaskHandler.connect();
		ksession.getWorkItemManager().registerWorkItemHandler("Human Task", humanTaskHandler);
		return new LocalTaskService(taskService);
    }

LocalTaskService.complete()のトランザクションコミット時にStaleObjectStateExceptionが出る。なんでやねん。

11:09:25,831 ERROR [AbstractFlushingEventListener] Could not synchronize database state with session
org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [org.jbpm.persistence.processinstance.ProcessInstanceInfo#12]

デバッグ。。。

Daemon Thread [http-127.0.0.1-8080-1] (Suspended (breakpoint at line 36 in HumanTaskNodeInstance))	
	HumanTaskNodeInstance.createWorkItem(WorkItemNode) line: 36	
	HumanTaskNodeInstance(WorkItemNodeInstance).internalTrigger(NodeInstance, String) line: 96	
	HumanTaskNodeInstance(NodeInstanceImpl).trigger(NodeInstance, String) line: 122	
	HumanTaskNodeInstance(NodeInstanceImpl).triggerNodeInstance(NodeInstance, String) line: 196	
	HumanTaskNodeInstance(NodeInstanceImpl).triggerCompleted(String, boolean) line: 155	
	HumanTaskNodeInstance(ExtendedNodeInstanceImpl).triggerCompleted(String, boolean) line: 47	
	HumanTaskNodeInstance(StateBasedNodeInstance).triggerCompleted(String, boolean) line: 162	
	HumanTaskNodeInstance(StateBasedNodeInstance).triggerCompleted() line: 143	
	HumanTaskNodeInstance(WorkItemNodeInstance).triggerCompleted(WorkItem) line: 239	
	HumanTaskNodeInstance.triggerCompleted(WorkItem) line: 90	
	HumanTaskNodeInstance(WorkItemNodeInstance).workItemAborted(WorkItem) line: 293	
	HumanTaskNodeInstance(WorkItemNodeInstance).signalEvent(String, Object) line: 279	
	RuleFlowProcessInstance(WorkflowProcessInstanceImpl).signalEvent(String, Object) line: 338	
	JPAWorkItemManager.abortWorkItem(long) line: 154	
	AbortWorkItemCommand.execute(Context) line: 56	
	AbortWorkItemCommand.execute(Context) line: 29	
	DefaultCommandService.execute(Command<T>) line: 36	
	SingleSessionCommandService.execute(Command<T>) line: 355	
	CommandBasedStatefulKnowledgeSession$1.abortWorkItem(long) line: 156	
	SyncWSHumanTaskHandler$TaskCompletedHandler.handleCompletedTask(long) line: 378	
	SyncWSHumanTaskHandler$TaskCompletedHandler.execute(Payload) line: 319	
	LocalTaskService$SimpleEventTransport.trigger(Payload) line: 287	
	MessagingTaskEventListener.taskCompleted(TaskCompletedEvent) line: 73	
	TaskEventSupport.fireTaskCompleted(long, String) line: 47	
	TaskServiceSession.postTaskCompleteOperation(Task) line: 502	
	TaskServiceSession.taskOperation(Operation, long, String, String, ContentData, List<String>) line: 469	
	LocalTaskService.complete(long, String, ContentData) line: 76	
	HelloBean.retrieveTaskByJohn(int) line: 101	
...

全く同じスタックで、HumanTaskNodeInstance.createWorkItemが4回実行される。そんなバカな。

MessagingTaskEventListener.taskCompleted()でループしている。MessagingTaskEventListenerのkeysにorg.jbpm.process.workitem.wsht.SyncWSHumanTaskHandler$TaskCompletedHandlerが6個入っていた。ksessionに
蓄積されているのだろう。つまりloadStatefulKnowledgeSessionの後にSyncWSHumanTaskHandlerのregisterWorkItemHandlerはいらんかったんや!

SyncWSHumanTaskHandlerをJPAKnowledgeService.newStatefulKnowledgeSession()直後だけにしたら、普通に動いた。

org.jbpm.task.service.TaskService の謎

org.jbpm.task.service.TaskServiceのstatic持ちを止め、毎回EntityManagerFactoryから生成するようにすると、2つ目のタスクが作成されなくなった。

デバッグ。。

MessagingTaskEventListener.taskCompleted()で keys.getTargets( key );の結果がnullだった。はて。

humanTaskHandler.connect();から、LocalTaskService.registerForEvent()が呼ばれ、responseHandlerをEventKeysにセットする。

    public void registerForEvent(EventKey key, boolean remove, EventResponseHandler responseHandler) {
        SimpleEventTransport transport = new SimpleEventTransport(session, responseHandler, remove);
        service.getEventKeys().register(key, transport);
    }

つまり、最初に登録するSyncWSHumanTaskHandlerが参照するLocalTaskServiceが参照するTaskServiceと、後でタスクオペレーションに使うLocalTaskServiceが参照するTaskServiceは同じインスタンスでないとダメー。ここの依存関係が肝か。(訂正:SyncWSHumanTaskHandlerが参照するLocalTaskServiceとタスクオペレーションに使うLocalTaskServiceを同じインスタンスにすればいい)(さらに追記。LocalTaskServiceを同じインスタンスにしないと冒頭のabortWorkItemになるので、同じにしないとだめ!JbpmJUnitTestCaseの実装がミスリーディングだと思うぞ!)

くっそ、じゃあやっぱりSyncWSHumanTaskHandlerの再設定が必要だろ。

再挑戦

loadStatefulKnowledgeSession()の度にSyncWSHumanTaskHandlerをregisterするように戻す。。。。あれ、動いた。待て待て。。。そうか

  • TaskServiceをキャッシュ、ksessionもキャッシュ -> 正常
  • TaskServiceをキャッシュ、毎回SyncWSHumanTaskHandlerを設定 -> responseHandlerが重複登録されてStaleObjectStateException
  • TaskServiceをキャッシュ、最初のみSyncWSHumanTaskHandlerを設定 -> 正常
  • キャッシュなし、最初のみSyncWSHumanTaskHandlerを設定 -> responseHandlerが無いので、プロセスが進まない
  • キャッシュなし、毎回SyncWSHumanTaskHandlerを設定 -> 正常

OK、納得した。最後のパターンが汎用性の高い使い方のはず。

締め

依存性を減らすため、ksessionも毎回作成する。startProcess時にprocessInstanceIdをreturnするが、今回は使わない。

https://github.com/tkobayas/example-projects/blob/master/jBPM5Example_SyncWSHumanTaskHandler/ejb/src/main/java/com/sample/HelloBean.java