This is an automated email from the ASF dual-hosted git repository. reswqa pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/flink.git
commit ae20292b5c151714f2f64d84398dd4e5020eb9fc Author: Zihao Chen <[email protected]> AuthorDate: Tue Jul 7 14:43:07 2026 +0800 [FLINK-40097][historyserver] Prioritize on-demand fetching for accessed jobs --- .../generated/history_server_configuration.html | 6 + .../flink/configuration/HistoryServerOptions.java | 11 + .../history/AbstractHistoryServerHandler.java | 131 +++++++++++- .../webmonitor/history/ArchiveMetaInfo.java | 12 +- .../runtime/webmonitor/history/HistoryServer.java | 77 +++++-- .../HistoryServerApplicationArchiveFetcher.java | 12 +- .../history/HistoryServerArchiveFetcher.java | 124 ++++++++++- .../history/HistoryServerRocksDBHandler.java | 10 +- .../HistoryServerStaticFileServerHandler.java | 19 +- .../history/AbstractHistoryServerHandlerTest.java | 226 +++++++++++++++++++-- ...HistoryServerApplicationArchiveFetcherTest.java | 1 + .../history/HistoryServerArchiveFetcherTest.java | 78 ++++++- .../webmonitor/utils/WebFrontendBootstrapTest.java | 8 +- 13 files changed, 657 insertions(+), 58 deletions(-) diff --git a/docs/layouts/shortcodes/generated/history_server_configuration.html b/docs/layouts/shortcodes/generated/history_server_configuration.html index 6048ea7d5ee..ef521d5f999 100644 --- a/docs/layouts/shortcodes/generated/history_server_configuration.html +++ b/docs/layouts/shortcodes/generated/history_server_configuration.html @@ -68,6 +68,12 @@ <td>Integer</td> <td>The size of the common pool for archive fetching.</td> </tr> + <tr> + <td><h5>historyserver.lazy.fetch.executor.individual.pool-size</h5></td> + <td style="word-wrap: break-word;">4</td> + <td>Integer</td> + <td>The size of the individual pool for archive fetching.</td> + </tr> <tr> <td><h5>historyserver.log.jobmanager.url-pattern</h5></td> <td style="word-wrap: break-word;">(none)</td> diff --git a/flink-core/src/main/java/org/apache/flink/configuration/HistoryServerOptions.java b/flink-core/src/main/java/org/apache/flink/configuration/HistoryServerOptions.java index 1e34bcc3656..05de6dee6ba 100644 --- a/flink-core/src/main/java/org/apache/flink/configuration/HistoryServerOptions.java +++ b/flink-core/src/main/java/org/apache/flink/configuration/HistoryServerOptions.java @@ -284,6 +284,17 @@ public class HistoryServerOptions { .text("The size of the common pool for archive fetching.") .build()); + public static final ConfigOption<Integer> + HISTORY_SERVER_LAZY_FETCH_EXECUTOR_INDIVIDUAL_POOL_SIZE = + key("historyserver.lazy.fetch.executor.individual.pool-size") + .intType() + .defaultValue(4) + .withDescription( + Description.builder() + .text( + "The size of the individual pool for archive fetching.") + .build()); + /** The type of archive storage. */ public enum HistoryServerArchiveStorageType { /** Local file system. */ diff --git a/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/AbstractHistoryServerHandler.java b/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/AbstractHistoryServerHandler.java index 8b93ef01349..d86191d512f 100644 --- a/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/AbstractHistoryServerHandler.java +++ b/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/AbstractHistoryServerHandler.java @@ -19,12 +19,17 @@ package org.apache.flink.runtime.webmonitor.history; +import org.apache.flink.configuration.HistoryServerOptions; +import org.apache.flink.core.fs.Path; import org.apache.flink.runtime.rest.NotFoundException; import org.apache.flink.runtime.rest.handler.RestHandlerException; import org.apache.flink.runtime.rest.handler.legacy.files.StaticFileServerHandler; import org.apache.flink.runtime.rest.handler.router.RoutedRequest; import org.apache.flink.runtime.rest.handler.util.HandlerUtils; +import org.apache.flink.runtime.rest.messages.ApplicationsOverviewHeaders; import org.apache.flink.runtime.rest.messages.ErrorResponseBody; +import org.apache.flink.runtime.rest.messages.JobsOverviewHeaders; +import org.apache.flink.util.Preconditions; import org.apache.flink.shaded.netty4.io.netty.channel.ChannelFuture; import org.apache.flink.shaded.netty4.io.netty.channel.ChannelFutureListener; @@ -45,6 +50,8 @@ import org.apache.flink.shaded.netty4.io.netty.handler.stream.ChunkedFile; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.Nullable; + import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; @@ -57,7 +64,11 @@ import java.text.SimpleDateFormat; import java.util.Collections; import java.util.Date; import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import static org.apache.flink.configuration.HistoryServerOptions.HistoryServerArchiveLoadMode.LAZY; +import static org.apache.flink.runtime.webmonitor.history.HistoryServerArchiveFetcher.JSON_FILE_ENDING; import static org.apache.flink.shaded.netty4.io.netty.handler.codec.http.HttpHeaderNames.CONNECTION; import static org.apache.flink.shaded.netty4.io.netty.handler.codec.http.HttpHeaderNames.IF_MODIFIED_SINCE; import static org.apache.flink.shaded.netty4.io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR; @@ -73,14 +84,34 @@ public abstract class AbstractHistoryServerHandler<Entry> private static final Logger LOG = LoggerFactory.getLogger(AbstractHistoryServerHandler.class); + /** Matches jobs/{jobId} or jobs/{jobId}/... paths. */ + private static final Pattern JOB_ID_PATTERN = Pattern.compile("^jobs/([^/]+)(?:/.*)?$"); + + /** Matches applications/{appId}. */ + private static final Pattern APPLICATION_ID_PATTERN = Pattern.compile("^applications/([^/]+)$"); + /** The path in which the static documents are. */ protected final File rootPath; protected final ArchiveStorage<Entry> archiveStorage; - protected AbstractHistoryServerHandler(ArchiveStorage<Entry> archiveStorage, File rootPath) + private final HistoryServerOptions.HistoryServerArchiveLoadMode archiveLoadMode; + + // for lazy load + @Nullable protected HistoryServerArchiveFetcher<Entry> archiveFetcher; + @Nullable protected HistoryServerApplicationArchiveFetcher<Entry> applicationArchiveFetcher; + + protected AbstractHistoryServerHandler( + ArchiveStorage<Entry> archiveStorage, + HistoryServerOptions.HistoryServerArchiveLoadMode archiveLoadMode, + @Nullable HistoryServerArchiveFetcher<Entry> archiveFetcher, + @Nullable HistoryServerApplicationArchiveFetcher<Entry> applicationArchiveFetcher, + File rootPath) throws IOException { this.archiveStorage = archiveStorage; + this.archiveLoadMode = archiveLoadMode; + this.archiveFetcher = archiveFetcher; + this.applicationArchiveFetcher = applicationArchiveFetcher; this.rootPath = checkNotNull(rootPath).getCanonicalFile(); } @@ -136,11 +167,12 @@ public abstract class AbstractHistoryServerHandler<Entry> requestPath = requestPath + "index.html"; } - if (!requestPath.contains(".")) { // we assume that the path ends in either .html or .js + // we assume that the path ends in either .html or .js + if (!requestPath.contains(".")) { requestPath = requestPath + ".json"; LOG.debug("Responding to request for path {}", requestPath); - Entry resource = loadResource(requestPath); + Entry resource = loadResource(requestPath, archiveLoadMode); if (resource == null) { LOG.debug("Unable to load requested resource {}", requestPath); @@ -266,14 +298,67 @@ public abstract class AbstractHistoryServerHandler<Entry> /** * Loads the resource for the given request path from the archive storage. * + * <p>The resource has four cases: + * + * <p>1. /index.html or other web resource files, should be loaded from classloader {@link + * #tryLoadFromClassloader} + * + * <p>2. /config.json, will be created when HistoryServer started + * + * <p>3. /jobs/overview.json (and /jobs/jobid.json) or /applications/overview.json (and + * /applications/applicationid.json), will be loaded synchronously + * + * <p>4. /jobs/<jobid>/.. will be loaded asynchronously + * * @param requestPath The request path + * @param archiveLoadMode The archive load mode * @return The resource for the given request path, or null if not found */ - private Entry loadResource(String requestPath) throws Exception { + private Entry loadResource( + String requestPath, HistoryServerOptions.HistoryServerArchiveLoadMode archiveLoadMode) + throws Exception { String requestKey = requestPath.startsWith("/") ? requestPath.substring(1) : requestPath; + + if (LAZY.equals(archiveLoadMode)) { + Preconditions.checkNotNull(archiveFetcher); + Preconditions.checkNotNull(applicationArchiveFetcher); + // need to update for overview + if (requestKey.equals(JobsOverviewHeaders.URL.substring(1) + JSON_FILE_ENDING) + || requestKey.equals( + ApplicationsOverviewHeaders.URL.substring(1) + JSON_FILE_ENDING)) { + archiveFetcher.fetchArchives(archiveLoadMode); + applicationArchiveFetcher.fetchArchives(archiveLoadMode); + return archiveStorage.getEntry(requestKey); + } + // for application/${applicationId}, return directly + String applicationId = extractApplicationId(requestKey); + if (applicationId != null) { + return archiveStorage.getEntry(requestKey); + } + // for job/${jobId}... + String jobId = extractJobId(requestKey); + if (jobId != null) { + if (archiveStorage.exists(requestKey)) { + return archiveStorage.getEntry(requestKey); + } + ArchiveMetaInfo jobArchiveMetaInfo = archiveFetcher.getArchiveMetaInfo(jobId); + if (archiveFetcher.needLazyLoadIndividually(jobId)) { + Path archivePath = + jobArchiveMetaInfo == null ? null : jobArchiveMetaInfo.getArchivePath(); + archiveFetcher.lazyFetchArchiveProactively(jobId, archivePath); + } + // wait for the job archive to be loaded + if (!requestKey.endsWith(jobId + JSON_FILE_ENDING)) { + archiveFetcher.waitLazyFetchArchiveFinished(jobId); + } + return archiveStorage.getEntry(requestKey); + } + } + if (archiveStorage.exists(requestKey)) { return archiveStorage.getEntry(requestKey); } + return null; } @@ -320,4 +405,42 @@ public abstract class AbstractHistoryServerHandler<Entry> } } } + + /** + * Extracts the job ID from the request path. + * + * @param requestPath Request path, e.g., {@code /jobs/abc123.../vertices} + * @return jobId string; returns {@code null} if path does not match + */ + @Nullable + protected static String extractJobId(String requestPath) { + Matcher matcher = JOB_ID_PATTERN.matcher(requestPath); + if (matcher.matches()) { + return matcher.group(1); + } + return null; + } + + /** + * Extracts the application ID from the request path. + * + * @param requestPath Request path, e.g., {@code /applications/abc123...} + * @return applicationId string; returns {@code null} if path does not match + */ + @Nullable + protected static String extractApplicationId(String requestPath) { + Matcher matcher = APPLICATION_ID_PATTERN.matcher(requestPath); + if (matcher.matches()) { + return matcher.group(1); + } + return null; + } + + /** Factory for creating instances of {@link AbstractHistoryServerHandler}. */ + public interface HistoryServerHandlerFactory { + AbstractHistoryServerHandler<?> createHistoryServerHandler( + HistoryServerArchiveFetcher<?> archiveFetcher, + HistoryServerApplicationArchiveFetcher<?> applicationArchiveFetcher) + throws IOException; + } } diff --git a/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/ArchiveMetaInfo.java b/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/ArchiveMetaInfo.java index 710868803dd..7a520506034 100644 --- a/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/ArchiveMetaInfo.java +++ b/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/ArchiveMetaInfo.java @@ -18,16 +18,22 @@ package org.apache.flink.runtime.webmonitor.history; +import org.apache.flink.core.fs.Path; + /** Meta info for archived job. */ public class ArchiveMetaInfo { private final String archiveId; private volatile HistoryServerArchiveFetcher.ArchiveEventType eventType; + private final Path archivePath; public ArchiveMetaInfo( - String archiveId, HistoryServerArchiveFetcher.ArchiveEventType eventType) { + String archiveId, + HistoryServerArchiveFetcher.ArchiveEventType eventType, + Path archivePath) { this.archiveId = archiveId; this.eventType = eventType; + this.archivePath = archivePath; } public String getArchiveId() { @@ -41,4 +47,8 @@ public class ArchiveMetaInfo { public void setEventType(HistoryServerArchiveFetcher.ArchiveEventType eventType) { this.eventType = eventType; } + + public Path getArchivePath() { + return archivePath; + } } diff --git a/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/HistoryServer.java b/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/HistoryServer.java index d3e77c035da..151fd43b823 100644 --- a/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/HistoryServer.java +++ b/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/HistoryServer.java @@ -77,6 +77,8 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import static org.apache.flink.configuration.HistoryServerOptions.HISTORY_SERVER_LAZY_FETCH_EXECUTOR_COMMON_POOL_SIZE; +import static org.apache.flink.configuration.HistoryServerOptions.HISTORY_SERVER_LAZY_FETCH_EXECUTOR_INDIVIDUAL_POOL_SIZE; +import static org.apache.flink.configuration.HistoryServerOptions.HistoryServerArchiveLoadMode.LAZY; import static org.apache.flink.runtime.webmonitor.history.HistoryServerApplicationArchiveFetcher.APPLICATIONS_SUBDIR; import static org.apache.flink.runtime.webmonitor.history.HistoryServerApplicationArchiveFetcher.APPLICATION_OVERVIEWS_SUBDIR; import static org.apache.flink.runtime.webmonitor.history.HistoryServerArchiveFetcher.JOBS_SUBDIR; @@ -259,6 +261,7 @@ public class HistoryServer { archiveLoadMode = config.get(HistoryServerOptions.HISTORY_SERVER_ARCHIVE_LOAD_MODE); HistoryServerOptions.HistoryServerArchiveStorageType archiveStorageType = config.get(HistoryServerOptions.HISTORY_SERVER_ARCHIVE_STORAGE_TYPE); + AbstractHistoryServerHandler.HistoryServerHandlerFactory historyServerHandlerFactory; switch (archiveStorageType) { case FILE: // create directories for job and application overview updates @@ -267,17 +270,15 @@ public class HistoryServer { Files.createDirectories(webDir.toPath().resolve(APPLICATIONS_SUBDIR)); Files.createDirectories(webDir.toPath().resolve(APPLICATION_OVERVIEWS_SUBDIR)); archiveStorage = new FileArchiveStorage(webDir); - historyServerHandler = - new HistoryServerStaticFileServerHandler( - (FileArchiveStorage) archiveStorage, webDir); + historyServerHandlerFactory = + createFileHandlerFactory((FileArchiveStorage) archiveStorage, webDir); break; case ROCKSDB: File dbPath = new File(webDir, "rocksdb-" + UUID.randomUUID()); Files.createDirectories(dbPath.toPath()); archiveStorage = new RocksDBArchiveStorage(dbPath, config); - historyServerHandler = - new HistoryServerRocksDBHandler( - (RocksDBArchiveStorage) archiveStorage, webDir); + historyServerHandlerFactory = + createRocksDBHandlerFactory((RocksDBArchiveStorage) archiveStorage, webDir); break; default: throw new FlinkException("Unsupported archive storage type: " + archiveStorageType); @@ -288,6 +289,8 @@ public class HistoryServer { new ConcurrentHashMap<>(); int lazyFetchExecutorCommonPoolSize = config.get(HISTORY_SERVER_LAZY_FETCH_EXECUTOR_COMMON_POOL_SIZE); + int lazyFetchExecutorIndividualPoolSize = + config.get(HISTORY_SERVER_LAZY_FETCH_EXECUTOR_INDIVIDUAL_POOL_SIZE); archiveFetcher = new HistoryServerArchiveFetcher<>( refreshDirs, @@ -297,7 +300,8 @@ public class HistoryServer { CompositeArchiveRetainedStrategy.createForJobFromConfig(config), archiveStorage, archiveMetaInfoCache, - lazyFetchExecutorCommonPoolSize); + lazyFetchExecutorCommonPoolSize, + lazyFetchExecutorIndividualPoolSize); applicationArchiveFetcher = new HistoryServerApplicationArchiveFetcher<>( refreshDirs, @@ -308,7 +312,12 @@ public class HistoryServer { archiveStorage, archiveMetaInfoCache, applicationArchiveMetaInfoCache, - lazyFetchExecutorCommonPoolSize); + lazyFetchExecutorCommonPoolSize, + lazyFetchExecutorIndividualPoolSize); + + historyServerHandler = + historyServerHandlerFactory.createHistoryServerHandler( + archiveFetcher, applicationArchiveFetcher); this.shutdownHook = ShutdownHookUtil.addShutdownHook( @@ -364,6 +373,30 @@ public class HistoryServer { } } + @SuppressWarnings("unchecked") + private AbstractHistoryServerHandler.HistoryServerHandlerFactory createFileHandlerFactory( + FileArchiveStorage fileArchiveStorage, File webDir) { + return (archiveFetcher, applicationArchiveFetcher) -> + new HistoryServerStaticFileServerHandler( + fileArchiveStorage, + archiveLoadMode, + (HistoryServerArchiveFetcher<File>) archiveFetcher, + (HistoryServerApplicationArchiveFetcher<File>) applicationArchiveFetcher, + webDir); + } + + @SuppressWarnings("unchecked") + private AbstractHistoryServerHandler.HistoryServerHandlerFactory createRocksDBHandlerFactory( + RocksDBArchiveStorage rocksDBArchiveStorage, File webDir) { + return (archiveFetcher, applicationArchiveFetcher) -> + new HistoryServerRocksDBHandler( + rocksDBArchiveStorage, + archiveLoadMode, + (HistoryServerArchiveFetcher<String>) archiveFetcher, + (HistoryServerApplicationArchiveFetcher<String>) applicationArchiveFetcher, + webDir); + } + // ------------------------------------------------------------------------ // Life-cycle // ------------------------------------------------------------------------ @@ -399,11 +432,20 @@ public class HistoryServer { createDashboardConfigFile(); router.addGet("/:*", historyServerHandler); - executor.scheduleWithFixedDelay( - getArchiveFetchingRunnable(archiveLoadMode), - 0, - refreshIntervalMillis, - TimeUnit.MILLISECONDS); + if (LAZY.equals(archiveLoadMode)) { + executor.submit(getArchiveFetchingRunnable(archiveLoadMode)); + executor.scheduleWithFixedDelay( + getArchiveCleaningRunnable(), + refreshIntervalMillis, + refreshIntervalMillis, + TimeUnit.MILLISECONDS); + } else { + executor.scheduleWithFixedDelay( + getArchiveFetchingRunnable(archiveLoadMode), + 0, + refreshIntervalMillis, + TimeUnit.MILLISECONDS); + } netty = new WebFrontendBootstrap( @@ -421,6 +463,15 @@ public class HistoryServer { FatalExitExceptionHandler.INSTANCE); } + private Runnable getArchiveCleaningRunnable() { + return Runnables.withUncaughtExceptionHandler( + () -> { + archiveFetcher.cleanUpArchives(archiveLoadMode); + applicationArchiveFetcher.cleanUpArchives(archiveLoadMode); + }, + FatalExitExceptionHandler.INSTANCE); + } + void stop() { if (shutdownRequested.compareAndSet(false, true)) { synchronized (startupShutdownLock) { diff --git a/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/HistoryServerApplicationArchiveFetcher.java b/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/HistoryServerApplicationArchiveFetcher.java index f3b479aaf16..038abc85632 100644 --- a/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/HistoryServerApplicationArchiveFetcher.java +++ b/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/HistoryServerApplicationArchiveFetcher.java @@ -89,7 +89,9 @@ public class HistoryServerApplicationArchiveFetcher<Entry> ArchiveStorage<Entry> archiveStorage, ConcurrentHashMap<String, ArchiveMetaInfo> archiveMetaInfoCache, ConcurrentHashMap<String, ArchiveMetaInfo> applicationArchiveMetaInfoCache, - int lazyFetchExecutorCommonPoolSize) { + int lazyFetchExecutorCommonPoolSize, + int lazyFetchExecutorIndividualPoolSize) + throws IOException { super( refreshDirs, webDir, @@ -98,7 +100,8 @@ public class HistoryServerApplicationArchiveFetcher<Entry> retainedStrategy, archiveStorage, archiveMetaInfoCache, - lazyFetchExecutorCommonPoolSize); + lazyFetchExecutorCommonPoolSize, + lazyFetchExecutorIndividualPoolSize); this.applicationArchiveMetaInfoCache = applicationArchiveMetaInfoCache; for (HistoryServer.RefreshLocation refreshDir : refreshDirs) { @@ -186,7 +189,7 @@ public class HistoryServerApplicationArchiveFetcher<Entry> .add(jobId); ArchiveEvent processArchiveEvents = LAZY.equals(archiveLoadMode) - ? lazyProcessJobArchive(jobId, jobArchive.getPath()) + ? lazyProcessJobArchive(jobId, jobArchive.getPath(), false) : processJobArchive(jobId, jobArchive.getPath()); events.add(processArchiveEvents); } @@ -314,7 +317,8 @@ public class HistoryServerApplicationArchiveFetcher<Entry> List<ArchiveEvent> lazyProcessArchive(String archiveId, Path archivePath, Path refreshDir) throws Exception { List<ArchiveEvent> events = new ArrayList<>(); - ArchiveMetaInfo archiveMetaInfo = new ArchiveMetaInfo(archiveId, OVERVIEW_PARSING); + ArchiveMetaInfo archiveMetaInfo = + new ArchiveMetaInfo(archiveId, OVERVIEW_PARSING, archivePath); ArchiveMetaInfo existing = applicationArchiveMetaInfoCache.putIfAbsent(archiveId, archiveMetaInfo); if (existing != null) { diff --git a/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/HistoryServerArchiveFetcher.java b/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/HistoryServerArchiveFetcher.java index 2de41ed8091..e667d9cd365 100644 --- a/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/HistoryServerArchiveFetcher.java +++ b/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/HistoryServerArchiveFetcher.java @@ -18,6 +18,7 @@ package org.apache.flink.runtime.webmonitor.history; +import org.apache.flink.annotation.VisibleForTesting; import org.apache.flink.api.common.JobID; import org.apache.flink.api.common.JobStatus; import org.apache.flink.configuration.HistoryServerOptions; @@ -40,6 +41,8 @@ import org.apache.flink.shaded.jackson2.com.fasterxml.jackson.databind.ObjectMap import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.Nullable; + import java.io.File; import java.io.IOException; import java.io.StringWriter; @@ -135,7 +138,9 @@ public class HistoryServerArchiveFetcher<Entry> implements AutoCloseable { /** Executor for loading archives. */ private final ExecutorService commonFetchExecutor; + private final ExecutorService individualFetchExecutor; private final Map<String, Future<?>> commonFetchTasks; + private final Map<String, Future<?>> individualFetchTasks; private final ConcurrentHashMap<String, ArchiveMetaInfo> archiveMetaInfoCache; HistoryServerArchiveFetcher( @@ -146,7 +151,9 @@ public class HistoryServerArchiveFetcher<Entry> implements AutoCloseable { ArchiveRetainedStrategy retainedStrategy, ArchiveStorage<Entry> archiveStorage, ConcurrentHashMap<String, ArchiveMetaInfo> archiveMetaInfoCache, - int lazyFetchExecutorCommonPoolSize) { + int lazyFetchExecutorCommonPoolSize, + int lazyFetchExecutorIndividualPoolSize) + throws IOException { this.refreshDirs = checkNotNull(refreshDirs); this.archiveEventListener = archiveEventListener; this.processExpiredArchiveDeletion = cleanupExpiredArchives; @@ -162,7 +169,12 @@ public class HistoryServerArchiveFetcher<Entry> implements AutoCloseable { Executors.newFixedThreadPool( lazyFetchExecutorCommonPoolSize, new ExecutorThreadFactory("HistoryServer-commonFetchExecutor")); + this.individualFetchExecutor = + Executors.newFixedThreadPool( + lazyFetchExecutorIndividualPoolSize, + new ExecutorThreadFactory("HistoryServer-individualFetchExecutor")); this.commonFetchTasks = new ConcurrentHashMap<>(); + this.individualFetchTasks = new ConcurrentHashMap<>(); updateJobOverview(); if (LOG.isInfoEnabled()) { @@ -174,6 +186,12 @@ public class HistoryServerArchiveFetcher<Entry> implements AutoCloseable { void fetchArchives(HistoryServerOptions.HistoryServerArchiveLoadMode archiveLoadMode) { LOG.debug("Starting archive fetching."); + scanArchives(archiveLoadMode, true); + } + + void scanArchives( + HistoryServerOptions.HistoryServerArchiveLoadMode archiveLoadMode, boolean fetch) { + LOG.debug("Starting archive fetching."); try { List<ArchiveEvent> events = new ArrayList<>(); Map<Path, Set<String>> archivesToRemove = new HashMap<>(); @@ -211,7 +229,9 @@ public class HistoryServerArchiveFetcher<Entry> implements AutoCloseable { continue; } - fetchArchive(refreshDir, archiveId, archivePath, archiveLoadMode, events); + if (fetch) { + fetchArchive(refreshDir, archiveId, archivePath, archiveLoadMode, events); + } } } @@ -228,7 +248,7 @@ public class HistoryServerArchiveFetcher<Entry> implements AutoCloseable { updateOverview(); } events.forEach(archiveEventListener); - LOG.debug("Finished archive fetching."); + LOG.debug("Finished archive scan."); } catch (Exception e) { LOG.error("Critical failure while fetching/processing archives.", e); } @@ -510,17 +530,30 @@ public class HistoryServerArchiveFetcher<Entry> implements AutoCloseable { // -------------------------------- Lazy Load ---------------------------------------- List<ArchiveEvent> lazyProcessArchive(String archiveId, Path archivePath, Path refreshDir) throws Exception { - return Collections.singletonList(lazyProcessJobArchive(archiveId, archivePath)); + return Collections.singletonList(lazyProcessJobArchive(archiveId, archivePath, false)); } - ArchiveEvent lazyProcessJobArchive(String jobId, Path jobArchive) throws Exception { - final ArchiveMetaInfo archiveMetaInfo = new ArchiveMetaInfo(jobId, PENDING); + ArchiveEvent lazyProcessJobArchive(String jobId, Path jobArchive, boolean individual) + throws Exception { + final ArchiveMetaInfo archiveMetaInfo = new ArchiveMetaInfo(jobId, PENDING, jobArchive); ArchiveMetaInfo existing = archiveMetaInfoCache.putIfAbsent(jobId, archiveMetaInfo); if (existing != null) { return new ArchiveEvent(jobId, existing.getEventType()); } archiveMetaInfo.setEventType(ArchiveEventType.OVERVIEW_PARSING); + ExecutorService fetchExecutor; + Map<String, Future<?>> fetchTasks; + if (individual) { + fetchExecutor = individualFetchExecutor; + fetchTasks = individualFetchTasks; + } else { + fetchExecutor = commonFetchExecutor; + fetchTasks = commonFetchTasks; + } + + archiveMetaInfo.setEventType(ArchiveEventType.OVERVIEW_PARSING); + Collection<ArchivedJson> archivedJsons = FsJsonArchivist.readArchivedJsons(jobArchive); List<ArchivedJson> detailArchives = new ArrayList<>(); boolean overviewCreated = false; @@ -554,7 +587,7 @@ public class HistoryServerArchiveFetcher<Entry> implements AutoCloseable { if (!detailArchives.isEmpty()) { Future<?> future = - commonFetchExecutor.submit( + fetchExecutor.submit( () -> { try { archiveMetaInfo.setEventType(ArchiveEventType.DETAIL_PARSING); @@ -577,12 +610,15 @@ public class HistoryServerArchiveFetcher<Entry> implements AutoCloseable { } } archiveMetaInfo.setEventType(ArchiveEventType.CREATED); + if (individual) { + updateOverview(); + } LOG.debug("Async detail parsing for job {} finished.", jobId); } finally { - commonFetchTasks.remove(jobId); + fetchTasks.remove(jobId); } }); - commonFetchTasks.put(jobId, future); + fetchTasks.put(jobId, future); } ArchiveEventType archiveEventType = @@ -591,15 +627,85 @@ public class HistoryServerArchiveFetcher<Entry> implements AutoCloseable { return new ArchiveEvent(jobId, archiveEventType); } + void lazyFetchArchiveProactively(String jobId, @Nullable Path archivePath) throws Exception { + resetWhenTriggerLazyFetch(jobId); + + if (archivePath != null) { + lazyProcessJobArchive(jobId, archivePath, true); + return; + } + + for (HistoryServer.RefreshLocation refreshDir : refreshDirs) { + archivePath = new Path(refreshDir.getPath(), jobId); + if (refreshDir.getFs().exists(archivePath)) { + lazyProcessJobArchive(jobId, archivePath, true); + } + } + } + + void cleanUpArchives(HistoryServerOptions.HistoryServerArchiveLoadMode archiveLoadMode) { + LOG.debug("Starting archive cleanup."); + scanArchives(archiveLoadMode, false); + } + + boolean needLazyLoadIndividually(String jobId) { + ArchiveMetaInfo archiveMetaInfo = archiveMetaInfoCache.get(jobId); + if (archiveMetaInfo == null) { + return true; + } + + switch (archiveMetaInfo.getEventType()) { + case PENDING: + case OVERVIEW_PARSING: + case OVERVIEW_CREATED: + return commonFetchTasks.containsKey(jobId) + && !individualFetchTasks.containsKey(jobId); + default: + return false; + } + } + void cleanUpLazyFetchTask(String jobId) { Future<?> commonFetchTask = commonFetchTasks.get(jobId); if (commonFetchTask != null) { commonFetchTask.cancel(true); + commonFetchTasks.remove(jobId); + } + Future<?> individualFetchTask = individualFetchTasks.get(jobId); + if (individualFetchTask != null) { + individualFetchTask.cancel(true); + individualFetchTasks.remove(jobId); } } @Override public void close() { ExecutorUtils.gracefulShutdown(1L, TimeUnit.SECONDS, commonFetchExecutor); + ExecutorUtils.gracefulShutdown(1L, TimeUnit.SECONDS, individualFetchExecutor); + } + + @VisibleForTesting + Future<?> getCommonFetchTask(String jobId) { + return commonFetchTasks.get(jobId); + } + + void resetWhenTriggerLazyFetch(String jobId) { + archiveMetaInfoCache.remove(jobId); + cleanUpLazyFetchTask(jobId); + } + + void waitLazyFetchArchiveFinished(String jobId) throws Exception { + Future<?> commonFetchTask = commonFetchTasks.get(jobId); + if (commonFetchTask != null) { + commonFetchTask.get(); + } + Future<?> individualFetchTask = individualFetchTasks.get(jobId); + if (individualFetchTask != null) { + individualFetchTask.get(); + } + } + + ArchiveMetaInfo getArchiveMetaInfo(String jobId) { + return archiveMetaInfoCache.get(jobId); } } diff --git a/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/HistoryServerRocksDBHandler.java b/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/HistoryServerRocksDBHandler.java index 147bc93eaf9..f6b5a67a0ce 100644 --- a/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/HistoryServerRocksDBHandler.java +++ b/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/HistoryServerRocksDBHandler.java @@ -19,6 +19,7 @@ package org.apache.flink.runtime.webmonitor.history; +import org.apache.flink.configuration.HistoryServerOptions; import org.apache.flink.runtime.rest.handler.util.HandlerUtils; import org.apache.flink.shaded.netty4.io.netty.channel.ChannelHandler; @@ -40,9 +41,14 @@ import java.util.Collections; @ChannelHandler.Sharable public class HistoryServerRocksDBHandler extends AbstractHistoryServerHandler<String> { - public HistoryServerRocksDBHandler(RocksDBArchiveStorage rocksDBArchiveStorage, File rootPath) + public HistoryServerRocksDBHandler( + ArchiveStorage<String> archiveStorage, + HistoryServerOptions.HistoryServerArchiveLoadMode archiveLoadMode, + HistoryServerArchiveFetcher<String> archiveFetcher, + HistoryServerApplicationArchiveFetcher<String> applicationArchiveFetcher, + File rootPath) throws IOException { - super(rocksDBArchiveStorage, rootPath); + super(archiveStorage, archiveLoadMode, archiveFetcher, applicationArchiveFetcher, rootPath); } @Override diff --git a/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/HistoryServerStaticFileServerHandler.java b/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/HistoryServerStaticFileServerHandler.java index 695f8edfffe..28e0e0b7a7c 100644 --- a/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/HistoryServerStaticFileServerHandler.java +++ b/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/HistoryServerStaticFileServerHandler.java @@ -26,6 +26,8 @@ package org.apache.flink.runtime.webmonitor.history; * https://github.com/netty/netty/blob/4.0/example/src/main/java/io/netty/example/http/file/HttpStaticFileServerHandler.java * *************************************************************************** */ +import org.apache.flink.annotation.VisibleForTesting; +import org.apache.flink.configuration.HistoryServerOptions; import org.apache.flink.runtime.rest.handler.legacy.files.StaticFileServerHandler; import org.apache.flink.shaded.netty4.io.netty.channel.ChannelHandler; @@ -35,6 +37,8 @@ import org.apache.flink.shaded.netty4.io.netty.handler.codec.http.HttpRequest; import java.io.File; import java.io.IOException; +import static org.apache.flink.configuration.HistoryServerOptions.HistoryServerArchiveLoadMode.EAGER; + /** * Simple file server handler used by the {@link HistoryServer} that serves requests to web * frontend's static files, such as HTML, CSS, JS or JSON files. @@ -52,13 +56,20 @@ public class HistoryServerStaticFileServerHandler extends AbstractHistoryServerH // ------------------------------------------------------------------------ - public HistoryServerStaticFileServerHandler(File rootPath) throws IOException { - this(new FileArchiveStorage(rootPath), rootPath); + @VisibleForTesting + public HistoryServerStaticFileServerHandler(ArchiveStorage<File> archiveStorage, File rootPath) + throws IOException { + this(archiveStorage, EAGER, null, null, rootPath); } public HistoryServerStaticFileServerHandler( - FileArchiveStorage fileArchiveStorage, File rootPath) throws IOException { - super(fileArchiveStorage, rootPath); + ArchiveStorage<File> archiveStorage, + HistoryServerOptions.HistoryServerArchiveLoadMode archiveLoadMode, + HistoryServerArchiveFetcher<File> archiveFetcher, + HistoryServerApplicationArchiveFetcher<File> applicationArchiveFetcher, + File rootPath) + throws IOException { + super(archiveStorage, archiveLoadMode, archiveFetcher, applicationArchiveFetcher, rootPath); } // ------------------------------------------------------------------------ diff --git a/flink-runtime-web/src/test/java/org/apache/flink/runtime/webmonitor/history/AbstractHistoryServerHandlerTest.java b/flink-runtime-web/src/test/java/org/apache/flink/runtime/webmonitor/history/AbstractHistoryServerHandlerTest.java index 479f3dcb07b..cbff6ef4496 100644 --- a/flink-runtime-web/src/test/java/org/apache/flink/runtime/webmonitor/history/AbstractHistoryServerHandlerTest.java +++ b/flink-runtime-web/src/test/java/org/apache/flink/runtime/webmonitor/history/AbstractHistoryServerHandlerTest.java @@ -18,8 +18,10 @@ package org.apache.flink.runtime.webmonitor.history; +import org.apache.flink.api.common.JobID; import org.apache.flink.api.java.tuple.Tuple2; import org.apache.flink.configuration.Configuration; +import org.apache.flink.configuration.HistoryServerOptions.HistoryServerArchiveLoadMode; import org.apache.flink.runtime.rest.handler.router.Router; import org.apache.flink.runtime.webmonitor.testutils.HttpUtils; import org.apache.flink.runtime.webmonitor.utils.WebFrontendBootstrap; @@ -35,12 +37,23 @@ import org.junit.jupiter.api.io.TempDir; import org.slf4j.LoggerFactory; import java.io.File; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import static org.apache.flink.configuration.HistoryServerOptions.HistoryServerArchiveLoadMode.EAGER; +import static org.apache.flink.configuration.HistoryServerOptions.HistoryServerArchiveLoadMode.LAZY; +import static org.apache.flink.runtime.webmonitor.history.HistoryServerTestUtils.createJobArchive; +import static org.apache.flink.runtime.webmonitor.history.HistoryServerTestUtils.createRefreshLocation; import static org.assertj.core.api.Assertions.assertThat; /** @@ -50,10 +63,37 @@ import static org.assertj.core.api.Assertions.assertThat; @ExtendWith(ParameterizedTestExtension.class) public class AbstractHistoryServerHandlerTest { + @Parameter public HandlerFactory handlerFactory; + + @TempDir private Path tmpDir; + private Path uploadDir; + private Path remoteArchiveRootPath; + + private Path webDir; + private AbstractHistoryServerHandler<?> handler; + private WebFrontendBootstrap webUI; + private String baseUrl; + /** Factory that creates a concrete handler bound to the given web directory. */ @FunctionalInterface public interface HandlerFactory { - AbstractHistoryServerHandler<?> create(File webDir) throws Exception; + AbstractHistoryServerHandler<?> create( + File webDir, + HistoryServerArchiveLoadMode archiveLoadMode, + List<HistoryServer.RefreshLocation> refreshDirs) + throws Exception; + } + + /** Constructor reference for a concrete {@link AbstractHistoryServerHandler} subclass. */ + @FunctionalInterface + private interface HandlerConstructor<T> { + AbstractHistoryServerHandler<T> create( + ArchiveStorage<T> archiveStorage, + HistoryServerArchiveLoadMode archiveLoadMode, + HistoryServerArchiveFetcher<T> archiveFetcher, + HistoryServerApplicationArchiveFetcher<T> applicationArchiveFetcher, + File webDir) + throws IOException; } /** @@ -62,32 +102,98 @@ public class AbstractHistoryServerHandlerTest { */ @Parameters(name = "handlerFactory={0}") private static Collection<HandlerFactory> handlerFactories() { - HandlerFactory staticFileServerHandlerFactory = HistoryServerStaticFileServerHandler::new; + HandlerFactory staticFileServerHandlerFactory = + (webDir, mode, refreshDirs) -> + buildHandler( + webDir, + mode, + refreshDirs, + new FileArchiveStorage(webDir), + HistoryServerStaticFileServerHandler::new); + HandlerFactory rocksDBHandlerFactory = - webDir -> - new HistoryServerRocksDBHandler( - new RocksDBArchiveStorage( - new File(webDir, "rocksdb-" + UUID.randomUUID()), - new Configuration()), - webDir); + (webDir, mode, refreshDirs) -> { + File dbPath = new File(webDir, "rocksdb-" + UUID.randomUUID()); + Files.createDirectories(dbPath.toPath()); + return buildHandler( + webDir, + mode, + refreshDirs, + new RocksDBArchiveStorage(dbPath, new Configuration()), + HistoryServerRocksDBHandler::new); + }; + return Arrays.asList(staticFileServerHandlerFactory, rocksDBHandlerFactory); } - @Parameter public HandlerFactory handlerFactory; + private static <T> AbstractHistoryServerHandler<?> buildHandler( + File webDir, + HistoryServerArchiveLoadMode mode, + List<HistoryServer.RefreshLocation> refreshDirs, + ArchiveStorage<T> baseStorage, + HandlerConstructor<T> handlerCtor) + throws Exception { + ArchiveStorage<T> storage = + LAZY == mode + ? new HistoryServerTestUtils.BlockingArchiveStorage<>( + baseStorage, "/config") + : baseStorage; - @TempDir private Path tmpDir; + ConcurrentHashMap<String, ArchiveMetaInfo> jobMetaInfoCache = new ConcurrentHashMap<>(); + ConcurrentHashMap<String, ArchiveMetaInfo> applicationMetaInfoCache = + new ConcurrentHashMap<>(); - private Path webDir; - private AbstractHistoryServerHandler<?> handler; - private WebFrontendBootstrap webUI; - private String baseUrl; + HistoryServerArchiveFetcher<T> archiveFetcher = + new HistoryServerArchiveFetcher<>( + refreshDirs, + webDir, + ignored -> {}, + false, + HistoryServerTestUtils.RETAIN_ALL, + storage, + jobMetaInfoCache, + 4, + 4); + HistoryServerApplicationArchiveFetcher<T> applicationArchiveFetcher = + new HistoryServerApplicationArchiveFetcher<>( + refreshDirs, + webDir, + ignored -> {}, + false, + HistoryServerTestUtils.RETAIN_ALL, + storage, + jobMetaInfoCache, + applicationMetaInfoCache, + 4, + 4); + + return handlerCtor.create(storage, mode, archiveFetcher, applicationArchiveFetcher, webDir); + } @BeforeEach void setUp() throws Exception { webDir = Files.createDirectory(tmpDir.resolve("webDir")); - final Path uploadDir = Files.createDirectory(tmpDir.resolve("uploadDir")); + uploadDir = Files.createDirectory(tmpDir.resolve("uploadDir")); + remoteArchiveRootPath = Files.createDirectories(tmpDir.resolve("remote")); + + // Default: eager mode + startServer(EAGER); + } + + @AfterEach + void tearDown() { + stopServer(); + } + + /** Support recreates the handler and web frontend for the given load mode. */ + private void startServer(HistoryServerArchiveLoadMode archiveLoadMode) throws Exception { + stopServer(); + + List<HistoryServer.RefreshLocation> refreshDirs = + Collections.singletonList(createRefreshLocation(remoteArchiveRootPath.toFile())); + + this.handler = handlerFactory.create(webDir.toFile(), archiveLoadMode, refreshDirs); - handler = handlerFactory.create(webDir.toFile()); Router<?> router = new Router().addGet("/:*", handler); webUI = new WebFrontendBootstrap( @@ -101,10 +207,10 @@ public class AbstractHistoryServerHandlerTest { baseUrl = "http://localhost:" + webUI.getServerPort(); } - @AfterEach - void tearDown() { + private void stopServer() { if (webUI != null) { webUI.shutdown(); + webUI = null; } } @@ -184,4 +290,88 @@ public class AbstractHistoryServerHandlerTest { assertThat(missing.f0).isEqualTo(404); assertThat(missing.f1).contains("not found"); } + + /** + * Tests {@code AbstractHistoryServerHandler#loadResource} in {@code LAZY} mode using a {@link + * HistoryServerTestUtils.BlockingArchiveStorage} to suspend writes for the detail key {@code + * jobs/<jobId>/config}. + * + * <p>Verifies the three core lazy-load behaviours: + * + * <ul> + * <li>requesting {@code /jobs/overview} triggers a synchronous phase-1 fetch that writes the + * overview keys but leaves the detail key blocked in an asynchronous task; + * <li>once phase-1 has completed, requesting {@code /jobs/<jobId>} is served immediately from + * the archive storage without waiting for the asynchronous detail task; + * <li>requesting {@code /jobs/<jobId>/config} blocks until the asynchronous detail task is + * allowed to finish, after which the response is served successfully. + * </ul> + */ + @TestTemplate + void testLazyModeLoadResource() throws Exception { + // Default mode is EAGER, we need to recreate the handler for LAZY mode with + // BlockingArchiveStorage. + startServer(LAZY); + + JobID jobId = JobID.generate(); + createJobArchive(remoteArchiveRootPath.toFile(), jobId, true); + + // Phase 1: requesting /jobs/overview triggers a synchronous fetch + // that writes the overview keys; the detail key is queued for an + // asynchronous write that is now blocked on releaseLatch. + Tuple2<Integer, String> overviewResponse = + HttpUtils.getFromHTTP(baseUrl + "/jobs/overview"); + assertThat(overviewResponse.f0).isEqualTo(200); + assertThat(overviewResponse.f1).contains(jobId.toString()); + + // The asynchronous detail write must have been reached by now. + assertThat(handler.archiveStorage) + .isInstanceOf(HistoryServerTestUtils.BlockingArchiveStorage.class); + HistoryServerTestUtils.BlockingArchiveStorage<?> blockingArchiveStorage = + (HistoryServerTestUtils.BlockingArchiveStorage<?>) handler.archiveStorage; + boolean asyncReached = blockingArchiveStorage.asyncStartLatch.await(10, TimeUnit.SECONDS); + assertThat(asyncReached).isTrue(); + + // Phase 1 keys are present, but the detail key is still blocked. + assertThat(blockingArchiveStorage.exists("overviews/" + jobId + ".json")).isTrue(); + assertThat(blockingArchiveStorage.exists("jobs/" + jobId + ".json")).isTrue(); + assertThat(blockingArchiveStorage.exists("jobs/overview.json")).isTrue(); + assertThat(blockingArchiveStorage.exists("jobs/" + jobId + "/config.json")).isFalse(); + + // Phase 2: a request for /jobs/<jobId> can be served immediately + // from the storage even though the detail task is still blocked. + Tuple2<Integer, String> jobResponse = HttpUtils.getFromHTTP(baseUrl + "/jobs/" + jobId); + assertThat(jobResponse.f0).isEqualTo(200); + assertThat(jobResponse.f1).contains(jobId.toString()); + assertThat(blockingArchiveStorage.exists("jobs/" + jobId + "/config.json")).isFalse(); + + // Phase 3: a request for /jobs/<jobId>/config will wait for the + // asynchronous detail task to finish. Issue it on a separate + // thread, verify it is still in flight, then release the latch. + AtomicReference<Tuple2<Integer, String>> detailResponse = new AtomicReference<>(); + CompletableFuture<Void> detailFuture = + CompletableFuture.runAsync( + () -> { + try { + detailResponse.set( + HttpUtils.getFromHTTP( + baseUrl + "/jobs/" + jobId + "/config")); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + + // Give the detail request enough time to reach + // waitLazyFetchArchiveFinished and block on the still-suspended async + // detail task. It must not have completed yet. + Thread.sleep(500); + assertThat(detailFuture.isDone()).isFalse(); + + blockingArchiveStorage.releaseLatch.countDown(); + + detailFuture.get(10, TimeUnit.SECONDS); + assertThat(detailResponse.get().f0).isEqualTo(200); + assertThat(detailResponse.get().f1).contains(jobId.toString()); + assertThat(blockingArchiveStorage.exists("jobs/" + jobId + "/config.json")).isTrue(); + } } diff --git a/flink-runtime-web/src/test/java/org/apache/flink/runtime/webmonitor/history/HistoryServerApplicationArchiveFetcherTest.java b/flink-runtime-web/src/test/java/org/apache/flink/runtime/webmonitor/history/HistoryServerApplicationArchiveFetcherTest.java index 8775be5eb7a..05f5e168389 100644 --- a/flink-runtime-web/src/test/java/org/apache/flink/runtime/webmonitor/history/HistoryServerApplicationArchiveFetcherTest.java +++ b/flink-runtime-web/src/test/java/org/apache/flink/runtime/webmonitor/history/HistoryServerApplicationArchiveFetcherTest.java @@ -194,6 +194,7 @@ class HistoryServerApplicationArchiveFetcherTest { storage, jobMetaInfoCache, applicationMetaInfoCache, + 4, 4); } diff --git a/flink-runtime-web/src/test/java/org/apache/flink/runtime/webmonitor/history/HistoryServerArchiveFetcherTest.java b/flink-runtime-web/src/test/java/org/apache/flink/runtime/webmonitor/history/HistoryServerArchiveFetcherTest.java index ff929837fd9..612d156f244 100644 --- a/flink-runtime-web/src/test/java/org/apache/flink/runtime/webmonitor/history/HistoryServerArchiveFetcherTest.java +++ b/flink-runtime-web/src/test/java/org/apache/flink/runtime/webmonitor/history/HistoryServerArchiveFetcherTest.java @@ -121,6 +121,7 @@ class HistoryServerArchiveFetcherTest { RETAIN_ALL, storage, archiveMetaInfoCache, + 4, 4); } @@ -231,7 +232,8 @@ class HistoryServerArchiveFetcherTest { HistoryServerArchiveFetcher<?> fetcher = createArchiveFetcher(remoteArchiveRootPath, false, archiveStorage); - assertThatThrownBy(() -> fetcher.lazyProcessJobArchive(jobId.toString(), archivePath)) + assertThatThrownBy( + () -> fetcher.lazyProcessJobArchive(jobId.toString(), archivePath, false)) .isInstanceOf(RuntimeException.class) .hasMessage("Archive of job " + jobId + " is empty"); @@ -275,7 +277,8 @@ class HistoryServerArchiveFetcherTest { // try to call lazyProcessJobArchive again, should return DETAIL_PARSING. HistoryServerArchiveFetcher.ArchiveEvent callAgainEvent = - fetcher.lazyProcessJobArchive(jobIdWithDetail.toString(), jobWithDetailArchivePath); + fetcher.lazyProcessJobArchive( + jobIdWithDetail.toString(), jobWithDetailArchivePath, false); assertThat(callAgainEvent.getType()).isEqualTo(DETAIL_PARSING); // Phase 2: execute the asynchronous task @@ -352,4 +355,75 @@ class HistoryServerArchiveFetcherTest { assertThat(archiveStorage.exists("overviews/" + jobId + ".json")).isTrue(); assertThat(archiveStorage.exists("jobs/overview.json")).isTrue(); } + + @TestTemplate + void testScanArchivesWithoutFetch() throws Exception { + JobID jobId = JobID.generate(); + createJobArchive(remoteArchiveRootPath, jobId, true); + + HistoryServerArchiveFetcher<?> fetcher = + createArchiveFetcher(remoteArchiveRootPath, true, archiveStorage); + + fetcher.scanArchives(EAGER, false); + assertThat(archiveEvents).isEmpty(); + assertThat(archiveStorage.exists("overviews/" + jobId + ".json")).isFalse(); + } + + @TestTemplate + void testLazyFetchArchiveProactively() throws Exception { + // with explicit path + JobID jobId = JobID.generate(); + Path archivePath = createJobArchive(remoteArchiveRootPath, jobId, true); + + HistoryServerArchiveFetcher<?> fetcher = + createArchiveFetcher(remoteArchiveRootPath, false, archiveStorage); + + fetcher.lazyFetchArchiveProactively(jobId.toString(), archivePath); + waitForArchiveLoaded(archiveMetaInfoCache, jobId.toString()); + + assertThat(archiveMetaInfoCache.get(jobId.toString()).getEventType()).isEqualTo(CREATED); + assertThat(archiveStorage.exists("overviews/" + jobId + ".json")).isTrue(); + assertThat(archiveStorage.exists("jobs/" + jobId + ".json")).isTrue(); + assertThat(archiveStorage.exists("jobs/" + jobId + "/config.json")).isTrue(); + + // without explicit path + jobId = JobID.generate(); + createJobArchive(remoteArchiveRootPath, jobId, true); + + // call without explicit path; the fetcher should locate the archive in refreshDirs. + fetcher.lazyFetchArchiveProactively(jobId.toString(), null); + waitForArchiveLoaded(archiveMetaInfoCache, jobId.toString()); + + assertThat(archiveMetaInfoCache.get(jobId.toString()).getEventType()).isEqualTo(CREATED); + assertThat(archiveStorage.exists("overviews/" + jobId + ".json")).isTrue(); + assertThat(archiveStorage.exists("jobs/" + jobId + ".json")).isTrue(); + assertThat(archiveStorage.exists("jobs/" + jobId + "/config.json")).isTrue(); + } + + @TestTemplate + void testCleanUpLazyFetchTaskCancelsRunningFuture() throws Exception { + JobID jobId = JobID.generate(); + createJobArchive(remoteArchiveRootPath, jobId, true); + + HistoryServerTestUtils.BlockingArchiveStorage<Object> blockingStorage = + new HistoryServerTestUtils.BlockingArchiveStorage<>( + archiveStorage, "jobs/" + jobId + "/config"); + archiveStorage = blockingStorage; + + HistoryServerArchiveFetcher<?> fetcher = + createArchiveFetcher(remoteArchiveRootPath, false, blockingStorage); + fetcher.fetchArchives(LAZY); + // make sure the async detail task has started + assertThat(blockingStorage.asyncStartLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(fetcher.getCommonFetchTask(jobId.toString())).isNotNull(); + assertThat(fetcher.getCommonFetchTask(jobId.toString())).isNotDone(); + + // cancel the in-flight task + fetcher.cleanUpLazyFetchTask(jobId.toString()); + + assertThat(fetcher.getCommonFetchTask(jobId.toString())).isNull(); + assertThat(archiveStorage.exists("overviews/" + jobId + ".json")).isTrue(); + assertThat(archiveStorage.exists("jobs/" + jobId + ".json")).isTrue(); + assertThat(archiveStorage.exists("jobs/" + jobId + "/config.json")).isFalse(); + } } diff --git a/flink-runtime-web/src/test/java/org/apache/flink/runtime/webmonitor/utils/WebFrontendBootstrapTest.java b/flink-runtime-web/src/test/java/org/apache/flink/runtime/webmonitor/utils/WebFrontendBootstrapTest.java index 968dcf73a7d..6a4bc3f9715 100644 --- a/flink-runtime-web/src/test/java/org/apache/flink/runtime/webmonitor/utils/WebFrontendBootstrapTest.java +++ b/flink-runtime-web/src/test/java/org/apache/flink/runtime/webmonitor/utils/WebFrontendBootstrapTest.java @@ -24,6 +24,7 @@ import org.apache.flink.runtime.io.network.netty.InboundChannelHandlerFactory; import org.apache.flink.runtime.io.network.netty.Prio0InboundChannelHandlerFactory; import org.apache.flink.runtime.io.network.netty.Prio1InboundChannelHandlerFactory; import org.apache.flink.runtime.rest.handler.router.Router; +import org.apache.flink.runtime.webmonitor.history.FileArchiveStorage; import org.apache.flink.runtime.webmonitor.history.HistoryServerStaticFileServerHandler; import org.apache.flink.runtime.webmonitor.testutils.HttpUtils; import org.apache.flink.testutils.junit.extensions.ContextClassLoaderExtension; @@ -34,6 +35,7 @@ import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.slf4j.LoggerFactory; +import java.io.File; import java.nio.file.Files; import java.nio.file.Path; @@ -57,12 +59,16 @@ class WebFrontendBootstrapTest { @Test void testHandlersMustBeLoaded() throws Exception { Path webDir = Files.createDirectories(tmp.resolve("webDir")); + File webDirFile = webDir.toFile(); Configuration configuration = new Configuration(); configuration.set(Prio0InboundChannelHandlerFactory.REDIRECT_FROM_URL, "/nonExisting"); configuration.set(Prio0InboundChannelHandlerFactory.REDIRECT_TO_URL, "/index.html"); Router<?> router = new Router<>() - .addGet("/:*", new HistoryServerStaticFileServerHandler(webDir.toFile())); + .addGet( + "/:*", + new HistoryServerStaticFileServerHandler( + new FileArchiveStorage(webDirFile), webDirFile)); WebFrontendBootstrap webUI = new WebFrontendBootstrap( router,
