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: Improve build status updates for users #9818

Open
wants to merge 18 commits into
base: develop
Choose a base branch
from
Open
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ public record BuildJobQueueItem(String id, String name, BuildAgentDTO buildAgent
*/
public BuildJobQueueItem(BuildJobQueueItem queueItem, ZonedDateTime buildCompletionDate, BuildStatus status) {
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);
queueItem.priority(), status, queueItem.repositoryInfo(), new JobTimingInfo(queueItem.jobTimingInfo.submissionDate(), queueItem.jobTimingInfo.buildStartDate(),
buildCompletionDate, queueItem.jobTimingInfo.estimatedCompletionDate(), queueItem.jobTimingInfo.estimatedDuration()),
queueItem.buildConfig(), null);
}

/**
Expand All @@ -39,9 +40,11 @@ public BuildJobQueueItem(BuildJobQueueItem queueItem, ZonedDateTime buildComplet
* @param queueItem The queued build job
* @param buildAgent The build agent that will process the build job
*/
public BuildJobQueueItem(BuildJobQueueItem queueItem, BuildAgentDTO buildAgent) {
public BuildJobQueueItem(BuildJobQueueItem queueItem, BuildAgentDTO buildAgent, ZonedDateTime estimatedCompletionDate) {
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);
null, queueItem.repositoryInfo(),
new JobTimingInfo(queueItem.jobTimingInfo.submissionDate(), ZonedDateTime.now(), null, estimatedCompletionDate, queueItem.jobTimingInfo.estimatedDuration()),
queueItem.buildConfig(), null);
BBesrour marked this conversation as resolved.
Show resolved Hide resolved
BBesrour marked this conversation as resolved.
Show resolved Hide resolved
}

public BuildJobQueueItem(BuildJobQueueItem queueItem, ResultDTO submissionResult) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public record ResultDTO(Long id, ZonedDateTime completionDate, Boolean successfu
* @return the converted DTO
*/
public static ResultDTO of(Result result) {
SubmissionDTO submissionDTO = result.getSubmission() == null ? null : SubmissionDTO.of(result.getSubmission());
SubmissionDTO submissionDTO = result.getSubmission() == null ? null : SubmissionDTO.of(result.getSubmission(), false, null, null);

return new ResultDTO(result.getId(), result.getCompletionDate(), result.isSuccessful(), result.getScore(), result.isRated(),
ParticipationDTO.of(result.getParticipation()), submissionDTO, result.getAssessmentType(), result.getTestCaseCount(), result.getPassedTestCaseCount(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
// 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 JobTimingInfo(ZonedDateTime submissionDate, ZonedDateTime buildStartDate, ZonedDateTime buildCompletionDate) implements Serializable {
public record JobTimingInfo(ZonedDateTime submissionDate, ZonedDateTime buildStartDate, ZonedDateTime buildCompletionDate, ZonedDateTime estimatedCompletionDate,
long estimatedDuration) implements Serializable {
}
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ private void checkAvailabilityAndProcessNextBuild() {
if (buildJob != null) {
processingJobs.remove(buildJob.id());

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

BuildJobQueueItem processingJob = new BuildJobQueueItem(buildJob, new BuildAgentDTO(buildAgentShortName, hazelcastMemberAddress, buildAgentDisplayName));
var estimatedDuration = Math.max(0, buildJob.jobTimingInfo().estimatedDuration());
ZonedDateTime estimatedCompletionDate = ZonedDateTime.now().plusSeconds(estimatedDuration);
BuildJobQueueItem processingJob = new BuildJobQueueItem(buildJob, new BuildAgentDTO(buildAgentShortName, hazelcastMemberAddress, buildAgentDisplayName),
estimatedCompletionDate);
BBesrour marked this conversation as resolved.
Show resolved Hide resolved

processingJobs.put(processingJob.id(), processingJob);
localProcessingJobs.incrementAndGet();
Expand Down Expand Up @@ -391,7 +394,8 @@ private void processBuild(BuildJobQueueItem buildJob) {
futureResult.thenAccept(buildResult -> {

log.debug("Build job completed: {}", buildJob);
JobTimingInfo jobTimingInfo = new JobTimingInfo(buildJob.jobTimingInfo().submissionDate(), buildJob.jobTimingInfo().buildStartDate(), ZonedDateTime.now());
JobTimingInfo jobTimingInfo = new JobTimingInfo(buildJob.jobTimingInfo().submissionDate(), buildJob.jobTimingInfo().buildStartDate(), ZonedDateTime.now(),
buildJob.jobTimingInfo().estimatedCompletionDate(), buildJob.jobTimingInfo().estimatedDuration());

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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ public final class Constants {

public static final String NEW_SUBMISSION_TOPIC = "/topic" + PROGRAMMING_SUBMISSION_TOPIC;

public static final String SUBMISSION_PROCESSING = "/submissionProcessing";

public static final String SUBMISSION_PROCESSING_TOPIC = "/topic" + SUBMISSION_PROCESSING;

public static final String ATHENA_PROGRAMMING_EXERCISE_REPOSITORY_API_PATH = "/api/public/athena/programming-exercises/";

// short names should have at least 3 characters and must start with a letter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,25 @@
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record SubmissionDTO(Long id, Boolean submitted, SubmissionType type, Boolean exampleSubmission, ZonedDateTime submissionDate, String commitHash, Boolean buildFailed,
Boolean buildArtifact, ParticipationDTO participation, String submissionExerciseType) implements Serializable {
Boolean buildArtifact, ParticipationDTO participation, String submissionExerciseType, boolean isProcessing, ZonedDateTime buildStartDate,
ZonedDateTime estimatedCompletionDate) implements Serializable {

/**
* Converts a Submission into a SubmissionDTO.
*
* @param submission to convert
* @return the converted DTO
*/
public static SubmissionDTO of(Submission submission) {
public static SubmissionDTO of(Submission submission, boolean isProcessing, ZonedDateTime buildStartDate, ZonedDateTime estimatedCompletionDate) {
if (submission instanceof ProgrammingSubmission programmingSubmission) {
// For programming submissions we need to extract additional information (e.g. the commit hash) and send it to the client
return new SubmissionDTO(programmingSubmission.getId(), programmingSubmission.isSubmitted(), programmingSubmission.getType(),
programmingSubmission.isExampleSubmission(), programmingSubmission.getSubmissionDate(), programmingSubmission.getCommitHash(),
programmingSubmission.isBuildFailed(), programmingSubmission.isBuildArtifact(), ParticipationDTO.of(programmingSubmission.getParticipation()),
programmingSubmission.getSubmissionExerciseType());
programmingSubmission.getSubmissionExerciseType(), isProcessing, buildStartDate, estimatedCompletionDate);
}
return new SubmissionDTO(submission.getId(), submission.isSubmitted(), submission.getType(), submission.isExampleSubmission(), submission.getSubmissionDate(), null, null,
null, ParticipationDTO.of(submission.getParticipation()), submission.getSubmissionExerciseType());
null, ParticipationDTO.of(submission.getParticipation()), submission.getSubmissionExerciseType(), false, null, null);
}
BBesrour marked this conversation as resolved.
Show resolved Hide resolved

}
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ public class ProgrammingExerciseBuildConfig extends DomainObject {
@Column(name = "build_plan_access_secret", length = 36)
private String buildPlanAccessSecret;

@Column(name = "build_duration_seconds")
private long buildDurationSeconds = 0;

@Column(name = "successful_build_count")
private long successfulBuildCount = 0;

public ProgrammingExerciseBuildConfig() {
}

Expand All @@ -104,6 +110,8 @@ public ProgrammingExerciseBuildConfig(ProgrammingExerciseBuildConfig originalBui
this.setAllowBranching(originalBuildConfig.isAllowBranching());
this.setBranchRegex(originalBuildConfig.getBranchRegex());
this.setProgrammingExercise(null);
this.setBuildDurationSeconds(0);
this.setSuccessfulBuildCount(0);
this.buildPlanAccessSecret = null;
}

Expand Down Expand Up @@ -292,6 +300,22 @@ public void setSolutionCheckoutPath(String solutionCheckoutPath) {
this.solutionCheckoutPath = solutionCheckoutPath;
}

public long getBuildDurationSeconds() {
return buildDurationSeconds;
}

public void setBuildDurationSeconds(long buildDurationSeconds) {
this.buildDurationSeconds = buildDurationSeconds;
}

public long getSuccessfulBuildCount() {
return successfulBuildCount;
}

public void setSuccessfulBuildCount(long successfulBuildCount) {
this.successfulBuildCount = successfulBuildCount;
}

BBesrour marked this conversation as resolved.
Show resolved Hide resolved
@Override
public String toString() {
return "BuildJobConfig{" + "id=" + getId() + ", sequentialTestRuns=" + sequentialTestRuns + ", branch='" + branch + '\'' + ", buildPlanConfiguration='"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public static ResultDTO of(Result result) {
public static ResultDTO of(Result result, List<Feedback> filteredFeedback) {
SubmissionDTO submissionDTO = null;
if (Hibernate.isInitialized(result.getSubmission()) && result.getSubmission() != null) {
submissionDTO = SubmissionDTO.of(result.getSubmission());
submissionDTO = SubmissionDTO.of(result.getSubmission(), false, null, null);
}
var feedbackDTOs = filteredFeedback.stream().map(FeedbackDTO::of).toList();
return new ResultDTO(result.getId(), result.getCompletionDate(), result.isSuccessful(), result.getScore(), result.isRated(), submissionDTO,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package de.tum.cit.aet.artemis.programming.dto;

import java.io.Serial;
import java.io.Serializable;
import java.time.ZonedDateTime;

public record SubmissionProcessingDTO(long exerciseId, long participationId, String commitHash, ZonedDateTime buildStartDate, ZonedDateTime estimatedCompletionDate)
implements Serializable {

@Serial
private static final long serialVersionUID = 1L;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import static de.tum.cit.aet.artemis.core.config.Constants.NEW_SUBMISSION_TOPIC;
import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE;
import static de.tum.cit.aet.artemis.core.config.Constants.PROGRAMMING_SUBMISSION_TOPIC;
import static de.tum.cit.aet.artemis.core.config.Constants.SUBMISSION_PROCESSING;
import static de.tum.cit.aet.artemis.core.config.Constants.SUBMISSION_PROCESSING_TOPIC;
import static de.tum.cit.aet.artemis.core.config.Constants.TEST_CASES_CHANGED_RUN_COMPLETED_NOTIFICATION;

import java.util.Optional;
Expand All @@ -23,13 +25,15 @@
import de.tum.cit.aet.artemis.exercise.domain.participation.Participation;
import de.tum.cit.aet.artemis.exercise.domain.participation.StudentParticipation;
import de.tum.cit.aet.artemis.exercise.dto.SubmissionDTO;
import de.tum.cit.aet.artemis.exercise.repository.ParticipationRepository;
import de.tum.cit.aet.artemis.exercise.repository.TeamRepository;
import de.tum.cit.aet.artemis.lti.service.LtiNewResultService;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseParticipation;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseStudentParticipation;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingSubmission;
import de.tum.cit.aet.artemis.programming.domain.build.BuildRunState;
import de.tum.cit.aet.artemis.programming.dto.SubmissionProcessingDTO;
import de.tum.cit.aet.artemis.programming.exception.BuildTriggerWebsocketError;

@Profile(PROFILE_CORE)
Expand All @@ -48,13 +52,17 @@ public class ProgrammingMessagingService {

private final TeamRepository teamRepository;

private final ParticipationRepository participationRepository;

public ProgrammingMessagingService(GroupNotificationService groupNotificationService, WebsocketMessagingService websocketMessagingService,
ResultWebsocketService resultWebsocketService, Optional<LtiNewResultService> ltiNewResultService, TeamRepository teamRepository) {
ResultWebsocketService resultWebsocketService, Optional<LtiNewResultService> ltiNewResultService, TeamRepository teamRepository,
ParticipationRepository participationRepository) {
this.groupNotificationService = groupNotificationService;
this.websocketMessagingService = websocketMessagingService;
this.resultWebsocketService = resultWebsocketService;
this.ltiNewResultService = ltiNewResultService;
this.teamRepository = teamRepository;
this.participationRepository = participationRepository;
}

public void notifyInstructorAboutStartedExerciseBuildRun(ProgrammingExercise programmingExercise) {
Expand All @@ -76,7 +84,7 @@ public void notifyInstructorAboutCompletedExerciseBuildRun(ProgrammingExercise p
* @param exerciseId used to build the correct topic
*/
public void notifyUserAboutSubmission(ProgrammingSubmission submission, Long exerciseId) {
var submissionDTO = SubmissionDTO.of(submission);
var submissionDTO = SubmissionDTO.of(submission, false, null, null);
if (submission.getParticipation() instanceof StudentParticipation studentParticipation) {
if (studentParticipation.getParticipant() instanceof Team team) {
// eager load the team with students so their information can be used for the messages below
Expand Down Expand Up @@ -148,6 +156,10 @@ private static String getExerciseTopicForTAAndAbove(long exerciseId) {
return EXERCISE_TOPIC_ROOT + exerciseId + PROGRAMMING_SUBMISSION_TOPIC;
}

private static String getSubmissionProcessingTopicForTAAndAbove(Long exerciseId) {
return EXERCISE_TOPIC_ROOT + exerciseId + SUBMISSION_PROCESSING;
}

public static String getProgrammingExerciseTestCaseChangedTopic(Long programmingExerciseId) {
return "/topic/programming-exercises/" + programmingExerciseId + "/test-cases-changed";
}
Expand All @@ -172,4 +184,30 @@ public void notifyUserAboutNewResult(Result result, ProgrammingExerciseParticipa
ltiNewResultService.get().onNewResult(studentParticipation);
}
}

/**
* Notifies the user about the processing of a submission.
* This method sends a notification to the user that their submission is processing
* It handles both student participations and template/solution participations.
*
* @param submission the submission processing data transfer object containing the submission details
* @param exerciseId the ID of the exercise associated with the submission
* @param participationId the ID of the participation associated with the submission
*/
public void notifyUserAboutSubmissionProcessing(SubmissionProcessingDTO submission, long exerciseId, long participationId) {
Participation participation = participationRepository.findWithProgrammingExerciseWithBuildConfigById(participationId).orElseThrow();
if (participation instanceof StudentParticipation studentParticipation) {
if (studentParticipation.getParticipant() instanceof Team team) {
// eager load the team with students so their information can be used for the messages below
studentParticipation.setParticipant(teamRepository.findWithStudentsByIdElseThrow(team.getId()));
}
studentParticipation.getStudents().forEach(user -> websocketMessagingService.sendMessageToUser(user.getLogin(), SUBMISSION_PROCESSING_TOPIC, submission));
}

// send an update to tutors, editors and instructors about submissions for template and solution participations
if (!(participation instanceof StudentParticipation)) {
var topicDestination = getSubmissionProcessingTopicForTAAndAbove(exerciseId);
websocketMessagingService.sendMessage(topicDestination, submission);
}
}
BBesrour marked this conversation as resolved.
Show resolved Hide resolved
}
Loading
Loading