From 7ddf68b89ce902a95d73c32ac05730fa4ad8ff5e Mon Sep 17 00:00:00 2001 From: Dmitriy Pavlov Date: Thu, 28 May 2026 10:15:12 +0300 Subject: [PATCH 1/3] Make AI prompt generation asynchronous --- .../build/GetSingleBuildTestFailuresRest.java | 16 +++- .../ci/web/rest/pr/GetPrTestFailures.java | 19 ++++- .../tracked/GetTrackedBranchTestResults.java | 19 ++++- .../src/main/webapp/build.html | 3 +- .../src/main/webapp/js/common-1.7.js | 49 ++++++++++- .../ignite/ci/web/CommonScriptTest.java | 5 +- .../build/SingleBuildResultsService.java | 68 +++++++++++++++ .../tcbot/engine/pr/PrChainsProcessor.java | 72 ++++++++++++++++ .../engine/process/BotProcessMonitor.java | 2 + .../engine/process/BotProcessStatus.java | 4 + .../tracked/TrackedBranchChainsProcessor.java | 82 ++++++++++++++++++- 11 files changed, 325 insertions(+), 14 deletions(-) diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/build/GetSingleBuildTestFailuresRest.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/build/GetSingleBuildTestFailuresRest.java index 64abbf58c..c95238962 100644 --- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/build/GetSingleBuildTestFailuresRest.java +++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/build/GetSingleBuildTestFailuresRest.java @@ -82,11 +82,21 @@ public String getTestFailsAiPrompt( @QueryParam("serverId") String srvCodeOrAlias, @QueryParam("buildId") Integer buildId, @Nullable @QueryParam("maxDetailsChars") Integer maxDetailsChars, + @Nullable @QueryParam("waitForTc") Boolean waitForTc, + @Nullable @QueryParam("background") Boolean background, @Nullable @QueryParam("processId") Long processId) throws ServiceUnauthorizedException { - return CtxListener.getApplicationContext(ctx) - .getInstance(SingleBuildResultsService.class) - .getSingleBuildFailuresAiPrompt(srvCodeOrAlias, buildId, maxDetailsChars, SyncMode.RELOAD_QUEUED, + SingleBuildResultsService processor = CtxListener.getApplicationContext(ctx) + .getInstance(SingleBuildResultsService.class); + + if (Boolean.TRUE.equals(background)) { + return processor.startSingleBuildFailuresAiPromptRefresh(srvCodeOrAlias, buildId, SyncMode.RELOAD_QUEUED, ITcBotUserCreds.get(req), processId); + } + + SyncMode mode = waitForTc == null || waitForTc ? SyncMode.RELOAD_QUEUED : SyncMode.NONE; + + return processor.getSingleBuildFailuresAiPrompt(srvCodeOrAlias, buildId, maxDetailsChars, mode, + ITcBotUserCreds.get(req), processId); } @GET diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/pr/GetPrTestFailures.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/pr/GetPrTestFailures.java index 634e6a727..0ac64c58c 100644 --- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/pr/GetPrTestFailures.java +++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/pr/GetPrTestFailures.java @@ -231,6 +231,7 @@ public DsSummaryUi getPrFailsWithSyncMode( * @param testName Optional full test name filter. * @param promptSuiteId Optional suite id filter. * @param waitForTc Wait for fresh TeamCity context and build log processing. + * @param background Start fresh context refresh in background and return immediately. */ @GET @Path("results/aiPrompt") @@ -246,10 +247,26 @@ public DsSummaryUi getPrFailsWithSyncMode( @Nullable @QueryParam("testName") String testName, @Nullable @QueryParam("promptSuiteId") String promptSuiteId, @Nullable @QueryParam("waitForTc") Boolean waitForTc, + @Nullable @QueryParam("background") Boolean background, @Nullable @QueryParam("processId") Long processId) { final TcBotApplicationContext appCtx = CtxListener.getApplicationContext(ctx); + final PrChainsProcessor processor = appCtx.getInstance(PrChainsProcessor.class); - return appCtx.getInstance(PrChainsProcessor.class).getPrFailuresAiPrompt( + if (Boolean.TRUE.equals(background)) { + return processor.startPrFailuresAiPromptRefresh( + ITcBotUserCreds.get(req), + srvId, + suiteId, + branchForTc, + act, + cnt, + baseBranchForTc, + testName, + promptSuiteId, + processId); + } + + return processor.getPrFailuresAiPrompt( ITcBotUserCreds.get(req), srvId, suiteId, diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/tracked/GetTrackedBranchTestResults.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/tracked/GetTrackedBranchTestResults.java index 571abaa02..bbef5f987 100644 --- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/tracked/GetTrackedBranchTestResults.java +++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/tracked/GetTrackedBranchTestResults.java @@ -98,12 +98,25 @@ public String getTestFailsAiPrompt(@Nullable @QueryParam("branch") String branch @Nullable @QueryParam("testName") String testName, @Nullable @QueryParam("promptSuiteId") String promptSuiteId, @Nullable @QueryParam("waitForTc") Boolean waitForTc, + @Nullable @QueryParam("background") Boolean background, @Nullable @QueryParam("processId") Long processId) { int actualMergeBuilds = (mergeCnt == null || mergeCnt < 1) ? 1 : mergeCnt; + TrackedBranchChainsProcessor processor = CtxListener.getApplicationContext(ctx) + .getInstance(TrackedBranchChainsProcessor.class); - return CtxListener.getApplicationContext(ctx) - .getInstance(TrackedBranchChainsProcessor.class) - .getTrackedBranchFailuresAiPrompt(branchOrNull, + if (Boolean.TRUE.equals(background)) { + return processor.startTrackedBranchFailuresAiPromptRefresh(branchOrNull, + actualMergeBuilds, + ITcBotUserCreds.get(req), + SyncMode.RELOAD_QUEUED, + tagForHistSelected, + SortOption.parseStringValue(sortOption), + testName, + promptSuiteId, + processId); + } + + return processor.getTrackedBranchFailuresAiPrompt(branchOrNull, actualMergeBuilds, ITcBotUserCreds.get(req), SyncMode.RELOAD_QUEUED, diff --git a/ignite-tc-helper-web/src/main/webapp/build.html b/ignite-tc-helper-web/src/main/webapp/build.html index e67f7a9f7..fadd54e50 100644 --- a/ignite-tc-helper-web/src/main/webapp/build.html +++ b/ignite-tc-helper-web/src/main/webapp/build.html @@ -128,7 +128,8 @@ var aiPromptUrl = "rest/build/failures/aiPrompt" + parmsForRest(); $("#divFailures").html(showChainOnServersResults(result) - + " AI prompt" + + " AI prompt" ); } diff --git a/ignite-tc-helper-web/src/main/webapp/js/common-1.7.js b/ignite-tc-helper-web/src/main/webapp/js/common-1.7.js index 9de453ca6..ec7095240 100644 --- a/ignite-tc-helper-web/src/main/webapp/js/common-1.7.js +++ b/ignite-tc-helper-web/src/main/webapp/js/common-1.7.js @@ -115,6 +115,11 @@ function botProcessStatusText(status) { : "Waiting for the bot to publish status."; } +function botProcessFailed(status) { + return status && (status.failed === true || + (isDefinedAndFilled(status.status) && status.status.indexOf("Failed:") === 0)); +} + function startBotProcessPolling(processId, onStatus, options) { if (!isDefinedAndFilled(processId)) return function () {}; @@ -251,8 +256,9 @@ function openAiPrompt(url) { initialMode: true, processKind: "aiPrompt", requestUrl: function (waitForTc, processId) { - return aiPromptUrlWithWaitForTc(url, waitForTc, processId); + return aiPromptUrlWithWaitForTc(url, waitForTc, processId, waitForTc); }, + backgroundInitial: true, timeoutMs: 70000, skip: { isVisible: function (waitForTc) { @@ -317,10 +323,21 @@ function requestTextCommand(options, state, mode, firstStep) { startTextCommandProgress(options, state, mode, firstStep); + let backgroundInitial = options.backgroundInitial === true && mode === true; + state.xhr = $.ajax({ - url: options.requestUrl(mode, state.processId), + url: options.requestUrl(mode, state.processId, backgroundInitial), timeout: options.timeoutMs == null ? 70000 : options.timeoutMs, success: function (result) { + if (backgroundInitial) { + state.xhr = null; + + if (isDefinedAndFilled(result)) + appendTextCommandStep(state, result); + + return; + } + finishTextCommandDialog(options, state, result); }, error: function (jqXHR, status, error) { @@ -499,9 +516,10 @@ function restoreWindowScroll(scrollLeft, scrollTop) { window.scrollTo(scrollLeft, scrollTop); } -function aiPromptUrlWithWaitForTc(url, waitForTc, processId) { +function aiPromptUrlWithWaitForTc(url, waitForTc, processId, background) { return url + (url.indexOf("?") >= 0 ? "&" : "?") + "waitForTc=" + waitForTc + - (isDefinedAndFilled(processId) ? "&processId=" + encodeURIComponent(processId) : ""); + (isDefinedAndFilled(processId) ? "&processId=" + encodeURIComponent(processId) : "") + + (background === true ? "&background=true" : ""); } function startTextCommandProgress(options, state, mode, firstStep) { @@ -520,6 +538,16 @@ function startTextCommandProgress(options, state, mode, firstStep) { state.processPollStop = startBotProcessPolling(state.processId, function (status) { appendTextCommandStep(state, botProcessStatusText(status)); + + if (options.backgroundInitial === true && mode === true && status.running === false) { + if (botProcessFailed(status)) { + failTextCommandDialogMessage(options, state, botProcessStatusText(status)); + + return; + } + + requestTextCommand(options, state, false, "Building prompt from refreshed context."); + } }); return; @@ -612,6 +640,19 @@ function failTextCommandDialog(options, state, jqXHR, status, error) { showErrInLoadStatus(jqXHR, status); } +function failTextCommandDialogMessage(options, state, message) { + if (state.timer) + clearInterval(state.timer); + + if (state.processPollStop) + state.processPollStop(); + + state.status.text(options.failureStatusText || "Command request failed."); + state.skipBtn.hide(); + state.errorBlock.text((options.failureMessagePrefix || "Command request failed: ") + message).show(); + appendTextCommandStep(state, message); +} + function openTextCommandResult(state) { if (!state.resultUrl) return false; diff --git a/ignite-tc-helper-web/src/test/java/org/apache/ignite/ci/web/CommonScriptTest.java b/ignite-tc-helper-web/src/test/java/org/apache/ignite/ci/web/CommonScriptTest.java index 2abdf9048..8fc1cb4a7 100644 --- a/ignite-tc-helper-web/src/test/java/org/apache/ignite/ci/web/CommonScriptTest.java +++ b/ignite-tc-helper-web/src/test/java/org/apache/ignite/ci/web/CommonScriptTest.java @@ -34,8 +34,11 @@ public void aiPromptSkipButtonRequestsCurrentContext() throws IOException { assertTrue(js.contains("buttonText: \"Use current context now\"")); assertTrue(js.contains("nextMode: false")); assertTrue(js.contains("requestTextCommand(options, state, nextMode, skip.stepText)")); - assertTrue(js.contains("return aiPromptUrlWithWaitForTc(url, waitForTc, processId)")); + assertTrue(js.contains("return aiPromptUrlWithWaitForTc(url, waitForTc, processId, waitForTc)")); assertTrue(js.contains("\"waitForTc=\" + waitForTc")); + assertTrue(js.contains("backgroundInitial: true")); + assertTrue(js.contains("\"&background=true\"")); + assertTrue(js.contains("Building prompt from refreshed context.")); } private static Path commonJs() { diff --git a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/build/SingleBuildResultsService.java b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/build/SingleBuildResultsService.java index e88ef120d..364975770 100644 --- a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/build/SingleBuildResultsService.java +++ b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/build/SingleBuildResultsService.java @@ -51,6 +51,9 @@ public class SingleBuildResultsService { /** Max time to wait for fresh AI prompt build context. */ private static final long AI_PROMPT_CONTEXT_WAIT_MS = TimeUnit.MINUTES.toMillis(1); + /** Max time to wait for AI prompt build log processing. */ + private static final long AI_PROMPT_LOG_WAIT_MS = TimeUnit.MINUTES.toMillis(1); + @Inject BuildChainProcessor buildChainProcessor; @Inject ITeamcityIgnitedProvider tcIgnitedProv; @Inject BranchEquivalence branchEquivalence; @@ -92,6 +95,41 @@ public class SingleBuildResultsService { return getSingleBuildFailuresAiPrompt(srvCodeOrAlias, buildId, maxDetailsChars, syncMode, prov, null); } + /** + * Starts background refresh of TeamCity context required by AI prompt generation. + * + * @param processId User-visible process id. + * @return User-visible acceptance message. + */ + @Nonnull public String startSingleBuildFailuresAiPromptRefresh(String srvCodeOrAlias, Integer buildId, + SyncMode syncMode, ICredentialsProv prov, @Nullable Long processId) { + long reqId = aiPromptMonitor.start("singleBuild", null, srvCodeOrAlias, String.valueOf(buildId), null); + + processMonitor.start(processId, "aiPrompt", "Queued AI prompt TeamCity refresh."); + + tcUpdatePool.getService().submit(() -> { + try { + promptStatus(processId, reqId, "Refreshing TeamCity data and build logs for the prompt."); + + FullChainRunCtx ctx = loadSingleBuildContext(srvCodeOrAlias, buildId, null, syncMode, prov, + ProcessLogsMode.ALL); + + waitForAiPromptLogs(reqId, ctx, processId); + + aiPromptMonitor.finish(reqId, "background refresh finished"); + processMonitor.finish(processId, "Fresh TeamCity context is ready."); + } + catch (RuntimeException e) { + aiPromptMonitor.fail(reqId, e); + processMonitor.fail(processId, e); + + throw e; + } + }); + + return "Fresh TeamCity context refresh started in background."; + } + /** * @param processId User-visible process id. */ @@ -168,6 +206,13 @@ private FullChainRunCtx loadSingleBuildContext(String srvCodeOrAlias, Integer bu */ private FullChainRunCtx loadSingleBuildContextBestEffort(long reqId, String srvCodeOrAlias, Integer buildId, SyncMode liveSyncMode, ICredentialsProv prov, @Nullable Long processId) { + if (liveSyncMode == SyncMode.NONE) { + promptStatus(processId, reqId, "Using cached build and test details for the prompt."); + + return loadSingleBuildContext(srvCodeOrAlias, buildId, null, SyncMode.NONE, prov, + ProcessLogsMode.CACHED_ONLY); + } + Future live = null; try { @@ -212,6 +257,29 @@ private void promptStatus(@Nullable Long processId, long reqId, String text) { processMonitor.status(processId, text); } + /** + * @param reqId AI prompt request id. + * @param ctx Chain context. + * @param processId User-visible process id. + */ + private void waitForAiPromptLogs(long reqId, FullChainRunCtx ctx, @Nullable Long processId) { + long started = ctx.logChecksStartedCount(); + + if (started == 0) + return; + + promptStatus(processId, reqId, "Analyzing build logs for the prompt."); + + ctx.awaitLogChecks(AI_PROMPT_LOG_WAIT_MS); + + long pending = ctx.pendingLogChecksCount(); + + if (pending > 0) + promptStatus(processId, reqId, "Build log analysis timed out; using available log context."); + else + promptStatus(processId, reqId, "Build log analysis finished."); + } + /** * @param srvCodeOrAlias Server id or alias. * @param buildId Build id. diff --git a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/pr/PrChainsProcessor.java b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/pr/PrChainsProcessor.java index 54c4de5bd..f8cdbef86 100644 --- a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/pr/PrChainsProcessor.java +++ b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/pr/PrChainsProcessor.java @@ -780,6 +780,78 @@ public String getPrFailuresAiPrompt( maxDetailsChars, testName, promptSuiteId, waitForTc, null); } + /** + * Starts background refresh of TeamCity context required by AI prompt generation. + * + * @param processId User-visible process id. + * @return User-visible acceptance message. + */ + public String startPrFailuresAiPromptRefresh( + ICredentialsProv creds, + String srvCodeOrAlias, + String suiteId, + String branchForTc, + String act, + Integer cnt, + @Nullable String tcBaseBranchParm, + @Nullable String testName, + @Nullable String promptSuiteId, + @Nullable Long processId) { + long reqId = aiPromptMonitor.start("pr", branchForTc, srvCodeOrAlias, suiteId, testName); + + processMonitor.start(processId, "aiPrompt", "Queued AI prompt TeamCity refresh."); + + tcUpdatePool.getService().submit(() -> { + try { + ITeamcityIgnited tcIgnited = tcIgnitedProvider.server(srvCodeOrAlias, creds); + + LatestRebuildMode rebuild; + if (Action.HISTORY.equals(act)) + rebuild = LatestRebuildMode.ALL; + else if (Action.CHAIN.equals(act)) + rebuild = LatestRebuildMode.NONE; + else + rebuild = LatestRebuildMode.LATEST; + + int buildResMergeCnt = rebuild == LatestRebuildMode.ALL ? cnt == null ? 10 : cnt : 1; + + promptStatus(processId, reqId, "Loading build history for the prompt."); + + List hist = tcIgnited.getLastNBuildsFromHistory(suiteId, branchForTc, buildResMergeCnt); + + String baseBranchForTc = Strings.isNullOrEmpty(tcBaseBranchParm) + ? dfltBaseTcBranch(srvCodeOrAlias) + : tcBaseBranchParm; + + promptStatus(processId, reqId, "Refreshing TeamCity data and build logs for the prompt."); + + FullChainRunCtx ctx = buildChainProcessor.loadFullChainContext( + tcIgnited, + hist, + rebuild, + ProcessLogsMode.ALL, + buildResMergeCnt == 1, + baseBranchForTc, + SyncMode.RELOAD_QUEUED, + null, + null); + + waitForAiPromptLogs(reqId, ctx, srvCodeOrAlias + "/" + suiteId, processId); + + aiPromptMonitor.finish(reqId, "background refresh finished"); + processMonitor.finish(processId, "Fresh TeamCity context is ready."); + } + catch (RuntimeException e) { + aiPromptMonitor.fail(reqId, e); + processMonitor.fail(processId, e); + + throw e; + } + }); + + return "Fresh TeamCity context refresh started in background."; + } + /** * @param processId User-visible process id. */ diff --git a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/process/BotProcessMonitor.java b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/process/BotProcessMonitor.java index da788cb3a..98b0d3f39 100644 --- a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/process/BotProcessMonitor.java +++ b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/process/BotProcessMonitor.java @@ -77,6 +77,7 @@ public void finish(@Nullable Long id, @Nullable String result) { synchronized (status) { status.status(Strings.nullToEmpty(result)); status.finished = System.currentTimeMillis(); + status.failed = false; } } @@ -101,6 +102,7 @@ public void fail(@Nullable Long id, @Nullable String error) { synchronized (status) { status.status("Failed: " + Strings.nullToEmpty(error)); status.finished = System.currentTimeMillis(); + status.failed = true; } } diff --git a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/process/BotProcessStatus.java b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/process/BotProcessStatus.java index 25b311078..e26359c3f 100644 --- a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/process/BotProcessStatus.java +++ b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/process/BotProcessStatus.java @@ -40,6 +40,9 @@ public class BotProcessStatus { /** Finished timestamp. */ public long finished; + /** Failure flag. */ + public boolean failed; + /** * @param id Process id. */ @@ -75,6 +78,7 @@ public BotProcessStatus(Long id) { started = src.started; updated = src.updated; finished = src.finished; + failed = src.failed; } /** diff --git a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/tracked/TrackedBranchChainsProcessor.java b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/tracked/TrackedBranchChainsProcessor.java index a63688387..89d28be68 100644 --- a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/tracked/TrackedBranchChainsProcessor.java +++ b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/tracked/TrackedBranchChainsProcessor.java @@ -139,6 +139,84 @@ public class TrackedBranchChainsProcessor implements IDetailedStatusForTrackedBr sortOption, maxDetailsChars, testName, suiteId, waitForTc, null); } + /** + * Starts background refresh of TeamCity context required by AI prompt generation. + * + * @param processId User-visible process id. + * @return User-visible acceptance message. + */ + @Nonnull public String startTrackedBranchFailuresAiPromptRefresh( + @Nullable String branch, + int buildResMergeCnt, + ICredentialsProv creds, + SyncMode syncMode, + @Nullable String tagForHistSelected, + @Nullable SortOption sortOption, + @Nullable String testName, + @Nullable String suiteId, + @Nullable Long processId) { + long reqId = aiPromptMonitor.start("trackedBranch", branch, null, suiteId, testName); + + processMonitor.start(processId, "aiPrompt", "Queued AI prompt TeamCity refresh."); + + tcUpdatePool.getService().submit(() -> { + try { + final String branchNn = isNullOrEmpty(branch) ? ITcServerConfig.DEFAULT_TRACKED_BRANCH_NAME : branch; + final ITrackedBranch tracked = tcBotCfg.getTrackedBranches().getBranchMandatory(branchNn); + + tracked.chainsStream() + .filter(chainTracked -> tcIgnitedProv.hasAccess(chainTracked.serverCode(), creds)) + .forEach(chainTracked -> { + String srvCodeOrAlias = chainTracked.serverCode(); + String branchForTc = chainTracked.tcBranch(); + String baseBranchTc = chainTracked.tcBaseBranch().orElse(branchForTc); + String suiteIdMandatory = chainTracked.tcSuiteId(); + + promptStatus(processId, reqId, "Loading build history for the prompt."); + + ITeamcityIgnited tcIgnited = tcIgnitedProv.server(srvCodeOrAlias, creds); + + Map requireParamVal = new HashMap<>(); + + if (!Strings.isNullOrEmpty(tagForHistSelected)) + requireParamVal.putAll(reverseTagToParametersRequired(tagForHistSelected, srvCodeOrAlias)); + + List chains = tcIgnited.getLastNBuildsFromHistory(suiteIdMandatory, branchForTc, + Math.max(buildResMergeCnt, 1)); + + LatestRebuildMode rebuild = buildResMergeCnt > 1 ? LatestRebuildMode.ALL : + LatestRebuildMode.LATEST; + + promptStatus(processId, reqId, "Refreshing TeamCity data and build logs for the prompt."); + + FullChainRunCtx ctx = chainProc.loadFullChainContext( + tcIgnited, + chains, + rebuild, + ProcessLogsMode.ALL, + buildResMergeCnt == 1, + baseBranchTc, + syncMode, + sortOption, + requireParamVal); + + waitForAiPromptLogs(reqId, ctx, srvCodeOrAlias + "/" + suiteIdMandatory, processId); + }); + + aiPromptMonitor.finish(reqId, "background refresh finished"); + processMonitor.finish(processId, "Fresh TeamCity context is ready."); + } + catch (RuntimeException e) { + aiPromptMonitor.fail(reqId, e); + processMonitor.fail(processId, e); + + throw e; + } + }); + + return "Fresh TeamCity context refresh started in background."; + } + /** * @param processId User-visible process id. */ @@ -183,7 +261,9 @@ public class TrackedBranchChainsProcessor implements IDetailedStatusForTrackedBr List chains = tcIgnited.getLastNBuildsFromHistory(suiteIdMandatory, branchForTc, Math.max(buildResMergeCnt, 1)); - LatestRebuildMode rebuild = buildResMergeCnt > 1 ? LatestRebuildMode.ALL : LatestRebuildMode.LATEST; + LatestRebuildMode rebuild = buildResMergeCnt > 1 + ? LatestRebuildMode.ALL + : LatestRebuildMode.LATEST; promptStatus(processId, reqId, "Collecting build and test details for the prompt."); From bbbd62d4c8e48c0e6ae1336cbbfa440af1676bec Mon Sep 17 00:00:00 2001 From: Dmitriy Pavlov Date: Fri, 29 May 2026 00:33:29 +0300 Subject: [PATCH 2/3] Unify AI prompt progress reporting --- .../build/GetSingleBuildTestFailuresRest.java | 20 +++ .../src/main/webapp/build.html | 5 +- .../src/main/webapp/current.html | 2 +- .../src/main/webapp/js/common-1.7.js | 45 ++++- ignite-tc-helper-web/src/main/webapp/pr.html | 2 +- ignite-tc-helper-web/src/main/webapp/prs.html | 2 +- .../build/SingleBuildResultsService.java | 162 ++++++++++++------ .../engine/chain/BuildChainProcessor.java | 30 +++- .../tcbot/engine/pr/PrChainsProcessor.java | 134 +++++++-------- .../engine/process/ProgressReporter.java | 135 ++++++++++++++- .../tracked/TrackedBranchChainsProcessor.java | 73 ++++---- 11 files changed, 436 insertions(+), 174 deletions(-) diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/build/GetSingleBuildTestFailuresRest.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/build/GetSingleBuildTestFailuresRest.java index c95238962..42c8bb0f7 100644 --- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/build/GetSingleBuildTestFailuresRest.java +++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/build/GetSingleBuildTestFailuresRest.java @@ -99,6 +99,26 @@ public String getTestFailsAiPrompt( ITcBotUserCreds.get(req), processId); } + @GET + @Path("failures/analyzeLogs") + @Produces(MediaType.TEXT_PLAIN) + public String analyzeBuildLogs( + @QueryParam("serverId") String srvCodeOrAlias, + @QueryParam("buildId") Integer buildId, + @Nullable @QueryParam("background") Boolean background, + @Nullable @QueryParam("processId") Long processId) throws ServiceUnauthorizedException { + SingleBuildResultsService processor = CtxListener.getApplicationContext(ctx) + .getInstance(SingleBuildResultsService.class); + + if (Boolean.TRUE.equals(background)) { + return processor.startSingleBuildLogAnalysis(srvCodeOrAlias, buildId, SyncMode.RELOAD_QUEUED, + ITcBotUserCreds.get(req), processId); + } + + return processor.analyzeSingleBuildLogs(srvCodeOrAlias, buildId, SyncMode.RELOAD_QUEUED, + ITcBotUserCreds.get(req), processId); + } + @GET @Path("failuresNoSync") public DsSummaryUi getBuildTestFailsNoSync( diff --git a/ignite-tc-helper-web/src/main/webapp/build.html b/ignite-tc-helper-web/src/main/webapp/build.html index a883a43a6..f1c7f32e0 100644 --- a/ignite-tc-helper-web/src/main/webapp/build.html +++ b/ignite-tc-helper-web/src/main/webapp/build.html @@ -10,7 +10,7 @@ - + @@ -126,10 +126,13 @@ function showData(result) { var aiPromptUrl = "rest/build/failures/aiPrompt" + parmsForRest(); + var logAnalysisUrl = "rest/build/failures/analyzeLogs" + parmsForRest(); $("#divFailures").html(showChainOnServersResults(result) + " AI prompt" + + " | Analyze logs" ); } diff --git a/ignite-tc-helper-web/src/main/webapp/current.html b/ignite-tc-helper-web/src/main/webapp/current.html index 822a61df5..d4f24623d 100644 --- a/ignite-tc-helper-web/src/main/webapp/current.html +++ b/ignite-tc-helper-web/src/main/webapp/current.html @@ -20,7 +20,7 @@ - + diff --git a/ignite-tc-helper-web/src/main/webapp/js/common-1.7.js b/ignite-tc-helper-web/src/main/webapp/js/common-1.7.js index ec7095240..e3988e834 100644 --- a/ignite-tc-helper-web/src/main/webapp/js/common-1.7.js +++ b/ignite-tc-helper-web/src/main/webapp/js/common-1.7.js @@ -283,6 +283,33 @@ function openAiPrompt(url) { }); } +function openBuildLogAnalysis(url) { + openTextCommandDialog({ + dialogId: "buildLogAnalysisDialog", + statusId: "buildLogAnalysisStatus", + logId: "buildLogAnalysisProgressLog", + errorId: "buildLogAnalysisError", + title: "Analyzing build logs", + initialMode: true, + processKind: "buildLogAnalysis", + requestUrl: function (mode, processId, background) { + return commandUrlWithProcess(url, processId, background); + }, + backgroundInitial: true, + backgroundFollowupStep: "Reading cached log analysis result.", + timeoutMs: 70000, + statusText: function () { + return "Analyzing build logs..."; + }, + showOpenButton: false, + showDownloadButton: false, + readyStatusText: "Build log analysis is ready.", + readyStepText: "Build log analysis completed.", + failureStatusText: "Build log analysis failed.", + failureMessagePrefix: "Build log analysis failed: " + }); +} + function openTextCommandDialog(options) { let state = createTextCommandDialog(options); @@ -522,6 +549,21 @@ function aiPromptUrlWithWaitForTc(url, waitForTc, processId, background) { (background === true ? "&background=true" : ""); } +function commandUrlWithProcess(url, processId, background) { + let params = []; + + if (isDefinedAndFilled(processId)) + params.push("processId=" + encodeURIComponent(processId)); + + if (background === true) + params.push("background=true"); + + if (params.length === 0) + return url; + + return url + (url.indexOf("?") >= 0 ? "&" : "?") + params.join("&"); +} + function startTextCommandProgress(options, state, mode, firstStep) { let idx = 0; let startedTs = Date.now(); @@ -546,7 +588,8 @@ function startTextCommandProgress(options, state, mode, firstStep) { return; } - requestTextCommand(options, state, false, "Building prompt from refreshed context."); + requestTextCommand(options, state, false, + options.backgroundFollowupStep || "Building prompt from refreshed context."); } }); diff --git a/ignite-tc-helper-web/src/main/webapp/pr.html b/ignite-tc-helper-web/src/main/webapp/pr.html index 4cd2be7f8..00cdc2114 100644 --- a/ignite-tc-helper-web/src/main/webapp/pr.html +++ b/ignite-tc-helper-web/src/main/webapp/pr.html @@ -12,7 +12,7 @@ - + diff --git a/ignite-tc-helper-web/src/main/webapp/prs.html b/ignite-tc-helper-web/src/main/webapp/prs.html index eeb350365..05ca8b3fa 100644 --- a/ignite-tc-helper-web/src/main/webapp/prs.html +++ b/ignite-tc-helper-web/src/main/webapp/prs.html @@ -17,7 +17,7 @@ - + diff --git a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/build/SingleBuildResultsService.java b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/build/SingleBuildResultsService.java index 364975770..f8368d049 100644 --- a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/build/SingleBuildResultsService.java +++ b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/build/SingleBuildResultsService.java @@ -32,7 +32,7 @@ import org.apache.ignite.tcbot.engine.chain.LatestRebuildMode; import org.apache.ignite.tcbot.engine.chain.ProcessLogsMode; import org.apache.ignite.tcbot.engine.pool.TcUpdatePool; -import org.apache.ignite.tcbot.engine.process.BotProcessMonitor; +import org.apache.ignite.tcbot.engine.process.ProgressReporter; import org.apache.ignite.tcbot.engine.ui.DsChainUi; import org.apache.ignite.tcbot.engine.ui.DsSummaryUi; import org.apache.ignite.tcbot.persistence.IStringCompactor; @@ -61,7 +61,7 @@ public class SingleBuildResultsService { @Inject UpdateCountersStorage updateCounters; @Inject AiPromptRequestMonitor aiPromptMonitor; @Inject TcUpdatePool tcUpdatePool; - @Inject BotProcessMonitor processMonitor; + @Inject ProgressReporter progress; @Nonnull public DsSummaryUi getSingleBuildResults(String srvCodeOrAlias, Integer buildId, @Nullable Boolean checkAllLogs, SyncMode syncMode, ICredentialsProv prov) { @@ -105,65 +105,97 @@ public class SingleBuildResultsService { SyncMode syncMode, ICredentialsProv prov, @Nullable Long processId) { long reqId = aiPromptMonitor.start("singleBuild", null, srvCodeOrAlias, String.valueOf(buildId), null); - processMonitor.start(processId, "aiPrompt", "Queued AI prompt TeamCity refresh."); - - tcUpdatePool.getService().submit(() -> { - try { - promptStatus(processId, reqId, "Refreshing TeamCity data and build logs for the prompt."); + tcUpdatePool.getService().submit(progress.preserveCallable(() -> { + progress.runUnchecked(processId, "aiPrompt", "Queued AI prompt TeamCity refresh.", () -> { + promptStatus(reqId, "Refreshing TeamCity data and build logs for the prompt."); FullChainRunCtx ctx = loadSingleBuildContext(srvCodeOrAlias, buildId, null, syncMode, prov, ProcessLogsMode.ALL); - waitForAiPromptLogs(reqId, ctx, processId); + waitForAiPromptLogs(reqId, ctx); aiPromptMonitor.finish(reqId, "background refresh finished"); - processMonitor.finish(processId, "Fresh TeamCity context is ready."); - } - catch (RuntimeException e) { - aiPromptMonitor.fail(reqId, e); - processMonitor.fail(processId, e); - throw e; - } - }); + return "Fresh TeamCity context is ready."; + }); + + return null; + })); return "Fresh TeamCity context refresh started in background."; } + /** + * Starts background build log analysis. + * + * @param processId User-visible process id. + * @return User-visible acceptance message. + */ + @Nonnull public String startSingleBuildLogAnalysis(String srvCodeOrAlias, Integer buildId, SyncMode syncMode, + ICredentialsProv prov, @Nullable Long processId) { + tcUpdatePool.getService().submit(progress.preserveCallable(() -> { + analyzeSingleBuildLogs(srvCodeOrAlias, buildId, syncMode, prov, processId); + + return null; + })); + + return "Build log analysis started in background."; + } + + /** + * Runs build log analysis and reports process progress. + * + * @param processId User-visible process id. + * @return User-visible result. + */ + @Nonnull public String analyzeSingleBuildLogs(String srvCodeOrAlias, Integer buildId, SyncMode syncMode, + ICredentialsProv prov, @Nullable Long processId) { + return progress.runUnchecked(processId, "buildLogAnalysis", "Preparing build log analysis.", () -> { + progress.report("Collecting build and test details for log analysis."); + + FullChainRunCtx ctx = loadSingleBuildContext(srvCodeOrAlias, buildId, null, syncMode, prov, + ProcessLogsMode.ALL); + + waitForBuildLogs(ctx, "Analyzing build logs.", "Build log analysis timed out; retry later to use cached results.", + "Build log analysis finished."); + + return "Build log analysis finished."; + }); + } + /** * @param processId User-visible process id. */ @Nonnull public String getSingleBuildFailuresAiPrompt(String srvCodeOrAlias, Integer buildId, @Nullable Integer maxDetailsChars, SyncMode syncMode, ICredentialsProv prov, @Nullable Long processId) { long reqId = aiPromptMonitor.start("singleBuild", null, srvCodeOrAlias, String.valueOf(buildId), null); - processMonitor.start(processId, "aiPrompt", "Preparing AI prompt generation."); - try { - promptStatus(processId, reqId, "Collecting build and test details for the prompt."); + return progress.runUnchecked(processId, "aiPrompt", "Preparing AI prompt generation.", + "Prompt text is ready.", () -> { + try { + promptStatus(reqId, "Collecting build and test details for the prompt."); - FullChainRunCtx ctx = loadSingleBuildContextBestEffort(reqId, srvCodeOrAlias, buildId, syncMode, prov, - processId); + FullChainRunCtx ctx = loadSingleBuildContextBestEffort(reqId, srvCodeOrAlias, buildId, syncMode, prov); - ITeamcityIgnited tcIgnited = tcIgnitedProv.server(srvCodeOrAlias, prov); + ITeamcityIgnited tcIgnited = tcIgnitedProv.server(srvCodeOrAlias, prov); - int maxDetails = TestFailuresAiPromptBuilder.restMaxDetailsChars(maxDetailsChars); + int maxDetails = TestFailuresAiPromptBuilder.restMaxDetailsChars(maxDetailsChars); - promptStatus(processId, reqId, "Assembling the final prompt text."); + promptStatus(reqId, "Assembling the final prompt text."); - String res = new TestFailuresAiPromptBuilder(compactor) - .buildPrompt(tcIgnited, ctx, ITeamcity.DEFAULT, maxDetails); + String res = new TestFailuresAiPromptBuilder(compactor) + .buildPrompt(tcIgnited, ctx, ITeamcity.DEFAULT, maxDetails); - aiPromptMonitor.finish(reqId, "chars=" + res.length()); - processMonitor.finish(processId, "Prompt text is ready."); + aiPromptMonitor.finish(reqId, "chars=" + res.length()); - return res; - } - catch (RuntimeException e) { - aiPromptMonitor.fail(reqId, e); - processMonitor.fail(processId, e); + return res; + } + catch (RuntimeException e) { + aiPromptMonitor.fail(reqId, e); - throw e; - } + throw e; + } + }); } /** @@ -205,9 +237,9 @@ private FullChainRunCtx loadSingleBuildContext(String srvCodeOrAlias, Integer bu * @param prov Credentials provider. */ private FullChainRunCtx loadSingleBuildContextBestEffort(long reqId, String srvCodeOrAlias, Integer buildId, - SyncMode liveSyncMode, ICredentialsProv prov, @Nullable Long processId) { + SyncMode liveSyncMode, ICredentialsProv prov) { if (liveSyncMode == SyncMode.NONE) { - promptStatus(processId, reqId, "Using cached build and test details for the prompt."); + promptStatus(reqId, "Using cached build and test details for the prompt."); return loadSingleBuildContext(srvCodeOrAlias, buildId, null, SyncMode.NONE, prov, ProcessLogsMode.CACHED_ONLY); @@ -216,10 +248,10 @@ private FullChainRunCtx loadSingleBuildContextBestEffort(long reqId, String srvC Future live = null; try { - promptStatus(processId, reqId, "Requesting fresh TeamCity data for the prompt."); + promptStatus(reqId, "Requesting fresh TeamCity data for the prompt."); - live = tcUpdatePool.getService().submit(() -> - loadSingleBuildContext(srvCodeOrAlias, buildId, null, liveSyncMode, prov, ProcessLogsMode.ALL)); + live = tcUpdatePool.getService().submit(progress.preserveCallable(() -> + loadSingleBuildContext(srvCodeOrAlias, buildId, null, liveSyncMode, prov, ProcessLogsMode.ALL))); return live.get(AI_PROMPT_CONTEXT_WAIT_MS, TimeUnit.MILLISECONDS); } @@ -227,7 +259,7 @@ private FullChainRunCtx loadSingleBuildContextBestEffort(long reqId, String srvC if (live != null) live.cancel(true); - promptStatus(processId, reqId, "TeamCity refresh timed out; using cached build context."); + promptStatus(reqId, "TeamCity refresh timed out; using cached build context."); } catch (InterruptedException e) { if (live != null) @@ -235,26 +267,24 @@ private FullChainRunCtx loadSingleBuildContextBestEffort(long reqId, String srvC Thread.currentThread().interrupt(); - promptStatus(processId, reqId, "TeamCity refresh was interrupted."); + promptStatus(reqId, "TeamCity refresh was interrupted."); throw new IllegalStateException("Interrupted while loading fresh TeamCity context", e); } catch (Exception e) { - promptStatus(processId, reqId, "TeamCity refresh failed; using cached build context."); + promptStatus(reqId, "TeamCity refresh failed; using cached build context."); } return loadSingleBuildContext(srvCodeOrAlias, buildId, null, SyncMode.NONE, prov, ProcessLogsMode.CACHED_ONLY); } /** - * @param processId User-visible process id. * @param reqId AI prompt monitor request id. - * @param stage Shared process stage. * @param text Detailed AI prompt stage text. */ - private void promptStatus(@Nullable Long processId, long reqId, String text) { + private void promptStatus(long reqId, String text) { aiPromptMonitor.stage(reqId, text); - processMonitor.status(processId, text); + progress.report(text); } /** @@ -262,22 +292,50 @@ private void promptStatus(@Nullable Long processId, long reqId, String text) { * @param ctx Chain context. * @param processId User-visible process id. */ - private void waitForAiPromptLogs(long reqId, FullChainRunCtx ctx, @Nullable Long processId) { + private void waitForAiPromptLogs(long reqId, FullChainRunCtx ctx) { + waitForBuildLogs(ctx, + "Analyzing build logs for the prompt.", + "Build log analysis timed out; using available log context.", + "Build log analysis finished.", + text -> promptStatus(reqId, text)); + } + + /** + * Waits for build log analyses and reports progress through the shared reporter. + */ + private void waitForBuildLogs(FullChainRunCtx ctx, String startStatus, String timeoutStatus, String finishStatus) { + waitForBuildLogs(ctx, startStatus, timeoutStatus, finishStatus, progress::report); + } + + /** + * Waits for build log analyses and reports progress. + */ + private void waitForBuildLogs(FullChainRunCtx ctx, String startStatus, String timeoutStatus, String finishStatus, + StatusReporter reporter) { long started = ctx.logChecksStartedCount(); - if (started == 0) + if (started == 0) { + reporter.report("No build log analysis was requested."); + return; + } - promptStatus(processId, reqId, "Analyzing build logs for the prompt."); + reporter.report(startStatus); ctx.awaitLogChecks(AI_PROMPT_LOG_WAIT_MS); long pending = ctx.pendingLogChecksCount(); if (pending > 0) - promptStatus(processId, reqId, "Build log analysis timed out; using available log context."); + reporter.report(timeoutStatus); else - promptStatus(processId, reqId, "Build log analysis finished."); + reporter.report(finishStatus); + } + + /** Status reporter. */ + private interface StatusReporter { + /** */ + void report(String status); } /** diff --git a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/chain/BuildChainProcessor.java b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/chain/BuildChainProcessor.java index 02a99fb46..926c25a5c 100644 --- a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/chain/BuildChainProcessor.java +++ b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/chain/BuildChainProcessor.java @@ -59,6 +59,7 @@ import org.apache.ignite.tcignited.history.IRunHistory; import org.apache.ignite.tcservice.model.hist.BuildRef; import org.apache.ignite.tcservice.model.result.Build; +import org.apache.ignite.tcbot.engine.process.ProgressReporter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -78,6 +79,9 @@ public class BuildChainProcessor { /** Build logs processor. */ @Inject private IBuildLogProcessor buildLogProcessor; + /** Shared progress reporter. */ + @Inject private ProgressReporter progress; + @Inject private UpdateCountersStorage counters; @@ -166,6 +170,8 @@ public FullChainRunCtx loadFullChainContext( if (entryPoints.isEmpty()) return new FullChainRunCtx(Build.createFakeStub()); + progress.report("Loading TeamCity build context for " + entryPoints.size() + " entry build(s)."); + Integer failRateBranchId = compactor.getStringIdIfPresent(BranchEquivalence.normalizeBranch(failRateBranch)); Map> builds = loadAllBuildsInChains(entryPoints, mode, tcIgn); @@ -518,7 +524,9 @@ else if ((procLog == ProcessLogsMode.SUITE_NOT_COMPLETE && incompleteFailure) || procLog == ProcessLogsMode.ALL) ctx.setLogCheckResFut( CompletableFuture.supplyAsync( - () -> { + progress.preserveSupplier(() -> { + progress.report("Loading build log " + teamcity.serverCode() + "/" + ctx.buildId() + "."); + ILogCheckResult res = buildLogProcessor.analyzeBuildLog(teamcity, ctx.buildId(), incompleteFailure); @@ -527,8 +535,11 @@ else if ((procLog == ProcessLogsMode.SUITE_NOT_COMPLETE && incompleteFailure) //build log result is ready for branch. counters.increment(branchName); + progress.report("Finished build log analysis " + teamcity.serverCode() + "/" + + ctx.buildId() + "."); + return res; - }, + }), tcUpdatePool.getService())); } } @@ -559,10 +570,21 @@ private Set dependencies( } public Future loadBuildAsync(Integer id, SyncMode mode, ITeamcityIgnited teamcityIgnited) { - if (mode == SyncMode.NONE) + if (mode == SyncMode.NONE) { + progress.report("Using cached TeamCity build " + teamcityIgnited.serverCode() + "/" + id + "."); + return Futures.immediateFuture(teamcityIgnited.getFatBuild(id, SyncMode.NONE)); + } + + return tcUpdatePool.getService().submit(progress.preserveCallable(() -> { + progress.report("Loading TeamCity build " + teamcityIgnited.serverCode() + "/" + id + "."); + + FatBuildCompacted build = teamcityIgnited.getFatBuild(id, mode); + + progress.report("Loaded TeamCity build " + teamcityIgnited.serverCode() + "/" + id + "."); - return tcUpdatePool.getService().submit(() -> teamcityIgnited.getFatBuild(id, mode)); + return build; + })); } private List> completed(List builds) { diff --git a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/pr/PrChainsProcessor.java b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/pr/PrChainsProcessor.java index f8cdbef86..b5489cf99 100644 --- a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/pr/PrChainsProcessor.java +++ b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/pr/PrChainsProcessor.java @@ -60,7 +60,7 @@ import org.apache.ignite.tcbot.engine.conf.ITrackedChain; import org.apache.ignite.tcbot.engine.newtests.NewTestsStorage; import org.apache.ignite.tcbot.engine.pool.TcUpdatePool; -import org.apache.ignite.tcbot.engine.process.BotProcessMonitor; +import org.apache.ignite.tcbot.engine.process.ProgressReporter; import org.apache.ignite.tcbot.engine.ui.DsChainUi; import org.apache.ignite.tcbot.engine.ui.DsSuiteUi; import org.apache.ignite.tcbot.engine.ui.DsSummaryUi; @@ -132,8 +132,8 @@ private static class Action { /** TC update pool for best-effort AI prompt refreshes. */ @Inject private TcUpdatePool tcUpdatePool; - /** User-visible process monitor. */ - @Inject private BotProcessMonitor processMonitor; + /** Shared progress reporter. */ + @Inject private ProgressReporter progress; /** * @param creds Credentials. @@ -799,55 +799,57 @@ public String startPrFailuresAiPromptRefresh( @Nullable Long processId) { long reqId = aiPromptMonitor.start("pr", branchForTc, srvCodeOrAlias, suiteId, testName); - processMonitor.start(processId, "aiPrompt", "Queued AI prompt TeamCity refresh."); + tcUpdatePool.getService().submit(progress.preserveCallable(() -> { + progress.runUnchecked(processId, "aiPrompt", "Queued AI prompt TeamCity refresh.", () -> { + try { + ITeamcityIgnited tcIgnited = tcIgnitedProvider.server(srvCodeOrAlias, creds); - tcUpdatePool.getService().submit(() -> { - try { - ITeamcityIgnited tcIgnited = tcIgnitedProvider.server(srvCodeOrAlias, creds); + LatestRebuildMode rebuild; + if (Action.HISTORY.equals(act)) + rebuild = LatestRebuildMode.ALL; + else if (Action.CHAIN.equals(act)) + rebuild = LatestRebuildMode.NONE; + else + rebuild = LatestRebuildMode.LATEST; - LatestRebuildMode rebuild; - if (Action.HISTORY.equals(act)) - rebuild = LatestRebuildMode.ALL; - else if (Action.CHAIN.equals(act)) - rebuild = LatestRebuildMode.NONE; - else - rebuild = LatestRebuildMode.LATEST; + int buildResMergeCnt = rebuild == LatestRebuildMode.ALL ? cnt == null ? 10 : cnt : 1; - int buildResMergeCnt = rebuild == LatestRebuildMode.ALL ? cnt == null ? 10 : cnt : 1; + promptStatus(reqId, "Loading build history for the prompt."); - promptStatus(processId, reqId, "Loading build history for the prompt."); + List hist = tcIgnited.getLastNBuildsFromHistory(suiteId, branchForTc, buildResMergeCnt); - List hist = tcIgnited.getLastNBuildsFromHistory(suiteId, branchForTc, buildResMergeCnt); + String baseBranchForTc = Strings.isNullOrEmpty(tcBaseBranchParm) + ? dfltBaseTcBranch(srvCodeOrAlias) + : tcBaseBranchParm; - String baseBranchForTc = Strings.isNullOrEmpty(tcBaseBranchParm) - ? dfltBaseTcBranch(srvCodeOrAlias) - : tcBaseBranchParm; + promptStatus(reqId, "Refreshing TeamCity data and build logs for the prompt."); - promptStatus(processId, reqId, "Refreshing TeamCity data and build logs for the prompt."); + FullChainRunCtx ctx = buildChainProcessor.loadFullChainContext( + tcIgnited, + hist, + rebuild, + ProcessLogsMode.ALL, + buildResMergeCnt == 1, + baseBranchForTc, + SyncMode.RELOAD_QUEUED, + null, + null); - FullChainRunCtx ctx = buildChainProcessor.loadFullChainContext( - tcIgnited, - hist, - rebuild, - ProcessLogsMode.ALL, - buildResMergeCnt == 1, - baseBranchForTc, - SyncMode.RELOAD_QUEUED, - null, - null); + waitForAiPromptLogs(reqId, ctx, srvCodeOrAlias + "/" + suiteId); - waitForAiPromptLogs(reqId, ctx, srvCodeOrAlias + "/" + suiteId, processId); + aiPromptMonitor.finish(reqId, "background refresh finished"); - aiPromptMonitor.finish(reqId, "background refresh finished"); - processMonitor.finish(processId, "Fresh TeamCity context is ready."); - } - catch (RuntimeException e) { - aiPromptMonitor.fail(reqId, e); - processMonitor.fail(processId, e); + return "Fresh TeamCity context is ready."; + } + catch (RuntimeException e) { + aiPromptMonitor.fail(reqId, e); - throw e; - } - }); + throw e; + } + }); + + return null; + })); return "Fresh TeamCity context refresh started in background."; } @@ -870,9 +872,9 @@ public String getPrFailuresAiPrompt( boolean waitForTc, @Nullable Long processId) { long reqId = aiPromptMonitor.start("pr", branchForTc, srvCodeOrAlias, suiteId, testName); - processMonitor.start(processId, "aiPrompt", "Preparing AI prompt generation."); - - try { + return progress.runUnchecked(processId, "aiPrompt", "Preparing AI prompt generation.", + "Prompt text is ready.", () -> { + try { ITeamcityIgnited tcIgnited = tcIgnitedProvider.server(srvCodeOrAlias, creds); LatestRebuildMode rebuild; @@ -885,7 +887,7 @@ else if (Action.CHAIN.equals(act)) int buildResMergeCnt = rebuild == LatestRebuildMode.ALL ? cnt == null ? 10 : cnt : 1; - promptStatus(processId, reqId, "Loading build history for the prompt."); + promptStatus(reqId, "Loading build history for the prompt."); List hist = tcIgnited.getLastNBuildsFromHistory(suiteId, branchForTc, buildResMergeCnt); @@ -893,31 +895,30 @@ else if (Action.CHAIN.equals(act)) ? dfltBaseTcBranch(srvCodeOrAlias) : tcBaseBranchParm; - promptStatus(processId, reqId, "Collecting build and test details for the prompt."); + promptStatus(reqId, "Collecting build and test details for the prompt."); FullChainRunCtx ctx = loadAiPromptContextBestEffort(reqId, tcIgnited, hist, rebuild, - buildResMergeCnt == 1, baseBranchForTc, srvCodeOrAlias + "/" + suiteId, waitForTc, processId); + buildResMergeCnt == 1, baseBranchForTc, srvCodeOrAlias + "/" + suiteId, waitForTc); if (waitForTc) - waitForAiPromptLogs(reqId, ctx, srvCodeOrAlias + "/" + suiteId, processId); + waitForAiPromptLogs(reqId, ctx, srvCodeOrAlias + "/" + suiteId); - promptStatus(processId, reqId, "Assembling the final prompt text."); + promptStatus(reqId, "Assembling the final prompt text."); String res = new TestFailuresAiPromptBuilder(compactor) .buildPrompt(tcIgnited, ctx, baseBranchForTc, TestFailuresAiPromptBuilder.restMaxDetailsChars(maxDetailsChars), testName, promptSuiteId); aiPromptMonitor.finish(reqId, "chars=" + res.length()); - processMonitor.finish(processId, "Prompt text is ready."); return res; } catch (RuntimeException e) { aiPromptMonitor.fail(reqId, e); - processMonitor.fail(processId, e); throw e; } + }); } /** @@ -938,10 +939,9 @@ private FullChainRunCtx loadAiPromptContextBestEffort( boolean includeScheduledInfo, String baseBranchForTc, String stageSuffix, - boolean waitForTc, - @Nullable Long processId) { + boolean waitForTc) { if (!waitForTc) { - promptStatus(processId, reqId, "Using cached build and test details for the prompt."); + promptStatus(reqId, "Using cached build and test details for the prompt."); return buildChainProcessor.loadFullChainContext( tcIgnited, @@ -957,9 +957,9 @@ private FullChainRunCtx loadAiPromptContextBestEffort( Future live = null; try { - promptStatus(processId, reqId, "Requesting fresh TeamCity data for the prompt."); + promptStatus(reqId, "Requesting fresh TeamCity data for the prompt."); - live = tcUpdatePool.getService().submit(() -> buildChainProcessor.loadFullChainContext( + live = tcUpdatePool.getService().submit(progress.preserveCallable(() -> buildChainProcessor.loadFullChainContext( tcIgnited, hist, rebuild, @@ -967,7 +967,7 @@ private FullChainRunCtx loadAiPromptContextBestEffort( includeScheduledInfo, baseBranchForTc, SyncMode.RELOAD_QUEUED, - null, null)); + null, null))); return live.get(AI_PROMPT_CONTEXT_WAIT_MS, TimeUnit.MILLISECONDS); } @@ -975,7 +975,7 @@ private FullChainRunCtx loadAiPromptContextBestEffort( if (live != null) live.cancel(true); - promptStatus(processId, reqId, "TeamCity refresh timed out; using cached build context."); + promptStatus(reqId, "TeamCity refresh timed out; using cached build context."); } catch (InterruptedException e) { if (live != null) @@ -983,12 +983,12 @@ private FullChainRunCtx loadAiPromptContextBestEffort( Thread.currentThread().interrupt(); - promptStatus(processId, reqId, "TeamCity refresh was interrupted."); + promptStatus(reqId, "TeamCity refresh was interrupted."); throw new IllegalStateException("Interrupted while loading fresh TeamCity context: " + stageSuffix, e); } catch (Exception e) { - promptStatus(processId, reqId, "TeamCity refresh failed; using cached build context."); + promptStatus(reqId, "TeamCity refresh failed; using cached build context."); } return buildChainProcessor.loadFullChainContext( @@ -1007,32 +1007,30 @@ private FullChainRunCtx loadAiPromptContextBestEffort( * @param ctx Chain context. * @param stageSuffix Stage suffix. */ - private void waitForAiPromptLogs(long reqId, FullChainRunCtx ctx, String stageSuffix, @Nullable Long processId) { + private void waitForAiPromptLogs(long reqId, FullChainRunCtx ctx, String stageSuffix) { long started = ctx.logChecksStartedCount(); if (started == 0) return; - promptStatus(processId, reqId, "Analyzing build logs for the prompt."); + promptStatus(reqId, "Analyzing build logs for the prompt."); ctx.awaitLogChecks(AI_PROMPT_LOG_WAIT_MS); long pending = ctx.pendingLogChecksCount(); if (pending > 0) - promptStatus(processId, reqId, "Build log analysis timed out; using available log context."); + promptStatus(reqId, "Build log analysis timed out; using available log context."); else - promptStatus(processId, reqId, "Build log analysis finished."); + promptStatus(reqId, "Build log analysis finished."); } /** - * @param processId User-visible process id. * @param reqId AI prompt monitor request id. - * @param stage Shared process stage. * @param text Detailed AI prompt stage text. */ - private void promptStatus(@Nullable Long processId, long reqId, String text) { + private void promptStatus(long reqId, String text) { aiPromptMonitor.stage(reqId, text); - processMonitor.status(processId, text); + progress.report(text); } } diff --git a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/process/ProgressReporter.java b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/process/ProgressReporter.java index 29e79e35c..976911fe1 100644 --- a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/process/ProgressReporter.java +++ b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/process/ProgressReporter.java @@ -17,6 +17,7 @@ package org.apache.ignite.tcbot.engine.process; import java.util.concurrent.Callable; +import java.util.function.Supplier; import javax.annotation.Nullable; import javax.inject.Inject; import javax.inject.Singleton; @@ -52,24 +53,136 @@ public void report(String status) { * @param action Action. * @return Action result. */ - public String run(@Nullable Long processId, String kind, String acceptedStatus, Callable action) + public T run(@Nullable Long processId, String kind, String acceptedStatus, Callable action) throws Exception { - processMonitor.start(processId, kind, acceptedStatus); + return run(processId, kind, acceptedStatus, null, action); + } - Long prevProcessId = currentProcessId.get(); + /** + * Runs an action with a current user-visible process id. + * + * @param processId Process id supplied by UI. + * @param kind Process kind. + * @param acceptedStatus Initial status. + * @param finishStatus Final status. Null means use action result. + * @param action Action. + * @return Action result. + */ + public T run(@Nullable Long processId, String kind, String acceptedStatus, @Nullable String finishStatus, + Callable action) throws Exception { + processMonitor.start(processId, kind, acceptedStatus); - try { - currentProcessId.set(processId); + return withProcess(processId, () -> { report(acceptedStatus); - String result = action.call(); + T result = action.call(); - processMonitor.finish(processId, result); + processMonitor.finish(processId, finishStatus == null ? String.valueOf(result) : finishStatus); return result; + }, e -> { + processMonitor.fail(processId, e); + }); + } + + /** + * Runs an action with a current user-visible process id and wraps checked exceptions. + * + * @param processId Process id supplied by UI. + * @param kind Process kind. + * @param acceptedStatus Initial status. + * @param action Action. + * @return Action result. + */ + public T runUnchecked(@Nullable Long processId, String kind, String acceptedStatus, Callable action) { + try { + return run(processId, kind, acceptedStatus, action); + } + catch (RuntimeException e) { + throw e; } catch (Exception e) { - processMonitor.fail(processId, e); + throw new RuntimeException(e); + } + } + + /** + * Runs an action with a current user-visible process id and wraps checked exceptions. + * + * @param processId Process id supplied by UI. + * @param kind Process kind. + * @param acceptedStatus Initial status. + * @param finishStatus Final status. Null means use action result. + * @param action Action. + * @return Action result. + */ + public T runUnchecked(@Nullable Long processId, String kind, String acceptedStatus, + @Nullable String finishStatus, Callable action) { + try { + return run(processId, kind, acceptedStatus, finishStatus, action); + } + catch (RuntimeException e) { + throw e; + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Captures current process id and restores it around a callable executed later, usually in a pool thread. + * + * @param action Action. + * @return Callable with process reporting context. + */ + public Callable preserveCallable(Callable action) { + Long processId = currentProcessId.get(); + + return () -> withProcess(processId, action, null); + } + + /** + * Captures current process id and restores it around a supplier executed later, usually in a pool thread. + * + * @param action Action. + * @return Supplier with process reporting context. + */ + public Supplier preserveSupplier(Supplier action) { + Long processId = currentProcessId.get(); + + return () -> { + try { + return withProcess(processId, action::get, null); + } + catch (RuntimeException e) { + throw e; + } + catch (Exception e) { + throw new RuntimeException(e); + } + }; + } + + /** + * Runs an action with a current user-visible process id without starting or finishing the process. + * + * @param processId Process id. + * @param action Action. + * @param onFail Failure callback. + * @return Action result. + */ + private T withProcess(@Nullable Long processId, Callable action, @Nullable FailureCallback onFail) + throws Exception { + Long prevProcessId = currentProcessId.get(); + + try { + currentProcessId.set(processId); + + return action.call(); + } + catch (Exception e) { + if (onFail != null) + onFail.onFail(e); throw e; } @@ -80,4 +193,10 @@ public String run(@Nullable Long processId, String kind, String acceptedStatus, currentProcessId.set(prevProcessId); } } + + /** Failure callback. */ + private interface FailureCallback { + /** */ + void onFail(Exception e); + } } diff --git a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/tracked/TrackedBranchChainsProcessor.java b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/tracked/TrackedBranchChainsProcessor.java index 89d28be68..102dd98e1 100644 --- a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/tracked/TrackedBranchChainsProcessor.java +++ b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/tracked/TrackedBranchChainsProcessor.java @@ -52,7 +52,7 @@ import org.apache.ignite.tcbot.engine.conf.ITrackedBranch; import org.apache.ignite.tcbot.engine.conf.ITrackedChain; import org.apache.ignite.tcbot.engine.pool.TcUpdatePool; -import org.apache.ignite.tcbot.engine.process.BotProcessMonitor; +import org.apache.ignite.tcbot.engine.process.ProgressReporter; import org.apache.ignite.tcbot.engine.ui.DsChainUi; import org.apache.ignite.tcbot.engine.ui.DsSummaryUi; import org.apache.ignite.tcbot.engine.ui.GuardBranchStatusUi; @@ -109,8 +109,8 @@ public class TrackedBranchChainsProcessor implements IDetailedStatusForTrackedBr /** TC update pool for best-effort AI prompt refreshes. */ @Inject private TcUpdatePool tcUpdatePool; - /** User-visible process monitor. */ - @Inject private BotProcessMonitor processMonitor; + /** Shared progress reporter. */ + @Inject private ProgressReporter progress; /** * @param branch Branch. @@ -157,9 +157,8 @@ public class TrackedBranchChainsProcessor implements IDetailedStatusForTrackedBr @Nullable Long processId) { long reqId = aiPromptMonitor.start("trackedBranch", branch, null, suiteId, testName); - processMonitor.start(processId, "aiPrompt", "Queued AI prompt TeamCity refresh."); - - tcUpdatePool.getService().submit(() -> { + tcUpdatePool.getService().submit(progress.preserveCallable(() -> { + progress.runUnchecked(processId, "aiPrompt", "Queued AI prompt TeamCity refresh.", () -> { try { final String branchNn = isNullOrEmpty(branch) ? ITcServerConfig.DEFAULT_TRACKED_BRANCH_NAME : branch; final ITrackedBranch tracked = tcBotCfg.getTrackedBranches().getBranchMandatory(branchNn); @@ -172,7 +171,7 @@ public class TrackedBranchChainsProcessor implements IDetailedStatusForTrackedBr String baseBranchTc = chainTracked.tcBaseBranch().orElse(branchForTc); String suiteIdMandatory = chainTracked.tcSuiteId(); - promptStatus(processId, reqId, "Loading build history for the prompt."); + promptStatus(reqId, "Loading build history for the prompt."); ITeamcityIgnited tcIgnited = tcIgnitedProv.server(srvCodeOrAlias, creds); @@ -187,7 +186,7 @@ public class TrackedBranchChainsProcessor implements IDetailedStatusForTrackedBr LatestRebuildMode rebuild = buildResMergeCnt > 1 ? LatestRebuildMode.ALL : LatestRebuildMode.LATEST; - promptStatus(processId, reqId, "Refreshing TeamCity data and build logs for the prompt."); + promptStatus(reqId, "Refreshing TeamCity data and build logs for the prompt."); FullChainRunCtx ctx = chainProc.loadFullChainContext( tcIgnited, @@ -200,19 +199,22 @@ public class TrackedBranchChainsProcessor implements IDetailedStatusForTrackedBr sortOption, requireParamVal); - waitForAiPromptLogs(reqId, ctx, srvCodeOrAlias + "/" + suiteIdMandatory, processId); + waitForAiPromptLogs(reqId, ctx, srvCodeOrAlias + "/" + suiteIdMandatory); }); aiPromptMonitor.finish(reqId, "background refresh finished"); - processMonitor.finish(processId, "Fresh TeamCity context is ready."); + + return "Fresh TeamCity context is ready."; } catch (RuntimeException e) { aiPromptMonitor.fail(reqId, e); - processMonitor.fail(processId, e); throw e; } - }); + }); + + return null; + })); return "Fresh TeamCity context refresh started in background."; } @@ -233,9 +235,10 @@ public class TrackedBranchChainsProcessor implements IDetailedStatusForTrackedBr boolean waitForTc, @Nullable Long processId) { long reqId = aiPromptMonitor.start("trackedBranch", branch, null, null, testName); - processMonitor.start(processId, "aiPrompt", "Preparing AI prompt generation."); StringBuilder res = new StringBuilder(); + return progress.runUnchecked(processId, "aiPrompt", "Preparing AI prompt generation.", + "Prompt text is ready.", () -> { try { final String branchNn = isNullOrEmpty(branch) ? ITcServerConfig.DEFAULT_TRACKED_BRANCH_NAME : branch; final ITrackedBranch tracked = tcBotCfg.getTrackedBranches().getBranchMandatory(branchNn); @@ -249,7 +252,7 @@ public class TrackedBranchChainsProcessor implements IDetailedStatusForTrackedBr String baseBranchTc = chainTracked.tcBaseBranch().orElse(branchForTc); String suiteIdMandatory = chainTracked.tcSuiteId(); - promptStatus(processId, reqId, "Loading build history for the prompt."); + promptStatus(reqId, "Loading build history for the prompt."); ITeamcityIgnited tcIgnited = tcIgnitedProv.server(srvCodeOrAlias, creds); @@ -265,35 +268,34 @@ public class TrackedBranchChainsProcessor implements IDetailedStatusForTrackedBr ? LatestRebuildMode.ALL : LatestRebuildMode.LATEST; - promptStatus(processId, reqId, "Collecting build and test details for the prompt."); + promptStatus(reqId, "Collecting build and test details for the prompt."); FullChainRunCtx ctx = loadAiPromptContextBestEffort(reqId, tcIgnited, chains, rebuild, buildResMergeCnt == 1, baseBranchTc, syncMode, sortOption, requireParamVal, - srvCodeOrAlias + "/" + suiteIdMandatory, waitForTc, processId); + srvCodeOrAlias + "/" + suiteIdMandatory, waitForTc); if (waitForTc) - waitForAiPromptLogs(reqId, ctx, srvCodeOrAlias + "/" + suiteIdMandatory, processId); + waitForAiPromptLogs(reqId, ctx, srvCodeOrAlias + "/" + suiteIdMandatory); if (res.length() > 0) res.append("\n\n"); - promptStatus(processId, reqId, "Assembling the final prompt text."); + promptStatus(reqId, "Assembling the final prompt text."); res.append(new TestFailuresAiPromptBuilder(compactor) .buildPrompt(tcIgnited, ctx, baseBranchTc, maxDetails, testName, suiteId)); }); aiPromptMonitor.finish(reqId, "chars=" + res.length()); - processMonitor.finish(processId, "Prompt text is ready."); return res.toString(); } catch (RuntimeException e) { aiPromptMonitor.fail(reqId, e); - processMonitor.fail(processId, e); throw e; } + }); } /** @@ -320,10 +322,9 @@ private FullChainRunCtx loadAiPromptContextBestEffort( @Nullable SortOption sortOption, @Nullable Map requireParamVal, String stageSuffix, - boolean waitForTc, - @Nullable Long processId) { + boolean waitForTc) { if (!waitForTc) { - promptStatus(processId, reqId, "Using cached build and test details for the prompt."); + promptStatus(reqId, "Using cached build and test details for the prompt."); return chainProc.loadFullChainContext( tcIgnited, @@ -341,9 +342,9 @@ private FullChainRunCtx loadAiPromptContextBestEffort( Future live = null; try { - promptStatus(processId, reqId, "Requesting fresh TeamCity data for the prompt."); + promptStatus(reqId, "Requesting fresh TeamCity data for the prompt."); - live = tcUpdatePool.getService().submit(() -> chainProc.loadFullChainContext( + live = tcUpdatePool.getService().submit(progress.preserveCallable(() -> chainProc.loadFullChainContext( tcIgnited, chains, rebuild, @@ -353,7 +354,7 @@ private FullChainRunCtx loadAiPromptContextBestEffort( liveSyncMode, sortOption, requireParamVal - )); + ))); return live.get(AI_PROMPT_CONTEXT_WAIT_MS, TimeUnit.MILLISECONDS); } @@ -361,7 +362,7 @@ private FullChainRunCtx loadAiPromptContextBestEffort( if (live != null) live.cancel(true); - promptStatus(processId, reqId, "TeamCity refresh timed out; using cached build context."); + promptStatus(reqId, "TeamCity refresh timed out; using cached build context."); } catch (InterruptedException e) { if (live != null) @@ -369,12 +370,12 @@ private FullChainRunCtx loadAiPromptContextBestEffort( Thread.currentThread().interrupt(); - promptStatus(processId, reqId, "TeamCity refresh was interrupted."); + promptStatus(reqId, "TeamCity refresh was interrupted."); throw new IllegalStateException("Interrupted while loading fresh TeamCity context: " + stageSuffix, e); } catch (Exception e) { - promptStatus(processId, reqId, "TeamCity refresh failed; using cached build context."); + promptStatus(reqId, "TeamCity refresh failed; using cached build context."); } return chainProc.loadFullChainContext( @@ -395,33 +396,31 @@ private FullChainRunCtx loadAiPromptContextBestEffort( * @param ctx Chain context. * @param stageSuffix Stage suffix. */ - private void waitForAiPromptLogs(long reqId, FullChainRunCtx ctx, String stageSuffix, @Nullable Long processId) { + private void waitForAiPromptLogs(long reqId, FullChainRunCtx ctx, String stageSuffix) { long started = ctx.logChecksStartedCount(); if (started == 0) return; - promptStatus(processId, reqId, "Analyzing build logs for the prompt."); + promptStatus(reqId, "Analyzing build logs for the prompt."); ctx.awaitLogChecks(AI_PROMPT_LOG_WAIT_MS); long pending = ctx.pendingLogChecksCount(); if (pending > 0) - promptStatus(processId, reqId, "Build log analysis timed out; using available log context."); + promptStatus(reqId, "Build log analysis timed out; using available log context."); else - promptStatus(processId, reqId, "Build log analysis finished."); + promptStatus(reqId, "Build log analysis finished."); } /** - * @param processId User-visible process id. * @param reqId AI prompt monitor request id. - * @param stage Shared process stage. * @param text Detailed AI prompt stage text. */ - private void promptStatus(@Nullable Long processId, long reqId, String text) { + private void promptStatus(long reqId, String text) { aiPromptMonitor.stage(reqId, text); - processMonitor.status(processId, text); + progress.report(text); } /** {@inheritDoc} */ From c2bffd3c10670402f76119891cb503a7ffc258ca Mon Sep 17 00:00:00 2001 From: Dmitriy Pavlov Date: Fri, 29 May 2026 01:00:58 +0300 Subject: [PATCH 3/3] Fix tracked AI prompt refresh scope --- .../apache/ignite/tcbot/engine/pr/PrChainsProcessor.java | 7 ++++--- .../engine/tracked/TrackedBranchChainsProcessor.java | 8 +++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/pr/PrChainsProcessor.java b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/pr/PrChainsProcessor.java index b5489cf99..4af27616b 100644 --- a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/pr/PrChainsProcessor.java +++ b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/pr/PrChainsProcessor.java @@ -1013,16 +1013,17 @@ private void waitForAiPromptLogs(long reqId, FullChainRunCtx ctx, String stageSu if (started == 0) return; - promptStatus(reqId, "Analyzing build logs for the prompt."); + promptStatus(reqId, "Analyzing build logs for the prompt: " + stageSuffix + "."); ctx.awaitLogChecks(AI_PROMPT_LOG_WAIT_MS); long pending = ctx.pendingLogChecksCount(); if (pending > 0) - promptStatus(reqId, "Build log analysis timed out; using available log context."); + promptStatus(reqId, "Build log analysis timed out for " + stageSuffix + + "; using available log context."); else - promptStatus(reqId, "Build log analysis finished."); + promptStatus(reqId, "Build log analysis finished for " + stageSuffix + "."); } /** diff --git a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/tracked/TrackedBranchChainsProcessor.java b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/tracked/TrackedBranchChainsProcessor.java index 102dd98e1..6b4a8fa9f 100644 --- a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/tracked/TrackedBranchChainsProcessor.java +++ b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/tracked/TrackedBranchChainsProcessor.java @@ -165,6 +165,7 @@ public class TrackedBranchChainsProcessor implements IDetailedStatusForTrackedBr tracked.chainsStream() .filter(chainTracked -> tcIgnitedProv.hasAccess(chainTracked.serverCode(), creds)) + .filter(chainTracked -> Strings.isNullOrEmpty(suiteId) || suiteId.equals(chainTracked.tcSuiteId())) .forEach(chainTracked -> { String srvCodeOrAlias = chainTracked.serverCode(); String branchForTc = chainTracked.tcBranch(); @@ -402,16 +403,17 @@ private void waitForAiPromptLogs(long reqId, FullChainRunCtx ctx, String stageSu if (started == 0) return; - promptStatus(reqId, "Analyzing build logs for the prompt."); + promptStatus(reqId, "Analyzing build logs for the prompt: " + stageSuffix + "."); ctx.awaitLogChecks(AI_PROMPT_LOG_WAIT_MS); long pending = ctx.pendingLogChecksCount(); if (pending > 0) - promptStatus(reqId, "Build log analysis timed out; using available log context."); + promptStatus(reqId, "Build log analysis timed out for " + stageSuffix + + "; using available log context."); else - promptStatus(reqId, "Build log analysis finished."); + promptStatus(reqId, "Build log analysis finished for " + stageSuffix + "."); } /**