@@ -47,8 +47,9 @@ abstract class Actor<Req : ActorProtocol, Res : ActorProtocol.Response>(
4747 protected val log: Logger = ActorSystem .loggerFactory.getLogger(this ::class )
4848
4949 private val stats: Stats = Stats ()
50- private var status = Status .CREATED
51- private var initializationFailed: Exception ? = null
50+ private var status: Status = Status .CREATED
51+ private var initializationError: Throwable ? = null
52+ private var terminationError: Throwable ? = null
5253 private val address: Address = Address .of(this ::class , key)
5354 private val ref: LocalRef = LocalRef (address = address, actor = this )
5455
@@ -164,10 +165,10 @@ abstract class Actor<Req : ActorProtocol, Res : ActorProtocol.Response>(
164165 onBeforeActivate()
165166 // Set 'ACTIVATING' status.
166167 status = Status .ACTIVATING
167- } catch (e: Exception ) {
168+ } catch (e: Throwable ) {
168169 // In case of an error, we need to close the [Actor] immediately.
169170 log.error(" [$address ::onBeforeActivate] Failed to activate, will shutdown (${e.message ? : " " } )" )
170- initializationFailed = e
171+ initializationError = e
171172 shutdown()
172173 }
173174
@@ -178,11 +179,18 @@ abstract class Actor<Req : ActorProtocol, Res : ActorProtocol.Response>(
178179 // Case that activation flow failed and we still have messages to consume.
179180 // If we get a shutdown event and the actor never initialized successfully,
180181 // we need to reply with an error and to drop all the messages.
181- if (initializationFailed != null ) {
182+ if (initializationError != null ) {
182183 replyActivationError(pattern)
183184 return @consumeEach
184185 }
185186
187+ // Case that termination flow was triggered.
188+ // In this case, we need to reply with an error and to drop all the messages.
189+ if (terminationError != null ) {
190+ replyTerminationError(pattern)
191+ return @consumeEach
192+ }
193+
186194 val msg: Req = pattern.msg.apply {
187195 id = stats.receivedMessages
188196 kind = pattern.messageKind
@@ -195,10 +203,10 @@ abstract class Actor<Req : ActorProtocol, Res : ActorProtocol.Response>(
195203 // Set 'READY' status.
196204 status = Status .READY
197205 stats.initializedAt = Clock .System .now().toEpochMilliseconds()
198- } catch (e: Exception ) {
206+ } catch (e: Throwable ) {
199207 // In case of an error, we need to close the [Actor] immediately.
200208 log.error(" [$address ::activate] Failed to activate, will shutdown (${e.message ? : " " } )" )
201- initializationFailed = e
209+ initializationError = e
202210 replyActivationError(pattern)
203211 shutdown()
204212 return @consumeEach
@@ -234,7 +242,7 @@ abstract class Actor<Req : ActorProtocol, Res : ActorProtocol.Response>(
234242
235243 else -> r
236244 }
237- } catch (e: Exception ) {
245+ } catch (e: Throwable ) {
238246 Behavior .Error (e)
239247 }
240248
@@ -262,7 +270,7 @@ abstract class Actor<Req : ActorProtocol, Res : ActorProtocol.Response>(
262270 // Handle afterReceive hook.
263271 try {
264272 afterReceive(msg, result)
265- } catch (e: Exception ) {
273+ } catch (e: Throwable ) {
266274 log.warn(" [$address ::afterReceive] Failed to process afterReceive hook (${e.message ? : " " } )" )
267275 }
268276 }
@@ -272,12 +280,13 @@ abstract class Actor<Req : ActorProtocol, Res : ActorProtocol.Response>(
272280 // This allows Router workers (FIRST_AVAILABLE) to re-register availability, avoiding deadlocks.
273281 try {
274282 afterReceive(msg)
275- } catch (e: Exception ) {
283+ } catch (e: Throwable ) {
276284 log.warn(" [$address ::afterReceive] Failed to process afterReceive hook for Behavior.None (${e.message ? : " " } )" )
277285 }
278286 }
279287
280288 is Behavior .Shutdown -> shutdown()
289+ is Behavior .Terminate -> terminate()
281290 }
282291 }
283292
@@ -323,7 +332,7 @@ abstract class Actor<Req : ActorProtocol, Res : ActorProtocol.Response>(
323332 mail.send(ask)
324333 ask.replyTo.receive()
325334 }
326- } catch (e: Exception ) {
335+ } catch (e: Throwable ) {
327336 Result .failure(e)
328337 } finally {
329338 ask.replyTo.close()
@@ -391,6 +400,8 @@ abstract class Actor<Req : ActorProtocol, Res : ActorProtocol.Response>(
391400 // Drain the stash channel.
392401 @OptIn(ExperimentalCoroutinesApi ::class )
393402 while (! stash.isEmpty) {
403+ // Skip processing if actor cannot handle messages (drain the stash).
404+ if (status == Status .SHUT_DOWN || status == Status .TERMINATED ) continue
394405 val pattern = stash.receive()
395406 stats.stashedMessages - = 1
396407 process(pattern)
@@ -415,6 +426,24 @@ abstract class Actor<Req : ActorProtocol, Res : ActorProtocol.Response>(
415426 stash.close()
416427 }
417428
429+ /* *
430+ * Initiates the termination process for the system or component.
431+ *
432+ * This method checks whether termination can be triggered based on the current status.
433+ * If termination is allowed, it updates the termination timestamp, changes the status
434+ * to `TERMINATING`, and closes associated resources such as mail and stash.
435+ *
436+ * The method has no effect if termination is not permitted.
437+ */
438+ fun terminate (error : Throwable ? = null) {
439+ if (! status.canTriggerTermination) return
440+ stats.triggeredTerminationAt = Clock .System .now().toEpochMilliseconds()
441+ terminationError = error ? : Exception (" Actor terminated by user." )
442+ status = Status .TERMINATING
443+ mail.close()
444+ stash.close()
445+ }
446+
418447 /* *
419448 * Represents message patterns used by the `Actor` for communication and message handling.
420449 *
@@ -479,17 +508,22 @@ abstract class Actor<Req : ActorProtocol, Res : ActorProtocol.Response>(
479508 * - `READY`: The actor is fully initialized and ready to process messages. Messages can be accepted during this state.
480509 * - `SHUTTING_DOWN`: The actor is in the process of shutting down. Messages cannot be accepted during this state.
481510 * - `SHUT_DOWN`: The actor has completed the shutdown process. Messages cannot be accepted during this state.
511+ * - `TERMINATING`: The actor is in the process of terminating. Messages cannot be accepted during this state.
512+ * - `TERMINATED`: The actor has terminated and is no longer available for use. Messages cannot be accepted during this state.
482513 */
483514 @Serializable
484515 enum class Status (
485516 val canAcceptMessages : Boolean ,
486517 val canTriggerShutdown : Boolean ,
518+ val canTriggerTermination : Boolean ,
487519 ) {
488- CREATED (true , true ),
489- ACTIVATING (true , true ),
490- READY (true , true ),
491- SHUTTING_DOWN (false , false ),
492- SHUT_DOWN (false , false )
520+ CREATED (true , true , true ),
521+ ACTIVATING (true , true , true ),
522+ READY (true , true , true ),
523+ SHUTTING_DOWN (false , false , false ),
524+ SHUT_DOWN (false , false , false ),
525+ TERMINATING (false , false , false ),
526+ TERMINATED (false , false , false );
493527 }
494528
495529 /* *
@@ -513,6 +547,8 @@ abstract class Actor<Req : ActorProtocol, Res : ActorProtocol.Response>(
513547 var initializedAt : Long? = null ,
514548 var triggeredShutDownAt : Long? = null ,
515549 var shutDownAt : Long? = null ,
550+ var triggeredTerminationAt : Long? = null ,
551+ var terminatedAt : Long? = null ,
516552 var lastMessageAt : Long = Clock .System .now().toEpochMilliseconds(),
517553 var receivedMessages : Long = 0 ,
518554 var stashedMessages : Long = 0
@@ -523,6 +559,8 @@ abstract class Actor<Req : ActorProtocol, Res : ActorProtocol.Response>(
523559 append(" initializedAt=${instantFromEpochMilliseconds(initializedAt)} , " )
524560 append(" triggeredShutDownAt=${instantFromEpochMilliseconds(triggeredShutDownAt)} , " )
525561 append(" shutDownAt=${instantFromEpochMilliseconds(shutDownAt)} , " )
562+ append(" triggeredTerminationAt=${instantFromEpochMilliseconds(triggeredTerminationAt)} , " )
563+ append(" terminatedAt=${instantFromEpochMilliseconds(terminatedAt)} , " )
526564 append(" lastMessageAt=${instantFromEpochMilliseconds(lastMessageAt)} , " )
527565 append(" receivedMessages=${instantFromEpochMilliseconds(receivedMessages)} , " )
528566 append(" stashedMessages=$stashedMessages " )
@@ -544,7 +582,7 @@ abstract class Actor<Req : ActorProtocol, Res : ActorProtocol.Response>(
544582 for (e in this ) {
545583 try {
546584 action(e)
547- } catch (e: Exception ) {
585+ } catch (e: Throwable ) {
548586 log.warn(" [$address ::consume] An error occurred while processing. ${e.message ? : " " } " )
549587 }
550588 }
@@ -561,12 +599,16 @@ abstract class Actor<Req : ActorProtocol, Res : ActorProtocol.Response>(
561599 }
562600 } catch (_: TimeoutCancellationException ) {
563601 log.error(" [$address ::onShutdown] Shutdown hook timed out after ${ActorSystem .conf.actorShutdownHookTimeout} . Forcing shutdown." )
564- } catch (e: Exception ) {
602+ } catch (e: Throwable ) {
565603 log.error(" [$address ::onShutdown] Error during shutdown hook: ${e.message ? : " Unknown error" } " )
566604 } finally {
567- // Unregister the actor even if the shutdown hook fails or times out
568- status = Status .SHUT_DOWN
569- stats.shutDownAt = Clock .System .now().toEpochMilliseconds()
605+ if (terminationError != null ) {
606+ status = Status .TERMINATED
607+ stats.terminatedAt = Clock .System .now().toEpochMilliseconds()
608+ } else {
609+ status = Status .SHUT_DOWN
610+ stats.shutDownAt = Clock .System .now().toEpochMilliseconds()
611+ }
570612 ActorSystem .registry.unregister(this @Actor.ref)
571613 }
572614 }
@@ -591,7 +633,7 @@ abstract class Actor<Req : ActorProtocol, Res : ActorProtocol.Response>(
591633 log.warn(" [$address ::$operation ] Could not reply in time (timeout after ${ActorSystem .conf.actorReplyTimeout} ) (the message was processed successfully)." )
592634 } catch (_: ClosedSendChannelException ) {
593635 log.warn(" [$address ::$operation ] Could not reply, the channel is closed (the message was processed successfully)." )
594- } catch (e: Exception ) {
636+ } catch (e: Throwable ) {
595637 val error = e.message ? : " Unknown error."
596638 log.warn(" [$address ::$operation ] Could not reply (the message was processed successfully). {}" , error)
597639 }
@@ -607,14 +649,32 @@ abstract class Actor<Req : ActorProtocol, Res : ActorProtocol.Response>(
607649 when (pattern) {
608650 is Patterns .Tell -> Unit
609651 is Patterns .Ask -> {
610- val e = initializationFailed
652+ val e = initializationError
611653 ? : IllegalStateException (" Actor is prematurely closed (could not be initialized)." )
612654 val r: Result <Res > = Result .failure(e)
613655 reply(operation = " activate" , pattern = pattern, reply = r)
614656 }
615657 }
616658 }
617659
660+ /* *
661+ * Handles the termination error scenario for the provided message pattern.
662+ *
663+ * @param pattern The message pattern that specifies the type of interaction (e.g., Tell or Ask)
664+ * and determines how the termination error is managed.
665+ */
666+ private suspend fun replyTerminationError (pattern : Patterns <Req , Res >) {
667+ when (pattern) {
668+ is Patterns .Tell -> Unit
669+ is Patterns .Ask -> {
670+ val e = terminationError
671+ ? : IllegalStateException (" Actor is terminated, all pending messages are replied with errors." )
672+ val r: Result <Res > = Result .failure(e)
673+ reply(operation = " terminate" , pattern = pattern, reply = r)
674+ }
675+ }
676+ }
677+
618678 companion object {
619679 /* *
620680 * Generates a unique random key as a string, which includes a "key-" prefix followed by a hash code
0 commit comments