Tech Blog

グローバルな家族アプリFammを運営するTimers inc (タイマーズ) の公式Tech Blogです。弊社のエンジニアリングを支える記事を随時公開。エンジニア絶賛採用中!→ https://timers-inc.com/engineering

FirebaseのExportスキーマ変更によるマイグレーション作業中にGoogle Cloud Shellのセッションが切れてしまう問題を解決する

こんにちは。主にiOSチーム、時々分析チームのちぎらです。

先月末、FirebaseのBigqueryへのExposrtスキーマが変更されました。新しいスキーマはネストが浅く(1イベント1レコードになった)、OSの種類をテーブル名ではなくwhere句で指定できるようになったので、その辺りを考慮した分析もしやすい構造になりました。公式のマイグレーション手順の説明も充実していて、ここに書かれた通りの操作を行えば何の問題もなくマイグレーション作業が行えるかのように見えていたのでした。

Google Cloud Shell のセッション問題

過去のデータと新しく追加されるデータとを連続した期間で分析する為には、旧テーブルのデータを新しい形式に直して新テーブルと統合するマイグレーション作業が必要になります。マイグレーション作業の為のスクリプトGoogleが既に用意してくれているものがあるので、作業は少しスクリプト内の値をいじるだけで行うことができます。

Google Cloud Shell 上でスクリプトを実行し始めると、全体のマイグレーション処理にとても時間がかかることが分かりました。データ量にもよりますが、数時間は余裕でかかります。走っているスクリプトを放置し、数時間後に Cloud Shell を走らせていた画面に戻ってみると、スクリプトはもはや動いていません。Cloud Shellの制限事項について書かれたページには、以下のような記述があります。

Cloud Shell セッションを支える仮想マシン インスタンスは、Cloud Shell セッションに永続的に割り振られているわけではなく、セッションが非アクティブな状態が 1 時間続くと終了します。インスタンスが終了すると、$HOME 以外での修正はすべて無効となります。

...

Cloud Shell は、インタラクティブな使用のみを想定しています。非インタラクティブなセッションは、警告が出され、その後に自動的に終了します。長時間の使用、計算プロセス、ネットワーク集約的なプロセスはサポートされておらず、警告がないままセッションが終了してしまうおそれがあります。

マイグレーション作業は非常に時間のかかる作業なので、人間がたまに操作をして Cloud Shell セッションをアクティブな状態に保ち続けるというのは現実的ではありません。

どう対処したか

Google Cloud Shell のセッションを維持し続けるためには、Cloud Shell のコンソール上で何らかの操作をし続ける必要があります。 エンターキーを押し続けるだけでもいいかもしれません。ひたすらにエンターキーを叩き続けるIOTデバイスの作成という案も出たのですが、実装に何日もかかりそうだったので断念しました。

少し調べてみると、キーボードの入力をシミュレートする為のコードなら比較的簡単に実装できることが分かりました。エンターキーをひたすら打つだけではさみしいので、指定したコマンドを定期的に実行する為のアプリケーションを作成しました。

naru-jpn/KeyboardSimulator

f:id:timers-tech:20180710164619g:plain

実装

キーボードやマウスに関するイベントに対応するコードの定義は、macのターミナル上で

less /System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h

とすることで確認できます。

アプリケーションでは、ファイル KeyEvent.swift にいくつかの記号とコードの対応を定義しています。文字とキーボードのキーは1対1対応ではなく、例えばアルファベットの大文字はシフトキーを押しながら入力する必要があります。キーイベントは KeyboardSimulator.swift にあるように、以下のコードで発行しています。

/// キーダウン
static func down(key: CGKeyCode) {
  DispatchQueue.main.async {
    let source = CGEventSource(stateID: CGEventSourceStateID.hidSystemState)
    let event = CGEvent(keyboardEventSource: source, virtualKey: key, keyDown: true)
    event?.post(tap: CGEventTapLocation.cghidEventTap)
  }
}
    
/// キーアップ
static func up(key: CGKeyCode) {
  DispatchQueue.main.async {
    let source = CGEventSource(stateID: CGEventSourceStateID.hidSystemState)
    let event = CGEvent(keyboardEventSource: source, virtualKey: key, keyDown: false)
    event?.post(tap: CGEventTapLocation.cghidEventTap)
  }
}
    
/// クリック
static func click(key: CGKeyCode) {
  down(key: key)
  up(key: key)
}

例えば、大文字の A の入力をシミュレートしたい場合は、上の関数を用いて

down(key: {シフトキーに対応するコード})
click(key: {文字aに対応するコード})
up(key: {シフトキーに対応するコード})

と書くことができます。

マイグレーション作業の改善

このアプリケーションによって、マイグレーション作業を行っているマシン上で Cloud Shell がアクティブに保ち続けられ、私たちは通常業務に専念することができるようになりました。

また、普段はiOS用のアプリケーション開発をしていてmacOS用のアプリケーションを作成するのはこれが初めてだったのですが、いつもと違う前提で実装を行うことができ、新しい課題も見つけることができました。これを機にmacOS用のHuman Interface Guidelinesも読んでみようと思っています。( Leading and Trailing constraints with relation "Equal To" can cause unhelpful limitations on the space the text can fill when there is already a center constraint. というレイアウトに関する警告が出ているのですが、iOSと勝手が異なるようでどう扱うのが適切なのかまだ分かっていません。キーボード入力をシミュレートする為の権限と App Sandbox の設定との関係も理解が曖昧です。 )

よりスマートな解決方法をご存知の方がいらっしゃいましたら教えていただけると嬉しいです。笑

積極採用中!!

子育て家族アプリFamm、カップル専用アプリPairyを運営するTimers inc. では、現在エンジニアを積極採用中! 急成長中のサービスの技術の話を少しでも聞いてみたい方、スタートアップで働きたい方など、是非お気軽にご連絡ください! 採用HP : http://timers-inc.com/engineerings

Timersでは各職種を積極採用中!

急成長スタートアップで、最高のものづくりをしよう。

募集の詳細をみる