Skip to content

Commit 5109942

Browse files
authored
cold start container failure should abort buffered concurrent activations (#4958)
1 parent b70ea5a commit 5109942

2 files changed

Lines changed: 98 additions & 22 deletions

File tree

core/invoker/src/main/scala/org/apache/openwhisk/core/containerpool/ContainerProxy.scala

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,13 @@ class ContainerProxy(factory: (TransactionId,
458458
destroyContainer(newData, true)
459459

460460
// Failed after /init (the first run failed) on prewarmed or cold start
461+
// - container will be destroyed
462+
// - buffered will be aborted (if init fails, we assume it will always fail)
461463
case Event(f: FailureMessage, data: PreWarmedData) =>
464+
logging.error(
465+
this,
466+
s"Failed during init of cold container ${data.getContainer}, queued activations will be aborted.")
467+
462468
activeCount -= 1
463469
//reuse an existing init failure for any buffered activations that will be aborted
464470
val r = f.cause match {
@@ -468,7 +474,12 @@ class ContainerProxy(factory: (TransactionId,
468474
destroyContainer(data, true, true, r)
469475

470476
// Failed for a subsequent /run
477+
// - container will be destroyed
478+
// - buffered will be resent (at least 1 has completed, so others are given a chance to complete)
471479
case Event(_: FailureMessage, data: WarmedData) =>
480+
logging.error(
481+
this,
482+
s"Failed during use of warm container ${data.getContainer}, queued activations will be resent.")
472483
activeCount -= 1
473484
if (activeCount == 0) {
474485
destroyContainer(data, true)
@@ -479,10 +490,13 @@ class ContainerProxy(factory: (TransactionId,
479490
}
480491

481492
// Failed at getting a container for a cold-start run
493+
// - container will be destroyed
494+
// - buffered will be aborted (if cold start container fails to start, we assume it will continue to fail)
482495
case Event(_: FailureMessage, _) =>
496+
logging.error(this, "Failed to start cold container, queued activations will be aborted.")
483497
activeCount -= 1
484498
context.parent ! ContainerRemoved(true)
485-
rejectBuffered()
499+
abortBuffered()
486500
stop()
487501

488502
case _ => delay
@@ -649,34 +663,16 @@ class ContainerProxy(factory: (TransactionId,
649663
*/
650664
def destroyContainer(newData: ContainerStarted,
651665
replacePrewarm: Boolean,
652-
abortBuffered: Boolean = false,
666+
abort: Boolean = false,
653667
abortResponse: Option[ActivationResponse] = None) = {
654668
val container = newData.container
655669
if (!rescheduleJob) {
656670
context.parent ! ContainerRemoved(replacePrewarm)
657671
} else {
658672
context.parent ! RescheduleJob
659673
}
660-
if (abortBuffered && runBuffer.length > 0) {
661-
logging.info(this, s"aborting ${runBuffer.length} queued activations after failed init")
662-
runBuffer.foreach { job =>
663-
implicit val tid = job.msg.transid
664-
logging.info(this, s"aborting activation ${job.msg.activationId} after failed init with ${abortResponse}")
665-
val result = ContainerProxy.constructWhiskActivation(
666-
job,
667-
None,
668-
Interval.zero,
669-
false,
670-
abortResponse.getOrElse(ActivationResponse.whiskError(Messages.abnormalRun)))
671-
val context = UserContext(job.msg.user)
672-
val msg = if (job.msg.blocking) {
673-
CombinedCompletionAndResultMessage(tid, result, instance)
674-
} else {
675-
CompletionMessage(tid, result, instance)
676-
}
677-
sendActiveAck(tid, result, job.msg.blocking, job.msg.rootControllerIndex, job.msg.user.namespace.uuid, msg)
678-
storeActivation(tid, result, job.msg.blocking, context)
679-
}
674+
if (abort && runBuffer.nonEmpty) {
675+
abortBuffered(abortResponse)
680676
} else {
681677
rejectBuffered()
682678
}
@@ -697,6 +693,35 @@ class ContainerProxy(factory: (TransactionId,
697693
}
698694
}
699695

696+
def abortBuffered(abortResponse: Option[ActivationResponse] = None) = {
697+
logging.info(this, s"aborting ${runBuffer.length} queued activations after failed init or failed cold start")
698+
runBuffer.foreach { job =>
699+
implicit val tid = job.msg.transid
700+
logging.info(
701+
this,
702+
s"aborting activation ${job.msg.activationId} after failed init or cold start with ${abortResponse}")
703+
val result = ContainerProxy.constructWhiskActivation(
704+
job,
705+
None,
706+
Interval.zero,
707+
false,
708+
abortResponse.getOrElse(ActivationResponse.whiskError(Messages.abnormalRun)))
709+
val context = UserContext(job.msg.user)
710+
val msg = if (job.msg.blocking) {
711+
CombinedCompletionAndResultMessage(tid, result, instance)
712+
} else {
713+
CompletionMessage(tid, result, instance)
714+
}
715+
sendActiveAck(tid, result, job.msg.blocking, job.msg.rootControllerIndex, job.msg.user.namespace.uuid, msg)
716+
.andThen {
717+
case Failure(e) => logging.error(this, s"failed to send abort ack $e")
718+
}
719+
storeActivation(tid, result, job.msg.blocking, context).andThen {
720+
case Failure(e) => logging.error(this, s"failed to store aborted activation $e")
721+
}
722+
}
723+
}
724+
700725
/**
701726
* Return any buffered jobs to parent, in case buffer is not empty at removal/error time.
702727
*/

tests/src/test/scala/org/apache/openwhisk/core/containerpool/test/ContainerProxyTests.scala

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1273,6 +1273,57 @@ class ContainerProxyTests
12731273
}
12741274
}
12751275

1276+
it should "terminate buffered concurrent activations when cold init fails to launch container" in {
1277+
assume(Option(WhiskProperties.getProperty("whisk.action.concurrency")).exists(_.toBoolean))
1278+
1279+
val initPromise = Promise[Interval]()
1280+
val container = new TestContainer(Some(initPromise))
1281+
val factory = createFactory(Future.failed(new Exception("simulating a container creation failure")))
1282+
val acker = createSyncAcker(concurrentAction)
1283+
val store = createSyncStore
1284+
val collector =
1285+
createCollector(Future.successful(ActivationLogs()), () => container.logs(0.MB, false)(TransactionId.testing))
1286+
1287+
val machine =
1288+
childActorOf(
1289+
ContainerProxy
1290+
.props(
1291+
factory,
1292+
acker,
1293+
store,
1294+
collector,
1295+
InvokerInstanceId(0, userMemory = defaultUserMemory),
1296+
poolConfig,
1297+
healthchecksConfig(),
1298+
pauseGrace = pauseGrace)
1299+
.withDispatcher(CallingThreadDispatcher.Id))
1300+
registerCallback(machine)
1301+
//no prewarming
1302+
1303+
machine ! Run(concurrentAction, message) //first in Uninitialized state
1304+
machine ! Run(concurrentAction, message) //second in Uninitialized or Running state
1305+
1306+
expectMsg(Transition(machine, Uninitialized, Running))
1307+
1308+
expectMsg(ContainerRemoved(true))
1309+
//go to Removing state when a failure happens while others are in flight
1310+
expectNoMessage(100.milliseconds)
1311+
awaitAssert {
1312+
factory.calls should have size 1
1313+
container.initializeCount shouldBe 0
1314+
container.runCount shouldBe 0
1315+
container.atomicLogsCount.get() shouldBe 0
1316+
container.suspendCount shouldBe 0
1317+
container.resumeCount shouldBe 0
1318+
acker.calls should have size 2
1319+
1320+
store.calls should have size 2
1321+
1322+
//we should have 2 activations that are whisk error
1323+
acker.calls.filter(_._2.response.isWhiskError) should have size 2
1324+
}
1325+
}
1326+
12761327
it should "complete the transaction and reuse the container on a failed run IFF failure was applicationError" in within(
12771328
timeout) {
12781329
val container = new TestContainer {

0 commit comments

Comments
 (0)