diff --git a/src/main/java/com/lion/lionwebsite/Service/RemoteService.java b/src/main/java/com/lion/lionwebsite/Service/RemoteService.java index 98c0543..75df51d 100644 --- a/src/main/java/com/lion/lionwebsite/Service/RemoteService.java +++ b/src/main/java/com/lion/lionwebsite/Service/RemoteService.java @@ -179,6 +179,8 @@ public class RemoteService { //子节点上线时,发送未完成的任务 resetUndone(); + // 节点刚上线时可能没有任何快照(例如刚重启),必须推一次; + // 这同时会刷新节点「最近收到快照」的时刻,避免内容未变时被判过期。 requestSubscriptionSync(); return true; }catch (Exception e){ @@ -252,8 +254,19 @@ public class RemoteService { } } - @Scheduled(fixedDelayString = "${subscription.standby.retry-interval-ms:60000}") - void scheduledSubscriptionSync() { + /** + * 低频保活推送。 + * + *
快照推送改为「订阅内容变化时立即推」之后,内容长期不变时节点将收不到任何推送, + * 而节点的过期判定依据「最近一次收到快照的时刻」——于是它会在一周后永久判过期。 + * 这个保活用于把「主站在线」这件事周期性告诉节点,刷新其新鲜度。 + * + *
周期远小于节点 `SubscriptionMaxStaleSeconds`(默认 7 天):默认 6 小时一次, + * 约 1.4 MiB/天,相比原先每 60 秒一次的约 486 MiB/天 降低约三个数量级。 + */ + @Scheduled(fixedDelayString = "${subscription.standby.keepalive-interval-ms:21600000}", + initialDelayString = "${subscription.standby.keepalive-initial-delay-ms:600000}") + void scheduledSubscriptionKeepalive() { requestSubscriptionSync(); } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 9bb2c84..2d02d03 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -76,7 +76,10 @@ subscription: standby: sync-enabled: "${SUBSCRIPTION_STANDBY_SYNC_ENABLED:false}" sync-secret: "${SUBSCRIPTION_SYNC_SECRET:}" - retry-interval-ms: 60000 + # 快照改为「订阅内容变化时立即推送」,另加低频保活刷新节点新鲜度。 + # 周期须远小于节点 SubscriptionMaxStaleSeconds(默认 7 天),默认 6 小时。 + keepalive-interval-ms: 21600000 + keepalive-initial-delay-ms: 600000 bot: token: "5222939329:AAHa6l9ZuVVdNSDLPI_H-c8O_VgeOEw5plA" diff --git a/src/test/java/com/lion/lionwebsite/Service/SubscriptionKeepaliveTest.java b/src/test/java/com/lion/lionwebsite/Service/SubscriptionKeepaliveTest.java new file mode 100644 index 0000000..6364b48 --- /dev/null +++ b/src/test/java/com/lion/lionwebsite/Service/SubscriptionKeepaliveTest.java @@ -0,0 +1,52 @@ +package com.lion.lionwebsite.Service; + +import org.junit.jupiter.api.Test; +import org.springframework.scheduling.annotation.Scheduled; + +import java.lang.reflect.Method; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * 备机快照推送时机与保活周期。 + * + *
快照推送是「订阅内容变化时立即推」(由各处 requestSubscriptionSync 触发)。 + * 但节点按「最近一次收到快照的时刻」判过期,因此内容长期不变时必须有低频保活, + * 否则节点会在一个有效期后永久判过期——这是本类要锁住的不变量。 + */ +class SubscriptionKeepaliveTest { + + /** 节点侧 SubscriptionMaxStaleSeconds 的默认值(7 天),保活周期必须远小于它。 */ + private static final long NODE_MAX_STALE_MILLIS = 7L * 24 * 3600 * 1000; + + /** + * 保活必须存在、且周期远小于节点有效期。 + * + *
回归背景:曾一度改成「只在内容变化时推送」,结果存储节点重启后收不到任何推送, + * 状态直接退化为 expired。保活就是为这个场景兜底的。 + */ + @Test + void keepaliveIsScheduledWellWithinNodeStaleWindow() throws Exception { + Method method = RemoteService.class.getDeclaredMethod("scheduledSubscriptionKeepalive"); + Scheduled scheduled = method.getAnnotation(Scheduled.class); + + assertNotNull(scheduled, "必须存在保活推送的定时入口"); + long interval = parseDefaultMillis(scheduled.fixedDelayString()); + assertTrue(interval > 0, "保活周期必须为正: " + scheduled.fixedDelayString()); + assertTrue(interval < NODE_MAX_STALE_MILLIS / 4, + "保活周期(" + interval + "ms)必须远小于节点有效期(" + NODE_MAX_STALE_MILLIS + "ms)," + + "否则节点可能在两次保活之间判过期"); + + long initialDelay = parseDefaultMillis(scheduled.initialDelayString()); + assertTrue(initialDelay >= 0, "初始延迟不能为负"); + assertTrue(initialDelay < interval, "初始延迟应小于一个周期,避免启动后长时间不刷新节点新鲜度"); + } + + /** 从 `${key:default}` 形式的占位符里取出默认毫秒值。 */ + private static long parseDefaultMillis(String placeholder) { + int colon = placeholder.indexOf(':'); + String value = colon >= 0 ? placeholder.substring(colon + 1) : placeholder; + value = value.replace("}", "").trim(); + return Long.parseLong(value); + } +}