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 9 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
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(String name, String memberAddress, int maxNumberOfConcurrentBuildJobs, int numberOfCurrentBuildJobs, List<BuildJobQueueItem> runningBuildJobs,
BuildAgentStatus status, List<BuildJobQueueItem> recentBuildJobs, String publicSshKey) implements Serializable {

@Serial
Expand All @@ -24,8 +24,8 @@ 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,
agentInformation.status(), recentBuildJobs, agentInformation.publicSshKey());
this(agentInformation.name(), agentInformation.memberAddress(), agentInformation.maxNumberOfConcurrentBuildJobs(), agentInformation.numberOfCurrentBuildJobs(),
agentInformation.runningBuildJobs, agentInformation.status(), recentBuildJobs, agentInformation.publicSshKey());
}

public enum BuildAgentStatus {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
// 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,
BuildStatus status, RepositoryInfo repositoryInfo, JobTimingInfo jobTimingInfo, BuildConfig buildConfig, ResultDTO submissionResult) implements Serializable {
public record BuildJobQueueItem(String id, String name, String buildAgentName, String buildAgentAddress, long participationId, long courseId, long exerciseId, int retryCount,
BBesrour marked this conversation as resolved.
Show resolved Hide resolved
int priority, BuildStatus status, RepositoryInfo repositoryInfo, JobTimingInfo jobTimingInfo, BuildConfig buildConfig, ResultDTO submissionResult) implements Serializable {

@Serial
private static final long serialVersionUID = 1L;
Expand All @@ -28,8 +28,8 @@ 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(),
queueItem.priority(), status, queueItem.repositoryInfo(),
this(queueItem.id(), queueItem.name(), queueItem.buildAgentName(), queueItem.buildAgentAddress(), 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);
}

Expand All @@ -39,14 +39,14 @@ public BuildJobQueueItem(BuildJobQueueItem queueItem, ZonedDateTime buildComplet
* @param queueItem The queued build job
* @param hazelcastMemberAddress The address of the hazelcast member that is processing 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, String buildAgentName, String hazelcastMemberAddress) {
this(queueItem.id(), queueItem.name(), buildAgentName, 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, ResultDTO submissionResult) {
this(queueItem.id(), queueItem.name(), queueItem.buildAgentAddress(), queueItem.participationId(), queueItem.courseId(), queueItem.exerciseId(), queueItem.retryCount(),
queueItem.priority(), queueItem.status(), queueItem.repositoryInfo(), queueItem.jobTimingInfo(), queueItem.buildConfig(), submissionResult);
this(queueItem.id(), queueItem.name(), queueItem.buildAgentName(), queueItem.buildAgentAddress(), 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 @@ -114,9 +114,12 @@ 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;

public SharedQueueProcessingService(@Qualifier("hazelcastInstance") HazelcastInstance hazelcastInstance, ExecutorService localCIBuildExecutorService,
BuildJobManagementService buildJobManagementService, BuildLogsMap buildLogsMap, BuildAgentSshKeyService buildAgentSSHKeyService, TaskScheduler taskScheduler) {
this.hazelcastInstance = hazelcastInstance;
Expand All @@ -132,6 +135,13 @@ 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

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

ITopic<String> pauseBuildAgentTopic = hazelcastInstance.getTopic("pauseBuildAgentTopic");
pauseBuildAgentTopic.addMessageListener(message -> {
if (message.getMessageObject().equals(hazelcastInstance.getCluster().getLocalMember().getAddress().toString())) {
if (message.getMessageObject().equals(buildAgentShortName)) {
BBesrour marked this conversation as resolved.
Show resolved Hide resolved
pauseBuildAgent();
}
});

ITopic<String> resumeBuildAgentTopic = hazelcastInstance.getTopic("resumeBuildAgentTopic");
resumeBuildAgentTopic.addMessageListener(message -> {
if (message.getMessageObject().equals(hazelcastInstance.getCluster().getLocalMember().getAddress().toString())) {
if (message.getMessageObject().equals(buildAgentShortName)) {
resumeBuildAgent();
}
});
Expand Down Expand Up @@ -191,6 +201,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 +254,7 @@ private void checkAvailabilityAndProcessNextBuild() {
if (buildJob != null) {
processingJobs.remove(buildJob.id());

buildJob = new BuildJobQueueItem(buildJob, "");
buildJob = new BuildJobQueueItem(buildJob, "", "");
log.info("Adding build job back to the queue: {}", buildJob);
queue.add(buildJob);
localProcessingJobs.decrementAndGet();
BBesrour marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -265,7 +276,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, buildAgentShortName, hazelcastMemberAddress);

processingJobs.put(processingJob.id(), processingJob);
localProcessingJobs.incrementAndGet();
Expand All @@ -287,10 +298,10 @@ private void updateLocalBuildAgentInformationWithRecentJob(BuildJobQueueItem rec
// Add/update
BuildAgentInformation info = getUpdatedLocalBuildAgentInformation(recentBuildJob);
try {
buildAgentInformation.put(info.name(), info);
buildAgentInformation.put(info.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.name(), info.memberAddress(), e);
}
}
finally {
Expand Down Expand Up @@ -324,7 +335,8 @@ private BuildAgentInformation getUpdatedLocalBuildAgentInformation(BuildJobQueue

String publicSshKey = buildAgentSSHKeyService.getPublicKeyAsString();

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

private List<BuildJobQueueItem> getProcessingJobsOfNode(String memberAddress) {
Expand Down Expand Up @@ -361,9 +373,9 @@ 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(),
buildJob.exerciseId(), buildJob.retryCount(), buildJob.priority(), BuildStatus.SUCCESSFUL, buildJob.repositoryInfo(), jobTimingInfo, buildJob.buildConfig(),
null);
BuildJobQueueItem finishedJob = new BuildJobQueueItem(buildJob.id(), buildJob.name(), buildJob.buildAgentName(), buildJob.buildAgentAddress(),
buildJob.participationId(), buildJob.courseId(), buildJob.exerciseId(), buildJob.retryCount(), buildJob.priority(), BuildStatus.SUCCESSFUL,
buildJob.repositoryInfo(), jobTimingInfo, buildJob.buildConfig(), null);

List<BuildLogEntry> buildLogs = buildLogsMap.getBuildLogs(buildJob.id());
buildLogsMap.removeBuildLogs(buildJob.id());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,8 @@ 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);
BuildJobQueueItem buildJobQueueItem = new BuildJobQueueItem(buildJobId, participation.getBuildPlanId(), null, null, participation.getId(), courseId,
programmingExercise.getId(), 0, priority, null, repositoryInfo, jobTimingInfo, buildConfig, null);
BBesrour marked this conversation as resolved.
Show resolved Hide resolved

queue.add(buildJobQueueItem);
log.info("Added build job {} to the queue", buildJobId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ public List<BuildAgentInformation> getBuildAgentInformation() {
}

public List<BuildAgentInformation> getBuildAgentInformationWithoutRecentBuildJobs() {
return buildAgentInformation.values().stream().map(agent -> new BuildAgentInformation(agent.name(), agent.maxNumberOfConcurrentBuildJobs(),
return buildAgentInformation.values().stream().map(agent -> new BuildAgentInformation(agent.name(), agent.memberAddress(), agent.maxNumberOfConcurrentBuildJobs(),
agent.numberOfCurrentBuildJobs(), agent.runningBuildJobs(), agent.status(), null, null)).toList();
}

Expand Down
1 change: 1 addition & 0 deletions src/main/resources/config/application-buildagent.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ artemis:
container-cleanup:
expiry-minutes: 5
cleanup-schedule-minutes: 60
pause-grace-period-seconds: 60
git:
name: Artemis
email: artemis@xcit.tum.de
Expand Down
8 changes: 8 additions & 0 deletions src/main/resources/config/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,14 @@ artemis:
c_plus_plus:
default: "ghcr.io/ls1intum/artemis-cpp-docker:v1.0.0"

# The following properties are used to configure the Artemis build agent.
# The build agent is responsible for executing the buildJob to test student submissions.
build-agent:
# Name of the build agent. Only lowercase letters, numbers and hyphens are allowed. ([a-z0-9-]+)
short-name: "artemis-build-agent-1"



BBesrour marked this conversation as resolved.
Show resolved Hide resolved
management:
endpoints:
web:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export enum BuildAgentStatus {
export class BuildAgent implements BaseEntity {
public id?: number;
public name?: string;
public memberAddress?: string;
public maxNumberOfConcurrentBuildJobs?: number;
public numberOfCurrentBuildJobs?: number;
public runningBuildJobs?: BuildJob[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import dayjs from 'dayjs/esm';
export class BuildJob implements StringBaseEntity {
public id?: string;
public name?: string;
public buildAgentName?: string;
public buildAgentAddress?: string;
public participationId?: number;
public courseId?: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
@if (buildAgent) {
<div class="mb-4">
<div class="d-flex align-items-center justify-content-between mb-3">
<div class="d-flex align-items-center">
<h3 class="mb-0" id="build-agent-heading" jhiTranslate="artemisApp.buildAgents.details"></h3>
<span class="h3 mb-0">:</span>
<h3 id="build-agent-name" class="mb-0 ms-2">{{ buildAgent.name }}</h3>
<div>
<div class="d-flex align-items-center">
<h3 class="mb-0" id="build-agent-heading" jhiTranslate="artemisApp.buildAgents.details"></h3>
<span class="h3 mb-0">:</span>
<h3 id="build-agent-name" class="mb-0 ms-2">{{ buildAgent.name }}</h3>
</div>
<span class="text-muted" id="build-agent-member-address">{{ buildAgent.memberAddress }}</span>
</div>
<div class="ms-3 d-flex justify-content-center gap-2">
@if (buildAgent.status === 'PAUSED') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ <h3 id="build-agents-heading" jhiTranslate="artemisApp.buildAgents.summary"></h3
</a>
</ng-template>
</ngx-datatable-column>
<ngx-datatable-column prop="memberAddress" [minWidth]="150">
<ng-template ngx-datatable-header-template>
<span class="datatable-header-cell-wrapper" (click)="controls.onSort('memberAddress')">
<span class="datatable-header-cell-label bold sortable" jhiTranslate="artemisApp.buildAgents.memberAddress"></span>
<fa-icon [icon]="controls.iconForSortPropField('memberAddress')" />
</span>
</ng-template>
</ngx-datatable-column>
<ngx-datatable-column prop="status" [minWidth]="150">
<ng-template ngx-datatable-header-template>
<span class="datatable-header-cell-wrapper" (click)="controls.onSort('status')">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ <h3 id="build-queue-running-heading" jhiTranslate="artemisApp.buildAgents.runnin
<fa-icon [icon]="controls.iconForSortPropField('buildAgentAddress')" />
</span>
</ng-template>
<ng-template ngx-datatable-cell-template let-value="value">
<a [routerLink]="['/admin/build-agents/details']" [queryParams]="{ agentName: value }">
<ng-template ngx-datatable-cell-template let-value="value" let-row="row">
<a [routerLink]="['/admin/build-agents/details']" [queryParams]="{ agentName: row.buildAgentName }">
{{ value }}
</a>
</ng-template>
Expand Down
1 change: 1 addition & 0 deletions src/main/webapp/i18n/de/buildAgents.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"summary": "Build Agenten Zusammenfassung",
"details": "Build Agenten Details",
"name": "Name",
"memberAddress": "Agentenadresse",
"maxNumberOfConcurrentBuildJobs": "Maximale Anzahl an parallelen Build Jobs",
"numberOfCurrentBuildJobs": "Anzahl aktueller Build Jobs",
"runningBuildJobs": "Laufende Build Jobs",
Expand Down
1 change: 1 addition & 0 deletions src/main/webapp/i18n/en/buildAgents.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"summary": "Build Agents Summary",
"details": "Build Agents Details",
"name": "Name",
"memberAddress": "Member Address",
"maxNumberOfConcurrentBuildJobs": "Max # of concurrent build jobs",
"numberOfCurrentBuildJobs": "# of current build jobs",
"runningBuildJobs": "Running build jobs",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ void testPullDockerImage() {
doReturn(inspectImageCmd).when(dockerClient).inspectImageCmd(anyString());
doThrow(new NotFoundException("")).when(inspectImageCmd).exec();
BuildConfig buildConfig = new BuildConfig("echo 'test'", "test-image-name", "test", "test", "test", "test", null, null, false, false, false, null, 0, null, null, null);
var build = new BuildJobQueueItem("1", "job1", "address1", 1, 1, 1, 1, 1, BuildStatus.SUCCESSFUL, null, null, buildConfig, null);
var build = new BuildJobQueueItem("1", "job1", "buildagent1", "address1", 1, 1, 1, 1, 1, BuildStatus.SUCCESSFUL, null, null, buildConfig, null);
// Pull image
try {
buildAgentDockerService.pullDockerImage(build, new BuildLogsMap());
Expand Down
Loading
Loading