分散刷新后补上备机同步

原实现中「全量刷新 → 通知备机同步」是同一步的两个动作:refreshAll()
由调用方跟一次 requestSubscriptionSync()。改为按账号分散刷新后,调度器
自己没有触发同步,刷新出的新缓存要等到下一次周期同步才可能送到备机。

生产上每 60 秒有一次周期同步兜底,所以影响被限制在最长约一分钟的滞后;
但这属于重构时漏掉的语义,且一旦把周期同步间隔调大就会变成真实缺口,
因此仍按原语义在刷新到内容后立即触发同步。没有账号到期时不打扰备机。

测试 441 通过。
This commit is contained in:
root
2026-09-15 16:27:29 +08:00
parent 3adb42c538
commit d949415e4d
3 changed files with 44 additions and 4 deletions
@@ -38,6 +38,7 @@ public class SubscriptionRefreshScheduler {
private final SubscriptionRefreshService refreshService;
private final SubscriptionRefreshPlanner planner;
private final PushService pushService;
private final RemoteService remoteService;
/** 单个 tick 最多刷新几个账号,避免重启后一批到期账号同时开火。 */
@Value("${subscription.refresh.max-per-tick:2}")
@@ -53,11 +54,13 @@ public class SubscriptionRefreshScheduler {
public SubscriptionRefreshScheduler(SubMapper subMapper,
SubscriptionRefreshService refreshService,
SubscriptionRefreshPlanner planner,
PushService pushService) {
PushService pushService,
RemoteService remoteService) {
this.subMapper = subMapper;
this.refreshService = refreshService;
this.planner = planner;
this.pushService = pushService;
this.remoteService = remoteService;
}
/**
@@ -97,6 +100,10 @@ public class SubscriptionRefreshScheduler {
refreshed++;
}
reportStale(enabled, now);
// 缓存内容变了就要让备机同步。原先全量刷新后由调用方跟一次同步,
// 改为分散刷新后必须在这里补上,否则备机要等到下一次周期同步才拿到新内容。
if (refreshed > 0)
remoteService.requestSubscriptionSync();
return refreshed;
}
@@ -35,12 +35,14 @@ class SubscriptionRefreshSchedulerTest {
private SubMapper subMapper;
private SubscriptionRefreshService refreshService;
private PushService pushService;
private RemoteService remoteService;
@BeforeEach
void setUp() {
subMapper = mock(SubMapper.class);
refreshService = mock(SubscriptionRefreshService.class);
pushService = mock(PushService.class);
remoteService = mock(RemoteService.class);
}
/** 用固定时钟构造,使「现在」可控。 */
@@ -49,7 +51,7 @@ class SubscriptionRefreshSchedulerTest {
Clock.fixed(instant, ZONE), new Random(42),
Duration.ofHours(24), Duration.ofHours(1), Duration.ofMinutes(5));
SubscriptionRefreshScheduler scheduler = new SubscriptionRefreshScheduler(
subMapper, refreshService, planner, pushService);
subMapper, refreshService, planner, pushService, remoteService);
scheduler.maxPerTick = 2;
scheduler.staleAlertMultiplier = 2.0;
return scheduler;
@@ -385,6 +387,31 @@ class SubscriptionRefreshSchedulerTest {
"铺开的计划应写回内存对象");
}
/**
* 刷新成功后必须通知备机同步。
* 原先「全量刷新」由调用方跟一次同步;改成按账号分散刷新后,若调度器不自己触发,
* 备机就要等到下一次周期同步才拿到新内容——这个缺口在重构时很容易漏掉。
*/
@Test
void refreshTriggersStandbySyncWhenSomethingChanged() {
existing(account(1, BASE.toEpochMilli() - MINUTE, BASE.toEpochMilli() - 2 * HOUR));
refreshSucceedsForAll();
scheduler().refreshDueAccounts();
verify(remoteService).requestSubscriptionSync();
}
/** 本轮没有任何账号到期时不应打扰备机。 */
@Test
void noRefreshMeansNoStandbySync() {
existing(account(1, BASE.toEpochMilli() + 5 * HOUR, BASE.toEpochMilli()));
scheduler().refreshDueAccounts();
verify(remoteService, never()).requestSubscriptionSync();
}
/** 已有计划的账号不应被重新排程,否则每次 tick 都会打乱分散效果。 */
@Test
void existingScheduleIsNotOverwritten() {
@@ -44,6 +44,11 @@ class SubscriptionRefreshWiringTest {
return mock(PushService.class);
}
@Bean
RemoteService remoteService() {
return mock(RemoteService.class);
}
@Bean
SubscriptionRefreshPlanner subscriptionRefreshPlanner() {
return new SubscriptionRefreshPlanner(java.time.Clock.systemDefaultZone(), new java.util.Random(),
@@ -54,8 +59,9 @@ class SubscriptionRefreshWiringTest {
SubscriptionRefreshScheduler subscriptionRefreshScheduler(SubMapper subMapper,
SubscriptionRefreshService refreshService,
SubscriptionRefreshPlanner planner,
PushService pushService) {
return new SubscriptionRefreshScheduler(subMapper, refreshService, planner, pushService);
PushService pushService,
RemoteService remoteService) {
return new SubscriptionRefreshScheduler(subMapper, refreshService, planner, pushService, remoteService);
}
}