/*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU General Public License, version 2 as published by the Free Software
* Foundation.
*
* You should have received a copy of the GNU General Public License along with this
* program; if not, you can obtain a copy at http://www.gnu.org/licenses/gpl-2.0.html
* or from the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
*
* Copyright 2006 - 2015 Pentaho Corporation. All rights reserved.
*/
package org.pentaho.platform.plugin.services.metadata;
import org.apache.commons.lang.reflect.FieldUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.pentaho.metadata.model.Domain;
import org.pentaho.metadata.util.XmiParser;
import org.pentaho.platform.api.repository2.unified.IAclNodeHelper;
import org.pentaho.platform.api.repository2.unified.IRepositoryFileData;
import org.pentaho.platform.api.repository2.unified.IUnifiedRepository;
import org.pentaho.platform.api.repository2.unified.RepositoryFile;
import org.pentaho.platform.api.repository2.unified.RepositoryFileAcl;
import org.pentaho.platform.api.repository2.unified.RepositoryRequest;
import org.pentaho.platform.api.repository2.unified.data.simple.SimpleRepositoryFileData;
import org.pentaho.test.platform.repository2.unified.EmptyUnifiedRepository;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
/**
* @author Andrey Khayrutdinov
*/
public class PentahoMetadataDomainRepositoryConcurrencyTest {
private static final String METADATA_DIR_ID = "metadataDirId";
private DomainsStubRepository repository;
private IAclNodeHelper aclNodeHelper;
private PentahoMetadataDomainRepository domainRepository;
@SuppressWarnings( "unchecked" )
@Before
public void setUp() throws Exception {
repository = new DomainsStubRepository();
repository = spy( repository );
RepositoryFile metadataDir = new RepositoryFile.Builder( METADATA_DIR_ID, "metadataDir" ).folder( true ).build();
doReturn( metadataDir ).when( repository ).getFile( PentahoMetadataDomainRepositoryInfo.getMetadataFolderPath() );
aclNodeHelper = mock( IAclNodeHelper.class );
when( aclNodeHelper.canAccess( any( RepositoryFile.class ), any( EnumSet.class ) ) ).thenReturn( true );
domainRepository = new PentahoMetadataDomainRepository( repository );
domainRepository = spy( domainRepository );
doReturn( aclNodeHelper ).when( domainRepository ).getAclHelper();
}
@SuppressWarnings( "unchecked" )
@After
public void cleanUp() throws Exception {
Map<IUnifiedRepository, ?> metaMapStore =
(Map<IUnifiedRepository, ?>) FieldUtils
.readStaticField( PentahoMetadataDomainRepository.class, "metaMapStore", true );
if ( metaMapStore != null ) {
metaMapStore.remove( repository );
}
repository = null;
aclNodeHelper = null;
domainRepository = null;
}
@Test
public void getMetadataRepositoryFile_TenReaders() throws Exception {
final int amountOfReaders = 10;
final int cycles = 30;
final int amountOfDomains = amountOfReaders - 1;
createRepositoryFiles( amountOfDomains );
List<FilesLookuper> readers = new ArrayList<FilesLookuper>( amountOfReaders );
for ( int i = 0; i < amountOfDomains; i++ ) {
readers.add( new FilesLookuper( domainRepository, generateDomainId( i ), cycles, true ) );
}
readers.add( new FilesLookuper( domainRepository, "non-existing domain", cycles, false ) );
// randomizing the order of readers
Collections.shuffle( readers );
runTest( readers );
}
@Test
public void getDomainIds_TenReaders() throws Exception {
final int amountOfReaders = 10;
final int cycles = 30;
createRepositoryFiles( amountOfReaders );
Set<String> ids = new HashSet<String>( amountOfReaders );
for ( int i = 0; i < amountOfReaders; i++ ) {
ids.add( generateDomainId( i ) );
}
ids = Collections.unmodifiableSet( ids );
List<IdsLookuper> readers = new ArrayList<IdsLookuper>( amountOfReaders );
for ( int i = 0; i < amountOfReaders; i++ ) {
readers.add( new IdsLookuper( domainRepository, ids, cycles ) );
}
runTest( readers );
}
@Test
public void addDomain_getDomain_Simultaneously() throws Exception {
final int readersAmount = 10;
final int cycles = 30;
final int addersAmount = 20;
createRepositoryFiles( readersAmount );
doAnswer( new Answer() {
@Override public Object answer( InvocationOnMock invocation ) throws Throwable {
String domainId = (String) invocation.getArguments()[ 0 ];
repository.createFile( null, createRepositoryFile( domainId ), null, null );
repository.setFileMetadata( domainId, generateMetadataFor( domainId ) );
return null;
}
} ).when( domainRepository ).createUniqueFile( anyString(), anyString(), any( SimpleRepositoryFileData.class ) );
domainRepository.setXmiParser( mockXmiParser() );
List<Callable<String>> actors = new ArrayList<Callable<String>>( readersAmount + addersAmount * 2 );
for ( int i = 0; i < readersAmount; i++ ) {
actors.add( new FilesLookuper( domainRepository, generateDomainId( i ), cycles, true ) );
}
for ( int i = 0; i < addersAmount; i++ ) {
int index = i + readersAmount;
String domainId = generateDomainId( index );
AtomicBoolean condition = new AtomicBoolean( true );
AtomicBoolean addedFlag = new AtomicBoolean( false );
actors.add( new DomainAdder( domainRepository, domainId, condition, addedFlag ) );
actors.add( new DomainLookuper( domainRepository, domainId, condition, addedFlag ) );
}
Collections.shuffle( actors );
runTest( actors );
}
private XmiParser mockXmiParser() throws Exception {
XmiParser parser = mock( XmiParser.class );
when( parser.generateXmi( any( Domain.class ) ) ).thenReturn( "" );
when( parser.parseXmi( any( InputStream.class ) ) ).thenAnswer( new Answer<Domain>() {
@Override public Domain answer( InvocationOnMock invocation ) throws Throwable {
return new Domain();
}
} );
return parser;
}
private void runTest( final List<? extends Callable<String>> actors ) throws Exception {
List<String> errors = new ArrayList<String>();
ExecutorService executorService = Executors.newFixedThreadPool( actors.size() );
try {
CompletionService<String> completionService = new ExecutorCompletionService<String>( executorService );
for ( Callable<String> reader : actors ) {
completionService.submit( reader );
}
for ( int i = 0; i < actors.size(); i++ ) {
Future<String> take = completionService.take();
String result;
try {
result = take.get();
} catch ( ExecutionException e ) {
result = "Execution exception: " + e.getMessage();
}
if ( result != null ) {
errors.add( result );
}
}
} finally {
executorService.shutdown();
}
if ( !errors.isEmpty() ) {
StringBuilder builder = new StringBuilder();
builder.append( "The following errors occurred: \n" );
for ( String error : errors ) {
builder.append( error ).append( '\n' );
}
fail( builder.toString() );
}
}
private void createRepositoryFiles( int amountOfDomains ) {
for ( int i = 0; i < amountOfDomains; i++ ) {
RepositoryFile file = createRepositoryFile( generateDomainId( i ) );
repository.createFile( null, file, null, null );
Map<String, Serializable> metadata = generateMetadataFor( file.getId() );
repository.setFileMetadata( file.getId(), metadata );
}
}
private Map<String, Serializable> generateMetadataFor( Serializable id ) {
Map<String, Serializable> metadata = new HashMap<String, Serializable>();
metadata.put( "file-type", "domain" );
metadata.put( "domain-id", id );
return metadata;
}
private static String generateDomainId( int index ) {
return "domain_" + index;
}
private static RepositoryFile createRepositoryFile( String id ) {
return new RepositoryFile.Builder( id, id ).build();
}
private static class FilesLookuper implements Callable<String> {
private final PentahoMetadataDomainRepository domainRepository;
private final String domainId;
private final int cycles;
private final boolean expectNotNull;
public FilesLookuper( PentahoMetadataDomainRepository domainRepository, String domainId, int cycles,
boolean expectNotNull ) {
this.domainRepository = domainRepository;
this.domainId = domainId;
this.cycles = cycles;
this.expectNotNull = expectNotNull;
}
@Override
public String call() throws Exception {
for ( int i = 0; i < cycles; i++ ) {
RepositoryFile file = domainRepository.getMetadataRepositoryFile( domainId );
if ( expectNotNull ) {
if ( file == null ) {
return String.format( "Expected to obtain existing domain: [%s]", domainId );
}
} else {
if ( file != null ) {
return String.format( "Expected to obtain null for non-existing domain: [%s]", domainId );
}
}
}
return null;
}
}
private static class IdsLookuper implements Callable<String> {
private final PentahoMetadataDomainRepository domainRepository;
private final Set<String> expectedIds;
private final int cycles;
public IdsLookuper( PentahoMetadataDomainRepository domainRepository, Set<String> expectedIds, int cycles ) {
this.domainRepository = domainRepository;
this.expectedIds = expectedIds;
this.cycles = cycles;
}
@Override
public String call() throws Exception {
for ( int i = 0; i < cycles; i++ ) {
Set<String> domainIds = domainRepository.getDomainIds();
if ( domainIds.size() != expectedIds.size() ) {
return error( domainIds );
} else {
Set<String> tmp = new HashSet<String>( expectedIds );
tmp.removeAll( domainIds );
if ( !tmp.isEmpty() ) {
return error( domainIds );
}
}
}
return null;
}
private String error( Set<String> domainIds ) {
return String.format( "Expected to obtain [%s], but got [%s]", expectedIds, domainIds );
}
}
private static class DomainLookuper implements Callable<String> {
private final PentahoMetadataDomainRepository domainRepository;
private final String domainId;
private final AtomicBoolean continueCondition;
private final AtomicBoolean addedFlag;
public DomainLookuper( PentahoMetadataDomainRepository domainRepository, String domainId,
AtomicBoolean continueCondition, AtomicBoolean addedFlag ) {
this.domainRepository = domainRepository;
this.domainId = domainId;
this.continueCondition = continueCondition;
this.addedFlag = addedFlag;
}
@Override
public String call() throws Exception {
while ( continueCondition.get() ) {
if ( addedFlag.get() ) {
Domain domain = domainRepository.getDomain( domainId );
if ( domain == null ) {
return String.format( "Expected to obtain [%s], but got null", domainId );
}
} else {
Domain domain = domainRepository.getDomain( domainId );
if ( domain != null ) {
// the reason we are doing such tricky hack is that the flag is not set inside
// a transaction with storing domain, in other words, it is possible that domain has been already stored,
// but the flag is not yet set
// it is a drawback of testing approach and it is hardly can occur in real application
Thread.sleep( 200 );
if ( !addedFlag.get() ) {
return String.format( "Expected not to find domain [%s], but got it", domainId );
}
}
}
}
return null;
}
}
private static class DomainAdder implements Callable<String> {
private final PentahoMetadataDomainRepository domainRepository;
private final String domainId;
private final AtomicBoolean continueCondition;
private final AtomicBoolean addedFlag;
public DomainAdder( PentahoMetadataDomainRepository domainRepository, String domainId,
AtomicBoolean continueCondition, AtomicBoolean addedFlag ) {
this.domainRepository = domainRepository;
this.domainId = domainId;
this.continueCondition = continueCondition;
this.addedFlag = addedFlag;
}
@Override
public String call() throws Exception {
try {
// sleep for a while to give lookupers a possibility to get nulls
Thread.sleep( 2000 + new Random().nextInt( 500 ) );
Domain domain = new Domain();
domain.setId( domainId );
domainRepository.storeDomain( domain, false );
addedFlag.set( true );
} finally {
continueCondition.set( false );
}
return null;
}
}
private static class DomainsStubRepository extends EmptyUnifiedRepository {
private final List<RepositoryFile> files;
private final Map<Serializable, Map<String, Serializable>> metadatas;
public DomainsStubRepository() {
this.files = new ArrayList<RepositoryFile>();
this.metadatas = new HashMap<Serializable, Map<String, Serializable>>();
}
@Override
public List<RepositoryFile> getChildren( Serializable folderId ) {
return getChildren( folderId, null );
}
@Override
public List<RepositoryFile> getChildren( Serializable folderId, String filter ) {
return getChildren( folderId, null, null );
}
@Override
public List<RepositoryFile> getChildren( Serializable folderId, String filter,
Boolean showHiddenFiles ) {
return getChildren( (RepositoryRequest) null );
}
@Override
public synchronized List<RepositoryFile> getChildren( RepositoryRequest repositoryRequest ) {
emulateJcrDelay();
return new ArrayList<RepositoryFile>( files );
}
@Override
public RepositoryFile createFile( Serializable parentFolderId, RepositoryFile file,
IRepositoryFileData data, String versionMessage ) {
return this.createFile( parentFolderId, file, data, null, versionMessage );
}
@Override
public synchronized RepositoryFile createFile( Serializable parentFolderId, RepositoryFile file,
IRepositoryFileData data, RepositoryFileAcl acl,
String versionMessage ) {
emulateJcrDelay();
files.add( file );
return file;
}
@Override
public synchronized Map<String, Serializable> getFileMetadata( Serializable fileId ) {
return metadatas.get( fileId );
}
@Override
public synchronized void setFileMetadata( Serializable fileId, Map<String, Serializable> metadataMap ) {
metadatas.put( fileId, metadataMap );
}
@Override
public synchronized <T extends IRepositoryFileData> T getDataForRead( Serializable fileId, Class<T> dataClass ) {
return (T) new SimpleRepositoryFileData( new ByteArrayInputStream( new byte[ 0 ] ), "utf-8", null );
}
private void emulateJcrDelay() {
try {
Thread.sleep( new Random().nextInt( 10 ) );
} catch ( Exception e ) {
throw new RuntimeException( e );
}
}
}
}