org.pentaho.platform.plugin.services.metadata.PentahoMetadataDomainRepositoryConcurrencyTest.java Source code

Java tutorial

Introduction

Here is the source code for org.pentaho.platform.plugin.services.metadata.PentahoMetadataDomainRepositoryConcurrencyTest.java

Source

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