|
|
@@ -0,0 +1,102 @@
|
|
|
+package com.adx.tencent.retention;
|
|
|
+
|
|
|
+import com.adx.tencent.leader.LeaderElection;
|
|
|
+import jakarta.annotation.PreDestroy;
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.boot.ApplicationArguments;
|
|
|
+import org.springframework.boot.ApplicationRunner;
|
|
|
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|
|
+import org.springframework.lang.Nullable;
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
+
|
|
|
+import javax.sql.DataSource;
|
|
|
+import java.time.Duration;
|
|
|
+import java.util.concurrent.ExecutorService;
|
|
|
+import java.util.concurrent.Executors;
|
|
|
+import java.util.concurrent.atomic.AtomicBoolean;
|
|
|
+
|
|
|
+@Component
|
|
|
+@ConditionalOnProperty(prefix = "adx", name = "legacy-retention-enabled", havingValue = "true")
|
|
|
+public class LegacyRetentionJob implements ApplicationRunner {
|
|
|
+ private static final Logger log = LoggerFactory.getLogger(LegacyRetentionJob.class);
|
|
|
+ static final long BATCH_DELAY_MS = 10_000;
|
|
|
+ static final String LOCK_KEY = "adx:lock:retention:adx_proxy:legacy-tables";
|
|
|
+ private final LegacyRetentionStore store;
|
|
|
+ private final LeaderElection leader;
|
|
|
+ private final Waiter waiter;
|
|
|
+ private final AtomicBoolean stopped = new AtomicBoolean();
|
|
|
+ private final ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
|
|
|
+ Thread t = new Thread(r, "legacy-retention");
|
|
|
+ t.setDaemon(true);
|
|
|
+ return t;
|
|
|
+ });
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ public LegacyRetentionJob(@Nullable DataSource dataSource, LeaderElection leader) {
|
|
|
+ this(dataSource == null ? null : new LegacyRetentionStore(dataSource), leader, LegacyRetentionJob::waitBetweenBatches);
|
|
|
+ }
|
|
|
+
|
|
|
+ LegacyRetentionJob(LegacyRetentionStore store, LeaderElection leader, Waiter waiter) {
|
|
|
+ this.store = store;
|
|
|
+ this.leader = leader;
|
|
|
+ this.waiter = waiter;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void run(ApplicationArguments args) {
|
|
|
+ if (store == null) {
|
|
|
+ log.warn("[LegacyRetention] no datasource; cleanup not started");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // Always elect one worker, even when skip-leader-election is set for unrelated jobs.
|
|
|
+ executor.submit(() -> leader.run(LOCK_KEY, Duration.ofMinutes(2), Duration.ofSeconds(30),
|
|
|
+ Duration.ofSeconds(10), stopped::get, this::runLoop,
|
|
|
+ e -> log.error("[LegacyRetention] leader error", e)));
|
|
|
+ }
|
|
|
+
|
|
|
+ void runLoop(LeaderElection.StopSignal signal) {
|
|
|
+ LeaderElection.StopSignal stop = () -> stopped.get() || signal.isStopped() || Thread.currentThread().isInterrupted();
|
|
|
+ LegacyRetentionStore.Table[] tables = LegacyRetentionStore.Table.values();
|
|
|
+ int next = 0;
|
|
|
+ while (!stop.isStopped()) {
|
|
|
+ LegacyRetentionStore.Table table = tables[next];
|
|
|
+ next = (next + 1) % tables.length;
|
|
|
+ try {
|
|
|
+ int deleted = store.deleteBatch(table);
|
|
|
+ if (deleted > 0) log.info("[LegacyRetention] table={} deleted={} retention=30days", table.qualifiedName(), deleted);
|
|
|
+ else log.debug("[LegacyRetention] table={} no expired rows", table.qualifiedName());
|
|
|
+ } catch (Exception e) {
|
|
|
+ // A broken/missing table must not prevent the other table from being cleaned.
|
|
|
+ log.error("[LegacyRetention] table={} batch failed; retry on next turn", table.qualifiedName(), e);
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ waiter.await(BATCH_DELAY_MS, stop);
|
|
|
+ } catch (InterruptedException e) {
|
|
|
+ Thread.currentThread().interrupt();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void waitBetweenBatches(long millis, LeaderElection.StopSignal stop) throws InterruptedException {
|
|
|
+ long deadline = System.nanoTime() + millis * 1_000_000;
|
|
|
+ while (!stop.isStopped()) {
|
|
|
+ long remaining = deadline - System.nanoTime();
|
|
|
+ if (remaining <= 0) return;
|
|
|
+ Thread.sleep(Math.min(1000, Math.max(1, remaining / 1_000_000)));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @PreDestroy
|
|
|
+ public void close() {
|
|
|
+ stopped.set(true);
|
|
|
+ executor.shutdownNow();
|
|
|
+ }
|
|
|
+
|
|
|
+ @FunctionalInterface
|
|
|
+ interface Waiter {
|
|
|
+ void await(long millis, LeaderElection.StopSignal stop) throws InterruptedException;
|
|
|
+ }
|
|
|
+}
|