Skip to content

Commit b751a94

Browse files
Mc/send heartbeat when presence no op (#341)
* Fix: when heartbeat interval is 0 SDK should send heartbeat on subscribe. * PubNub SDK v10.4.7 release. --------- Co-authored-by: PubNub Release Bot <[email protected]>
1 parent 2643a15 commit b751a94

File tree

16 files changed

+393
-151
lines changed

16 files changed

+393
-151
lines changed

.pubnub.yml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
name: kotlin
2-
version: 10.4.6
2+
version: 10.4.7
33
schema: 1
44
scm: github.com/pubnub/kotlin
55
files:
6-
- build/libs/pubnub-kotlin-10.4.6-all.jar
6+
- build/libs/pubnub-kotlin-10.4.7-all.jar
77
sdks:
88
-
99
type: library
@@ -23,8 +23,8 @@ sdks:
2323
-
2424
distribution-type: library
2525
distribution-repository: maven
26-
package-name: pubnub-kotlin-10.4.6
27-
location: https://repo.maven.apache.org/maven2/com/pubnub/pubnub-kotlin/10.4.6/pubnub-kotlin-10.4.6.jar
26+
package-name: pubnub-kotlin-10.4.7
27+
location: https://repo.maven.apache.org/maven2/com/pubnub/pubnub-kotlin/10.4.7/pubnub-kotlin-10.4.7.jar
2828
supported-platforms:
2929
supported-operating-systems:
3030
Android:
@@ -121,6 +121,11 @@ sdks:
121121
license-url: https://www.apache.org/licenses/LICENSE-2.0.txt
122122
is-required: Required
123123
changelog:
124+
- date: 2025-04-15
125+
version: v10.4.7
126+
changes:
127+
- type: bug
128+
text: "Heartbeat is sent for subscribe when presenceTimeout or heartbeatInterval not set."
124129
- date: 2025-03-20
125130
version: v10.4.6
126131
changes:

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## v10.4.7
2+
April 15 2025
3+
4+
#### Fixed
5+
- Heartbeat is sent for subscribe when presenceTimeout or heartbeatInterval not set.
6+
17
## v10.4.6
28
March 20 2025
39

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ You will need the publish and subscribe keys to authenticate your app. Get your
2020
<dependency>
2121
<groupId>com.pubnub</groupId>
2222
<artifactId>pubnub-kotlin</artifactId>
23-
<version>10.4.6</version>
23+
<version>10.4.7</version>
2424
</dependency>
2525
```
2626

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ RELEASE_SIGNING_ENABLED=true
1818
SONATYPE_HOST=DEFAULT
1919
SONATYPE_AUTOMATIC_RELEASE=false
2020
GROUP=com.pubnub
21-
VERSION_NAME=10.4.6
21+
VERSION_NAME=10.4.7
2222
POM_PACKAGING=jar
2323

2424
POM_NAME=PubNub SDK

pubnub-gson/pubnub-gson-impl/config/ktlint/baseline.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@
44
<error line="1" column="1" source="standard:no-empty-file" />
55
</file>
66
<file name="src/test/kotlin/com/pubnub/api/PNConfigurationTest.kt">
7-
<error line="120" column="13" source="standard:function-naming" />
7+
<error line="93" column="13" source="standard:function-naming" />
88
</file>
99
</baseline>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import static com.pubnub.api.enums.PNLogVerbosity.BODY;
1212

1313

14-
public class ReconnectionProblemWithoutReconnectionPolicyIT extends AbstractReconnectionProblemIT {
14+
public class ReconnectionProblemWithoutRetryConfigurationIT extends AbstractReconnectionProblemIT {
1515
@Override
1616
protected @NotNull PubNub privilegedClientPubNub() {
1717
com.pubnub.api.java.v2.PNConfiguration.Builder pnConfiguration;

pubnub-gson/pubnub-gson-impl/src/test/kotlin/com/pubnub/api/PNConfigurationTest.kt

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -62,33 +62,6 @@ class PNConfigurationTest {
6262
// }
6363
//
6464
// @Test
65-
// fun `reconnection policy should set retry configuration`() {
66-
// val config = PNConfiguration(UserId(PubNub.generateUUID()))
67-
// config.setReconnectionPolicy(PNReconnectionPolicy.NONE)
68-
// Assert.assertTrue(config.retryConfiguration is RetryConfiguration.None)
69-
//
70-
// config.setReconnectionPolicy(PNReconnectionPolicy.LINEAR)
71-
// Assert.assertTrue(config.retryConfiguration is RetryConfiguration.Linear)
72-
//
73-
// config.setReconnectionPolicy(PNReconnectionPolicy.EXPONENTIAL)
74-
// Assert.assertTrue(config.retryConfiguration is RetryConfiguration.Exponential)
75-
// }
76-
//
77-
// @Test
78-
// fun `maximumReconnectionRetries policy should reset retry configuration`() {
79-
// val config = PNConfiguration(UserId(PubNub.generateUUID()))
80-
//
81-
// config.setReconnectionPolicy(PNReconnectionPolicy.LINEAR)
82-
// config.setMaximumReconnectionRetries(5)
83-
// Assert.assertTrue(config.retryConfiguration is RetryConfiguration.Linear)
84-
// Assert.assertEquals(5, (config.retryConfiguration as RetryConfiguration.Linear).maxRetryNumber)
85-
//
86-
// config.setMaximumReconnectionRetries(10)
87-
// Assert.assertTrue(config.retryConfiguration is RetryConfiguration.Linear)
88-
// Assert.assertEquals(10, (config.retryConfiguration as RetryConfiguration.Linear).maxRetryNumber)
89-
// }
90-
//
91-
// @Test
9265
// fun `cryptomodule uses cipherKey when cryptomodule is not set`() {
9366
// val config = PNConfiguration(UserId(PubNub.generateUUID()))
9467
//

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

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import com.pubnub.api.callbacks.SubscribeCallback
55
import com.pubnub.api.enums.PNStatusCategory
66
import com.pubnub.api.models.consumer.PNStatus
77
import com.pubnub.api.models.consumer.pubsub.PNPresenceEventResult
8+
import com.pubnub.api.v2.callbacks.EventListener
89
import com.pubnub.api.v2.subscriptions.SubscriptionOptions
910
import com.pubnub.test.CommonUtils
11+
import com.pubnub.test.CommonUtils.DEFAULT_LISTEN_DURATION
1012
import com.pubnub.test.CommonUtils.generatePayload
1113
import com.pubnub.test.CommonUtils.randomChannel
1214
import com.pubnub.test.listen
@@ -58,6 +60,67 @@ class PresenceEventsIntegrationTests : BaseIntegrationTest() {
5860
success.listen()
5961
}
6062

63+
@Test
64+
fun testMultipleSubscribeShouldCauseJoinEventToAppear() {
65+
// “Generate Leave on TCP FIN or RST” should be disabled
66+
val countDownLatchForJoinChannel01 = CountDownLatch(1)
67+
val countDownLatchForJoinChannel02 = CountDownLatch(1)
68+
val countDownLatchForJoinChannel03 = CountDownLatch(1)
69+
val channel01Name = randomChannel() + "chan01"
70+
val channel02Name = randomChannel() + "chan02"
71+
val channel03Name = randomChannel() + "chan03"
72+
73+
pubnub.addListener(object : EventListener {
74+
override fun presence(pubnub: PubNub, result: PNPresenceEventResult) {
75+
if (result.event == "join") {
76+
when (result.channel) {
77+
channel01Name -> countDownLatchForJoinChannel01.countDown()
78+
channel02Name -> countDownLatchForJoinChannel02.countDown()
79+
channel03Name -> countDownLatchForJoinChannel03.countDown()
80+
}
81+
}
82+
}
83+
})
84+
85+
pubnub.subscribe(channels = listOf(channel01Name), withPresence = true)
86+
Thread.sleep(2000)
87+
pubnub.subscribe(channels = listOf(channel01Name, channel02Name), withPresence = true)
88+
Thread.sleep(2000)
89+
pubnub.subscribe(channels = listOf(channel03Name), withPresence = true)
90+
91+
assertTrue(countDownLatchForJoinChannel01.await(DEFAULT_LISTEN_DURATION.toLong(), TimeUnit.SECONDS))
92+
assertTrue(countDownLatchForJoinChannel02.await(DEFAULT_LISTEN_DURATION.toLong(), TimeUnit.SECONDS))
93+
assertTrue(countDownLatchForJoinChannel03.await(DEFAULT_LISTEN_DURATION.toLong(), TimeUnit.SECONDS))
94+
}
95+
96+
@Test
97+
fun testMultipleSubscribeOnChannelEntitiesShouldCauseJoinEventToAppear() {
98+
// “Generate Leave on TCP FIN or RST” should be disabled
99+
val countDownLatchForJoinPubNubUser = CountDownLatch(1)
100+
val countDownLatchForJoinPubQuestUser = CountDownLatch(1)
101+
val channel01Name = randomChannel() + "chan01"
102+
103+
val subscription01 = pubnub.channel(channel01Name).subscription(SubscriptionOptions.receivePresenceEvents())
104+
val subscription02 = guest.channel(channel01Name).subscription(SubscriptionOptions.receivePresenceEvents())
105+
106+
subscription01.onPresence = { pnPresenceEventResult: PNPresenceEventResult ->
107+
if (pubnub.configuration.userId.value == pnPresenceEventResult.uuid && pnPresenceEventResult.event == "join") {
108+
countDownLatchForJoinPubNubUser.countDown()
109+
}
110+
if (guest.configuration.userId.value == pnPresenceEventResult.uuid && pnPresenceEventResult.event == "join") {
111+
countDownLatchForJoinPubQuestUser.countDown()
112+
}
113+
}
114+
115+
subscription01.subscribe()
116+
Thread.sleep(2000)
117+
subscription02.subscribe()
118+
Thread.sleep(2000)
119+
120+
assertTrue(countDownLatchForJoinPubNubUser.await(DEFAULT_LISTEN_DURATION.toLong(), TimeUnit.SECONDS))
121+
assertTrue(countDownLatchForJoinPubQuestUser.await(DEFAULT_LISTEN_DURATION.toLong(), TimeUnit.SECONDS))
122+
}
123+
61124
@Test
62125
fun testJoinChannelUsingOnPresenceField() {
63126
val successPresenceEventCont = AtomicInteger()

pubnub-kotlin/pubnub-kotlin-impl/src/main/kotlin/com/pubnub/internal/presence/Presence.kt

Lines changed: 67 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package com.pubnub.internal.presence
22

3+
import com.pubnub.api.PubNubException
34
import com.pubnub.api.enums.PNHeartbeatNotificationOptions
5+
import com.pubnub.api.enums.PNStatusCategory
6+
import com.pubnub.api.models.consumer.PNStatus
47
import com.pubnub.internal.eventengine.EffectDispatcher
58
import com.pubnub.internal.eventengine.EventEngineConf
69
import com.pubnub.internal.managers.ListenerManager
@@ -31,37 +34,41 @@ internal interface Presence {
3134
executorService: ScheduledExecutorService,
3235
): Presence {
3336
if (heartbeatInterval <= Duration.ZERO) {
34-
return PresenceNoOp(suppressLeaveEvents, leaveProvider)
35-
}
36-
37-
val effectFactory =
38-
PresenceEffectFactory(
39-
heartbeatProvider = heartbeatProvider,
40-
leaveProvider = leaveProvider,
41-
presenceEventSink = eventEngineConf.eventSink,
42-
executorService = executorService,
43-
heartbeatInterval = heartbeatInterval,
44-
suppressLeaveEvents = suppressLeaveEvents,
45-
heartbeatNotificationOptions = heartbeatNotificationOptions,
46-
statusConsumer = listenerManager,
47-
presenceData = presenceData,
48-
sendStateWithHeartbeat = sendStateWithHeartbeat,
37+
return PresenceNoOp(
38+
suppressLeaveEvents,
39+
leaveProvider,
40+
heartbeatProvider,
41+
listenerManager,
42+
heartbeatNotificationOptions,
43+
presenceData,
44+
sendStateWithHeartbeat
4945
)
46+
}
5047

51-
val eventEngineManager =
52-
PresenceEventEngineManager(
53-
eventEngine =
54-
PresenceEventEngine(
55-
effectSink = eventEngineConf.effectSink,
56-
eventSource = eventEngineConf.eventSource,
57-
),
58-
eventSink = eventEngineConf.eventSink,
59-
effectDispatcher =
60-
EffectDispatcher(
61-
effectFactory = effectFactory,
62-
effectSource = eventEngineConf.effectSource,
63-
),
64-
).also { it.start() }
48+
val effectFactory = PresenceEffectFactory(
49+
heartbeatProvider = heartbeatProvider,
50+
leaveProvider = leaveProvider,
51+
presenceEventSink = eventEngineConf.eventSink,
52+
executorService = executorService,
53+
heartbeatInterval = heartbeatInterval,
54+
suppressLeaveEvents = suppressLeaveEvents,
55+
heartbeatNotificationOptions = heartbeatNotificationOptions,
56+
statusConsumer = listenerManager,
57+
presenceData = presenceData,
58+
sendStateWithHeartbeat = sendStateWithHeartbeat,
59+
)
60+
61+
val eventEngineManager = PresenceEventEngineManager(
62+
eventEngine = PresenceEventEngine(
63+
effectSink = eventEngineConf.effectSink,
64+
eventSource = eventEngineConf.eventSource,
65+
),
66+
eventSink = eventEngineConf.eventSink,
67+
effectDispatcher = EffectDispatcher(
68+
effectFactory = effectFactory,
69+
effectSource = eventEngineConf.effectSource,
70+
),
71+
).also { it.start() }
6572

6673
return EnabledPresence(eventEngineManager)
6774
}
@@ -101,6 +108,11 @@ internal interface Presence {
101108
internal class PresenceNoOp(
102109
private val suppressLeaveEvents: Boolean = false,
103110
private val leaveProvider: LeaveProvider,
111+
private val heartbeatProvider: HeartbeatProvider,
112+
private val listenerManager: ListenerManager,
113+
private val heartbeatNotificationOptions: PNHeartbeatNotificationOptions,
114+
private val presenceData: PresenceData,
115+
private val sendStateWithHeartbeat: Boolean,
104116
) : Presence {
105117
private val log = LoggerFactory.getLogger(PresenceNoOp::class.java)
106118
private val channels = mutableSetOf<String>()
@@ -113,6 +125,32 @@ internal class PresenceNoOp(
113125
) {
114126
this.channels.addAll(channels)
115127
this.channelGroups.addAll(channelGroups)
128+
129+
// for subscribe with tt=0 this heartbeat call will be not needed because server generates implicit heartbeat for it
130+
// but for now we don't offer possibility to enable smartHeartbeat that would eliminate this shortcoming.
131+
if (channels.isNotEmpty() || channelGroups.isNotEmpty()) {
132+
heartbeatProvider.getHeartbeatRemoteAction(
133+
channels = channels,
134+
channelGroups = channelGroups,
135+
state = if (sendStateWithHeartbeat) {
136+
presenceData.channelStates
137+
} else {
138+
null
139+
},
140+
).async { result ->
141+
result.onFailure { exception ->
142+
if (heartbeatNotificationOptions == PNHeartbeatNotificationOptions.ALL ||
143+
heartbeatNotificationOptions == PNHeartbeatNotificationOptions.FAILURES
144+
) {
145+
listenerManager.announce(PNStatus(PNStatusCategory.PNHeartbeatFailed, PubNubException.from(exception)))
146+
}
147+
}.onSuccess {
148+
if (heartbeatNotificationOptions == PNHeartbeatNotificationOptions.ALL) {
149+
listenerManager.announce(PNStatus(PNStatusCategory.PNHeartbeatSuccess))
150+
}
151+
}
152+
}
153+
}
116154
}
117155

118156
@Synchronized

pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/api/legacy/BaseTest.kt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ package com.pubnub.api.legacy
22

33
import com.github.tomakehurst.wiremock.WireMockServer
44
import com.github.tomakehurst.wiremock.client.WireMock
5+
import com.github.tomakehurst.wiremock.client.WireMock.aResponse
6+
import com.github.tomakehurst.wiremock.client.WireMock.get
7+
import com.github.tomakehurst.wiremock.client.WireMock.stubFor
8+
import com.github.tomakehurst.wiremock.client.WireMock.urlMatching
59
import com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig
610
import com.pubnub.api.UserId
711
import com.pubnub.api.enums.PNLogVerbosity
@@ -71,4 +75,36 @@ abstract class BaseTest {
7175
fun clearConfiguration() {
7276
config = PNConfiguration.builder(userId = UserId(PubNubImpl.generateUUID()), "")
7377
}
78+
79+
protected fun stubForHeartbeatWhenHeartbeatIntervalIs0ThusPresenceEEDoesNotWork(
80+
channels: Set<String>,
81+
channelGroups: Set<String> = emptySet()
82+
) {
83+
val channelsAsString = if (channels.isEmpty()) {
84+
","
85+
} else {
86+
channels.joinToString(separator = ",")
87+
}
88+
val channelGroupsAsString = if (channelGroups.isEmpty()) {
89+
null
90+
} else {
91+
channelGroups.joinToString(separator = "%2C") // URL encode the comma separator
92+
}
93+
94+
val baseUrl = "/v2/presence/sub-key/mySubscribeKey/channel/$channelsAsString/heartbeat"
95+
val urlPattern = if (channelGroupsAsString != null) {
96+
"$baseUrl\\?.*channel-group=$channelGroupsAsString.*"
97+
} else {
98+
"$baseUrl\\?.*"
99+
}
100+
stubFor(
101+
get(urlMatching(urlPattern)).willReturn(
102+
aResponse().withBody(
103+
"""
104+
{"message":"OK","service":"Presence","status":200}
105+
""".trimIndent()
106+
)
107+
)
108+
)
109+
}
74110
}

pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/api/legacy/PubNubImplTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ class PubNubImplTest : BaseTest() {
5656
fun getVersionAndTimeStamp() {
5757
val version = PubNubImpl.SDK_VERSION
5858
val timeStamp = PubNubImpl.timestamp()
59-
assertEquals("10.4.6", version)
59+
assertEquals("10.4.7", version)
6060
assertTrue(timeStamp > 0)
6161
}
6262

0 commit comments

Comments
 (0)