GASの排他制御（ロック）の利用方法を調べた

# はじめに

ここでいう排他制御（ロック）とは「何かの処理中は他の処理は待たせる」といった類いのものです。
自分自身、 GAS 以外の開発では排他に触れていたものの GAS では経験が無く、そのうち必要になるかなと思い気になったので調べてみました。
わかったことをまとめました。

# 対象の方

* 基本的な JavaScript が読める方
* GAS で何かを作ったことがある方

# ロックの利用手順

GAS で提供されるロック機能は、以下の流れで利用します。

1. `LockService` から目的に応じた `Lock` オブジェクトを取得
1. 取得した `Lock` の `tryLock()` や `waitLock()` を実行しロックを取得 **（ここで排他制御開始）**
1. 排他が必要な処理が終わったら `Lock` の `releaseLock()` でロックを開放 **（ここで排他制御終了）**

まず利用するロックの種類を決めて、排他制御を開始し、用事が済んだら排他制御を終える、という流れです。

# ３種類のロック

GAS のロックを利用するにあたって最初に決めるべきロックの種類を紹介します。
GAS では 3 種類のロックが提供されています。
それぞれ以下の特徴があり、目的に応じて使い分けます。

* **ドキュメントロック** （[リファレンス](https://developers.google.com/apps-script/reference/lock/lock-service#getDocumentLock())）
    * Googleドキュメントなどのリソース単位で排他制御する場合に使い、これらに内包された GAS から利用する
    * 上記のためか、Webアプリやスタンドアロンの GAS からは利用できない
    * 対象のリソースを利用するすべてのユーザーに対して排他制御が適用される
    * `LockService.getDocumentLock()` で取得する
* **スクリプトロック** （[リファレンス](https://developers.google.com/apps-script/reference/lock/lock-service#getScriptLock())）
    * スクリプト単位で排他制御する場合に利用する
    * たとえば複数人（スレッド）から同時に実行されたくない関数を作る場合に使える
    * ドキュメントロックと異なり、Webアプリやスタンドアロンの GAS からも利用可能
    * `LockService.getScriptLock()` で取得する
* **ユーザーロック** （[リファレンス](https://developers.google.com/apps-script/reference/lock/lock-service#getUserLock())）
    * 同一ユーザーが特定のコードを同時実行するのを制御する目的で利用できる
    * このロックは **ユーザーが異なれば同時にコード実行できる** 点に注意する
      * 例えばボタンをダブルクリックされた場合に、関数が二重で動くのを抑止する目的で使えそう
    * `LockService.getUserLock()` で取得する

上記 3 種類のロックはすべて `Lock` オブジェクトとして取得されます。

# ２種類の排他制御開始（ロック取得）方法

排他制御開始（ロック取得）方法は 2 種類あります。
いずれも `Lock` オブジェクトのメソッドです。

* `tryLock(timeoutInMillis)`
    * 指定したミリ秒の間、ロックの取得を試みます。ロックを取得できれば `true` を、そうでなければ `false` を返します
* `waitLock(timeoutInMillis)`
    * `tryLock` と同様の動作をしますが、こちらはロックを取得できなかった場合に例外を投げます

# ３種類とか２種類とか...

ややこしく感じるかもしれませんが、要はこういうことです。

1. まず 3 種類あるロックの種類から、目的に応じたロックを選ぶ
1. 次に 2 種類の排他制御開始方法があるので、好きな方法で排他制御を開始する

# 使ってみる

### ドキュメントロック

スプレッドシートで動きを確認しました。
新規にスプレッドシートを作り、「ツール」→「スクリプトエディタ」を開いて、下記のコードを記述します。

```js:Code.gs
function onEdit(e) {
  
  // ドキュメントロック
  const lock = LockService.getDocumentLock();
  
  // ロックを取得する
  if (lock.tryLock(1 * 1000)) {
    
    // A1をB1~F1にコピーする
    const sheet = SpreadsheetApp.getActiveSheet();
    const a1 = sheet.getRange('A1');
    a1.copyTo(sheet.getRange('B1:F1'));
    SpreadsheetApp.flush();
    
    // ちょっと待つ
    Utilities.sleep(5 * 1000);
    
    // ロック開放
    lock.releaseLock();
  }
}
```

この関数は各セルの値を変更する度に走ります。（[詳細](https://developers.google.com/apps-script/guides/triggers#onedite)）

やっていること

* ドキュメントロックを取得
* A1 セルを B1~F1 セルにコピー
* 少し待ってからロック開放

A1 セルに何か入力すると B1~F1 にコピーされますが、この前にロックを取得しているため、ロックを取得できなかった場合は、B1~F1 へコピーされません。
つまり連続して A1 セルを書き換えると、初回はコピーされるもののそれ以降（ロックが開放されて次にロックが取れるまで）はコピーされなくなる、という動きを期待しています。
ドキュメントロックはすべてのユーザーに排他が適用されるため、１アカウントで動作確認できます。（複数アカウントでも試し、期待通りに動きました）

### スクリプトロック

Web アプリを作って確認しました。
単体の GAS プロジェクトを作って、下記 2 ファイルを追加します。

```js:Code.gs
function doGet(e) {
  return HtmlService.createTemplateFromFile('index').evaluate();
}

function incrementWithScriptLock() {
  
  // スクリプトロック
  const lock = LockService.getScriptLock();
  
  // ロックを取得する
  if (lock.tryLock(10 * 1000)) {
    
    // A1の値をインクリメント
    incrementA1();
    
    // ロック開放
    lock.releaseLock();
  }
}

function incrementWithoutScriptLock() {
  // A1の値をインクリメント
  incrementA1();
}

function incrementA1() {
  const SSID = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
  const a1 = SpreadsheetApp.openById(SSID).getActiveSheet().getRange('A1');
  let value = a1.getValue();
  a1.setValue(++value);
  SpreadsheetApp.flush();
}
```

```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <button id="withLock">ロック取得あり</button>
    <button id="withoutLock">ロック取得なし</button>
  </body>
  <script>
document.querySelector('#withLock').addEventListener('click', function() {
  for (let i = 0; i < 5; i++) {
    google.script.run.incrementWithScriptLock();
  }
});
document.querySelector('#withoutLock').addEventListener('click', function() {
  for (let i = 0; i < 5; i++) {
    google.script.run.incrementWithoutScriptLock();
  }
});
  </script>
</html>
```

やっていること

HTML 上のボタンが押されたら、下記処理を 5 回繰り返します。

  * スプレッドシートの A1 セルの値を取得する
  * その値をインクリメントして A1 セルに書き込む

スクリプトロックの働きを確認できるように、この処理の前にスクリプトロックを取る関数と取らない関数を作りました。期待する結果は、インクリメントを 5 回繰り返すので処理前より 5 増えていること、です。

動かすと、ロックを取得しない方は A1 セルの値が 5 増えていません。何回か動かしましたが増えたのは 1 だけでした。
対してロックを取得する方は期待通り 5 増えています。
（排他あるあるですね）

なお、この画面を別のアカウントで開いてボタンを押した場合、ロックを取得する方は期待通りの動きをすることが確認できました。

### ユーザーロック

上記、スクリプトロックで使ったコードを一部書き換えて確認しました。
具体的にはスクリプトロックを取得する箇所を、ユーザーロックを取得するように書き換えました。

```js:Code.gs
  /*
  // スクリプトロック
  const lock = LockService.getScriptLock();
  */
  // ユーザーロック
  const lock = LockService.getUserLock();
```

動かしてみると、スクリプトロックで動かしたときと特に変化はありません。
ロックを取得する方は期待通りの動きをしていました。

ただしこちらはユーザーごとのロックなので、別アカウントも含めて動かした場合にスクリプトロックとは挙動が異なりました。別々のアカウントで同時に画面のボタンを押すと、期待する結果とはならず、ユーザーごとのロックだということが確認できました。

# 所感

GAS にも排他制御の機能が備わっていて、割と簡単に利用できることが分かって良かったです。
個人的なぼやきですが、記事を書く時にわかりやすいサンプルコードが書ければなあと常々思っています。