Skip to content

Commit f65ba25

Browse files
author
Daniel Rees
authored
Moving heartbeat task to repeat at fixed rate (#52)
1 parent 86a20a2 commit f65ba25

File tree

5 files changed

+64
-17
lines changed

5 files changed

+64
-17
lines changed

src/main/kotlin/org/phoenixframework/DispatchQueue.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ import java.util.concurrent.TimeUnit
3636
interface DispatchQueue {
3737
/** Queue a Runnable to be executed after a given time unit delay */
3838
fun queue(delay: Long, unit: TimeUnit, runnable: () -> Unit): DispatchWorkItem
39+
40+
/**
41+
* Creates and executes a periodic action that becomes enabled first after the given initial
42+
* delay, and subsequently with the given period; that is, executions will commence after
43+
* initialDelay, then initialDelay + period, then initialDelay + 2 * period, and so on.
44+
*/
45+
fun queueAtFixedRate(delay: Long, period: Long, unit: TimeUnit, runnable: () -> Unit): DispatchWorkItem
3946
}
4047

4148
/** Abstracts away a future task */
@@ -64,6 +71,16 @@ class ScheduledDispatchQueue(poolSize: Int = 8) : DispatchQueue {
6471
val scheduledFuture = scheduledThreadPoolExecutor.schedule(runnable, delay, unit)
6572
return ScheduledDispatchWorkItem(scheduledFuture)
6673
}
74+
75+
override fun queueAtFixedRate(
76+
delay: Long,
77+
period: Long,
78+
unit: TimeUnit,
79+
runnable: () -> Unit
80+
): DispatchWorkItem {
81+
val scheduledFuture = scheduledThreadPoolExecutor.scheduleAtFixedRate(runnable, delay, period, unit)
82+
return ScheduledDispatchWorkItem(scheduledFuture)
83+
}
6784
}
6885

6986
/**

src/main/kotlin/org/phoenixframework/Socket.kt

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,6 @@ const val WS_CLOSE_NORMAL = 1000
5454
/** The socket was closed due to a SocketException. Likely the client lost connectivity */
5555
const val WS_CLOSE_SOCKET_EXCEPTION = 4000
5656

57-
/** The socket was closed due to an SSLException. Likely the client lost connectivity */
58-
const val WS_CLOSE_SSL_EXCEPTION = 4001
59-
60-
/** The socket was closed due to an EOFException. Likely the server abruptly closed */
61-
const val WS_CLOSE_EOF_EXCEPTION = 4002
6257

6358
/**
6459
* Connects to a Phoenix Server
@@ -366,8 +361,11 @@ class Socket(
366361

367362
// Do not start up the heartbeat timer if skipHeartbeat is true
368363
if (skipHeartbeat) return
364+
val delay = heartbeatInterval
365+
val period = heartbeatInterval
366+
369367
heartbeatTask =
370-
dispatchQueue.queue(heartbeatInterval, TimeUnit.MILLISECONDS) { sendHeartbeat() }
368+
dispatchQueue.queueAtFixedRate(delay, period, TimeUnit.MILLISECONDS) { sendHeartbeat() }
371369
}
372370

373371
internal fun sendHeartbeat() {

src/test/kotlin/org/phoenixframework/SocketTest.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,7 @@ class SocketTest {
503503

504504
socket.onConnectionOpened()
505505
verify(mockTask).cancel()
506-
verify(mockDispatchQueue).queue(any(), any(), any())
506+
verify(mockDispatchQueue).queueAtFixedRate(any(), any(), any(), any())
507507
}
508508

509509
@Test
@@ -541,7 +541,7 @@ class SocketTest {
541541
@Test
542542
fun `resetHeartbeat() creates a future heartbeat task`() {
543543
val mockTask = mock<DispatchWorkItem>()
544-
whenever(mockDispatchQueue.queue(any(), any(), any())).thenReturn(mockTask)
544+
whenever(mockDispatchQueue.queueAtFixedRate(any(), any(), any(), any())).thenReturn(mockTask)
545545

546546
whenever(connection.readyState).thenReturn(Transport.ReadyState.OPEN)
547547
socket.connect()
@@ -552,7 +552,7 @@ class SocketTest {
552552

553553
assertThat(socket.heartbeatTask).isNotNull()
554554
argumentCaptor<() -> Unit> {
555-
verify(mockDispatchQueue).queue(eq(5_000L), eq(TimeUnit.MILLISECONDS), capture())
555+
verify(mockDispatchQueue).queueAtFixedRate(eq(5_000L), eq(5_000L), eq(TimeUnit.MILLISECONDS), capture())
556556

557557
// fire the task
558558
allValues.first().invoke()

src/test/kotlin/org/phoenixframework/TestUtilities.kt

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class ManualDispatchQueue : DispatchQueue {
2424

2525
// Filter all work items that are due to be fired and have not been
2626
// cancelled. Return early if there are no items to fire
27-
val pastDueWorkItems = workItems.filter { it.deadline <= this.tickTime && !it.isCancelled }
27+
val pastDueWorkItems = workItems.filter { it.isPastDue(tickTime) && !it.isCancelled }
2828

2929
// if no items are due, then return early
3030
if (pastDueWorkItems.isEmpty()) return
@@ -33,10 +33,9 @@ class ManualDispatchQueue : DispatchQueue {
3333
pastDueWorkItems.forEach { it.perform() }
3434

3535
// Remove all work items that are past due or canceled
36-
workItems.removeAll { it.deadline <= this.tickTime || it.isCancelled }
36+
workItems.removeAll { it.isPastDue(tickTime) || it.isCancelled }
3737
}
3838

39-
4039
override fun queue(delay: Long, unit: TimeUnit, runnable: () -> Unit): DispatchWorkItem {
4140
// Converts the given unit and delay to the unit used by this class
4241
val delayInMs = tickTimeUnit.convert(delay, unit)
@@ -47,23 +46,56 @@ class ManualDispatchQueue : DispatchQueue {
4746

4847
return workItem
4948
}
49+
50+
override fun queueAtFixedRate(
51+
delay: Long,
52+
period: Long,
53+
unit: TimeUnit,
54+
runnable: () -> Unit
55+
): DispatchWorkItem {
56+
57+
val delayInMs = tickTimeUnit.convert(delay, unit)
58+
val periodInMs = tickTimeUnit.convert(period, unit)
59+
val deadline = tickTime + delayInMs
60+
61+
val workItem = ManualDispatchWorkItem(runnable, deadline, periodInMs)
62+
workItems.add(workItem)
63+
64+
return workItem
65+
}
5066
}
5167

5268
//------------------------------------------------------------------------------
5369
// Work Item
5470
//------------------------------------------------------------------------------
5571
class ManualDispatchWorkItem(
5672
private val runnable: () -> Unit,
57-
val deadline: Long
73+
private var deadline: Long,
74+
private val period: Long = 0
5875
) : DispatchWorkItem {
5976

60-
override var isCancelled: Boolean = false
77+
private var performCount = 0
6178

62-
override fun cancel() { this.isCancelled = true }
79+
80+
// Test
81+
fun isPastDue(tickTime: Long): Boolean {
82+
return this.deadline <= tickTime
83+
}
6384

6485
fun perform() {
6586
if (isCancelled) return
6687
runnable.invoke()
88+
performCount += 1
89+
90+
// If the task is repeatable, then schedule the next deadline after the given period
91+
deadline += (performCount * period)
92+
}
93+
94+
// DispatchWorkItem
95+
override var isCancelled: Boolean = false
96+
97+
override fun cancel() {
98+
this.isCancelled = true
6799
}
68100
}
69101

src/test/kotlin/org/phoenixframework/WebSocketTransportTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ class WebSocketTransportTest {
107107
val throwable = SSLException("t")
108108
transport.onFailure(mockWebSocket, throwable, mockResponse)
109109
verify(mockOnError).invoke(throwable, mockResponse)
110-
verify(mockOnClose).invoke(4001)
110+
verify(mockOnClose).invoke(4000)
111111
}
112112

113113
@Test
@@ -120,7 +120,7 @@ class WebSocketTransportTest {
120120
val throwable = EOFException()
121121
transport.onFailure(mockWebSocket, throwable, mockResponse)
122122
verify(mockOnError).invoke(throwable, mockResponse)
123-
verify(mockOnClose).invoke(4002)
123+
verify(mockOnClose).invoke(4000)
124124
}
125125

126126
@Test

0 commit comments

Comments
 (0)