/*! ******************************************************************************
*
* Pentaho Data Integration
*
* Copyright (C) 2002-2016 by Pentaho : http://www.pentaho.com
*
*******************************************************************************
*
* 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 org.pentaho.di.concurrency;
import org.junit.BeforeClass;
import org.junit.Test;
import org.pentaho.di.core.KettleClientEnvironment;
import org.pentaho.di.core.row.RowMeta;
import org.pentaho.di.core.row.ValueMetaInterface;
import org.pentaho.di.core.row.value.ValueMetaString;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
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.atomic.AtomicBoolean;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
/**
* @author Andrey Khayrutdinov
*/
public class RowMetaConcurrencyTest {
private static final int cycles = 50;
@BeforeClass
public static void initKettle() throws Exception {
KettleClientEnvironment.init();
}
@Test
public void fiveAddersAgainstTenReaders() throws Exception {
final int addersAmount = 5;
final int readersAmount = 10;
final AtomicBoolean condition = new AtomicBoolean( true );
final RowMeta rowMeta = new RowMeta();
List<Adder> adders = new ArrayList<Adder>( addersAmount );
for ( int i = 0; i < addersAmount; i++ ) {
adders.add( new Adder( condition, rowMeta, cycles, "adder" + i ) );
}
List<Getter> getters = new ArrayList<Getter>( readersAmount );
for ( int i = 0; i < readersAmount; i++ ) {
getters.add( new Getter( condition, rowMeta ) );
}
ConcurrencyTestRunner<List<ValueMetaInterface>, ?> runner =
new ConcurrencyTestRunner<List<ValueMetaInterface>, Object>( adders, getters, condition );
runner.runConcurrentTest();
runner.checkNoExceptionRaised();
Set<ValueMetaInterface> results = new HashSet<ValueMetaInterface>( cycles * addersAmount );
for ( List<ValueMetaInterface> list : runner.getMonitoredTasksResults() ) {
results.addAll( list );
}
List<ValueMetaInterface> metas = rowMeta.getValueMetaList();
assertEquals( cycles * addersAmount, metas.size() );
assertEquals( cycles * addersAmount, results.size() );
for ( ValueMetaInterface meta : metas ) {
assertTrue( meta.getName(), results.remove( meta ) );
}
assertTrue( results.isEmpty() );
}
@Test
public void fiveShufflersAgainstTenSearchers() throws Exception {
final int elementsAmount = 100;
final int shufflersAmount = 5;
final int searchersAmount = 10;
final RowMeta rowMeta = new RowMeta();
for ( int i = 0; i < elementsAmount; i++ ) {
rowMeta.addValueMeta( new ValueMetaString( "meta_" + i ) );
}
final AtomicBoolean condition = new AtomicBoolean( true );
List<Shuffler> shufflers = new ArrayList<Shuffler>( shufflersAmount );
for ( int i = 0; i < shufflersAmount; i++ ) {
shufflers.add( new Shuffler( condition, rowMeta, cycles ) );
}
List<Searcher> searchers = new ArrayList<Searcher>( searchersAmount );
for ( int i = 0; i < searchersAmount; i++ ) {
String name = "meta_" + ( new Random().nextInt( elementsAmount ) );
assertTrue( rowMeta.indexOfValue( name ) >= 0 );
searchers.add( new Searcher( condition, rowMeta, name ) );
}
ConcurrencyTestRunner.runAndCheckNoExceptionRaised( shufflers, searchers, condition );
}
@SuppressWarnings( "unchecked" )
@Test
public void addRemoveSearch() throws Exception {
final int addersAmount = 5;
final int removeAmount = 10;
final int searchersAmount = 10;
final RowMeta rowMeta = new RowMeta();
List<String> toRemove = new ArrayList<String>( removeAmount );
for ( int i = 0; i < removeAmount; i++ ) {
String name = "toBeRemoved_" + i;
toRemove.add( name );
rowMeta.addValueMeta( new ValueMetaString( name ) );
}
final AtomicBoolean condition = new AtomicBoolean( true );
List<Searcher> searchers = new ArrayList<Searcher>( searchersAmount );
for ( int i = 0; i < searchersAmount; i++ ) {
String name = "kept_" + i;
rowMeta.addValueMeta( new ValueMetaString( name ) );
searchers.add( new Searcher( condition, rowMeta, name ) );
}
List<Remover> removers = Collections.singletonList( new Remover( condition, rowMeta, toRemove ) );
List<Adder> adders = new ArrayList<Adder>( addersAmount );
for ( int i = 0; i < addersAmount; i++ ) {
adders.add( new Adder( condition, rowMeta, cycles, "adder" + i ) );
}
List<Callable<?>> monitored = new ArrayList<Callable<?>>();
monitored.addAll( adders );
monitored.addAll( removers );
ConcurrencyTestRunner<?, ?> runner =
new ConcurrencyTestRunner<Object, Object>( monitored, searchers, condition );
runner.runConcurrentTest();
runner.checkNoExceptionRaised();
Map<? extends Callable<?>, ? extends ExecutionResult<?>> results = runner.getMonitoredResults();
// removers should remove all elements
for ( Remover remover : removers ) {
ExecutionResult<List<String>> result = (ExecutionResult<List<String>>) results.get( remover );
assertTrue( result.getResult().isEmpty() );
for ( String name : remover.getToRemove() ) {
assertEquals( name, -1, rowMeta.indexOfValue( name ) );
}
}
// adders should add all elements
Set<ValueMetaInterface> metas = new HashSet<ValueMetaInterface>( rowMeta.getValueMetaList() );
for ( Adder adder : adders ) {
ExecutionResult<List<ValueMetaInterface>> result =
(ExecutionResult<List<ValueMetaInterface>>) results.get( adder );
for ( ValueMetaInterface meta : result.getResult() ) {
assertTrue( meta.getName(), metas.remove( meta ) );
}
}
assertEquals( searchersAmount, metas.size() );
}
private static class Getter extends StopOnErrorCallable<Object> {
private final RowMeta rowMeta;
public Getter( AtomicBoolean condition, RowMeta rowMeta ) {
super( condition );
this.rowMeta = rowMeta;
}
@Override
Object doCall() throws Exception {
Random random = new Random();
while ( condition.get() ) {
int acc = 0;
for ( ValueMetaInterface meta : rowMeta.getValueMetaList() ) {
// fake cycle to from eliminating this snippet by JIT
acc += meta.getType() / 10;
}
Thread.sleep( random.nextInt( Math.max( 100, acc ) ) );
}
return null;
}
}
private static class Adder extends StopOnErrorCallable<List<ValueMetaInterface>> {
private final RowMeta rowMeta;
private final int cycles;
private final String nameSeed;
public Adder( AtomicBoolean condition, RowMeta rowMeta, int cycles, String nameSeed ) {
super( condition );
this.rowMeta = rowMeta;
this.cycles = cycles;
this.nameSeed = nameSeed;
}
@Override
List<ValueMetaInterface> doCall() throws Exception {
Random random = new Random();
List<ValueMetaInterface> result = new ArrayList<ValueMetaInterface>( cycles );
for ( int i = 0; ( i < cycles ) && condition.get(); i++ ) {
ValueMetaInterface added = new ValueMetaString( nameSeed + '_' + i );
rowMeta.addValueMeta( added );
result.add( added );
Thread.sleep( random.nextInt( 100 ) );
}
return result;
}
}
private static class Searcher extends StopOnErrorCallable<Object> {
private final RowMeta rowMeta;
private final String name;
public Searcher( AtomicBoolean condition, RowMeta rowMeta, String name ) {
super( condition );
this.rowMeta = rowMeta;
this.name = name;
}
@Override
Object doCall() throws Exception {
Random random = new Random();
while ( condition.get() ) {
int index = rowMeta.indexOfValue( name );
if ( index < 0 ) {
throw new IllegalStateException( name + " was not found among " + rowMeta.getValueMetaList() );
}
Thread.sleep( random.nextInt( 100 ) );
}
return null;
}
}
private static class Shuffler extends StopOnErrorCallable<Object> {
private final RowMeta rowMeta;
private final int cycles;
public Shuffler( AtomicBoolean condition, RowMeta rowMeta, int cycles ) {
super( condition );
this.rowMeta = rowMeta;
this.cycles = cycles;
}
@Override
Object doCall() throws Exception {
Random random = new Random();
for ( int i = 0; ( i < cycles ) && condition.get(); i++ ) {
List<ValueMetaInterface> list = new ArrayList<ValueMetaInterface>( rowMeta.getValueMetaList() );
Collections.shuffle( list );
rowMeta.setValueMetaList( list );
Thread.sleep( random.nextInt( 100 ) );
}
return null;
}
}
private static class Remover extends StopOnErrorCallable<List<String>> {
private final RowMeta rowMeta;
private final List<String> toRemove;
public Remover( AtomicBoolean condition, RowMeta rowMeta, List<String> toRemove ) {
super( condition );
this.rowMeta = rowMeta;
this.toRemove = toRemove;
}
@Override
List<String> doCall() throws Exception {
Random random = new Random();
List<String> result = new LinkedList<String>( toRemove );
for ( Iterator<String> it = result.iterator(); it.hasNext() && condition.get(); ) {
String name = it.next();
rowMeta.removeValueMeta( name );
it.remove();
Thread.sleep( random.nextInt( 100 ) );
}
return result;
}
public List<String> getToRemove() {
return toRemove;
}
}
}