/*
* (C) Copyright 2016 Nuxeo SA (http://nuxeo.com/) and others.
*
* 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.
*
* Contributors:
* Kevin Leturc
*/
package org.nuxeo.ecm.core.storage.dbs;
import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyListOf;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ID;
import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_NAME;
import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_PARENT_ID;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.nuxeo.ecm.core.storage.State;
import org.nuxeo.ecm.core.storage.State.StateDiff;
public class TestDBSCachingRepository {
private DBSCachingRepository repository;
private DBSRepository subRepository;
@Before
@SuppressWarnings("unchecked")
public void before() {
subRepository = mock(DBSRepository.class);
when(subRepository.readState(any())).then(invocation -> newState(invocation.getArguments()[0].toString()));
when(subRepository.readStates(anyListOf(String.class))).then(
invocation -> ((List<String>) invocation.getArguments()[0]).stream().map(id -> {
State state = new State();
state.setSingle(KEY_ID, id);
return state;
}).collect(Collectors.toList()));
when(subRepository.readChildState(any(), any(), any())).then(invocation -> {
Object parentId = invocation.getArguments()[0];
Object name = invocation.getArguments()[1];
State state = newState(parentId.toString() + "_" + name);
state.setSingle(KEY_PARENT_ID, parentId);
state.setSingle(KEY_NAME, name);
return state;
});
repository = new DBSCachingRepository(subRepository, newDBSRepositoryDescriptor());
}
@After
public void after() {
// Used to remove metrics
repository.shutdown();
}
@Test
public void testReadState() {
String id = "ID";
// First read - call sub repository
State dbState = repository.readState(id);
verify(subRepository, times(1)).readState(eq(id));
// Second read - call cache
State cachedState = repository.readState(id);
verify(subRepository, times(1)).readState(eq(id));
assertEquals(dbState, cachedState);
}
@Test
public void testReadStates() {
String id1 = "ID1";
String id2 = "ID2";
// First read - call sub repository
List<State> dbStates = repository.readStates(Collections.singletonList(id1));
verify(subRepository, times(1)).readStates(eq(Collections.singletonList(id1)));
// Second read - call cache for id1 and repository for id2
List<State> cachedStates = repository.readStates(Arrays.asList(id1, id2));
verify(subRepository, times(1)).readStates(eq(Collections.singletonList(id2)));
assertEquals(1, dbStates.size());
assertEquals(2, cachedStates.size());
assertEquals(dbStates.get(0), cachedStates.get(0));
List<State> states = new ArrayList<>(dbStates);
states.add(cachedStates.get(1));
assertEquals(states, cachedStates);
}
@Test
public void testUpdateState() {
String id = "ID";
// First add a state in cache
repository.readState(id);
repository.readState(id);
verify(subRepository, times(1)).readState(eq(id));
// Second update this state
repository.updateState(id, mock(StateDiff.class), null);
verify(subRepository, times(1)).updateState(eq(id), any(), any());
// Check state is no longer in cache
repository.readState(id);
verify(subRepository, times(2)).readState(eq(id));
}
@Test
public void testDeleteStates() {
String id = "ID";
// First add a state in cache
repository.readState(id);
repository.readState(id);
verify(subRepository, times(1)).readState(eq(id));
// Second delete this state
repository.deleteStates(Collections.singleton(id));
verify(subRepository, times(1)).deleteStates(eq(Collections.singleton(id)));
// Check state is no longer in cache
repository.readState(id);
verify(subRepository, times(2)).readState(eq(id));
}
@Test
public void testReadChildState() {
String parentId = "PARENT-ID";
String name = "NAME";
String id = parentId + "_" + name;
// First read - call sub repository
State dbState = repository.readChildState(parentId, name, Collections.emptySet());
verify(subRepository, times(1)).readChildState(eq(parentId), eq(name), any());
// Second read - call cache
State cachedState = repository.readChildState(parentId, name, Collections.emptySet());
verify(subRepository, times(1)).readChildState(eq(parentId), eq(name), any());
assertEquals(dbState, cachedState);
// Third read cached state from cache with readState
cachedState = repository.readState(id);
assertEquals(dbState, cachedState);
}
private State newState(String id) {
State state = new State();
state.setSingle(KEY_ID, id);
return state;
}
private DBSRepositoryDescriptor newDBSRepositoryDescriptor() {
DBSRepositoryDescriptor descriptor = new DBSRepositoryDescriptor();
descriptor.setCacheEnabled(true);
descriptor.cacheTTL = 10L;
descriptor.cacheMaxSize = 1000L;
descriptor.cacheConcurrencyLevel = 1000;
return descriptor;
}
}