Skip to content

Commit

Permalink
Merge branch 'develop' into feature/course-details-show-llm-usage
Browse files Browse the repository at this point in the history
  • Loading branch information
wasnertobias authored Dec 17, 2024
2 parents a89db19 + 69f72d0 commit 8bd072a
Show file tree
Hide file tree
Showing 48 changed files with 1,564 additions and 76 deletions.
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);
}

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 @@ -66,6 +66,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);
}

}
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 {

@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;

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);

@Query("""
SELECT COUNT(b)
FROM BuildJob b
WHERE b.exerciseId = :exerciseId AND b.buildStatus = 'SUCCESSFUL'
""")
long fetchSuccessfulBuildJobCountByExerciseId(@Param("exerciseId") Long exerciseId);
}
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 @@ -24,6 +26,7 @@
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.iris.service.pyris.PyrisEventService;
import de.tum.cit.aet.artemis.iris.service.pyris.event.NewResultEvent;
Expand All @@ -33,6 +36,7 @@
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 @@ -53,21 +57,28 @@ public class ProgrammingMessagingService {

private final Optional<PyrisEventService> pyrisEventService;

private final ParticipationRepository participationRepository;

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

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 Down Expand Up @@ -95,7 +106,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 @@ -204,4 +215,30 @@ private void notifyIrisAboutSubmissionStatus(Result result, ProgrammingExerciseS
});
}
}

/**
* 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) {
// Eagerly 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

0 comments on commit 8bd072a

Please sign in to comment.