feat(license): LicenseRevalidationJob — daily cron + 60s post-startup
@Scheduled(cron = "0 0 3 * * *") triggers svc.revalidate() daily. @EventListener(ApplicationReadyEvent.class) @Async fires once 60s after boot to catch ABSENT->ACTIVE transitions if the license was written to PG between server starts. Exceptions are logged but never propagate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
package com.cameleer.server.app.license;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Daily revalidation cron + on-startup revalidation 60s after {@link ApplicationReadyEvent}.
|
||||
*
|
||||
* <p>The startup tick catches ABSENT->ACTIVE transitions when the license was written to
|
||||
* PostgreSQL between server starts (e.g. SaaS provisioning), and gives slow downstream
|
||||
* components time to come up before the first license event fires. The daily cron ensures
|
||||
* expirations and clock drift are caught even in long-running deployments.</p>
|
||||
*
|
||||
* <p>Both invocations call {@link LicenseService#revalidate()} which is internally idempotent
|
||||
* and exception-safe; this class additionally swallows any escape so a misbehaving validator
|
||||
* cannot crash the scheduler thread.</p>
|
||||
*/
|
||||
@Component
|
||||
public class LicenseRevalidationJob {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(LicenseRevalidationJob.class);
|
||||
|
||||
private final LicenseService svc;
|
||||
|
||||
public LicenseRevalidationJob(LicenseService svc) {
|
||||
this.svc = svc;
|
||||
}
|
||||
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
@Async
|
||||
public void onStartup() {
|
||||
try {
|
||||
Thread.sleep(60_000);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
revalidate();
|
||||
}
|
||||
|
||||
@Scheduled(cron = "0 0 3 * * *")
|
||||
public void daily() {
|
||||
revalidate();
|
||||
}
|
||||
|
||||
private void revalidate() {
|
||||
try {
|
||||
svc.revalidate();
|
||||
} catch (Exception e) {
|
||||
log.error("Revalidation crashed: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.cameleer.server.app.license;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
class LicenseRevalidationJobTest {
|
||||
|
||||
@Test
|
||||
void daily_callsService() {
|
||||
LicenseService svc = mock(LicenseService.class);
|
||||
new LicenseRevalidationJob(svc).daily();
|
||||
verify(svc).revalidate();
|
||||
}
|
||||
|
||||
@Test
|
||||
void daily_swallowsServiceException() {
|
||||
LicenseService svc = mock(LicenseService.class);
|
||||
doThrow(new RuntimeException("revalidate failed")).when(svc).revalidate();
|
||||
// No exception escapes
|
||||
new LicenseRevalidationJob(svc).daily();
|
||||
verify(svc).revalidate();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user