GASで排他処理を実現してみる
本記事では、排他処理というものをご紹介しようと思います。
おそらく、この記事にたどり着いた方は、フォームからGASでスプレッドシートにデータを書き込む際に、複数人でデータ送信をすると他のデータで上書きしてしまうといった事象に遭遇した方ではないかなと思います。
排他処理を聞いたことがない方もいると思いますので、外部のサイトでの説明を引用させていただきます。
排他制御とは、複数の主体が同じ資源を同時に利用すると競合状態(race condition)が生じる場合に、ある主体が資源を利用している間、別の主体による資源の利用を制限もしくは禁止する仕組みのこと。
出典:IT用語辞典 e-Words
とのことです。
一言で言うと、プログラムが同時に動かないように制限 / 禁止する処理とでもいいましょうか。
たとえば、冒頭に話したフォームから送られてきたデータを最終行に追加するというプログラムがあった場合に、全く同時に複数の人が入力フォームを送信すると、どちらも最終行が同じとなり、片方のデータをもう片方のデータで上書きしてしまうという場合に有効な手段です。
こういったことが発生する可能性がある場合には、プログラムに排他処理を追加して、ある特定の期間は1つの処理しか動かないようにして、他のユーザーの処理を排他するということが必要になります。
本記事が役立つターゲット読者
- 複数人で利用しているフォームからのデータが全て出力されない方
- 更新されているはずのデータが更新されず、古いデータが参照されてしまう方
- 追加されたはずのデータが参照できない方
スプレッドシートの操作でよく起こる問題でもありますが、その他の場面でも同時に複数の処理をさせたくない場合に利用できる処理になります。
記事で紹介するメソッド一覧
LockServiceというクラスのメソッドをご紹介します。
- getDocumentLock(ドキュメントファイル単位でスクリプトにロックをかける)
- getScriptLock(スクリプトファイル単位でスクリプトにロックをかける)
- getUserLock(ユーザー単位でスクリプトにロックをかける)
- hasLock(ロックの有無を確認する)
- releaseLock(ロックを解除する)
- tryLock(ロックを取得を試みる)
- waitLock(ロックを取得を試みる)
似たようなメソッドが多いのですが、微妙に異なりますので、詳細は下記で説明します。
LockServiceの適用範囲
LockServiceには、3つの範囲が存在します。
- DocumentLock
- ScriptLock
- UserLock
これはそれぞれどの範囲を排他処理の対象とするのかを表しています。
基本的な考え方として、『LockServiceを利用する場合は、可能な限り必要最低限の範囲に限定する』ということを念頭に置く必要があります。
なぜかと言うと、LockServiceは他の処理が実行しないように一定時間優先的に処理を実行できますが、優先されていない処理はプログラム処理時間を使って待機をします。
GASでは、処理時間の上限が6分間と決まっていますので、待機時間は極力短くした方が良いためです。
そのため、LockServiceを利用する際は、極力狭い範囲で処理を追加するべきだと考えます。
DocumentLockの対象範囲
DocumentLockの対象範囲は、1つのファイル(スプレッドシートやドキュメント等)になります。
1つのファイルの上で同時に処理が行われないようにしたい時に利用してください。
実行者が別のユーザーだとしても1つのファイル上で同時に実行をしようとすると排他されます。
ちなみに、同じスクリプトを複数のファイルで同時に実行する場合には別ファイルとなりますので排他処理はされません。
ScriptLockの対象範囲
ScriptLockの対象範囲は、実行対象のスクリプトファイルになります。LockServiceが記述されているファイルのことですね。
誰がどのファイルに対して実行しても同時に処理が行われないようにしたい時に利用してください。
UserLockの対象範囲
UserLockの対象範囲は、実行するユーザー単位となります。
同じスクリプト上やファイルに対して実行する場合も、実行者が異なるアカウントであれば排他処理はされません。
処理中に実行ボタンを再度押されないようにするというような場合に有効な処理かと思います。
対象範囲の大きさ順に並べると・・・
こんな感じかなと思います。
※もちろん、状況によっては順番が前後する場合もあります。
LockServiceのサンプルコード
引数に入れる値
引数なし
サンプルコード
//DocumentLockのサンプルコード
function sampleCodeForDocumentLock() {
let msg;
let lock = LockService.getDocumentLock();
if (lock.tryLock(5000)) { //10秒間他の処理が終わるのを待ち
Utilities.sleep(10000); //15秒処理をスリープ
lock.releaseLock(); //ロックを解除
msg = "実行完了";
}
else {
msg = "他の処理が実行中です";
}
Browser.msgBox(msg);
}
//ScriptLockのサンプルコード
function sampleCodeForScriptLock() {
let msg;
let lock = LockService.getScriptLock();
if (lock.tryLock(5000)) { //10秒間他の処理が終わるのを待ち
Utilities.sleep(10000); //15秒処理をスリープ
lock.releaseLock(); //ロックを解除
msg = "実行完了";
}
else {
msg = "他の処理が実行中です";
}
Browser.msgBox(msg);
}
//UserLockのサンプルコード
function sampleCodeForUserLock() {
let msg;
let lock = LockService.getUserLock();
if (lock.tryLock(5000)) { //10秒間他の処理が終わるのを待ち
Utilities.sleep(10000); //15秒処理をスリープ
lock.releaseLock(); //ロックを解除
msg = "実行完了";
}
else {
msg = "他の処理が実行中です";
}
Browser.msgBox(msg);
}
変数の説明
msg = 処理完了後に表示するメッセージ
lock = LockServiceのロック
DocumentLock、ScriptLock、UserLockそれぞれのサンプルコードです。
これは少し上で動画で動作確認をしてもらったコードになります。
LockService.get****Lockの****以外はどれも同じ書き方です。
なので、どの範囲に排他処理を適用させたいかで使うコードが変わってくるということになります。
必要最小限の範囲をロックしよう!
2回目ですが、これがポイントです。
hasLock()の役割
hasLockは、排他処理がされて実行中の処理があるかどうかを確認するためのメソッドになります。
もし、実行中の処理があれば、falseが返ってきます。
そのため、排他処理で自分の処理が続行できない場合の処理を記述することができます。
let lock = LockService.getScriptLock();
lock.tryLock(10000);
if (!lock.hasLock()) {
console.log("10秒待ちましたが、ロックを取得できませんでした。");
}
releaseLock()の役割
releaseLockは、LockServiceにおいて非常に大事なメソッドになります。
ロックを取得し、自分の処理が排他処理を開始すると、他の処理は同時に実行ができなくなります。
先程から述べているように、排他処理は必要最低限にする必要がありますので、排他処理をする必要がある処理が終了したら、できるだけすぐにreleaseLockをしてロックを解除します。
ロックを解除しないと他の処理がずっと待機状態に
忘れずにロックを解除しましょう
let lock = LockService.getScriptLock();
lock.waitLock(10000);
//排他処理を必要な処理を実行する。
lock.releaseLock();
引数に入れる値
- milisecond(整数) − ミリ秒でロックする時間を指定する
サンプルコードの解説
最大10秒の間、排他処理を実行するようにロックをかけます。
処理が終了したらreleaseLockでロックを解除しますので、10秒以内に処理が終われば待機中の処理が処理を再開します。
tryLock()の役割
tryLockはロックを取得する(ロックをかける)ためのメソッドになります。
tryLockの場合、他の処理が実行中であれば、引数に指定したミリ秒の間、処理を待機させます。
指定したミリ秒の間に他の処理が終了した場合は、trueを返しますので、条件分岐でロックが取得できた場合とそうでない場合の処理を分けることができます。
指定したミリ秒の間に他の処理が終了しなかった場合はfalseを返します。
let lock = LockService.getScriptLock();
let success = lock.tryLock(10000);
if (success) {
//他の処理が終わった場合の正規処理をこちらに記述します
lock.releaseLock();
}
else {
console.log("10秒間待機しましたが、他の処理が終了しませんでした。");
}
引数に入れる値
- milisecond(整数) − ミリ秒でロックする時間を指定する
サンプルコードの解説
tryLock(10000)で最大で10秒間待機して他の処理が終わるのを待つようにしてありますので、10秒以内に他の処理が終われば正規の処理を、終わらなければelse 文の処理を行うことになります。
ちなみに、指定する引数は公式ドキュメントには上限時間がかいていませんでしたので、任意の数字を入れることができると思います。
ただし、GASの上限6分を超えないように調整してくださいね。
waitLock()の役割
waitLockもロックを取得する(ロックをかける)ためのメソッドになります。
waitLockの場合も、tryLockと同様に他の処理が実行中であれば、引数に指定したミリ秒の間、処理を待機させます。
指定したミリ秒の間に他の処理が終了した場合は、通常通り処理を進めますが、指定したミリ秒の間に他の処理が終了しなかった場合はエラーを吐きます。
そのため、waitLockはtry〜catch文と併用して使うことになります。
try {
LockService.getScriptLock().waitLock(10000);
//他の処理が終わった場合の正規処理をこちらに記述します
lock.releaseLock();
} catch (e) {
console.log("10秒間待機しましたが、他の処理が終了しませんでした。");
}
引数に入れる値
- milisecond(整数) − ミリ秒でロックする時間を指定する
サンプルコードの解説
waitLock(10000)で最大で10秒間待機して他の処理が終わるのを待つようにしてありますので、10秒以内に他の処理が終わればtry文の処理を、終わらなければcatch 文の処理を行うことになります。
waitLockの引数も公式ドキュメントには上限時間がかいていませんでしたので、任意の数字を入れることができると思います。
以前、海外の技術者フォーラムでLockServiceについて質問をした時に、『LockService.getScriptLock()を変数に入れるな。』と言われたことがあります。
理由を聞いてみると、『LockService.getScriptLock()の時点でエラーが発生したらその後の処理もされずに処理が止まってしまう。だから、変数に入れちゃダメなんだ。』と言われ、なるほどなーと思ったのですが今書きながら思い返してみるとtry文の中で変数に入れれば良くね?と思いました。
どっちがいいんですかねー?
まとめ
今回は、GASにおいて排他処理を実現するための方法をご紹介しました。
データが上書きされてしまったりする方は使い方をぜひ身につけてくださいね。