Skip to content

Commit 986142b

Browse files
authored
Isolate exceptions in event/status listeners (#314)
An exception in a user's listener shouldn't crash the whole event engine
1 parent 4e27a06 commit 986142b

File tree

4 files changed

+81
-28
lines changed

4 files changed

+81
-28
lines changed

pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/api/integration/PublishIntegrationTests.kt

-2
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,10 @@ import com.pubnub.api.callbacks.SubscribeCallback
88
import com.pubnub.api.crypto.CryptoModule
99
import com.pubnub.api.enums.PNStatusCategory
1010
import com.pubnub.api.models.consumer.PNBoundedPage
11-
import com.pubnub.api.models.consumer.PNPublishResult
1211
import com.pubnub.api.models.consumer.PNStatus
1312
import com.pubnub.api.models.consumer.pubsub.PNMessageResult
1413
import com.pubnub.api.v2.PNConfigurationOverride
1514
import com.pubnub.api.v2.callbacks.EventListener
16-
import com.pubnub.api.v2.callbacks.Result
1715
import com.pubnub.api.v2.callbacks.StatusListener
1816
import com.pubnub.api.v2.callbacks.getOrThrow
1917
import com.pubnub.api.v2.entities.Channel

pubnub-kotlin/pubnub-kotlin-impl/src/main/kotlin/com/pubnub/internal/managers/ListenerManager.kt

+32-19
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import com.pubnub.api.v2.callbacks.StatusListener
1818
import com.pubnub.api.v2.subscriptions.Subscription
1919
import com.pubnub.internal.subscribe.eventengine.effect.MessagesConsumer
2020
import com.pubnub.internal.subscribe.eventengine.effect.StatusConsumer
21+
import org.slf4j.LoggerFactory
2122
import java.util.concurrent.CopyOnWriteArrayList
2223

2324
class ListenerManager(val pubnub: PubNub) : MessagesConsumer, StatusConsumer, EventEmitter, StatusEmitter {
@@ -26,6 +27,8 @@ class ListenerManager(val pubnub: PubNub) : MessagesConsumer, StatusConsumer, Ev
2627
private val statusListeners get() = listeners.filterIsInstance<StatusListener>()
2728
private val eventListeners get() = listeners.filterIsInstance<EventListener>()
2829

30+
private val log = LoggerFactory.getLogger(this.javaClass.simpleName)
31+
2932
/**
3033
* Add a listener.
3134
*
@@ -65,49 +68,59 @@ class ListenerManager(val pubnub: PubNub) : MessagesConsumer, StatusConsumer, Ev
6568
}
6669

6770
override fun announce(status: PNStatus) {
68-
statusListeners.forEach { it.status(pubnub, status) }
71+
statusListeners.safeForEach { it.status(pubnub, status) }
6972
}
7073

7174
override fun announce(message: PNMessageResult) {
72-
eventListeners.forEach { it.message(pubnub, message) }
75+
eventListeners.safeForEach { it.message(pubnub, message) }
7376
val envelope = AnnouncementEnvelope(message)
74-
subscriptionCallbacks.forEach { it.message(pubnub, envelope) }
75-
setCallbacks.forEach { it.message(pubnub, envelope) }
77+
subscriptionCallbacks.safeForEach { it.message(pubnub, envelope) }
78+
setCallbacks.safeForEach { it.message(pubnub, envelope) }
7679
}
7780

7881
override fun announce(presence: PNPresenceEventResult) {
79-
eventListeners.forEach { it.presence(pubnub, presence) }
82+
eventListeners.safeForEach { it.presence(pubnub, presence) }
8083
val envelope = AnnouncementEnvelope(presence)
81-
subscriptionCallbacks.forEach { it.presence(pubnub, envelope) }
82-
setCallbacks.forEach { it.presence(pubnub, envelope) }
84+
subscriptionCallbacks.safeForEach { it.presence(pubnub, envelope) }
85+
setCallbacks.safeForEach { it.presence(pubnub, envelope) }
8386
}
8487

8588
override fun announce(signal: PNSignalResult) {
86-
eventListeners.forEach { it.signal(pubnub, signal) }
89+
eventListeners.safeForEach { it.signal(pubnub, signal) }
8790
val envelope = AnnouncementEnvelope(signal)
88-
subscriptionCallbacks.forEach { it.signal(pubnub, envelope) }
89-
setCallbacks.forEach { it.signal(pubnub, envelope) }
91+
subscriptionCallbacks.safeForEach { it.signal(pubnub, envelope) }
92+
setCallbacks.safeForEach { it.signal(pubnub, envelope) }
9093
}
9194

9295
override fun announce(messageAction: PNMessageActionResult) {
93-
eventListeners.forEach { it.messageAction(pubnub, messageAction) }
96+
eventListeners.safeForEach { it.messageAction(pubnub, messageAction) }
9497
val envelope = AnnouncementEnvelope(messageAction)
95-
subscriptionCallbacks.forEach { it.messageAction(pubnub, envelope) }
96-
setCallbacks.forEach { it.messageAction(pubnub, envelope) }
98+
subscriptionCallbacks.safeForEach { it.messageAction(pubnub, envelope) }
99+
setCallbacks.safeForEach { it.messageAction(pubnub, envelope) }
97100
}
98101

99102
override fun announce(pnObjectEventResult: PNObjectEventResult) {
100-
eventListeners.forEach { it.objects(pubnub, pnObjectEventResult) }
103+
eventListeners.safeForEach { it.objects(pubnub, pnObjectEventResult) }
101104
val envelope = AnnouncementEnvelope(pnObjectEventResult)
102-
subscriptionCallbacks.forEach { it.objects(pubnub, envelope) }
103-
setCallbacks.forEach { it.objects(pubnub, envelope) }
105+
subscriptionCallbacks.safeForEach { it.objects(pubnub, envelope) }
106+
setCallbacks.safeForEach { it.objects(pubnub, envelope) }
104107
}
105108

106109
override fun announce(pnFileEventResult: PNFileEventResult) {
107-
eventListeners.forEach { it.file(pubnub, pnFileEventResult) }
110+
eventListeners.safeForEach { it.file(pubnub, pnFileEventResult) }
108111
val envelope = AnnouncementEnvelope(pnFileEventResult)
109-
subscriptionCallbacks.forEach { it.file(pubnub, envelope) }
110-
setCallbacks.forEach { it.file(pubnub, envelope) }
112+
subscriptionCallbacks.safeForEach { it.file(pubnub, envelope) }
113+
setCallbacks.safeForEach { it.file(pubnub, envelope) }
114+
}
115+
116+
private inline fun <T> Iterable<T>.safeForEach(action: (T) -> Unit) {
117+
for (element in this) {
118+
try {
119+
action(element)
120+
} catch (e: Throwable) {
121+
log.warn("Uncaught exception in listener.", e)
122+
}
123+
}
111124
}
112125
}
113126

pubnub-kotlin/pubnub-kotlin-impl/src/main/kotlin/com/pubnub/internal/subscribe/eventengine/effect/EmitMessagesEffect.kt

+11-7
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,17 @@ internal class EmitMessagesEffect(
1919
override fun runEffect() {
2020
log.trace("Running EmitMessagesEffect")
2121
for (message in messages) {
22-
when (message) {
23-
is PNMessageResult -> messagesConsumer.announce(message)
24-
is PNPresenceEventResult -> messagesConsumer.announce(message)
25-
is PNSignalResult -> messagesConsumer.announce(message)
26-
is PNMessageActionResult -> messagesConsumer.announce(message)
27-
is PNObjectEventResult -> messagesConsumer.announce(message)
28-
is PNFileEventResult -> messagesConsumer.announce(message)
22+
try {
23+
when (message) {
24+
is PNMessageResult -> messagesConsumer.announce(message)
25+
is PNPresenceEventResult -> messagesConsumer.announce(message)
26+
is PNSignalResult -> messagesConsumer.announce(message)
27+
is PNMessageActionResult -> messagesConsumer.announce(message)
28+
is PNObjectEventResult -> messagesConsumer.announce(message)
29+
is PNFileEventResult -> messagesConsumer.announce(message)
30+
}
31+
} catch (_: Throwable) {
32+
// ignore
2933
}
3034
}
3135
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.pubnub.internal.managers
2+
3+
import com.google.gson.JsonPrimitive
4+
import com.pubnub.api.PubNub
5+
import com.pubnub.api.legacy.BaseTest
6+
import com.pubnub.api.models.consumer.pubsub.BasePubSubResult
7+
import com.pubnub.api.models.consumer.pubsub.PNMessageResult
8+
import com.pubnub.api.v2.callbacks.EventListener
9+
import org.junit.Test
10+
import org.junit.jupiter.api.Assertions.assertEquals
11+
12+
class ListenerManagerTest : BaseTest() {
13+
@Test
14+
fun `exception in listener is isolated from other listeners`() {
15+
// given
16+
val listenerManager = ListenerManager(pubnub)
17+
val exception = Exception("Crash!")
18+
var received = mutableListOf<Any>()
19+
20+
listenerManager.addListener(object : EventListener {
21+
override fun message(pubnub: PubNub, result: PNMessageResult) {
22+
received += exception
23+
throw exception
24+
}
25+
})
26+
listenerManager.addListener(object : EventListener {
27+
override fun message(pubnub: PubNub, result: PNMessageResult) {
28+
received += true
29+
}
30+
})
31+
32+
// when
33+
listenerManager.announce(PNMessageResult(BasePubSubResult("a", null, null, null, null), JsonPrimitive("a")))
34+
35+
// then
36+
assertEquals(listOf(exception, true), received)
37+
}
38+
}

0 commit comments

Comments
 (0)