Updates and payments additions
This commit is contained in:
111
src/main/java/com/test/payment/service/TokenCacheService.java
Normal file
111
src/main/java/com/test/payment/service/TokenCacheService.java
Normal file
@@ -0,0 +1,111 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user