Skip to content

Conversation

@Elfari1028
Copy link
Contributor

@Elfari1028 Elfari1028 commented Sep 1, 2025

Checklist

General

There are no UI changes in the PR, existing logic does not get interrupted

Server

  • Important: I implemented the changes with a very good performance and prevented too many (unnecessary) and too complex database calls.
  • I strictly followed the principle of data economy for all database calls.
  • I strictly followed the server coding and design guidelines.
  • I added multiple integration tests (Spring) related to the features (with a high test coverage).
  • I documented the Java code using JavaDoc style.

Motivation and Context

#10967
This change sets up exercise creation/updates to be automatically captured for maintaining a versioning log. This will enable future features such as exercise rollbacks, and iterative reviews on exercises (problem statement + code).

Description

There are no UI changes in the PR, existing logic does not get interrupted
an EntityListener is set up for Exercise and a few related entities.

  • ExerciseVersion table is added to keep track of versioning.
  • On persist/update/deletion, or a new push to a repository an ExerciseChangedEvent will be published to ApplicationContext. ExerciseVersionService, upon receiving the event, will add it to a queue and batch process it every second.
  • createNewExerciseVersion will fetch the exercise eagerly with fields used in versioning, creates a snapshot DTO from the exercise, compares with last version's snapshot (if exists), then saves ExerciseVersion to database.

Steps for Testing

Prerequisites:

  • 1 Instructor
  • Local environment
  1. Log in to Artemis
  2. Navigate to Course Managment
  3. Create an exercise/update an existing exercise (any of the 5 types)
  4. Check that there are no errors.
  5. Check in database whether an exercise version is created.

Testserver States

You can manage test servers using Helios. Check environment statuses in the environment list. To deploy to a test server, go to the CI/CD page, find your PR or branch, and trigger the deployment.

Review Progress

Performance Review

  • I (as a reviewer) confirm that the server changes (in particular related to database calls) are implemented with a very good performance even for very large courses with more than 2000 students.

Code Review

  • Code Review 1
  • Code Review 2

Test Coverage

Extensive assertions for server tests are implemented

Summary by CodeRabbit

  • New Features

    • Automatic exercise versioning for Programming, Text, Modeling, Quiz, and File Upload exercises with snapshotting of core configuration and repository commit references.
    • Versioning now triggers on relevant entity changes and on specific repository pushes for timely traceability.
    • Repository scanning updated to include versioning components.
  • Bug Fixes

    • More reliable fallback to default build configuration in local CI when windfile lookup fails.
  • Improvements

    • Enhanced access checks and event publishing around repository operations.
  • Tests

    • Extensive integration and architecture tests added for versioning behavior.

@github-project-automation github-project-automation bot moved this to Work In Progress in Artemis Development Sep 1, 2025
@github-actions github-actions bot added tests server Pull requests that update Java code. (Added Automatically!) database Pull requests that update the database. (Added Automatically!). Require a CRITICAL deployment. exercise Pull requests that affect the corresponding module fileupload Pull requests that affect the corresponding module modeling Pull requests that affect the corresponding module programming Pull requests that affect the corresponding module quiz Pull requests that affect the corresponding module text Pull requests that affect the corresponding module labels Sep 1, 2025
@Elfari1028 Elfari1028 changed the title Feature/exercise versioning/create new exercise snapshot during hibernate post flush Programming exercise: Create a ExerciseVersion on Exercise creation/updates on new flush to database. (all exercise types) Sep 1, 2025
@github-actions
Copy link

github-actions bot commented Sep 1, 2025

End-to-End (E2E) Test Results Summary

TestsPassed ✅SkippedFailedTime ⏱
End-to-End (E2E) Test Report1 ran1 passed0 skipped0 failed1s 487ms
TestResultTime ⏱
No test annotations available

@github-actions
Copy link

github-actions bot commented Sep 1, 2025

End-to-End (E2E) Test Results Summary

TestsPassed ✅SkippedFailedTime ⏱
End-to-End (E2E) Test Report1 ran1 passed0 skipped0 failed1s 580ms
TestResultTime ⏱
No test annotations available

bensofficial
bensofficial previously approved these changes Sep 1, 2025
Copy link
Member

@bensofficial bensofficial left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DB migration and new exercise_version table looks good

@github-actions
Copy link

github-actions bot commented Sep 9, 2025

There hasn't been any activity on this pull request recently. Therefore, this pull request has been automatically marked as stale and will be closed if no further activity occurs within seven days. Thank you for your contributions.

@github-actions github-actions bot added the stale label Sep 9, 2025
@github-actions
Copy link

github-actions bot commented Sep 9, 2025

End-to-End (E2E) Test Results Summary

TestsPassed ✅SkippedFailedTime ⏱
End-to-End (E2E) Test Report1 ran1 passed0 skipped0 failed1s 581ms
TestResultTime ⏱
No test annotations available

@github-actions
Copy link

github-actions bot commented Sep 9, 2025

End-to-End (E2E) Test Results Summary

TestsPassed ✅SkippedFailedTime ⏱
End-to-End (E2E) Test Report1 ran1 passed0 skipped0 failed1s 463ms
TestResultTime ⏱
No test annotations available

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/main/java/de/tum/cit/aet/artemis/versioning/config/ExerciseVersioningEnabled.java (1)

13-19: LGTM with optional optimization.

The implementation correctly instantiates ArtemisConfigHelper in the constructor. While this doesn't follow typical dependency injection patterns, it's appropriate for Spring Condition classes, which are evaluated before the full application context is available.

Optional optimization: Since ArtemisConfigHelper appears stateless, consider making the field static to avoid creating multiple instances if this condition is evaluated multiple times:

-    private final ArtemisConfigHelper artemisConfigHelper;
+    private static final ArtemisConfigHelper ARTEMIS_CONFIG_HELPER = new ArtemisConfigHelper();

     public ExerciseVersioningEnabled() {
-        this.artemisConfigHelper = new ArtemisConfigHelper();
     }

Then update the matches method to use the static reference:

     @Override
     public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
-        return artemisConfigHelper.isExerciseVersioningEnabled(context.getEnvironment());
+        return ARTEMIS_CONFIG_HELPER.isExerciseVersioningEnabled(context.getEnvironment());
     }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 74d2427 and 2232e1c.

📒 Files selected for processing (1)
  • src/main/java/de/tum/cit/aet/artemis/versioning/config/ExerciseVersioningEnabled.java (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
src/main/java/**/*.java

⚙️ CodeRabbit configuration file

naming:CamelCase; principles:{single_responsibility,small_methods,no_duplication}; db:{perf_queries,datetime_not_timestamp}; rest:{stateless,singleton,delegate_logic,http_only,minimal_dtos}; dtos:{java_records,no_entities,min_data,single_resp}; di:constructor_injection; kiss:simple_code; file_handling:os_indep_paths; practices:{least_access,avoid_transactions,code_reuse,static_member_ref,prefer_primitives}; sql:{param_annotation,uppercase,avoid_subqueries};java:avoid_star_imports

Files:

  • src/main/java/de/tum/cit/aet/artemis/versioning/config/ExerciseVersioningEnabled.java
🧬 Code graph analysis (1)
src/main/java/de/tum/cit/aet/artemis/versioning/config/ExerciseVersioningEnabled.java (1)
src/main/java/de/tum/cit/aet/artemis/core/config/ArtemisConfigHelper.java (1)
  • ArtemisConfigHelper (16-116)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Build and Push Docker Image / Build linux/amd64 Docker Image for ls1intum/artemis
  • GitHub Check: Build and Push Docker Image / Build linux/arm64 Docker Image for ls1intum/artemis
  • GitHub Check: Build .war artifact
  • GitHub Check: client-tests
  • GitHub Check: client-style
  • GitHub Check: server-tests
  • GitHub Check: Analyse
  • GitHub Check: bean-instantiation-check
🔇 Additional comments (2)
src/main/java/de/tum/cit/aet/artemis/versioning/config/ExerciseVersioningEnabled.java (2)

1-7: LGTM!

Package and imports are correctly structured with no wildcard imports.


21-24: LGTM!

The matches method correctly delegates to ArtemisConfigHelper and properly retrieves the Environment from the ConditionContext.

coderabbitai[bot]
coderabbitai bot previously approved these changes Oct 8, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/main/java/de/tum/cit/aet/artemis/versioning/service/ExerciseVersionEntityListener.java (1)

45-51: Consider documenting the Exercise exclusion logic.

The early return for Exercise instances in handlePostRemove appears intentional (likely to avoid versioning exercise deletions), but this logic would benefit from a brief comment explaining the rationale.

Example:

 @PostRemove
 public void handlePostRemove(Object entity) {
+    // Exercise deletions are not versioned; only changes to related entities trigger versions
     if (entity instanceof Exercise) {
         return;
     }
     handleEntityChange(entity);
 }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2232e1c and 171b8b6.

📒 Files selected for processing (1)
  • src/main/java/de/tum/cit/aet/artemis/versioning/service/ExerciseVersionEntityListener.java (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
src/main/java/**/*.java

⚙️ CodeRabbit configuration file

naming:CamelCase; principles:{single_responsibility,small_methods,no_duplication}; db:{perf_queries,datetime_not_timestamp}; rest:{stateless,singleton,delegate_logic,http_only,minimal_dtos}; dtos:{java_records,no_entities,min_data,single_resp}; di:constructor_injection; kiss:simple_code; file_handling:os_indep_paths; practices:{least_access,avoid_transactions,code_reuse,static_member_ref,prefer_primitives}; sql:{param_annotation,uppercase,avoid_subqueries};java:avoid_star_imports

Files:

  • src/main/java/de/tum/cit/aet/artemis/versioning/service/ExerciseVersionEntityListener.java
🧬 Code graph analysis (1)
src/main/java/de/tum/cit/aet/artemis/versioning/service/ExerciseVersionEntityListener.java (1)
src/main/webapp/app/atlas/shared/entities/competency.model.ts (1)
  • CompetencyExerciseLink (103-110)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: Build and Push Docker Image / Build linux/arm64 Docker Image for ls1intum/artemis
  • GitHub Check: Build and Push Docker Image / Build linux/amd64 Docker Image for ls1intum/artemis
  • GitHub Check: Build .war artifact
  • GitHub Check: server-style
  • GitHub Check: server-tests
  • GitHub Check: client-style
  • GitHub Check: client-tests
  • GitHub Check: bean-instantiation-check
  • GitHub Check: Analyse

Comment on lines +57 to +86
else if (entity instanceof CompetencyExerciseLink link) {
if (link.getExercise() == null) {
return;
}
publishExerciseChangedEvent(link.getExercise().getId(), link.getExercise().getExerciseType());
}
else if (entity instanceof AuxiliaryRepository repository) {
if (repository.getExercise() == null) {
return;
}
publishExerciseChangedEvent(repository.getExercise().getId(), repository.getExercise().getExerciseType());
}
else if (entity instanceof StaticCodeAnalysisCategory category) {
if (category.getExercise() == null) {
return;
}
publishExerciseChangedEvent(category.getExercise().getId(), category.getExercise().getExerciseType());
}
else if (entity instanceof SubmissionPolicy submissionPolicy) {
if (submissionPolicy.getProgrammingExercise() == null) {
return;
}
publishExerciseChangedEvent(submissionPolicy.getProgrammingExercise().getId(), submissionPolicy.getProgrammingExercise().getExerciseType());
}
else if (entity instanceof ProgrammingExerciseBuildConfig config) {
if (config.getProgrammingExercise() == null) {
return;
}
publishExerciseChangedEvent(config.getProgrammingExercise().getId(), config.getProgrammingExercise().getExerciseType());
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Eliminate code duplication with a helper method.

Lines 57–86 repeat the same pattern five times: null-check an exercise reference, then call its getters twice. This violates DRY and the code reuse guideline. Extract the pattern into a helper method to improve maintainability.

Example refactor:

 private void handleEntityChange(Object entity) {
     if (entity instanceof Exercise exercise) {
         publishExerciseChangedEvent(exercise.getId(), exercise.getExerciseType());
-    }
-    else if (entity instanceof CompetencyExerciseLink link) {
-        if (link.getExercise() == null) {
-            return;
-        }
-        publishExerciseChangedEvent(link.getExercise().getId(), link.getExercise().getExerciseType());
-    }
-    else if (entity instanceof AuxiliaryRepository repository) {
-        if (repository.getExercise() == null) {
-            return;
-        }
-        publishExerciseChangedEvent(repository.getExercise().getId(), repository.getExercise().getExerciseType());
-    }
-    else if (entity instanceof StaticCodeAnalysisCategory category) {
-        if (category.getExercise() == null) {
-            return;
-        }
-        publishExerciseChangedEvent(category.getExercise().getId(), category.getExercise().getExerciseType());
-    }
-    else if (entity instanceof SubmissionPolicy submissionPolicy) {
-        if (submissionPolicy.getProgrammingExercise() == null) {
-            return;
-        }
-        publishExerciseChangedEvent(submissionPolicy.getProgrammingExercise().getId(), submissionPolicy.getProgrammingExercise().getExerciseType());
-    }
-    else if (entity instanceof ProgrammingExerciseBuildConfig config) {
-        if (config.getProgrammingExercise() == null) {
-            return;
-        }
-        publishExerciseChangedEvent(config.getProgrammingExercise().getId(), config.getProgrammingExercise().getExerciseType());
+    }
+    else if (entity instanceof CompetencyExerciseLink link) {
+        publishEventForExercise(link.getExercise());
+    }
+    else if (entity instanceof AuxiliaryRepository repository) {
+        publishEventForExercise(repository.getExercise());
+    }
+    else if (entity instanceof StaticCodeAnalysisCategory category) {
+        publishEventForExercise(category.getExercise());
+    }
+    else if (entity instanceof SubmissionPolicy submissionPolicy) {
+        publishEventForExercise(submissionPolicy.getProgrammingExercise());
+    }
+    else if (entity instanceof ProgrammingExerciseBuildConfig config) {
+        publishEventForExercise(config.getProgrammingExercise());
     }
 }
+
+private void publishEventForExercise(Exercise exercise) {
+    if (exercise == null) {
+        return;
+    }
+    publishExerciseChangedEvent(exercise.getId(), exercise.getExerciseType());
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
else if (entity instanceof CompetencyExerciseLink link) {
if (link.getExercise() == null) {
return;
}
publishExerciseChangedEvent(link.getExercise().getId(), link.getExercise().getExerciseType());
}
else if (entity instanceof AuxiliaryRepository repository) {
if (repository.getExercise() == null) {
return;
}
publishExerciseChangedEvent(repository.getExercise().getId(), repository.getExercise().getExerciseType());
}
else if (entity instanceof StaticCodeAnalysisCategory category) {
if (category.getExercise() == null) {
return;
}
publishExerciseChangedEvent(category.getExercise().getId(), category.getExercise().getExerciseType());
}
else if (entity instanceof SubmissionPolicy submissionPolicy) {
if (submissionPolicy.getProgrammingExercise() == null) {
return;
}
publishExerciseChangedEvent(submissionPolicy.getProgrammingExercise().getId(), submissionPolicy.getProgrammingExercise().getExerciseType());
}
else if (entity instanceof ProgrammingExerciseBuildConfig config) {
if (config.getProgrammingExercise() == null) {
return;
}
publishExerciseChangedEvent(config.getProgrammingExercise().getId(), config.getProgrammingExercise().getExerciseType());
}
private void handleEntityChange(Object entity) {
if (entity instanceof Exercise exercise) {
publishExerciseChangedEvent(exercise.getId(), exercise.getExerciseType());
}
else if (entity instanceof CompetencyExerciseLink link) {
publishEventForExercise(link.getExercise());
}
else if (entity instanceof AuxiliaryRepository repository) {
publishEventForExercise(repository.getExercise());
}
else if (entity instanceof StaticCodeAnalysisCategory category) {
publishEventForExercise(category.getExercise());
}
else if (entity instanceof SubmissionPolicy submissionPolicy) {
publishEventForExercise(submissionPolicy.getProgrammingExercise());
}
else if (entity instanceof ProgrammingExerciseBuildConfig config) {
publishEventForExercise(config.getProgrammingExercise());
}
}
private void publishEventForExercise(Exercise exercise) {
if (exercise == null) {
return;
}
publishExerciseChangedEvent(exercise.getId(), exercise.getExerciseType());
}
🤖 Prompt for AI Agents
In
src/main/java/de/tum/cit/aet/artemis/versioning/service/ExerciseVersionEntityListener.java
around lines 57 to 86, multiple branches repeat the same pattern of
null-checking an exercise (or programmingExercise) and then calling its getters
twice; extract this into a single private helper method that accepts the
exercise (or programming exercise) object, performs a single null-check, reads
id and exerciseType into local variables, and calls
publishExerciseChangedEvent(id, exerciseType); replace each duplicated branch
with a call to that helper so getters are not invoked twice and the logic is
centralized.

@github-actions
Copy link

github-actions bot commented Oct 8, 2025

End-to-End (E2E) Test Results Summary

TestsPassed ✅SkippedFailedTime ⏱
End-to-End (E2E) Test Report1 ran1 passed0 skipped0 failed1s 751ms
TestResultTime ⏱
No test annotations available

…ersioning/create-new-exercise-snapshot-during-hibernate-post-flush
@github-actions
Copy link

github-actions bot commented Oct 8, 2025

End-to-End (E2E) Test Results Summary

TestsPassed ✅SkippedFailedTime ⏱
End-to-End (E2E) Test Report1 ran1 passed0 skipped0 failed1s 676ms
TestResultTime ⏱
No test annotations available

@github-actions
Copy link

github-actions bot commented Oct 8, 2025

End-to-End (E2E) Test Results Summary

TestsPassed ✅SkippedFailedTime ⏱
End-to-End (E2E) Test Report1 ran1 passed0 skipped0 failed1s 592ms
TestResultTime ⏱
No test annotations available

Elfari1028 and others added 3 commits October 8, 2025 22:25
…t-during-hibernate-post-flush' of github.com:ls1intum/Artemis into feature/exercise-versioning/create-new-exercise-snapshot-during-hibernate-post-flush
@github-actions
Copy link

github-actions bot commented Oct 8, 2025

End-to-End (E2E) Test Results Summary

TestsPassed ✅SkippedFailedTime ⏱
End-to-End (E2E) Test Report1 ran1 passed0 skipped0 failed1s 902ms
TestResultTime ⏱
No test annotations available

@github-actions
Copy link

github-actions bot commented Oct 8, 2025

End-to-End (E2E) Test Results Summary

TestsPassed ✅SkippedFailedTime ⏱
End-to-End (E2E) Test Report1 ran1 passed0 skipped0 failed1s 569ms
TestResultTime ⏱
No test annotations available

@github-actions github-actions bot removed the config-change Pull requests that change the config in a way that they require a deployment via Ansible. label Oct 9, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/test/java/de/tum/cit/aet/artemis/versioning/service/ExerciseVersionServiceTest.java (1)

109-112: Restore base teardown cleanup.

This overrides AbstractProgrammingIntegrationLocalCILocalVCTestBase.tearDown() but never calls super, so build-job cleanup from the base class stops running. Subsequent programming tests will see leftover build jobs.

Delegate to the superclass before deleting exercise versions:

-    @AfterEach
-    void tearDown() {
-        exerciseVersionRepository.deleteAll();
-    }
+    @AfterEach
+    @Override
+    void tearDown() {
+        super.tearDown();
+        exerciseVersionRepository.deleteAll();
+    }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 27e0fe9 and 30616c5.

📒 Files selected for processing (3)
  • src/main/java/de/tum/cit/aet/artemis/versioning/service/ExerciseVersionEntityListener.java (1 hunks)
  • src/main/java/de/tum/cit/aet/artemis/versioning/service/ExerciseVersionService.java (1 hunks)
  • src/test/java/de/tum/cit/aet/artemis/versioning/service/ExerciseVersionServiceTest.java (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
src/main/java/**/*.java

⚙️ CodeRabbit configuration file

naming:CamelCase; principles:{single_responsibility,small_methods,no_duplication}; db:{perf_queries,datetime_not_timestamp}; rest:{stateless,singleton,delegate_logic,http_only,minimal_dtos}; dtos:{java_records,no_entities,min_data,single_resp}; di:constructor_injection; kiss:simple_code; file_handling:os_indep_paths; practices:{least_access,avoid_transactions,code_reuse,static_member_ref,prefer_primitives}; sql:{param_annotation,uppercase,avoid_subqueries};java:avoid_star_imports

Files:

  • src/main/java/de/tum/cit/aet/artemis/versioning/service/ExerciseVersionEntityListener.java
  • src/main/java/de/tum/cit/aet/artemis/versioning/service/ExerciseVersionService.java
src/test/java/**/*.java

⚙️ CodeRabbit configuration file

test_naming: descriptive; test_size: small_specific; fixed_data: true; junit5_features: true; assert_use: assertThat; assert_specificity: true; archunit_use: enforce_package_rules; db_query_count_tests: track_performance; util_service_factory_pattern: true; avoid_db_access: true; mock_strategy: static_mocks; context_restart_minimize: true

Files:

  • src/test/java/de/tum/cit/aet/artemis/versioning/service/ExerciseVersionServiceTest.java
🧬 Code graph analysis (2)
src/test/java/de/tum/cit/aet/artemis/versioning/service/ExerciseVersionServiceTest.java (2)
src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationLocalCILocalVCTestBase.java (1)
  • AbstractProgrammingIntegrationLocalCILocalVCTestBase (51-234)
src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseFactory.java (1)
  • ProgrammingExerciseFactory (50-512)
src/main/java/de/tum/cit/aet/artemis/versioning/service/ExerciseVersionService.java (1)
src/main/java/de/tum/cit/aet/artemis/core/config/Constants.java (1)
  • Constants (8-526)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: Build and Push Docker Image / Build linux/amd64 Docker Image for ls1intum/artemis
  • GitHub Check: Build and Push Docker Image / Build linux/arm64 Docker Image for ls1intum/artemis
  • GitHub Check: Build .war artifact
  • GitHub Check: client-tests
  • GitHub Check: server-style
  • GitHub Check: client-style
  • GitHub Check: server-tests
  • GitHub Check: Analyse
  • GitHub Check: bean-instantiation-check

Comment on lines +90 to +117
private void createExerciseVersion(Long exerciseId, ExerciseType exerciseType, String userLogin) {
try {
User author = userRepository.findOneByLogin(userLogin).orElse(null);
if (author == null) {
log.error("No active user during exercise version creation check");
return;
}
Exercise exercise = fetchExerciseEagerly(exerciseId, exerciseType);
if (exercise == null) {
log.error("Exercise with id {} not found", exerciseId);
return;
}
ExerciseVersion exerciseVersion = new ExerciseVersion();
exerciseVersion.setExercise(exercise);
exerciseVersion.setAuthor(author);
ExerciseSnapshotDTO exerciseSnapshot = ExerciseSnapshotDTO.of(exercise, gitService);
Optional<ExerciseVersion> previousVersion = exerciseVersionRepository.findTopByExerciseIdOrderByCreatedDateDesc(exercise.getId());
if (previousVersion.isPresent()) {
ExerciseSnapshotDTO previousVersionSnapshot = previousVersion.get().getExerciseSnapshot();
boolean equal = previousVersionSnapshot.equals(exerciseSnapshot);
if (equal) {
log.info("Exercise {} has no versionable changes from last version", exercise.getId());
return;
}
}
exerciseVersion.setExerciseSnapshot(exerciseSnapshot);
ExerciseVersion savedExerciseVersion = exerciseVersionRepository.save(exerciseVersion);
log.info("Exercise version {} has been created for exercise {}", savedExerciseVersion.getId(), exercise.getId());
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Race condition creates duplicate snapshots.

createExerciseVersion runs outside a shared transaction: two concurrent events can both read the same “latest” version, see no changes, and each save a new ExerciseVersion. We need a transactional section plus a pessimistic lock on the latest row before comparing.

Please wrap the compare-and-save in a transaction and lock the latest version, e.g.:

@@
-    private void createExerciseVersion(Long exerciseId, ExerciseType exerciseType, String userLogin) {
+    @Transactional
+    public void createExerciseVersion(Long exerciseId, ExerciseType exerciseType, String userLogin) {
@@
-            Optional<ExerciseVersion> previousVersion = exerciseVersionRepository.findTopByExerciseIdOrderByCreatedDateDesc(exercise.getId());
+            Optional<ExerciseVersion> previousVersion = exerciseVersionRepository
+                    .findTopByExerciseIdOrderByCreatedDateDescForUpdate(exercise.getId());

and add in ExerciseVersionRepository:

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("""
       select v
         from ExerciseVersion v
        where v.exercise.id = :exerciseId
     order by v.createdDate desc
       """)
Optional<ExerciseVersion> findTopByExerciseIdOrderByCreatedDateDescForUpdate(@Param("exerciseId") Long exerciseId);

Call the transactional method from the async listener via the Spring proxy (e.g., inject ExerciseVersionService into itself or use TransactionTemplate) so the lock actually applies.

🤖 Prompt for AI Agents
In
src/main/java/de/tum/cit/aet/artemis/versioning/service/ExerciseVersionService.java
around lines 90 to 117, the compare-and-save flow is vulnerable to a race that
can create duplicate ExerciseVersion rows; fix it by moving the portion that
reads the latest version, compares snapshots and saves the new version into a
dedicated @Transactional method and acquire a pessimistic write lock on the
latest version row before comparing; add a repository query method annotated
with @Lock(LockModeType.PESSIMISTIC_WRITE) that selects the latest
ExerciseVersion for an exercise (ordered by createdDate desc) and use that
method inside the new transactional method, then ensure the transactional method
is invoked through the Spring proxy (self-inject the service or use
TransactionTemplate) from the async listener so the transaction and lock are
actually applied.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when using order by, using Optional<> is not allowed. return type must be list. therefore we cannot fetch single most recent exercise version with jpql.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Elfari1028, I appreciate your feedback, but I need to clarify: Spring Data JPA does support Optional<> as a return type with ORDER BY in both derived queries and @Query methods. Your current code already uses this pattern successfully:

Optional<ExerciseVersion> findTopByExerciseIdOrderByCreatedDateDesc(Long exerciseId);

For the pessimistic lock query, this should work:

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("""
       SELECT v FROM ExerciseVersion v
       WHERE v.exercise.id = :exerciseId
       ORDER BY v.createdDate DESC
       """)
Optional<ExerciseVersion> findTopByExerciseIdOrderByCreatedDateDescForUpdate(@Param("exerciseId") Long exerciseId);

However, if you prefer using List<> or encountered a specific error, here's an alternative:

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("""
       SELECT v FROM ExerciseVersion v
       WHERE v.exercise.id = :exerciseId
       ORDER BY v.createdDate DESC
       """)
List<ExerciseVersion> findByExerciseIdOrderByCreatedDateDescForUpdate(@Param("exerciseId") Long exerciseId);

Then in the service:

List<ExerciseVersion> versions = exerciseVersionRepository.findByExerciseIdOrderByCreatedDateDescForUpdate(exercise.getId());
Optional<ExerciseVersion> previousVersion = versions.isEmpty() ? Optional.empty() : Optional.of(versions.get(0));

Important: Regardless of the return type, the race condition I flagged is critical and must be addressed. Without proper locking and transactional boundaries, concurrent events will create duplicate versions. The solution requires:

  1. A @Transactional method for the compare-and-save logic
  2. Pessimistic locking on the latest version row
  3. Proper proxy invocation to ensure the transaction applies

If you encountered a specific error message when trying Optional<> with ORDER BY, please share it so I can help diagnose the actual issue.

@github-actions
Copy link

github-actions bot commented Oct 9, 2025

End-to-End (E2E) Test Results Summary

TestsPassed ✅SkippedFailedTime ⏱
End-to-End (E2E) Test Report1 ran1 passed0 skipped0 failed1s 668ms
TestResultTime ⏱
No test annotations available

@github-actions
Copy link

End-to-End (E2E) Test Results Summary

TestsPassed ✅SkippedFailedTime ⏱
End-to-End (E2E) Test Report1 ran1 passed0 skipped0 failed1s 742ms
TestResultTime ⏱
No test annotations available

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/main/java/de/tum/cit/aet/artemis/versioning/service/ExerciseVersionService.java (1)

90-117: Transactional gap allows duplicate snapshots under load

createExerciseVersion still runs outside a shared transaction: two async events for the same exercise can both read the same “latest” version, pass the equality check, and each save a new row. We need the compare+save to execute within a single transaction that holds a pessimistic write lock on the latest version record. Please refactor so the logic executes via a proxied @Transactional method (e.g. self-injected proxy or TransactionTemplate) and replace the plain lookup with a findTop...ForUpdate(...) query annotated with @Lock(PESSIMISTIC_WRITE), handling the ordered result. This guarantees only one writer proceeds per exercise.

🧹 Nitpick comments (1)
src/main/java/de/tum/cit/aet/artemis/versioning/domain/ExerciseVersion.java (1)

61-75: Keep entity reference fields in sync with their IDs

setExercise/setAuthor only copy the IDs, so getExercise()/getAuthor() still return null until the entity is reloaded. That’s surprising for callers and breaks the usual setter/getter contract. Please store the association as well (null-safe), e.g.:

-    public void setExercise(Exercise exercise) {
-        this.exerciseId = exercise.getId();
-    }
+    public void setExercise(Exercise exercise) {
+        this.exercise = exercise;
+        this.exerciseId = exercise != null ? exercise.getId() : null;
+    }
...
-    public void setAuthor(User author) {
-        this.authorId = author.getId();
-    }
+    public void setAuthor(User author) {
+        this.author = author;
+        this.authorId = author != null ? author.getId() : null;
+    }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2625836 and e15bd3c.

⛔ Files ignored due to path filters (1)
  • .github/workflows/bean-instantiations.yml is excluded by !**/*.yml
📒 Files selected for processing (2)
  • src/main/java/de/tum/cit/aet/artemis/versioning/domain/ExerciseVersion.java (1 hunks)
  • src/main/java/de/tum/cit/aet/artemis/versioning/service/ExerciseVersionService.java (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
src/main/java/**/*.java

⚙️ CodeRabbit configuration file

naming:CamelCase; principles:{single_responsibility,small_methods,no_duplication}; db:{perf_queries,datetime_not_timestamp}; rest:{stateless,singleton,delegate_logic,http_only,minimal_dtos}; dtos:{java_records,no_entities,min_data,single_resp}; di:constructor_injection; kiss:simple_code; file_handling:os_indep_paths; practices:{least_access,avoid_transactions,code_reuse,static_member_ref,prefer_primitives}; sql:{param_annotation,uppercase,avoid_subqueries};java:avoid_star_imports

Files:

  • src/main/java/de/tum/cit/aet/artemis/versioning/service/ExerciseVersionService.java
  • src/main/java/de/tum/cit/aet/artemis/versioning/domain/ExerciseVersion.java
🧬 Code graph analysis (2)
src/main/java/de/tum/cit/aet/artemis/versioning/service/ExerciseVersionService.java (1)
src/main/java/de/tum/cit/aet/artemis/core/config/Constants.java (1)
  • Constants (8-526)
src/main/java/de/tum/cit/aet/artemis/versioning/domain/ExerciseVersion.java (1)
src/main/java/de/tum/cit/aet/artemis/exercise/domain/Exercise.java (1)
  • Entity (75-903)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: Build and Push Docker Image / Build linux/arm64 Docker Image for ls1intum/artemis
  • GitHub Check: Build and Push Docker Image / Build linux/amd64 Docker Image for ls1intum/artemis
  • GitHub Check: Build .war artifact
  • GitHub Check: Analyse
  • GitHub Check: client-tests
  • GitHub Check: client-style
  • GitHub Check: server-tests
  • GitHub Check: server-style
  • GitHub Check: bean-instantiation-check

@github-actions
Copy link

End-to-End (E2E) Test Results Summary

TestsPassed ✅SkippedFailedTime ⏱
End-to-End (E2E) Test Report1 ran1 passed0 skipped0 failed1s 513ms
TestResultTime ⏱
No test annotations available

// Entity references (for queries, read-only)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "exercise_id", insertable = false, updatable = false)
private Exercise exercise;
Copy link
Member

@krusche krusche Oct 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need the full exercise materialized here? I would assume exerciseId above should be sufficient, right?
(also keep in mind that ManyToOne is always eager and cannot really be lazy)


@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id", insertable = false, updatable = false)
private User author;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need the full author materialized here? I would assume authorId above should be sufficient, right?
(also keep in mind that ManyToOne is always eager and cannot really be lazy)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will have this removed in the new PR, and save the authorId only. same with exerciseId

@Elfari1028
Copy link
Contributor Author

Closing this PR in favor of explicit exercise version creation on specific endpoints:
#11491

@Elfari1028 Elfari1028 closed this Oct 13, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

atlas Pull requests that affect the corresponding module core Pull requests that affect the corresponding module database Pull requests that update the database. (Added Automatically!). Require a CRITICAL deployment. exam Pull requests that affect the corresponding module exercise Pull requests that affect the corresponding module fileupload Pull requests that affect the corresponding module modeling Pull requests that affect the corresponding module programming Pull requests that affect the corresponding module quiz Pull requests that affect the corresponding module ready for review server Pull requests that update Java code. (Added Automatically!) tests text Pull requests that affect the corresponding module

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

9 participants