Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature request: parallel execution #168

Open
Kidlike opened this issue Feb 28, 2021 · 1 comment
Open

feature request: parallel execution #168

Kidlike opened this issue Feb 28, 2021 · 1 comment

Comments

@Kidlike
Copy link

Kidlike commented Feb 28, 2021

(thanks for the recent upgrade to cucumber 6 🥳)

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
Copy link
Author

Kidlike commented Apr 2, 2021

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant