/*! ******************************************************************************
*
* 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.apache.commons.collections.ListUtils;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.pentaho.di.core.gui.JobTracker;
import org.pentaho.di.job.JobEntryResult;
import org.pentaho.di.job.JobMeta;
import org.pentaho.di.job.entry.JobEntryCopy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* This test consists of two similar cases. There are three type of actors: getters, searchers and updaters. They work
* simultaneously within their own threads. Getters invoke {@linkplain JobTracker#getJobTracker(int)} with a random
* index, searchers call {@linkplain JobTracker#findJobTracker(JobEntryCopy)}, updaters add new children
* <tt>updatersCycles</tt> times. The difference between two cases is the second has a small limit of stored children,
* so the parent JobTracker will be forced to remove some of its elements.
*
* @author Andrey Khayrutdinov
*/
@RunWith( Parameterized.class )
@Ignore
public class JobTrackerConcurrencyTest {
private static final int gettersAmount = 10;
private static final int searchersAmount = 20;
private static final int updatersAmount = 5;
private static final int updatersCycles = 10;
private static final int jobsLimit = 20;
@SuppressWarnings( "ConstantConditions" )
@BeforeClass
public static void setUp() {
// a guarding check for tests' parameters
int jobsToBeAdded = updatersAmount * updatersCycles;
assertTrue( "The limit of stored jobs must be less than the amount of children to be added",
jobsLimit < jobsToBeAdded );
}
@Parameterized.Parameters
public static List<Object[]> getData() {
return Arrays.asList(
new Object[] { new JobTracker( mockJobMeta( "parent" ) ) },
new Object[] { new JobTracker( mockJobMeta( "parent" ), jobsLimit ) }
);
}
private static JobMeta mockJobMeta( String name ) {
JobMeta meta = mock( JobMeta.class );
when( meta.getName() ).thenReturn( name );
return meta;
}
private final JobTracker tracker;
public JobTrackerConcurrencyTest( JobTracker tracker ) {
this.tracker = tracker;
}
@Test
public void readAndUpdateTrackerConcurrently() throws Exception {
final AtomicBoolean condition = new AtomicBoolean( true );
List<Getter> getters = new ArrayList<Getter>( gettersAmount );
for ( int i = 0; i < gettersAmount; i++ ) {
getters.add( new Getter( condition, tracker ) );
}
List<Searcher> searchers = new ArrayList<Searcher>( searchersAmount );
for ( int i = 0; i < searchersAmount; i++ ) {
int lookingFor = updatersAmount * updatersCycles / 2 + i;
assertTrue( "We are looking for reachable index", lookingFor < updatersAmount * updatersCycles );
searchers.add( new Searcher( condition, tracker, mockJobEntryCopy( "job-entry-" + lookingFor, lookingFor ) ) );
}
final AtomicInteger generator = new AtomicInteger( 0 );
List<Updater> updaters = new ArrayList<Updater>( updatersAmount );
for ( int i = 0; i < updatersAmount; i++ ) {
updaters.add( new Updater( tracker, updatersCycles, generator, "job-entry-%d" ) );
}
//noinspection unchecked
ConcurrencyTestRunner.runAndCheckNoExceptionRaised( updaters, ListUtils.union( getters, searchers ), condition );
assertEquals( updatersAmount * updatersCycles, generator.get() );
}
static JobEntryCopy mockJobEntryCopy( String name, int number ) {
JobEntryCopy copy = mock( JobEntryCopy.class );
when( copy.getName() ).thenReturn( name );
when( copy.getNr() ).thenReturn( number );
return copy;
}
private static class Getter extends StopOnErrorCallable<Object> {
private final JobTracker tracker;
private final Random random;
public Getter( AtomicBoolean condition, JobTracker tracker ) {
super( condition );
this.tracker = tracker;
this.random = new Random();
}
@Override
public Object doCall() throws Exception {
while ( condition.get() ) {
int amount = tracker.nrJobTrackers();
if ( amount == 0 ) {
continue;
}
int i = random.nextInt( amount );
JobTracker t = tracker.getJobTracker( i );
if ( t == null ) {
throw new IllegalStateException(
String.format( "Returned tracker must not be null. Index = %d, trackers' amount = %d", i, amount ) );
}
}
return null;
}
}
private static class Searcher extends StopOnErrorCallable<Object> {
private final JobTracker tracker;
private final JobEntryCopy copy;
public Searcher( AtomicBoolean condition, JobTracker tracker, JobEntryCopy copy ) {
super( condition );
this.tracker = tracker;
this.copy = copy;
}
@Override Object doCall() throws Exception {
while ( condition.get() ) {
// can be null, it is OK here
tracker.findJobTracker( copy );
}
return null;
}
}
private static class Updater implements Callable<Exception> {
private final JobTracker tracker;
private final int cycles;
private final AtomicInteger idGenerator;
private final String resultNameTemplate;
public Updater( JobTracker tracker, int cycles, AtomicInteger idGenerator, String resultNameTemplate ) {
this.tracker = tracker;
this.cycles = cycles;
this.idGenerator = idGenerator;
this.resultNameTemplate = resultNameTemplate;
}
@Override
public Exception call() throws Exception {
Exception exception = null;
try {
for ( int i = 0; i < cycles; i++ ) {
int id = idGenerator.getAndIncrement();
JobEntryResult result = new JobEntryResult();
result.setJobEntryName( String.format( resultNameTemplate, id ) );
result.setJobEntryNr( id );
JobTracker child = new JobTracker( mockJobMeta( "child-" + id ), result );
tracker.addJobTracker( child );
}
} catch ( Exception e ) {
exception = e;
}
return exception;
}
}
}