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