From 9facc847c48b7411fbf998c2124f054c9bf1acd6 Mon Sep 17 00:00:00 2001 From: Stephan Krusche Date: Thu, 28 Nov 2024 20:40:37 +0100 Subject: [PATCH 1/8] Development: Reduce payload for live synchronization of build overview (#9888) --- .../buildagent/dto/BuildJobQueueItem.java | 5 ++ .../service/SharedQueueProcessingService.java | 21 ++++-- .../aet/artemis/core/config/MetricsBean.java | 10 +-- .../localci/LocalCIQueueWebsocketService.java | 71 +++++++++++++++++-- .../LocalCIWebsocketMessagingService.java | 1 - .../localci/SharedQueueManagementService.java | 13 +++- 6 files changed, 103 insertions(+), 18 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildJobQueueItem.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildJobQueueItem.java index 7a4220399819..331cb3dd6f77 100644 --- a/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildJobQueueItem.java +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildJobQueueItem.java @@ -48,4 +48,9 @@ public BuildJobQueueItem(BuildJobQueueItem queueItem, ResultDTO submissionResult this(queueItem.id(), queueItem.name(), queueItem.buildAgent(), queueItem.participationId(), queueItem.courseId(), queueItem.exerciseId(), queueItem.retryCount(), queueItem.priority(), queueItem.status(), queueItem.repositoryInfo(), queueItem.jobTimingInfo(), queueItem.buildConfig(), submissionResult); } + + public BuildJobQueueItem(BuildJobQueueItem queueItem, BuildAgentDTO buildAgent, int newRetryCount) { + this(queueItem.id(), queueItem.name(), buildAgent, queueItem.participationId(), queueItem.courseId(), queueItem.exerciseId(), newRetryCount, queueItem.priority(), null, + queueItem.repositoryInfo(), new JobTimingInfo(queueItem.jobTimingInfo.submissionDate(), ZonedDateTime.now(), null), queueItem.buildConfig(), null); + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/SharedQueueProcessingService.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/SharedQueueProcessingService.java index 8b43315ab01b..6bf67b2626e2 100644 --- a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/SharedQueueProcessingService.java +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/SharedQueueProcessingService.java @@ -266,16 +266,26 @@ private void checkAvailabilityAndProcessNextBuild() { processBuild(buildJob); } catch (RejectedExecutionException e) { - log.error("Couldn't add build job to threadpool: {}\n Concurrent Build Jobs Count: {} Active tasks in pool: {}, Concurrent Build Jobs Size: {}", buildJob, + // TODO: we should log this centrally and not on the local node + log.error("Couldn't add build job to thread pool: {}\n Concurrent Build Jobs Count: {} Active tasks in pool: {}, Concurrent Build Jobs Size: {}", buildJob, localProcessingJobs.get(), localCIBuildExecutorService.getActiveCount(), localCIBuildExecutorService.getMaximumPoolSize(), e); // Add the build job back to the queue if (buildJob != null) { processingJobs.remove(buildJob.id()); - buildJob = new BuildJobQueueItem(buildJob, new BuildAgentDTO("", "", "")); - log.info("Adding build job back to the queue: {}", buildJob); - queue.add(buildJob); + // At most try out the build job 5 times when they get rejected + if (buildJob.retryCount() >= 5) { + // TODO: we should log this centrally and not on the local node + log.error("Build job was rejected 5 times. Not adding build job back to the queue: {}", buildJob); + } + else { + // NOTE: we increase the retry count here, because the build job was not processed successfully + // TODO: we should try to run this job on a different build agent to avoid getting the same error again + buildJob = new BuildJobQueueItem(buildJob, new BuildAgentDTO("", "", ""), buildJob.retryCount() + 1); + log.info("Adding build job {} back to the queue with retry count {}", buildJob, buildJob.retryCount()); + queue.add(buildJob); + } localProcessingJobs.decrementAndGet(); } @@ -551,7 +561,8 @@ private void resumeBuildAgent() { private boolean nodeIsAvailable() { log.debug("Currently processing jobs on this node: {}, active threads in Pool: {}, maximum pool size of thread executor : {}", localProcessingJobs.get(), localCIBuildExecutorService.getActiveCount(), localCIBuildExecutorService.getMaximumPoolSize()); - return localProcessingJobs.get() < localCIBuildExecutorService.getMaximumPoolSize(); + return localProcessingJobs.get() < localCIBuildExecutorService.getMaximumPoolSize() + && localCIBuildExecutorService.getActiveCount() < localCIBuildExecutorService.getMaximumPoolSize() && localCIBuildExecutorService.getQueue().isEmpty(); } public class QueuedBuildJobItemListener implements ItemListener { diff --git a/src/main/java/de/tum/cit/aet/artemis/core/config/MetricsBean.java b/src/main/java/de/tum/cit/aet/artemis/core/config/MetricsBean.java index 6d2bff114552..3accd0fdd184 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/config/MetricsBean.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/config/MetricsBean.java @@ -290,21 +290,21 @@ private void registerLocalCIMetrics() { } private static int extractRunningBuilds(Optional sharedQueueManagementService) { - return sharedQueueManagementService.map(queueManagementService -> queueManagementService.getBuildAgentInformation().stream() - .map(buildAgentInformation -> buildAgentInformation.runningBuildJobs().size()).reduce(0, Integer::sum)).orElse(0); + return sharedQueueManagementService.map(SharedQueueManagementService::getProcessingJobsSize).orElse(0); } private static int extractQueuedBuilds(Optional sharedQueueManagementService) { - return sharedQueueManagementService.map(queueManagementService -> queueManagementService.getQueuedJobs().size()).orElse(0); + return sharedQueueManagementService.map(SharedQueueManagementService::getQueuedJobsSize).orElse(0); } private static int extractBuildAgents(Optional sharedQueueManagementService) { - return sharedQueueManagementService.map(queueManagementService -> queueManagementService.getBuildAgentInformation().size()).orElse(0); + return sharedQueueManagementService.map(SharedQueueManagementService::getBuildAgentInformationSize).orElse(0); } private static int extractMaxConcurrentBuilds(Optional sharedQueueManagementService) { return sharedQueueManagementService.map(queueManagementService -> queueManagementService.getBuildAgentInformation().stream() - .map(BuildAgentInformation::maxNumberOfConcurrentBuildJobs).reduce(0, Integer::sum)).orElse(0); + .filter(agent -> agent.status() != BuildAgentInformation.BuildAgentStatus.PAUSED).map(BuildAgentInformation::maxNumberOfConcurrentBuildJobs) + .reduce(0, Integer::sum)).orElse(0); } private void registerWebsocketMetrics() { diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIQueueWebsocketService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIQueueWebsocketService.java index 5a805ff54d03..d25304141c24 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIQueueWebsocketService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIQueueWebsocketService.java @@ -1,5 +1,6 @@ package de.tum.cit.aet.artemis.programming.service.localci; +import java.util.ArrayList; import java.util.List; import jakarta.annotation.PostConstruct; @@ -20,7 +21,9 @@ import com.hazelcast.map.listener.EntryUpdatedListener; import de.tum.cit.aet.artemis.buildagent.dto.BuildAgentInformation; +import de.tum.cit.aet.artemis.buildagent.dto.BuildConfig; import de.tum.cit.aet.artemis.buildagent.dto.BuildJobQueueItem; +import de.tum.cit.aet.artemis.buildagent.dto.RepositoryInfo; /** * This service is responsible for sending build job queue information over websockets. @@ -68,18 +71,21 @@ public void init() { } private void sendQueuedJobsOverWebsocket(long courseId) { - localCIWebsocketMessagingService.sendQueuedBuildJobs(sharedQueueManagementService.getQueuedJobs()); - localCIWebsocketMessagingService.sendQueuedBuildJobsForCourse(courseId, sharedQueueManagementService.getQueuedJobsForCourse(courseId)); + var queuedJobs = removeUnnecessaryInformation(sharedQueueManagementService.getQueuedJobs()); + var queuedJobsForCourse = queuedJobs.stream().filter(job -> job.courseId() == courseId).toList(); + localCIWebsocketMessagingService.sendQueuedBuildJobs(queuedJobs); + localCIWebsocketMessagingService.sendQueuedBuildJobsForCourse(courseId, queuedJobsForCourse); } private void sendProcessingJobsOverWebsocket(long courseId) { - localCIWebsocketMessagingService.sendRunningBuildJobs(sharedQueueManagementService.getProcessingJobs()); - localCIWebsocketMessagingService.sendRunningBuildJobsForCourse(courseId, sharedQueueManagementService.getProcessingJobsForCourse(courseId)); + var processingJobs = removeUnnecessaryInformation(sharedQueueManagementService.getProcessingJobs()); + var processingJobsForCourse = processingJobs.stream().filter(job -> job.courseId() == courseId).toList(); + localCIWebsocketMessagingService.sendRunningBuildJobs(processingJobs); + localCIWebsocketMessagingService.sendRunningBuildJobsForCourse(courseId, processingJobsForCourse); } private void sendBuildAgentSummaryOverWebsocket() { - // remove the recentBuildJobs from the build agent information before sending it over the websocket - List buildAgentSummary = sharedQueueManagementService.getBuildAgentInformationWithoutRecentBuildJobs(); + var buildAgentSummary = removeUnnecessaryInformationFromBuildAgentInformation(sharedQueueManagementService.getBuildAgentInformationWithoutRecentBuildJobs()); localCIWebsocketMessagingService.sendBuildAgentSummary(buildAgentSummary); } @@ -142,4 +148,57 @@ public void entryUpdated(com.hazelcast.core.EntryEvent removeUnnecessaryInformation(List queuedJobs) { + var filteredQueuedJobs = new ArrayList(); // make list mutable in case it is not + for (BuildJobQueueItem job : queuedJobs) { + var buildConfig = removeUnnecessaryInformationFromBuildConfig(job.buildConfig()); + var repositoryInfo = removeUnnecessaryInformationFromRepositoryInfo(job.repositoryInfo()); + filteredQueuedJobs.add(new BuildJobQueueItem(job.id(), job.name(), job.buildAgent(), job.participationId(), job.courseId(), job.exerciseId(), job.retryCount(), + job.priority(), job.status(), repositoryInfo, job.jobTimingInfo(), buildConfig, null)); + + } + return filteredQueuedJobs; + } + + /** + * Removes unnecessary information (e.g. build script, docker image) from the build config before sending it over the websocket. + * + * @param buildConfig the build config + */ + private static BuildConfig removeUnnecessaryInformationFromBuildConfig(BuildConfig buildConfig) { + // We pass "" instead of null strings to avoid errors when serializing to JSON + return new BuildConfig("", "", buildConfig.commitHashToBuild(), "", "", "", null, null, buildConfig.scaEnabled(), buildConfig.sequentialTestRunsEnabled(), + buildConfig.testwiseCoverageEnabled(), null, buildConfig.timeoutSeconds(), "", "", ""); + } + + /** + * Removes unnecessary information (RepositoryUris) from the repository info before sending it over the websocket. + * + * @param repositoryInfo the repository info + */ + private static RepositoryInfo removeUnnecessaryInformationFromRepositoryInfo(RepositoryInfo repositoryInfo) { + // We pass "" instead of null strings to avoid errors when serializing to JSON + return new RepositoryInfo(repositoryInfo.repositoryName(), repositoryInfo.repositoryType(), repositoryInfo.triggeredByPushTo(), "", "", "", null, null); + } + + /** + * Removes unnecessary information (e.g. recent build jobs, public ssh key, result) from the running jobs before sending them over the websocket. + * + * @param buildAgentSummary the build agent summary + */ + private static List removeUnnecessaryInformationFromBuildAgentInformation(List buildAgentSummary) { + var filteredBuildAgentSummary = new ArrayList(); // make list mutable in case it is not + for (BuildAgentInformation agent : buildAgentSummary) { + var runningJobs = removeUnnecessaryInformation(agent.runningBuildJobs()); + filteredBuildAgentSummary.add(new BuildAgentInformation(agent.buildAgent(), agent.maxNumberOfConcurrentBuildJobs(), agent.numberOfCurrentBuildJobs(), runningJobs, + agent.status(), null, null)); + } + return filteredBuildAgentSummary; + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIWebsocketMessagingService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIWebsocketMessagingService.java index e27ec440d5aa..f17ef965750c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIWebsocketMessagingService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIWebsocketMessagingService.java @@ -95,7 +95,6 @@ public void sendRunningBuildJobs(List buildJobQueue) { public void sendBuildAgentSummary(List buildAgentInfo) { String channel = "/topic/admin/build-agents"; log.debug("Sending message on topic {}: {}", channel, buildAgentInfo); - // TODO: convert into a proper DTO and strip unnecessary information, e.g. build config, because it's not shown in the client and contains too much information websocketMessagingService.sendMessage(channel, buildAgentInfo); } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/SharedQueueManagementService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/SharedQueueManagementService.java index 059df76379da..4e29ec53fe73 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/SharedQueueManagementService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/SharedQueueManagementService.java @@ -116,6 +116,10 @@ public List getQueuedJobs() { return new ArrayList<>(queue); } + public int getQueuedJobsSize() { + return queue.size(); + } + /** * @return a copy of the processing jobs as ArrayList */ @@ -124,6 +128,10 @@ public List getProcessingJobs() { return new ArrayList<>(processingJobs.values()); } + public int getProcessingJobsSize() { + return processingJobs.size(); + } + public List getQueuedJobsForCourse(long courseId) { return getQueuedJobs().stream().filter(job -> job.courseId() == courseId).toList(); } @@ -145,6 +153,10 @@ public List getBuildAgentInformation() { return new ArrayList<>(buildAgentInformation.values()); } + public int getBuildAgentInformationSize() { + return buildAgentInformation.size(); + } + public List getBuildAgentInformationWithoutRecentBuildJobs() { return getBuildAgentInformation().stream().map(agent -> new BuildAgentInformation(agent.buildAgent(), agent.maxNumberOfConcurrentBuildJobs(), agent.numberOfCurrentBuildJobs(), agent.runningBuildJobs(), agent.status(), null, null)).toList(); @@ -303,5 +315,4 @@ public Page getFilteredFinishedBuildJobs(FinishedBuildJobPageableSearc return new PageImpl<>(orderedBuildJobs, buildJobIdsPage.getPageable(), buildJobIdsPage.getTotalElements()); } - } From da042a7509eb96cd85eb21a586524206214f6640 Mon Sep 17 00:00:00 2001 From: Mohamed Bilel Besrour <58034472+BBesrour@users.noreply.github.com> Date: Thu, 28 Nov 2024 20:46:32 +0100 Subject: [PATCH 2/8] Integrated code lifecycle: Allow admins to pause all build agents (#9892) --- .../web/admin/AdminBuildJobQueueResource.java | 46 +++++++++++++-- .../localci/SharedQueueManagementService.java | 8 +++ .../build-agent-summary.component.html | 50 ++++++++++++++-- .../build-agent-summary.component.ts | 59 +++++++++++++++++-- .../build-agents/build-agents.service.ts | 26 +++++++- src/main/webapp/i18n/de/buildAgents.json | 9 ++- src/main/webapp/i18n/en/buildAgents.json | 13 ++-- .../icl/LocalCIResourceIntegrationTest.java | 23 +++++++- .../build-agent-summary.component.spec.ts | 44 +++++++++++++- .../build-agents/build-agents.service.spec.ts | 59 +++++++++++++++++-- 10 files changed, 307 insertions(+), 30 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminBuildJobQueueResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminBuildJobQueueResource.java index 27cd50a6e4b9..07c7579751ca 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminBuildJobQueueResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminBuildJobQueueResource.java @@ -195,7 +195,7 @@ public ResponseEntity getBuildJobStatistics(@RequestPara } /** - * {@code PUT /api/admin/agent/{agentName}/pause} : Pause the specified build agent. + * {@code PUT /api/admin/agents/{agentName}/pause} : Pause the specified build agent. * This endpoint allows administrators to pause a specific build agent by its name. * Pausing a build agent will prevent it from picking up any new build jobs until it is resumed. * @@ -207,7 +207,7 @@ public ResponseEntity getBuildJobStatistics(@RequestPara * @return {@link ResponseEntity} with status code 204 (No Content) if the agent was successfully paused * or an appropriate error response if something went wrong */ - @PutMapping("agent/{agentName}/pause") + @PutMapping("agents/{agentName}/pause") public ResponseEntity pauseBuildAgent(@PathVariable String agentName) { log.debug("REST request to pause agent {}", agentName); localCIBuildJobQueueService.pauseBuildAgent(agentName); @@ -215,7 +215,26 @@ public ResponseEntity pauseBuildAgent(@PathVariable String agentName) { } /** - * {@code PUT /api/admin/agent/{agentName}/resume} : Resume the specified build agent. + * {@code PUT /api/admin/agents/pause-all} : Pause all build agents. + * This endpoint allows administrators to pause all build agents. + * Pausing all build agents will prevent them from picking up any new build jobs until they are resumed. + * + *

+ * Authorization: This operation requires admin privileges, enforced by {@code @EnforceAdmin}. + *

+ * + * @return {@link ResponseEntity} with status code 204 (No Content) if all agents were successfully paused + * or an appropriate error response if something went wrong + */ + @PutMapping("agents/pause-all") + public ResponseEntity pauseAllBuildAgents() { + log.debug("REST request to pause all agents"); + localCIBuildJobQueueService.pauseAllBuildAgents(); + return ResponseEntity.noContent().build(); + } + + /** + * {@code PUT /api/admin/agents/{agentName}/resume} : Resume the specified build agent. * This endpoint allows administrators to resume a specific build agent by its name. * Resuming a build agent will allow it to pick up new build jobs again. * @@ -227,10 +246,29 @@ public ResponseEntity pauseBuildAgent(@PathVariable String agentName) { * @return {@link ResponseEntity} with status code 204 (No Content) if the agent was successfully resumed * or an appropriate error response if something went wrong */ - @PutMapping("agent/{agentName}/resume") + @PutMapping("agents/{agentName}/resume") public ResponseEntity resumeBuildAgent(@PathVariable String agentName) { log.debug("REST request to resume agent {}", agentName); localCIBuildJobQueueService.resumeBuildAgent(agentName); return ResponseEntity.noContent().build(); } + + /** + * {@code PUT /api/admin/agents/resume-all} : Resume all build agents. + * This endpoint allows administrators to resume all build agents. + * Resuming all build agents will allow them to pick up new build jobs again. + * + *

+ * Authorization: This operation requires admin privileges, enforced by {@code @EnforceAdmin}. + *

+ * + * @return {@link ResponseEntity} with status code 204 (No Content) if all agents were successfully resumed + * or an appropriate error response if something went wrong + */ + @PutMapping("agents/resume-all") + public ResponseEntity resumeAllBuildAgents() { + log.debug("REST request to resume all agents"); + localCIBuildJobQueueService.resumeAllBuildAgents(); + return ResponseEntity.noContent().build(); + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/SharedQueueManagementService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/SharedQueueManagementService.java index 4e29ec53fe73..f03325c503d8 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/SharedQueueManagementService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/SharedQueueManagementService.java @@ -166,10 +166,18 @@ public void pauseBuildAgent(String agent) { pauseBuildAgentTopic.publish(agent); } + public void pauseAllBuildAgents() { + getBuildAgentInformation().forEach(agent -> pauseBuildAgent(agent.buildAgent().name())); + } + public void resumeBuildAgent(String agent) { resumeBuildAgentTopic.publish(agent); } + public void resumeAllBuildAgents() { + getBuildAgentInformation().forEach(agent -> resumeBuildAgent(agent.buildAgent().name())); + } + /** * Cancel a build job by removing it from the queue or stopping the build process. * diff --git a/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.html b/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.html index 7021b3f79c4d..9f75be4268e7 100644 --- a/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.html +++ b/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.html @@ -1,11 +1,24 @@
-

@if (buildAgents) { -

- {{ buildAgents.length }} : {{ currentBuilds }} - {{ buildCapacity }} -

-
+
+
+

+

+ {{ buildAgents.length }} : {{ currentBuilds }} + {{ buildCapacity }} +

+
+
+ + +
+
}
+ + + + + + + diff --git a/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.ts b/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.ts index 711fa4a40dce..5e79ceac3c75 100644 --- a/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.ts +++ b/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.ts @@ -1,12 +1,14 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; -import { BuildAgentInformation } from 'app/entities/programming/build-agent-information.model'; +import { BuildAgentInformation, BuildAgentStatus } from 'app/entities/programming/build-agent-information.model'; import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; import { BuildAgentsService } from 'app/localci/build-agents/build-agents.service'; import { Subscription } from 'rxjs'; -import { faTimes } from '@fortawesome/free-solid-svg-icons'; +import { faPause, faPlay, faTimes } from '@fortawesome/free-solid-svg-icons'; import { BuildQueueService } from 'app/localci/build-queue/build-queue.service'; import { Router } from '@angular/router'; import { BuildAgent } from 'app/entities/programming/build-agent.model'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { AlertService, AlertType } from 'app/core/util/alert.service'; @Component({ selector: 'jhi-build-agents', @@ -23,13 +25,17 @@ export class BuildAgentSummaryComponent implements OnInit, OnDestroy { routerLink: string; //icons - faTimes = faTimes; + protected readonly faTimes = faTimes; + protected readonly faPause = faPause; + protected readonly faPlay = faPlay; constructor( private websocketService: JhiWebsocketService, private buildAgentsService: BuildAgentsService, private buildQueueService: BuildQueueService, private router: Router, + private modalService: NgbModal, + private alertService: AlertService, ) {} ngOnInit() { @@ -59,7 +65,9 @@ export class BuildAgentSummaryComponent implements OnInit, OnDestroy { private updateBuildAgents(buildAgents: BuildAgentInformation[]) { this.buildAgents = buildAgents; - this.buildCapacity = this.buildAgents.reduce((sum, agent) => sum + (agent.maxNumberOfConcurrentBuildJobs || 0), 0); + this.buildCapacity = this.buildAgents + .filter((agent) => agent.status !== BuildAgentStatus.PAUSED) + .reduce((sum, agent) => sum + (agent.maxNumberOfConcurrentBuildJobs || 0), 0); this.currentBuilds = this.buildAgents.reduce((sum, agent) => sum + (agent.numberOfCurrentBuildJobs || 0), 0); } @@ -86,4 +94,47 @@ export class BuildAgentSummaryComponent implements OnInit, OnDestroy { this.buildQueueService.cancelAllRunningBuildJobsForAgent(buildAgentToCancel.buildAgent?.name).subscribe(); } } + + displayPauseBuildAgentModal(modal: any) { + this.modalService.open(modal); + } + + pauseAllBuildAgents(modal?: any) { + this.buildAgentsService.pauseAllBuildAgents().subscribe({ + next: () => { + this.load(); + this.alertService.addAlert({ + type: AlertType.SUCCESS, + message: 'artemisApp.buildAgents.alerts.buildAgentsPaused', + }); + }, + error: () => { + this.alertService.addAlert({ + type: AlertType.DANGER, + message: 'artemisApp.buildAgents.alerts.buildAgentPauseFailed', + }); + }, + }); + if (modal) { + modal.close(); + } + } + + resumeAllBuildAgents() { + this.buildAgentsService.resumeAllBuildAgents().subscribe({ + next: () => { + this.load(); + this.alertService.addAlert({ + type: AlertType.SUCCESS, + message: 'artemisApp.buildAgents.alerts.buildAgentsResumed', + }); + }, + error: () => { + this.alertService.addAlert({ + type: AlertType.DANGER, + message: 'artemisApp.buildAgents.alerts.buildAgentResumeFailed', + }); + }, + }); + } } diff --git a/src/main/webapp/app/localci/build-agents/build-agents.service.ts b/src/main/webapp/app/localci/build-agents/build-agents.service.ts index 6aac582940d3..c3b388586bcf 100644 --- a/src/main/webapp/app/localci/build-agents/build-agents.service.ts +++ b/src/main/webapp/app/localci/build-agents/build-agents.service.ts @@ -33,22 +33,44 @@ export class BuildAgentsService { */ pauseBuildAgent(agentName: string): Observable { const encodedAgentName = encodeURIComponent(agentName); - return this.http.put(`${this.adminResourceUrl}/agent/${encodedAgentName}/pause`, null).pipe( + return this.http.put(`${this.adminResourceUrl}/agents/${encodedAgentName}/pause`, null).pipe( catchError((err) => { return throwError(() => new Error(`Failed to pause build agent ${agentName}\n${err.message}`)); }), ); } + /** + * Pause All Build Agents + */ + pauseAllBuildAgents(): Observable { + return this.http.put(`${this.adminResourceUrl}/agents/pause-all`, null).pipe( + catchError((err) => { + return throwError(() => new Error(`Failed to pause build agents\n${err.message}`)); + }), + ); + } + /** * Resume Build Agent */ resumeBuildAgent(agentName: string): Observable { const encodedAgentName = encodeURIComponent(agentName); - return this.http.put(`${this.adminResourceUrl}/agent/${encodedAgentName}/resume`, null).pipe( + return this.http.put(`${this.adminResourceUrl}/agents/${encodedAgentName}/resume`, null).pipe( catchError((err) => { return throwError(() => new Error(`Failed to resume build agent ${agentName}\n${err.message}`)); }), ); } + + /** + * Resume all Build Agents + */ + resumeAllBuildAgents(): Observable { + return this.http.put(`${this.adminResourceUrl}/agents/resume-all`, null).pipe( + catchError((err) => { + return throwError(() => new Error(`Failed to resume build agents\n${err.message}`)); + }), + ); + } } diff --git a/src/main/webapp/i18n/de/buildAgents.json b/src/main/webapp/i18n/de/buildAgents.json index 186bd1432540..bad9a78469d3 100644 --- a/src/main/webapp/i18n/de/buildAgents.json +++ b/src/main/webapp/i18n/de/buildAgents.json @@ -22,10 +22,15 @@ "buildAgentResumed": "Anfrage zum Fortsetzen des BuildJobs erfolgreich gesendet. Der Agent wird wieder neue BuildJobs annehmen.", "buildAgentPauseFailed": "Anhalten des Build-Agenten fehlgeschlagen.", "buildAgentResumeFailed": "Fortsetzen des Build-Agenten fehlgeschlagen.", - "buildAgentWithoutName": "Der Name des Build-Agenten ist erforderlich." + "buildAgentWithoutName": "Der Name des Build-Agenten ist erforderlich.", + "buildAgentsPaused": "Anfrage zum Anhalten aller Build-Agenten erfolgreich gesendet. Die Agenten akzeptieren keine neuen Build-Jobs und werden die aktuellen Build-Jobs entweder ordnungsgemäß beenden oder nach einer konfigurierbaren Anzahl von Sekunden abbrechen.", + "buildAgentsResumed": "Anfrage zum Fortsetzen aller Build-Agenten erfolgreich gesendet. Die Agenten werden wieder neue Build-Jobs annehmen." }, "pause": "Anhalten", - "resume": "Fortsetzen" + "resume": "Fortsetzen", + "pauseAll": "Alle Agenten Anhalten", + "resumeAll": "Alle Agenten fortsetzen", + "pauseAllWarning": "Du bist dabei, alle Build-Agenten anzuhalten. Dies wird verhindern, dass sie neue Build-Jobs verarbeiten.\nBist du sicher, dass du fortfahren möchtest?" } } } diff --git a/src/main/webapp/i18n/en/buildAgents.json b/src/main/webapp/i18n/en/buildAgents.json index 974086655128..ce0cf7620104 100644 --- a/src/main/webapp/i18n/en/buildAgents.json +++ b/src/main/webapp/i18n/en/buildAgents.json @@ -20,12 +20,17 @@ "alerts": { "buildAgentPaused": "Build agent pause request sent successfully. The agent will not accept new build jobs and will gracefully finish the current build jobs or will cancel them after a configurable seconds.", "buildAgentResumed": "Build agent resume request sent successfully. The agent will start accepting new build jobs.", - "buildAgentPauseFailed": "Failed to pause the build agent.", - "buildAgentResumeFailed": "Failed to resume the build agent.", - "buildAgentWithoutName": "Build agent name is required." + "buildAgentPauseFailed": "Failed to pause build agent.", + "buildAgentResumeFailed": "Failed to resume build agent.", + "buildAgentWithoutName": "Build agent name is required.", + "buildAgentsPaused": "Pause request sent to all build agents. The agents will not accept new build jobs and will gracefully finish the current build jobs or will cancel them after a configurable seconds.", + "buildAgentsResumed": "Resume request sent to all build agents. The agents will start accepting new build jobs." }, "pause": "Pause", - "resume": "Resume" + "resume": "Resume", + "pauseAll": "Pause All Agents", + "resumeAll": "Resume All Agents", + "pauseAllWarning": "You are about to pause all build agents. This will prevent them from processing any new build jobs.\nAre you sure you want to proceed?" } } } diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIResourceIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIResourceIntegrationTest.java index 28a919289e61..9d02a9f4dcaf 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIResourceIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIResourceIntegrationTest.java @@ -364,10 +364,29 @@ void testPauseBuildAgent() throws Exception { // We need to clear the processing jobs to avoid the agent being set to ACTIVE again processingJobs.clear(); - request.put("/api/admin/agent/" + URLEncoder.encode(agent1.buildAgent().name(), StandardCharsets.UTF_8) + "/pause", null, HttpStatus.NO_CONTENT); + request.put("/api/admin/agents/" + URLEncoder.encode(agent1.buildAgent().name(), StandardCharsets.UTF_8) + "/pause", null, HttpStatus.NO_CONTENT); await().until(() -> buildAgentInformation.get(agent1.buildAgent().memberAddress()).status() == BuildAgentInformation.BuildAgentStatus.PAUSED); - request.put("/api/admin/agent/" + URLEncoder.encode(agent1.buildAgent().name(), StandardCharsets.UTF_8) + "/resume", null, HttpStatus.NO_CONTENT); + request.put("/api/admin/agents/" + URLEncoder.encode(agent1.buildAgent().name(), StandardCharsets.UTF_8) + "/resume", null, HttpStatus.NO_CONTENT); await().until(() -> buildAgentInformation.get(agent1.buildAgent().memberAddress()).status() == BuildAgentInformation.BuildAgentStatus.IDLE); } + + @Test + @WithMockUser(username = TEST_PREFIX + "admin", roles = "ADMIN") + void testPauseAllBuildAgents() throws Exception { + // We need to clear the processing jobs to avoid the agent being set to ACTIVE again + processingJobs.clear(); + + request.put("/api/admin/agents/pause-all", null, HttpStatus.NO_CONTENT); + await().until(() -> { + var agents = buildAgentInformation.values(); + return agents.stream().allMatch(agent -> agent.status() == BuildAgentInformation.BuildAgentStatus.PAUSED); + }); + + request.put("/api/admin/agents/resume-all", null, HttpStatus.NO_CONTENT); + await().until(() -> { + var agents = buildAgentInformation.values(); + return agents.stream().allMatch(agent -> agent.status() == BuildAgentInformation.BuildAgentStatus.IDLE); + }); + } } diff --git a/src/test/javascript/spec/component/localci/build-agents/build-agent-summary.component.spec.ts b/src/test/javascript/spec/component/localci/build-agents/build-agent-summary.component.spec.ts index fe898f69474a..f71ad2c8d920 100644 --- a/src/test/javascript/spec/component/localci/build-agents/build-agent-summary.component.spec.ts +++ b/src/test/javascript/spec/component/localci/build-agents/build-agent-summary.component.spec.ts @@ -2,18 +2,19 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { BuildAgentSummaryComponent } from 'app/localci/build-agents/build-agent-summary/build-agent-summary.component'; import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; import { BuildAgentsService } from 'app/localci/build-agents/build-agents.service'; -import { of } from 'rxjs'; +import { of, throwError } from 'rxjs'; import { BuildJob } from 'app/entities/programming/build-job.model'; import dayjs from 'dayjs/esm'; import { ArtemisTestModule } from '../../../test.module'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { DataTableComponent } from 'app/shared/data-table/data-table.component'; -import { MockComponent, MockPipe } from 'ng-mocks'; +import { MockComponent, MockPipe, MockProvider } from 'ng-mocks'; import { NgxDatatableModule } from '@siemens/ngx-datatable'; import { BuildAgentInformation, BuildAgentStatus } from '../../../../../../main/webapp/app/entities/programming/build-agent-information.model'; import { RepositoryInfo, TriggeredByPushTo } from 'app/entities/programming/repository-info.model'; import { JobTimingInfo } from 'app/entities/job-timing-info.model'; import { BuildConfig } from 'app/entities/programming/build-config.model'; +import { AlertService, AlertType } from '../../../../../../main/webapp/app/core/util/alert.service'; describe('BuildAgentSummaryComponent', () => { let component: BuildAgentSummaryComponent; @@ -27,6 +28,8 @@ describe('BuildAgentSummaryComponent', () => { const mockBuildAgentsService = { getBuildAgentSummary: jest.fn().mockReturnValue(of([])), + pauseAllBuildAgents: jest.fn().mockReturnValue(of({})), + resumeAllBuildAgents: jest.fn().mockReturnValue(of({})), }; const repositoryInfo: RepositoryInfo = { @@ -135,6 +138,8 @@ describe('BuildAgentSummaryComponent', () => { status: BuildAgentStatus.ACTIVE, }, ]; + let alertService: AlertService; + let alertServiceAddAlertStub: jest.SpyInstance; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -144,11 +149,14 @@ describe('BuildAgentSummaryComponent', () => { { provide: JhiWebsocketService, useValue: mockWebsocketService }, { provide: BuildAgentsService, useValue: mockBuildAgentsService }, { provide: DataTableComponent, useClass: DataTableComponent }, + MockProvider(AlertService), ], }).compileComponents(); fixture = TestBed.createComponent(BuildAgentSummaryComponent); component = fixture.componentInstance; + alertService = TestBed.inject(AlertService); + alertServiceAddAlertStub = jest.spyOn(alertService, 'addAlert'); })); beforeEach(() => { @@ -218,4 +226,36 @@ describe('BuildAgentSummaryComponent', () => { expect(component.buildCapacity).toBe(0); expect(component.currentBuilds).toBe(0); }); + + it('should call correct service method when pausing and resuming build agents', () => { + component.pauseAllBuildAgents(); + expect(alertServiceAddAlertStub).toHaveBeenCalledWith({ + type: AlertType.SUCCESS, + message: 'artemisApp.buildAgents.alerts.buildAgentsPaused', + }); + + component.resumeAllBuildAgents(); + expect(alertServiceAddAlertStub).toHaveBeenCalledWith({ + type: AlertType.SUCCESS, + message: 'artemisApp.buildAgents.alerts.buildAgentsResumed', + }); + }); + + it('should show alert when error in pausing or resuming build agents', () => { + mockBuildAgentsService.pauseAllBuildAgents.mockReturnValue(throwError(() => new Error())); + + component.pauseAllBuildAgents(); + expect(alertServiceAddAlertStub).toHaveBeenCalledWith({ + type: AlertType.DANGER, + message: 'artemisApp.buildAgents.alerts.buildAgentPauseFailed', + }); + + mockBuildAgentsService.resumeAllBuildAgents.mockReturnValue(throwError(() => new Error())); + + component.resumeAllBuildAgents(); + expect(alertServiceAddAlertStub).toHaveBeenCalledWith({ + type: AlertType.DANGER, + message: 'artemisApp.buildAgents.alerts.buildAgentResumeFailed', + }); + }); }); diff --git a/src/test/javascript/spec/component/localci/build-agents/build-agents.service.spec.ts b/src/test/javascript/spec/component/localci/build-agents/build-agents.service.spec.ts index 261acf610ff2..18b9f0135b83 100644 --- a/src/test/javascript/spec/component/localci/build-agents/build-agents.service.spec.ts +++ b/src/test/javascript/spec/component/localci/build-agents/build-agents.service.spec.ts @@ -134,7 +134,7 @@ describe('BuildAgentsService', () => { it('should pause build agent', () => { service.pauseBuildAgent('buildAgent1').subscribe(); - const req = httpMock.expectOne(`${service.adminResourceUrl}/agent/buildAgent1/pause`); + const req = httpMock.expectOne(`${service.adminResourceUrl}/agents/buildAgent1/pause`); expect(req.request.method).toBe('PUT'); req.flush({}); }); @@ -142,7 +142,23 @@ describe('BuildAgentsService', () => { it('should resume build agent', () => { service.resumeBuildAgent('buildAgent1').subscribe(); - const req = httpMock.expectOne(`${service.adminResourceUrl}/agent/buildAgent1/resume`); + const req = httpMock.expectOne(`${service.adminResourceUrl}/agents/buildAgent1/resume`); + expect(req.request.method).toBe('PUT'); + req.flush({}); + }); + + it('should pause all build agents', () => { + service.pauseAllBuildAgents().subscribe(); + + const req = httpMock.expectOne(`${service.adminResourceUrl}/agents/pause-all`); + expect(req.request.method).toBe('PUT'); + req.flush({}); + }); + + it('should resume all build agents', () => { + service.resumeAllBuildAgents().subscribe(); + + const req = httpMock.expectOne(`${service.adminResourceUrl}/agents/resume-all`); expect(req.request.method).toBe('PUT'); req.flush({}); }); @@ -152,7 +168,7 @@ describe('BuildAgentsService', () => { const observable = lastValueFrom(service.pauseBuildAgent('buildAgent1')); - const req = httpMock.expectOne(`${service.adminResourceUrl}/agent/buildAgent1/pause`); + const req = httpMock.expectOne(`${service.adminResourceUrl}/agents/buildAgent1/pause`); expect(req.request.method).toBe('PUT'); req.flush({ message: errorMessage }, { status: 500, statusText: 'Internal Server Error' }); @@ -170,7 +186,42 @@ describe('BuildAgentsService', () => { const observable = lastValueFrom(service.resumeBuildAgent('buildAgent1')); // Set up the expected HTTP request and flush the response with an error. - const req = httpMock.expectOne(`${service.adminResourceUrl}/agent/buildAgent1/resume`); + const req = httpMock.expectOne(`${service.adminResourceUrl}/agents/buildAgent1/resume`); + expect(req.request.method).toBe('PUT'); + req.flush({ message: errorMessage }, { status: 500, statusText: 'Internal Server Error' }); + + try { + await observable; + throw new Error('expected an error, but got a success'); + } catch (error) { + expect(error.message).toContain(errorMessage); + } + }); + + it('should handle pause all build agents error', async () => { + const errorMessage = 'Failed to pause build agents'; + + const observable = lastValueFrom(service.pauseAllBuildAgents()); + + const req = httpMock.expectOne(`${service.adminResourceUrl}/agents/pause-all`); + expect(req.request.method).toBe('PUT'); + req.flush({ message: errorMessage }, { status: 500, statusText: 'Internal Server Error' }); + + try { + await observable; + throw new Error('expected an error, but got a success'); + } catch (error) { + expect(error.message).toContain(errorMessage); + } + }); + + it('should handle resume all build agents error', async () => { + const errorMessage = 'Failed to resume build agents'; + + const observable = lastValueFrom(service.resumeAllBuildAgents()); + + // Set up the expected HTTP request and flush the response with an error. + const req = httpMock.expectOne(`${service.adminResourceUrl}/agents/resume-all`); expect(req.request.method).toBe('PUT'); req.flush({ message: errorMessage }, { status: 500, statusText: 'Internal Server Error' }); From d0d6616cc87cd6086cfec9c71e0f84483835e6e2 Mon Sep 17 00:00:00 2001 From: Florian Glombik <63976129+florian-glombik@users.noreply.github.com> Date: Thu, 28 Nov 2024 21:03:58 +0100 Subject: [PATCH 3/8] Development: Update pull request template test coverage section (#9870) --- .github/PULL_REQUEST_TEMPLATE.md | 8 ++++---- supporting_scripts/generate_code_cov_table/README.md | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 2296ed26a829..22968f1531b4 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -111,10 +111,10 @@ Prerequisites: - [ ] Test 2 ### Test Coverage - - - - + + + +