HeadSpin Documentation
Documentation

Appium Load Balancer

Table of Contents

Overview

Appium Load Balancer, also known as AppiumLB, is HeadSpin’s solution to more efficient device management when testing with Appium and Selenium tests. Identifying and tracking each individual test target, particularly when you want your tests to cover a variety of geographical regions and device SKUs, can be time-consuming and frustrating. AppiumLB does not function like a traditional load balancer program handling incoming network traffic; rather, it simulates that same balancing logic to API commands and device UDIDs or WebDriver URLs to cut down on time and effort spent on device or host management.

AppiumLB is a single, generic Web Driver URL granting access to any HeadSpin devices available to you. It functions somewhat like a proxy server in that all API commands will touch AppiumLB before touching your device hosts, and AppiumLB will select devices at random that meet your test criteria. Test devices are specified through the new <code class="dcode">headspin:selector</code> capability, using the selector syntax as described in the Selectors DSL docs. AppiumLB automatically picks the proper devices and start a new Appium session with that device's <code class="dcode">udid</code>, <code class="dcode">headspin:appiumVersion</code> and the original <code class="dcode">headspin:selector</code> capabilities. AppiumLB also automatically picks a proper browser device with <code class="dcode">browserName</code>, <code class="dcode">browserVersion</code> and the original <code class="dcode">headspin:selector</code> capabilities for a Selenium session. Using AppiumLB, your testing experience will have minimal incidents of tests failing due to offline or unresponsive devices, reserved devices that are unavailable, or user error in identifying devices. Every request, barring user error in its design, will gain some kind of response and cut down on test troubleshooting. Please note that, as described in the Capabilities section below, you can specify a particular device using the <code class="dcode">udid</code> capability.

Using AppiumLB

To use AppiumLB, set your Web Driver URL as <code class="dcode">https://appium-dev.headspin.io/v0/your-api-token/wd/hub</code> and make sure that the <code class="dcode">headspin:selector</code> capability reflects target devices for your test. The Web Driver URL is available as Load Balanced Driver URL in the automation configuration.

AppiumLB will look for a device or devices matching the selectors, lock the device for automation and start an Appium or a Selenium session against the device's host server. It then returns a response containing the <code class="dcode">sessionId</code> of the newly created session, where you can send Appium or Selenium commands.

Retry and avoid previously failed devices

AppiumLB will retry establishing a new session against available devices in the device pool up to the value of <code class="dcode">appium:newCommandTimeout</code> (or <code class="dcode">headspin:newCommandTimeout</code>) before responding to the client with a timeout error. As part of the new session request, if <code class="dcode">headspin:waitForDeviceOnlineTimeout</code> is specified, AppiumLB will wait for a device to be available up to this value. AppiumLB will keep the http connection between AppiumLB and a selected Pbox alive without communication for up to the <code class="dcode">newCommandTimeout</code> minus the time taken to acquire the selected device. I.e., <code class="dcode">newCommandTimeout</code> is the max time that a new session request will be allowed to take, including time finding an appropriate device. The client should always keep the client-side HTTP request timeout to a value higher than <code class="dcode">appium:newCommandTimeout</code> (or <code class="dcode">headspin:newCommandTimeout</code>), plus a few buffer seconds such as 30 additional seconds, to ensure that it will receive timeout errors. If the client-side HTTP request timeout is lower, the client script will disconnect and it will not be possible to handle timeout-related errors. <code class="dcode">headspin:ignoreFailedDevice</code>, when set to False, does nothing; when set to True, this capability will ignore failed-connection devices when attempting to secure an available device (note that by default this value is set to True). As an example of AppiumLB securing an available device, if a given device is unlocked around 20 seconds before the request to find a free device is issued, and the new session request takes 150 seconds against the free device, then the request could take 170 seconds in total.

Each retry will avoid previously failed devices if the cause is a device-side issue to make the session creation reliable. For instance, if a device fails due to an unexpected disconnection in the new session request, AppiumLB will avoid the device for selection for the next 10 minutes. AppiumLB can be configured to skip this avoidance behavior with the capability <code class="dcode">headspin:ignoreFailedDevice</code>. Please note that at this time there is no action that can be taken to manually force-stop new session request timeout.

If Appium fails to find an available device in your device pool with the given <code class="dcode">appium:udid</code> or <code class="dcode">headspin:selector</code>, it will respond with an error message that starts with No matched devices were found. It indicates the given udid or selector syntax was wrong or the syntax was correct but no matched devices existed in your device pool.

If AppiumLB fails to find an available device in your device pool, it will respond with an error message that starts with No available good condition devices ... "No available devices" indicates devices are offline, are already locked by a user, must be reserved by you, or failed against a new session request. AppiumLB will respond with an error message if the last retry fails in a new session request. <code class="dcode">headspin:retryNewSessionFailure</code> and <code class="dcode">headspin:waitForDeviceOnlineTimeout</code> help you to control the retry rule.

If no devices are available in the device pool after excluding previously failed devices then AppiumLB will select a device from the device pool without considering the previously failed devices. This behavior helps reduce obstacles in the case of your device pool containing few devices for the given selector(s). The selected device may manifest an error due to its previously failed state.

The error previously failed devices only indicates devices for which AppiumLB failed to create a new session due to a problem on the device. Errors due to other causes (such as invalid Appium capabilities) do not cause devices to be marked as "previously failed".

<code class="dcode">message</code> key in the error response could include past error messages up to 10 latest cases to help error investigation.

AppiumLB Capabilities

In addition to the capabilities in Appium Capabilities docs and Selenium Capabilities docs, AppiumLB also support these capabilities:

  • headspin:selector
    • AppiumLB will select devices matching the characteristics in this selector. Read more about HeadSpin selectors in the Selectors Docs.
    • The headspin:selector format can be string (**recommended**) or JSON object as shown in the examples below. The string format can write more complex operations. Please check the available operators in Selectors as a query parameter section in Selectors Docs for more details.
    • If a udid capability is provided, AppiumLB prioritizes udid over headspin:selector. It will select the device matching the udid, regardless of the device status (i.e even if the device is locked or unavailable).
      • The udid can also be a string formatted selector. Then, AppiumLB handles it in the same way as headspin:selector.
  • Note that : is used as a separator in the Selectors usage to detect a pair of key/value. If the value could include :, please wrap with quote. The typical case is Bring Your Own Device devices. It could include : in the device id.
    • e.g. byod.headspin.io:9999 should be:
      • "headspin:selector": "\"byod.headspin.io:9999\""
      • "headspin:selector": "device_id:\"byod.headspin.io:9999\""
      • "appium:udid": "\"byod.headspin.io:9999\""
      • "appium:udid": "device_id:\"byod.headspin.io:9999\""
  • (Deprecated) headspin:requestTimeout
    • Defaults to 120 seconds.
    • Please use newCommandTimeout or headspin:newCommandTimeout in Appium or Selenium capabilities instead of headspin:requestTimeout. AppiumLB will respect newCommandTimeout and headspin:newCommandTimeout for Appium and Selenium to configure request timeout between AppiumLB and each appium/selenium server.
  • headspin:waitForDeviceOnlineTimeout
    • AppiumLB will wait for a device to be online until the timeout given in seconds (i.e., this capability defines how long AppiumLB waits for a device to be available). AppiumLB will wait while the device is in a locked or unavailable status. 10800 (3 hours) is the maximum value.
    • Defaults to 20 seconds or newCommandTimeout or headspin:newCommandTimeout in Appium Capabilities docs or Selenium Capabilities docs capabilities, if provided.
  • headspin:newSessionRequestProgress
    • AppiumLB will send a progress message to the client every 10 seconds during a new session request proceeding in order to avoid the client side HTTP read timeout error. The client will get the whole response body after succeeding or failing the new session request. The response might have a progress key in the JSON response body.
    • Defaults to false. The value is enabled automatically if waitForDeviceOnlineTimeout was over 1 hour, but you can explicitly disable this feature by giving false as the value. Once you enable this capability, the new session request will keep the HTTP session up to waitForDeviceOnlineTimeout or newCommandTimeout by sending progress messages.
  • headspin:retryNewSessionFailure
    • How AppiumLB will handle a new session request when a new session request to a device got failed because of an error. The default value is true. Then, AppiumLB attempts to send a new session request against another available device up to newCommandTimeout or headspin:newCommandTimeout. If the value sets as false, AppiumLB will reply the error message back to the client without any retry as addressed in Retry and avoid previously failed devices. The client can handle any error cases on their side. This false capability will help when the client wants to use AppiumLB as a device selector but not for request handling.
  • headspin:deviceStrategy
    • How AppiumLB will pick a device from available devices in the device pool.
    • Available values are:
      • ranked (default): Pick a device deterministically from the device pool.
      • random: Pick a device randomly from the device pool.
  • headspin:ignoreFailedDevices
    • AppiumLB will avoid sending a new session request against the list of previously failed devices, which got a failure in their new session request, to prevent continuous session creation failure. Each failed device will be selectable after 10 minutes.
    • defaults to true.
  • headspin:skipVendorPrefixDirectconnect
    • Prevent adding appium: and headspin: prefixes of directConnect feature in the response of new session request from AppiumLB. This capability is mostly for webdriverio specific. If you use webdriverio for Selenium automation over AppiumLB, the webdriverio instance thinks the session is isMobil as true by referencing the appium: in the response while the session is not Appium for mobile devices. It could break some Selenium commands. This capability helps to prevent the behavior.This is not necessary for webdriverio v8.10.7+.
    • defaults to false.

AppiumLB will restrict target devices if the given capabilities includes <code class="dcode">platformName</code>. If the capability specifies <code class="dcode">Android</code>, AppiumLB will choose a device from Android devices. The capability is necessary for Appium, but not for Selenium. <code class="dcode">browserName</code> and <code class="dcode">browserVersion</code> capabilities will be part of selector for Selenium to detect a browser device.

Examples

Appium

  • Python

# Selector as JSON object
server_url = 'https://appium-dev.headspin.io/v0/your-api-token/wd/hub'
desired_capability = {
    'platformName': 'android',
    'automationName': 'uiautomator2',
    'browserName': 'Chrome',
    'deviceName': 'Android',
    'headspin:selector': {
        'sku': 'Moto X',
        'city': 'Mountain View',
        'carrier': ['Sprint', 'WiFi']
    },
    'headspin:requestTimeout': 120
}
driver = webdriver.Remote(server_url, desired_capability)
driver.title

# Selector as string with more operators
server_url = 'https://appium-dev.headspin.io/v0/your-api-token/wd/hub'
desired_capability = {
    'platformName': 'android',
    'automationName': 'uiautomator2',
    'browserName': 'Chrome',
    'deviceName': 'Android',
    'headspin:selector': 'os_version:>9 city:"Mountain View, US"',
    'headspin:requestTimeout': 120
}
driver = webdriver.Remote(server_url, desired_capability)
driver.title
  • Ruby Core

require 'appium_lib_core'

server_url = 'https://appium-dev.headspin.io/v0/your-api-token/wd/hub'
desired_cap = {
  platformName: :android,
  automationName: 'uiautomator2',
  browserName: 'Chrome',
  deviceName: 'Android',
  udid: 'particular-device-udid'  # Can be selector by string like 'udid:"model: \"Nexus 5X\""'
  'headspin:requestTimeout' => 120 # sec
}

core = Appium::Core.for url: server_url, desired_capabilities: desired_cap
driver = core.start_driver
driver.title

Selenium

  • Python

server_url = 'https://appium-dev.headspin.io/v0/your-api-token/wd/hub'
desired_capability = {
    'browserName': 'firefox',
}
driver = webdriver.Remote(server_url, desired_capability)
driver.title
  • Ruby

require "selenium-webdriver"

server_url = 'https://appium-dev.headspin.io/v0/your-api-token/wd/hub'
desired_cap = {
  'headspin:selector' => '(device_skus: "firefox", city: "Mountain View, US")',
}

driver = Selenium::WebDriver.for :remote, url: server_url, desired_capabilities: desired_cap
driver.title

Resolving Errors

AppiumLB returns standard errors by Appium instances on proxy servers, and also the following:

HTTP status code Error (error) Note How to Resolve
400 invalid argument Request body is invalid in create session command Make sure your Appium or Selenium commands are correct.
404 invalid session id Cannot find active session id which AppiumLB should proxy commands to Run your tests from session creation.
404 unknown command The Appium or Selenium command is unknown Make sure your Appium or Selenium commands are correct.
500 session not created Cannot find available devices in your device pool. Make sure there are available devices in your device pool matching the selector. Matching devices might be temporarily locked by other users or unavailable. Please read Retry and avoid previously failed devices to understand how AppiumLB behaves in this case.
500 unknown error Something is wrong in AppiumLB Please contact us if it happens continuously.

Advanced Usage

Increasing Network Efficiency With directConnect Capabilities

With AppiumLB, a request goes through an additional AppiumLB server and therefore takes a small amount of additional network time, usually of about 5-10 seconds but potentially up to a minute (see this issue Pro for more details). If this presents a problem, the <code class="dcode">directConnect</code> capabilities can be used to speed up network time. To use this, your Appium client must support the <code class="dcode">directConnect</code> capabilities and the <code class="dcode">directConnect</code> capability must be set to <code class="dcode">true</code>. Currently only limited clients support this capbility. Please work with your internal IT departments and HeadSpin contacts to determine whether directConnect is a possibility for your testing environment.

A successful request with <code class="dcode">directConnect</code> made to AppiumLB will receive a <code class="dcode">create session</code> response, with information about the session through the <code class="dcode">directConnect</code> capabilities. The capabilities contain hostname, port and path to the actual Appium server with the devices selected for the tests. They are:

  • appium:directConnectProtocol and directConnectProtocol
    • The connection protocol AppiumLB proxies to. Can be https or http.
  • appium:directConnectPort and directConnectPort
    • The port number the AppiumLB proxies to
  • appium:directConnectHost and directConnectHost
    • The hostname the AppiumLB proxies to
  • appium:directConnectPath and directConnectPath
    • The base path the AppiumLB proxies to
  • headspin:directConnectDeviceAddress and directConnectDeviceAddress
    • The device the AppiumLB proxies to
    • This value is HeadSpin specific attribute. This value is device_address as device_id@hostname to detect the device in HeadSpin platform.

With this information, your Appium client can be configured to communicate with the test device directly.

Example Requests

Below are example requests sent with <code class="dcode">directConnect</code>.

Python

● Python client <code class="dcode">0.39+</code>

  • documentation
  • <code class="dcode">direct_connection</code> is enabled by default.

# Python

remote_url = 'https://appium-dev.headspin.io/v0/{your_api_token}/wd/hub'
direct_connection=True # This is necessary to enable the directConnect feature.

base_capability_chrome = {
    'platformName': 'Android',
    'browserName': 'Chrome',
    'deviceName': 'Android',
    'autoAcceptsAlerts': 'true',
    'automationName': 'uiautomator2',
    'headspin:selector': 'city:"Mountain View"'
}
driver = webdriver.Remote(self.remote_url.format(host=self.appiumlb, api_token=self.api_token),
    caps, direct_connection=direct_connection)

# For Python client 2.3.0+
# from appium.options.android import UiAutomator2Options
# options = UiAutomator2Options().load_capabilities({
#     'platformVersion': '12',
#     'deviceName': 'Android',
#     'browserName': 'chrome',
#     'headspin:selector': 'city:"Mountain View"'
# })
#
# self.driver = webdriver.Remote(self.remote_url.format(host=self.appiumlb, api_token=self.api_token),
#     options=options, direct_connection=direct_connection
# )

driver.title
driver.capabilities['directConnectDeviceAddress'] # u'device_id@proxy-us-sf-16.headspin.io'

Ruby

● ruby_lib <code class="dcode">10.0.0+</code>

● ruby_lib_core <code class="dcode">3.0.1+</code>

  • documentation
  • <code class="dcode">direct_connect</code> is enabled by default.

# ruby_lib_core
caps = {
    platformName: 'ios',
    platformVersion: '11.0',
    deviceName: 'iPhone Simulator',
    automationName: 'XCUITest',
    app: '/path/to/MyiOS.app'
}
appium_lib = { direct_connect: true }

# Ruby client sends a create session command to
# https://appium-dev.headspin.io/v0/your-api-token/wd/hub (AppiumLB)
@core = Appium::Core.for url: 'https://appium-dev.headspin.io/v0/your-api-token/wd/hub', capabilities: caps, appium_lib: appium_lib
driver = core.start_driver


driver.capabilities['directConnectDeviceAddress'] #=> 'device_id@proxy-us-sf-16.headspin.io'

driver.capabilities
#=> #<Selenium::WebDriver::Remote::Capabilities:0x00007f9bfe8ae7a0
# @capabilities=
#   {:browser_name=>"Chrome",
#   ...
#   "directConnectProtocol"=>"https",
#   "directConnectPort"=>7200,
#   ...
#   "directConnectHost"=>"proxy-us-sf-16.headspin.io",
#   ...
#   "directConnectPath"=>"/v0/your-token/wd/hub"}>

# Ruby client sends a get page source command to
# https://proxy-us-sf-16.headspin.io:7200/v0/your-api-token/wd/hub
driver.page_source
#=> "<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>\r\n<hierarchy...

Java

● Java client <code class="dcode">8.2.1+</code>

  • Disabled by default

AppiumClientConfig appiumClientConfig = AppiumClientConfig.defaultConfig()
    .directConnect(true)
    .baseUri(URI.create("https://appium-dev.headspin.io/v0/your-api-token/wd/hub"))
UiAutomator2Options options = new UiAutomator2Options();
AndroidDriver driver = new AndroidDriver(appiumClientConfig, options);

JavaScript (webdriverio)

● webdriverio <code class="dcode">v7.16.14+</code>

  • enableDirectConnect option configures the availability.
  • <code class="dcode">enableDirectConnect</code> is enabled by default.

Example Responses

Below are some example responses as W3C or MJOSNWP with a session creation command.

W3C create session response


{
    "value": {
        "capabilities": {
            ...
            "platformName":"ios",
            "automationName":"XCUITest",
            "app":"your/test/app",
            "platformVersion":"11.4",
            "deviceName":"iPhone 8",
            "udid":"8CB5BE32-3600-4007-B894-13E48EFB4D65",
            ...
            "directConnectProtocol": "https",
            "directConnectPort": 7200,
            "directConnectHost": "proxy-us-sf-16.headspin.io",
            "directConnectPath": "/v0/your-api-token/wd/hub",
            "directConnectDeviceAddress": "device_id@proxy-us-sf-16.headspin.io"
        },
        "sessionId":"85447dd9-cfc2-4091-9851-9eb738681ff7"
    }
}

MJSONWP create session response


{
    "status": 0,
    "value": {
        ...
        "platformName":"ios",
        "automationName":"XCUITest",
        "app":"your/test/app",
        "platformVersion":"11.4",
        "deviceName":"iPhone 8",
        "udid":"8CB5BE32-3600-4007-B894-13E48EFB4D65",
        ...
        "directConnectProtocol": "https",
        "directConnectPort": 7200,
        "directConnectHost": "proxy-us-sf-16.headspin.io",
        "directConnectPath": "/v0/your-api-token/wd/hub",
        "directConnectDeviceAddress": "device_id@proxy-us-sf-16.headspin.io"
    },
    "sessionId": "8cb4b6d4-d9e7-4da0-b5da-04efbbd09af3"
}

Selenium Grid and AppiumLB

Selenium Grid (hub) and AppiumLB behave similarly.

Selenium Grid has a hub and nodes. A node is a Selenium driver or an Appium server hosting a device. The hub handles a new session request to a proper node in order to establish a session following the capabilities. The node responds to the new session request with a session ID. Then, the hub proxies requests that have the same session ID to the proper node. This helps clients handle various nodes behind the hub since they need to know only one WebDriver URL through the hub. Clients do not need to know each WebDriver URL behind the hub for each separate Selenium driver and Appium server.

The Selenium Grid hub and node behavior and relationships are similar to AppiumLB and available devices in your device pool. AppiumLB picks a device up from your device pool in a new session request. It has the ability to wait for a device to be available using the <code class="dcode">waitForDeviceOnlineTimeout</code> capability. Once the new session request succeeds, commands for the same session ID can reach the device until the session is expired. One unique feature in AppiumLB is its directConnect capabilities to reduce network latency between the client and the device after a new session request. This feature allows clients to communicate with each PBox directly after a new session request. With direct connect, only the first new session request may have additional network latency due to routing through the load balancer.

A client needs to keep the HTTP connection to get a response by Selenium Grid or AppiumLB. A new session request tends to have a longer timeout than other commands since it includes some device setup processes. Client read timeouts before getting the response for a new session request can cause a new session creation failure. <code class="dcode">newSessionWaitTimeout</code> in Selenium Grid is similar to <code class="dcode">waitForDeviceOnlineTimeout</code> in AppiumLB to attempt to find an available node or device to coordinate the new session request.