これは、このセクションの複数ページの印刷可能なビューです。 印刷するには、ここをクリックしてください.

このページの通常のビューに戻る.

Grid

複数のマシンで並行してテストを実行したいですか? Grid が手助けします。

Selenium Grid を利用して、クライアントからリモートブラウザーインスタンスにコマンドを ルーティングし、リモートマシン上で WebDriver スクリプトを実行することができます。

Grid の目標は、

  • 複数のマシンでの並行したテスト実行を、簡単な方法で提供する
  • 異なるバージョンのブラウザでのテストを可能にする
  • クロスプラットフォームテストを可能にする

興味がありますか? Grid の仕組みと設定方法が知りたければ以下のセクションを読んでください。

1 - Gridを始める

Selenium Gridの導入方法

クイックスタート

  1. 事前条件

  2. Grid の起動

    • java -jar selenium-server-<version>.jar standalone
  3. あなたの WebDriver テストの対象を http://localhost:4444 に向ける

  4. (必要があれば) ブラウザでhttp://localhost:4444を開いて実行中のテストや利用可能な capabilities を確認する。

さらにオプションを知りたい場合は以降のセクションに進んでください。

Grid コンポーネントロール

Grid は 6 つの異なるコンポーネントで構成され、様々な方法でデプロイすることができます。

必要に応じて、それぞれ個別に起動する(分散)か、ハブ&ノードのグループに分けるか、 全てを一つのマシンで起動する(スタンドアロン)かを選べます。

スタンドアロン

スタンドアロンは全ての Gridコンポーネントを 1 つに連結します。 スタンドアロンモードはシングルプロセスで動き、Grid の全機能を利用することができます。 スタンドアロンは単一のマシン上でのみ動かすことができます。

スタンドアロンは Selenium Grid を起動する最も簡単な方法でもあります。 デフォルトではサーバーはhttp://localhost:4444RemoteWebDriver リクエストをリッスンします。 サーバーはデフォルトでシステムパス上の利用可能なドライバーを検出します。

java -jar selenium-server-<version>.jar standalone

スタンドアロンで Grid のを起動したら、WebDriver テストの対象をhttp://localhost:4444に向けてください。

スタンドアロンの一般的なユースケースは:

  • RemoteWebDriver を使用したローカルでの開発やデバッグ
  • コードをプッシュする前の簡易なテスト実行
  • CI/CD 向けの Grid のセットアップ(GitHub Actions, Jenkins など)

ハブ&ノード

ハブ&ノードは最も利用されているロールです。その理由は:

  • 様々なマシンを Grid に統合できます
    • 様々な OS やブラウザーバージョンを持つマシンなど
  • WebDriver テストのエントリーポイントを持っています
  • Grid を停止せずにキャパシティのスケールアップ・ダウンが可能です

ハブ

ハブは以下のコンポーネントで構成されています。 ルーター、ディストリビューター、セッションマップ、新規セッションキュー、イベントバス。

java -jar selenium-server-<version>.jar hub

デフォルトでは、サーバーはhttp://localhost:4444にて RemoteWebDriver リクエストを待ち受けます。

ノード

ノードは起動時にシステムのパス が通っている利用可能なドライバーを検出します。

次のコマンドはノードハブと同じマシン上で動作していることを前提としています。

java -jar selenium-server-<version>.jar node
同一マシン上での複数ノード

ノード 1

java -jar selenium-server-<version>.jar node --port 5555

ノード 2

java -jar selenium-server-<version>.jar node --port 6666
異なるマシンでノードとハブを動かす

ハブノードは HTTP とイベントバスを介して通信します (イベントバスハブの一部として存在します)。 ノードイベントバスを通じてメッセージを送信し、登録処理を開始します。 ハブがメッセージを受け取り、ノードの存在を確かめるため HTTP を使ってノードにアクセスします。

ハブがデフォルトのポートを使用していれば、 --hub フラグでノードを登録することができます。

java -jar selenium-server-<version>.jar node --hub http://<hub-ip>:4444

ハブがデフォルトのポートを使用していない場合、--publish-events--subscribe-events のフラグが必要です。

例えばハブ8886 8887 8888 ポートを利用している場合、

java -jar selenium-server-<version>.jar hub --publish-events tcp://<hub-ip>:8886 --subscribe-events tcp://<hub-ip>:8887 --port 8888

ノードはこれらのポートを登録する際に使用します。

java -jar selenium-server-<version>.jar node --publish-events tcp://<hub-ip>:8886 --subscribe-events tcp://<hub-ip>:8887

分散

分散 Grid を利用すると、各コンポーネントは別々に起動され異なるマシン上で動作します。

  1. イベントバスは Grid コンポーネント間での内部通信を可能にします。

デフォルトポートは 4442, 4443, 5557 です。

java -jar selenium-server-<version>.jar event-bus --publish-events tcp://<event-bus-ip>:4442 --subscribe-events tcp://<event-bus-ip>:4443 --port 5557
  1. 新規セッションキューは新規セッションリクエストをキューに積み、ディストリビューターがリクエストを取得できるようにします。

デフォルトポートは 5559 です。

java -jar selenium-server-<version>.jar sessionqueue --port 5559
  1. セッションマップはセッション ID とそのセッションが実行中のノードのマップを持ちます。

デフォルトのセッションマップのポートは 5556 です。 セッションマップイベントバスと通信します。

java -jar selenium-server-<version>.jar sessions --publish-events tcp://<event-bus-ip>:4442 --subscribe-events tcp://<event-bus-ip>:4443 --port 5556
  1. ディストリビューター新規セッションキューに新規セッションリクエストを問い合わせ、 capabilities がマッチするノードにアサインします。ノードは、ハブ&ノード構成の Grid におけるハブの登録と同じように、ディストリビューターに登録します。

デフォルトのディストリビューターのポートは 5553 です。 ディストリビューター新規セッションキューセッションマップイベントバスノードと通信します。

java -jar selenium-server-<version>.jar distributor --publish-events tcp://<event-bus-ip>:4442 --subscribe-events tcp://<event-bus-ip>:4443 --sessions http://<sessions-ip>:5556 --sessionqueue http://<new-session-queue-ip>:5559 --port 5553 --bind-bus false
  1. ルーター新規セッションリクエストをキューに、既存セッションのリクエストをそのセッションが実行中のノードに転送します。

デフォルトのルーターのポートは 4444 です。 ルーター新規セッションキューセッションマップディストリビューターと通信します。

java -jar selenium-server-<version>.jar router --sessions http://<sessions-ip>:5556 --distributor http://<distributor-ip>:5553 --sessionqueue http://<new-session-queue-ip>:5559 --port 4444
  1. ノード

デフォルトのノードのポートは 5555 です。

java -jar selenium-server-<version>.jar node --publish-events tcp://<event-bus-ip>:4442 --subscribe-events tcp://<event-bus-ip>:4443

テストメタデータ

テストにメタデータを追加して、GraphQL 経由で使用するか、Selenium Grid UI 経由でその一部( se:name など) を可視化します。

メタデータは capability にse:プリフィックスをつけることで追加できます。 Java での簡単な例を紹介します。

ChromeOptions chromeOptions = new ChromeOptions();
chromeOptions.setCapability("browserVersion", "100");
chromeOptions.setCapability("platformName", "Windows");
// Showing a test name instead of the session id in the Grid UI
chromeOptions.setCapability("se:name", "My simple test");
// Other type of metadata can be seen in the Grid UI by clicking on the
// session info or via GraphQL
chromeOptions.setCapability("se:sampleMetadata", "Sample metadata value");
WebDriver driver = new RemoteWebDriver(new URL("http://gridUrl:4444"), chromeOptions);
driver.get("http://www.google.com");
driver.quit();

Selenium Grid のクエリ

Grid 起動後、ステータスを問い合わせる方法は、 Grid UI と API 呼び出しの主に 2 通りあります。

Grid UI は、お好みのブラウザでhttp://localhost:4444 にアクセスすることで見られます。

API 呼び出しはhttp://localhost:4444/statusのエンドポイントか、 GraphQLが利用できます。

このページで紹介するコマンドの例は、わかりやすくするために コンポーネントがローカルで動作していると仮定しています。 より詳細な例と使用方法はコンポーネントの章を参照してください。

Java 11 の HTTP クライアントを利用する

Selenium v4.5

デフォルトでは Grid はAsyncHttpClientを使用します。 AsyncHttpClient は Netty を使ったオープンソースのライブラリで、非同期での HTTP リクエストを実現します。 さらに WebSocket をサポートするため Grid に適しています。

しかし、AsyncHttpClient は 2021 年からあまり活発にメンテナンスされていません。 そして Java 11+ではビルトインの HTTP と WebSocket のクライアントを提供しています。 現在 Selenium はサポートする最小バージョンを Java11 にアップグレードする計画をしていますが、 それにはかなりの労力が必要です。メジャーリリースに合わせることはユーザー体験にとって重要です。

Java11 のクライアントを利用するにはselenium-http-jdk-clientjar ファイルをダウンロードし、 --extフラグで Grid の jar のクラスパスに通す必要があります。

jar ファイルはrepo1.maven.orgから直接ダウンロードできます。 Grid を起動する方法は以下の通りです:

java -Dwebdriver.http.factory=jdk-http-client -jar selenium-server-<version>.jar --ext selenium-http-jdk-client-<version>.jar standalone

selenium-http-jdk-clientをダウンロードする別の方法としてCoursierを使う方法があります。

java -Dwebdriver.http.factory=jdk-http-client -jar selenium-server-<version>.jar --ext $(coursier fetch -p org.seleniumhq.selenium:selenium-http-jdk-client:<version>) standalone

ハブ&ノードか分散モードで動かす場合、-Dwebdriver.http.factory=jdk-http-client--extフラグの設定が各コンポーネントに必要になります。

Grid サイズ

Grid ロールの選択は、どのような OS やブラウザをサポートする必要があるかによって決まります。 どの OS、ブラウザをサポートするか、どのくらいの並列セッションを実行するか、マシンの数とそれらの性能(CPU、RAM)に依存します。

セッションを並列で作成するのは、ディストリビューターが利用可能なプロセッサーに依存します。たとえば、 マシンに 4 つの CPU がある場合、ディストリビューターは同時に最大 4 つのセッションしか作成できません。

デフォルトでは、ノードがサポートする同時セッションの最大数は、利用可能な CPU の数によって制限されます。 たとえば、ノードのマシンに 8CPU がある場合、同時に最大 8 つのブラウザセッションを実行できます(ただし、Safari は常に 1 つです)。 また各ブラウザセッションは約 1GB の RAM を使用することが期待されます。

一般的にノードはできるだけ小さくすることが推奨されます。32CPU と 32GB の RAM を持つマシンで 32 の同時ブラウザセッションを実行するよりも、 32 の小さな プロセスをよりよく分離するために、 32 の小さなノードを持つことが推奨されます。これによって、もしノードに障害が発生しても、分離されて処理されます。 Docker はこの方法を実現するための優れたツールです。

デフォルト値(ブラウザあたり 1CPU/1GB RAM)は推奨値であり、あなたの用途に沿わない可能性があることに注意してください。 この値は参考値としての推奨であり、継続的にパフォーマンスを測定することで、あなたの環境にとって理想的な値を見つけることができるでしょう。

Grid のサイズは、サポートされる同時セッションの数と、ノードの数に関連しており、 万能なサイズというものはありません。以下のサイズは概算で、環境が違えば変わる可能性があります。 例えば、120 台のノードを持つハブ&ノードの場合、ハブが十分なリソースを持っていればうまく機能するかもしれません。 また、この数値は確定したものではありません。フィードバックをお待ちしています。

Small

5 台以下のノードで、スタンドアロンハブ&ノード

Middle

6〜60 台のノードで、ハブ&ノード

Large

60〜100 台のノードで、ハブ&ノード、あるいは 100 台以上のノード分散

警告

Grid を保護しないと、以下のような問題が発生する可能性があります。

  • Grid インフラストラクチャへのオープンアクセスを許容してしまう。
  • サードパーティが内部 Web アプリケーションやファイルにアクセスすることを許可してしまう。
  • サードパーティにカスタムバイナリの実行を許可してしまう。

Detectify のブログで公開されてしまった Grid が どのように悪用されるかを紹介しています: Don’t Leave your Grid Wide Open

参考文献

2 - いつGridを使用すべきか

Gridがあなたにとって最適なツールでしょうか?

Grid をいつ使うべきでしょうか?

  • 複数のブラウザー、異なるタイプまたは異なるバージョンのブラウザー、あるいは異なる OS 上でで実行されているブラウザーに対して並列でテストを実行したい場合
  • テストスイートの実行時間を減らしたい場合

Selenium Grid はノードと呼ばれる複数のマシンを使用して並列でテストスイートを実行します。 大規模な長時間実行されるテストでは数分から数時間、あるいは数日単位での短縮が可能です。 つまり、あなたのテスト対象のアプリケーションのテスト結果を得るまでの時間を短縮します。

Grid は複数の異なるブラウザーに対してテストを実行でき、また同じブラウザーインスタンスに対して複数のテストを実行することも可能です。 たとえば 6 つのノードで構成された Grid があるとします。 最初のマシンは FireFox の最新バージョン、2 つめは FireFox の最新から一つ前のバージョン、 3 つめは 最新の Chrome、そして残りは Mac Mini で最新の Safari を使って 3 つのテストを並行して実行することができます。

実行時間は簡単な式で表すことができます:

テストの数 * 平均テスト時間 / ノード数 = 合計実行時間

   15      *       45s        /        1        =      11m 15s   // Grid なし
   15      *       45s        /        5        =      2m 15s    // 5 ノードの Grid
   15      *       45s        /        15       =      45s       // 15 ノードの Grid
  100      *       120s       /        15       =      13m 20s   // Grid なしの場合3時間以上かかります

テストスイートが実行されると Grid はテストで設定されたブラウザに対して実行するテストを割り当てます。

このような設定により、大規模な Selenium テストスイートであっても実行時間を大幅に短縮することができます。

Selenium Grid は Selenium プロジェクトの一部であり、 Selenium コアの開発コミッターと同じチームによってメンテナンスされています。 テストの実行速度の重要性を認識し、Grid は Selenium プロジェクトの初期段階から重要な役割を担っています。

3 - Serenium Grid のコンポーネント

Grid コンポーネントの使い方について

Selenium Grid 4 は以前のバージョンから一新し、全面的に作り直されました。 全体的なパフォーマンスの改善と標準の準拠に加え、より現代的なコンピューティングとソフトウェア開発 に適応するために機能ごとに分割されました。 コンテナ化とクラウド上での分散スケーラビリティのために構築された、現代に適した全く新しいソリューションです。

Selenium Grid 4 Components

ルーター

これは Grid のエントリポイントであり、すべての外部リクエストを受信し正しいコンポーネントへルーティングします。

ルーター新規セッションリクエストを受信すると、新規セッションキューに転送します。

リクエストが既存のセッションのものである場合、ルーターセッションマップに、 セッションが実行されている ノード ID の取得を要求します。そしてノードにリクエストを直接転送します。

ルーターは、リクエストをより処理能力の高いコンポーネントに負荷を分散させます。 この負荷分散処理自体もコンポーネントに不必要に負荷をかけることはありません。

ディストリビューター

ディストリビューターの主な責務は 2 つあります:

すべてのノードとその capabilities を登録し追跡します

イベントバスを通じてノード登録イベントを贈ることでノードディストリビューターに登録します。 ディストリビューターはそのイベントを受けてノードの存在を HTTP リクエストで確認します。 リクエストが成功した場合、ディストリビューターノードを登録し、Grid モデルを通じて追跡を開始します。

保留中の新規セッションリクエストを処理する

新規セッションリクエストがルーターに送信されると、リクエストは新規セッションキューに転送されます。 ディストリビューター新規セッションキューをポーリングし、保留中の新規セッションリクエストを見つけると、 セッションが作成可能なノードを探します。 セッションが作成されるとディストリビューターは セッション ID とセッションが実行されるノードの紐付けをセッションマップに保存します。

セッションマップ

セッションマップはセッション ID とセッションが実行されているノードの紐付けを保存します。 これによりルーターがリクエストをノードに転送できるようにします。 ルーターセッションマップにセッション ID に紐づくノードを問い合わせます。

新規セッションキュー

新規セッションキューはすべての新規セッションリクエストを FIFO 順で保持します。 リクエストのタイムアウトとリトライ間隔の設定が可能です。

ルーターは新規セッションリクエストを新規セッションキューに追加し、レスポンスを待ちます。 新規セッションキューは定期的にキュー内のリクエストがタイムアウトしていないかをチェックし、 タイムアウトしたリクエストがあればリクエストを拒否しキューから取り除きます。

ディストリビューターはスロットに空きがあるかを定期的にチェックします。 もし空きがあれば、新規セッションキューから最初にマッチするリクエストを取り出し、 新規セッションの作成を試みます。

リクエストされた capabilities にマッチする空きノードスロットがあれば、 ディストリビューターは空きスロットの確保を試みます。全てのスロットがビジーだった場合、 ディストリビューターはリクエストをキューに戻します。 リトライ中やキューに戻す最中にリクエストがタイムアウトした場合リクエストは拒否されます。

セッションの作成に成功すると、ディストリビューターはセッションの情報を新規セッションキューに送信し、 これがルーターへのレスポンスとして送信され、最終的にクライアントに返ります。

ノード

Grid は複数のノードを持つことができます。 ノードは、各ノードが実行されているマシン上の利用可能なブラウザのスロットを管理します。

ノードは、イベントバスを介して自身をディストリビューターに登録します。 構成情報は登録メッセージの一部として送信されます。

デフォルトでは、ノードはマシンのパス上に存在する全てのブラウザドライバーを自動で登録します。 また FireFox と Chromium ベースブラウザの場合、CPU1 つにつき 1 スロットを作成します。 Safari の場合は 1 つのスロットのみ作成します。 特定の設定によってセッションを Docker コンテナで実行したり、コマンドを中継したりすることも可能です。

ノードは受信したコマンドを実行するだけで、コマンドの評価・判断や、フロー制御以外の制御は行いません。 ノードが実行されているマシンは、他のコンポーネントと同じ OS を持つ必要はありません。 たとえば、Windows ノードには IE Mode on Edge をブラウザーオプションとして提供する機能がありますが、 これは Linux または Mac では不可能です。 Grid は 複数の Windows, Mac, Linux ノードで構成することが可能です。

イベントバス

イベントバスノードディストリビューター、セッションキュー、セッションマップ間の通信経路として機能します。 Grid は内部通信のほとんどをメッセージで行うことで、負荷の高い HTTP 呼び出しを避けています。 分散モードで Grid を起動する場合、イベントバスは最初に起動されるべきコンポーネントです。

4 - コンポーネントの構成

ここでは、各コンポーネントを、共通の設定とコンポーネント固有の設定で個別に設定する方法を確認できます。

4.1 - 構成ヘルプ

Gridの設定に利用可能なオプション

ヘルプコマンドは、現在のコード実装に基づいて情報を表示します。 したがって、ドキュメントが更新されない場合に備えて、正確な情報を提供します。 それは、新しいバージョンのグリッド 4 の構成について学習する最も簡単な方法です。

Info コマンド

info コマンドは、次のトピックに関する詳細なドキュメントを提供します。

  • Selenium の構成
  • セキュリティ
  • セッションマップの設定
  • トレース

構成ヘルプ

クイック設定のヘルプと概要は、以下を実行することで提供されます。

java -jar selenium-server-<version>.jar info config

セキュリティ

安全な通信とノード登録のためのグリッドサーバーの設定の詳細を取得するには、以下を実行します。

java -jar selenium-server-<version>.jar info security

セッションマップの設定

デフォルトでは、グリッドはローカルセッションマップを使用してセッション情報を保存します。 グリッドは、Redis や JDBC-SQL がサポートするデータベースなどの追加のストレージオプションをサポートしています。 別のセッションストレージをセットアップするには、次のコマンドを使用してセットアップ手順を取得します。

java -jar selenium-server-<version>.jar info sessionmap

OpenTelemetry と Jaeger を使用したトレースの設定

デフォルトでは、トレースは有効になっています。 トレースをエクスポートして Jaeger 経由で視覚化するには、次のコマンドを使用して手順を実行します。

java -jar selenium-server-<version>.jar info tracing

SeleniumGrid コマンドを一覧表示する

java -jar selenium-server-<version>.jar --config-help

使用可能なすべてのコマンドとそれぞれの説明が表示されます。

コンポーネントヘルプコマンド

Selenium ロールの後に–help config オプションを渡して、コンポーネント固有の構成情報を取得します。

スタンドアロン

java -jar selenium-server-<version>.jar standalone --help

ハブ

java -jar selenium-server-<version>.jar hub --help

セッション

java -jar selenium-server-<version>.jar sessions --help

新規セッションキュー

java -jar selenium-server-<version>.jar sessionqueue --help

ディストリビューター

java -jar selenium-server-<version>.jar distributor --help

ルーター

java -jar selenium-server-<version>.jar router --help

ノード

java -jar selenium-server-<version>.jar node --help

4.2 - Selenium GridのCLI オプション

全てのGridコンポーネントのCLIオプション詳細

Grid の設定には、さまざまなセクションが用意されています。 各セクションには、コマンドライン引数で設定可能なオプションがあります。コマンドライン引数で設定できます。

コンポーネントとセクションの対応は以下の通りです。

オプションが変更、または追加されたが文書化されていない場合、 このドキュメントは古くなる可能性があることに注意してください。 もしそのような状況を見つけたら、“構成ヘルプ”を確認し、 ドキュメントを更新するプルリクエストを気軽に送ってください。

セクション

スタンドアロン ハブ ノード ディストリビューター ルーター セッション 新規セッションキュー
Distributor
Docker
Events
Logging
Network
Node
Router
Relay
Server
SessionQueue
Sessions

Distributor

オプション 値/例 概要
--healthcheck-interval int 120 全てのノードに対してヘルスチェックを実行する頻度(秒)を指定します。これにより、サーバーは全てのノードに対して正常に ping を送信できるようになります。
--distributor uri http://localhost:5553 ディストリビューターの URL。
--distributor-host string localhost ディストリビューターがリッスンするホスト名。
--distributor-implementation string org.openqa.selenium.grid.distributor.local.LocalDistributor デフォルトでないディストリビューター実装の完全なクラス名。
--distributor-port int 5553 ディストリビューターがリッスンするポート番号。
--reject-unsupported-caps boolean false Grid がサポートしていない capabilities をリクエストされた時、ディストリビューターがリクエストを即座に今日できるようにします。これはオンデマンドでノードを立ち上げをしない Grid の設定に適しています。
--slot-matcher string org.openqa.selenium.grid.data.DefaultSlotMatcher デフォルト以外で使用するスロットマッチャーの完全なクラス名。これはノードが特定のセッションをサポートできるかを判断するために使用されます。
--slot-selector string org.openqa.selenium.grid.distributor.selector.DefaultSlotSelector デフォルト以外のスロットセレクターの完全なクラス名。これは、ノードがマッチした後ノード内のスロットを選択するために使用されます。

Docker

オプション 値/例 概要
--docker-assets-path string /opt/selenium/assets アセットが保存される絶対パス。
--docker- string[] selenium/standalone-firefox:latest '{"browserName": "firefox"}' イメージとステレオタイプの capabilities を対応付ける Docker 設定 (例 `-D selenium/standalone-firefox:latest ‘{“browserName”: “firefox”}’)
--docker-devices string[] /dev/kvm:/dev/kvm コンテナに対してデバイスを公開します。各デバイスマッピングは、ホストとコンテナの両方のデバイスへのパスを、コロンで区切って保つ必要があります。例: /device/path/in/host:/device/path/in/container
--docker-host string localhost Docker デーモンが動作しているホスト名。
--docker-port int 2375 Docker デーモンが動作しているポート名。
--docker-url string http://localhost:2375 Docker デーモンに接続するための URL。
--docker-video-image string selenium/video:latest ビデオレコーディングが有効になっているときに利用される Docker イメージ。

Events

オプション 値/例 概要
--bind-bus boolean false 接続をバインドするかコネクトするかを指定します。
true の場合、コンポーネントはイベントバスにバインドされます(イベントバスもコンポーネントによって起動されます、通常はディストリビューターとハブによって起動されます)。
false の場合、コンポーネントがイベントバスにコネクトします。
--events-implementation string org.openqa.selenium.events.zeromq.ZeroMqEventBus デフォルトでないイベントバス実装の完全なクラス名。
--publish-events string tcp://*:4442 イベントをイベントバスに配信するための接続文字列。
--subscribe-events string tcp://*:4443 イベントをイベントバスから購読するための接続文字列。

Logging

オプション 値/例 概要
--http-logs boolean false http ログを有効にします。http ログを記録するには、トレースを有効にする必要があります。
--log-encoding string UTF-8 ログのエンコーディング。
--log string Windows パスの例:
'\path\to\file\gridlog.log'
or
'C:\path\path\to\file\gridlog.log'

Linux/Unix/MacOS パスの例:
'/path/to/file/gridlog.log'
ログを出力するファイル。OS のファイルパスと互換性があることを確認してください。
--log-level string “INFO” ログレベル。デフォルトは INFO です。 ログレベルはこちらを参照してください。 https://docs.oracle.com/javase/7/docs/api/java/util/logging/Level.html
--plain-logs boolean true プレーンなログを使用します。
--structured-logs boolean false 構造化ログを使用します。
--tracing boolean true トレースを有効にします。
--log-timestamp-format string HH:mm:ss.SSS ログのタイムスタンプ形式を設定できます。

Network

オプション 値/例 概要
--relax-checks boolean false 受信リクエストのオリジンヘッダーとコンテンツタイプに対する、厳格な W3C 準拠の検証をを緩和します。

Node

オプション 値/例 概要
--detect-drivers boolean true 現在のシステム上で利用可能なドライバーを自動で検出してノードに追加します。
--driver-configuration string[] display-name="Firefox Nightly" max-sessions=2 webdriver-path="/usr/local/bin/geckodriver" stereotype='{"browserName": "firefox", "browserVersion": "86", "moz:firefoxOptions": {"binary":"/Applications/Firefox Nightly.app/Contents/MacOS/firefox-bin"}}' ノードがサポートするドライバーの一覧。可読性向上のため TOML ファイルで設定することを推奨します。
--driver-factory string[] org.openqa.selenium.example.LynxDriverFactory '{"browserName": "lynx"}' 完全修飾クラス名と、そのクラスが対応するブラウザの設定とのマッピング。
--driver-implementation string[] "firefox" チェックされるドライバー。指定された場合、自動設定はスキップされます。
--node-implementation string "org.openqa.selenium.grid.node.local.LocalNodeFactory" デフォルトでないノード実装の完全なクラス名。これはセッションのライフサイクルを管理するために使用されます。
--grid-url string https://grid.example.com Grid 全体のパブリックな URL (通常ハブかルーターのアドレスです)。
--heartbeat-period int 60 ノードが生存していることを知らせるため、ノードがディストリビューターに送るハードビートを、どのくらいの頻度(秒)で送るか。
--max-sessions int 8 最大同時接続セッション数。デフォルトは利用可能なプロセッサーの数です。
--override-max-sessions boolean false 利用可能なプロセッサーの数は、推奨される最大セッション数(プロセッサーごとに 1 つのブラウザセッション)です。このフラグを true に設定すると、推奨される最大値を上書きすることができます。セッションの安定性と信頼性が損なわれ、ホストがリソースを使い果たす可能性があります。
--register-cycle int 10 ノードがディストリビューターに初回登録を試みる頻度(秒)。
--register-period int 120 ノードが初めてディストリビューターに初回登録を試みるのにかかる時間(秒)。この時間が経過すると、ノードは再登録を試みない。
--session-timeout int 300 X をセッションタイムアウト(秒)としたとき、 ノード は、過去 X 秒間に何の活動もなかったセッションを自動的に終了させます。 これにより他のテストが利用できるようスロットを解放します。
--vnc-env-var string START_XVFB VNC ストリームが利用可能かどうかを判断するために利用する環境変数。
--no-vnc-port int 7900 VNC が利用可能な場合、ローカルの noVNC ストリームを取得できるポートを設定します。
--drain-after-session-count int 1 X 個のセッションが実行された後に、ノードをドレインしてシャットダウンします。 Kubernetes のような環境で有用です。 0 より大きい値を指定すると、この機能が有効になります。
--hub string http://localhost:4444 ハブ・ノード構成におけるハブのアドレスを指定します。ホスト名か IP アドレスが指定できます。この場合、ハブは http://hostname:4444 とみなされ、 --grid-url は同じものになります。 --publish-eventstcp://hostname:4442--subscribe-eventstcp://hostname:4443 となります。 hostname にポート番号が含まれている場合は、それが --grid-url に使用されますが、イベントバスの URI は変更されません。これらのデフォルト値は、適切なフラグを設定することでオーバーライドすることができます。ホスト名にプロトコル(httpsのような)が含まれる場合もそれが利用されます。
--enable-cdp boolean true Grid 内で CDP プロキシーを有効にします。もしネットワークが web socket を許可していない場合、Grid 管理者は CDP を無効にできます。デフォルトは true です。
--enable-managed-downloads boolean false This causes the Node to auto manage files downloaded for a given session on the Node.
--selenium-manager boolean false When drivers are not available on the current system, use Selenium Manager. False by default.

Relay

オプション 値/例 概要
--service-url string http://localhost:4723 Appium サーバーやクラウドサービスなど、WebDriver コマンドをサポートするサービスに接続するための URL です。
--service-host string localhost WebDriver コマンドをサポートしてるサービスが稼働しているホスト名。
--service-port int 4723 WebDriver コマンドをサポートしてるサービスが稼働しているポート番号。
--service-status-endpoint string /status WebDriver サービスの状態を問い合わせるエンドポイント、オプショナルです。HTTP 200 レスポンスが期待されます。
--service-configuration string[] max-sessions=2 stereotype='{"browserName": "safari", "platformName": "iOS", "appium:platformVersion": "14.5"}}' 呼び出しの中継先となるサービスの設定。可読性向上のため、TOML ファイルで設定することを推奨します。

Router

オプション 値/例 概要
--password string myStrongPassword クライアントがサーバーに接続する際に使用するパスワード。このパスワードとユーザー名の両方が設定されていないと使用できません。
--username string admin クライアントがサーバーに接続する際に使用するユーザー名。このユーザー名とパスワードの両方が設定されていないと使用できません。
--sub-path string my_company/selenium_grid A sub-path that should be considered for all user facing routes on the Hub/Router/Standalone.

Server

オプション 値/例 概要
--allow-cors boolean true Selenium サーバーが任意のホストからのウェブブラウザ接続を許可するかどうか。
--host string localhost サーバーの IP もしくはホスト名、通常自動的に決定されます。
--bind-host boolean true サーバがホストアドレス/ホスト名にバインドするか、あるいは到達可能な URL を知らせるためだけに使用するかを指定します。複雑なネットワーク構成で、サーバが現在の IP やホスト名ではなく、 外部の IP やホスト名で自分自身を公開する場合に有用です (例: Docker コンテナ内)。
--https-certificate path /path/to/cert.pem HTTPS のためのサーバー証明書。詳細は “java -jar selenium-server.jar info security” を実行してください。
--https-private-key path /path/to/key.pkcs8 HTTPS のための秘密鍵。 詳細は “java -jar selenium-server.jar info security” を実行してください。
--max-threads int 24 リスナースレッドの最大数。デフォルトは、有効なプロセッサーの * 3 です。
--port int 4444 リッスンポート。このパラメータは異なるコンポーネントによって使用されるため、デフォルトはありません。例えば、ルータ/ハブ/スタンドアロンは 4444 を使用し、ノードは 5555 を使用します。

SessionQueue

オプション 値/例 概要
--sessionqueue uri http://localhost:1237 新規セッションキューサーバーのアドレス。
-sessionqueue-host string localhost 新規セッションキューがリッスンするホスト。
--sessionqueue-port int 1234 新規セッションキューがリッスンするポート
--session-request-timeout int 300 タイムアウト(秒)。 新規セッションリクエストはキューに追加され、設定された時間以上キューに残っているリクエストはタイムアウトします。
--session-retry-interval int 5 リトライ間隔(秒)。すべてのスロットがビジーな場合、 新規セッションリクエストはこの時間の間隔をおいてからリトライされます。

Sessions

オプション 値/例 概要
--sessions uri http://localhost:1234 セッションマップサーバーのアドレス。
--sessions-host string localhost セッションマップサーバーがリッスンするホスト。
--sessions-port int 1234 セッションマップサーバーがリッスンするポート。

設定例

上記のオプションはすべて、Grid コンポーネントを起動する際に使用することができます。 Grid の適切な設定を模索するのに利用してください。

TOML ファイル を使用して Grid を設定することをおすすめします。 設定ファイルは読みやすく、コード管理できます。

必要に応じて TOML ファイルと CLI オプションを併用することができます。

コマンドラインフラグ

コマンドラインフラグとしてオプションを渡すには、適切なコンポーネントを特定し以下のテンプレートのようにします。

java -jar selenium-server-<version>.jar <component> --<option> value

スタドアロン、最大セッションとメインポートを設定する

java -jar selenium-server-<version>.jar standalone --max-sessions 4 --port 4444

ハブ、新規セッションリクエストのタイムアウト、メインポートを設定し、トレースを無効にする

java -jar selenium-server-<version>.jar hub --session-request-timeout 500 --port 3333 --tracing false

ノード、最大 4 セッション、デバッグログ、ポート 7777, FireFox と Edge のみ

java -jar selenium-server-<version>.jar node --max-sessions 4 --log-level "fine" --port 7777 --driver-implementation "firefox" --driver-implementation "edge"

ディストリビューター、セッションマップ・新規セッションキューの URL を指定、バスを無効にする

java -jar selenium-server-<version>.jar distributor --sessions http://localhost:5556 --sessionqueue http://localhost:5559 --bind-bus false

特定ノードにカスタム capabilities を設定する

重要: カスタム capabilities は全てのノードに設定される必要があります。 また全てのセッションリクエストに含まれなければいけません。

ハブの起動
java -jar selenium-server-<version>.jar hub
customcap に true をセットしてノード A を起動する
java -jar selenium-server-<version>.jar node --detect-drivers false --driver-configuration display-name="Chrome (custom capability true)" max-sessions=1 stereotype='{"browserName":"chrome","gsg:customcap":true}' --port 6161
customcap に false をセットしてノード B を起動する
java -jar selenium-server-<version>.jar node --detect-drivers false --driver-configuration display-name="Chrome (custom capability true)" max-sessions=1 stereotype='{"browserName":"chrome","gsg:customcap":false}' --port 6262
ノード A とマッチ
ChromeOptions options = new ChromeOptions();
options.setCapability("gsg:customcap", true);
WebDriver driver = new RemoteWebDriver(new URL("http://localhost:4444"), options);
driver.get("https://selenium.dev");
driver.quit();

ノード B とマッチさせるにはカスタム capability を false に設定します。

Enabling Managed downloads by the Node

At times a test may need to access files that were downloaded by it on the Node. To retrieve such files, following can be done.

Start the Hub
java -jar selenium-server-<version>.jar hub
Start the Node with manage downloads enabled
java -jar selenium-server-<version>.jar node --enable-managed-downloads true
Set the capability at the test level

Tests that want to use this feature should set the capability "se:downloadsEnabled"to true

options.setCapability("se:downloadsEnabled", true);
How does this work
  • The Grid infrastructure will try to match a session request with "se:downloadsEnabled" against ONLY those nodes which were started with --enable-managed-downloads true
  • If a session is matched, then the Node automatically sets the required capabilities to let the browser know, as to where should a file be downloaded.
  • The Node now allows a user to:
    • List all the files that were downloaded for a specific session and
    • Retrieve a specific file from the list of files.
  • The directory into which files were downloaded for a specific session gets automatically cleaned up when the session ends (or) timesout due to inactivity.

Note: Currently this capability is ONLY supported on:

  • Edge
  • Firefox and
  • Chrome browser
Listing files that can be downloaded for current session:
  • The endpoint to GET from is /session/<sessionId>/se/files.
  • The session needs to be active in order for the command to work.
  • The raw response looks like below:
{
  "value": {
    "names": [
      "Red-blue-green-channel.jpg"
    ]
  }
}

In the response the list of file names appear under the key names.

Dowloading a file:
  • The endpoint to POST from is /session/<sessionId>/se/files with a payload of the form {"name": "fileNameGoesHere}
  • The session needs to be active in order for the command to work.
  • The raw response looks like below:
{
  "value": {
    "filename": "Red-blue-green-channel.jpg",
    "contents": "Base64EncodedStringContentsOfDownloadedFileAsZipGoesHere"
  }
}
  • The response blob contains two keys,
    • filename - The file name that was downloaded.
    • contents - Base64 encoded zipped contents of the file.
  • The file contents are Base64 encoded and they need to be unzipped.
List files that can be downloaded

The below mentioned curl example can be used to list all the files that were downloaded by the current session in the Node, and which can be retrieved locally.

curl -X GET "http://localhost:4444/session/90c0149a-2e75-424d-857a-e78734943d4c/se/files"

A sample response would look like below:

{
  "value": {
    "names": [
      "Red-blue-green-channel.jpg"
    ]
  }
}
Retrieve a downloaded file

Assuming the downloaded file is named Red-blue-green-channel.jpg, and using curl, the file could be downloaded with the following command:

curl -H "Accept: application/json" \
-H "Content-Type: application/json; charset=utf-8" \
-X POST -d '{"name":"Red-blue-green-channel.jpg"}' \
"http://localhost:4444/session/18033434-fa4f-4d11-a7df-9e6d75920e19/se/files"

A sample response would look like below:

{
  "value": {
    "filename": "Red-blue-green-channel.jpg",
    "contents": "UEsDBBQACAgIAJpagVYAAAAAAAAAAAAAAAAaAAAAUmVkLWJsAAAAAAAAAAAAUmVkLWJsdWUtZ3JlZW4tY2hhbm5lbC5qcGdQSwUGAAAAAAEAAQBIAAAAcNkAAAAA"
  }
}
Complete sample code in Java

Below is an example in Java that does the following:

  • Sets the capability to indicate that the test requires automatic managing of downloaded files.
  • Triggers a file download via a browser.
  • Lists the files that are available for retrieval from the remote node (These are essentially files that were downloaded in the current session)
  • Picks one file and downloads the file from the remote node to the local machine.
import com.google.common.collect.ImmutableMap;

import org.openqa.selenium.By;
import org.openqa.selenium.io.Zip;
import org.openqa.selenium.json.Json;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.remote.http.HttpClient;
import org.openqa.selenium.remote.http.HttpRequest;
import org.openqa.selenium.remote.http.HttpResponse;

import java.io.File;
import java.net.URL;
import java.nio.file.Files;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

import static org.openqa.selenium.remote.http.Contents.asJson;
import static org.openqa.selenium.remote.http.Contents.string;
import static org.openqa.selenium.remote.http.HttpMethod.GET;
import static org.openqa.selenium.remote.http.HttpMethod.POST;

public class DownloadsSample {

  public static void main(String[] args) throws Exception {
    // Assuming the Grid is running locally.
    URL gridUrl = new URL("http://localhost:4444");
    ChromeOptions options = new ChromeOptions();
    options.setCapability("se:downloadsEnabled", true);
    RemoteWebDriver driver = new RemoteWebDriver(gridUrl, options);
    try {
      demoFileDownloads(driver, gridUrl);
    } finally {
      driver.quit();
    }
  }

	private static void demoFileDownloads(RemoteWebDriver driver, URL gridUrl) throws Exception {
		driver.get("https://www.selenium.dev/selenium/web/downloads/download.html");
		// Download the two available files on the page
		driver.findElement(By.id("file-1")).click();
		driver.findElement(By.id("file-2")).click();

		// The download happens in a remote Node, which makes it difficult to know when the file
		// has been completely downloaded. For demonstration purposes, this example uses a
		// 10-second sleep which should be enough time for a file to be downloaded.
		// We strongly recommend to avoid hardcoded sleeps, and ideally, to modify your
		// application under test, so it offers a way to know when the file has been completely
		// downloaded.
		TimeUnit.SECONDS.sleep(10);

		//This is the endpoint which will provide us with list of files to download and also to
		//let us download a specific file.
		String downloadsEndpoint = String.format("/session/%s/se/files", driver.getSessionId());

		String fileToDownload;

		try (HttpClient client = HttpClient.Factory.createDefault().createClient(gridUrl)) {
			// To list all files that are were downloaded on the remote node for the current session
			// we trigger GET request.
			HttpRequest request = new HttpRequest(GET, downloadsEndpoint);
			HttpResponse response = client.execute(request);
			Map<String, Object> jsonResponse = new Json().toType(string(response), Json.MAP_TYPE);
			@SuppressWarnings("unchecked")
			Map<String, Object> value = (Map<String, Object>) jsonResponse.get("value");
			@SuppressWarnings("unchecked")
			List<String> names = (List<String>) value.get("names");
			// Let's say there were "n" files downloaded for the current session, we would like
			// to retrieve ONLY the first file.
			fileToDownload = names.get(0);
		}

		// Now, let's download the file
		try (HttpClient client = HttpClient.Factory.createDefault().createClient(gridUrl)) {
			// To retrieve a specific file from one or more files that were downloaded by the current session
			// on a remote node, we use a POST request.
			HttpRequest request = new HttpRequest(POST, downloadsEndpoint);
			request.setContent(asJson(ImmutableMap.of("name", fileToDownload)));
			HttpResponse response = client.execute(request);
			Map<String, Object> jsonResponse = new Json().toType(string(response), Json.MAP_TYPE);
			@SuppressWarnings("unchecked")
			Map<String, Object> value = (Map<String, Object>) jsonResponse.get("value");
			// The returned map would contain 2 keys,
			// filename - This represents the name of the file (same as what was provided by the test)
			// contents - Base64 encoded String which contains the zipped file.
			String zippedContents = value.get("contents").toString();
			// The file contents would always be a zip file and has to be unzipped.
			File downloadDir = Zip.unzipToTempDir(zippedContents, "download", "");
			// Read the file contents
			File downloadedFile = Optional.ofNullable(downloadDir.listFiles()).orElse(new File[]{})[0];
			String fileContent = String.join("", Files.readAllLines(downloadedFile.toPath()));
			System.out.println("The file which was "
					+ "downloaded in the node is now available in the directory: "
					+ downloadDir.getAbsolutePath() + " and has the contents: " + fileContent);
		}
	}


}

4.3 - Toml オプション

Tomlファイルを使用したGridの設定例.

CLI オプション に記載されている全てのオプションは TOML ファイルでも設定ができます。 このページでは異なる Grid コンポーネントの設定例を紹介します。

オプションが変更、または追加されたが文書化されていない場合、 このドキュメントは古くなる可能性があることに注意してください。 もしそのような状況を見つけたら、“構成ヘルプ”を確認し、 ドキュメントを更新するプルリクエストを気軽に送ってください。

概要

Selenium Grid はTOMLフォーマットの設定ファイルを使用します。 設定ファイルはセクションで構成され、各セクションはオプションとその値が設定されています。

詳しい使い方はTOML ドキュメントを参照してください。 パースエラーに遭遇した場合、TOML リンターを使って検証してください。

一般的な設定の構成は以下のパターンです:

[section1]
option1="value"

[section2]
option2=["value1","value2"]
option3=true

TOML ファイルで設定された Grid コンポーネントを起動するには以下のように起動できます:

java -jar selenium-server-<version>.jar <component> --config /path/to/file/<file-name>.toml

スタンドアロン

ポート 4449 で動作し、新規セッションリクエストのタイムアウトが 500 秒のスタンドアロンサーバー。

[server]
port = 4449

[sessionqueue]
session-request-timeout = 500

特定のブラウザとセッションの上限

Firefox と Chrome のみがデフォルトで有効になっているスタンドアロンサーバー、またはノード

[node]
drivers = ["chrome", "firefox"]
max-sessions = 3

ドライバーのカスタマイズと設定

Firefox Beta や Nightly のような、異なるブラウザのバージョンを持つことができるカスタマイズされた ドライバを用いた、スタンドアロン、またはノード。

[node]
detect-drivers = false
[[node.driver-configuration]]
max-sessions = 100
display-name = "Firefox Nightly"
stereotype = "{\"browserName\": \"firefox\", \"browserVersion\": \"93\", \"platformName\": \"MAC\", \"moz:firefoxOptions\": {\"binary\": \"/Applications/Firefox Nightly.app/Contents/MacOS/firefox-bin\"}}"
[[node.driver-configuration]]
display-name = "Chrome Beta"
stereotype = "{\"browserName\": \"chrome\", \"browserVersion\": \"94\", \"platformName\": \"MAC\", \"goog:chromeOptions\": {\"binary\": \"/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta\"}}"
[[node.driver-configuration]]
display-name = "Chrome Dev"
stereotype = "{\"browserName\": \"chrome\", \"browserVersion\": \"95\", \"platformName\": \"MAC\", \"goog:chromeOptions\": {\"binary\": \"/Applications/Google Chrome Dev.app/Contents/MacOS/Google Chrome Dev\"}}"
webdriver-executable = '/path/to/chromedriver/95/chromedriver'

Docker を利用したスタンドアロン、またはノード

Docker コンテナでセッションを実行できるスタンドアロン、またはノードサーバー。 ドライバを検出を無効にし、最大 2 つの同時セッションを持ちます。 ステレオタイプは、Docker イメージにマッピングされる必要があり、 Docker デーモンが http/tcp で公開されている必要があります。 また、devicesプロパティを用いて、ホスト上でアクセス可能なデバイスファイルを、コンテナで利用できるようにすることも可能です。 Docker デバイスをマッピングする詳しい方法はdocker を参照してください。

[node]
detect-drivers = false
max-sessions = 2

[docker]
configs = [
    "selenium/standalone-chrome:93.0", "{\"browserName\": \"chrome\", \"browserVersion\": \"91\"}",
    "selenium/standalone-firefox:92.0", "{\"browserName\": \"firefox\", \"browserVersion\": \"92\"}"
]
#Optionally define all device files that should be mapped to docker containers
#devices = [
#    "/dev/kvm:/dev/kvm"
#]
url = "http://localhost:2375"
video-image = "selenium/video:latest"

WebDriver をサポートするサービスエンドポイントへのコマンド中継

WebDriver をサポートする外部サービスを Selenium Grid に接続すると便利です。 例えばクラウドプロバイダーや Appium サーバーなどです。 Grid はローカルに存在しないプラットフォームやバージョンなどを幅広くカバーできるようになります。

以下は Appium サーバーを Grid に接続する例です。

[node]
detect-drivers = false

[relay]
# Default Appium/Cloud server endpoint
url = "http://localhost:4723/wd/hub"
status-endpoint = "/status"
# Stereotypes supported by the service. The initial number is "max-sessions", and will allocate
# that many test slots to that particular configuration
configs = [
  "5", "{\"browserName\": \"chrome\", \"platformName\": \"android\", \"appium:platformVersion\": \"11\"}"
]

Basic 認証の有効化

ルーター、ハブ、スタンドアロンにユーザー名とパスワードを設定することで、 Basic 認証で Grid を保護することができます。 このユーザーとパスワードは、Grid UI を読み込む時や 新しいセッションを開始する時に必要になります。

[router]
username = "admin"
password = "myStrongPassword"

Java でユーザーとパスワードを使ってセッションを開始する方法の例です。

URL gridUrl = new URL("http://admin:myStrongPassword@localhost:4444");
RemoteWebDriver webDriver = new RemoteWebDriver(gridUrl, new ChromeOptions());

特定のノードにマッチするカスタム capabilities の設定

重要: カスタム capabilities は全てのノードで設定する必要があります。 また全てのセッションリクエストで常に含まれる必要があります。

[node]
detect-drivers = false

[[node.driver-configuration]]
display-name = "firefox"
stereotype = '{"browserName": "firefox", "platformName": "macOS", "browserVersion":"96", "networkname:applicationName":"node_1", "nodename:applicationName":"app_1" }'
max-sessions = 5

Java でノードにマッチさせる方法の例です。

FirefoxOptions options = new FirefoxOptions();
options.setCapability("networkname:applicationName", "node_1");
options.setCapability("nodename:applicationName", "app_1");
options.setBrowserVersion("96");
options.setPlatformName("macOS");
WebDriver driver = new RemoteWebDriver(new URL("http://localhost:4444"), options);
driver.get("https://selenium.dev");
driver.quit();

Enabling Managed downloads by the Node.

The Node can be instructed to manage downloads automatically. This will cause the Node to save all files that were downloaded for a particular session into a temp directory, which can later be retrieved from the node. To turn this capability on, use the below configuration:

[node]
enable-managed-downloads = true

Refer to the CLI section for a complete example.

5 - Grid アーキテクチャ

Grid は、Grid を運用するためのコンポーネントの集合体として設計されています。 非常に複雑に見えるかもしれませんが、 このドキュメントが混乱を解消する手助けとなることを期待します。

キーとなるコンポーネント

主要な Grid のコンポーネント:

イベントバス
他のコンポーネント間で非同期で受信される可能性のあるメッセージを送信します。
新規セッションキュー
まだディストリビューターによってノードに割り当てられていない 受信セッションの一覧を管理します。
ディストリビューター
Grid内でWebDriverセッション を実行する場所("スロット"と呼ぶ)を管理し、 新しいセッションリクエストを受け取りスロットに割り当てる役割を担います。
ノード
WebDriverセッション を実行します。各セッションはスロットに割り当てられ、 各ノードは1つ以上のスロットを持っています。
セッションマップ
セッションID とセッションが実行されているノードのアドレスのマップを管理します。
ルーター
Gridのフロントエンドとして動作します。 Gridの中で唯一ウェブに公開してもよい部分です(ただし、公開しないことを強く推奨します)。 受け取ったリクエストを新規セッションキューかセッションが実行されているノードどちらかに振り分けます。

グリッドについて説明する際に、覚えておくと便利な概念がいくつかあります:

  • スロット はセッションが実行されるところです。
  • 各スロットは ステレオタイプ を持っています。これは ディストリビューターがスロットを所有するノードに 新規セッション リクエストを送信する前に、 リクエストがマッチしなければならない最小限の capabilities のセットです。
  • Grid モデル はディストリビューターが Grid の状態を追跡する方法です。 その名が示すように、これは時々実際と一致しないことがあります (ディストリビューターが開始したばかりだからかもしれません)。 ディストリビューターが新規セッションのリクエストを素早くスロットに割り当てることができるように、 各ノードに問い合わせるより優先して利用されます。

同期呼び出しと非同期呼び出し

Grid 内では主に 2 つの通信メカニズムが使われています。

  1. 同期で “REST 風” な、JSON を用いた HTTP リクエスト
  2. イベントバスによって送信される非同期なイベント

どちらの仕組みを使うべきかどのように決めればよいでしょうか? 結局のところ、Grid 全体をイベントベースにモデリングしたとしても、 うまくいくでしょう。

答えは、もし実行されるアクションが同期である場合(例えばほとんどの WebDriver の呼び出しなど)、 あるいはレスポンスを受け取れなかったときに問題になるような場合、 Grid は同期呼び出しを利用します。かわりにもし、関心のあるコンポーネントに情報を ブロードキャストしたい場合、あるいはレスポンスを受け取れなくても問題にならない場合は イベントバスを使用することが望ましいです。

特筆する点として、非同期呼び出しは同期呼び出しよりも、 よりリスナーが分離されています。

起動シーケンスとコンポーネント間の依存

Grid はコンポーネントを任意の順番で起動できるように設計されていますが、 概念的にはコンポーネントが起動する順番は:

  1. イベントバスとセッションマップが最初に起動します。これらは 他のコンポーネントへの依存もお互いの依存もないため、 並列に開始しても安全です。
  2. 次に 新規セッションキューが起動します。
  3. ここでディストリビューターが起動できるようになります。これは 定期的に 新規セッションキューに接続し、ジョブをポーリングします。 このポーリングはイベント(新規セッションがキューに追加された)あるいは 定期的な間隔によって行われる可能性があります。
  4. ルーターを起動できます。新しいセッションのリクエストは 新規セッションキューに送られ、ディストリビューターはセッションを実行する スロットを探そうとします。
  5. これでノードを起動することができます。ノードが Grid に登録されるまでの 流れは後述します。登録が完了すると、Grid はトラフィックを提供することが できるようになります。

コンポーネント間の依存関係を以下のよう表すことができます。 “✅” は同期的な依存関係があることを示しています。

イベントバス ディストリビューター ノード ルーター セッションマップ 新規セッションキュー
イベントバス X
ディストリビューター X
ノード X
ルーター X
セッションマップ X
新規セッションキュー X

ノードの登録

Grid に新しいノードを登録するプロセスは軽量です。

  1. ノードは開始する時 “ハートビート” イベントを発信する必要があります。 このハートビートはNode Statusを含んでいます.
  2. ディストリビューターはハートビートイベントを受け取ります。 受け取ったら、ノードの /status エンドポイントに GET リクエストを試みます。 この情報をもとに Grid が設定されます。

ディストリビューターは同じ /status エンドポイントを使用して、定期的にノードをチェックしますが、 ノードは起動した後もハートビートを送り続けなければなりません。 これにより、ディストリビューターは Grid の状態を永続化しませんが、 再起動しても Grid を(最終的に)最新の状態にすることができます。

Node Status オブジェクト

Node Status は以下のフィールドを持つ JSON オブジェクトです:

名前 概要
availability string up, draining, down のいずれかの文字列です。重要なのは draining で、これはノードに新しいセッションを送らないことを示し、最後のセッションが終了するとノードは終了、または再起動します。
externalUrl string Grid 内の他のコンポーネントが接続するための URI。
lastSessionCreated integer このノードで最後にセッションが作成された時間のエポックタイムスタンプです。ディストリビューターは、他の条件がすべて同じであれば、最も長くアイドルであったノードに新しいセッションを送信しようとします。
maxSessionCount integer セッション数は利用可能なスロット数をカウントすることで推測できますが、この値はノードが「満杯」とみなされるまでに、ノード上で同時に実行されるセッションの最大数を決定するために使用されます。
nodeId string ノード ID を識別する UUID です。
osInfo object arch, name, version フィールドを持つオブジェクトです。Grid UI と GraphQL クエリーで利用されます。
slots array Slot オブジェクト(以下を参照)の配列。
version string ノードのバージョン(Selenium では、これは Selenium のバージョンと同じです)。

すべてのフィールドの値を設定することが推奨されます。

Slot オブジェクト

Slot オブジェクトは 1 ノードでの 1 スロットを表します。 1 スロットにつき 1 セッションを実行することができます。 1 つのノードが同時に実行できる数よりも多くのスロットを持つことができます。 例えばあるノードが 10 セッションまで同時に実行でき、 それらのセッションは Chrome, Edge, Firefox のどの組み合わせでも良い時、 ノードは ‘max session count’ を 10 として、 10 の Chrome スロット、10 の Edge スロット、10 の Firefox スロットを持ちます。

名前 概要
id string スロット ID。
lastStarted string スロットが最後にセッションを開始した時間。ISO-8601 フォーマット。
stereotype object このスロットがマッチする最小限のcapabilities のセット。 最小の例: {"browserName": "firefox"}
session object Session オブジェクト(以下を参照)。

Session オブジェクト

スロットで実行中のセッションを表します。

名前 概要
capabilities object セッションが持つ実際の capabilities。 The actual capabilities provided by the session. 新規セッションコマンドの戻り値と同じです。
startTime string セッションが開始した時間。ISO-8601 フォーマット。
stereotype object このスロットがマッチする最小限のcapabilities のセット。 最小の例: {"browserName": "firefox"}
uri string ノードがセッションに接続するための URI。

6 - 高度な機能

高度な機能のすべての詳細を取得し、それがどのように機能するか、および独自の設定方法を理解するには、次のセクションを参照してください。

6.1 - 可観測性

目次

Selenium Grid

Grid は、さまざまなブラウザとオペレーティングシステムの組み合わせでテストを実行することにより、テストのスケーリングと分散を支援します。

可観測性

可観測性(Observability) には、トレース、メトリクス、ログの 3 つの柱があります。 Selenium Grid 4 は完全分散型に設計されているため、可観測性を確保することで内部を理解し、デバッグすることが容易になります。

分散トレーシング

1 つのリクエストやトランザクションは、複数のサービスやコンポーネントにまたがります。 トレースは、各サービスがリクエストを実行する際に、リクエストのライフサイクルをトラックします。これは、エラーシナリオのデバッグに有用です。 トレースで使用される用語は次のとおりです:

トレース トレースでは、複数のサービスを通じてリクエストの出発点から最終地点までを追跡することができます。 このリクエストの旅は、デバッグ、エンドツーエンドフローの監視、障害の特定に役立ちます。 トレースは、エンドツーエンドのリクエストフローを描きます。 各トレースは識別子としてユニークな ID を持っています。

スパン

各トレースは、スパンと呼ばれる時間で区切られたオペレーションで構成されています。 スパンには開始時刻と終了時刻があり、サービスによって実行される操作を表します。 スパンの粒度は実装方法に依存します。各スパンは一意の識別子を持ちます。 トレース内のすべてのスパンは、同じトレース ID を持ちます。

スパン属性 スパン属性は各スパンの付加的な情報を提供するキーと値のペアです。

イベント イベントは、スパン内のタイムスタンプ付きログです。 既存のスパンに追加のコンテキストを提供します。 イベントには、イベント属性としてキーと値のペアも含まれます。

イベントロギング

アプリケーションのデバッグには、ロギングが欠かせません。 ログの記録は多くの場合、人間が読める形式で行われます。 しかし、機械がログを検索・分析するためには、明確に定義されたフォーマットである必要があります。 構造化ロギングは、固定フォーマットで一貫してログを記録する一般的な方法です。 一般的には次のようなフィールドが含まれます。

  • タイムスタンプ
  • ログレベル
  • ロガークラス
  • ログメッセージ (これはさらに、ログが記録された操作に関するフィールドに分解されます)

ログとイベントは密接に関連しています。 イベントは、1 つの処理を行うための情報を全てカプセル化します。 ログは基本的にイベントのサブセットです。 重要なのは、どちらもデバッグを支援することです。 詳細については、以下のリソースを参照してください。

  1. https://www.honeycomb.io/blog/how-are-structured-logs-different-from-events/
  2. https://charity.wtf/2019/02/05/logs-vs-structured-events/

Grid の可観測性

Selenium サーバーは OpenTelemetry を使ってトレースできるようになっています。 サーバへのすべてのリクエストは、最初から最後までトレースされます。 各トレースは、リクエストがサーバ内で実行されるときの一連のスパンから構成されます。 Selenium サーバのスパンのほとんどは、2 つのイベントから構成されています。

  1. 通常イベント- 単一の処理に関するすべての情報を記録し、処理が正常に完了したことを知らせます。
  2. エラーイベント- エラーが発生するまでのすべての情報を記録し、エラー情報を記録します。例外イベントをマークします。

Selenium サーバーを起動する:

  1. スタンドアロン
  2. ハブ・ノード
  3. 完全分散モード
  4. Docker

トレースの可視化

すべてのスパン、イベント、およびそれぞれの属性がトレースの一部となります。 トレースは、上記のすべてのモードでサーバを実行している間動作します。 トレースはデフォルトで Selenium サーバで有効になっています。

Selenium サーバーは、2 つのエクスポーター経由でトレースをエクスポートします。

  1. コンソール - すべてのトレースと、それに含まれるスパンを FINE レベルでログに出力します。デフォルトでは、Selenium サーバーは INFO レベル以上のログを出力します。 log-level フラグを使うと、Selenium Grid jar を実行する際に任意のログレベルを指定することができます。
java -jar selenium-server-4.0.0-<selenium-version>.jar standalone --log-level FINE
  1. Jaeger UI - OpenTelemetry は、コード内のトレースを計測するための API と SDK を提供します。一方、Jaeger はトレースのバックエンドで、トレースのテレメトリデータを収集し、データのクエリ、フィルタリング、ビジュアライズの機能を提供します。

Jaeger UI を用いたトレースの可視化の詳細な手順を確認するには、次のコマンドを実行してください:

java -jar selenium-server-4.0.0-<selenium-version>.jar info tracing

非常に参考になる例と、Jaeger にトレースを送信するスクリプトです

イベントログの活用

トレースを可視化しない場合でも、イベントロギングではトレースを有効にする必要があります。 デフォルトでは、トレースは有効です。コンソールでログを見るために、追加のパラメータを渡す必要はありません。 スパン内のすべてのイベントは FINE レベルでログに記録されます。エラーイベントは、WARN レベルでログに記録されます。

全てのイベントは次のフィールドを持ちます:

フィールド フィールド名 概要
イベント時刻 eventId イベントのタイムスタンプ(エポックナノ秒)。
トレース ID tracedId 各トレースはトレース ID で一意に識別されます。
スパン ID spanId トレース内の各スパンは、スパン ID により一意に識別されます。
スパン種別 spanKind スパン種別は、スパンの種類を示すスパンのプロパティです。スパンの処理の性質を識別するのに役立ちます。
イベント名 eventName ログメッセージにマッピングされます。
イベント属性 eventAttributes イベントログの核となるもので、実行された操作に基づいて JSON フォーマットのキーと値のペアが用意されています。また、ロガークラスを表示するために、ハンドラークラスアトリビュートも含まれます。

サンプルログ

FINE [LoggingOptions$1.lambda$export$1] - {
  "traceId": "fc8aef1d44b3cc8bc09eb8e581c4a8eb",
  "spanId": "b7d3b9865d3ddd45",
  "spanKind": "INTERNAL",
  "eventTime": 1597819675128886121,
  "eventName": "Session request execution complete",
  "attributes": {
    "http.status_code": 200,
    "http.handler_class": "org.openqa.selenium.grid.router.HandleSession",
    "http.url": "\u002fsession\u002fdd35257f104bb43fdfb06242953f4c85",
    "http.method": "DELETE",
    "session.id": "dd35257f104bb43fdfb06242953f4c85"
  }
}

上記のフィールドに加えて、OpenTelemetry の仕様に基づきエラーログは以下のフィールドで構成されます:

フィールド フィールド名 概要
例外タイプ exception.type 例外クラス名。
例外メッセージ exception.message 例外の原因。
スタックトレース exception.stacktrace 例外が発生した時点のコールスタックを表示します。 例外の発生源を把握するのに役立ちます。

サンプルエラーログ

WARN [LoggingOptions$1.lambda$export$1] - {
  "traceId": "7efa5ea57e02f89cdf8de586fe09f564",
  "spanId": "914df6bc9a1f6e2b",
  "spanKind": "INTERNAL",
  "eventTime": 1597820253450580272,
  "eventName": "exception",
  "attributes": {
    "exception.type": "org.openqa.selenium.ScriptTimeoutException",
    "exception.message": "Unable to execute request: java.sql.SQLSyntaxErrorException: Table 'mysql.sessions_mappa' doesn't exist ..." (full message will be printed),
    "exception.stacktrace": "org.openqa.selenium.ScriptTimeoutException: java.sql.SQLSyntaxErrorException: Table 'mysql.sessions_mappa' doesn't exist\nBuild info: version: '4.0.0-alpha-7', revision: 'Unknown'\nSystem info: host: 'XYZ-MacBook-Pro.local', ip: 'fe80:0:0:0:10d5:b63a:bdc6:1aff%en0', os.name: 'Mac OS X', os.arch: 'x86_64', os.version: '10.13.6', java.version: '11.0.7'\nDriver info: driver.version: unknown ...." (full stack will be printed),
    "http.handler_class": "org.openqa.selenium.grid.distributor.remote.RemoteDistributor",
    "http.url": "\u002fsession",
    "http.method": "POST"
  }
}

注: ログは読みやすさのためプリティプリントされています。Selenimu サーバーではぷるティプリントはオフになっています。

以上がトレースとログをセットアップするための手順です。

参考

  1. Understanding Tracing
  2. OpenTelemetry Tracing API Specification
  3. Selenium Wiki
  4. Structured logs vs events
  5. Jaeger framework

6.2 - GraphQLクエリのサポート

GraphQLは、APIのクエリ言語であり、既存のデータでこれらのクエリを実行するためのランタイムです。 これにより、ユーザーは必要なものだけを正確に要求することができます。

列挙型(Enum)

列挙型は、フィールドの可能な値のセットを表します。

たとえば、 Node オブジェクトには status というフィールドがあります。 UPDRAINING 、または UNAVAILABLE の可能性があるため、状態は、 列挙型(具体的には、Status タイプ)です。

スカラー

スカラーはプリミティブ値です: IntFloatStringBoolean 、または ID

GraphQL APIを呼び出すときは、スカラーのみを返すまでネストされたサブフィールドを指定する必要があります。

スキーマの構造

グリッドスキーマの構造は次のとおりです。

{
    session(id: "<session-id>") : {
        id,
        capabilities,
        startTime,
        uri,
        nodeId,
        nodeUri,
        sessionDurationMillis
        slot : {
            id,
            stereotype,
            lastStarted
        }
    }
    grid: {
        uri,
        totalSlots,
        nodeCount,
        maxSession,
        sessionCount,
        version,
        sessionQueueSize
    }
    sessionsInfo: {
        sessionQueueRequests,
        sessions: [
            {
                id,
                capabilities,
                startTime,
                uri,
                nodeId,
                nodeUri,
                sessionDurationMillis
                slot : {
                    id,
                    stereotype,
                    lastStarted
                }
            }
        ]
    }
    nodesInfo: {
        nodes : [
            {
                id,
                uri,
                status,
                maxSession,
                slotCount,
                sessions: [
                    {
                        id,
                        capabilities,
                        startTime,
                        uri,
                        nodeId,
                        nodeUri,
                        sessionDurationMillis
                        slot : {
                            id,
                            stereotype,
                            lastStarted
                        }
                    }
                ],
                sessionCount,
                stereotypes,
                version,
                osInfo: {
                    arch,
                    name,
                    version
                }
            }
        ]
    }
}

GraphQLで照会する

GraphQLをクエリする最良の方法は、 curl リクエストを使用することです。 GraphQLを使用すると、必要なデータのみをフェッチできます。それ以上でもそれ以下でもありません。

GraphQLクエリの例のいくつかを以下に示します。 必要に応じて独自のクエリを作成できます。

グリッド内の maxSession sessionCountの数を照会する

curl -X POST -H "Content-Type: application/json" --data '{"query": "{ grid { maxSession, sessionCount } }"}' -s <LINK_TO_GRAPHQL_ENDPOINT>

通常、ローカルマシンでは、 <LINK_TO_GRAPHQL_ENDPOINT>http://localhost:4444/graphql になります。

セッション、ノード、グリッドのすべての詳細を照会する

curl -X POST -H "Content-Type: application/json" --data '{"query":"{ grid { uri, maxSession, sessionCount }, nodesInfo { nodes { id, uri, status, sessions { id, capabilities, startTime, uri, nodeId, nodeUri, sessionDurationMillis, slot { id, stereotype, lastStarted } }, slotCount, sessionCount }} }"}' -s <LINK_TO_GRAPHQL_ENDPOINT>

グリッドで現在のセッション数を取得するためのクエリ

curl -X POST -H "Content-Type: application/json" --data '{"query":"{ grid { sessionCount } }"}' -s <LINK_TO_GRAPHQL_ENDPOINT>

グリッドで最大セッション数を取得するためのクエリ

curl -X POST -H "Content-Type: application/json" --data '{"query":"{ grid { maxSession } }"}' -s <LINK_TO_GRAPHQL_ENDPOINT>

グリッド内のすべてのノードのすべてのセッションの詳細を取得するためのクエリ

curl -X POST -H "Content-Type: application/json" --data '{"query":"{ sessionsInfo { sessions { id, capabilities, startTime, uri, nodeId, nodeId, sessionDurationMillis } } }"}' -s <LINK_TO_GRAPHQL_ENDPOINT>

グリッド内の各ノードのすべてのセッションのスロット情報を取得するためのクエリ

curl -X POST -H "Content-Type: application/json" --data '{"query":"{ sessionsInfo { sessions { id, slot { id, stereotype, lastStarted } } } }"}' -s <LINK_TO_GRAPHQL_ENDPOINT>

特定のセッションのセッション情報を取得するためのクエリ

curl -X POST -H "Content-Type: application/json" --data '{"query":"{ session (id: \"<session-id>\") { id, capabilities, startTime, uri, nodeId, nodeUri, sessionDurationMillis, slot { id, stereotype, lastStarted } } } "}' -s <LINK_TO_GRAPHQL_ENDPOINT>

グリッド内の各ノードのcapabilityを照会する

curl -X POST -H "Content-Type: application/json" --data '{"query": "{ nodesInfo { nodes { stereotypes } } }"}' -s <LINK_TO_GRAPHQL_ENDPOINT>

グリッド内の各ノードのステータスを照会する

curl -X POST -H "Content-Type: application/json" --data '{"query": "{ nodesInfo { nodes { status } } }"}' -s <LINK_TO_GRAPHQL_ENDPOINT>

各ノードとグリッドのURIを照会する

curl -X POST -H "Content-Type: application/json" --data '{"query": "{ nodesInfo { nodes { uri } } }"}' -s <LINK_TO_GRAPHQL_ENDPOINT>

新しいセッションキューで現在のリクエストを取得するためのクエリ

curl -X POST -H "Content-Type: application/json" --data '{"query":"{ sessionsInfo { sessionQueueRequests } }"}' -s <LINK_TO_GRAPHQL_ENDPOINT>

新しいセッションキューのサイズを取得するためのクエリ

curl -X POST -H "Content-Type: application/json" --data '{"query":"{ grid { sessionQueueSize } }"}' -s <LINK_TO_GRAPHQL_ENDPOINT>

6.3 - Grid エンドポイント

Grid

Grid ステータス

Grid ステータスは Grid の現在の状態を提供します。 登録されている全てのノードの詳細で構成されます。 各ノードのステータスには、ノードの稼働状況、セッション、およびスロットに関する情報が含まれます。

cURL GET 'http://localhost:4444/status'

スタンドアロンモードでは、Grid URL は スタンドアロンサーバーのアドレスになります。

ハブ&ノードモードでは、Grid URL は ハブのアドレスになります。

完全分散モードでは、Grid URL は ルーターのアドレスになります。

上記すべてのモードのデフォルトの URL は http://localhost:4444 です。

ディストリビューター

ノード削除

ノードを Grid から削除するには、以下の cURL コマンドを使用します。 このコマンドは、そのノード上で実行中のセッションを停止させるものではありません。 ノードは明示的に強制終了されない限り、そのまま動作し続けます。 ディストリビューターはそのノードを認識しなくなるため、マッチする新しいセッションのリクエストは はその Node に転送されません。

スタンドアロンモードでは、ディストリビューターの URL はスタンドアロンサーバーのアドレスとなります。

ハブ&ノードモードでは、ディストリビューターの URL は ハブのアドレスになります。

cURL --request DELETE 'http://localhost:4444/se/grid/distributor/node/<node-id>' --header 'X-REGISTRATION-SECRET: <secret> '

完全分散モードでは、ディストリビューター URL は ディストリビューターのアドレスになります。

cURL --request DELETE 'http://localhost:5553/se/grid/distributor/node/<node-id>' --header 'X-REGISTRATION-SECRET: <secret>'

Grid の設定時に登録用の secret を設定していない場合は次のようにします:

cURL --request DELETE 'http://<Distributor-URL>/se/grid/distributor/node/<node-id>' --header 'X-REGISTRATION-SECRET;'

ノードのドレイン

ノードドレインコマンドはノードをグレースフルシャットダウンするために利用します。 ドレインは実行中のセッションがすべて完了した後にノードを停止します。 新規のセッションは受け付けません。

スタンドアロンモードでは、ディストリビューターの URL はスタンドアロンサーバーのアドレスとなります。

ハブ&ノードモードでは、ディストリビューターの URL は ハブのアドレスになります。

cURL --request POST 'http://localhost:4444/se/grid/distributor/node/<node-id>/drain' --header 'X-REGISTRATION-SECRET: <secret> '

完全分散モードでは、ディストリビューター URL は ディストリビューターのアドレスになります。

cURL --request POST 'http://localhost:5553/se/grid/distributor/node/<node-id>/drain' --header 'X-REGISTRATION-SECRET: <secret>'

Grid の設定時に登録用の secret を設定していない場合は次のようにします:

cURL --request POST 'http://<Distributor-URL>/se/grid/distributor/node/<node-id>/drain' --header 'X-REGISTRATION-SECRET;'

ノード

この節でのエンドポイントは、ハブ&ノードモードとノードが独立して動作する完全分散型 Grid モードに適用されます。 ノードが 1 つの場合、デフォルトのノード URL は http://localhost:5555 です。 複数のノードがある場合は、Grid ステータス を使ってすべてのノードの詳細とノードアドレスを取得してください。

ステータス

ノードステータスは基本的にノードのヘルスチェックのためのものです。 ディストリビューターは定期的にノードの状態を ping で取得し、それに応じて Grid モデルを更新します。 ステータスには稼働状況、セッション、およびスロットに関する情報が含まれます。

cURL --request GET 'http://localhost:5555/status'

ドレイン

ディストリビューターは ドレインコマンドを適切なノードに渡します。 ノードを直接ドレインするには以下の cURL コマンドを使います。 どちらのエンドポイントも有効であり、同じ結果になります。 ドレインは、ノードを停止する前に進行中のセッションを終了させます。

cURL --request POST 'http://localhost:5555/se/grid/node/drain' --header 'X-REGISTRATION-SECRET: <secret>'

Grid の設定時に登録用の secret を設定していない場合は次のようにします:

cURL --request POST 'http://<node-URL>/se/grid/node/drain' --header 'X-REGISTRATION-SECRET;'

セッションオーナーのチェック

あるセッションがノードに属しているかどうかをチェックするには、以下の cURL コマンドを使います。

cURL --request GET 'http://localhost:5555/se/grid/node/owner/<session-id>' --header 'X-REGISTRATION-SECRET: <secret>'

Grid の設定時に登録用の secret を設定していない場合は次のようにします:

cURL --request GET 'http://<node-URL>/se/grid/node/owner/<session-id>' --header 'X-REGISTRATION-SECRET;'

もしセッションがノードに属していたら true を返し、そうでなければ false が返ります。

セッションの削除

セッションを削除すると、WebDriver セッションが終了し、ドライバがアクティブなセッションマップから削除されます。 削除されたセッション ID を使用するリクエストや、ドライバのインスタンスを再利用しようとすると、エラーとなります。

cURL --request DELETE 'http://localhost:5555/se/grid/node/session/<session-id>' --header 'X-REGISTRATION-SECRET: <secret>'

Grid の設定時に登録用の secret を設定していない場合は次のようにします:

cURL --request DELETE 'http://<node-URL>/se/grid/node/session/<session-id>' --header 'X-REGISTRATION-SECRET;'

新規セッションキュー

新規セッションキューのクリア

新規セッションキューには、新規セッションリクエストが格納されます。 キューをクリアするには、以下に挙げる cURL コマンドを使用します。 キューを消去すると、キューにあるすべてのリクエストを拒否します。 サーバーは各リクエストのそれぞれのクライアントにエラーレスポンスを返します。 クリアコマンドの結果は、削除されたリクエストの数です。

スタンドアロンモードでは、キューの URL はスタンドアロンサーバーのアドレスとなります。

ハブ&ノードモードでは、キューの URL は ハブのアドレスになります。

cURL --request DELETE 'http://localhost:4444/se/grid/newsessionqueue/queue' --header 'X-REGISTRATION-SECRET: <secret>'

完全分散モードでは、キューの URL は 新規セッションキューのアドレスになります。

cURL --request DELETE 'http://localhost:5559/se/grid/newsessionqueue/queue' --header 'X-REGISTRATION-SECRET: <secret>'

Grid の設定時に登録用の secret を設定していない場合は次のようにします:

cURL --request DELETE 'http://<URL>/se/grid/newsessionqueue/queue' --header 'X-REGISTRATION-SECRET;'

新規セッションリクエストの取得

新規セッションキューには、新規セッションリクエストが格納されます。 キューにある現在のリクエストを取得するには、以下に挙げる cURL コマンドを使用します。 レスポンスはキュー内のリクエストの数とリクエストのペイロードを返します。

スタンドアロンモードでは、キューの URL はスタンドアロンサーバーのアドレスとなります。

ハブ&ノードモードでは、キューの URL は ハブのアドレスになります。

cURL --request GET 'http://localhost:4444/se/grid/newsessionqueue/queue'

完全分散モードでは、キューの URL は 新規セッションキューのアドレスになります。

cURL --request GET 'http://localhost:5559/se/grid/newsessionqueue/queue'

6.4 - Customizing a Node

Page being translated from English to Japanese. Do you speak Japanese? Help us to translate it by sending us pull requests!

How to customize a Node

There are times when we would like a Node to be customized to our needs.

For e.g., we may like to do some additional setup before a session begins execution and some clean-up after a session runs to completion.

Following steps can be followed for this:

  • Create a class that extends org.openqa.selenium.grid.node.Node

  • Add a static method (this will be our factory method) to the newly created class whose signature looks like this:

    public static Node create(Config config). Here:

    • Node is of type org.openqa.selenium.grid.node.Node
    • Config is of type org.openqa.selenium.grid.config.Config
  • Within this factory method, include logic for creating your new Class.

  • To wire in this new customized logic into the hub, start the node and pass in the fully qualified class name of the above class to the argument --node-implementation

Let’s see an example of all this:

Custom Node as an uber jar

  1. Create a sample project using your favourite build tool (Maven|Gradle).
  2. Add the below dependency to your sample project.
  3. Add your customized Node to the project.
  4. Build an uber jar to be able to start the Node using java -jar command.
  5. Now start the Node using the command:
java -jar custom_node-server.jar node \
--node-implementation org.seleniumhq.samples.DecoratedLoggingNode

Note: If you are using Maven as a build tool, please prefer using maven-shade-plugin instead of maven-assembly-plugin because maven-assembly plugin seems to have issues with being able to merge multiple Service Provider Interface files (META-INF/services)

Custom Node as a regular jar

  1. Create a sample project using your favourite build tool (Maven|Gradle).
  2. Add the below dependency to your sample project.
  3. Add your customized Node to the project.
  4. Build a jar of your project using your build tool.
  5. Now start the Node using the command:
java -jar selenium-server-4.6.0.jar \
--ext custom_node-1.0-SNAPSHOT.jar node \
--node-implementation org.seleniumhq.samples.DecoratedLoggingNode

Below is a sample that just prints some messages on to the console whenever there’s an activity of interest (session created, session deleted, a webdriver command executed etc.,) on the Node.

Sample customized node
package org.seleniumhq.samples;

import java.net.URI;
import java.util.UUID;
import org.openqa.selenium.Capabilities;
import org.openqa.selenium.NoSuchSessionException;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.grid.config.Config;
import org.openqa.selenium.grid.data.CreateSessionRequest;
import org.openqa.selenium.grid.data.CreateSessionResponse;
import org.openqa.selenium.grid.data.NodeId;
import org.openqa.selenium.grid.data.NodeStatus;
import org.openqa.selenium.grid.data.Session;
import org.openqa.selenium.grid.log.LoggingOptions;
import org.openqa.selenium.grid.node.HealthCheck;
import org.openqa.selenium.grid.node.Node;
import org.openqa.selenium.grid.node.local.LocalNodeFactory;
import org.openqa.selenium.grid.security.Secret;
import org.openqa.selenium.grid.security.SecretOptions;
import org.openqa.selenium.grid.server.BaseServerOptions;
import org.openqa.selenium.internal.Either;
import org.openqa.selenium.remote.SessionId;
import org.openqa.selenium.remote.http.HttpRequest;
import org.openqa.selenium.remote.http.HttpResponse;
import org.openqa.selenium.remote.tracing.Tracer;

public class DecoratedLoggingNode extends Node {

  private Node node;

  protected DecoratedLoggingNode(Tracer tracer, URI uri, Secret registrationSecret) {
    super(tracer, new NodeId(UUID.randomUUID()), uri, registrationSecret);
  }

  public static Node create(Config config) {
    LoggingOptions loggingOptions = new LoggingOptions(config);
    BaseServerOptions serverOptions = new BaseServerOptions(config);
    URI uri = serverOptions.getExternalUri();
    SecretOptions secretOptions = new SecretOptions(config);

    // Refer to the foot notes for additional context on this line.
    Node node = LocalNodeFactory.create(config);

    DecoratedLoggingNode wrapper = new DecoratedLoggingNode(loggingOptions.getTracer(),
        uri, secretOptions.getRegistrationSecret());
    wrapper.node = node;
    return wrapper;
  }

  @Override
  public Either<WebDriverException, CreateSessionResponse> newSession(
      CreateSessionRequest sessionRequest) {
    System.out.println("Before newSession()");
    try {
      return this.node.newSession(sessionRequest);
    } finally {
      System.out.println("After newSession()");
    }
  }

  @Override
  public HttpResponse executeWebDriverCommand(HttpRequest req) {
    try {
      System.out.println("Before executeWebDriverCommand(): " + req.getUri());
      return node.executeWebDriverCommand(req);
    } finally {
      System.out.println("After executeWebDriverCommand()");
    }
  }

  @Override
  public Session getSession(SessionId id) throws NoSuchSessionException {
    try {
      System.out.println("Before getSession()");
      return node.getSession(id);
    } finally {
      System.out.println("After getSession()");
    }
  }

  @Override
  public HttpResponse uploadFile(HttpRequest req, SessionId id) {
    try {
      System.out.println("Before uploadFile()");
      return node.uploadFile(req, id);
    } finally {
      System.out.println("After uploadFile()");
    }
  }

  @Override
  public void stop(SessionId id) throws NoSuchSessionException {
    try {
      System.out.println("Before stop()");
      node.stop(id);
    } finally {
      System.out.println("After stop()");
    }
  }

  @Override
  public boolean isSessionOwner(SessionId id) {
    try {
      System.out.println("Before isSessionOwner()");
      return node.isSessionOwner(id);
    } finally {
      System.out.println("After isSessionOwner()");
    }
  }

  @Override
  public boolean isSupporting(Capabilities capabilities) {
    try {
      System.out.println("Before isSupporting");
      return node.isSupporting(capabilities);
    } finally {
      System.out.println("After isSupporting()");
    }
  }

  @Override
  public NodeStatus getStatus() {
    try {
      System.out.println("Before getStatus()");
      return node.getStatus();
    } finally {
      System.out.println("After getStatus()");
    }
  }

  @Override
  public HealthCheck getHealthCheck() {
    try {
      System.out.println("Before getHealthCheck()");
      return node.getHealthCheck();
    } finally {
      System.out.println("After getHealthCheck()");
    }
  }

  @Override
  public void drain() {
    try {
      System.out.println("Before drain()");
      node.drain();
    } finally {
      System.out.println("After drain()");
    }

  }

  @Override
  public boolean isReady() {
    try {
      System.out.println("Before isReady()");
      return node.isReady();
    } finally {
      System.out.println("After isReady()");
    }
  }
}

Foot Notes:

In the above example, the line Node node = LocalNodeFactory.create(config); explicitly creates a LocalNode.

There are basically 2 types of user facing implementations of org.openqa.selenium.grid.node.Node available.

These classes are good starting points to learn how to build a custom Node and also to learn the internals of a Node.

  • org.openqa.selenium.grid.node.local.LocalNode - Used to represent a long running Node and is the default implementation that gets wired in when you start a node.
    • It can be created by calling LocalNodeFactory.create(config);, where:
      • LocalNodeFactory belongs to org.openqa.selenium.grid.node.local
      • Config belongs to org.openqa.selenium.grid.config
  • org.openqa.selenium.grid.node.k8s.OneShotNode - This is a special reference implementation wherein the Node gracefully shuts itself down after servicing one test session. This class is currently not available as part of any pre-built maven artifact.
    • You can refer to the source code here to understand its internals.
    • To build it locally refer here.
    • It can be created by calling OneShotNode.create(config), where:
      • OneShotNode belongs to org.openqa.selenium.grid.node.k8s
      • Config belongs to org.openqa.selenium.grid.config

6.5 - External datastore

Page being translated from English to Japanese. Do you speak Japanese? Help us to translate it by sending us pull requests!

Table of Contents

Introduction

Selenium Grid allows you to persist information related to currently running sessions into an external data store. The external data store could be backed by your favourite database (or) Redis Cache system.

Setup

  • Coursier - As a dependency resolver, so that we can download maven artifacts on the fly and make them available in our classpath
  • Docker - To manage our PostGreSQL/Redis docker containers.

Database backed Session Map

For the sake of this illustration, we are going to work with PostGreSQL database.

We will spin off a PostGreSQL database as a docker container using a docker compose file.

Steps

You can skip this step if you already have a PostGreSQL database instance available at your disposal.

  • Create a sql file named init.sql with the below contents:
CREATE TABLE IF NOT EXISTS sessions_map(
    session_ids varchar(256),
    session_caps text,
    session_uri varchar(256),
    session_stereotype text,
    session_start varchar(256)
 );
  • In the same directory as the init.sql, create a file named docker-compose.yml with its contents as below:
version: '3.8'
services:
  db:
    image: postgres:9.6-bullseye
    restart: always
    environment:
      - POSTGRES_USER=seluser
      - POSTGRES_PASSWORD=seluser
      - POSTGRES_DB=selenium_sessions
    ports:
      - "5432:5432"
    volumes:
    - ./init.sql:/docker-entrypoint-initdb.d/init.sql

We can now start our database container by running:

docker-compose up -d

Our database name is selenium_sessions with its username and password set to seluser

If you are working with an already running PostGreSQL DB instance, then you just need to create a database named selenium_sessions and the table sessions_map using the above mentioned SQL statement.

  • Create a Selenium Grid configuration file named sessions.toml with the below contents:
[sessions]
implementation = "org.openqa.selenium.grid.sessionmap.jdbc.JdbcBackedSessionMap"
jdbc-url = "jdbc:postgresql://localhost:5432/selenium_sessions"
jdbc-user = "seluser"
jdbc-password = "seluser"

Note: If you plan to use an existing PostGreSQL DB instance, then replace localhost:5432 with the actual host and port number of your instance.

  • Below is a simple shell script (let’s call it distributed.sh) that we will use to bring up our distributed Grid.
SE_VERSION=<current_selenium_version>
JAR_NAME=selenium-server-${SE_VERSION}.jar
PUBLISH="--publish-events tcp://localhost:4442"
SUBSCRIBE="--subscribe-events tcp://localhost:4443"
SESSIONS="--sessions http://localhost:5556"
SESSIONS_QUEUE="--sessionqueue http://localhost:5559"
echo 'Starting Event Bus'
java -jar $JAR_NAME event-bus $PUBLISH $SUBSCRIBE --port 5557 &
echo 'Starting New Session Queue'
java -jar $JAR_NAME sessionqueue --port 5559 &
echo 'Starting Sessions Map'
java -jar $JAR_NAME \
--ext $(coursier fetch -p org.seleniumhq.selenium:selenium-session-map-jdbc:${SE_VERSION} org.postgresql:postgresql:42.3.1) \
sessions $PUBLISH $SUBSCRIBE --port 5556 --config sessions.toml &
echo 'Starting Distributor'
java -jar $JAR_NAME  distributor $PUBLISH $SUBSCRIBE $SESSIONS $SESSIONS_QUEUE --port 5553 --bind-bus false &
echo 'Starting Router'
java -jar $JAR_NAME router $SESSIONS --distributor http://localhost:5553 $SESSIONS_QUEUE --port 4444 &
echo 'Starting Node'
java -jar $JAR_NAME node $PUBLISH $SUBSCRIBE &
  • At this point the current directory should contain the following files:

    • docker-compose.yml
    • init.sql
    • sessions.toml
    • distributed.sh
  • You can now spawn the Grid by running distributed.sh shell script and quickly run a test. You will notice that the Grid now stores session information into the PostGreSQL database.

In the line which spawns a SessionMap on a machine:

export SE_VERSION=<current_selenium_version>
java -jar selenium-server-${SE_VERSION}.jar \
--ext $(coursier fetch -p org.seleniumhq.selenium:selenium-session-map-jdbc:${SE_VERSION} org.postgresql:postgresql:42.3.1) \
sessions --publish-events tcp://localhost:4442 \
--subscribe-events tcp://localhost:4443 \
--port 5556 --config sessions.toml 
  • The variable names from the above script have been replaced with their actual values for clarity.
  • Remember to substitute localhost with the actual hostname of the machine where your Event-Bus is running.
  • The arguments being passed to coursier are basically the GAV (Group Artifact Version) Maven co-ordinates of:
  • sessions.toml is the configuration file that we created earlier.

Redis backed Session Map

We will spin off a Redis Cache docker container using a docker compose file.

Steps

You can skip this step if you already have a Redis Cache instance available at your disposal.

  • Create a file named docker-compose.yml with its contents as below:
version: '3.8'
services:
  redis:
    image: redis:bullseye
    restart: always
    ports:
      - "6379:6379"

We can now start our Redis container by running:

docker-compose up -d
  • Create a Selenium Grid configuration file named sessions.toml with the below contents:
[sessions]
scheme = "redis"
implementation = "org.openqa.selenium.grid.sessionmap.redis.RedisBackedSessionMap"
hostname = "localhost"
port = 6379

Note: If you plan to use an existing Redis Cache instance, then replace localhost and 6379 with the actual host and port number of your instance.

  • Below is a simple shell script (let’s call it distributed.sh) that we will use to bring up our distributed grid.
SE_VERSION=<current_selenium_version>
JAR_NAME=selenium-server-${SE_VERSION}.jar
PUBLISH="--publish-events tcp://localhost:4442"
SUBSCRIBE="--subscribe-events tcp://localhost:4443"
SESSIONS="--sessions http://localhost:5556"
SESSIONS_QUEUE="--sessionqueue http://localhost:5559"
echo 'Starting Event Bus'
java -jar $JAR_NAME event-bus $PUBLISH $SUBSCRIBE --port 5557 &
echo 'Starting New Session Queue'
java -jar $JAR_NAME sessionqueue --port 5559 &
echo 'Starting Session Map'
java -jar $JAR_NAME \
--ext $(coursier fetch -p org.seleniumhq.selenium:selenium-session-map-redis:${SE_VERSION}) \
sessions $PUBLISH $SUBSCRIBE --port 5556 --config sessions.toml &
echo 'Starting Distributor'
java -jar $JAR_NAME  distributor $PUBLISH $SUBSCRIBE $SESSIONS $SESSIONS_QUEUE --port 5553 --bind-bus false &
echo 'Starting Router'
java -jar $JAR_NAME router $SESSIONS --distributor http://localhost:5553 $SESSIONS_QUEUE --port 4444 &
echo 'Starting Node'
java -jar $JAR_NAME node $PUBLISH $SUBSCRIBE &
  • At this point the current directory should contain the following files:

    • docker-compose.yml
    • sessions.toml
    • distributed.sh
  • You can now spawn the Grid by running distributed.sh shell script and quickly run a test. You will notice that the Grid now stores session information into the Redis instance. You can perhaps make use of a Redis GUI such as TablePlus to see them (Make sure that you have setup a debug point in your test, because the values will get deleted as soon as the test runs to completion).

In the line which spawns a SessionMap on a machine:

export SE_VERSION=<current_selenium_version>
java -jar selenium-server-${SE_VERSION}.jar \
--ext $(coursier fetch -p org.seleniumhq.selenium:selenium-session-map-redis:${SE_VERSION}) \
sessions --publish-events tcp://localhost:4442 \
--subscribe-events tcp://localhost:4443 \
--port 5556 --config sessions.toml 
  • The variable names from the above script have been replaced with their actual values for clarity.
  • Remember to substitute localhost with the actual hostname of the machine where your Event-Bus is running.
  • The arguments being passed to coursier are basically the GAV (Group Artifact Version) Maven co-ordinates of:
  • sessions.toml is the configuration file that we created earlier.