Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,14 @@
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
public class BackendApplication {

public static void main(String[] args) {
Expand Down
36 changes: 36 additions & 0 deletions backend/src/main/java/programming/tutorial/config/CacheConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package programming.tutorial.config;

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;

import java.time.Duration;

@Configuration
@EnableCaching
public class CacheConfig {

@Bean
public CacheManager cacheManager() {
try {
LettuceConnectionFactory redisConnectionFactory = new LettuceConnectionFactory("localhost", 6379);
redisConnectionFactory.afterPropertiesSet();
redisConnectionFactory.getConnection().ping();

RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10));
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(config)
.build();
} catch (Exception e) {
System.out.println("Redis not available. Using in-memory cache.");
return new ConcurrentMapCacheManager();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package programming.tutorial.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import programming.tutorial.services.impl.CacheServiceJpa;

@RestController
@RequestMapping("/admin/cache")
public class CacheController {

@Autowired
private CacheServiceJpa cacheService;

@DeleteMapping("/courses")
public ResponseEntity<String> clearAllCoursesCache() {
cacheService.evictAllCoursesCache();
System.out.println("Called");
return ResponseEntity.ok("All course caches cleared!");
}

@DeleteMapping("/all")
public ResponseEntity<String> clearAllCaches() {
cacheService.evictAllCaches();
return ResponseEntity.ok("All caches cleared");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ public List<CourseDTO> getAllCourses() {
}
return courses;
}
@GetMapping("/uncached")
public List<CourseDTO> getAllCoursesUncached() {
return courseService.getAllCoursesUncached();
}

@PostMapping("/create-with-lessons")
public ResponseEntity<?> createCourseWithLessons(@RequestBody CourseWithLessonsDTO dto) {
Expand Down
12 changes: 8 additions & 4 deletions backend/src/main/java/programming/tutorial/dto/CourseDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@

import programming.tutorial.domain.User;

public class CourseDTO {
import java.io.Serial;
import java.io.Serializable;

public class CourseDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private Integer courseId;
private String courseName;
private int courseLength;
private String description;
private String category;
private String creatorId;
private User creator;
private UserDTO creator;
private boolean systemCourse;

public CourseDTO() {
Expand Down Expand Up @@ -85,11 +89,11 @@ public void setCreatorId(String creatorId) {
this.creatorId = creatorId;
}

public User getCreator() {
public UserDTO getCreator() {
return creator;
}

public void setCreator(User creator) {
public void setCreator(UserDTO creator) {
this.creator = creator;
}

Expand Down
6 changes: 5 additions & 1 deletion backend/src/main/java/programming/tutorial/dto/UserDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
import programming.tutorial.domain.Role;
import programming.tutorial.domain.Tier;

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

public class UserDTO {
public class UserDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
public Integer id;
public String name;
public String surname;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package programming.tutorial.monitoring;

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Component;

@Component
public class CacheMetrics {

private final Counter cacheHit;
private final Counter cacheMiss;

public CacheMetrics(MeterRegistry registry) {
this.cacheHit = Counter.builder("redis_cache_hit_total")
.description("Cache hits")
.register(registry);

this.cacheMiss = Counter.builder("redis_cache_miss_total")
.description("Cache misses")
.register(registry);
}

public void hit() { cacheHit.increment(); }
public void miss() { cacheMiss.increment(); }
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,6 @@ public interface CourseService {
boolean isCourseOwner(String userId, Integer courseId);

List<LessonDTO> getLessonsForCourse(Integer courseId);

List<CourseDTO> getAllCoursesUncached();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package programming.tutorial.services.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;

@Service
public class CacheServiceJpa {

@Autowired
private CacheManager cacheManager;

public void evictAllCoursesCache() {
var cache = cacheManager.getCache("all_courses");
if (cache != null) {
cache.clear();
}
}

public void evictCoursesByUser(String auth0UserId) {
var cache = cacheManager.getCache("courses_by_user");
if (cache != null) {
cache.evict(auth0UserId);
}
}

public void evictAllCaches() {
cacheManager.getCacheNames().forEach(name -> {
var cache = cacheManager.getCache(name);
if (cache != null) {
cache.clear();
}
});
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package programming.tutorial.services.impl;

import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Service;
import programming.tutorial.dao.CourseRepository;
import programming.tutorial.dao.LessonRepository;
Expand All @@ -9,7 +13,10 @@
import programming.tutorial.dto.CourseDTO;
import programming.tutorial.dto.CourseWithLessonsDTO;
import programming.tutorial.dto.LessonDTO;
import programming.tutorial.dto.UserDTO;
import programming.tutorial.monitoring.CacheMetrics;
import programming.tutorial.services.CourseService;
import io.micrometer.core.instrument.Timer;

import java.util.*;
import java.util.stream.Collectors;
Expand All @@ -25,18 +32,32 @@ public class CourseServiceJpa implements CourseService {

@Autowired
private LessonRepository lessonRepository;
@Autowired
private CacheServiceJpa cacheService;
@Autowired
private CacheMetrics cacheMetrics;
@Autowired
private MeterRegistry meterRegistry;

@Override
@Cacheable(value = "course_by_name", key = "#name")
public Optional<Course> findByName(CourseDTO courseDTO) {
return Optional.ofNullable(courseRepository.findByCourseName(courseDTO.getCourseName()));
}

@Override
@Cacheable(value = "course_by_id", key = "#courseDTO.courseId")
public Optional<Course> findById(CourseDTO courseDTO) {
return courseRepository.findById(courseDTO.getCourseId());
}

@Override
@Caching(evict = {
@CacheEvict(value = "all_courses", allEntries = true),
@CacheEvict(value = "course_by_id", key = "#courseDTO.courseId"),
@CacheEvict(value = "course_by_name", key = "#courseDTO.courseName"),
@CacheEvict(value = "courses_by_user", key = "#courseDTO.creator.auth0UserId")
})
public Course saveCourse(CourseDTO courseDTO) {
Course course = new Course();
course.setId(courseDTO.getCourseId());
Expand All @@ -48,23 +69,86 @@ public Course saveCourse(CourseDTO courseDTO) {
}

@Override
@CacheEvict(value = "all_courses", allEntries = true)
public void deleteCourse(Integer courseId) {
courseRepository.deleteById(courseId);
}

private UserDTO mapUser(User user) {
if (user == null) return null;
UserDTO dto = new UserDTO();
dto.setId(user.getId());
dto.setName(user.getName());
dto.setSurname(user.getSurname());
dto.setUsername(user.getUsername());
dto.setAuth0UserId(user.getAuth0UserId());
dto.setRole(user.getRole());
dto.setActive(user.isActive());
dto.setDateCreated(user.getDateCreated());
dto.setTier(user.getTier());
return dto;
}

@Override
@Cacheable(value = "all_courses")
public List<CourseDTO> getAllCourses() {
List<Course> courses = courseRepository.findAll();
System.out.println("Retrieved courses from database:");
for (Course course : courses) {
System.out.println("Course ID: " + course.getId() + ", Name: " + course.getCourseName());
}
return courses.stream()
.map(course -> new CourseDTO(course.getId(), course.getCourseName(), course.getLength(), course.getDescription(), course.getCategory()))
/* First run will print this if cache isn't evicted previously
* Other runs will not print this unless cache is invalidated
* */
System.out.println(">>> DB QUERY EXECUTED — NO CACHE HIT <<<");
List<CourseDTO> courses = courseRepository.findAll().stream()
.map(course -> {
CourseDTO dto = new CourseDTO(
course.getId(),
course.getCourseName(),
course.getLength(),
course.getDescription(),
course.getCategory(),
course.getCreator() != null ? course.getCreator().getId() : null,
course.isSystemCourse()
);
dto.setCreator(mapUser(course.getCreator()));
return dto;
})
.collect(Collectors.toList());

return courses;
}

/**
* Method to check how caching improves/behaves versus regular method.
* This bypasses the cache entirely and records execution time for comparison.
*/
public List<CourseDTO> getAllCoursesUncached() {
Timer.Sample sample = Timer.start(meterRegistry);
long start = System.currentTimeMillis();
System.out.println("Uncached getAllCourses method called");
List<CourseDTO> courses = courseRepository.findAll().stream()
.map(course -> new CourseDTO(
course.getId(),
course.getCourseName(),
course.getLength(),
course.getDescription(),
course.getCategory()
))
.collect(Collectors.toList());

sample.stop(Timer.builder("app.cache.getAllCourses_uncached.time")
.description("Execution time for getAllCourses WITHOUT cache")
.register(meterRegistry));

long duration = System.currentTimeMillis() - start;
System.out.println("getAllCoursesUncached execution time: " + duration + " ms");
return courses;
}


@Override
@Caching(evict = {
@CacheEvict(value = "all_courses", allEntries = true),
@CacheEvict(value = "courses_by_user", key = "#dto.auth0UserId"),
@CacheEvict(value = "lessons_for_course", key = "#result.id")
})
public Course createCourseWithLessons(CourseWithLessonsDTO dto) {
User creator = userRepository.findByAuth0UserId(dto.getAuth0UserId())
.orElseThrow(() -> new IllegalArgumentException("User not found for Auth0 ID: " + dto.getAuth0UserId()));
Expand Down Expand Up @@ -143,6 +227,7 @@ private static int getRequestedLessonCount(CourseWithLessonsDTO dto, User creato
}

@Override
@Cacheable(value = "courses_by_user", key = "#auth0UserId")
public List<CourseDTO> getCoursesByUserAuth0Id(String auth0UserId) {
User user = userRepository.findByAuth0UserId(auth0UserId)
.orElseThrow(() -> new RuntimeException("User not found"));
Expand All @@ -162,6 +247,7 @@ public boolean isCourseOwner(String userId, Integer courseId) {
}

@Override
@Cacheable(value = "lessons_for_course", key = "#courseId")
public List<LessonDTO> getLessonsForCourse(Integer courseId) {
List<Lesson> lessons = lessonRepository.findByCourseIdOrderByIdAsc(courseId);
return lessons.stream()
Expand Down
Loading