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 48 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 46 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
f23653c
queued ui
BBesrour Oct 29, 2024
8b5c83f
queued server
BBesrour Oct 29, 2024
874f8e5
fix race condition
BBesrour Nov 2, 2024
2cf8f9c
add build queue estimation
BBesrour Nov 4, 2024
b336c47
queue estimation server side
BBesrour Nov 6, 2024
8db4625
estimation
BBesrour Nov 7, 2024
00b4632
wip
BBesrour Nov 12, 2024
fcd0afe
wip
BBesrour Nov 13, 2024
4119523
wip
BBesrour Nov 16, 2024
ef505f2
Merge branch 'develop' into feature/integrated-code-lifecycle/improve…
BBesrour Nov 16, 2024
68f8adb
wip
BBesrour Nov 17, 2024
a3cbf02
client tests
BBesrour Nov 18, 2024
b6c654d
server tests
BBesrour Nov 18, 2024
fa7bc1f
add javadoc
BBesrour Nov 18, 2024
c9fcc6c
wip
BBesrour Nov 18, 2024
06b4e01
remove useless code
BBesrour Nov 18, 2024
5b9abde
feedback
BBesrour Nov 19, 2024
aff0ac4
Merge branch 'develop' into feature/integrated-code-lifecycle/improve…
BBesrour Nov 19, 2024
d7f6912
wip
BBesrour Nov 20, 2024
61b23d6
wip
BBesrour Nov 20, 2024
98b0d73
fix style
BBesrour Nov 21, 2024
ecc0533
tests
BBesrour Nov 22, 2024
3691798
tests
BBesrour Nov 22, 2024
34f4e49
tests
BBesrour Nov 22, 2024
1886a74
Merge branch 'develop' into feature/integrated-code-lifecycle/improve…
BBesrour Nov 22, 2024
911a047
tests
BBesrour Nov 22, 2024
1b798db
tests
BBesrour Nov 22, 2024
d3a8603
wip
BBesrour Nov 24, 2024
d5827a1
wip
BBesrour Nov 24, 2024
d6b44f7
wip
BBesrour Nov 24, 2024
ab1f4ac
wip
BBesrour Nov 24, 2024
cc6118d
wip
BBesrour Nov 24, 2024
c7e15b0
wip
BBesrour Nov 24, 2024
f7d7b5e
wip
BBesrour Nov 24, 2024
c183d9a
wip
BBesrour Nov 24, 2024
45ed233
wip
BBesrour Nov 24, 2024
f653947
wip
BBesrour Nov 25, 2024
126b113
feedback
BBesrour Nov 26, 2024
65db0c7
feedback
BBesrour Nov 26, 2024
bf61b8c
Merge branch 'develop' into feature/integrated-code-lifecycle/improve…
BBesrour Nov 26, 2024
df56636
Merge branch 'develop' into feature/integrated-code-lifecycle/improve…
BBesrour Nov 27, 2024
1c0469b
Merge branch 'develop' into feature/integrated-code-lifecycle/improve…
BBesrour Nov 28, 2024
9a2063f
Merge branch 'develop' into feature/integrated-code-lifecycle/improve…
BBesrour Dec 1, 2024
3037731
merge conflicts
BBesrour Dec 1, 2024
7b86c54
Merge branch 'develop' into feature/integrated-code-lifecycle/improve…
BBesrour Dec 3, 2024
6640a0e
feedback
BBesrour Dec 3, 2024
5ae3f8e
Merge branch 'develop' into feature/integrated-code-lifecycle/improve…
BBesrour Dec 3, 2024
778cdfc
fix
BBesrour Dec 3, 2024
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 @@ -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);
BBesrour marked this conversation as resolved.
Show resolved Hide resolved
}

/**
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 All @@ -51,6 +54,8 @@ public BuildJobQueueItem(BuildJobQueueItem queueItem, ResultDTO 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);
queueItem.repositoryInfo(),
new JobTimingInfo(queueItem.jobTimingInfo.submissionDate(), ZonedDateTime.now(), null, null, queueItem.jobTimingInfo().estimatedDuration()),
queueItem.buildConfig(), null);
}
}
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 @@ -305,7 +305,10 @@ private BuildJobQueueItem addToProcessingJobs() {
if (buildJob != null) {
String hazelcastMemberAddress = hazelcastInstance.getCluster().getLocalMember().getAddress().toString();

BuildJobQueueItem processingJob = new BuildJobQueueItem(buildJob, new BuildAgentDTO(buildAgentShortName, hazelcastMemberAddress, buildAgentDisplayName));
long estimatedDuration = Math.max(0, buildJob.jobTimingInfo().estimatedDuration());
ZonedDateTime estimatedCompletionDate = ZonedDateTime.now().plusSeconds(estimatedDuration);
BuildJobQueueItem processingJob = new BuildJobQueueItem(buildJob, new BuildAgentDTO(buildAgentShortName, hazelcastMemberAddress, buildAgentDisplayName),
estimatedCompletionDate);

processingJobs.put(processingJob.id(), processingJob);
localProcessingJobs.incrementAndGet();
Expand Down Expand Up @@ -403,7 +406,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
@@ -0,0 +1,57 @@
package de.tum.cit.aet.artemis.programming.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;

import com.fasterxml.jackson.annotation.JsonInclude;

import de.tum.cit.aet.artemis.core.domain.DomainObject;

@Entity
@Table(name = "programming_exercise_build_statistics")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class ProgrammingExerciseBuildStatistics extends DomainObject {
BBesrour marked this conversation as resolved.
Show resolved Hide resolved

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

@Column(name = "build_count_when_updated")
private long buildCountWhenUpdated = 0;

@Column(name = "exercise_id")
private Long exerciseId;
BBesrour marked this conversation as resolved.
Show resolved Hide resolved

public ProgrammingExerciseBuildStatistics() {
}

public ProgrammingExerciseBuildStatistics(Long exerciseId, long buildDurationSeconds, long buildCountWhenUpdated) {
this.buildDurationSeconds = buildDurationSeconds;
this.buildCountWhenUpdated = buildCountWhenUpdated;
this.exerciseId = exerciseId;
}

public long getBuildDurationSeconds() {
return buildDurationSeconds;
}

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

public long getBuildCountWhenUpdated() {
return buildCountWhenUpdated;
}

public void setBuildCountWhenUpdated(long buildCountWhenUpdated) {
this.buildCountWhenUpdated = buildCountWhenUpdated;
}

public Long getExerciseId() {
return exerciseId;
}

public void setExerciseId(Long exerciseId) {
this.exerciseId = exerciseId;
}
}
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 submissionDate, 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 @@ -104,4 +104,20 @@ SELECT COUNT(b)
WHERE e.id IN :exerciseIds
""")
long countBuildJobsByExerciseIds(@Param("exerciseIds") List<Long> exerciseIds);

@Query("""
SELECT b
FROM BuildJob b
WHERE b.exerciseId = :exerciseId AND b.buildStatus = 'SUCCESSFUL'
ORDER BY b.buildStartDate DESC
LIMIT :limit
""")
List<BuildJob> fetchSuccessfulBuildJobsByExerciseIdWithLimit(@Param("exerciseId") Long exerciseId, @Param("limit") int limit);
BBesrour marked this conversation as resolved.
Show resolved Hide resolved

@Query("""
SELECT COUNT(b)
FROM BuildJob b
WHERE b.exerciseId = :exerciseId AND b.buildStatus = 'SUCCESSFUL'
""")
long fetchSuccessfulBuildJobCountByExerciseId(@Param("exerciseId") Long exerciseId);
BBesrour marked this conversation as resolved.
Show resolved Hide resolved
BBesrour marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package de.tum.cit.aet.artemis.programming.repository;

import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE;

import java.util.Optional;

import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Repository;

import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseBuildStatistics;

@Profile(PROFILE_CORE)
@Repository
public interface ProgrammingExerciseBuildStatisticsRepository extends ArtemisJpaRepository<ProgrammingExerciseBuildStatistics, Long> {

Optional<ProgrammingExerciseBuildStatistics> findByExerciseId(Long exerciseId);
}
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)) {
String topicDestination = getSubmissionProcessingTopicForTAAndAbove(exerciseId);
websocketMessagingService.sendMessage(topicDestination, submission);
}
}
}
Loading
Loading