|
@@ -7,13 +7,19 @@ import com.adx.tencent.conversionsync.ConversionSyncRunner;
|
|
|
import com.adx.tencent.conversionsync.ConversionSyncService;
|
|
import com.adx.tencent.conversionsync.ConversionSyncService;
|
|
|
import com.adx.tencent.conversionsync.ManualTencentCallbackService;
|
|
import com.adx.tencent.conversionsync.ManualTencentCallbackService;
|
|
|
import com.adx.tencent.conversionsync.RetryService;
|
|
import com.adx.tencent.conversionsync.RetryService;
|
|
|
|
|
+import org.springframework.data.redis.core.Cursor;
|
|
|
|
|
+import org.springframework.data.redis.core.ScanOptions;
|
|
|
|
|
+import org.springframework.data.redis.core.StringRedisTemplate;
|
|
|
import org.springframework.http.HttpStatus;
|
|
import org.springframework.http.HttpStatus;
|
|
|
import org.springframework.lang.Nullable;
|
|
import org.springframework.lang.Nullable;
|
|
|
import org.springframework.web.bind.annotation.*;
|
|
import org.springframework.web.bind.annotation.*;
|
|
|
import org.springframework.web.server.ResponseStatusException;
|
|
import org.springframework.web.server.ResponseStatusException;
|
|
|
|
|
|
|
|
|
|
+import java.time.Duration;
|
|
|
|
|
+import java.util.ArrayList;
|
|
|
import java.util.List;
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
import java.util.Map;
|
|
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 管理接口:手动触发转化同步和回调重试。
|
|
* 管理接口:手动触发转化同步和回调重试。
|
|
@@ -28,19 +34,22 @@ public class AdminController {
|
|
|
private final ConversionSyncService conversionSyncer;
|
|
private final ConversionSyncService conversionSyncer;
|
|
|
private final RetryService retryService;
|
|
private final RetryService retryService;
|
|
|
private final ManualTencentCallbackService manualTencentCallbackService;
|
|
private final ManualTencentCallbackService manualTencentCallbackService;
|
|
|
|
|
+ private final StringRedisTemplate redisTemplate;
|
|
|
|
|
|
|
|
public AdminController(ConversionClient conversionClient,
|
|
public AdminController(ConversionClient conversionClient,
|
|
|
@Nullable ConversionSyncRunner conversionSyncRunner,
|
|
@Nullable ConversionSyncRunner conversionSyncRunner,
|
|
|
@Nullable ConversionBackfillJobService backfillJobService,
|
|
@Nullable ConversionBackfillJobService backfillJobService,
|
|
|
@Nullable ConversionSyncService conversionSyncer,
|
|
@Nullable ConversionSyncService conversionSyncer,
|
|
|
@Nullable RetryService retryService,
|
|
@Nullable RetryService retryService,
|
|
|
- @Nullable ManualTencentCallbackService manualTencentCallbackService) {
|
|
|
|
|
|
|
+ @Nullable ManualTencentCallbackService manualTencentCallbackService,
|
|
|
|
|
+ @Nullable StringRedisTemplate redisTemplate) {
|
|
|
this.conversionClient = conversionClient;
|
|
this.conversionClient = conversionClient;
|
|
|
this.conversionSyncRunner = conversionSyncRunner;
|
|
this.conversionSyncRunner = conversionSyncRunner;
|
|
|
this.backfillJobService = backfillJobService;
|
|
this.backfillJobService = backfillJobService;
|
|
|
this.conversionSyncer = conversionSyncer;
|
|
this.conversionSyncer = conversionSyncer;
|
|
|
this.retryService = retryService;
|
|
this.retryService = retryService;
|
|
|
this.manualTencentCallbackService = manualTencentCallbackService;
|
|
this.manualTencentCallbackService = manualTencentCallbackService;
|
|
|
|
|
+ this.redisTemplate = redisTemplate;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@PostMapping("/conversions/query")
|
|
@PostMapping("/conversions/query")
|
|
@@ -103,6 +112,57 @@ public class AdminController {
|
|
|
return manualTencentCallbackService.replayDeductedCallback(id);
|
|
return manualTencentCallbackService.replayDeductedCallback(id);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ @PostMapping("/redis/tighten-bid-media-ttl")
|
|
|
|
|
+ public Object tightenBidMediaTtl(@RequestBody(required = false) Map<String, Object> body) {
|
|
|
|
|
+ if (redisTemplate == null) {
|
|
|
|
|
+ throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,
|
|
|
|
|
+ "redis template is not configured");
|
|
|
|
|
+ }
|
|
|
|
|
+ long ttlSeconds = toLong(body == null ? null : body.get("ttlSeconds"));
|
|
|
|
|
+ if (ttlSeconds <= 0) {
|
|
|
|
|
+ ttlSeconds = Duration.ofDays(3).getSeconds();
|
|
|
|
|
+ }
|
|
|
|
|
+ long scanCount = toLong(body == null ? null : body.get("scanCount"));
|
|
|
|
|
+ if (scanCount <= 0) {
|
|
|
|
|
+ scanCount = 2000;
|
|
|
|
|
+ }
|
|
|
|
|
+ long maxKeys = toLong(body == null ? null : body.get("maxKeys"));
|
|
|
|
|
+ if (maxKeys <= 0) {
|
|
|
|
|
+ maxKeys = 200000;
|
|
|
|
|
+ }
|
|
|
|
|
+ List<String> patterns = parseStringList(body == null ? null : body.get("patterns"));
|
|
|
|
|
+ if (patterns.isEmpty()) {
|
|
|
|
|
+ patterns = defaultBidMediaPatterns();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ List<Map<String, Object>> results = new ArrayList<>();
|
|
|
|
|
+ long totalScanned = 0;
|
|
|
|
|
+ long totalUpdated = 0;
|
|
|
|
|
+ for (String pattern : patterns) {
|
|
|
|
|
+ TtlTightenResult result = tightenTtl(pattern, ttlSeconds, scanCount, Math.max(0, maxKeys - totalScanned));
|
|
|
|
|
+ results.add(Map.of(
|
|
|
|
|
+ "pattern", pattern,
|
|
|
|
|
+ "scanned", result.scanned(),
|
|
|
|
|
+ "updated", result.updated(),
|
|
|
|
|
+ "stoppedByLimit", result.stoppedByLimit()
|
|
|
|
|
+ ));
|
|
|
|
|
+ totalScanned += result.scanned();
|
|
|
|
|
+ totalUpdated += result.updated();
|
|
|
|
|
+ if (totalScanned >= maxKeys) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return Map.of(
|
|
|
|
|
+ "ttlSeconds", ttlSeconds,
|
|
|
|
|
+ "ttlHuman", Duration.ofSeconds(ttlSeconds).toString(),
|
|
|
|
|
+ "scanCount", scanCount,
|
|
|
|
|
+ "maxKeys", maxKeys,
|
|
|
|
|
+ "scanned", totalScanned,
|
|
|
|
|
+ "updated", totalUpdated,
|
|
|
|
|
+ "patterns", results
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private static int toInt(Object v) {
|
|
private static int toInt(Object v) {
|
|
|
if (v == null) return 0;
|
|
if (v == null) return 0;
|
|
|
if (v instanceof Number n) return n.intValue();
|
|
if (v instanceof Number n) return n.intValue();
|
|
@@ -115,6 +175,58 @@ public class AdminController {
|
|
|
try { return Long.parseLong(v.toString()); } catch (NumberFormatException e) { return 0L; }
|
|
try { return Long.parseLong(v.toString()); } catch (NumberFormatException e) { return 0L; }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private TtlTightenResult tightenTtl(String pattern, long ttlSeconds, long scanCount, long maxKeys) {
|
|
|
|
|
+ if (maxKeys <= 0 || pattern == null || pattern.isBlank()) {
|
|
|
|
|
+ return new TtlTightenResult(0, 0, true);
|
|
|
|
|
+ }
|
|
|
|
|
+ long scanned = 0;
|
|
|
|
|
+ long updated = 0;
|
|
|
|
|
+ ScanOptions options = ScanOptions.scanOptions().match(pattern).count(scanCount).build();
|
|
|
|
|
+ try (Cursor<String> cursor = redisTemplate.scan(options)) {
|
|
|
|
|
+ while (cursor.hasNext()) {
|
|
|
|
|
+ String key = cursor.next();
|
|
|
|
|
+ scanned++;
|
|
|
|
|
+ Long currentTtl = redisTemplate.getExpire(key, TimeUnit.SECONDS);
|
|
|
|
|
+ if (currentTtl == null || currentTtl < 0 || currentTtl > ttlSeconds) {
|
|
|
|
|
+ Boolean ok = redisTemplate.expire(key, Duration.ofSeconds(ttlSeconds));
|
|
|
|
|
+ if (Boolean.TRUE.equals(ok)) {
|
|
|
|
|
+ updated++;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (scanned >= maxKeys) {
|
|
|
|
|
+ return new TtlTightenResult(scanned, updated, true);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return new TtlTightenResult(scanned, updated, false);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static List<String> parseStringList(Object raw) {
|
|
|
|
|
+ if (!(raw instanceof Iterable<?> iterable)) {
|
|
|
|
|
+ return List.of();
|
|
|
|
|
+ }
|
|
|
|
|
+ List<String> values = new ArrayList<>();
|
|
|
|
|
+ for (Object item : iterable) {
|
|
|
|
|
+ if (item != null && !item.toString().isBlank()) {
|
|
|
|
|
+ values.add(item.toString());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return values;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static List<String> defaultBidMediaPatterns() {
|
|
|
|
|
+ return List.of(
|
|
|
|
|
+ "adx:bid:*",
|
|
|
|
|
+ "adx:media:*",
|
|
|
|
|
+ "adx:honor:bid:*",
|
|
|
|
|
+ "adx:honor:media:*",
|
|
|
|
|
+ "adx:kuaishou:bid:*",
|
|
|
|
|
+ "adx:kuaishou:media:*",
|
|
|
|
|
+ "adx:vivo:bid:*",
|
|
|
|
|
+ "adx:vivo:media:*"
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private static List<Integer> parseOffsets(Map<String, Object> body) {
|
|
private static List<Integer> parseOffsets(Map<String, Object> body) {
|
|
|
if (body == null || body.isEmpty()) {
|
|
if (body == null || body.isEmpty()) {
|
|
|
return defaultBackfillOffsets();
|
|
return defaultBackfillOffsets();
|
|
@@ -133,4 +245,6 @@ public class AdminController {
|
|
|
private static List<Integer> defaultBackfillOffsets() {
|
|
private static List<Integer> defaultBackfillOffsets() {
|
|
|
return List.of(-1, -2, -3, -4, -5, -6, -7);
|
|
return List.of(-1, -2, -3, -4, -5, -6, -7);
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ private record TtlTightenResult(long scanned, long updated, boolean stoppedByLimit) {}
|
|
|
}
|
|
}
|