Skip to content

Commit 9a43db0

Browse files
committed
fix: use duration and exponential backoff
1 parent 261e582 commit 9a43db0

5 files changed

Lines changed: 71 additions & 86 deletions

File tree

‎app/src/main/kotlin/com/github/gotify/service/WebSocketConnection.kt‎

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import com.github.gotify.client.model.Message
1212
import java.util.Calendar
1313
import java.util.concurrent.TimeUnit
1414
import java.util.concurrent.atomic.AtomicLong
15+
import kotlin.math.pow
16+
import kotlin.time.Duration
17+
import kotlin.time.Duration.Companion.minutes
18+
import kotlin.time.Duration.Companion.seconds
1519
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
1620
import okhttp3.OkHttpClient
1721
import okhttp3.Request
@@ -25,8 +29,8 @@ internal class WebSocketConnection(
2529
settings: SSLSettings,
2630
private val token: String?,
2731
private val alarmManager: AlarmManager,
28-
private val reconnectInterval: Int,
29-
private val disableBackoff: Boolean
32+
private val reconnectDelay: Duration,
33+
private val exponentialBackoff: Boolean
3034
) {
3135
companion object {
3236
private val ID = AtomicLong(0)
@@ -130,19 +134,19 @@ internal class WebSocketConnection(
130134
state = State.Disconnected
131135
}
132136

133-
fun scheduleReconnectNow(seconds: Long) = scheduleReconnect(ID.get(), seconds)
137+
fun scheduleReconnectNow(scheduleIn: Duration) = scheduleReconnect(ID.get(), scheduleIn)
134138

135139
@Synchronized
136-
fun scheduleReconnect(id: Long, seconds: Long) {
140+
fun scheduleReconnect(id: Long, scheduleIn: Duration) {
137141
if (state == State.Connecting || state == State.Connected) {
138142
return
139143
}
140144
state = State.Scheduled
141145

142146
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
143-
Logger.info("WebSocket: scheduling a restart in $seconds second(s) (via alarm manager)")
147+
Logger.info("WebSocket: scheduling a restart in $scheduleIn (via alarm manager)")
144148
val future = Calendar.getInstance()
145-
future.add(Calendar.SECOND, seconds.toInt())
149+
future.add(Calendar.SECOND, scheduleIn.inWholeSeconds.toInt())
146150

147151
alarmManagerCallback?.run(alarmManager::cancel)
148152
val cb = OnAlarmListener { syncExec(id) { start() } }
@@ -155,11 +159,11 @@ internal class WebSocketConnection(
155159
null
156160
)
157161
} else {
158-
Logger.info("WebSocket: scheduling a restart in $seconds second(s)")
162+
Logger.info("WebSocket: scheduling a restart in $scheduleIn")
159163
handlerCallback?.run(reconnectHandler::removeCallbacks)
160164
val cb = Runnable { syncExec(id) { start() } }
161165
handlerCallback = cb
162-
reconnectHandler.postDelayed(cb, TimeUnit.SECONDS.toMillis(seconds))
166+
reconnectHandler.postDelayed(cb, scheduleIn.inWholeMilliseconds)
163167
}
164168
}
165169

@@ -206,16 +210,15 @@ internal class WebSocketConnection(
206210
closed()
207211

208212
errorCount++
209-
val seconds = if (disableBackoff) {
210-
reconnectInterval
211-
} else {
212-
((errorCount * 2 - 1) * reconnectInterval)
213-
.coerceAtMost(TimeUnit.MINUTES.toSeconds(20).toInt())
213+
214+
var scheduleIn = reconnectDelay
215+
if (exponentialBackoff) {
216+
scheduleIn *= 2.0.pow(errorCount - 1)
214217
}
218+
scheduleIn = scheduleIn.coerceIn(5.seconds..20.minutes)
215219

216-
val rounded = seconds.coerceAtLeast(5)
217-
onFailure.execute(response?.message ?: "unreachable", rounded)
218-
scheduleReconnect(id, rounded.toLong())
220+
onFailure.execute(response?.message ?: "unreachable", scheduleIn)
221+
scheduleReconnect(id, scheduleIn)
219222
}
220223
super.onFailure(webSocket, t, response)
221224
}
@@ -229,7 +232,7 @@ internal class WebSocketConnection(
229232
}
230233

231234
internal fun interface OnNetworkFailureRunnable {
232-
fun execute(status: String, seconds: Int)
235+
fun execute(status: String, reconnectIn: Duration)
233236
}
234237

235238
internal enum class State {

‎app/src/main/kotlin/com/github/gotify/service/WebSocketService.kt‎

Lines changed: 20 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ import com.github.gotify.messages.MessagesActivity
4040
import io.noties.markwon.Markwon
4141
import java.util.concurrent.ConcurrentHashMap
4242
import java.util.concurrent.atomic.AtomicLong
43+
import kotlin.time.Duration
44+
import kotlin.time.Duration.Companion.minutes
45+
import kotlin.time.Duration.Companion.seconds
46+
import kotlin.time.DurationUnit
47+
import kotlin.time.toDuration
4348
import org.tinylog.kotlin.Logger
4449

4550
internal class WebSocketService : Service() {
@@ -111,29 +116,28 @@ internal class WebSocketService : Service() {
111116
val cm = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
112117
val alarmManager = getSystemService(ALARM_SERVICE) as AlarmManager
113118
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
114-
var reconnectInterval = sharedPreferences
115-
.getString(getString(R.string.setting_key_reconnect_interval), "60")
116-
?.trim()
117-
?.toIntOrNull() ?: 60
118-
119-
reconnectInterval = reconnectInterval.coerceIn(5, 60)
120-
121-
val disableBackoff = sharedPreferences.getBoolean(
122-
getString(R.string.setting_key_backoff),
123-
false
119+
val reconnectDelay =
120+
sharedPreferences.getString(
121+
getString(R.string.setting_key_reconnect_delay),
122+
null
123+
)?.toIntOrNull()?.toDuration(DurationUnit.SECONDS) ?: 1.minutes
124+
125+
val exponentialBackoff = sharedPreferences.getBoolean(
126+
getString(R.string.setting_key_exponential_backoff),
127+
true
124128
)
125129

126130
connection = WebSocketConnection(
127131
settings.url,
128132
settings.sslSettings(),
129133
settings.token,
130134
alarmManager,
131-
reconnectInterval,
132-
disableBackoff
135+
reconnectDelay,
136+
exponentialBackoff
133137
)
134138
.onOpen { onOpen() }
135139
.onClose { onClose() }
136-
.onFailure { status, seconds -> onFailure(status, seconds) }
140+
.onFailure { status, reconnectIn -> onFailure(status, reconnectIn) }
137141
.onMessage { message -> onMessage(message) }
138142
.onReconnected { notifyMissedNotifications() }
139143
.start()
@@ -194,24 +198,14 @@ internal class WebSocketService : Service() {
194198
}
195199

196200
private fun doReconnect() {
197-
connection?.scheduleReconnectNow(15)
201+
connection?.scheduleReconnectNow(15.seconds)
198202
}
199203

200-
private fun onFailure(status: String, seconds: Int) {
204+
private fun onFailure(status: String, reconnectIn: Duration) {
201205
val title = getString(R.string.websocket_error, status)
202-
val intervalUnit = if (seconds >= 60) {
203-
val minutes = seconds / 60
204-
resources.getQuantityString(R.plurals.websocket_retry_interval, minutes, minutes)
205-
} else {
206-
resources.getQuantityString(
207-
R.plurals.websocket_retry_interval_seconds,
208-
seconds,
209-
seconds
210-
)
211-
}
212206
showForegroundNotification(
213207
title,
214-
"${getString(R.string.websocket_reconnect)} $intervalUnit"
208+
getString(R.string.websocket_reconnect, reconnectIn.toString())
215209
)
216210
}
217211

‎app/src/main/kotlin/com/github/gotify/settings/SettingsActivity.kt‎

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import androidx.preference.SwitchPreferenceCompat
2121
import com.github.gotify.R
2222
import com.github.gotify.Utils
2323
import com.github.gotify.databinding.SettingsActivityBinding
24+
import com.github.gotify.service.WebSocketService
2425
import com.google.android.material.dialog.MaterialAlertDialogBuilder
2526

2627
internal class SettingsActivity :
@@ -75,28 +76,23 @@ internal class SettingsActivity :
7576
)?.isEnabled = true
7677
}
7778
findPreference<androidx.preference.EditTextPreference>(
78-
getString(R.string.setting_key_reconnect_interval)
79-
)?.let {
80-
it.setOnBindEditTextListener { editText ->
81-
editText.inputType = android.text.InputType.TYPE_CLASS_NUMBER
82-
}
83-
it.onPreferenceChangeListener =
84-
Preference.OnPreferenceChangeListener { _, newValue ->
85-
val value = (newValue as String).trim().toIntOrNull() ?: 60
86-
if (value < 5 || value > 60) {
87-
Utils.showSnackBar(
88-
requireActivity(),
89-
"Please enter a value between 5 and 60"
90-
)
91-
return@OnPreferenceChangeListener false
92-
}
93-
94-
requestWebSocketRestart()
95-
true
79+
getString(R.string.setting_key_reconnect_delay)
80+
)?.onPreferenceChangeListener =
81+
Preference.OnPreferenceChangeListener { _, newValue ->
82+
val value = (newValue as String).trim().toIntOrNull() ?: 60
83+
if (value !in 5..1200) {
84+
Utils.showSnackBar(
85+
requireActivity(),
86+
"Please enter a value between 5 and 1200"
87+
)
88+
return@OnPreferenceChangeListener false
9689
}
97-
}
90+
91+
requestWebSocketRestart()
92+
true
93+
}
9894
findPreference<SwitchPreferenceCompat>(
99-
getString(R.string.setting_key_backoff)
95+
getString(R.string.setting_key_exponential_backoff)
10096
)?.onPreferenceChangeListener =
10197
Preference.OnPreferenceChangeListener { _, _ ->
10298
requestWebSocketRestart()
@@ -105,8 +101,7 @@ internal class SettingsActivity :
105101
}
106102

107103
private fun requestWebSocketRestart() {
108-
val intent =
109-
Intent(requireContext(), com.github.gotify.service.WebSocketService::class.java)
104+
val intent = Intent(requireContext(), WebSocketService::class.java)
110105
requireContext().startService(intent)
111106
}
112107

‎app/src/main/res/values/strings.xml‎

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -115,22 +115,14 @@
115115
<string name="action_dialog_button_cancel">Cancel</string>
116116

117117
<string name="websocket_error">Error %s (see logs)</string>
118-
<string name="websocket_reconnect">Trying to reconnect</string>
119-
<plurals name="websocket_retry_interval">
120-
<item quantity="one">in %d minute</item>
121-
<item quantity="other">in %d minutes</item>
122-
</plurals>
123-
<plurals name="websocket_retry_interval_seconds">
124-
<item quantity="one">in %d second</item>
125-
<item quantity="other">in %d seconds</item>
126-
</plurals>
127-
<string name="setting_reconnect_interval">Reconnect Interval (Seconds)</string>
128-
<string name="setting_key_reconnect_interval">reconnect_interval</string>
118+
<string name="websocket_reconnect">Trying to reconnect in %s</string>
119+
<string name="setting_reconnect_delay">Reconnect Delay (Seconds)</string>
120+
<string name="setting_key_reconnect_delay">reconnect_delay</string>
129121
<string name="setting_connection">Connection</string>
130-
<string name="setting_reconnect_interval_summary">Wait time between connection attempts</string>
131-
<string name="setting_backoff_title">Constant Retry Interval</string>
132-
<string name="setting_backoff_summary">Do not increase wait time between retries</string>
133-
<string name="setting_key_backoff">reconnect_backoff</string>
122+
<string name="setting_reconnect_interval_summary">Delay between reconnect attempts</string>
123+
<string name="setting_exponential_backoff_title">Exponential Backoff</string>
124+
<string name="setting_exponential_backoff_summary">Exponentially increase the reconnect delay for each reconnect attempt</string>
125+
<string name="setting_key_exponential_backoff">reconnect_exponential_backoff</string>
134126

135127
<string name="notification_channel_title_foreground">Gotify foreground notification</string>
136128
<string name="notification_channel_title_min">Min priority messages (&lt;1)</string>

‎app/src/main/res/xml/root_preferences.xml‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,15 @@
5454
<PreferenceCategory app:title="@string/setting_connection">
5555
<EditTextPreference
5656
android:defaultValue="60"
57-
android:key="@string/setting_key_reconnect_interval"
58-
android:title="@string/setting_reconnect_interval"
57+
android:inputType="numberSigned"
58+
android:key="@string/setting_key_reconnect_delay"
59+
android:title="@string/setting_reconnect_delay"
5960
android:summary="@string/setting_reconnect_interval_summary" />
6061
<SwitchPreferenceCompat
61-
android:key="@string/setting_key_backoff"
62-
android:title="@string/setting_backoff_title"
63-
android:summary="@string/setting_backoff_summary"
64-
android:defaultValue="false" />
62+
android:key="@string/setting_key_exponential_backoff"
63+
android:title="@string/setting_exponential_backoff_title"
64+
android:summary="@string/setting_exponential_backoff_summary"
65+
android:defaultValue="true" />
6566
</PreferenceCategory>
6667

6768
</PreferenceScreen>

0 commit comments

Comments
 (0)