Skip to content

Commit

Permalink
Use Spring caching
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisgleissner committed Jun 16, 2024
1 parent f2b35df commit 9c1cf6f
Show file tree
Hide file tree
Showing 14 changed files with 46 additions and 73 deletions.
4 changes: 0 additions & 4 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,10 @@ bootRun { jvmArgs(["-Xms2g", "-Xmx2g", "-XX:+ExitOnOutOfMemoryError", "-Djdk.tra

dependencies {
implementation 'com.github.ben-manes.caffeine:caffeine'
implementation 'org.springframework.data:spring-data-jpa:3.3.1'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.hibernate.orm:hibernate-jcache'
implementation 'org.hibernate:hibernate-ehcache:5.6.15.Final'
implementation 'org.ehcache:ehcache:3.10.8'
runtimeOnly 'com.h2database:h2'
testImplementation 'io.github.hakky54:logcaptor:2.9.2'
testImplementation 'io.github.oshai:kotlin-logging-jvm:6.0.9'
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/uk/gleissner/loomwebflux/LoomWebfluxApp.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.scheduling.annotation.EnableScheduling;
import uk.gleissner.loomwebflux.config.AppProperties;

@SpringBootApplication
@ConfigurationPropertiesScan(basePackageClasses = AppProperties.class)
@EnableScheduling
@EnableCaching
public class LoomWebfluxApp {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import reactor.core.publisher.Mono;
import uk.gleissner.loomwebflux.controller.LoomWebFluxController;
import uk.gleissner.loomwebflux.movie.domain.Movie;
import uk.gleissner.loomwebflux.movie.repo.AppPropertiesAwareMovieRepo;
import uk.gleissner.loomwebflux.movie.repo.CachedMovieRepo;

import java.util.List;
import java.util.Set;
Expand All @@ -28,9 +28,9 @@
public class MovieController extends LoomWebFluxController {

private static final String API_PATH = "/movies";
private final AppPropertiesAwareMovieRepo movieRepo;
private final CachedMovieRepo movieRepo;

MovieController(WebClient webClient, AppPropertiesAwareMovieRepo movieRepo) {
MovieController(WebClient webClient, CachedMovieRepo movieRepo) {
super(webClient);
this.movieRepo = movieRepo;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package uk.gleissner.loomwebflux.movie.domain;

import jakarta.persistence.Cacheable;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
Expand All @@ -13,7 +12,6 @@
import static jakarta.persistence.GenerationType.IDENTITY;

@Entity
@Cacheable
@Data
@Builder(toBuilder = true)
@NoArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package uk.gleissner.loomwebflux.movie.domain;

import jakarta.persistence.Cacheable;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
Expand All @@ -15,7 +14,6 @@
import static jakarta.persistence.GenerationType.IDENTITY;

@Entity
@Cacheable
@Data
@Builder(toBuilder = true)
@NoArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package uk.gleissner.loomwebflux.movie.domain;

import jakarta.persistence.Cacheable;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
Expand All @@ -22,7 +21,6 @@
import static jakarta.persistence.GenerationType.IDENTITY;

@Entity
@Cacheable
@Data
@Builder(toBuilder = true)
@NoArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package uk.gleissner.loomwebflux.movie.domain;

import jakarta.annotation.Nullable;
import jakarta.persistence.Cacheable;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
Expand All @@ -17,7 +16,6 @@
import static jakarta.persistence.GenerationType.IDENTITY;

@Entity
@Cacheable
@Data
@Builder(toBuilder = true)
@NoArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,29 @@

import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;
import uk.gleissner.loomwebflux.config.AppProperties;
import uk.gleissner.loomwebflux.movie.domain.Movie;

import java.util.Set;

@Component
public class AppPropertiesAwareMovieRepo {
public class CachedMovieRepo {

private static final String MOVIES_BY_DIRECTOR_NAME_CACHE_NAME = "moviesByDirectorName";

private final AppProperties appProperties;
private final MovieRepo underlying;
private final Cache moviesByDirectorNameCache;

AppPropertiesAwareMovieRepo(AppProperties appProperties, MovieRepo underlying, CacheManager cacheManager) {
CachedMovieRepo(AppProperties appProperties, MovieRepo underlying, CacheManager cacheManager) {
this.appProperties = appProperties;
this.underlying = underlying;
this.moviesByDirectorNameCache = cacheManager.getCache(MOVIES_BY_DIRECTOR_NAME_CACHE_NAME);
}

// @Cacheable("moviesByDirectorName")
@Cacheable(MOVIES_BY_DIRECTOR_NAME_CACHE_NAME)
public Set<Movie> findByDirectorName(String directorName) {
return underlying.findByDirectorName(directorName);
}
Expand All @@ -32,15 +33,19 @@ public Movie save(Movie movie) {
if (appProperties.repoReadOnly()) {
return movie;
} else {
// movie.getDirectors().forEach(director -> moviesByDirectorNameCache.put(director.getLastName(), movie));
evictMovieFromCache(movie);
return underlying.save(movie);
}
}

public void deleteById(Long id) {
if (!appProperties.repoReadOnly()) {
// underlying.findById(id).ifPresent(movie -> movie.getDirectors().forEach(director -> moviesByDirectorNameCache.evict(director.getLastName())));
underlying.findById(id).ifPresent(this::evictMovieFromCache);
underlying.deleteById(id);
}
}

private void evictMovieFromCache(Movie movie) {
movie.getDirectors().forEach(director -> moviesByDirectorNameCache.evict(director.getLastName()));
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
package uk.gleissner.loomwebflux.movie.repo;

import jakarta.persistence.QueryHint;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.QueryHints;
import org.springframework.data.repository.ListCrudRepository;
import uk.gleissner.loomwebflux.movie.domain.Movie;

import java.util.Set;

import static org.hibernate.jpa.HibernateHints.HINT_CACHEABLE;

public interface MovieRepo extends ListCrudRepository<Movie, Long> {

@QueryHints(@QueryHint(name = HINT_CACHEABLE, value = "true"))
@Query("SELECT m FROM Movie m JOIN m.directors d WHERE d.lastName = :directorName")
Set<Movie> findByDirectorName(String directorName);
}
6 changes: 0 additions & 6 deletions src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,6 @@ spring:
properties:
hibernate:
globally_quoted_identifiers: true
javax.cache.missing_cache_strategy: create
cache:
region:
factory_class: jcache
use_query_cache: true
use_second_level_cache: true
hibernate:
ddl-auto: update
datasource:
Expand Down
4 changes: 2 additions & 2 deletions src/main/resources/scenarios/scenarios-get-post-movies.csv
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
scenario,k6Config,delayCallDepth,delayInMillis,connections,requestsPerSecond,warmupDurationInSeconds,testDurationInSeconds
10k-vus-smooth-spike-get-post-movies-call-depth-1,get-post-movies-smooth-vus-spike.js,1,100,10000,,0,300
25k-vus-smooth-spike-get-post-movies-call-depth-1,get-post-movies-smooth-vus-spike.js,1,100,25000,,0,300
10k-vus-smooth-spike-get-post-movies-call-depth-1,get-post-movies-smooth-vus-spike.js,1,100,10000,,0,60
25k-vus-smooth-spike-get-post-movies-call-depth-1,get-post-movies-smooth-vus-spike.js,1,100,25000,,0,60
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ import uk.gleissner.loomwebflux.movie.repo.MovieRepo
import java.time.Duration
import java.time.Instant.now

private const val HIBERNATE_ORM_CACHE_LOG_NAME = "org.hibernate.orm.cache"
private const val RETURNING_CACHED_QUERY_RESULTS = "Returning cached query results"
private const val CACHED_QUERY_RESULTS_WERE_NOT_UP_TO_DATE = "Cached query results were not up-to-date"
private const val SQL_LOG_NAME = "org.hibernate.SQL"

internal class MovieControllerIntegrationTest : AbstractIntegrationTest() {

Expand All @@ -45,39 +43,30 @@ internal class MovieControllerIntegrationTest : AbstractIntegrationTest() {

val savedMovies = saveMovies(approach, movies, delayCallDepth = delayCallDepth)
assertThat(savedMovies).hasSize(movies.size)
savedMovies.forEach {
assertThat(it.id).isNotNull()
}
savedMovies.forEach { assertThat(it.id).isNotNull() }
assertThat(savedMovies).usingRecursiveComparison().ignoringFieldsMatchingRegexes(".*id").isEqualTo(movies)
assertThatSqlQueryIssued(true) { assertThat(getMovies()).containsExactlyElementsOf(savedMovies) }
assertThatSqlQueryIssued(false) { assertThat(getMovies()).containsExactlyElementsOf(savedMovies) }

secondLevelCacheLogCaptor().use { logCaptor ->
assertThat(getMovies()).containsExactlyElementsOf(savedMovies)
logCaptor.assertThatCached(false)
}

secondLevelCacheLogCaptor().use { logCaptor ->
assertThat(getMovies()).containsExactlyElementsOf(savedMovies)
logCaptor.assertThatCached(true)
}

secondLevelCacheLogCaptor().use { logCaptor ->
savedMovies.forEach { savedMovie ->
deleteMovie(approach, movieId = savedMovie.id, delayCallDepth = delayCallDepth)
}
assertThat(getMovies()).isEmpty()
logCaptor.assertThatCached(false)
}

savedMovies.forEach { savedMovie -> deleteMovie(approach, movieId = savedMovie.id, delayCallDepth = delayCallDepth) }
assertThatSqlQueryIssued(true) { assertThat(getMovies()).isEmpty() }
logCaptor.assertCorrectThreadType(approach, expectedLogCount = (delayCallDepth + 1) * 7)
}

private fun secondLevelCacheLogCaptor() = LogCaptor.forName(HIBERNATE_ORM_CACHE_LOG_NAME)

private fun LogCaptor.assertThatCached(cached: Boolean) {
if (cached) {
assertThat(debugLogs).contains(RETURNING_CACHED_QUERY_RESULTS).doesNotContain(CACHED_QUERY_RESULTS_WERE_NOT_UP_TO_DATE)
} else {
assertThat(debugLogs).doesNotContain(RETURNING_CACHED_QUERY_RESULTS).contains(CACHED_QUERY_RESULTS_WERE_NOT_UP_TO_DATE)
private fun assertThatSqlQueryIssued(queryIssued: Boolean, repoCall: Runnable) {
val logCaptor = LogCaptor.forName(SQL_LOG_NAME)
try {
logCaptor.setLogLevelToDebug()
repoCall.run()
val selectCount = logCaptor.debugLogs.filter { it.startsWith("select") }.size
if (queryIssued) {
assertThat(selectCount).isPositive()
} else {
assertThat(selectCount).isZero()
}
} finally {
logCaptor.setLogLevelToInfo()
logCaptor.close()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import uk.gleissner.loomwebflux.movie.domain.Movies.mulhollandDrive
import uk.gleissner.loomwebflux.movie.domain.Movies.theStraightStory

@ExtendWith(MockitoExtension::class)
class AppPropertiesAwareMovieRepoTest {
class CachedMovieRepoTest {

@Mock
private lateinit var appProperties: AppProperties
Expand All @@ -30,13 +30,13 @@ class AppPropertiesAwareMovieRepoTest {
@Mock
private lateinit var cacheManager: CacheManager

private lateinit var appPropertiesAwareMovieRepo: AppPropertiesAwareMovieRepo
private lateinit var sut: CachedMovieRepo


@BeforeEach
fun beforeEach() {
`when`(cacheManager.getCache(any())).thenReturn(cache);
appPropertiesAwareMovieRepo = AppPropertiesAwareMovieRepo(appProperties, movieRepo, cacheManager)
sut = CachedMovieRepo(appProperties, movieRepo, cacheManager)
}

@Test
Expand All @@ -45,7 +45,7 @@ class AppPropertiesAwareMovieRepoTest {
val expectedMovies = setOf(mulhollandDrive, theStraightStory)
`when`(movieRepo.findByDirectorName(directorName)).thenReturn(expectedMovies)

val movies = appPropertiesAwareMovieRepo.findByDirectorName(directorName)
val movies = sut.findByDirectorName(directorName)

assertThat(movies).isEqualTo(expectedMovies)
verify(movieRepo).findByDirectorName(directorName)
Expand All @@ -55,7 +55,7 @@ class AppPropertiesAwareMovieRepoTest {
fun `save should return movie without saving when repoReadOnly is true`() {
`when`(appProperties.repoReadOnly()).thenReturn(true)

val movie = appPropertiesAwareMovieRepo.save(mulhollandDrive)
val movie = sut.save(mulhollandDrive)

assertThat(movie).isSameAs(mulhollandDrive)
verify(movieRepo, never()).save(any(Movie::class.java))
Expand All @@ -69,7 +69,7 @@ class AppPropertiesAwareMovieRepoTest {
`when`(appProperties.repoReadOnly()).thenReturn(false)
`when`(movieRepo.save(movie)).thenReturn(savedMovie)

val result = appPropertiesAwareMovieRepo.save(movie)
val result = sut.save(movie)

assertThat(result).isEqualTo(savedMovie)
verify(movieRepo).save(movie)
Expand All @@ -80,7 +80,7 @@ class AppPropertiesAwareMovieRepoTest {
val movieId = 1L
`when`(appProperties.repoReadOnly()).thenReturn(true)

appPropertiesAwareMovieRepo.deleteById(movieId)
sut.deleteById(movieId)

verify(movieRepo, never()).deleteById(movieId)
}
Expand All @@ -90,7 +90,7 @@ class AppPropertiesAwareMovieRepoTest {
val movieId = 1L
`when`(appProperties.repoReadOnly()).thenReturn(false)

appPropertiesAwareMovieRepo.deleteById(movieId)
sut.deleteById(movieId)

verify(movieRepo).deleteById(movieId)
}
Expand Down
2 changes: 1 addition & 1 deletion src/test/resources/application-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ loom-webflux:
logging:
level:
uk.gleissner.loomwebflux: debug
org.hibernate.orm.cache: debug
# org.hibernate.SQL: debug

0 comments on commit 9c1cf6f

Please sign in to comment.