Skip to content

Commit

Permalink
Merge pull request #67 from szjani/feature/reactive-cache-as-operator
Browse files Browse the repository at this point in the history
Feature/reactive cache as operator
  • Loading branch information
danielimre committed Mar 11, 2021
2 parents 5809b64 + 8e5d18d commit 2074cfb
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 7 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.1.2]
### Added
- Added `ReactiveCache#cachingWith` to use as a caching operator in a reactive chain.

## [1.1.1]
### Added
- Added the legacy mockito support to the auto-configuration.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package com.hotels.molten.cache;

import java.util.function.Function;

import reactor.core.publisher.Mono;

/**
Expand Down Expand Up @@ -43,4 +45,46 @@ public interface ReactiveCache<K, V> {
* @return a completion once the operation has finished
*/
Mono<Void> put(K key, V value);

/**
* Return the value stored to {@code key} if any,
* otherwise subscribe to the given {@code Mono}
* and store its return value in the cache.
*
* <pre>
* return Mono.fromCallable(() -&gt; expensiveServiceCall(key))
* .as(serviceCallCache.cachingWith(key));
* </pre>
*
* @param key the cache key
* @return a function
*/
default Function<Mono<V>, Mono<V>> cachingWith(K key) {
return cachingWith(key, Function.identity(), Function.identity());
}

/**
* Return {@code valueFromConverter.apply(value)}, if {@code value} already stored to {@code key},
* otherwise subscribe to the given {@code Mono}
* and store its return value as {@code valueToConverter.apply(returnValue)} in the cache.
*
* <p>Useful for negative cache implementations.</p>
*
* @param key the cache key
* @param valueToConverter transforming the non-cached value to be storable, should not return null
* @param valueFromConverter transforming the cached value
* @param <U> type of the exposed value
* @return a function
*/
default <U> Function<Mono<U>, Mono<U>> cachingWith(K key,
Function<U, V> valueToConverter,
Function<V, U> valueFromConverter) {
return nonCachedMono -> get(key)
.switchIfEmpty(nonCachedMono
.map(valueToConverter)
.switchIfEmpty(Mono.defer(() -> Mono.justOrEmpty(valueToConverter.apply(null))))
.flatMap(nonCachedWrapped -> put(key, nonCachedWrapped)
.thenReturn(nonCachedWrapped)))
.flatMap(value -> Mono.justOrEmpty(valueFromConverter.apply(value)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright (c) 2020 Expedia, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.hotels.molten.cache;

import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import lombok.Value;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;

/**
* {@code ReactiveCache} contract to test the expected behaviours.
*/
public interface ReactiveCacheContract {

String VALUE = "one";
int KEY = 1;

<T> ReactiveCache<Integer, T> createCacheForContractTest();

@Test
default void shouldCacheValueViaOperator() {
ReactiveCache<Integer, String> reactiveCache = spy(createCacheForContractTest());
Mono.just(VALUE)
.as(reactiveCache.cachingWith(KEY))
.as(StepVerifier::create)
.expectSubscription()
.thenRequest(1)
.expectNext(VALUE)
.verifyComplete();

Mono.<String>error(() -> new RuntimeException("Should not be called as the value is already in cache"))
.as(reactiveCache.cachingWith(KEY))
.as(StepVerifier::create)
.expectSubscription()
.thenRequest(1)
.expectNext(VALUE)
.verifyComplete();

verify(reactiveCache, times(1)).put(KEY, VALUE);
}

@Test
default void shouldConvertValueViaOperator() {
ReactiveCache<Integer, String> reactiveCache = spy(createCacheForContractTest());
Mono.just(VALUE)
.as(reactiveCache.cachingWith(KEY, String::toUpperCase, String::toLowerCase))
.as(StepVerifier::create)
.expectSubscription()
.thenRequest(1)
.expectNext(VALUE)
.verifyComplete();

verify(reactiveCache, times(1)).put(KEY, "ONE");
}

@Test
default void shouldNotFailIfThereAreNoEmittedItems() {
ReactiveCache<Integer, String> reactiveCache = createCacheForContractTest();
Mono.<String>empty()
.as(reactiveCache.cachingWith(KEY))
.as(StepVerifier::create)
.expectSubscription()
.thenRequest(1)
.verifyComplete();
}

@Test
default void shouldWorkAsNegativeCacheViaOperator() {
ReactiveCache<Integer, StringWrapper> reactiveNegativeCache = spy(createCacheForContractTest());
Mono.<String>empty()
.as(reactiveNegativeCache.cachingWith(KEY, StringWrapper::new, StringWrapper::getValue))
.as(StepVerifier::create)
.expectSubscription()
.thenRequest(1)
.verifyComplete();

Mono.<String>error(() -> new RuntimeException("Should not be called as the value is already in cache"))
.as(reactiveNegativeCache.cachingWith(KEY, StringWrapper::new, StringWrapper::getValue))
.as(StepVerifier::create)
.expectSubscription()
.thenRequest(1)
.verifyComplete();

verify(reactiveNegativeCache, times(1)).put(KEY, new StringWrapper(null));
}

@Value
class StringWrapper {
private String value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,29 +21,35 @@
import static org.mockito.Mockito.when;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.testng.MockitoTestNGListener;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;

/**
* Unit test for {@link ReactiveMapCache}.
*/
@Listeners(MockitoTestNGListener.class)
public class ReactiveMapCacheTest {
@ExtendWith(MockitoExtension.class)
class ReactiveMapCacheTest implements ReactiveCacheContract {
private static final String VALUE = "one";
private static final int KEY = 1;
@Mock
private Map<Integer, String> cache;
@InjectMocks
private ReactiveMapCache<Integer, String> reactiveCache;

@Override
public <T> ReactiveCache<Integer, T> createCacheForContractTest() {
return new ReactiveMapCache<>(new ConcurrentHashMap<>());
}

@Test
public void shouldDelegateGetLazily() {
void shouldDelegateGetLazily() {
when(cache.get(KEY)).thenReturn(VALUE);
Mono<String> get = reactiveCache.get(KEY);
verifyNoInteractions(cache);
Expand All @@ -56,7 +62,7 @@ public void shouldDelegateGetLazily() {
}

@Test
public void shouldDelegatePutLazily() {
void shouldDelegatePutLazily() {
Mono<Void> put = reactiveCache.put(KEY, VALUE);
verifyNoInteractions(cache);
StepVerifier.create(put)
Expand Down
Empty file modified mvnw
100644 → 100755
Empty file.

0 comments on commit 2074cfb

Please sign in to comment.