Skip to content

Commit ad529a4

Browse files
Tim-LinschotenTim Linschoten
andauthored
Fix the bug that the ProcessInstance would passivate on retry interaction if that interaction took too long. (#1936)
* Fix for passivating the instance on interaction retry. --------- Co-authored-by: Tim Linschoten <tim.linschoten@ing.com>
1 parent 22a3416 commit ad529a4

2 files changed

Lines changed: 101 additions & 9 deletions

File tree

core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/ProcessInstance.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,7 @@ class ProcessInstance(
577577
// execute the job immediately if there is no timeout
578578
if (timeout == 0) {
579579
executeJob(job, originalSender)
580+
updateState(updatedInstance, scheduledRetries)
580581
}
581582
else {
582583
// schedule the retry

core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/ProcessInstanceSpec.scala

Lines changed: 100 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import akka.actor.{ActorRef, ActorSystem, PoisonPill, Props, Terminated}
44
import akka.event.DiagnosticLoggingAdapter
55
import akka.testkit.{TestDuration, TestProbe}
66
import akka.util.Timeout
7+
import cats.effect.IO
78
import cats.effect.unsafe.IORuntime
89
import com.ing.baker.il.failurestrategy.{BlockInteraction, FireEventAfterFailure, InteractionFailureStrategy, RetryWithIncrementalBackoff}
910
import com.ing.baker.il.petrinet.{InteractionTransition, Place}
@@ -19,6 +20,7 @@ import com.ing.baker.runtime.akka.actor.process_instance.ProcessInstanceSpec._
1920
import com.ing.baker.runtime.akka.actor.process_instance.dsl.TestUtils.{PlaceMethods, place}
2021
import com.ing.baker.runtime.akka.actor.process_instance.dsl._
2122
import com.ing.baker.runtime.akka.actor.process_instance.internal.ExceptionStrategy.RetryWithDelay
23+
import com.ing.baker.runtime.akka.actor.process_instance.internal.Job
2224
import com.ing.baker.runtime.akka.actor.process_instance.{ProcessInstanceProtocol => protocol}
2325
import com.ing.baker.runtime.akka.internal.FatalInteractionException
2426
import com.ing.baker.runtime.akka.namedCachedThreadPool
@@ -30,6 +32,7 @@ import org.mockito.ArgumentMatchers.any
3032
import org.mockito.Mockito._
3133
import org.mockito.invocation.InvocationOnMock
3234
import org.mockito.stubbing.Answer
35+
import org.scalatest.BeforeAndAfterEach
3336
import org.scalatest.concurrent.ScalaFutures
3437
import org.scalatest.matchers.should.Matchers
3538
import org.scalatest.time.{Milliseconds, Span}
@@ -56,9 +59,12 @@ trait TestSequenceNet extends SequenceNet {
5659

5760
object ProcessInstanceSpec {
5861

62+
var countDownLatch = new java.util.concurrent.CountDownLatch(0)
63+
5964
def Added(n: Int): EventInstance = EventInstance(n.toString, Map.empty)
6065

6166
val testExecutionContext = namedCachedThreadPool(s"Baker.CachedThreadPool")
67+
val interactionExecutorContext = namedCachedThreadPool(s"Baker.InteractionExecutor")
6268

6369
val instanceSettings = Settings(
6470
executionContext = testExecutionContext,
@@ -79,7 +85,14 @@ object ProcessInstanceSpec {
7985
CompiledRecipe("name", UUID.randomUUID().toString, topology, Marking.empty, Seq.empty, Option.empty, Option.empty),
8086
settings,
8187
runtime,
82-
delayedTransitionActor)
88+
delayedTransitionActor) {
89+
override def executeJobViaExecutor(job: Job[RecipeInstanceState], originalSender: ActorRef): Unit = {
90+
IO.delay {
91+
countDownLatch.await(10, TimeUnit.SECONDS)
92+
super.executeJobViaExecutor(job, originalSender)
93+
}.evalOn(interactionExecutorContext).unsafeRunAndForget()(IORuntime.global)
94+
}
95+
}
8396
)
8497
}
8598

@@ -96,7 +109,7 @@ object ProcessInstanceSpec {
96109
}
97110
}
98111

99-
class ProcessInstanceSpec extends AkkaTestBase("ProcessInstanceSpec") with ScalaFutures with MockitoSugar with Matchers {
112+
class ProcessInstanceSpec extends AkkaTestBase("ProcessInstanceSpec") with ScalaFutures with MockitoSugar with Matchers with BeforeAndAfterEach {
100113

101114
def dilatedMillis(millis: Long)(implicit system: ActorSystem): Long = FiniteDuration(millis, TimeUnit.MILLISECONDS).dilated.toMillis
102115

@@ -107,6 +120,11 @@ class ProcessInstanceSpec extends AkkaTestBase("ProcessInstanceSpec") with Scala
107120
Thread.sleep(dilatedMillis(100))
108121
}
109122

123+
override def beforeEach(): Unit = {
124+
countDownLatch = new java.util.concurrent.CountDownLatch(0)
125+
super.beforeEach()
126+
}
127+
110128
"A persistent petri net actor" should {
111129

112130
"Respond with an Initialized response after processing an Initialize command" in new TestSequenceNet {
@@ -190,7 +208,6 @@ class ProcessInstanceSpec extends AkkaTestBase("ProcessInstanceSpec") with Scala
190208
}
191209

192210
"Be able to retry a failed (blocked) transition when requested" in new TestSequenceNet {
193-
194211
val counter = new AtomicInteger(0)
195212

196213
override val sequence = Seq(
@@ -207,25 +224,99 @@ class ProcessInstanceSpec extends AkkaTestBase("ProcessInstanceSpec") with Scala
207224
expectMsgClass(classOf[Initialized])
208225

209226
actor ! FireTransition(transitionId = 1, input = null)
210-
211227
expectMsgClass(classOf[TransitionFailed])
212228

229+
// Validate the instance is in the correct state
213230
actor ! GetState
214-
215231
val state: InstanceState = expectMsgClass(classOf[InstanceState])
216-
217232
state.jobs.size shouldBe 1
218-
219233
val (jobId, jobState) = state.jobs.head
220-
221234
jobState.exceptionState should matchPattern {
222235
case Some(ExceptionState(_, _, BlockTransition)) =>
223236
}
224237

238+
// Set the Countdown latch so that the job is only finished when we want.
239+
countDownLatch = new java.util.concurrent.CountDownLatch(1)
240+
241+
// Send the OverrideExceptionStrategy message
225242
actor ! OverrideExceptionStrategy(jobId, protocol.ExceptionStrategy.RetryWithDelay(0))
226243

227-
// expect that the failure is resolved
244+
// Validate job is executing
245+
actor ! GetState
246+
val newState: InstanceState = expectMsgClass(classOf[InstanceState])
247+
newState.jobs.size shouldBe 1
248+
val (newJobId, newJobState) = newState.jobs.head
249+
newJobId shouldBe jobId
250+
newJobState.exceptionState should matchPattern {
251+
case Some(ExceptionState(_, _, com.ing.baker.runtime.akka.actor.process_instance.ProcessInstanceProtocol.ExceptionStrategy.RetryWithDelay(0))) =>
252+
}
253+
254+
// Allow the job to finish
255+
countDownLatch.countDown()
256+
257+
// Validate the transition is fired successfully after retry
258+
expectMsgPF() { case TransitionFired(_, 1, _, _, _, _, _) => }
259+
260+
// Validate no job is left open
261+
actor ! GetState
262+
val finalState: InstanceState = expectMsgClass(classOf[InstanceState])
263+
finalState.jobs.size shouldBe 0
264+
}
265+
266+
"Be able to retry a failed (blocked) transition when requested with delay" in new TestSequenceNet {
267+
val counter = new AtomicInteger(0)
268+
269+
override val sequence = Seq(
270+
transition() { _ =>
271+
if (counter.getAndIncrement() == 0)
272+
throw new RuntimeException("t1 failed!")
273+
else
274+
Added(1)
275+
})
276+
277+
val actor = createProcessInstance(petriNet, runtime)
278+
279+
actor ! Initialize(initialMarking, RecipeInstanceState(UUID.randomUUID().toString, UUID.randomUUID().toString, Map.empty, Map.empty, Seq.empty))
280+
expectMsgClass(classOf[Initialized])
281+
282+
actor ! FireTransition(transitionId = 1, input = null)
283+
expectMsgClass(classOf[TransitionFailed])
284+
285+
// Validate the instance is in the correct state
286+
actor ! GetState
287+
val state: InstanceState = expectMsgClass(classOf[InstanceState])
288+
state.jobs.size shouldBe 1
289+
val (jobId, jobState) = state.jobs.head
290+
jobState.exceptionState should matchPattern {
291+
case Some(ExceptionState(_, _, BlockTransition)) =>
292+
}
293+
294+
// Set the Countdown latch so that the job is only finished when we want.
295+
countDownLatch = new java.util.concurrent.CountDownLatch(1)
296+
297+
// Send the OverrideExceptionStrategy message
298+
actor ! OverrideExceptionStrategy(jobId, protocol.ExceptionStrategy.RetryWithDelay(1000))
299+
300+
// Validate job is executing
301+
actor ! GetState
302+
val newState: InstanceState = expectMsgClass(classOf[InstanceState])
303+
newState.jobs.size shouldBe 1
304+
val (newJobId, newJobState) = newState.jobs.head
305+
newJobId shouldBe jobId
306+
newJobState.exceptionState should matchPattern {
307+
case Some(ExceptionState(_, _, com.ing.baker.runtime.akka.actor.process_instance.ProcessInstanceProtocol.ExceptionStrategy.RetryWithDelay(1000))) =>
308+
}
309+
310+
// Allow the job to finish
311+
countDownLatch.countDown()
312+
313+
// Validate the transition is fired successfully after retry
228314
expectMsgPF() { case TransitionFired(_, 1, _, _, _, _, _) => }
315+
316+
// Validate no job is left open
317+
actor ! GetState
318+
val finalState: InstanceState = expectMsgClass(classOf[InstanceState])
319+
finalState.jobs.size shouldBe 0
229320
}
230321

231322
"Be able to resolve a failed (blocked) transition when requested" in new TestSequenceNet {

0 commit comments

Comments
 (0)