Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrated code lifecycle: Add build agent name #9529

Merged
merged 25 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/admin/setup/distributed.rst
Original file line number Diff line number Diff line change
Expand Up @@ -617,8 +617,13 @@ These credentials are used to clone repositories via HTTPS. You must also add th
container-cleanup:
expiry-minutes: 5 # Time after a hanging container will automatically be removed
cleanup-schedule-minutes: 60 # Schedule for container cleanup
build-agent:
short-name: "artemis-build-agent-X" # Short name of the build agent. This should be unique for each build agent. Only lowercase letters, numbers and hyphens are allowed.
BBesrour marked this conversation as resolved.
Show resolved Hide resolved
BBesrour marked this conversation as resolved.
Show resolved Hide resolved
display-name: "Artemis Build Agent X" # This value is optional. If omitted, the short name will be used as display name. Display name of the build agent. This is shown in the Artemis UI.


Please note that ``artemis.continuous-integration.build-agent.short-name`` must be provided. Otherwise, the build agent will not start.

Build agents run as `Hazelcast Lite Members <https://docs.hazelcast.com/hazelcast/5.3/maintain-cluster/lite-members>`__ and require a full member, in our case a core node, to be running.
Thus, before starting a build agent make sure that at least the primary node is running. You can then add and remove build agents to the cluster as desired.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package de.tum.cit.aet.artemis.buildagent.dto;

import java.io.Serial;
import java.io.Serializable;

public record BuildAgentDTO(String name, String memberAddress, String displayName) implements Serializable {

@Serial
private static final long serialVersionUID = 1L;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
// in the future are migrated or cleared. Changes should be communicated in release notes as potentially breaking changes.
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record BuildAgentInformation(String name, int maxNumberOfConcurrentBuildJobs, int numberOfCurrentBuildJobs, List<BuildJobQueueItem> runningBuildJobs,
public record BuildAgentInformation(BuildAgentDTO buildAgent, int maxNumberOfConcurrentBuildJobs, int numberOfCurrentBuildJobs, List<BuildJobQueueItem> runningBuildJobs,
BuildAgentStatus status, List<BuildJobQueueItem> recentBuildJobs, String publicSshKey) implements Serializable {

@Serial
Expand All @@ -24,7 +24,7 @@ public record BuildAgentInformation(String name, int maxNumberOfConcurrentBuildJ
* @param recentBuildJobs The list of recent build jobs
*/
public BuildAgentInformation(BuildAgentInformation agentInformation, List<BuildJobQueueItem> recentBuildJobs) {
this(agentInformation.name(), agentInformation.maxNumberOfConcurrentBuildJobs(), agentInformation.numberOfCurrentBuildJobs(), agentInformation.runningBuildJobs,
this(agentInformation.buildAgent(), agentInformation.maxNumberOfConcurrentBuildJobs(), agentInformation.numberOfCurrentBuildJobs(), agentInformation.runningBuildJobs,
agentInformation.status(), recentBuildJobs, agentInformation.publicSshKey());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
// in the future are migrated or cleared. Changes should be communicated in release notes as potentially breaking changes.
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record BuildJobQueueItem(String id, String name, String buildAgentAddress, long participationId, long courseId, long exerciseId, int retryCount, int priority,
public record BuildJobQueueItem(String id, String name, BuildAgentDTO buildAgent, long participationId, long courseId, long exerciseId, int retryCount, int priority,
BuildStatus status, RepositoryInfo repositoryInfo, JobTimingInfo jobTimingInfo, BuildConfig buildConfig, ResultDTO submissionResult) implements Serializable {

@Serial
Expand All @@ -28,25 +28,24 @@ public record BuildJobQueueItem(String id, String name, String buildAgentAddress
* @param status The status/result of the build job
*/
public BuildJobQueueItem(BuildJobQueueItem queueItem, ZonedDateTime buildCompletionDate, BuildStatus status) {
this(queueItem.id(), queueItem.name(), queueItem.buildAgentAddress(), queueItem.participationId(), queueItem.courseId(), queueItem.exerciseId(), queueItem.retryCount(),
this(queueItem.id(), queueItem.name(), queueItem.buildAgent(), queueItem.participationId(), queueItem.courseId(), queueItem.exerciseId(), queueItem.retryCount(),
queueItem.priority(), status, queueItem.repositoryInfo(),
new JobTimingInfo(queueItem.jobTimingInfo.submissionDate(), queueItem.jobTimingInfo.buildStartDate(), buildCompletionDate), queueItem.buildConfig(), null);
}

/**
* Constructor used to create a new processing build job from a queued build job
*
* @param queueItem The queued build job
* @param hazelcastMemberAddress The address of the hazelcast member that is processing the build job
* @param queueItem The queued build job
* @param buildAgent The build agent that will process the build job
*/
public BuildJobQueueItem(BuildJobQueueItem queueItem, String hazelcastMemberAddress) {
this(queueItem.id(), queueItem.name(), hazelcastMemberAddress, queueItem.participationId(), queueItem.courseId(), queueItem.exerciseId(), queueItem.retryCount(),
queueItem.priority(), null, queueItem.repositoryInfo(), new JobTimingInfo(queueItem.jobTimingInfo.submissionDate(), ZonedDateTime.now(), null),
queueItem.buildConfig(), null);
public BuildJobQueueItem(BuildJobQueueItem queueItem, BuildAgentDTO buildAgent) {
this(queueItem.id(), queueItem.name(), buildAgent, queueItem.participationId(), queueItem.courseId(), queueItem.exerciseId(), queueItem.retryCount(), queueItem.priority(),
null, queueItem.repositoryInfo(), new JobTimingInfo(queueItem.jobTimingInfo.submissionDate(), ZonedDateTime.now(), null), queueItem.buildConfig(), null);
BBesrour marked this conversation as resolved.
Show resolved Hide resolved
}

public BuildJobQueueItem(BuildJobQueueItem queueItem, ResultDTO submissionResult) {
this(queueItem.id(), queueItem.name(), queueItem.buildAgentAddress(), queueItem.participationId(), queueItem.courseId(), queueItem.exerciseId(), queueItem.retryCount(),
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;

import org.apache.commons.lang3.StringUtils;
BBesrour marked this conversation as resolved.
Show resolved Hide resolved
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
Expand All @@ -43,6 +44,7 @@
import com.hazelcast.map.IMap;
import com.hazelcast.topic.ITopic;

import de.tum.cit.aet.artemis.buildagent.dto.BuildAgentDTO;
import de.tum.cit.aet.artemis.buildagent.dto.BuildAgentInformation;
import de.tum.cit.aet.artemis.buildagent.dto.BuildJobQueueItem;
import de.tum.cit.aet.artemis.buildagent.dto.BuildResult;
Expand Down Expand Up @@ -114,9 +116,15 @@ public class SharedQueueProcessingService {
*/
private final AtomicBoolean processResults = new AtomicBoolean(true);

@Value("${artemis.continuous-integration.pause-grace-period-seconds:15}")
@Value("${artemis.continuous-integration.pause-grace-period-seconds:60}")
BBesrour marked this conversation as resolved.
Show resolved Hide resolved
BBesrour marked this conversation as resolved.
Show resolved Hide resolved
BBesrour marked this conversation as resolved.
Show resolved Hide resolved
private int pauseGracePeriodSeconds;

@Value("${artemis.continuous-integration.build-agent.short-name}")
private String buildAgentShortName;

@Value("${artemis.continuous-integration.build-agent.display-name:}")
private String buildAgentDisplayName;
BBesrour marked this conversation as resolved.
Show resolved Hide resolved

public SharedQueueProcessingService(@Qualifier("hazelcastInstance") HazelcastInstance hazelcastInstance, ExecutorService localCIBuildExecutorService,
BuildJobManagementService buildJobManagementService, BuildLogsMap buildLogsMap, BuildAgentSshKeyService buildAgentSSHKeyService, TaskScheduler taskScheduler) {
this.hazelcastInstance = hazelcastInstance;
Expand All @@ -132,6 +140,17 @@ public SharedQueueProcessingService(@Qualifier("hazelcastInstance") HazelcastIns
*/
@PostConstruct
public void init() {
if (!buildAgentShortName.matches("^[a-z0-9-]+$")) {
String errorMessage = "Build agent short name must not be empty and only contain lowercase letters, numbers and hyphens."
+ " Build agent short name should be changed in the application properties under 'artemis.continuous-integration.build-agent.short-name'.";
log.error(errorMessage);
throw new IllegalArgumentException(errorMessage);
}
BBesrour marked this conversation as resolved.
Show resolved Hide resolved

if (StringUtils.isBlank(buildAgentDisplayName)) {
buildAgentDisplayName = buildAgentShortName;
}

this.buildAgentInformation = this.hazelcastInstance.getMap("buildAgentInformation");
this.processingJobs = this.hazelcastInstance.getMap("processingJobs");
this.queue = this.hazelcastInstance.getQueue("buildJobQueue");
Expand All @@ -152,14 +171,14 @@ public void init() {

ITopic<String> pauseBuildAgentTopic = hazelcastInstance.getTopic("pauseBuildAgentTopic");
pauseBuildAgentTopic.addMessageListener(message -> {
if (message.getMessageObject().equals(hazelcastInstance.getCluster().getLocalMember().getAddress().toString())) {
if (buildAgentShortName.equals(message.getMessageObject())) {
pauseBuildAgent();
}
});

ITopic<String> resumeBuildAgentTopic = hazelcastInstance.getTopic("resumeBuildAgentTopic");
resumeBuildAgentTopic.addMessageListener(message -> {
if (message.getMessageObject().equals(hazelcastInstance.getCluster().getLocalMember().getAddress().toString())) {
if (buildAgentShortName.equals(message.getMessageObject())) {
resumeBuildAgent();
}
});
Expand Down Expand Up @@ -191,6 +210,7 @@ public void updateBuildAgentInformation() {
log.debug("There are only lite member in the cluster. Not updating build agent information.");
return;
}

// Remove build agent information of offline nodes
removeOfflineNodes();

Expand Down Expand Up @@ -243,7 +263,7 @@ private void checkAvailabilityAndProcessNextBuild() {
if (buildJob != null) {
processingJobs.remove(buildJob.id());

buildJob = new BuildJobQueueItem(buildJob, "");
buildJob = new BuildJobQueueItem(buildJob, new BuildAgentDTO("", "", ""));
BBesrour marked this conversation as resolved.
Show resolved Hide resolved
log.info("Adding build job back to the queue: {}", buildJob);
queue.add(buildJob);
localProcessingJobs.decrementAndGet();
Expand All @@ -265,7 +285,7 @@ private BuildJobQueueItem addToProcessingJobs() {
if (buildJob != null) {
String hazelcastMemberAddress = hazelcastInstance.getCluster().getLocalMember().getAddress().toString();

BuildJobQueueItem processingJob = new BuildJobQueueItem(buildJob, hazelcastMemberAddress);
BuildJobQueueItem processingJob = new BuildJobQueueItem(buildJob, new BuildAgentDTO(buildAgentShortName, hazelcastMemberAddress, buildAgentDisplayName));

processingJobs.put(processingJob.id(), processingJob);
localProcessingJobs.incrementAndGet();
Expand All @@ -287,10 +307,10 @@ private void updateLocalBuildAgentInformationWithRecentJob(BuildJobQueueItem rec
// Add/update
BuildAgentInformation info = getUpdatedLocalBuildAgentInformation(recentBuildJob);
try {
buildAgentInformation.put(info.name(), info);
buildAgentInformation.put(info.buildAgent().memberAddress(), info);
}
catch (Exception e) {
log.error("Error while updating build agent information for agent {}", info.name(), e);
log.error("Error while updating build agent information for agent {} with address {}", info.buildAgent().name(), info.buildAgent().memberAddress(), e);
}
}
finally {
Expand Down Expand Up @@ -324,11 +344,13 @@ private BuildAgentInformation getUpdatedLocalBuildAgentInformation(BuildJobQueue

String publicSshKey = buildAgentSSHKeyService.getPublicKeyAsString();

return new BuildAgentInformation(memberAddress, maxNumberOfConcurrentBuilds, numberOfCurrentBuildJobs, processingJobsOfMember, status, recentBuildJobs, publicSshKey);
BuildAgentDTO agentInfo = new BuildAgentDTO(buildAgentShortName, memberAddress, buildAgentDisplayName);

return new BuildAgentInformation(agentInfo, maxNumberOfConcurrentBuilds, numberOfCurrentBuildJobs, processingJobsOfMember, status, recentBuildJobs, publicSshKey);
}

private List<BuildJobQueueItem> getProcessingJobsOfNode(String memberAddress) {
return processingJobs.values().stream().filter(job -> Objects.equals(job.buildAgentAddress(), memberAddress)).toList();
return processingJobs.values().stream().filter(job -> Objects.equals(job.buildAgent().memberAddress(), memberAddress)).toList();
BBesrour marked this conversation as resolved.
Show resolved Hide resolved
}

private void removeOfflineNodes() {
Expand Down Expand Up @@ -361,7 +383,7 @@ private void processBuild(BuildJobQueueItem buildJob) {
log.debug("Build job completed: {}", buildJob);
JobTimingInfo jobTimingInfo = new JobTimingInfo(buildJob.jobTimingInfo().submissionDate(), buildJob.jobTimingInfo().buildStartDate(), ZonedDateTime.now());

BuildJobQueueItem finishedJob = new BuildJobQueueItem(buildJob.id(), buildJob.name(), buildJob.buildAgentAddress(), buildJob.participationId(), buildJob.courseId(),
BuildJobQueueItem finishedJob = new BuildJobQueueItem(buildJob.id(), buildJob.name(), buildJob.buildAgent(), buildJob.participationId(), buildJob.courseId(),
buildJob.exerciseId(), buildJob.retryCount(), buildJob.priority(), BuildStatus.SUCCESSFUL, buildJob.repositoryInfo(), jobTimingInfo, buildJob.buildConfig(),
null);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import java.time.ZonedDateTime;
import java.util.List;
import java.util.Optional;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -95,9 +96,12 @@ public ResponseEntity<List<BuildAgentInformation>> getBuildAgentSummary() {
@GetMapping("build-agent")
public ResponseEntity<BuildAgentInformation> getBuildAgentDetails(@RequestParam String agentName) {
log.debug("REST request to get information on build agent {}", agentName);
BuildAgentInformation buildAgentDetails = localCIBuildJobQueueService.getBuildAgentInformation().stream().filter(agent -> agent.name().equals(agentName)).findFirst()
.orElse(null);
return ResponseEntity.ok(buildAgentDetails);
Optional<BuildAgentInformation> buildAgentDetails = localCIBuildJobQueueService.getBuildAgentInformation().stream()
.filter(agent -> agent.buildAgent().name().equals(agentName)).findFirst();
if (buildAgentDetails.isEmpty()) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(buildAgentDetails.get());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public BuildJob(BuildJobQueueItem queueItem, BuildStatus buildStatus, Result res
this.courseId = queueItem.courseId();
this.participationId = queueItem.participationId();
this.result = result;
this.buildAgentAddress = queueItem.buildAgentAddress();
this.buildAgentAddress = queueItem.buildAgent().memberAddress();
this.buildStartDate = queueItem.jobTimingInfo().buildStartDate();
this.buildCompletionDate = queueItem.jobTimingInfo().buildCompletionDate();
this.repositoryType = queueItem.repositoryInfo().repositoryType();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ private void sendBuildAgentSummaryOverWebsocket() {
}

private void sendBuildAgentDetailsOverWebsocket(String agentName) {
sharedQueueManagementService.getBuildAgentInformation().stream().filter(agent -> agent.name().equals(agentName)).findFirst()
sharedQueueManagementService.getBuildAgentInformation().stream().filter(agent -> agent.buildAgent().name().equals(agentName)).findFirst()
.ifPresent(localCIWebsocketMessagingService::sendBuildAgentDetails);
}

Expand Down Expand Up @@ -127,19 +127,19 @@ private class BuildAgentListener
@Override
public void entryAdded(com.hazelcast.core.EntryEvent<String, BuildAgentInformation> event) {
log.debug("Build agent added: {}", event.getValue());
sendBuildAgentInformationOverWebsocket(event.getValue().name());
sendBuildAgentInformationOverWebsocket(event.getValue().buildAgent().name());
}

@Override
public void entryRemoved(com.hazelcast.core.EntryEvent<String, BuildAgentInformation> event) {
log.debug("Build agent removed: {}", event.getOldValue());
sendBuildAgentInformationOverWebsocket(event.getOldValue().name());
sendBuildAgentInformationOverWebsocket(event.getOldValue().buildAgent().name());
}

@Override
public void entryUpdated(com.hazelcast.core.EntryEvent<String, BuildAgentInformation> event) {
log.debug("Build agent updated: {}", event.getValue());
sendBuildAgentInformationOverWebsocket(event.getValue().name());
sendBuildAgentInformationOverWebsocket(event.getValue().buildAgent().name());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,8 @@ public void processResult() {
*/
private void addResultToBuildAgentsRecentBuildJobs(BuildJobQueueItem buildJob, Result result) {
try {
buildAgentInformation.lock(buildJob.buildAgentAddress());
BuildAgentInformation buildAgent = buildAgentInformation.get(buildJob.buildAgentAddress());
buildAgentInformation.lock(buildJob.buildAgent().memberAddress());
BuildAgentInformation buildAgent = buildAgentInformation.get(buildJob.buildAgent().memberAddress());
if (buildAgent != null) {
List<BuildJobQueueItem> recentBuildJobs = buildAgent.recentBuildJobs();
for (int i = 0; i < recentBuildJobs.size(); i++) {
Expand All @@ -221,11 +221,11 @@ private void addResultToBuildAgentsRecentBuildJobs(BuildJobQueueItem buildJob, R
break;
}
}
buildAgentInformation.put(buildJob.buildAgentAddress(), new BuildAgentInformation(buildAgent, recentBuildJobs));
buildAgentInformation.put(buildJob.buildAgent().memberAddress(), new BuildAgentInformation(buildAgent, recentBuildJobs));
}
}
finally {
buildAgentInformation.unlock(buildJob.buildAgentAddress());
buildAgentInformation.unlock(buildJob.buildAgent().memberAddress());
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.map.IMap;

import de.tum.cit.aet.artemis.buildagent.dto.BuildAgentDTO;
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.JobTimingInfo;
Expand Down Expand Up @@ -196,8 +197,10 @@ else if (triggeredByPushTo.equals(RepositoryType.TESTS)) {

BuildConfig buildConfig = getBuildConfig(participation, commitHashToBuild, assignmentCommitHash, testCommitHash, programmingExerciseBuildConfig);

BuildJobQueueItem buildJobQueueItem = new BuildJobQueueItem(buildJobId, participation.getBuildPlanId(), null, participation.getId(), courseId, programmingExercise.getId(),
0, priority, null, repositoryInfo, jobTimingInfo, buildConfig, null);
BuildAgentDTO buildAgent = new BuildAgentDTO(null, null, null);

BuildJobQueueItem buildJobQueueItem = new BuildJobQueueItem(buildJobId, participation.getBuildPlanId(), buildAgent, participation.getId(), courseId,
programmingExercise.getId(), 0, priority, null, repositoryInfo, jobTimingInfo, buildConfig, null);

queue.add(buildJobQueueItem);
log.info("Added build job {} to the queue", buildJobId);
Expand Down
Loading