How to Achieve Parallel Test Execution Using ThreadLocal with Appium?

I am attempting to run parallel tests using Appium with the help of ThreadLocal. However, I am encountering issues where one device waits while the other initializes. Furthermore, once both devices are up, they either stop executing tests or fail to find elements, preventing true parallel test execution. My appium version: 8.6.0 and selenium version: 4.11.0
Below is my code setup

BaseTest.java

package org.Base;

import io.appium.java_client.AppiumDriver;
import io.appium.java_client.android.*;
import io.appium.java_client.android.options.UiAutomator2Options;
import io.appium.java_client.ios.IOSDriver;
import io.appium.java_client.ios.options.XCUITestOptions;
import org.Listener.Listener;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.Capabilities.MobileCapabilityTypes;
import org.openqa.selenium.Platform;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.testng.annotations.*;

import java.net.MalformedURLException;
import java.net.URL;
import java.time.Duration;
import java.util.HashMap;

import static org.Base.AppiumServer.startAppiumServer;
import static org.Utilities.StepInit.init;

@Listeners({Listener.class})
public abstract class BaseTest {

    private static final Logger LOGGER = LogManager.getLogger(BaseTest.class);
    private static final ThreadLocal<AppiumDriver> appiumDriver = new ThreadLocal<>();
    public AppiumDriver driver;

    public static AppiumDriver getDriver() {
        return appiumDriver.get();
    }

    public void setDriver(String deviceType, boolean noReset,
                          String platformVersion, String appPackage, String appActivity,
                          boolean appWaitForLaunch, boolean pushPermission, boolean enableMultiWindows,
                          boolean allowInvisibleElements, String url, @Optional String app, String udid,
                          String bundleId, String appId, String deleteApp, @Optional("0") int wdaLocalPort, @Optional("0") int port, @Optional String deviceName) {

        String osName = System.getProperty("os.name").toLowerCase();
        if (osName.contains("win")) {
            url = url + "/wd/hub"; // Just Windows
        }

        switch (deviceType) {
            case "IOS":
                startAppiumServer(port);
                XCUITestOptions xcuiTestOptions = new XCUITestOptions();
                xcuiTestOptions.setCapability(MobileCapabilityTypes.platformName, "iOS");
                xcuiTestOptions.setCapability(MobileCapabilityTypes.platformVersion, platformVersion);
                xcuiTestOptions.setCapability(MobileCapabilityTypes.automationName, "XCUITest");
                xcuiTestOptions.setCapability(MobileCapabilityTypes.bundleId, bundleId);
                xcuiTestOptions.setCapability(MobileCapabilityTypes.pushPermissions, pushPermission);
                xcuiTestOptions.setCapability(MobileCapabilityTypes.autoAcceptAlert, true);
                xcuiTestOptions.setCapability(MobileCapabilityTypes.udid, udid);
                xcuiTestOptions.setCapability(MobileCapabilityTypes.appId, appId);
                xcuiTestOptions.setCapability("xcodeOrgId", "18AYT1T2YF");
                xcuiTestOptions.setCapability("xcodeSigningId", "iPhone Developer");
                xcuiTestOptions.setCapability("updatedWDABundleId", "com.facebook17283073.WebDriverAgentRunner");
                xcuiTestOptions.setWdaLocalPort(wdaLocalPort);

                try {
                    appiumDriver.set(new IOSDriver(new URL(url), xcuiTestOptions));
                } catch (MalformedURLException e) {
                    LOGGER.error("Driver could not be created! ErrorMessage: " + e.getMessage());
                }
                break;
            case "Android":
                startAppiumServer(port);
                UiAutomator2Options uiAutomator2Options = new UiAutomator2Options();
                uiAutomator2Options.setCapability(MobileCapabilityTypes.platformName, Platform.ANDROID);
                uiAutomator2Options.setCapability("newCommandTimeout", 22600);
                uiAutomator2Options.setCapability(MobileCapabilityTypes.platformVersion, platformVersion);
                uiAutomator2Options.setCapability(MobileCapabilityTypes.automationName, MobileCapabilityTypes.automatorAndroid);
                uiAutomator2Options.setCapability(MobileCapabilityTypes.appPackage, appPackage);
                uiAutomator2Options.setCapability(MobileCapabilityTypes.appActivity, appActivity);
                uiAutomator2Options.setCapability(MobileCapabilityTypes.appWaitForLaunch, appWaitForLaunch);
                uiAutomator2Options.setCapability(MobileCapabilityTypes.pushPermissions, pushPermission);
                uiAutomator2Options.setCapability(MobileCapabilityTypes.noReset, noReset);
                uiAutomator2Options.setCapability(MobileCapabilityTypes.enableMultiWindows, enableMultiWindows);
                uiAutomator2Options.setCapability(MobileCapabilityTypes.allowInvisibleElements, allowInvisibleElements);
                uiAutomator2Options.setCapability(MobileCapabilityTypes.app, app);
                uiAutomator2Options.setCapability(MobileCapabilityTypes.udid, udid);
                uiAutomator2Options.setCapability(MobileCapabilityTypes.appId, appId);

                try {
                    appiumDriver.set(new AndroidDriver(new URL(url), uiAutomator2Options));
                    driver = appiumDriver.get();
                } catch (MalformedURLException e) {
                    LOGGER.error("Driver could not be created! ErrorMessage: " + e.getMessage());
                }

                getDriver().manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
                break;

            default:
                LOGGER.info("There is no such a device");
                break;
        }
    }

    @Parameters({"platform", "noReset", "platformVersion", "appPackage", "appActivity",
            "appWaitForLaunch", "pushPermission", "enableMultiWindows", "allowInvisibleElements", "url",
            "app", "udid", "bundleId", "appId", "deleteApp", "wdaLocalPort", "port", "deviceName"})
    @BeforeClass
    public void beforeClass(String platform, @Optional("true") boolean noReset, String platformVersion,
                            String appPackage, String appActivity, boolean appWaitForLaunch, @Optional("true") boolean pushPermission,
                            boolean enableMultiWindows, boolean allowInvisibleElements, String url, @Optional String app,
                            String udid, String bundleId, String appId, String deleteApp, @Optional("0") int wdaLocalPort,
                            @Optional("0") int port, @Optional String deviceName) {

        setDriver(platform, noReset, platformVersion, appPackage, appActivity, appWaitForLaunch,
                pushPermission, enableMultiWindows, allowInvisibleElements, url, app, udid, bundleId, appId, deleteApp,
                wdaLocalPort, port, deviceName);
        getDriver().manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
        init();
    }

    @Parameters({"platform", "noReset", "platformVersion", "appPackage", "appActivity",
            "appWaitForLaunch", "pushPermission", "enableMultiWindows", "allowInvisibleElements", "url",
            "app", "udid", "bundleId", "appId", "deleteApp", "wdaLocalPort", "port", "deviceName"})
    @BeforeMethod
    public void beforeMethod(String platform, @Optional("true") boolean noReset, String platformVersion,
                             String appPackage, String appActivity, boolean appWaitForLaunch, @Optional("true") boolean pushPermission,
                             boolean enableMultiWindows, boolean allowInvisibleElements, String url, @Optional String app,
                             String udid, String bundleId, String appId, String deleteApp, @Optional("0") int wdaLocalPort,
                             @Optional("0") int port, @Optional String deviceName) {
        if (getDriver() != null && noReset) {
            if (platform.equals("Android")) {
                ((AndroidDriver) getDriver()).terminateApp(appPackage);
                ((AndroidDriver) getDriver()).activateApp(appPackage);
            } else {
                ((IOSDriver) getDriver()).terminateApp(bundleId);
                ((IOSDriver) getDriver()).activateApp(bundleId);
            }
        }
        getDriver().manage().timeouts().implicitlyWait(Duration.ofSeconds(10));

    }

    @Parameters({"platform", "appPackage"})
    @AfterClass
    public void afterClass(String platform, String appPackage) {
        if (platform.equals("Android")) {
            ((AndroidDriver) getDriver()).terminateApp(appPackage);
        } else {
            ((IOSDriver) getDriver()).terminateApp(appPackage);
        }
    }

    @AfterSuite(alwaysRun = true)
    public static void afterSuite() {
        AppiumServer.stopAppiumServer();
    }
}

AppiumServer.java

package org.Base;

import io.appium.java_client.service.local.AppiumDriverLocalService;
import io.appium.java_client.service.local.AppiumServiceBuilder;

public class AppiumServer {
    private static AppiumDriverLocalService service;

    public static void startAppiumServer(int port) {
        AppiumServiceBuilder builder = new AppiumServiceBuilder()
                .withIPAddress("127.0.0.1")
                .usingPort(port)
                .withArgument(() -> "--allow-insecure=get_server_logs");

        String osName = System.getProperty("os.name").toLowerCase();
        if (osName.contains("win")) {
            builder.withArgument(() -> "--base-path", "/wd/hub"); // Just Windows
        }
        service = AppiumDriverLocalService.buildService(builder);
        service.start();
    }

    public static void stopAppiumServer() {
        if (service != null) {
            service.stop();
        }
    }
}

ParallelEmulator.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="All Test Suite" parallel="tests" thread-count="2">
    <listeners>
        <listener class-name="org.Listener.ExtentITestListenerClassAdapter"/>
    </listeners>

    <test name="Pixel">
        <parameter name="platform" value="Android"/>
        <parameter name="deviceType" value="AndroidEmulator"/>
        <parameter name="noReset" value="true"/>
        <parameter name="platformVersion" value="13.0"/>
        <parameter name="appPackage" value="tr.xyz.app"/>
        <parameter name="appActivity" value="tr.com.xyz.app.ui.SplashActivity"/>
        <parameter name="appWaitForLaunch" value="false"/>
        <parameter name="pushPermission" value="true"/>
        <parameter name="enableMultiWindows" value="true"/>
        <parameter name="allowInvisibleElements" value="true"/>
        <parameter name="url" value="http://127.0.0.1:4725"/>
        <parameter name="port" value="4725"/>
        <parameter name="app" value=""/>
        <parameter name="udid" value="emulator-5556"/>
        <parameter name="bundleId" value="com.xyz.test"/>
        <parameter name="deleteApp" value=""/>
        <parameter name="appId" value="tr.com.xyz.app"/>

        <classes>
            <class name="com.deneme.Android.test.Katalog.HomePageTests"/>
        </classes>
    </test>

    <test name="Fold">
        <parameter name="platform" value="Android"/>
        <parameter name="deviceType" value="AndroidEmulator"/>
        <parameter name="noReset" value="true"/>
        <parameter name="platformVersion" value="12.0"/>
        <parameter name="appPackage" value="tr.xyz.app"/>
        <parameter name="appActivity" value="tr.com.xyz.app.ui.SplashActivity"/>
        <parameter name="appWaitForLaunch" value="false"/>
        <parameter name="pushPermission" value="true"/>
        <parameter name="enableMultiWindows" value="true"/>
        <parameter name="allowInvisibleElements" value="true"/>
        <parameter name="url" value="http://127.0.0.1:4723"/>
        <parameter name="port" value="4723"/>
        <parameter name="app" value=""/>
        <parameter name="udid" value="emulator-5554"/>
        <parameter name="bundleId" value="com.xyz.test"/>
        <parameter name="deleteApp" value=""/>
        <parameter name="appId" value="tr.com.xyz.app"/>

        <classes>
            <class name="com.deneme.Android.test.KatalogTests">
            </class>
        </classes>
    </test>
</suite>

This is not looks nice for me. First thread fun on it first appium, second second. As result only last runs.

Hello Aleksei,
So how can i fix it

share full code at git to understand

Here is the link GitHub - CvMrRj/ParallelExecution Thank you for your support

try first

// public abstract class BaseTest
 - remove "public AppiumDriver driver;", you already have it here "ThreadLocal<AppiumDriver> appiumDriver"
 - public static void afterSuite() { -> public void afterSuite() {```

// public class AppiumServer {
 - remove word "static" in all three functions

Unfortunately it didnt work

pls update code in git to check again

I just updated it. You can take a look whenever you’re available. thank you for your support [GitHub - CvMrRj/ParallelExecution]