分散刷新后补上备机同步

原实现中「全量刷新 → 通知备机同步」是同一步的两个动作: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 SubscriptionRefreshService refreshService;
private final SubscriptionRefreshPlanner planner; private final SubscriptionRefreshPlanner planner;
private final PushService pushService; private final PushService pushService;
private final RemoteService remoteService;
/** 单个 tick 最多刷新几个账号,避免重启后一批到期账号同时开火。 */ /** 单个 tick 最多刷新几个账号,避免重启后一批到期账号同时开火。 */
@Value("${subscription.refresh.max-per-tick:2}") @Value("${subscription.refresh.max-per-tick:2}")
@@ -53,11 +54,13 @@ public class SubscriptionRefreshScheduler {
public SubscriptionRefreshScheduler(SubMapper subMapper, public SubscriptionRefreshScheduler(SubMapper subMapper,
SubscriptionRefreshService refreshService, SubscriptionRefreshService refreshService,
SubscriptionRefreshPlanner planner, SubscriptionRefreshPlanner planner,
PushService pushService) { PushService pushService,
RemoteService remoteService) {
this.subMapper = subMapper; this.subMapper = subMapper;
this.refreshService = refreshService; this.refreshService = refreshService;
this.planner = planner; this.planner = planner;
this.pushService = pushService; this.pushService = pushService;
this.remoteService = remoteService;
} }
/** /**
@@ -97,6 +100,10 @@ public class SubscriptionRefreshScheduler {
refreshed++; refreshed++;
} }
reportStale(enabled, now); reportStale(enabled, now);
// 缓存内容变了就要让备机同步。原先全量刷新后由调用方跟一次同步,
// 改为分散刷新后必须在这里补上,否则备机要等到下一次周期同步才拿到新内容。
if (refreshed > 0)
remoteService.requestSubscriptionSync();
return refreshed; return refreshed;
} }
@@ -35,12 +35,14 @@ class SubscriptionRefreshSchedulerTest {
private SubMapper subMapper; private SubMapper subMapper;
private SubscriptionRefreshService refreshService; private SubscriptionRefreshService refreshService;
private PushService pushService; private PushService pushService;
private RemoteService remoteService;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
subMapper = mock(SubMapper.class); subMapper = mock(SubMapper.class);
refreshService = mock(SubscriptionRefreshService.class); refreshService = mock(SubscriptionRefreshService.class);
pushService = mock(PushService.class); pushService = mock(PushService.class);
remoteService = mock(RemoteService.class);
} }
/** 用固定时钟构造,使「现在」可控。 */ /** 用固定时钟构造,使「现在」可控。 */
@@ -49,7 +51,7 @@ class SubscriptionRefreshSchedulerTest {
Clock.fixed(instant, ZONE), new Random(42), Clock.fixed(instant, ZONE), new Random(42),
Duration.ofHours(24), Duration.ofHours(1), Duration.ofMinutes(5)); Duration.ofHours(24), Duration.ofHours(1), Duration.ofMinutes(5));
SubscriptionRefreshScheduler scheduler = new SubscriptionRefreshScheduler( SubscriptionRefreshScheduler scheduler = new SubscriptionRefreshScheduler(
subMapper, refreshService, planner, pushService); subMapper, refreshService, planner, pushService, remoteService);
scheduler.maxPerTick = 2; scheduler.maxPerTick = 2;
scheduler.staleAlertMultiplier = 2.0; scheduler.staleAlertMultiplier = 2.0;
return scheduler; 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 都会打乱分散效果。 */ /** 已有计划的账号不应被重新排程,否则每次 tick 都会打乱分散效果。 */
@Test @Test
void existingScheduleIsNotOverwritten() { void existingScheduleIsNotOverwritten() {
@@ -44,6 +44,11 @@ class SubscriptionRefreshWiringTest {
return mock(PushService.class); return mock(PushService.class);
} }
@Bean
RemoteService remoteService() {
return mock(RemoteService.class);
}
@Bean @Bean
SubscriptionRefreshPlanner subscriptionRefreshPlanner() { SubscriptionRefreshPlanner subscriptionRefreshPlanner() {
return new SubscriptionRefreshPlanner(java.time.Clock.systemDefaultZone(), new java.util.Random(), return new SubscriptionRefreshPlanner(java.time.Clock.systemDefaultZone(), new java.util.Random(),
@@ -54,8 +59,9 @@ class SubscriptionRefreshWiringTest {
SubscriptionRefreshScheduler subscriptionRefreshScheduler(SubMapper subMapper, SubscriptionRefreshScheduler subscriptionRefreshScheduler(SubMapper subMapper,
SubscriptionRefreshService refreshService, SubscriptionRefreshService refreshService,
SubscriptionRefreshPlanner planner, SubscriptionRefreshPlanner planner,
PushService pushService) { PushService pushService,
return new SubscriptionRefreshScheduler(subMapper, refreshService, planner, pushService); RemoteService remoteService) {
return new SubscriptionRefreshScheduler(subMapper, refreshService, planner, pushService, remoteService);
} }
} }