Akatsuki Hackers Lab | 株式会社アカツキ(Akatsuki Inc.)

Akatsuki Hackers Labは株式会社アカツキが運営しています。

2022年 7月 サーバサイドエンジニアとして働いてきました!

初めまして、2022年の7/4~7/29の3週間、アカツキゲームスのサーバーサイドインターンに参加させていただきました、白木といいます。この記事では、私が取りくんだこと、学んだこと、参加してよかったことについて書かせていただきたいと思います。

自己紹介

名古屋工業大学大学院工学専攻情報工学系プログラムの修士1年の白木です。普段はTLSの研究、Go言語を用いた開発を行なっています。自宅でLinuxを搭載したサーバでアプリケーションを動かしたりもしています。

今回取り組んだこと

今回のインターンで私が配属させていただいたプロジェクトは、Ruby on Railsによるゲームのサーバチームでした。インターン開始前の事前面談で私の経験してみたいことをふまえ、「大規模システムの負荷改善」のタスクに取り組むことになりました。以下がインターン期間に取り組んだことです。

  • 機能改修中のAPIの高速化(N+1問題の改善)
  • 負荷試験
  • ログのDBのidがオーバーフローする前に検知し、slackに通知を投げるシステムの開発

機能改修中のAPIの高速化(N+1問題の改善)

どういったN+1問題が発生していたか

実際にN+1問題が発生したAPIのログを見ると、下記図のような現象が起こっていることが判明しました。

これは、example_func関数の内部のusers.selectでusersテーブルからuserのデータ取得をを行うクエリを1回発行し、そして、ループでn_func関数を呼び、取得したユーザデータと紐付いたarticlesを得るクエリをN回発行する状態示した図です。

どのように解決したか

下記図のような、ループ開始前に、userと紐付いたarticlesをincludeメソッドを用いて予め取得しておくコードを書き、冗長なクエリを除去し、問題を解決しました。


RailsのActiveRecordにはEager loadingと呼ばれる機能があります。これは、予め、アソシエーションしているテーブルのデータをメモリ上にキャッシュすることができます。そのため、ループ開始前に、ループ内部で将来的に使われるアソシエーションをEager loadingすることにより、クエリの発行回数を減らすことができます。今回は、このEager loadingを使いました。

評価結果

この評価結果は、負荷試験環境で計測したものであり、実測値ではありません。クエリの発行回数は、N+1問題解消時にはN+1問題発生時の0.4倍ほどに抑えられていることがわかります。この変更によって、大量の同時リクエスト発生時にも、データベースへの負荷を最小限にとどめることができます。

N+1問題発生時 N+1問題解消時
クエリの総呼び出し回数 279 108
苦労したこと & 学んだこと

ソースコードとログの規模が大きく、処理の中で様々な関数が呼ばれることから、N+1問題の発生している処理が書かれている箇所を探すのに苦労しました。また、メンターのyasuさんとのモブプロを通じたRailsのデバッグ方法・ログの解析方法、N+1問題の対策を講じていたが、コードが複雑になっているが故に一部解決できていなかったのを目の当たりにし、エンジニアが実装時にN+1問題の発生に気付きにくいということも学ぶことができました。

負荷試験

今回、私がN+1問題の解決に取り組んでいたのは新しいバージョンの機能改修の箇所です。実は、この改修はあるAPIの負荷を減らし、パフォーマンスを向上させるものでした。そのため、その機能改修によってAPIが前のバージョンと比べ、適切に直っている、また、新たに負荷懸念となるようなAPIがないか調査をするために、負荷試験を行いました。

調査結果

Locustというツールで負荷試験シナリオを記述し、実施しました。クライアント側から90分間、負荷を与え、その結果をNew Relic、Cloudwatchのメトリクスを見ることで、調査をしました。機能改修箇所は、意図されたパフォーマンスを示し、新たに負荷懸念となるようなAPIは見つかりませんでした。

苦労したこと & 学んだこと

私が、負荷試験を実施したことがなく、負荷試験の勉強をするところから始めました。前バージョンの負荷試験の結果、メンターのyasuさんの助力もあり、なんとか苦労しながらも理解することができました。また、私がAWSの使用経験がなく、AWSが提供するサービスの知識不足により、色々とハマったりもしました。しかし、苦労はしましたが、非常に良い経験ができたと感じています。学生のポケットマネーでは、クラウドサービスの学習に対して投資することが困難な場合が多いです。そのため、AWSのサービスを触ることができたこと自体にも大きな価値があると思いました。

ログのDBのidがオーバーフローする前に検知し、slackに通知を投げるシステムの開発

課題背景

APIを処理するアプリケーションサーバでは、ログをとっています。このログは、APIの解析、障害時の原因究明をするために用いられており、重要な役割を担っています。このログをDBで管理しているのですが、以前、このDBに問題が発生しました。int、unsigned int型でauto increment属性を付与しているテーブルで、idがオーバーフローをし、ログをDBに保存することができなくなるという問題が発生しました。そのため、エンジニアがこの問題に事前に気が付けるように「ログのDBのidがオーバーフローする前に検知し、slackに通知を投げるシステム」の開発が必要になりました。

どのように解決したか

本番環境と接続しているDBで、先述のオーバーフローを検知するrakeタスクは既にあります。そのため、この実装を利用し、ログDBのオーバーフロー検知を実現させます。auto incrementの値を取得して、閾値を超えているかの判定処理を、関数化してログDBにも適用させました。時間が足りず、テストの修正ができませんでした。

苦労したこと & 学んだこと

ログDBをどのように扱うかについて苦労しました。ログDBはRailsのAPIサーバと接続するものでないため、どのように接続させれば良いのかが全くわかりませんでした。しかし、過去にログDBに接続を行う処理が書かれていたため、それを参考にして、解決しました。また、既存の処理を関数化させる際にも、多くの問題を起こしてしまいました。処理を抽象化させる際に、綺麗にさせることを意識していなかったため、非常に汚いソースコードを生成してしました。そのことをレビューで指摘されたため、今後、しっかりと目を向けていきたいと思いました。 また、閾値を決める際に、本番環境のデータの集計するクエリを発行しました。この結果を見た際に、私の想像を遥かに超える数が集計され、インターン期間に携わってきたシステムの規模の大きさに驚きました。

感想

このインターンで当初、取り組みたかった大規模システムに携われることができ、非常に楽しい時間を過ごすことができました。技術的な学びは先述の取り組みの中で多く得られました。手を動かした箇所だけでなく、データベースのシャーディングさせて、アプリケーションを動かしているのを見て、大規模システムを支える技術を見ることもできました。その他の学びとして、アカツキゲームスが採用しているスクラム開発も経験することができました。デイリースクラム、スプリントレビュー、スプリント・レトロスペクティブ、スプリント・プランニングに参加することで、よりスクラム開発への理解が膨らみました。そして、実環境で運用される機能開発をインターンで行うことができ、いい意味で緊張感のある開発を行うことができました。

謝辞

最後に、メンターのyasuさん、そして、技術的なサポート、ブログ執筆のお手伝いをしてくださったプロジェクト関係の皆様、ありがとうございました!

Akatsuki Games Internship 2022 Ruby on Rails / AWS(バックエンド)コース 参加記

はじめまして!7 月 4 日から 7 月 22 日にかけて、株式会社アカツキゲームスのインターンシップにバックエンドエンジニアとして参加させて頂きました、菊池と申します。

本記事では、私がインターンシップの期間で取り組んだ課題を紹介すると共に、インターンシップを通じて感じたこと等を書いていきたいと思います。この記事がアカツキのインターンシップへの参加を検討している方の一助となれば幸いです!

  • インターンシップに参加するまで
  • インターンシップで行ったこと
    • ミッションの新規追加
    • ゲーム内文言均一化ツールの開発
    • その他
  • 感想
続きを読む

Akatsuki Games Internship 2022 Go/GCP コースに参加させていただきました

こんにちは、若松と申します。

今回は、2022年7月4日から22日までAkatsuki Games Internship 2022のGo/GCP コースに参加させていただきました。

インターン情報 aktsk.jp

目次

エントリーのきっかけ

Akatsuki Games Internshipの存在を知ったのは、2022年の3月でした。 とある、面談イベントでAkatsuki Gamesの人事の方と一度お話しする機会があり興味を持ちました。

Akatsuki Gamesを選んだ理由
  • 「今後の就活を意識してたくさんの事業ドメインをこの半年間で見てみたい」という気持ちがあった + 自分の利用していたゲームを運用していたから
Go/GCPコースを選んだ理由
  • 自分の技術スタックとマッチしていたから
  • 自分の伸ばしたい・学びたいと思っている領域が学べると思ったから

インターン期間中に行ったこと

配属先と開発に携わったシステムについて

自分が今回配属されたチームは、Akatsuki Gamesの基盤開発を担っている「ATLASチーム」でした。

業務内容としては、ゲーム内通貨管理基盤の開発に携わらせていただきました。

通貨管理基盤は、図1のような構成になっています。

システム構成

通貨管理基盤は、Goで開発され、App Engine上で動いています。また、管理者がダッシュボード上から各ゲーム内通貨の状況を確認するための管理画面は、Python(Flask)で実装されており、Cloud Run上で動いています。

データの管理などには、Sercret Manager、Datastore、Cloud Storage、Big Queryが使用されており、インフラのリソースはTerraformで管理しています。

取り組んだタスク

参加時期が前期の末だったので、大学の課題や研究と並行しての参加となり、実際に稼働できたのは2週間ちょっとでした。
(大学と並行して無理のない範囲で柔軟に対応して、参加させていただきました。)

その間に自分は、以下の2つのタスクに取り組みました。

  1. 支払い情報検証用キーのバリデーション

  2. Load Balancerを設置することによる、システムの拡張性の向上

1. 支払い情報検証用キーのバリデーション

修正対象となる部分は、Google Play Storeでの後払い購入された際の購入情報(後払いレシート)の検証フローでした。

Google Play Storeで後払い購入を有効にしたサービスには、後払いレシートを検証するためのキーが発行されます。

一連の流れとしては、サービス公開前に発行された検証用キーを通貨管理基盤に登録する。サービス公開後、ユーザーがゲーム内通貨を購入した際に、検証用キーを用いて後払いレシートを検証するという流れでした。

自分は、サービス公開前に検証用キーを登録する際の処理に、キーの検証処理を実装しました。

登録をリクエストされたキーを用いて、Play Storeの検証用クライアントを生成できるか検証することでキーの検証処理としました。

2. Load Balancerを設置することによる、システムの拡張性の向上

2つ目は、システムを変更に強い構成にすることを目的としたタスクに取り組みました。

最終的な構成としては、App Engine(通貨管理基盤)をLoad Balancer経由でアクセスできるようにすることで実現しました。 通貨管理基盤は、複数のプロジェクト(リージョン)で運用されており、それぞれのプロジェクトごとにロードバランサーの設置の有無を選択可能にしたいという要件もありました。

それらの要件を加味した上で、最終的に図2のような構成で実装しました。

図2

従来の構成では、App Engineにデプロイした際に発行されるURLをクライアントに叩いてもらうことでリクエストを送ってもらっていましたが、システムの構成を変える際に、変更の度にリクエスト先を修正する手間をクライアントに要さないために、新しく通貨管理基盤用のドメインを取得しました。

ドメインの取得には、Google DomainsとCloud Domainsの利用が考えられましたが、Cloud Domainsを採用しました。

採用理由は以下のようなことが挙げられます。

  • Cloud DomainsはGCP上のプロジェクト用のドメイン取得を行うサービスであるため、GCPで完結している通貨管理基盤用と相性がいい
  • 月単位での課金のため、検証段階の今においてコスト的リスクが少ない
  • GCPコンソール上で、他のリソースと同じようにDNSなどの状態を確認が可能

ただ、現在TerraformではCloud Domainsはサポートされていないため、新しくドメイン管理用のプロジェクトを作成しました。 ドメイン管理用のプロジェクトを作成した理由としては、以下の理由が挙げられます。

  1. 通貨管理基盤をデプロイしているプロジェクト同士の責務は同じにしたい.

  2. Terraformで管理できていないリソースは、それだけでまとめたい

1つめの理由についてもう少し具体的にすると、通貨管理基盤をデプロイしているプロジェクトのどれか1つでドメインを取得した場合、ロードバランサーを設置する際のDNS周りの設定が1つのプロジェクトに依存してしまいます(図3)

図3

現状Terraformで管理できていない部分は、Cloud Domainsでのドメイン管理と、新たにロードバランサーを設置するプロジェクトへサブドメイン追加の権限付与が挙げられます。
(サブドメインの追加自体は、Terraformで管理しています。)

これらの人為的ミスが起こりうる部分でどれだけミスの可能性を落とせるかが課題となりそうな気がしています。

無事に、applyでリソースが生成できることを確認した後は、簡単な負荷計測を行いました。 今回のシステム構成の改善に伴う性能の劣化を懸念していましたが、秒間100リクエストほどの試験では問題ありませんでした。

また、Terraformのモジュール化、新たに追加していた権限の最適化を行いコードとしての完成度も高めることができました。

苦労と学び

自分にとっては2つ目のタスクの過程全てが難所でした。

特に悩まされたのは、以下の2つでした。

  • Stateと実環境のリソースでの差分発生時のエラー
  • 各リソースへの適切パラメーターの付与

Stateと実環境のリソースでの差分発生時のエラー

リソース間の依存関係を意識せずに作業を進めていた結果、前の変更でたまたま必要なroleが追加されていたがために、問題なくapplyされてしまい、リソースを削除する際には、権限が先に削除され、権限不足でエラーが発生しました。 そのほかにも、State上だとAPIが有効なはずなのに、実際は無効になっていてエラーになってしまうといった状況もありました。

先にCLI上できちんとリソースが生成可能かどうか検証する、一度の変更差分を小さくする(リソースをまとめて追加・削除しない)ことの必要性を感じました。

各リソースへの適切パラメーターの付与

Terraformは、各ベンダー・各リソースのテンプレートが準備されているため、一見簡単にリソースを定義できそうに最初は感じていましたが、そんなことはありませんでした。 GCPの仕様変更によって、参考にしていた公式ドキュメントのままでは、上手くいかない場面や、文章としての記載はなくExample Codeで示されているパラメーターのルールなどを把握する必要がありました。 いきなりTerraformからリソースを生成する前に、一度CLI上でリソースを生成して確認する必要性を感じました。

感想

しかし今回のインターンでは、あえて自分がこれまでに全く経験したことがない分野のタスクに挑戦させていただきました。

現状のシステム構成、変更する目的・要件を加味し、必要なリソースや変更点を洗い出す → 壁打ちしながら構成を検討する → コードに落とし込む → 検証する

という一連の流れを経験することができました。

システムのインフラをコードで管理することで、属人化を防ぐことができると感じました。 また、インフラをコードで管理することで多少の導入コストはあるものの、長い目で見ると確実に生産性が向上するのではないかと感じました。

今回は、非常に学びになった3週間でした。

メンターのなかひこさん、チームメンバーのみなさん、アカツキゲームスのみなさん、ありがとうございました!

アカツキゲームスのクライアントエンジニアインターンに参加しました

はじめまして。アカツキゲームスのクライアントエンジニアインターンとしてお世話になりました、今井と申します。

今回とあるゲーム開発プロジェクトに6/6 ~ 6/24の3週間ほどお邪魔させていただきました。

本記事では、今回のインターンで私が取り組んだことや学んだことについて書きたいと思います。

インターン開始前

インターンの実施開始1ヶ月ほど前に、メンターの方とインターンの内容について事前面談を行う機会がありました。

その面談で、事前に勉強しておいてほしいものとしてUniRxとUniTaskがありこの本をアカツキゲームスさんから頂いて勉強しました。また、課題として触っておいてほしいゲーム等の指示もありゲーム会社らしさを感じました。

課題について

僕の配属されたプロジェクトでは、課題をあらかじめ用意してくださっていたのでそれに従って課題を進めることにしました。

提示された課題は大きく分けて2つで、1つがUIの裏地にぼかしをかける機能の実装、もう1つがデバッグ用のキャラクタービューワーの作成でした。

UIの裏地にぼかしをかける機能はある程度できていたものが既にあり、これの性能向上及び使用するアーティストの方の意見を参考に改修するといったものでした。

キャラクタービューワーの機能については、全く作成されていない状態から1から要件に従ってビューワーを作るといったタスクでした。

UIの裏地にぼかしをかける機能

既にUIの裏地にぼかしをかける機能はできていましたが、アーティストの方との意見交換で以下のような要望が得られました。

  • UIの裏にUIがあったときにぼかしをかけても裏にあるUIがぼけない
  • UIのぼかしのかかり具合を徐々にアニメーションさせたい

また、メンターの方とのミーティングで以下のような問題があることも分かりました。

  • 今のぼかし機能だとゲームが実行されてから毎フレームずっと実行されてしまっている処理であり重い

これらを解決するために以下のように既存のぼかし機能を改修しました。

  • ぼかしをかける前のテクスチャ取得タイミングをUIが表示された後にずらすことによってぼかし対象にUIまで含められるようにした
  • ぼかしのかかり具合はテクスチャのアルファ値を時間で変化させることによって徐々にぼけて見えるようにした
  • ぼかし処理がずっと実行されないように、ぼかしをかけるテクスチャを貼り付けるUIが表示される瞬間のタイミングを取ってきて一回だけぼかしの計算を行うように変更

キャラクタービューワーの作成

キャラクタービューワーの作成は、何も作成されていない状態からデバッグメニューに機能を追加するといったもので、エンジニアの方との用件定義で以下のような仕様にすると話しました。

  • エディタと実機の両方で確認したい
  • キャラリストやモーションリストをプルダウンから選んで再生できるようにしたい
  • マウスやスマホのタッチ操作でカメラの移動や回転を行いたい

プロジェクトの規模が大きかったのでデバッグメニューを扱うコードがどこにあるのか調べるのに時間がかかったり、機能をどこにどのように実装すればいいのかが分からず実装に苦戦しました。こういった開発経験は初めてなのでかなり勉強になる部分が多かったです。

インターン期間終盤に、自分なりに考えて1通り機能を実装したものを実機でビルドしてエンジニアの方にフィードバックを貰いました。

私が考えて実装していたカメラ操作は1本指スワイプでスワイプしている間ずっとカメラ移動をし、キャラクターの回転はボタンを押して操作するといったものでした。

フィードバックの結果、カメラ操作は2本指スワイプでスワイプした移動量分だけカメラ移動をするように、キャラクターの回転は1本指でスワイプした分だけ回転させるといった実装に変更することにしました。

この操作周りの仕様は、具体的な指示があったわけではないので自分で考えて実装した部分だったのですが、結果的に改修することになったので作り始める前にあらかじめ相談してどういった入力にするのかを決めた方が良かったと感じました。

キャラビューワーの方は、最終的に要件に加えて追加でアニメーションの再生やライティング調整の機能を追加しプルリクエストを本番環境ブランチにマージするところまで出来ました。

インターンを終えて

リモートワークならではの取り組み

今回のインターンは、初日と最終日のみ出社してそれ以外は完全にリモートで行いました。インターン前はリモートワークだとコミュニケーションが疎かになったりするのではないかという心配がありました。

しかし、私の所属したチームでは朝にあさまると呼ばれるチームのメンバーが各々が好きな物事を紹介する時間があったり、事前に趣味のアンケートをとって似たような趣味同士の人でブレイクアウトルームを作って話し合う機会などがあり、チームの色々な人を知ったり交流することができました。

また、チーム全体で誰かが発信したときにそれにリアクションするということが徹底されていると感じました。例えば、Zoomで発表する時があったのですが沢山メンバーの方がコメントをつけてくださり、リモートながらコミュニケーションを相互に行いつつ発表することが出来ました。

研究活動等でリモートで発表することは多々あるのですが、何も反応がつかず寂しい思いをするといったことが多くあったのでこういった文化が形成されていることは素晴らしいと思います。

反省点

チームによって方針がかなり違う部分があり、僕のチームはかなり自主性を重んじる文化で基本的に個人の裁量に任せてくれる部分が大きかったです。

したがって、仕様をどうしたらいいかよく分からない部分は基本的に自分から聞きに行く必要がありました。しかし、あまり自分から聞きに行くことができず結果として時間の無駄になってしまう部分があったことが反省です。

しかし、この個人の裁量に任せてくれるという部分はプラスに働いていた部分もあると感じていて、どのように実装するのかといった部分から自分で考えて自由に実装することができたところもありました。特にキャラビューワーは自分でこうやりたいと考えた機能を追加できたので良かったです。

良かった点

私にとって、実際に機能を使うアーティストの方から意見をもらいつつ自分で改善案を考えて実装するといったサイクルを体験できたことは非常に価値のあるものでした。

私は今まで個人で開発することが多かったので、ブランチを切って実装、プルリクを上げてレビューしてもらう、修正する、マージするといった一通りの流れを経験できたことも新鮮でした。特にレビュー内容は気付かされることが多くかなり学びになったと思います。

また、インターンを通して自分のスキルで足りていない部分と通用していそうな部分が分かり、社会に出る前にどういったスキルを学ぶべきかの方針がついたことも良かったです。

ゲーム系のインターンに関して、実際の環境で業務を行うといった内容を実施している企業さんが少ないためアカツキゲームスさんのインターンは貴重だと感じます。

最後に

メンターの方をはじめとして、関わってくださるチームメンバーの方々がとても暖かく楽しくインターンを行うことができました。3週間という短い期間でしたがありがとうございました。

AWS Device Farm で Airtest を動かす方法

AWS Device Farmとは?

https://docs.aws.amazon.com/ja_jp/devicefarm/latest/developerguide/welcome.html

Device Farm は、アマゾン ウェブ サービス (AWS) によりホストされている実際の物理的な電話やタブレットで、Android や iOS、およびウェブアプリケーションをテストしてやり取りできるアプリケーションテストサービスです。

  • フレームワークを使用したアプリケーションの自動テスト
  • リアルタイムリモートアクセス

の二つのサービスがあり、今回は前者の自動テストについての内容です
自動テストサービスの一連の流れは以下の記事がわかりやすいです
AWS Device Farm で Appium を使って Androidネイティブアプリの自動テストを試してみた

対応フレームワークは以下があります

  • Appium
  • Calabash
  • Instrumentation(Android)
  • UI Automator(Android)
  • UI Automation(iOS)
  • XCTest(iOS)

Airtest とは?

https://airtest.netease.com/

With image recognition and UI hierarchy, our automation framework support game testing (訳: 画像認識とUI階層化により、ゲームテストをサポートする自動化フレームワークです。)

CEDEC2020ではその利用例が紹介されています
AirtestとPocoとOpenSTFによるUnity製スマートフォン向けゲームの実機自動テスト環境構築とその利用方法

一般的なモバイルアプリケーションの自動化は、アクセシビリティや自動化がサポートされたUI要素を指定しアクションを行います
しかし、ゲーム(正確にはゲームエンジン)のUI要素はレンダリングエンジンが描画したただのピクセルなため、外部スクリプトから直接UI要素を指定することができません
そのため、画像認識や内部にエージェントを組み込むなどしてUIを指定します

AWS Device Farmでサポートされているフレームワークの中ではAppiumが画像認識でのテストをオプションでサポートしています。しかし、Device Farmでは現在実行することができません(2022/06時点で確認済み)

AWS Device Farm 上で Airtest を動かすには

公式にサポートはされていないフレームワークでも、YAML 形式のテスト仕様 (テストスペック) にスクリプトを書くことで実行することが可能です

対応フレームワークに見せかける

テスト一式(test_bundle.zip)をアップロードする際にはフレームワークごとの形式に沿っている必要があるので、Appium Python と認識されるために必要最低限の体裁を整えます

構成例(pytestっぽくする)

.
├── requirements.txt(Appiumが含まれたもの)
└── tests(「tests」という名前のディレクトリ)
    ├── smoke.py(任意のAirtestファイル)
    └── test_main.py(test_hoge or hoge_test という形式の名前.py)

test_main.pyはこのようにAirtestのファイルを呼び出すようにします

import sys

def run_test():
    import smoke  # Run top-level commands

print("[TEST_SCRIPT]Start Airtest", file=sys.stderr) # Device Farmで標準出力がバッファリングされて見にくいのでstderrへ
run_test()
print("[TEST_SCRIPT]Finish Airtest", file=sys.stderr)
# 起動確認程度の簡単なスモークテスト
from airtest.core.api import *

CWD = os.getcwd()
PKG = os.environ['APP_PACKAGE']
LOG_DIR = os.environ['LOG_DIR']
auto_setup(__file__, logdir=LOG_DIR, devices=["Android:///", ])

if PKG not in device().list_app():
    install(CWD + "/" + os.environ['APP_PATH'])
start_app(PKG)

assert_exists(Template(r"tpl0000000000.png", "スタート画面があること")

uninstall(PKG)

テストスペックは以下のようにします

version: 0.1

phases:
  install:
    commands:
      - export PYTHON_VERSION=3
      - cd $DEVICEFARM_TEST_PACKAGE_PATH
      - . bin/activate
      - pip install --upgrade pip
      - pip install -r requirements.txt

  pre_test:
    commands:
      - cd $DEVICEFARM_TEST_PACKAGE_PATH
      - echo "Pre Test. OK"

  test:
    commands:
      - echo "Test Start"
      - export APP_PACKAGE='jp.hoge.huga.dev'
      - export LOG_DIR=$DEVICEFARM_LOG_DIR
      - python3 tests/test_main.py
      - echo "Test Done"

  post_test:
    commands:
      - echo "Post Test. OK"

artifacts:
  - $DEVICEFARM_LOG_DIR

しかし、このままではエラーで終了してしまいます

adb server version (39) doesn't match this client (40); killing...
ADB server didn't ACK

どうやらadbがDevice Farmのホストマシン上のものとAirtest内蔵のものでversionが異なり、既に起動しているDevice FarmのものをAirtest側がkillしようとして失敗するようです

adbをホストマシンのに差し替える

AirtestのFAQによると、競合した際はpackage内のadbを任意のものに差し替えるとのことです
なのでtestspecのpre_testでinstallしたAirtest package内のadbをDevice Farmのものに差し替えます

~略~

  pre_test:
    commands:
      - cd $DEVICEFARM_TEST_PACKAGE_PATH
      # Airtest package 内の adb を Device Farm のものに差し替える
      - echo "adb setup start"
      - rm ./lib/python3.7/site-packages/airtest/core/android/static/adb/linux/adb
      - $adb_path=$(which adb)
      - cp adb_path ./lib/python3.7/site-packages/airtest/core/android/static/adb/linux/adb
      - sed -i -e s#^\$ADB#$adb_path.orig#g ./lib/python3.7/site-packages/airtest/core/android/static/adb/linux/adb

      - echo "Pre Test. OK"

~略~

これでAirtestが動作するようになりますが、今度はテストシナリオは終了しているのに (testspecでいう)post_testに進行しなくなります
test 内 python3 tests/test_main.py 後の echo "Test Done" すら実行されていません. (uninstallや終了処理はされているようです)

[DEBUG]<airtest.core.android.adb> /tmp/scratchP4PNu8.scratch/test-packageXI1AKZ/lib/python3.7/site-packages/airtest/core/android/static/adb/linux/adb -s XXXXX uninstall 'jp.hoge.huga.dev
[DEBUG]<airtest.core.android.adb> /tmp/scratchP4PNu8.scratch/test-packageXI1AKZ/lib/python3.7/site-packages/airtest/core/android/static/adb/linux/adb -s XXXXX forward --remove tcp:14967
[07:16:55][DEBUG]<airtest.utils.nbsp> [rotation_server]b''
[07:16:55][DEBUG]<airtest.core.android.adb> /tmp/scratchP4PNu8.scratch/test-packageXI1AKZ/lib/python3.7/site-packages/airtest/core/android/static/adb/linux/adb -s XXXXX forward --remove tcp:18833

終了できていないプロセスを強制終了する

Airtestでは自動操作や画面のキャプチャのためにAndroid側やAirtestを実行しているマシンにいくつかのアプリケーションをインストールして、最後に終了するようになっています
その中の rotationwatcher という端末の回転を検知するプロセスがうまく終了できていなさそうでした

Airtest側でうまく終了できなそうだったので、強引ですがプロセスをkillする処理をAirtestの終了処理に差し込むことでDevice Farmが正常に終了しました

import sys

def register_force_kill():
    import time
    import subprocess

    def run_shell(cmd):
            print(cmd, file=sys.stderr)
            process = subprocess.Popen(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE, shell=True)
            stdout, stderr = process.communicate(timeout=150)
            exit_code = process.returncode
            print("stdout: ", stdout, " stderr: ", stderr, " exit_code: ", exit_code, file=sys.stderr)

    def teardown():
        print("[TEST_SCRIPT]teardown", file=sys.stderr)
        #run_shell("ps aux | grep rotationwatcher") # 確認用
        run_shell("pkill -f rotationwatcher")
        time.sleep(1) # Wait for rotationwatcher to terminate
        #run_shell("ps aux | grep rotationwatcher") # 確認用

    from airtest.utils.snippet import reg_cleanup
    reg_cleanup(teardown) # Insert into airtest termination process


def run_test():
    import smoke  # Run top-level commands

print("[TEST_SCRIPT]Registration of forced termination", file=sys.stderr)
register_force_kill()
print("[TEST_SCRIPT]Start Airtest", file=sys.stderr)
run_test()
print("[TEST_SCRIPT]Finish Airtest", file=sys.stderr)

無理やりkillしてるので本来の終了プロセスで参照してエラーがでていますが、 例外は無視されていてDevice Farm自体は正しく動いています

Exception ignored in: <module 'threading' from '/usr/local/lib/python3.7/threading.py'>
Traceback (most recent call last):
  File "/tmp/scratchnIe8tr.scratch/test-package8h1e4P/lib/python3.7/site-packages/airtest/utils/snippet.py", line 92, in exitfunc
    _cleanup()
~略~
ValueError: Invalid file object: <_io.BufferedReader name=7>

動画やログ、成果物の保存なども正常にできています

Device Farm の画面
ダウンロードしたレポート

まとめ

  • 対応フレームワークに見せかけて
  • AirtestのadbをDevice Farmのものに差し替えて
  • 終了に失敗するプロセスを自前で止めることで

AWS Device Farm上でAirtestが動きました! (サポート外の方法かつ無理やりな部分もあるのであくまで一例として…)