/* * 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 ); } } } }