feat(idempotency): scheduled cleanup для expired keys

@Scheduled(cron='0 17 * * * *') запускает deleteExpired раз в час (17-я
минута, чтобы не совпало с другими cron). Конфигурируется через
ordinis.idempotency.cleanup-cron. Counter ordinis_idempotency_cleaned_total
для observability.
This commit is contained in:
Александр Зимин
2026-05-04 00:50:14 +03:00
parent a969c50072
commit adc5315948
@@ -0,0 +1,46 @@
package cloud.nstart.terravault.ordinis.restapi.idempotency;
import cloud.nstart.terravault.ordinis.domain.idempotency.IdempotencyKeyRepository;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.OffsetDateTime;
/**
* Scheduled cleanup для idempotency_keys.
*
* <p>Retention 30 дней (см. migration 0006). Запускается раз в час, удаляет
* записи с expires_at &lt; now. Неблокирующий — простой DELETE по индексу.
*/
@Component
public class IdempotencyCleanupJob {
private static final Logger log = LoggerFactory.getLogger(IdempotencyCleanupJob.class);
private final IdempotencyKeyRepository repository;
private final Counter cleanedCounter;
public IdempotencyCleanupJob(IdempotencyKeyRepository repository, MeterRegistry meterRegistry) {
this.repository = repository;
this.cleanedCounter = Counter.builder("ordinis_idempotency_cleaned_total")
.description("Удалённые expired idempotency keys")
.register(meterRegistry);
}
@Scheduled(cron = "${ordinis.idempotency.cleanup-cron:0 17 * * * *}")
public void cleanup() {
try {
int deleted = repository.deleteExpired(OffsetDateTime.now());
if (deleted > 0) {
log.info("Idempotency cleanup: удалено {} expired ключей", deleted);
cleanedCounter.increment(deleted);
}
} catch (Exception e) {
log.warn("Idempotency cleanup failed: {}", e.getMessage());
}
}
}