cukes icon indicating copy to clipboard operation
cukes copied to clipboard

feature request: parallel execution

Open Kidlike opened this issue 3 years ago • 1 comments

(thanks for the recent upgrade to cucumber 6 :partying_face:)

Are there any ongoing efforts for supporting parallel execution?

After some failed attempts, it seems that it fails on the 2 following points (depending on the timing of the threads):

  1. io.cucumber.guice.SequentialScenarioScope enter/exit methods throw IllegalStateException https://stackoverflow.com/questions/44166354/cucumber-guice-injector-seems-not-to-be-thread-safe-parallel-execution-exec

  2. RestAssured -> Apache HttpClient -> BasicClientConnManager. Maybe replace with PoolingHttpClientConnectionManager? I think that one is thread-safe.

      java.lang.IllegalStateException: Invalid use of BasicClientConnManager: connection still allocated.
Make sure to release the connection before allocating another one.

...     at lv.ctco.cukes.rest.api.WhenSteps.perform_Http_Request(WhenSteps.java:19)

Not sure if there are other issues related to cukes-rest specifically, like how some variables are used (e.g. world). Probably there needs to be some more thread-isolation changes :/

Kidlike avatar Feb 28 '21 09:02 Kidlike

If anyone else is interested (or the maintainers of this library), I managed to achieve parallel execution for this library, with the following ObjectFactory.

The basic idea, is that instead of a single Guice context, it starts a new context for every thread that is started by surefire.

import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Module;
import com.google.inject.Stage;
import io.cucumber.core.backend.ObjectFactory;
import io.cucumber.guice.CucumberModules;
import io.cucumber.guice.ScenarioScope;
import java.lang.annotation.Annotation;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import lv.ctco.cukes.core.extension.CukesInjectableModule;
import lv.ctco.cukes.core.internal.di.CukesGuiceModule;
import org.reflections.Reflections;

/**
 * Will create a new Guice context for each <code>threadCount</code> that is configured in surefire.
 *
 * <p>
 * This class can be enabled by <code>classpath:cucumber.properties#cucumber.object-factory</code,
 *
 * or by {@link io.cucumber.junit.CucumberOptions#objectFactory()}
 * </p>
 */
public class ThreadedObjectFactory implements ObjectFactory {
    private final ThreadLocal<Injector> localInjector = new ThreadLocal<>();

    private Injector getInjector() {
        lazyInitInjector();
        return localInjector.get();
    }

    public void start() {
        getInjector().getInstance(ScenarioScope.class).enterScope();
    }

    public void stop() {
        getInjector().getInstance(ScenarioScope.class).exitScope();
    }

    public boolean addClass(Class<?> aClass) {
        return true;
    }

    public <T> T getInstance(Class<T> aClass) {
        return getInjector().getInstance(aClass);
    }

    private void lazyInitInjector() {
        if (localInjector.get() == null) {
            Set<Module> modules = new HashSet<>();

            modules.add(CucumberModules.createScenarioModule());
            modules.add(new CukesGuiceModule());

            modules.addAll(createInstancesOf(CukesInjectableModule.class, "lv.ctco.cukes"));

            localInjector.set(Guice.createInjector(Stage.PRODUCTION, modules));
        }
    }

    private <T> Set<T> createInstancesOf(Class<? extends Annotation> annotation, String scanPackage) {
        return new Reflections(scanPackage)
            .getTypesAnnotatedWith(annotation)
            .stream()
            .map(aClass -> {
                try {
                    return (T) aClass.getConstructor().newInstance();
                } catch (Exception e) {
                    return null;
                }
            })
            .filter(Objects::nonNull)
            .collect(Collectors.toSet());
    }
}

I understand that this solution might be a bit somewhat memory consuming, but for my use case it's not a problem. Thankfully, Guice is very light.

The alternative of having a single Guice context for all threads, and making all singletons inside the cukes-rest library thread-isolated is a huge amount of work and I wouldn't recommend it.

Kidlike avatar Apr 02 '21 11:04 Kidlike