use of de.tum.in.www1.artemis.domain.quiz.QuizBatch in project ArTEMiS by ls1intum.
the class QuizScheduleService method processCachedQuizSubmissions.
/**
* // @formatter:off
* 1. Check cached submissions for new submissions with “isSubmitted() == true”
* a. Process each Submission (set submissionType to “SubmissionType.MANUAL”) and create Participation and Result and save them to Database (DB WRITE)
* b. Remove processed Submissions from SubmissionHashMap and write Participation with Result into ParticipationHashMap and write Result into ResultHashMap
* 2. If Quiz has ended:
* a. Process all cached Submissions that belong to this quiz i. set “isSubmitted” to “true” and submissionType to “SubmissionType.TIMEOUT”
* ii. Create Participation and Result and save to Database (DB WRITE)
* iii. Remove processed Submissions from cache and write the Participations with Result and the Results into the cache
* b. Send out cached Participations (including QuizExercise and Result) from to each participant and remove them from the cache (WEBSOCKET SEND)
* 3. Update Statistics with Results from ResultHashMap (DB READ and DB WRITE) and remove from cache
* 4. Send out new Statistics to instructors (WEBSOCKET SEND)
*/
public void processCachedQuizSubmissions() {
log.debug("Process cached quiz submissions");
// global try-catch for error logging
try {
for (QuizExerciseCache cachedQuiz : quizCache.getAllQuizExerciseCaches()) {
// this way near cache is used (values will deserialize new objects)
Long quizExerciseId = cachedQuiz.getExerciseId();
// Get fresh QuizExercise from DB
QuizExercise quizExercise = quizExerciseRepository.findOne(quizExerciseId);
// check if quiz has been deleted
if (quizExercise == null) {
log.debug("Remove quiz {} from resultHashMap", quizExerciseId);
quizCache.removeAndClear(quizExerciseId);
continue;
}
// Update cached exercise object (use the expensive operation upfront)
quizExercise = quizExerciseRepository.findByIdWithQuestionsAndStatisticsElseThrow(quizExerciseId);
Map<Long, QuizBatch> batchCache = quizExercise.getQuizBatches().stream().collect(Collectors.toUnmodifiableMap(QuizBatch::getId, batch -> batch));
// this is required to ensure that students cannot gain extra attempts this way
for (var batch : cachedQuiz.getBatches().entrySet()) {
if (batchCache.get(batch.getValue()).isEnded()) {
cachedQuiz.getSubmissions().putIfAbsent(batch.getKey(), new QuizSubmission());
}
}
// (Boolean wrapper is safe to auto-unbox here)
boolean hasEnded = quizExercise.isQuizEnded();
// Note that those might not be true later on due to concurrency and a distributed system,
// do not rely on that for actions upon the whole set, such as clear()
boolean hasNewSubmissions = !cachedQuiz.getSubmissions().isEmpty();
boolean hasNewParticipations = !cachedQuiz.getParticipations().isEmpty();
boolean hasNewResults = !cachedQuiz.getResults().isEmpty();
// Skip quizzes with no cached changes
if (!hasNewSubmissions && !hasNewParticipations && !hasNewResults) {
// Remove quiz if it has ended
if (hasEnded) {
removeCachedQuiz(cachedQuiz);
}
continue;
}
// Save cached Submissions (this will also generate results and participations and place them in the cache)
long start = System.nanoTime();
if (hasNewSubmissions) {
// Create Participations and Results if the submission was submitted or if the quiz has ended and save them to Database (DB Write)
Map<String, QuizSubmission> submissions = cachedQuiz.getSubmissions();
Map<String, Long> batches = cachedQuiz.getBatches();
// This call will remove the processed Submission map entries itself
int numberOfSubmittedSubmissions = saveQuizSubmissionWithParticipationAndResultToDatabase(quizExercise, submissions, batches, batchCache);
// .. and likely generate new participations and results
if (numberOfSubmittedSubmissions > 0) {
// .. so we set the boolean variables here again if some were submitted
hasNewParticipations = true;
hasNewResults = true;
log.info("Saved {} submissions to database in {} in quiz {}", numberOfSubmittedSubmissions, formatDurationFrom(start), quizExercise.getTitle());
}
}
// Send out Participations from ParticipationHashMap to each user if the quiz has ended
start = System.nanoTime();
if (hasNewParticipations && hasEnded) {
// Send the participation with containing result and quiz back to the users via websocket and remove the participation from the ParticipationHashMap
Collection<Entry<String, StudentParticipation>> finishedParticipations = cachedQuiz.getParticipations().entrySet();
// TODO maybe find a better way to optimize the performance (use an executor service with e.g. X parallel threads)
finishedParticipations.parallelStream().forEach(entry -> {
StudentParticipation participation = entry.getValue();
if (participation.getParticipant() == null || participation.getParticipantIdentifier() == null) {
log.error("Participation is missing student (or student is missing username): {}", participation);
} else {
sendQuizResultToUser(quizExerciseId, participation);
cachedQuiz.getParticipations().remove(entry.getKey());
}
});
if (!finishedParticipations.isEmpty()) {
log.info("Sent out {} participations in {} for quiz {}", finishedParticipations.size(), formatDurationFrom(start), quizExercise.getTitle());
}
}
// Update Statistics with Results (DB Read and DB Write) and remove the results from the cache
start = System.nanoTime();
if (hasNewResults) {
// Fetch a new quiz exercise here including deeper attribute paths (this is relatively expensive, so we only do that if necessary)
try {
// Get a Set because QuizStatisticService needs one (currently)
Set<Result> newResultsForQuiz = Set.copyOf(cachedQuiz.getResults().values());
// Update the statistics
quizStatisticService.updateStatistics(newResultsForQuiz, quizExercise);
log.info("Updated statistics with {} new results in {} for quiz {}", newResultsForQuiz.size(), formatDurationFrom(start), quizExercise.getTitle());
// Remove only processed results
for (Result result : newResultsForQuiz) {
cachedQuiz.getResults().remove(result.getId());
}
} catch (Exception e) {
log.error("Exception in StatisticService.updateStatistics(): {}", e.getMessage(), e);
}
}
}
} catch (Exception e) {
log.error("Exception in Quiz Schedule: {}", e.getMessage(), e);
}
}
use of de.tum.in.www1.artemis.domain.quiz.QuizBatch in project Artemis by ls1intum.
the class QuizScheduleService method processCachedQuizSubmissions.
/**
* // @formatter:off
* 1. Check cached submissions for new submissions with “isSubmitted() == true”
* a. Process each Submission (set submissionType to “SubmissionType.MANUAL”) and create Participation and Result and save them to Database (DB WRITE)
* b. Remove processed Submissions from SubmissionHashMap and write Participation with Result into ParticipationHashMap and write Result into ResultHashMap
* 2. If Quiz has ended:
* a. Process all cached Submissions that belong to this quiz i. set “isSubmitted” to “true” and submissionType to “SubmissionType.TIMEOUT”
* ii. Create Participation and Result and save to Database (DB WRITE)
* iii. Remove processed Submissions from cache and write the Participations with Result and the Results into the cache
* b. Send out cached Participations (including QuizExercise and Result) from to each participant and remove them from the cache (WEBSOCKET SEND)
* 3. Update Statistics with Results from ResultHashMap (DB READ and DB WRITE) and remove from cache
* 4. Send out new Statistics to instructors (WEBSOCKET SEND)
*/
public void processCachedQuizSubmissions() {
log.debug("Process cached quiz submissions");
// global try-catch for error logging
try {
for (Cache cache : quizCache.getAllCaches()) {
QuizExerciseCache cachedQuiz = (QuizExerciseCache) cache;
// this way near cache is used (values will deserialize new objects)
Long quizExerciseId = cachedQuiz.getExerciseId();
// Get fresh QuizExercise from DB
QuizExercise quizExercise = quizExerciseRepository.findOne(quizExerciseId);
// check if quiz has been deleted
if (quizExercise == null) {
log.debug("Remove quiz {} from resultHashMap", quizExerciseId);
quizCache.removeAndClear(quizExerciseId);
continue;
}
// Update cached exercise object (use the expensive operation upfront)
quizExercise = quizExerciseRepository.findByIdWithQuestionsAndStatisticsElseThrow(quizExerciseId);
Map<Long, QuizBatch> batchCache = quizExercise.getQuizBatches().stream().collect(Collectors.toUnmodifiableMap(QuizBatch::getId, batch -> batch));
// this is required to ensure that students cannot gain extra attempts this way
for (var batch : cachedQuiz.getBatches().entrySet()) {
if (batchCache.get(batch.getValue()).isEnded()) {
cachedQuiz.getSubmissions().putIfAbsent(batch.getKey(), new QuizSubmission());
}
}
// (Boolean wrapper is safe to auto-unbox here)
boolean hasEnded = quizExercise.isQuizEnded();
// Note that those might not be true later on due to concurrency and a distributed system,
// do not rely on that for actions upon the whole set, such as clear()
boolean hasNewSubmissions = !cachedQuiz.getSubmissions().isEmpty();
boolean hasNewParticipations = !cachedQuiz.getParticipations().isEmpty();
boolean hasNewResults = !cachedQuiz.getResults().isEmpty();
// Skip quizzes with no cached changes
if (!hasNewSubmissions && !hasNewParticipations && !hasNewResults) {
// Remove quiz if it has ended
if (hasEnded) {
removeCachedQuiz(cachedQuiz);
}
continue;
}
// Save cached Submissions (this will also generate results and participations and place them in the cache)
long start = System.nanoTime();
if (hasNewSubmissions) {
// Create Participations and Results if the submission was submitted or if the quiz has ended and save them to Database (DB Write)
Map<String, QuizSubmission> submissions = cachedQuiz.getSubmissions();
Map<String, Long> batches = cachedQuiz.getBatches();
// This call will remove the processed Submission map entries itself
int numberOfSubmittedSubmissions = saveQuizSubmissionWithParticipationAndResultToDatabase(quizExercise, submissions, batches, batchCache);
// .. and likely generate new participations and results
if (numberOfSubmittedSubmissions > 0) {
// .. so we set the boolean variables here again if some were submitted
hasNewParticipations = true;
hasNewResults = true;
log.info("Saved {} submissions to database in {} in quiz {}", numberOfSubmittedSubmissions, formatDurationFrom(start), quizExercise.getTitle());
}
}
// Send out Participations from ParticipationHashMap to each user if the quiz has ended
start = System.nanoTime();
if (hasNewParticipations && hasEnded) {
// Send the participation with containing result and quiz back to the users via websocket and remove the participation from the ParticipationHashMap
Collection<Entry<String, StudentParticipation>> finishedParticipations = cachedQuiz.getParticipations().entrySet();
// TODO maybe find a better way to optimize the performance (use an executor service with e.g. X parallel threads)
finishedParticipations.parallelStream().forEach(entry -> {
StudentParticipation participation = entry.getValue();
if (participation.getParticipant() == null || participation.getParticipantIdentifier() == null) {
log.error("Participation is missing student (or student is missing username): {}", participation);
} else {
sendQuizResultToUser(quizExerciseId, participation);
cachedQuiz.getParticipations().remove(entry.getKey());
}
});
if (!finishedParticipations.isEmpty()) {
log.info("Sent out {} participations in {} for quiz {}", finishedParticipations.size(), formatDurationFrom(start), quizExercise.getTitle());
}
}
// Update Statistics with Results (DB Read and DB Write) and remove the results from the cache
start = System.nanoTime();
if (hasNewResults) {
// Fetch a new quiz exercise here including deeper attribute paths (this is relatively expensive, so we only do that if necessary)
try {
// Get a Set because QuizStatisticService needs one (currently)
Set<Result> newResultsForQuiz = Set.copyOf(cachedQuiz.getResults().values());
// Update the statistics
quizStatisticService.updateStatistics(newResultsForQuiz, quizExercise);
log.info("Updated statistics with {} new results in {} for quiz {}", newResultsForQuiz.size(), formatDurationFrom(start), quizExercise.getTitle());
// Remove only processed results
for (Result result : newResultsForQuiz) {
cachedQuiz.getResults().remove(result.getId());
}
} catch (Exception e) {
log.error("Exception in StatisticService.updateStatistics(): {}", e.getMessage(), e);
}
}
}
} catch (Exception e) {
log.error("Exception in Quiz Schedule: {}", e.getMessage(), e);
}
}
use of de.tum.in.www1.artemis.domain.quiz.QuizBatch in project Artemis by ls1intum.
the class QuizScheduleService method saveQuizSubmissionWithParticipationAndResultToDatabase.
/**
* check if the user submitted the submission or if the quiz has ended: if true: -> Create Participation and Result and save to Database (DB Write) Remove processed Submissions
* from SubmissionHashMap and write Participations with Result into ParticipationHashMap and Results into ResultHashMap
*
* @param quizExercise the quiz which should be checked
* @param userSubmissionMap a Map with all submissions for the given quizExercise mapped by the username
* @param userBatchMap a Map of the username to quiz batch id for the given quizExercise
* @param batchCache a Map of all the batches for the given quizExercise
* @return the number of processed submissions (submit or timeout)
*/
private int saveQuizSubmissionWithParticipationAndResultToDatabase(@NotNull QuizExercise quizExercise, Map<String, QuizSubmission> userSubmissionMap, Map<String, Long> userBatchMap, Map<Long, QuizBatch> batchCache) {
int count = 0;
for (String username : userSubmissionMap.keySet()) {
try {
// first case: the user submitted the quizSubmission
QuizSubmission quizSubmission = userSubmissionMap.get(username);
QuizBatch quizBatch = batchCache.get(userBatchMap.getOrDefault(username, 0L));
if (quizSubmission.isSubmitted()) {
if (quizSubmission.getType() == null) {
quizSubmission.setType(SubmissionType.MANUAL);
}
} else // second case: the quiz or batch has ended
if (quizExercise.isQuizEnded() || quizBatch != null && quizBatch.isEnded()) {
quizSubmission.setSubmitted(true);
quizSubmission.setType(SubmissionType.TIMEOUT);
quizSubmission.setSubmissionDate(ZonedDateTime.now());
} else {
// the quiz is running and the submission was not yet submitted.
continue;
}
if (quizBatch != null) {
// record which batch the submission belongs to
quizSubmission.setQuizBatch(quizBatch.getId());
}
count++;
// Create Participation and Result and save to Database (DB Write)
// Remove processed Submissions from SubmissionHashMap and write Participations with Result into ParticipationHashMap and Results into ResultHashMap
StudentParticipation participation = new StudentParticipation();
// TODO: when this is set earlier for the individual quiz start of a student, we don't need to set this here anymore
participation.setInitializationDate(quizSubmission.getSubmissionDate());
Optional<User> user = userRepository.findOneByLogin(username);
user.ifPresent(participation::setParticipant);
// add the quizExercise to the participation
participation.setExercise(quizExercise);
participation.setInitializationState(InitializationState.FINISHED);
// create new result
Result result = new Result().participation(participation);
result.setRated(true);
result.setAssessmentType(AssessmentType.AUTOMATIC);
result.setCompletionDate(quizSubmission.getSubmissionDate());
result.setSubmission(quizSubmission);
// calculate scores and update result and submission accordingly
quizSubmission.calculateAndUpdateScores(quizExercise);
result.evaluateQuizSubmission();
// add result to participation
participation.addResult(result);
// add submission to participation
participation.setSubmissions(Set.of(quizSubmission));
// NOTE: we save (1) participation and (2) submission (in this particular order) here individually so that one exception (e.g. duplicated key) cannot
// destroy multiple student answers
participation = studentParticipationRepository.save(participation);
quizSubmission.addResult(result);
quizSubmission.setParticipation(participation);
// this automatically saves the results due to CascadeType.ALL
quizSubmission = quizSubmissionRepository.save(quizSubmission);
log.info("Successfully saved submission in quiz " + quizExercise.getTitle() + " for user " + username);
// reconnect entities after save
participation.setSubmissions(Set.of(quizSubmission));
participation.setResults(Set.of(result));
result.setSubmission(quizSubmission);
result.setParticipation(participation);
// no point in keeping the participation around for non-synchronized modes where the due date may only be in a week
if (quizExercise.getQuizMode() == QuizMode.SYNCHRONIZED) {
// add the participation to the participationHashMap for the send out at the end of the quiz
addParticipation(quizExercise.getId(), participation);
}
// remove the submission only after the participation has been added to the participation hashmap to avoid duplicated key exceptions for multiple participations for
// the same user
userSubmissionMap.remove(username);
// clean up the batch association
userBatchMap.remove(username);
// add the result of the participation resultHashMap for the statistic-Update
addResultForStatisticUpdate(quizExercise.getId(), result);
} catch (ConstraintViolationException constraintViolationException) {
log.error("ConstraintViolationException in saveQuizSubmissionWithParticipationAndResultToDatabase() for user {} in quiz {}: {}", username, quizExercise.getId(), constraintViolationException.getMessage(), constraintViolationException);
// We got a ConstraintViolationException -> The "User-Quiz" pair is already saved in the database, but for some reason was not removed from the maps
// We remove it from the maps now to prevent this error from occurring again
// We do NOT add it to the participation map, as this should have been done already earlier (when the entry was added to the database)
userSubmissionMap.remove(username);
// clean up the batch association
userBatchMap.remove(username);
} catch (Exception e) {
log.error("Exception in saveQuizSubmissionWithParticipationAndResultToDatabase() for user {} in quiz {}: {}", username, quizExercise.getId(), e.getMessage(), e);
}
}
return count;
}
use of de.tum.in.www1.artemis.domain.quiz.QuizBatch in project ArTEMiS by ls1intum.
the class QuizScheduleService method saveQuizSubmissionWithParticipationAndResultToDatabase.
/**
* check if the user submitted the submission or if the quiz has ended: if true: -> Create Participation and Result and save to Database (DB Write) Remove processed Submissions
* from SubmissionHashMap and write Participations with Result into ParticipationHashMap and Results into ResultHashMap
*
* @param quizExercise the quiz which should be checked
* @param userSubmissionMap a Map with all submissions for the given quizExercise mapped by the username
* @param userBatchMap a Map of the username to quiz batch id for the given quizExercise
* @param batchCache a Map of all the batches for the given quizExercise
* @return the number of processed submissions (submit or timeout)
*/
private int saveQuizSubmissionWithParticipationAndResultToDatabase(@NotNull QuizExercise quizExercise, Map<String, QuizSubmission> userSubmissionMap, Map<String, Long> userBatchMap, Map<Long, QuizBatch> batchCache) {
int count = 0;
for (String username : userSubmissionMap.keySet()) {
try {
// first case: the user submitted the quizSubmission
QuizSubmission quizSubmission = userSubmissionMap.get(username);
QuizBatch quizBatch = batchCache.get(userBatchMap.getOrDefault(username, 0L));
if (quizSubmission.isSubmitted()) {
if (quizSubmission.getType() == null) {
quizSubmission.setType(SubmissionType.MANUAL);
}
} else // second case: the quiz or batch has ended
if (quizExercise.isQuizEnded() || quizBatch != null && quizBatch.isEnded()) {
quizSubmission.setSubmitted(true);
quizSubmission.setType(SubmissionType.TIMEOUT);
quizSubmission.setSubmissionDate(ZonedDateTime.now());
} else {
// the quiz is running and the submission was not yet submitted.
continue;
}
if (quizBatch != null) {
// record which batch the submission belongs to
quizSubmission.setQuizBatch(quizBatch.getId());
}
count++;
// Create Participation and Result and save to Database (DB Write)
// Remove processed Submissions from SubmissionHashMap and write Participations with Result into ParticipationHashMap and Results into ResultHashMap
StudentParticipation participation = new StudentParticipation();
// TODO: when this is set earlier for the individual quiz start of a student, we don't need to set this here anymore
participation.setInitializationDate(quizSubmission.getSubmissionDate());
Optional<User> user = userRepository.findOneByLogin(username);
user.ifPresent(participation::setParticipant);
// add the quizExercise to the participation
participation.setExercise(quizExercise);
participation.setInitializationState(InitializationState.FINISHED);
// create new result
Result result = new Result().participation(participation);
result.setRated(true);
result.setAssessmentType(AssessmentType.AUTOMATIC);
result.setCompletionDate(quizSubmission.getSubmissionDate());
result.setSubmission(quizSubmission);
// calculate scores and update result and submission accordingly
quizSubmission.calculateAndUpdateScores(quizExercise);
result.evaluateQuizSubmission();
// add result to participation
participation.addResult(result);
// add submission to participation
participation.setSubmissions(Set.of(quizSubmission));
// NOTE: we save (1) participation and (2) submission (in this particular order) here individually so that one exception (e.g. duplicated key) cannot
// destroy multiple student answers
participation = studentParticipationRepository.save(participation);
quizSubmission.addResult(result);
quizSubmission.setParticipation(participation);
// this automatically saves the results due to CascadeType.ALL
quizSubmission = quizSubmissionRepository.save(quizSubmission);
log.info("Successfully saved submission in quiz " + quizExercise.getTitle() + " for user " + username);
// reconnect entities after save
participation.setSubmissions(Set.of(quizSubmission));
participation.setResults(Set.of(result));
result.setSubmission(quizSubmission);
result.setParticipation(participation);
// no point in keeping the participation around for non-synchronized modes where the due date may only be in a week
if (quizExercise.getQuizMode() == QuizMode.SYNCHRONIZED) {
// add the participation to the participationHashMap for the send out at the end of the quiz
addParticipation(quizExercise.getId(), participation);
}
// remove the submission only after the participation has been added to the participation hashmap to avoid duplicated key exceptions for multiple participations for
// the same user
userSubmissionMap.remove(username);
// clean up the batch association
userBatchMap.remove(username);
// add the result of the participation resultHashMap for the statistic-Update
addResultForStatisticUpdate(quizExercise.getId(), result);
} catch (ConstraintViolationException constraintViolationException) {
log.error("ConstraintViolationException in saveQuizSubmissionWithParticipationAndResultToDatabase() for user {} in quiz {}: {}", username, quizExercise.getId(), constraintViolationException.getMessage(), constraintViolationException);
// We got a ConstraintViolationException -> The "User-Quiz" pair is already saved in the database, but for some reason was not removed from the maps
// We remove it from the maps now to prevent this error from occurring again
// We do NOT add it to the participation map, as this should have been done already earlier (when the entry was added to the database)
userSubmissionMap.remove(username);
// clean up the batch association
userBatchMap.remove(username);
} catch (Exception e) {
log.error("Exception in saveQuizSubmissionWithParticipationAndResultToDatabase() for user {} in quiz {}: {}", username, quizExercise.getId(), e.getMessage(), e);
}
}
return count;
}
use of de.tum.in.www1.artemis.domain.quiz.QuizBatch in project ArTEMiS by ls1intum.
the class QuizScheduleService method executeQuizStartNowTask.
/**
* Internal method to start and send the {@link QuizExercise} to the clients when called
*/
void executeQuizStartNowTask(Long quizExerciseId) {
quizCache.performCacheWriteIfPresent(quizExerciseId, quizExerciseCache -> {
quizExerciseCache.getQuizStart().clear();
log.debug("Removed quiz {} start tasks", quizExerciseId);
return quizExerciseCache;
});
log.debug("Sending quiz {} start", quizExerciseId);
QuizExercise quizExercise = quizExerciseRepository.findByIdWithQuestionsAndStatisticsElseThrow(quizExerciseId);
updateQuizExercise(quizExercise);
if (quizExercise.getQuizMode() != QuizMode.SYNCHRONIZED) {
throw new IllegalStateException();
}
// TODO: quiz cleanup: We create a batch that has just started here because we can't access QuizBatchService here because of dependencies
var quizBatch = new QuizBatch();
quizBatch.setQuizExercise(quizExercise);
quizBatch.setStartTime(ZonedDateTime.now());
quizExercise.setQuizBatches(Set.of(quizBatch));
quizMessagingService.sendQuizExerciseToSubscribedClients(quizExercise, quizBatch, "start-now");
}
Aggregations