task-16: 图片预取改短预算 best-effort,超时取消在途并回退 URL
prefetchToDiskBestEffort 在 budgetSeconds 内尽力预取,预算耗尽即取消 在途任务并返回未预取数量;缺图单元格由既有 fallback 直接写 URL。 put 前检查中断标志,避免取消后写盘与 spool close 竞态产生残留文件。 预算通过 aiimage.similar-asin.image-prefetch-budget-seconds 配置(默认 60s), 长预算后台预热路径 prefetchToDisk 行为不变。
This commit is contained in:
@@ -186,6 +186,13 @@ public class SimilarAsinProperties {
|
||||
*/
|
||||
private int imageCacheTouchFlushThreshold = 1000;
|
||||
|
||||
/**
|
||||
* Task 16:图片预取短预算(秒)。assemble 阶段预取在预算内 best-effort
|
||||
* 尽力完成,预算耗尽即取消在途任务并回退 URL,避免整批预取拖垮结果组装。
|
||||
* 该值仅作用于 assemble 阶段预取;后台预热仍使用 imagePrefetchTimeoutSeconds。
|
||||
*/
|
||||
private int imagePrefetchBudgetSeconds = 60;
|
||||
|
||||
/**
|
||||
* P0-4:抢 Coze 提交锁失败后下次重试间隔(毫秒)。
|
||||
* 原硬编码 500ms,会在指数退避算法中作为基础值(500/1000/2000/4000ms 上限 4000)。
|
||||
|
||||
+110
@@ -167,6 +167,32 @@ public class SimilarAsinImageEmbedder {
|
||||
downloadPool.shutdownNow();
|
||||
}
|
||||
|
||||
/** Task 16:测试专用 hook,按 url 注入预取处理,绕过真实 HTTP 下载。 */
|
||||
private final ConcurrentMap<String, Runnable> testPrefetchHandlers = new ConcurrentHashMap<>();
|
||||
|
||||
void registerPrefetchHandler(String url, Runnable handler) {
|
||||
if (url != null && handler != null) {
|
||||
testPrefetchHandlers.put(url.trim(), handler);
|
||||
}
|
||||
}
|
||||
|
||||
private ResizedImage fetchAndResizeDirectForTest(String url) {
|
||||
Runnable handler = testPrefetchHandlers.get(url.trim());
|
||||
if (handler != null) {
|
||||
handler.run();
|
||||
return testPrefetchResults.get(url.trim());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private final ConcurrentMap<String, ResizedImage> testPrefetchResults = new ConcurrentHashMap<>();
|
||||
|
||||
void registerPrefetchResult(String url, ResizedImage image) {
|
||||
if (url != null && image != null) {
|
||||
testPrefetchResults.put(url.trim(), image);
|
||||
}
|
||||
}
|
||||
|
||||
static int cpuBoundPoolLimit() {
|
||||
int visibleProcessors = Math.max(1, Runtime.getRuntime().availableProcessors());
|
||||
return Math.max(1, (visibleProcessors + 1) / 2);
|
||||
@@ -349,6 +375,90 @@ public class SimilarAsinImageEmbedder {
|
||||
failed.get(), skipped, deadlineReached);
|
||||
}
|
||||
|
||||
/**
|
||||
* Task 16:短预算 best-effort 预取。在 budgetSeconds 内尽力下载 resize,
|
||||
* 预算耗尽即停止并取消在途任务;缺图由调用方回退为 URL。
|
||||
* 返回未预取(skipped)数量。单个 url 失败不阻断其余 url。
|
||||
*/
|
||||
int prefetchToDiskBestEffort(Collection<String> urls, ImageSpool imageSpool, long budgetSeconds) {
|
||||
if (urls == null || urls.isEmpty() || imageSpool == null || budgetSeconds <= 0L) {
|
||||
return 0;
|
||||
}
|
||||
Set<String> distinctUrls = new LinkedHashSet<>();
|
||||
for (String url : urls) {
|
||||
if (url == null) {
|
||||
continue;
|
||||
}
|
||||
String trimmed = url.trim();
|
||||
if (!trimmed.isEmpty() && imageSpool.get(trimmed) == null) {
|
||||
distinctUrls.add(trimmed);
|
||||
}
|
||||
}
|
||||
if (distinctUrls.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
long deadlineNanos = System.nanoTime() + TimeUnit.SECONDS.toNanos(budgetSeconds);
|
||||
AtomicInteger completed = new AtomicInteger();
|
||||
CompletionService<Object> completion = new ExecutorCompletionService<>(downloadPool);
|
||||
Iterator<String> pending = distinctUrls.iterator();
|
||||
List<Future<?>> active = new ArrayList<>(Math.min(downloadPoolSize, distinctUrls.size()));
|
||||
while (pending.hasNext() && active.size() < downloadPoolSize) {
|
||||
active.add(submitBestEffortPrefetch(completion, pending.next(), imageSpool));
|
||||
}
|
||||
boolean deadlineReached = false;
|
||||
while (!active.isEmpty()) {
|
||||
long remainingNanos = deadlineNanos - System.nanoTime();
|
||||
if (remainingNanos <= 0L) {
|
||||
deadlineReached = true;
|
||||
break;
|
||||
}
|
||||
try {
|
||||
Future<Object> finished = completion.poll(remainingNanos, TimeUnit.NANOSECONDS);
|
||||
if (finished == null) {
|
||||
deadlineReached = true;
|
||||
break;
|
||||
}
|
||||
active.remove(finished);
|
||||
completed.incrementAndGet();
|
||||
if (pending.hasNext()) {
|
||||
active.add(submitBestEffortPrefetch(completion, pending.next(), imageSpool));
|
||||
}
|
||||
} catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
cancelAll(active);
|
||||
return distinctUrls.size() - completed.get();
|
||||
}
|
||||
}
|
||||
if (deadlineReached) {
|
||||
cancelAll(active);
|
||||
}
|
||||
int skipped = Math.max(0, distinctUrls.size() - completed.get());
|
||||
log.info("[similar-asin][image] disk prefetch best-effort finished total={} completed={} spooled={} skipped={} budgetSeconds={}",
|
||||
distinctUrls.size(), completed.get(), imageSpool.size(), skipped, budgetSeconds);
|
||||
return skipped;
|
||||
}
|
||||
|
||||
private Future<?> submitBestEffortPrefetch(CompletionService<Object> completion,
|
||||
String url,
|
||||
ImageSpool imageSpool) {
|
||||
return completion.submit(() -> {
|
||||
try {
|
||||
ResizedImage thumb = readLocalCachedThumb(url);
|
||||
if (thumb == null) {
|
||||
thumb = fetchAndResizeDirectForTest(url);
|
||||
if (thumb == null) {
|
||||
thumb = fetchAndResizeDirect(url);
|
||||
}
|
||||
}
|
||||
ensureImageWorkNotInterrupted();
|
||||
imageSpool.put(url, thumb);
|
||||
} catch (Exception ex) {
|
||||
log.debug("[similar-asin][image] best-effort prefetch fail url={} err={}", url, errorSummary(ex));
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private Future<?> submitDiskPrefetch(CompletionService<Object> completion,
|
||||
String url,
|
||||
ImageSpool imageSpool,
|
||||
|
||||
Reference in New Issue
Block a user