Google Apps Script (以下、GAS)便利ですね。
そりゃ、できないこともいろいろあって、スプレッドシートに紐づく形式の場合、エクスポートができなかったり(なので、GitHubが使いにくい)、いろいろと不満もありますが、サーバーなしにいろいろとできるんで便利です、ハイ。
で、先日ちょっとしたきっかけでスプレッドシートにスクリプトを作ったのですが、その際にユーザーの応答を得るためにカスタムダイアログを使いました。このとき、カスタムダイアログの呼び出し前後を一括して排他制御をしたかったので、ロックをかけようとしたら、見事にはまりました。
せっかくなので、その時の経緯と自分のやった対応策をメモっておきます。たぶん本当はもっといい方法があると思うので、ご存知の方ぜひ教えてください。
構成
基本的な構成はこんな感じです。
- 独自メニューを追加し、各メニューからスプレッドシートを操作する処理を呼び出す
- メニューから呼び出された処理で、カスタムダイアログを表示して、ユーザーに選択肢を示す
- カスタムダイアログで選択された選択肢に基づいて、処理を続行する
- このスプレッドシートを利用するユーザーは複数おり、同時に独自メニューを使うこともあり得る
- 独自メニューから呼び出される処理は、排他的に行いたい
- 一連の処理は1スプレッドシートからのみ使う(GASプロジェクトを複数のスプレッドシート等で共有しない)
まあ、いろいろと書いてもわかりにくいので、サンプル作ってみました。
基本的な動作は、次の通りです。『独自メニュー』から、『追加』を選択します。
すると、追加したいテキストを入力するダイアログが表示され、
適当にテキストを入力して、『OK』を押すと、
のように、スプレッドシートの所定の列の末尾に入力したテキストが追記されるというものです。
まあ、これだけだと、わざわざダイアログで応答求めるようなものでもないのですが、実際にはスプレッドシートの内容を読み取りつつ、ユーザーにいろいろと選択肢を示し、その結果に応じて処理を変えるような処理を行うイメージです。 排他処理を除けば、これでやりたいことができているという状態と仮定します。これに排他処理を追加していくことを考えていきます。
なお、この状態(排他処理のない状態)でのソースコードは下記を参考にしてください。
https://github.com/samples-of-junichim/GAS_custom_dialog_lock_sample/tree/01_no_exclusive_proc
参考
上記サンプルを作成する際に参考にしたサイトです。
- Google Apps Scriptを使った独自メニューの作り方 - Qiita
- ダイアログ全般 公式ガイドの「Dialogs and Sidebars in Google Apps」を制覇する!(その3) - Qiita
なお、この記事中に UiApp のことが触れられていますが、現時点で既に deprecated になってるので HtmlService を使うほうが無難だと思います。
- Dialogs and Sidebars in G Suite Documents | Apps Script | Google Developers
- カスタムダイアログ HTML Service: Templated HTML | Apps Script | Google Developers
- コールバック HTML Service: Communicate with Server Functions | Apps Script | Google Developers
排他処理の追加 (1) : Lock オブジェクト
さて、GAS の場合、排他処理をどう追加すればよいのか?と調べてみると、簡単に利用できることが分かりました。
LockService の getDocumentLock を使って、ロックオブジェクトを取得して、ロックオブジェクトの tryLock を呼び出せばOKです。 ロックが取得できれば、trueが帰り、ロックが取得できなければfalseになります。
そこで、早速、 LockService / Lock を追加してみました。
https://github.com/samples-of-junichim/GAS_custom_dialog_lock_sample/tree/02_lockservice
リファレンスによると、ロックオブジェクトは、明示的に開放しなくても、処理終了時に解放されるとあります。 で、カスタムダイアログを呼び出すと一旦処理が終了して、ダイアログからは非同期でコールバックされます。 このため、ロックオブジェクトをカスタムダイアログ呼び出し前後で保持するために、グローバル変数を使おうとしました(が後述するように使えません)。
試してみると
早速試してみます。
これで動くかな?と思いきや、一つ目のブラウザからダイアログ表示後、別のブラウザで独自メニューを呼び出してみるとロックが取れて、ダイアログが表示されます。 おまけにコールバック呼び出し後、lockオブジェクトのメソッドがないということで、エラーになります。
ひょっとしたらと思い調べてみると、GASの場合、グローバル変数は使えないようです。
でも、カスタムダイアログの表示は非同期に行われるので、 そうなると、カスタムダイアログ呼び出し前にロックオブジェクトを取得したとしても、コールバック関数にそのオブジェクトを渡せないので、どうやってダイアログ表示前後を含む一連の処理が完了するまでを排他処理すればいいんだろうか?というところで、壁にぶち当たりました。
排他処理の追加 (2) : PropertyService
さて、困ったので、いろいろと調べてみると、こういう場合は、 PropertiesService をグローバル変数代わりに使うというのが定番のようです。
まあ、小さなアプリケーションなので、ちょっと大げさな気がしないでもないですが、ここはひとつグローバル変数代わりに Property を頼ってみよう、としました。
そこで、前述の LockService はやめて、メニューから処理を呼び出した際に、PropertiesService に現在時刻を記入するようにします。 カスタムダイアログからのコールバックでは、処理終了時に記入されている時刻を削除します。 独自メニューから呼ばれる各メソッドの先頭(サンプルだと1つですが)では、 PropertiesService の時刻を確認して、記入がなければ、排他状態ではないと判断するようにします。
時刻を使ったのは、一定以上時間が経過している場合は、処理が中断されたと判断したいためです。
ソースコードはこちらになります。
https://github.com/samples-of-junichim/GAS_custom_dialog_lock_sample/tree/03_propertiesservice
にしても、自分であれこれ実装するのいやですねー。排他処理周りって思わぬバグの温床なんで触りたくないんですよね・・・。LockService 使えれば楽なのになー。
ま、さて、これでうまくいくはずです。 2つのブラウザでログインして、一方でカスタムダイアログを表示中に別のブラウザからメニューを呼び出してみると、ちゃんと排他制御できているっぽいです。
排他処理の問題発見と原因の推測
ということで、一件落着と思ったので、適当に遊びながらいろいろと試していると、どうも挙動がおかしい時があります。
具体的には、カスタムダイアログからのコールバックが正しく処理されません。 ただ、毎回おかしいというわけではなく、おかしい場合もある、という感じです。
というのも、ユーザーがカスタムダイアログを表示後、そのまま放置した場合に備えて、記録した時間が一定以上たっていたらキャンセルとみなして中断するような処理を入れています。 その際、PropertiesService から値の読み込みができなければ、記録開始をしていないとしてキャンセルと同じ処理としています。
明らかに、決めた時間(10分)も経っていないのに、カスタムダイアログからのコールバックで、時間が経過した旨のダイアログが表示されたのです(画像撮り損ねました・・・)。
そこで、Logger を使って、Property 設定前後の変数の値などをいろいろとログに出して観察していると、挙動がおかしい時は、 PropertiesService にセットしたはずの時刻が取得できていません!
なんだこれは?と思いつつ、 設定した Property についていろいろとログを出して調べると、どうも PropertyService への書き込みは即座に反映されるというわけではないようです(独自調べなので、根拠なしですよ)。
やられました。 単純な変数のようには使えないようです。
排他処理の追加 (3) : PropertiesService の書き込み確認
そもそも、 PropertiesService は『変数』ではないので、無理もないことなのでしょう。まあ、これを排他制御に流用する筋が悪いのかもしれません。この辺りは、GASに詳しい方にぜひ教えていただければと思います。
なんにせよ、排他制御しないと目的のものが作れず困ってしまうので、 Property が書き込まれたか否かを確認する処理を追記します。
追記&いろいろと修正した形がこちら。
https://github.com/samples-of-junichim/GAS_custom_dialog_lock_sample/tree/04_complate
主な修正としてはカスタムダイアログからのコールバック処理に問題があったので対応しました。 コールバックでは、 Property に設定した時刻を元に時間の経過を判定していたのですが、実際に一定以上の時間が過ぎた場合に、別ユーザーがカスタムダイアログを呼び出し、処理開始時刻を書き換えるとタイムアウトするべきユーザーが処理を継続できてしまうという問題があったので、カスタムダイアログ経由で呼び出し時刻の受け渡しを行うようにしています。
これで試すと、(試した範囲では)問題なく排他制御できていました。
ふー、一件落着
注意点
ちなみに後日、この記事まとめなおすときにいろいろと試したら、Property 設定でほとんどタイムラグがないような振る舞いをしていました。 たまたまなのか、Google Apps Script 側で何か変わったのかちょっとわかりません。
なので、ひょっとしたら最後の Property の書き込み確認は余計な負荷をかけるだけになるので、不要かもしれません。 もうちょっと GAS を使い込まないと見えないところかもしれませんね。
こういうこともあったらしいぐらいにとどめておいてください。
まとめ
GAS 便利ですが、カスタムダイアログを使った場合の排他処理に意外と手間取りました。 とはいえ、GASは頻繁にアップデートされているので、そのうち、これにも対応したロック処理が追加されるのではないかと期待しています。
せっかく無料で使えて、いろいろとできるので、もうちょっとGAS使った仕事を増やしたいところです。
おまけ
冒頭で、Google Apps Script で『スプレッドシートに紐づく形式の場合』Gitで管理しにくい、などと書きましたが、後から、 Chrome 拡張『Google Apps Script GitHub アシスタント』を使えば簡単にリポジトリにpush/pullできることが分かりました! (この記事中のスクリプトは、このプラグインでGitHubのリポジトリにpushしたものを示しています。)
こちらを試した話は、別の記事にまとめていますので、ご興味のある方はそちらもご覧ください。