112 lines
4.8 KiB
Java
112 lines
4.8 KiB
Java
package com.test.payment.service;
|
|
|
|
import com.test.payment.models.ProviderToken;
|
|
import com.test.payment.repository.ProviderTokenRepository;
|
|
import lombok.RequiredArgsConstructor;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.springframework.core.env.Environment;
|
|
import org.springframework.data.redis.core.ReactiveStringRedisTemplate;
|
|
import org.springframework.stereotype.Service;
|
|
import reactor.core.publisher.Mono;
|
|
|
|
import java.time.Duration;
|
|
import java.time.LocalDateTime;
|
|
import java.util.function.Supplier;
|
|
|
|
/**
|
|
* Tiered OAuth token cache shared by all providers: Redis first, then the database,
|
|
* then a live fetch (persisted to both tiers on success). Redis being down never
|
|
* fails a request — reads/writes time out and degrade to the next tier.
|
|
*/
|
|
@Service
|
|
@RequiredArgsConstructor
|
|
@Slf4j
|
|
public class TokenCacheService {
|
|
|
|
private static final Duration REDIS_TIMEOUT = Duration.ofMillis(800);
|
|
|
|
private final ProviderTokenRepository tokenRepository;
|
|
private final ReactiveStringRedisTemplate redisTemplate;
|
|
private final Environment environment;
|
|
|
|
public record FetchedToken(String accessToken, long expiresInSeconds) {
|
|
}
|
|
|
|
public Mono<String> getToken(String provider, Supplier<Mono<FetchedToken>> fetcher) {
|
|
return fromRedis(provider)
|
|
.switchIfEmpty(Mono.defer(() -> fromDatabase(provider)))
|
|
.switchIfEmpty(Mono.defer(() -> fetchAndStore(provider, fetcher)));
|
|
}
|
|
|
|
/**
|
|
* Drops the cached token from Redis and the database (used on 401 from the provider).
|
|
*/
|
|
public Mono<Void> evictToken(String provider) {
|
|
return redisTemplate.opsForValue().delete(redisKey(provider))
|
|
.timeout(REDIS_TIMEOUT)
|
|
.onErrorResume(e -> {
|
|
log.warn("[{}] Redis unavailable while evicting token: {}", provider, e.toString());
|
|
return Mono.just(false);
|
|
})
|
|
.then(tokenRepository.deleteByProvider(provider));
|
|
}
|
|
|
|
private Mono<String> fromRedis(String provider) {
|
|
return redisTemplate.opsForValue().get(redisKey(provider))
|
|
.timeout(REDIS_TIMEOUT)
|
|
.doOnNext(t -> log.debug("[{}] token served from Redis", provider))
|
|
.onErrorResume(e -> {
|
|
log.warn("[{}] Redis unavailable for token read ({}) — falling back to database", provider, e.toString());
|
|
return Mono.empty();
|
|
});
|
|
}
|
|
|
|
private Mono<String> fromDatabase(String provider) {
|
|
int buffer = expiryBufferSeconds();
|
|
return tokenRepository.findFirstByProviderAndExpiresAtAfterOrderByIdDesc(provider, LocalDateTime.now().plusSeconds(buffer))
|
|
.flatMap(token -> {
|
|
long ttl = Duration.between(LocalDateTime.now(), token.getExpiresAt()).getSeconds() - buffer;
|
|
log.debug("[{}] token served from database", provider);
|
|
return cacheInRedis(provider, token.getAccessToken(), ttl).thenReturn(token.getAccessToken());
|
|
});
|
|
}
|
|
|
|
private Mono<String> fetchAndStore(String provider, Supplier<Mono<FetchedToken>> fetcher) {
|
|
return fetcher.get()
|
|
.flatMap(fetched -> {
|
|
ProviderToken token = ProviderToken.builder()
|
|
.provider(provider)
|
|
.accessToken(fetched.accessToken())
|
|
.expiresAt(LocalDateTime.now().plusSeconds(fetched.expiresInSeconds()))
|
|
.createdAt(LocalDateTime.now())
|
|
.build();
|
|
log.info("[{}] fetched new access token (expires in {}s)", provider, fetched.expiresInSeconds());
|
|
return tokenRepository.save(token)
|
|
.then(cacheInRedis(provider, fetched.accessToken(), fetched.expiresInSeconds() - expiryBufferSeconds()))
|
|
.thenReturn(fetched.accessToken());
|
|
});
|
|
}
|
|
|
|
private Mono<Void> cacheInRedis(String provider, String token, long ttlSeconds) {
|
|
if (ttlSeconds <= 0) {
|
|
return Mono.empty();
|
|
}
|
|
return redisTemplate.opsForValue().set(redisKey(provider), token, Duration.ofSeconds(ttlSeconds))
|
|
.timeout(REDIS_TIMEOUT)
|
|
.doOnNext(ok -> log.debug("[{}] token cached in Redis for {}s", provider, ttlSeconds))
|
|
.onErrorResume(e -> {
|
|
log.warn("[{}] Redis unavailable for token write ({}) — token persisted in database only", provider, e.toString());
|
|
return Mono.just(false);
|
|
})
|
|
.then();
|
|
}
|
|
|
|
private String redisKey(String provider) {
|
|
return provider.toLowerCase() + ":access_token";
|
|
}
|
|
|
|
private int expiryBufferSeconds() {
|
|
return environment.getProperty("payments.token-expiry-buffer-seconds", Integer.class, 60);
|
|
}
|
|
}
|