/*! ******************************************************************************
*
* Pentaho Data Integration
*
* Copyright (C) 2002-2015 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.core.database;
import org.junit.After;
import org.junit.BeforeClass;
import org.junit.Test;
import org.pentaho.di.core.KettleClientEnvironment;
import org.pentaho.di.core.database.map.DatabaseConnectionMap;
import org.pentaho.di.core.exception.KettleDatabaseException;
import org.pentaho.di.core.logging.LoggingObjectInterface;
import java.sql.Connection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
/**
* @author Andrey Khayrutdinov
*/
public class DatabaseConnectingTest {
private static final String GROUP = "group";
private static final String ANOTHER_GROUP = "another-group";
@BeforeClass
public static void setUp() throws Exception {
KettleClientEnvironment.init();
}
@After
public void removeFromSharedConnectionMap() {
removeFromSharedConnectionMap( GROUP );
}
private void removeFromSharedConnectionMap( String group ) {
DatabaseConnectionMap.getInstance().removeConnection( group, null, createStubDatabase( null ) );
}
@Test
public void connect_GroupIsNull() throws Exception {
Connection connection1 = mock( Connection.class );
DatabaseStub db1 = createStubDatabase( connection1 );
Connection connection2 = mock( Connection.class );
DatabaseStub db2 = createStubDatabase( connection2 );
db1.connect();
db2.connect();
assertEquals( connection1, db1.getConnection() );
assertEquals( connection2, db2.getConnection() );
}
@Test
public void connect_GroupIsEqual_Consequently() throws Exception {
Connection shared = mock( Connection.class );
DatabaseStub db1 = createStubDatabase( shared );
db1.connect( GROUP, null );
DatabaseStub db2 = createStubDatabase( shared );
db2.connect( GROUP, null );
assertSharedAmongTheGroup( shared, db1, db2 );
}
@Test
public void connect_GroupIsEqual_InParallel() throws Exception {
final Connection shared = mock( Connection.class );
final int dbsAmount = 300;
final int threadsAmount = 50;
List<DatabaseStub> dbs = new ArrayList<DatabaseStub>( dbsAmount );
Set<Integer> copies = new HashSet<Integer>( dbsAmount );
ExecutorService pool = Executors.newFixedThreadPool( threadsAmount );
try {
CompletionService<DatabaseStub> service = new ExecutorCompletionService<DatabaseStub>( pool );
for ( int i = 0; i < dbsAmount; i++ ) {
service.submit( createStubDatabase( shared ) );
copies.add( i + 1 );
}
for ( int i = 0; i < dbsAmount; i++ ) {
DatabaseStub db = service.take().get();
assertEquals( shared, db.getConnection() );
dbs.add( db );
}
} finally {
pool.shutdown();
}
for ( DatabaseStub db : dbs ) {
String message =
String.format( "There should be %d shares of the connection, but found %d", dbsAmount, db.getOpened() );
// 0 is for those instances that use the shared connection
assertTrue( message, db.getOpened() == 0 || db.getOpened() == dbsAmount );
assertTrue( "Each instance should have a unique 'copy' value", copies.remove( db.getCopy() ) );
}
assertTrue( copies.isEmpty() );
}
@Test
public void connect_TwoGroups() throws Exception {
try {
Connection shared1 = mock( Connection.class );
Connection shared2 = mock( Connection.class );
DatabaseStub db11 = createStubDatabase( shared1 );
DatabaseStub db12 = createStubDatabase( shared1 );
DatabaseStub db21 = createStubDatabase( shared2 );
DatabaseStub db22 = createStubDatabase( shared2 );
db11.connect( GROUP, null );
db12.connect( GROUP, null );
db21.connect( ANOTHER_GROUP, null );
db22.connect( ANOTHER_GROUP, null );
assertSharedAmongTheGroup( shared1, db11, db12 );
assertSharedAmongTheGroup( shared2, db21, db22 );
} finally {
removeFromSharedConnectionMap( ANOTHER_GROUP );
}
}
private void assertSharedAmongTheGroup( Connection shared, DatabaseStub db1, DatabaseStub db2 ) {
assertEquals( shared, db1.getConnection() );
assertEquals( shared, db2.getConnection() );
assertEquals( 2, db1.getOpened() );
assertEquals( 0, db2.getOpened() );
}
@Test
public void connect_ManyGroups_Simultaneously() throws Exception {
final int groupsAmount = 30;
Map<String, Connection> groups = new HashMap<String, Connection>( groupsAmount );
for ( int i = 0; i < groupsAmount; i++ ) {
groups.put( Integer.toString( i ), mock( Connection.class ) );
}
try {
ExecutorService pool = Executors.newFixedThreadPool( groupsAmount );
try {
CompletionService<DatabaseStub> service = new ExecutorCompletionService<DatabaseStub>( pool );
Map<DatabaseStub, String> mapping = new HashMap<DatabaseStub, String>( groupsAmount );
for ( Map.Entry<String, Connection> entry : groups.entrySet() ) {
DatabaseStub stub = createStubDatabase( entry.getValue() );
mapping.put( stub, entry.getKey() );
service.submit( stub );
}
Set<String> unmatchedGroups = new HashSet<String>( groups.keySet() );
for ( int i = 0; i < groupsAmount; i++ ) {
DatabaseStub stub = service.take().get();
assertTrue( unmatchedGroups.remove( mapping.get( stub ) ) );
}
assertTrue( unmatchedGroups.isEmpty() );
} finally {
pool.shutdown();
}
} finally {
for ( String group : groups.keySet() ) {
removeFromSharedConnectionMap( group );
}
}
}
private DatabaseStub createStubDatabase( Connection sharedConnection ) {
DatabaseMeta meta = new DatabaseMeta( "test", "H2", "", "", "", "", "", "" );
return new DatabaseStub( null, meta, sharedConnection );
}
private static class DatabaseStub extends Database implements Callable<DatabaseStub> {
private final Connection sharedConnection;
private boolean connected;
public DatabaseStub( LoggingObjectInterface parentObject,
DatabaseMeta databaseMeta, Connection sharedConnection ) {
super( parentObject, databaseMeta );
this.sharedConnection = sharedConnection;
this.connected = false;
}
@Override
public synchronized void normalConnect( String partitionId ) throws KettleDatabaseException {
if ( !connected ) {
// make a delay to emulate real scenario
try {
Thread.sleep( 250 );
} catch ( InterruptedException e ) {
fail( e.getMessage() );
}
setConnection( sharedConnection );
connected = true;
}
}
@Override
public DatabaseStub call() throws Exception {
connect( GROUP, null );
return this;
}
}
}