こんにちは、EventHubの井上です。
EventHubではE2EテストにCypress、CI環境にCircleCIを利用しています。
以前から、CircleCI上でCypressを実行した際に、5〜10%程度の確率で以下のエラーが発生し、テストがflakyに落ちる問題に悩んでいました。
Missing X server or $DISPLAY
テスト自体には問題がないためrerunすれば通るのですが、失敗するたびにCIを確認してrerunする必要があります。また、CircleCIの実行時間も余分に消費していました。
そこで原因を調査して対応したところ、修正後は1000回以上Cypressを実行しても一度も同じエラーが発生しなくなりました。
rerunによるCircleCIのコストをざっくり計算すると、月15,000円以上の削減にもなりました。
今回は、この問題の原因と行った対応について紹介します。
結論: DISPLAY を空にする
今回の環境では、CircleCIのDockerイメージとして cimg/node:*-browsers を利用し、Cypress CircleCI Orbを使わずに cypress run を直接実行していました。
同様の問題は、cimg-node のupstreamでも Issue #502 として報告されています。
Issue内で紹介されていた回避策を参考に、CircleCIの設定で DISPLAY 環境変数を空にしました。
jobs: test: docker: - image: cimg/node:24.14.0-browsers environment: DISPLAY: '' # 回避策 steps: - checkout - node/install-packages - browser-tools/install_chrome - run: command: npx cypress run --browser chrome
この変更を適用して以降、1000回以上CircleCI上でCypressを起動していますが、今のところ Missing X server or $DISPLAY は一度も発生していません。
では、なぜこれで安定するようになったのでしょうか。
Missing X server or $DISPLAY が発生していた理由
Linux上でCypressを動かす場合、ブラウザを起動するためにX11サーバーが必要になります。
Cypressの公式ドキュメントによると、Linux上でX11サーバーが利用できない場合、Cypressは自身でXvfb(X Virtual Framebuffer)を起動します。
一方、今回使用していた cimg/node:*-browsers では、あらかじめXvfbが起動されるようになっており、DISPLAY 環境変数にはその接続先として :99 が設定されています。
この処理は cimg-node のDockerfile でも確認できます。
そのため、Cypressは起動時に DISPLAY=:99 を参照し、CircleCIのDockerイメージ側で起動したXvfbへ接続します。
今回の問題は、このXvfbの起動が完了する前にCypressが実行されてしまうレースコンディションが原因だったと考えています。
イメージとしては以下のような状態です。
CircleCI container start
|
+---- Xvfbを起動 --------+
| |
+---- Cypressを起動 |
| |
+--> :99へ接続 |
↓ |
Xvfbがまだ起動していない
↓
Missing X server or $DISPLAY
実際、upstreamの Issue #502 でも、DISPLAY が設定されているもののXvfbの起動が完了していない場合に、CypressがX11サーバーへ接続できず失敗することが報告されています。
cimg/node:*-browsers 側にもXvfbの起動を待つ処理は存在しますが、今回の環境では5〜10%程度の確率でこのエラーが発生していました。
なぜ DISPLAY: '' の回避策が有効か
前述のとおり、CypressはLinux上で利用可能なX11サーバーが存在しない場合、自身でXvfbを起動します。
そこで、
DISPLAY: ''
とすることで、cimg/node:*-browsers が用意している :99 のXvfbをCypressから参照しないようにしました。
するとCypress側でXvfbを起動するようになり、CircleCI側のXvfbの起動タイミングに依存しなくなります。
Before
CircleCI
|
+--> Cypress
|
+--> 既存のDISPLAY=:99を使用
|
+--> CircleCI側のXvfb
↑
起動タイミングに依存
After
CircleCI
|
+--> Cypress
|
+--> DISPLAYを空にする
|
+--> Cypress自身がXvfbを起動
非常に小さな変更ですが、これによってflakyが解消しました。
なお、同じjob内で複数のCypressインスタンスを並列実行する場合など、環境によっては別の対応が必要になる可能性があります。この点についてもCypressの公式ドキュメントに注意事項が記載されています。
flakyを放置すると意外とコストがかかる
今回改善したかった一番の理由は、開発者体験でした。
E2Eテストが失敗すると、
- CIの失敗通知を見る
- ログを確認する
- アプリケーションの問題ではなくflakyだと判断する
- rerunする
- 再び結果を確認する
という作業が発生します。
一回あたりは数分程度でも、開発チーム全体で何度も発生すると無視できない時間になります。
さらにCircleCIではCIをrerunするたびに実行コストも発生します。
今回のケースでは、flakyによって発生していたrerunの回数とCircleCIの利用料金から概算すると、月15,000円以上のコストを削減できました。
金額自体はインフラコスト全体から見ればそれほど大きくありません。
しかし、
- CIを確認する時間が減る
- 不要なrerunがなくなる
- 「またflakyだろう」とテスト失敗を軽視することがなくなる
- CircleCIの利用コストも減る
といった効果を考えると、小さな修正の割には効果の大きな改善だったと思います。
upstreamでの根本解決について
今回行った DISPLAY: '' は、あくまでアプリケーション側での回避策です。
調査する中で、cimg/node:*-browsers 側でXvfbがreadyになるまで確実に待てれば、より根本的に解決できるのではないかと考えました。
具体的には、Xvfbの -displayfd オプションを利用して起動完了を検知する方法について調査し、その内容を Issue #502 にコメントしています。
今回の記事では詳細には触れませんが、アプリケーション側で問題を回避するだけでなく、可能であればupstream側でも改善できる形を探っていきたいと考えています。
さいごに
今回は、CircleCI上でCypressを実行した際に発生していた Missing X server or $DISPLAY のflakyを解消した事例を紹介しました。
最終的な修正は DISPLAY を空にするだけでしたが、なぜエラーが発生するのかを調べる過程で、CypressやCircleCIの裏側でXvfbがどのように使われているのかを知ることができました。
CIのflakyは「rerunすれば通るから」と放置してしまいがちですが、積み重なると開発者の時間やCIコストを少しずつ消費していきます。
今回のような小さな改善でも、長期的には開発速度や開発者体験の改善につながるので、今後も地道に改善していきたいと思います。
同じようにCircleCI × Cypress環境で Missing X server or $DISPLAY に悩んでいる方の参考になれば幸いです。
現在、EventHubではエンジニアを募集しています。本記事を読んで少しでも興味を持ってくださった方は、ぜひカジュアル面談からご連絡ください!