lunes, 18 de octubre de 2021

Spring boot, cache con ttl diferente y reactor

 La idea es mostrar como tener un cache en memoria con caffeine(una forma de hacer cache en spring de manera muy optima) funcional en spring boot, usando reactor(akka flux/mono) y que podamos definir diferentes ttl(time to live, o tiempo de vida es decir cuando dura el cache hasta que deje de ser valido y se borre) para diferentes caches en una aplicacion.

El cache es para mi una estrategia de optimizacion que es conveniente usar en varios casos por ejemplo:

  • si tenemos una respuesta de un consumo de una api que es valida por 10 horas, pero la usamos cada hora podriamos hacer un cache que viva 9 horas pues asi no ahorramos la espera en cada uno de los consumos pues guardamos los resultados en una capa de memoria intermedia
  • si vemos que nuestra aplicacion va consumir de manera intensiva otra aplicacion y no queremos que la otra api tenga que procesar esos niveles de carga(haciendole un dos no intencionado).
  • si estamos preocupados por el tiempo que va tomar un proceso en nuestra aplicacion y es aceptable tener información no actualizada para disminuir el tiempo que toma hacer una operación.

el primer llamado a un servicio puede lucir asi

 

pero luego de que el servicio esta cacheado nos ahorramos el llamado al servicio usando la información del llamado anterior.

en una aplicacion podemos tener diferentes servicios con diferentes necesidades y por tanto también podemos necesitar que unos caches vivan mas o menos que otros. por ejemplo si tenemos un cache para una apikey que nos sirve por 5 minutos y otro servicio que lo cacheamos por que lo llamamos muy seguido y lo cacheamos 5 segundos. por eso en algunos casos hace sentido que tengamos diferentes tiempo de vida.


bueno para hacer la demo lo primero que vamos a hacer es ir a https://start.spring.io/ y vamos a crear un proyecto con gradle, kotlin, spring boot 2.5.5 group com.cacheejemplo, nombre cacheejemplo, packaging jar, y java 17.

agregamos la dependencia de spring reactive web nos queda algo asi.


los descargamos, descomprimimos y abrimos en mi caso con intellij ide.

para usar caffeine un gestor de cache muy bueno vamos a agregarlo en el archivo build.gradle.kts en dependencies.

implementation("com.github.ben-manes.caffeine:caffeine")
implementation( "org.springframework:spring-context-support")

 Colocamos la anotacion @EnableCaching en el archivo  CacheejemploApplication (para que spring nos ponga a funcionar el cache), ademas de la anotacion @ConfigurationPropertiesScan para que podamos definir nuestra configuracion para cada uno de los caches nos queda asi.

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.boot.runApplication
import org.springframework.cache.annotation.EnableCaching

@SpringBootApplication
@EnableCaching
@ConfigurationPropertiesScan
class CacheejemploApplication

fun main(args: Array<String>) {
runApplication<CacheejemploApplication>(*args)
}

 luego vamos a crear un archivo llamado CacheStockApi.kt donde vamos a definir nuestros cache consumiendo estoy usando una api que encontre en el internet  que devuelve lastUpdateAt por que me servia para validar manualmente que las respuestas se estaban cacheando y no requeria autentificacion ni nada https://walltime.info/api.html

import com.cacheejmplo.demo.CacheStockApi.CacheStockApi.min1Cache
import com.cacheejmplo.demo.CacheStockApi.CacheStockApi.min5Cache
import org.springframework.cache.annotation.CacheConfig
import org.springframework.cache.annotation.Cacheable
import org.springframework.http.MediaType
import org.springframework.stereotype.Service
import org.springframework.web.reactive.function.client.WebClient
import reactor.core.publisher.Mono

@Service
@CacheConfig(cacheNames=[min5Cache,min1Cache])
 
class CacheStockApi {

val webClient = WebClient.create("https://s3.amazonaws.com")
@Cacheable(min5Cache)
fun getApiInfo5Min(): Mono<String> =
getApiInfo().cache();


@Cacheable(min1Cache)
fun getApiInfo1Min(): Mono<String> =
getApiInfo().cache();


    private fun getApiInfo(): Mono<String> {
    return webClient.get().uri("/data-production-walltime-info/production/dynamic/walltime-info.json?now=1528962473468.679.0000000000873")
     .accept(MediaType.APPLICATION_JSON).retrieve().bodyToMono(String::class.java);
    }
object CacheStockApi{
const val min5Cache="5min"
const val min1Cache="1min"
}
}

 

aqui resaltar poner la anotacion @CacheConfig arriba de la clase con los nombres de los diferentes caches que vamos a usar, luego poner en cada metodo que queremos cachear poner la anotacion @Cacheable con el nombre del cache para ese metodo, y luego .cache al Mono que nos retorna la respuesta(esto es un hack para lograr que el cache nos funcione con spring https://stackoverflow.com/questions/48156424/spring-webflux-and-cacheable-proper-way-of-caching-result-of-mono-flux-type/48156827#comment122359092_48156827


lo siguiente es entonces habilitar estas configuraciones para el cache para estos vamos a crear un archivo CacheConfig.kt con el siguiente contenido.

import com.github.benmanes.caffeine.cache.Caffeine
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.ConstructorBinding
import org.springframework.cache.CacheManager
import org.springframework.cache.support.SimpleCacheManager
import org.springframework.context.annotation.Bean
import java.util.concurrent.TimeUnit
import java.util.stream.Collectors
import com.github.benmanes.caffeine.cache.Ticker
import org.springframework.cache.caffeine.CaffeineCache

@ConstructorBinding
@ConfigurationProperties(prefix = "caching")
data class CacheConfig(
val specs: Map<String, CacheSpecification>
) {
private val logger: Logger = LoggerFactory.getLogger(CacheejemploApplication::class.java)

@Bean
fun cacheManager(ticker: Ticker): CacheManager {
val manager = SimpleCacheManager()
if (specs != null) {
val caches = specs.entries.stream()
.map { (key, value): Map.Entry<String, CacheSpecification> ->
buildCache(
key,
value,
ticker
)
}.collect(Collectors.toList())
manager.setCaches(caches)
}
return manager
}

private fun buildCache(name: String, cacheSpec: CacheSpecification, ticker: Ticker): CaffeineCache {
logger.info("Cache {} specified timeout of {} min, max of {}", name, cacheSpec.timeoutSeconds?:0, cacheSpec.maxEntries)
val caffeineBuilder: Caffeine<Any, Any> = Caffeine.newBuilder()
.expireAfterWrite(cacheSpec.timeoutSeconds, TimeUnit.SECONDS).maximumSize(cacheSpec.maxEntries)
.ticker(ticker)
return CaffeineCache(name, caffeineBuilder.build())
}

@Bean
fun ticker(): Ticker {
return Ticker.systemTicker()
}
}

data class CacheSpecification(
val timeoutSeconds: Long,
val maxEntries: Long
)


con esto lo que logramos es usar caffeine como cache manager y habilitar que le pasemos configuracion al cache para que cada nombre de cache tenga su tiempo de vida en este caso en segundos.

vamos entonces a definir las configuraciones de nuestro cache en el archivo src.main.resources.application.properties

spring.main.allow-bean-definition-overriding=true
caching.specs.5min.timeoutSeconds=300
caching.specs.5min.maxEntries=200
caching.specs.1min.timeoutSeconds=60
caching.specs.1min.maxEntries=200

la linea de bean definition overriding es importante para que la aplicacion no nos falle, luego definimos por cada nombre de cache de antes cuanto tiempo dura en segundo y cuantas entradas queremos que mantenga maximas. en este caso definimos 2 caches uno de 5 minutos(60 segundos * 5=300) y otro de 1 minuto.

ya con esto listo para poder probarlo vamos a crear un archivo en src.main.kotlin.cacheejemplo.demo como controlador rest para usar nuestro cache. creamos un archivo ControladorApi.kt y pasamos el siguiente contenido.

import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@RestController
class ControladorApi(val cacheStockApi: CacheStockApi) {

@GetMapping("/5min")
fun min5()=
cacheStockApi.getApiInfo5Min()


@GetMapping("/1min")
fun min1()=
cacheStockApi.getApiInfo1Min()
}

ya con esto podemos correr la aplicacion, probar nuestro cache y revisar que unas respuestas se guardan por 5 minutos y otras solo 1 minuto.

localhost:8080/5min

localhost:8080/1min

quiero recordar que para que el cache funcione debe hacerse un llamado desde otro clase, cuando se llama a un método cacheado desde la misma clase spring no lo coge(no lo cachea lo llama)y es como si el metodo no fuera cacheado. por eso es mejor crear una clase que solo se encargue de cachear para que siempre la podamos llamar desde afuera(desde otra clase). tambien recordar poner todas las anotaciones relacionadas al cache EnabledCaching, CacheConfig, Cacheable y el .cache al Flux/mono para que el cache funcione.

 este mismo proceso en video https://youtu.be/GmCBTNpVDRo

referencias:

.cache() para el mono extraida de https://stackoverflow.com/questions/48156424/spring-webflux-and-cacheable-proper-way-of-caching-result-of-mono-flux-type/48156827#comment122359092_48156827 respondida por Oleh Dokuka.

y la configuracion base para el cache con multiples tiempos de vida extraida de https://stackoverflow.com/questions/49885064/is-it-possible-to-set-a-different-specification-per-cache-using-caffeine-in-spri/61096118#comment122304131_61096118 respondida por nokieng


miércoles, 15 de septiembre de 2021

Flyway, r2dbc y spring boot

realizacion en video: https://www.youtube.com/watch?v=lJE5tkSmUPk


Flyway es una herramienta para versionar los cambios en base de datos que sirve para parecido a lo que se hace con git mantener un historial de los cambios en la base de datos y aplicarlos con facilidad.

Para usar flyway con spring lo primero es crear una aplicación de spring para esto nos dirijimos a https://start.spring.io/ ya estando hay vamos a seleccionar un proyecto gradle, con kotlin, en la versión 2.5.4(al momento que hice el tutorial), le vamos a poner el nombre flywaydemo y el package com.example.flywaydemo(si esto cambia solo es cambiar en los ejemplos la ruta de los package), packaging war y versión de java 16.

Luego vamos a dar clic en ADD DEPENDENCIES y vamos a seleccionar las dependencias de Spring Reactive Web(para que podamos trabajar con programación reactiva, flux, monos etc), Spring Data R2DBC como driver reactivo para conectarnos a la base de datos,Flyway Migration y postgresql. tal como se muestra en la imagen

 



En la imagen me faltó agregar postgresql pero agregarlo también.

teniendo escogido todo esto vamos a descargar el proyecto base y abrirlos con nuestro editor favorito de java en mi caso intellij y vamos a crear un archivo docker-compose que nos permita subir la base de datos postgres en docker de manera fácil para esto vamos a crear un archivo en la carpeta del proyecto llamado docker-compose.yml que tenga el siguiente contenido.
version: '3'
services:
postgresdb:
image: postgres
ports:
- "5432:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: admin
POSTGRES_DB: flywaydemo


y vamos a correr en la terminal

$docker-compose up -d

esto nos va correr postgres con los valores que tenemos en el archivo y lo va subir listo para usarlo

lo siguiente que vamos a hacer es crear una carpeta configuration dentro de src/main/kotlin/com/example/flywayDemo/configuration/ y dentro creamos un archivo FlywayConfig como clase de kotlin con el siguiente contenido
package com.example.flywayDemo.configuration

import org.flywaydb.core.Flyway
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class FlywayConfig(@Value("\${spring.flyway.url}")private val url: String,
@Value("\${spring.flyway.user}")private val user: String,
@Value("\${spring.flyway.password}")private val password: String,
) {
@Bean(initMethod = "migrate")
fun flyway(): Flyway? = Flyway(Flyway.configure().baselineOnMigrate(true).dataSource(url, user, password))
}


esta clase es para que flyway interactue correctamente con r2dbc y fue sacada de aca https://stackoverflow.com/questions/59553647/how-to-run-flyway-migration-for-reactive-r2dbc-driver-on-sprintboot-stratup

luego entonces vamos a crear un archivo src/main/resources con el nombre application.yml donde vamos a poner la configuracion de nuestra aplicación tanto de flyway como de la conexion a postgres colocando el siguiente contenido
logging:
level:
root: INFO
spring:
r2dbc:
url: 'r2dbc:postgresql://localhost:5432/flywaydemo'
username: 'postgres'
password: 'admin'
pool:
validation-query: 'SELECT 1'

flyway:
url: 'jdbc:postgresql://localhost:5432/flywaydemo'
user: 'postgres'
password: 'admin'
schemas: 'public'
baseline-on-migrate: true


Teniendo esto listo vamos a crear una tabla de prueba para que flyway la cree en la base de datos dentro de la carpeta src/main/resources/db/migration un archivo llamado V20210915082200__init.sql en este caso estamos siguiendo el formato para nombrar el archivo de AÑO MES DIA HORA_MILITAR MINUTO SEGUNDO dos guión bajo <cualquier nombre> . sql, la idea es que con esto podemos saber cuando se hizo cada cambio con la fecha y que flyway pueda ordenar la aplicación de los registros dentro de este archivo vamos a poner la creación de una tabla simple.
CREATE TABLE public.demouser
(
"id" BIGSERIAL PRIMARY KEY,
"username" text NOT NULL,
"password" text NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT now(),
"last_updated_at" timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX IF NOT EXISTS demouser_userName_index ON public.demouser ("username");


en esta misma carpeta también vamos a colocar otro archivo llamado V20210915083000__registro.sql con el siguiente contenido
insert into public.demouser(username,password) values('sebastian','miclave')


teniendo esto listo ya no es sino correr la aplicación y conectarnos a la database que estamos corriendo con docker para revisar en este caso yo me voy a conectar usando DBeaver con las credenciales que se definieron en el archivo docker-compose

 


y correr un script como este

select * from public.demouser;

Con esto verificamos que flyway se encargó por nosotros de crear la tabla e insertar el registro, los cambios futuros en la base de datos se crean como archivos independientes respetando el formato y haciendo las modificaciones en la base de datos como haríamos en la consola de postgresql de esta manera mantenemos los cambios versionados.

Solo una nota más si es posible recomiendo que los nombres de propiedades en la tabla no tengan mayúsculas para evitar problemas tengo entendido que tambien tiene solucion pero entra en juego otra configuracion para el formatter