All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.netcetera.girders.dbunit.datamanagement.ParallelizedDataManagementProvider Maven / Gradle / Ivy

package com.netcetera.girders.dbunit.datamanagement;

import com.netcetera.girders.dbunit.dataset.DataSetProvider;
import org.dbunit.IDatabaseTester;
import org.dbunit.dataset.IDataSet;
import org.dbunit.operation.DatabaseOperation;

import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import static java.lang.Boolean.TRUE;

/**
 * Provides data management for parallelized tests. Every data set will be loaded exactly once, then this data
 * management provider will not interact with the database anymore.
 * 

* Using this data management provider requires that: *

* *
    *
  • the test data for a given test case is fully contained within the data set that the {@link DataSetProvider} * supplies for that test case, and the data for each test case is independent of the data for all other test * cases
  • *
  • each data set is self-contained (i.e. loading any one of them on its own never produces any constraint * violations)
  • *
  • the union of all data sets that are loaded in parallel does not contain any constraint violations
  • *
  • the {@link DataSetProvider} needs to provide unique names for all data sets being loaded in parallel
  • *
* */ public class ParallelizedDataManagementProvider extends DataManagementProvider { private final DatabaseOperation updateOperation; /** * . */ public ParallelizedDataManagementProvider() { this(DatabaseOperation.UPDATE); } /** * * @param updateOperation the operation used to update the database with the * {@link DataSetProvider#getDbUnitUpdateDataSet() update data set}. */ @SuppressWarnings("WeakerAccess") // Public API. public ParallelizedDataManagementProvider(DatabaseOperation updateOperation) { this.updateOperation = updateOperation; } /** * We want to make sure every data set is only loaded once, but we also want parallelized test execution. * Therefore we need to do two things: *
    *
  • have separate flags on a per-data-set basis that tell us whether a given data set has been loaded yet
  • *
  • make that check atomic so there can be no race conditions between threads spawned for test cases for which * the {@link DataSetProvider} provides the same data set
  • *
*

* We do the following: for each data set, we keep a flag to determine whether some thread has started loading * it. Because JUnit creates a separate instance of the test class for every test method being run, we need to have * those flags in a {@code static} context, and access to the flags has to be atomic. We cannot override static * methods or fields in Java, so this {@link ConcurrentMap} maps data sets to their flags. The Java standard * libraries don't offer a concurrent {@link Set} implementation that's more convenient than this, and having it be * a {@link Map} won't cause much overhead.

*

* {@link #FINISHED_LOADING} marks whether a given data set has finished loading. *

*/ private static final Map STARTED_LOADING = new ConcurrentHashMap<>(); /** * Marks whether a given data set has been loaded. * * @see #STARTED_LOADING */ private static final Collection FINISHED_LOADING = new HashSet<>(); @SuppressWarnings("ProhibitedExceptionDeclared") @Override public void setUpDataBase(IDatabaseTester databaseTester, DataSetProvider dataSetProvider) // CHECKSTYLE:OFF throws Exception { // CHECKSTYLE:ON // adding the canonical name of the test class to this dataSetIdentifier that we use to identify the data set // makes it possible to have data set files with the same file name for separate test classes String dataSetIdentifier = dataSetProvider.getTestClass().getCanonicalName() + dataSetProvider.getDataSetName(); // Map.put(key, newValue) returns the value previously mapped by the given key before the call, so the first thread // that executes this .put will get null, all subsequent calls will get TRUE; works because it's a ConcurrentHashMap Boolean someThreadStartedLoadingDataSet = STARTED_LOADING.put(dataSetIdentifier, TRUE); //noinspection VariableNotUsedInsideIf if (someThreadStartedLoadingDataSet == null) { // the current thread is the first one to try to load the data set // -> it starts loading it try { databaseTester.onSetup(); updateData(dataSetProvider, databaseTester); } finally { // Synchronization through a constructor object would complicate the usage for integration projects. Performance // seams to be fine like this. //noinspection SynchronizationOnStaticField synchronized (FINISHED_LOADING) { FINISHED_LOADING.add(dataSetIdentifier); FINISHED_LOADING.notifyAll(); } } } else { // the current thread is not the first one to try to load the data set -> wait until the thread loading // the data set finishes // Synchronization through a constructor object would complicate the usage for integration projects. Performance // seams to be fine like this. //noinspection SynchronizationOnStaticField synchronized (FINISHED_LOADING) { while (!FINISHED_LOADING.contains(dataSetIdentifier)) { //noinspection WaitOrAwaitWithoutTimeout FINISHED_LOADING.wait(); } } } } /** *

We cannot insert objects with circular foreign key references with {@link DatabaseOperation#INSERT} because to * do that we would have to disable foreign key checks during the insert, but due to this data management provider * being used for pparallelized tests, there is no suitable moment to re-enable them, so tests using this data * management provider would run without foreign key checks. Instead, keep the checks enabled throughout and * separate the data set into the {@link DataSetProvider#getDbUnitDataSet() initial data} and the * {@link DataSetProvider#getDbUnitUpdateDataSet() UPDATE data}. First * {@link IDatabaseTester#onSetup} inserts the initial data set into the DB, * then this method {@code UPDATE}s the initial data with the {@code UPDATE data}. This way, circular foreign key * references can be put into the {@code UPDATE data}.

* *

NOTE: if your data set does not contain any circular foreign key references, you do not need to supply a * {@link DataSetProvider#getDbUnitUpdateDataSet()}.

*/ @SuppressWarnings("ProhibitedExceptionDeclared") private void updateData(DataSetProvider dataSetProvider, IDatabaseTester databaseTester) //CHECKSTYLE:OFF throws Exception { //CHECKSTYLE:ON Optional updateDataSet = dataSetProvider.getDbUnitUpdateDataSet(); if (updateDataSet.isPresent()) { databaseTester.setDataSet(updateDataSet.get()); databaseTester.setSetUpOperation(updateOperation); databaseTester.onSetup(); } } @Override public void tearDownDatabase() { // do nothing } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy