2017年11月2018年1月のアップデートで、kintoneへのREST APIの同時接続数に関するパラメータの取得が追加されました。今回はこのパラメータのの利用方法を見ていきたいと思います。

kintone REST API 同時接続数の取得

取得できるパラメータと取得方法を見たいと思います。

取得できるパラメータ

まずは取得できるパラメータですが、シンプルに次の2つです。
・その時点でのドメイン内の同時接続数 [running]
・同じく同時接続数の上限値 [limit]

取得方法がREST APIとJavaScript APIで異なるので、それぞれ見ていきましょう。

REST APIによる取得

すべてのkintone REST APIのレスポンスヘッダに含まれます。ということで、cURLコマンドでレスポンスヘッダを表示するiオプションをつけて試してみます。

curl -i -H "X-Cybozu-API-Token: $API_TOKEN" "https://$DOMAIN/k/v1/records.json?app=$APP_ID"

とすると、

HTTP/1.1 200 OK
Server: nginx
Date: Sun, 11 Feb 2018 16:51:53 GMT
Content-Type: application/json; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
Vary: Accept-Encoding
X-Frame-Options: SAMEORIGIN
Cache-Control: no-cache, no-store, must-revalidate
X-ConcurrencyLimit-Limit: 100
X-ConcurrencyLimit-Running: 1
Set-Cookie: JSESSIONID=henohenoSessionID;Path=/;Secure;HttpOnly
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Strict-Transport-Security: max-age=315360000; includeSubDomains; preload;
X-UA-Compatible: IE=Edge
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Set-Cookie: __ctc=henohenoCookies==; expires=Wed, 09-Feb-28 16:51:53 GMT; domain=cybozu.com; path=/

{"records":[],"totalCount":null}

ズラーっと、レスポンスヘッダが表示されます。「X-ConcurrencyLimit-Limit」「X-ConcurrencyLimit-Running」という項目があり、それぞれ同時接続数の上限値と現在値をさしています。2017年11月のアップデートでの追加項目でした。ここで疑問が生じます。kintone JavaScript APIでkintone.api()は、レスポンスヘッダは返さない訳で、これらの値を使いこなす必要があるときにはわざわざXHR使ってリクエストするのかと。答えは2ヶ月後の2018年1月のアップデートに現れてきます。

kintone JavaScript APIによる取得

kintone JavaScript APIでは、 kintone.api.getConcurrencyLimit という専用の関数が準備されました。ドキュメントに次のような記述例があります。

kintone.api.getConcurrencyLimit().then(function(result) {
  console.log(result);
  // {limit: 100, running: 1}
});

特徴的なのは、まず非同期であること。そして、これまでの非同期APIではコールバックのサポートもありましたが、この関数はPromiseオブジェクト返却のみのサポートです。これはもうkintone JavaScript APIを使いこなすにはPromise前提なのかなぁという感じですね^^;

同時接続数を考慮する必要性が出てきた背景を考えてみる

カスタマイズ開発を行う立場からだとディフェンシブなAPIですし、考慮ポイントとコード上のオーバーヘッドも増えた感が否めない部分もありますが、このタイミングでこのようなAPIが追加されたということは、kintoneがこれを考慮すべき領域に入ってきたということだと思います。つまり、「ドメイン内のユーザー数が大規模化してきた」ということでしょう。導入時はスモールスタートで利用範囲を拡大してきた、もしくはいきなり多数ユーザーで利用を開始する企業が出てきたというところでしょうか。

同時接続数の取り扱い

実際にどのように使っていくかですが、適用していくべきカスタマイズユーザー層とどのようにコーディングに適用していくかを考える必要がありそうに思います。ドキュメント等見てもサイボウズさんからもベストプラクティスは現状出ていないようなので、以降は我々のこれからの試行錯誤案になります。

適用していくべきカスタマイズユーザー層

同時に100リクエストが発生しうるという状況は、単純に100ユーザーを超える利用がひとつの目安になるのではないでしょうか(もちろんAPIを利用したカスタマイズ実装方法による部分がありますが)。事前の想定で適用の必要性を見定めるのはなかなか難しそうです。

コーディング適用案

どのようにコーディングに適用していくかですが、APIひとつ呼び出す前に都度チェックというのもちょっとやり過ぎ・非効率感があるので、
・イベント単位(編集画面を開いたら・・・、ボタンを押したら・・・ etc.)
・ロジック関数単位(値を更新する、集計する etc.)
といったイメージで適用していくのがよろしいかなぁと考えています。

1日1回ボタン押下で社員名簿アプリの年齢を更新するカスタマイズを例に実際のコードを見てみましょう。

/*
 * dependencies:
 *   moment (2.20.1)
 *   sweetalert (v2.1.0)
 *   kintoneUtility
 */
(function () {
  'use strict';

  // 同時リクエスト数をチェックしてイベント・ロジックを実行するヘルパー関数
  var checkConcurrencyLimit = function (params) {
    return kintone.api.getConcurrencyLimit().then(function (result) {
      console.log(result);
      if (result.running >= result.limit) {
        alert('kintoneへの同時アクセス数が制限に達しています。しばらく時間をおいてアクセスし直してください。');
        return kintone.Promise.rejct({
          message: '同時アクセス数制限に達しているため処理を中断しました。'
        });
      }
      return params;
    });
  };

  // 全レコードを検索して年齢を一括更新するロジック関数
  var updateAges = function (params) {
    var get_params = {
      app: params.app,
      query: '',
      fields: [params.fields.age.code, params.fields.birthday.code],
      isGuest: params.isGuest
    };
    return kintoneUtility.rest.getAllRecordsByQuery(params).then(function (r) {
      var update_params = {
        app: params.app,
        records: [],
        isGuest: params.isGuest
      };
      r.records.forEach(function (record) {
        var existing_age = parseInt(record[params.fields.age.code].value);
        if (record[params.fields.birthday.code].value != null) {
          // 生年月日から年齢の計算
          // refer to https://msdn.microsoft.com/en-us/library/ie/ee532932%28v=vs.94%29.aspx
          var birthday = new Date(record[params.fields.birthday.code].value);
          var today = new Date();
          var years = today.getFullYear() - birthday.getFullYear();
          birthday.setFullYear(today.getFullYear());
          if (today < birthday) {
            years--;
          }
          var real_age = years;

          // レコード登録中の年齢と計算した年齢が異なれば更新対象として追加
          if (existing_age != real_age) {
            update_params.records.push({
              id: record['$id'].value,
              record: {
                age: {
                  value: real_age
                }
              }
            })
          }
        }
      });
      return update_params.records.length > 0 ?
        kintoneUtility.rest.putAllRecords(update_params) :
        kintone.Promise.resolve({ message: '更新対象のレコードは存在しません。' });
    });
  };

  kintone.events.on(['app.record.index.show'], function (event) {
    var length = document.getElementsByClassName('update-ages').length;
    if (length === 0) {
      var button = document.createElement('button');
      button.setAttribute('id', 'update-ages');
      button.setAttribute('class', 'update-ages');
      button.innerText = '年齢一括更新';
      kintone.app.getHeaderMenuSpaceElement().appendChild(button);
      button.onclick = function () {
        var params = {
          app: kintone.app.getId(),
          isGuest: false,
          fields: {
            age: {
              code: 'age'
            },
            birthday: {
              code: 'birthday'
            }
          }
        };
        /*        
        // 同時接続数をチェックしない場合
        updateAges(params)
          .then(function (response) {
            console.log(response);
            return swal('一括更新完了', '年齢の一括更新が完了しました。', 'success');
          }).catch(function (error) {
            console.log(error);
            return swal('エラー', '年齢の一括更新中にエラーが発生しました。。', 'error');
          });
        */
        // 同時接続数をチェックする場合
        checkConcurrencyLimit(params)
          .then(updateAges)
          .then(function (response) {
            console.log(response);
            return swal('一括更新完了', '年齢の一括更新が完了しました。', 'success');
          }).catch(function (error) {
            console.log(error);
            return swal('エラー', '年齢の一括更新中にエラーが発生しました。。', 'error');
          });
      };
    }
    return event;
  });
})();

ボタンクリックというイベントもしくは updateAges() というロジック関数の発火前に、checkConcurrencyLimit() を挟んでチェックするという考え方です。checkConcurrencyLimit()の引数は後続で処理されるロジック関数 updateAges() に渡されるべき引数を渡しておき、同時接続数の上限値に達していなければ処理を継続し、同時接続数に達していれば処理を中止するというものです。

ポイントをまとめ直します。
・イベント、ロジック関数発火前に挟み込む checkConcurrencyLimit() を定義する
・checkConcurrencyLimit() のチェック後に処理されるロジック関数を準備しておく(kintone.Promiseオブジェクトをreturnするのと引数もひとつにしておくのがポイントです)
・checkConcurrencyLimit() の引数には後続で処理されるロジック関数に渡されるべき引数をセットする

まとめ

kintone REST APIの同時接続数に関するパラメータの利用を考察しました。ベストプラクティスは今後見出されると思いますが、今回JavaScriptカスタマイズでの利用を想定して、コーディング適用案を載せました。実際に運用してみて、より良い適用方法が出てきたらまた共有させて頂きたいと思います。


株式会社ジョイゾー