Skip to content

Commit 89210a7

Browse files
Robclaude
andcommitted
fix(telegram): scope transient-error retry to failed recipients only
When postSend/postEdit returns null for one recipient, the 60s retry re-ran the full recipient loop, causing recipients that already had a successful stale/missed post to receive a redundant API call with a fresh timestamp. Add scheduleInternal() which captures a retryFor set. On transient error, track the failing recipient in failedRecipients and pass that set to the retry schedule. Retry invocations skip any recipient not in the set. Normal schedule() calls (from OutboundApi) pass emptySet() so all recipients are evaluated as before. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 15d34b0 commit 89210a7

1 file changed

Lines changed: 18 additions & 6 deletions

File tree

Common/src/main/java/tk/glucodata/TelegramStaleCheckWork.kt

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,21 @@ object TelegramStaleCheckWork {
3636
private const val STALE_PREFIX = "telegram_stale_check:"
3737
private const val TRANSIENT_RETRY_DELAY_MS = 60_000L
3838

39-
fun schedule(context: Context, destinationId: String, delayMs: Long) {
39+
fun schedule(context: Context, destinationId: String, delayMs: Long) =
40+
scheduleInternal(context, destinationId, delayMs, emptySet())
41+
42+
private fun scheduleInternal(
43+
context: Context,
44+
destinationId: String,
45+
delayMs: Long,
46+
retryFor: Set<String>
47+
) {
4048
val key = STALE_PREFIX + destinationId
4149
pending.remove(key)?.let { handler.removeCallbacks(it) }
4250
val appContext = context.applicationContext
4351
val runnable = Runnable {
4452
pending.remove(key)
45-
run(appContext, destinationId)
53+
run(appContext, destinationId, retryFor)
4654
}
4755
pending[key] = runnable
4856
handler.postDelayed(runnable, delayMs)
@@ -53,7 +61,7 @@ object TelegramStaleCheckWork {
5361
pending.remove(key)?.let { handler.removeCallbacks(it) }
5462
}
5563

56-
private fun run(context: Context, destinationId: String) {
64+
private fun run(context: Context, destinationId: String, retryFor: Set<String> = emptySet()) {
5765
val config = OutboundApiSettings.load(context)
5866
val destination = config.findDestination(destinationId) ?: return
5967
if (!destination.enabled || !destination.staleEnabled) return
@@ -67,8 +75,11 @@ object TelegramStaleCheckWork {
6775
) * 60_000L
6876

6977
var earliestNextDelayMs = Long.MAX_VALUE
78+
val failedRecipients = mutableSetOf<String>()
7079
val recipients = destination.recipients()
7180
for (recipient in recipients) {
81+
// On retry invocations only re-process the recipients that previously failed.
82+
if (retryFor.isNotEmpty() && recipient !in retryFor) continue
7283
val lastSentMs = destination.lastSentAtMsByRecipient[recipient] ?: 0L
7384
if (lastSentMs <= 0L) continue
7485
val elapsedMs = now - lastSentMs
@@ -105,14 +116,15 @@ object TelegramStaleCheckWork {
105116
)
106117
}
107118
null -> {
108-
// Transient network error — reschedule so the notification is
109-
// retried rather than silently dropped.
119+
// Transient network error — track this recipient for a targeted
120+
// retry so other recipients that already succeeded are not re-sent.
121+
failedRecipients.add(recipient)
110122
earliestNextDelayMs = minOf(earliestNextDelayMs, TRANSIENT_RETRY_DELAY_MS)
111123
}
112124
}
113125
}
114126
if (earliestNextDelayMs < Long.MAX_VALUE) {
115-
schedule(context, destinationId, earliestNextDelayMs)
127+
scheduleInternal(context, destinationId, earliestNextDelayMs, failedRecipients)
116128
}
117129
}
118130

0 commit comments

Comments
 (0)