🥪

NXとkintoneをつなぐ

に公開

はじめに

本記事は、PLC(Programmable Logic Controller)向けのソフトウェア開発に従事、または関心のある方で、Structured Text(ST)による開発に興味がある方向けです。OMRON社のSysmac Studioとコントローラ(NX1またはNX5)、サイボウズ社のkintoneを使用します。

今回は、kintone REST APIを使用してNXとkintoneをつなぎます。kintone REST APIを簡潔に使用できるようにしてユーザーが詳細を意識する必要が無いようにしました。kintone REST API clientサービス(以下、APIクライアントサービス)を提供するFBが適当なタスクで動作していればkintone REST APIを使用できます。

NXとkintoneをつなぐ目的は、目的に特化したアプリケーションを迅速に展開し、目的を達したら破棄するという弾力的な運用を可能にすることです。作っておいて何ですが、APIクライアントサービスは気にしなくていいです。実際にkintoneにアプリを作成してNXをつないで使ってみることの方が各段に楽しいです。kintoneを治具にすることで、MESや設備監視システムとは異なる方向の機能性を得られます。

ユーザーは、kintone REST APIを以下のようなコードで使用します。fetch、resolved、rejectedという単語で予想がつくと思います。

POU/プログラム/TroubleCollection_FetchDemo
100:
    NewCreateTroubleRecordFetch(
        Context:=iFetchContext,
        AppName:=KINTONE_APP_NAME,
        Name:=iTroubleName,
        OccureTimestamp:=iTroubledAt,
        ErrorCode:=iErrorCode,
        Description:=iTroubleDescription);
    
    Inc(iState);
101:
    CASE KintoneRestApi_fetch(iFetchContext) OF
        ATS_RESOLVED: // 成功
            iState := iReturnState;
        ATS_REJECTED: // 失敗
            KintoneRestApiFetch_getError(
                Context:=iFetchContext,
                Error=>iError,
                ErrorID=>iErrorID,
                ErrorIDEx=>iErrorIDEx);
            
            iState := iReturnState;
    END_CASE;

NewCreateTroubleRecordFetchは、以下のコードです。GenerateTroubleRecordでkintoneアプリに追加するレコードを生成し、KintoneRestApi_newCreateRecordFetchでFetchというAPI呼び出し単位を生成します。使用にFetchの詳細は不要で、KintoneRestApi_newCreateRecordFetchが1件のレコードを登録するkintone REST APIに対応することが分かれば十分です。

POU/ファンクション/NewCreateTroubleRecordFetch
NewCreateTroubleRecordFetch := FALSE;
IF NOT EN THEN
    RETURN;
END_IF;

IF NOT GenerateTroubleRecord(
           Record:=iRecord,
           Head:=0,
           Size=>iRecordSize,
           Name:=Name,
           OccureTimestamp:=OccureTimestamp,
           ErrorCode:=ErrorCode,
           Description:=Description)
THEN
    RETURN;
END_IF;
NewCreateTroubleRecordFetch
    := KintoneRestApi_newCreateRecordFetch(
           Context:=Context,
           AppName:=AppName,
           Record:=iRecord,
           RecordHead:=0,
           RecordSize:=iRecordSize);

LDは以下のようになります。APIクライアントサービスは投げっぱなしを許容しないので、成否の応答があるまで監視します。ほったらかしにするとAPIクライアントサービスのリソースが枯渇します。

LDでの使用例
LDでの使用例

上記のコードはいずれもAPI呼び出し要求を行うだけです。別途、API呼び出し処理を行うPOUがあり、あらかじめ実行しておく必要があります。以下のようなAPI呼び出し処理を行うPOUを含むプログラムを実行します。POUにシークレット情報を配置するのであれば、別途パスワード保護です。

POU/プログラム/KintoneRestApiClientServiceRunner
CASE iState OF
    // STATE_INIT
    0:
        // 設定初期化
        InitKintoneRestApiClientServiceSettings(
            LockKey:=17);
        
        // Kintoneアプリの登録

        // トラブル収集アプリの登録
        RegisterKintoneApp(
            // Nameは、プログラム内でアプリ情報を取得するキーとなる。
            Name:='TroubleCollection',
            // 割り当てられたサブドメイン。
            Subdomain:='YOUR_KINTONE_SUBDOMAIN',
            // 作成したアプリのID。
            AppId:='YOUR_APP_ID',
            // アプリで生成したAPIトークン。
            ApiToken:='YOUR_APP_API_TOKEN');
            
        // 生産モニタアプリの登録
        RegisterKintoneApp(
            Name:='ProductionMonitor',
            Subdomain:='YOUR_KINTONE_SUBDOMAIN',
            AppId:='YOUR_APP_ID',
            ApiToken:='YOUR_APP_API_TOKEN');
            
        // 生産タスクアプリの登録
        RegisterKintoneApp(
            Name:='ProductionTask',
            Subdomain:='YOUR_KINTONE_SUBDOMAIN',
            AppId:='YOUR_APP_ID',
            ApiToken:='YOUR_APP_API_TOKEN');
        
        // 無制約TLSセッションの登録
        RegisterUnrestrictedTlsSession(
            TlsSessionName:='TLSSession0');
        RegisterUnrestrictedTlsSession(
            TlsSessionName:='TLSSession1');

        Inc(iState);
    1:
        IF iCtrlReload THEN
            ReloadKintoneRestApiClientService();
            iCtrlReload := FALSE;
        ELSE
            EnableKintoneRestApiClientService();
            iService.Enable := TRUE;
        END_IF;

        iState := STATE_ACTIVE;
    
    // STATE_ACTIVE
    10:
        // オンラインによる手動操作用
        IF iCtrlEnable THEN
            EnableKintoneRestApiClientService();
            iCtrlEnable := FALSE;
        ELSIF iCtrlDisable THEN
            DisableKintoneRestApiClientService();
            iCtrlDisable := FALSE;
        ELSIF iCtrlReload THEN
            iState := STATE_INIT;
        END_IF;
END_CASE;

iService();

サンプルプロジェクトを編集してコントローラで実行すると、kintoneアプリに以下のようにレコードを作成します。これはトラブル収集アプリで、トラブルが発生した時点でkintoneにレコードを作成します。
トラブル収集アプリのトラブル一覧画面
トラブル収集アプリのトラブル一覧画面

kintoneについて

kintoneは、CRUDアプリケーションを作成するサービスであり、リレーションによるデータモデルでアプリケーションを構築するのではなく、多数の独立した単一もしくは少数テーブルの小さなCRUDアプリケーションとワークフローを前提とした体系なのかなというのが私の認識です。

多数のステークホルダーが作成と破棄を繰り返すアプリケーションの多産多死を前提とすれば、リレーションは、ステークホルダー間の調整を増やすことでアプリケーション作成のコストを増やし、簡単にアプリケーションを作成して使用できるという最大の売りを強く阻害する要因であると判断しても不思議ではありません。

また、アプリによる小宇宙の方がデータモデルによる小宇宙よりも誰にも分かりやすく、かつ、いずれのアプリも他と連携しているか否かに関係なく、何らかの目的のために機能しているという点では付加価値を生むという最大の目的において合理的であるようにも思えます。

kintone上にアプリケーションを構築するのであれば、アプリケーションを構築するという認識よりも、業務フローを構築するという認識で取り組むことが良さそうです。一般的な業務アプリケーションの開発でも、初めからテクニカルな内容を持ち込むと物事があらぬ方向を向いてしまうということがあると思います。kintoneではそれがより極端に、そもそも出来ないという判断になるように思えます。

私の認識として、kintoneは多数の小アプリとアプリの多産多死を前提にしているので、APIクライアントサービスは同時に複数のアプリに対してアクセスでき、アクセスのためのシークレット情報も比較的頻繁に更新する必要があることになります。

kintoneにつなぐことで何ができるか

kintoneで何ができるかという命題と同じなので難題です。例えば、トラブルとその対策についての情報収集などはすぐに思いつきます。

まず、トラブル発生時にコントローラがkintoneの指定アプリにレコードを作成します。オペレータは、kintoneアプリをインストールしたモバイル端末を携帯しており、トラブル発生の通知を受けます。オペレータはモバイル端末のkintoneのアプリで即座に状況を動画や画像で記録します。モバイル端末であれば、添付ファイルフィールドはカメラの選択ができるので手間がかかりません。少しの手間を減らすことは重要です。アプリには、対処や真因の記述、それらを示す添付ファイルフィールドも設けてトラブルに関連した情報を集約できるようにしておきます。これだけで情報収集とナレッジベースになります。kintoneは検索AIという機能をリリースするようなので、より効果的になるかもしれません。

事態が深刻であったり、シビアな情報収集が必要な場合はプレイバック機能を使用すればよいですが、軽微な障害のためにセットアップするのは手間です。そのような時にインスタントな手段というのは活きてきます。軽微であっても生産を阻害することに変わりないので、何とかしたいはずです。そして、そのためには現象としての障害と対処の効果についての情報収集が必要です。

Sysmacプロジェクト

Sysmacプロジェクトとライブラリは以下にあります。

https://github.com/kmu2030/KintoneRestApiClientServiceLib

サンプルプロジェクトの使い方

サンプルプロジェクトの実行には、kintone、サンプルプロジェクト、コントローラのそれぞれについて作業が必要です。kintoneはスタンダードコース以上か、開発者ライセンスでの使用が前提です。いずれも無い場合、kintone開発者ライセンスを取得します。

サンプルプロジェクトは、以下のアプリと連携します。いずれのkintoneアプリもリポジトリにテンプレートがあります。

  • トラブル収集アプリ
    トラブル発生時にコントローラからkintoneアプリにトラブル情報を記録します。
  • 生産モニタアプリ
    一定間隔で生産情報をkintoneアプリに記録します。
  • 生産タスクアプリ
    kintoneアプリの情報をポーリングして、未処理の生産タスクがあれば一定時間後に生産したものとしてレコードを更新します。

サンプルプロジェクトの使用には、最終的に以下の情報が必要です。

  • kintoneのサブドメイン
  • kintoneアプリごとのアプリIDとAPIトークン
  • 使用コントローラの型式
  • コントローラをインターネット接続するための設定
  • コントローラのセキュアソケットのセッションNo

作業は以下の手順で行います。

  1. kintoneアプリを作成
  2. kintoneアプリのAPIトークンを生成
  3. サンプルプロジェクトを使用環境に合わせる
  4. サンプルプロジェクトのAPIクライアントサービスの設定を変更
  5. コントローラのセキュアソケット設定にTLSセッションを登録
  6. コントローラにサンプルプロジェクトを転送

1. kintoneアプリを作成

リポジトリにアプリテンプレートがあるので、それを使用してアプリを作成します。テンプレートからのアプリ作成は、以下を参考にします。

テンプレートファイルは、リポジトリのexample-app-templates.zipです。

2. kintoneアプリのAPIトークンを生成

アプリごとにAPIトークンを生成します。APIトークンの生成については、以下を参考にします。

各アプリが必要とするアクセス権限は以下です。

アプリ アクセス権限
トラブル収集 レコード追加
生産モニタ レコード追加
生産タスク レコード閲覧, レコード編集

APIトークンを生成したら、アプリIDと合わせて控えておきます。アプリIDは、APIトークン生成ページであれば、ページのURLに以下のように含まれています。

https://{サブドメイン}.cybozu.com/k/admin/app/apitoken?app={アプリID}

3. サンプルプロジェクトを使用環境に合わせる

サンプルプロジェクトを使用環境に合わせます。以下の変更が必要です。

  • コントローラの型式
    使用するコントローラの型式に変更します。
  • コントローラのネットワーク設定
    使用環境でインターネット接続可能な設定とします。DNSは特別な理由が無ければ、"1.1.1.1"のようなパブリックDNSを使用します。
4. サンプルプロジェクトのAPIクライアントサービスの設定を変更

POU/プログラム/KintoneRestApiClientServiceRunnerを編集します。各RegisterKintoneAppの引数を作成したkintoneアプリに合わせます。kintoneのサブドメイン、アプリID、APIトークンを変更します。YOUR_KINTONE_SUBDOMAINをログイン時に求められるサブドメインで、YOUR_APP_ID を2で控えたアプリID、 YOUR_APP_API_TOKEN をAPIトークンで置き換えます。

POU/プログラム/KintoneRestApiClientServiceRunner
// トラブル収集アプリの登録
RegisterKintoneApp(
    // Nameは、プログラム内でアプリ情報を取得するキーとなる。
    Name:='TroubleCollection',
    // Subdomainは、kintoneで割り当てられたサブドメイン。
    Subdomain:='YOUR_KINTONE_SUBDOMAIN',
    // AppIdは、作成したアプリのID。
    AppId:='YOUR_APP_ID',
    // アプリで生成したAPIトークン。
    ApiToken:='YOUR_APP_API_TOKEN');

上記を使用するアプリ全てについて行います。TLSセッションは、変更の理由が無ければ以下のままにします。

POU/プログラム/KintoneRestApiClientServiceRunner
RegisterUnrestrictedTlsSession(
    TlsSessionName:='TLSSession0');
RegisterUnrestrictedTlsSession(
    TlsSessionName:='TLSSession1');

5. コントローラのセキュアソケット設定にTLSセッションを登録

コントローラに接続、プログラムモードに変更してセキュアソケット設定に4で指定したNoと同じNoのセッションを登録します。変更していなければ、IDが0と1のセッションを作成します。以下のように作業します。
セキュアソケット設定でのTLSセッションの登録
セキュアソケット設定でのTLSセッションの登録

6. コントローラにサンプルプロジェクトを転送

コントローラにサンプルプロジェクトを転送し、運転モードに切り替えます。ネットワークエラーが発生していないか確認します。ネットワークエラーが発生している場合、エラー原因を取り除きます。

エラーが発生した場合、以下の可能性があります。

  • TLSセッションIDとTLSセッション名の不一致  
    4で指定したTLSセッション名の番号と5で指定したTLSセッションIDが一致していることを確認します。
  • サブドメインの不一致
    4で指定した登録したサブドメインが一致していることを確認します。
  • APIトークンの不一致あるいは、アクセス権限の不足
    4で指定したAPIトークンがkintoneで生成したAPIトークンに一致ていること、APIトークンのアクセス権限に不備がないことを確認します。
  • インターネットに接続できないか、名前解決ができない
    kintoneドメインまでのルートをtracertで確認します。
  • kintoneで障害が発生している  
    cybozu.com 稼働状況を確認します。

サンプルプロジェクトの実行

デモのkintoneアプリとプログラムは以下のように対応します。

アプリ POU/プログラム
トラブル収集 TroubleCollection_FetchDemo
生産モニタ ProductionMonitor_FetchDemo
生産タスク ProductionTaskWatcher_FetchDemo

生産タスク以外は、プログラムが動作すれば勝手にレコードを登録し始めます。生産タスクは、kintoneアプリでレコードを登録しておく必要があります。

トラブル収集

トラブル収集は、イベント発生に起因するkintoneへのレコード登録をテストします。動作に問題がなければ、以下のようにkintoneにレコードを登録します。

トラブル収集アプリのトラブル一覧画面
トラブル収集アプリのトラブル一覧画面

生産モニタ

生産モニタは、継続した一定間隔でのkintoneへのレコード登録をテストします。動作に問題がなければ、以下のようにkintoneにレコードを登録します。

生産モニタアプリの生産情報一覧画面
生産モニタアプリの生産情報一覧画面

分かりにくいので、Vue3とvue-chartjsでグラフにすると以下のようになります。1分毎、72時間のデータです。このグラフはテンプレートファイルにはありません。
生産モニタアプリの生産グラフ画面
生産モニタアプリの生産グラフ画面

グラフは、"Visual Studio Code Live Server Extensionを使ってkintoneカスタマイズ開発効率をあげよう!" を試す目的で作成しました。mkcertはホスト側になりますが、それ以外はWSL上に構築して配信しました。Live Serverの設定は異なりますが問題ありませんでした。ローカルCAをホストにインストールしたことは記録しておく必要があると思います。

生産タスク

生産タスクは、他の二つと異なり、レコード取得レコード更新をテストします。レコード取得は、クエリを使用するので複数レコード取得です。外部からの入力は注意が必要です。デモプログラムでは行っていませんが、十分なバリデーションを行ってください。また、レコードの更新はデータ設計を工夫して関連レコードの作成で同様の目的を達成できないか検討したほうがよいと思います。

生産タスクは、kintoneアプリにレコードを登録しないと何も起こりません。まず、以下のように適当なレコードを登録します。レコード登録とプログラムの動作状態は関係しません。

生産タスクアプリのタスク登録画面
生産タスクアプリのタスク登録画面

上記の操作を繰り返して以下のようにいくつかのレコードを登録して放置します。

生産タスクアプリのタスク一覧画面
生産タスクアプリのタスク一覧画面

しばらくしてからkintoneアプリを確認すると、以下のようにレコードの更新を確認できます。

タスクが処理された生産タスクアプリのタスク一覧画面
タスクが処理された生産タスクアプリのタスク一覧画面

APIクライアントサービスの構成と機能

APIクライアントサービスは、kintoneアプリのレコード操作に集中します。レコードの登録、更新、取得操作のPOUを提供し、レコード削除操作のPOUは提供しません。また、複数のAPIクライアントサービスを同時に使用する必要はないので、ユーザー向けにはシングルトンとして扱いやすくしました。単体のAPIクライアントサービスでも、最大で32までのkintoneアプリを登録でき、16のタスクキューと最大で8のワーカーを同時に実行できるためです。ユーザーには、以下のPOUを提供します。

  • APIクライアントサービスをシングルトン実行するためのFB
  • レコードCRU(D)操作のためのHTTPリクエストを作成するヘルパーFUN
  • タスク操作FUN
  • サービス制御FUN

また、API呼び出しのための一連のタスク操作をFetch(フェッチ)という単位にまとめ、簡潔に使用するためのPOUを提供します。Fetchを使用すると以下のようにコードが簡潔になり、意図を把握しやすくなります。通常はこちらのPOUの使用を推奨します。

NEW_FETCH:
    GenerateRecord(
        Record:=iRecord,
        Head:=0,
        Size=>iRecordSize,
        ...);
    KintoneRestApi_newCreateRecordFetch(
        Context:=iFetchContext,
        AppName:=KINTONE_APP_NAME,
        Record:=iRecord,
        RecordHead:=0,
        RecordSize:=iRecordSize);
    
    iState := DO_FETCH;
DO_FETCH:
    CASE KintoneRestApi_fetch(iFetchContext) OF
        ATS_RESOLVED:
            iState := iReturnState;
        ATS_REJECTED:
            KintoneRestApiFetch_getError(
                Context:=iFetchContext,
                Error=>iError,
                ErrorID=>iErrorID,
                ErrorIDEx=>iErrorIDEx);
            
            iState := iReturnState;
    END_CASE;

タスク操作によるAPI呼び出しは、以下のようになります。タスク操作が前面に出てくるので、API呼び出しの処理手続きが強調されます。

NEW_API_CALL_TASK:
    IF NewKintoneRestApiCallTask(iCallTask) THEN
        GetKintoneAppInfo(
            Name:=KINTONE_APP_NAME,
            AppInfo:=iAppInfo);
        GenerateRecord(
            Record:=iRecord,
            Head:=0,
            Size=>iRecordSize,
            ...);
        KintoneRestApiRequest_createRecord(
            Request:=iRequest,
            AppInfo:=iAppInfo,
            Record:=iRecord,
            Head:=0,
            Size:=iRecordSize);
        InvokeKintoneRestApiCallTask(
            CallTask:=iCallTask,
            AppInfo:=iAppInfo,
            Request:=iRequest);
            
        iState := WATCH_API_CALL_TASK
    END_IF;
WATCH_API_CALL_TASK:
    CASE WaitKintoneRestApiCallTask(iCallTask) OF
        ATS_RESOLVED:
            DoneKintoneRestApiCallTask(iCallTask);
            iState := iReturnState;
        ATS_REJECTED:
            GetKintoneRestApiCallResponse(
                CallTask:=iCallTask,
                Response:=iResponse);
            GetKintoneRestApiCallError(
                CallTask:=iCallTask,
                Error=>iError,
                ErrorID=>iErrorID,
                ErrorIDEx=>iErrorIDEx);
                
            DoneKintoneRestApiCallTask(iCallTask);
            iState := iReturnState;
    END_CASE;

APIクライアントサービスサービスは、以下を機能として提供しません。

  • ロギング
  • フォールバック
  • フェイルセーフ

いずれも、要件によって扱いが異なるので機能として提供しません。ロギングについては、試験運用であればSDカードへの簡単なロギングで十分ですが、期間がある場合にはローリングや分割が必要かもしれません。

フォールバックは本運用であれば必須です。試験運用でフォールバックが必要かどうかは、kintoneの定期メンテナンスが問題になるかどうかです。簡単な対策は、kintoneのサービス停止中、SDカードにkintoneにインポートできるCSV形式で蓄積し、kintoneが使用可能になった時点で手動でkintoneアプリにインポートすることです。

フェイルセーフは、何らかの理由(ネットワーク障害、kintoneサービス停止、コントローラ障害等)でkintoneにアクセスできなくなったとしてもAPIクライアントサービスは処理を停止しないということです。API呼び出し要求を停止するかどうかの判断はユーザーにあります。通常はkintoneにアクセスできない状態でAPI呼び出し要求を行っても失敗するだけです。通常生じないコントローラ障害等の場合、単に失敗するだけである可能性が高いですが何が起こるのかを確認していません。

APIクライアントサービスは主に以下の機能を有し、これらを使用してユーザーにkintone REST API呼び出し機能を提供します。いずれもユーザーがその処理を意識する必要はありません。ユーザーはAPI呼び出しを要求し、成功と失敗のいずれかを受け取るだけです。そのように扱えることを設計上の要求にしています。

  • タスクリソース管理
  • タスクキュー監視
  • TLSセッションリソース管理
  • ワーカー管理

タスクリソース管理

kintone REST API呼び出しはタスクとして管理します。実行に必要とするメモリを構造体として定義し、タスクの実体とします。動的なメモリ確保はできないので、構造体の固定長配列でリソースプールを作成して再利用します。

リソースプールのデータ構造は双方向リストですが、少数リソース(256個まで)を管理するためのライブラリ(ResourceIndexManagerLib)を使用します。同ライブラリは、双方向リストとして扱いたい要素数が256までの固定長配列に対して、インデックスの双方向リストを構築します。また、同ライブラリは双方向インデックスリストを作成するライブラリ(DoublyLindexIndexListLib)に依存します。これは、要素数が65535までの固定長配列についてインデックスの双方向リストを構築します。ResourceIndexManagerは、DoublyLinkedIndexListにいくつかの機能を付加したものです。

タスクキュー監視

ユーザーからのAPI呼び出し要求は、キューとして管理します。API呼び出し処理の開始順はキューに入力した順ですが、完了は処理が終わった順です。タスクキューもResourceIndexManagerLibを使用します。ライブラリには、双方向リストに対するキュー操作に加え、最大で8のサブリストを作成する機能があります。ゲーム開発のECSの考え方を参考にしています。

タスクを単一リストで管理し、タスク状態をタスクリソースにフラグを保持させて管理すると、タスク監視のイテレーションでの無駄が増えるか、無駄を避けようと全ての処理を包含する巨大なPOUを作ることになります。また、マルチタスクな処理を許容する際の悲観的排他制御(Lock/Unlockを使用)の副作用が大きくなり、意図せずタスクタイムオーバーをする可能性が高くなります。

タスク状態ごとにサブリストを作成することで、タスク状態に対する処理分岐の削減、タスク状態によるイテレート頻度の変更、排他制御方法の選択と悲観的排他制御の副作用軽減といった効用を得られます。しかし、最大の効用は処理がシンプルになることです。いずれのサブリストも同一のタスク状態にあるため、素朴なフラグ管理である場合に生じる不格好な条件分岐によるロジックの散在を避けることができます。

不格好な条件分岐はロジックの可読性を低下させるだけではありません。テストパスが確実に増えるので必要とするテストが増加し、テストされない組み合わせが生じた時点でテスト結果に対する信頼性が揺らぎ始めます。実際には、使用にあたって可能性のあるテストケースを網羅するはずなので、問題として露呈する可能性は小さいと思いますが、実装に依存したテストケースや、意図の分かりにくいテストケースが増えることになります。

TLSセッションリソース管理

TLSセッションもタスクリソース同様に有限であり、リソースプールを作成して再利用します。ResourceIndexManagerを使用します。ワーカーは必ずTLSセッションを必要とするのでワーカーにTLSセッションを割り当て、ワーカーの管理だけで済ませることもできましたが、別途管理することにしました。TLSセッションの管理は、平文のTCPとUDPを使用しなければ実質的にソケットの同時使用数を管理することになります。

TLSセッションは、kintone REST APIの呼び出しに限ったリソースではありません。例えば、SlackPostサービス、MQTTSのMQTTクライアントを同時に使用する場合、TLSセッションは競合し得るリソースです。もちろん、コントローラに設定できるTLSセッション数は余裕があるのでコントローラに必要とするだけのTLSセッションを登録することはできます。

NX1及びNX5ではUDPとTCP合計で使用可能なソケット数が60、ソケット命令の同時実行可能数は64です。しかし、ソケット命令の定義可能数は64に限ったものではありません。任意数のアプリケーションについて、複数コネクションの同時使用を可能とするのであれば、いずれの上限も超える可能性があることになります。ソケット命令のエラー捕捉とインターバルによる再試行で対応することもできますが、高頻度に大量のデータを送受信するアプリケーションがあると、リソース取得がレースになる可能性があります。レースになった場合の問題は、リソースの取得に時間がかかることではなく、動作が不安定であるように見える振る舞いをすることです。結局は、アプリケーション間で協調するための機構を必要とすることになります。

TLSセッションをリソースとして管理するのは、ソケットの同時使用数について全体としては、楽観的なリソース管理で十分であってもその上限を設けたいというのもあります。スペックの限界近辺は、未確認の問題が生じやすい領域なので極力避けたいからです。また、ソケットを60まで使えるとあっても、ソケット当たりの帯域はソケット数が1である時と60である時は同じではありません。これは、高頻度に大量のデータを送受信するアプリケーションにとっては致命的です。ソケットの同時使用数の上限をソケット当たりの帯域が許容できる下限未満とすることで避けることができます。これらを間接的に行うためにTLSセッションをリソースとして管理するという理由もあります。

ワーカー管理

ワーカー管理は、ワーカーFB(KintoneRestApiCallWorker)の実行管理です。ワーカーは、kintone REST APIの呼び出し処理を行います。kinone REST APIを使用するだけであれば、このワーカーだけで十分です。APIクライアントサービスは、API呼び出し処理とその管理を強く分離し、コードの多くをユーザーの使用負担と制約を減らすことに充てています。

Fetch POU

Fetch POUは、APIクライアントサービスによるkintone REST API呼び出しを簡潔にするための一連のヘルパーPOUです。各操作はAPI呼び出し単位であるFetchに関連する要素を集約したKintoneRestApiFetchContext構造体を介して行います。Fetchを使用したAPI呼び出し手続きは以下です。

  1. Fetchを生成する
  2. Fetchを実行・監視する

この手続きの概形は以下のようになります。

NEW_FETCH:
    GeneratePayload(
        Payload:=iPayload,
        ...);
    KintoneRestApi_newXXXFetch(
        Context:=iFetchContext,
        AppName:=KINTONE_APP_NAME,
        Payload:=iPayload,
        ...);
    
    iState := DO_FETCH;
DO_FETCH:
    CASE KintoneRestApi_fetch(iFetchContext) OF
        ATS_RESOLVED:
            iState := DONE;
        ATS_REJECTED:
            KintoneRestApiFetch_getError(
                Context:=iFetchContext,
                Error=>iError,
                ErrorID=>iErrorID,
                ErrorIDEx=>iErrorIDEx);
            
            iState := DONE;
    END_CASE;

NEW_FETCH節でAPI呼び出しのペイロードを生成し、それを引数としてFetchを生成します。Fetchを生成したら処理をDO_FETCH節に移し、Fetchを毎サイクル監視して成否が返ってきたら完了します。

Fetchの使用は、呼び出すkintone REST APIに対応するFetchの生成から始まります。Fetch生成POUとkintone REST APIの対応は以下です。

POU kintone REST API
KintoneRestApi_newCreateRecordFetch 1件のレコードを登録する
KintoneRestApi_newReadRecordFetch 1件のレコードを取得する
KintoneRestApi_newUpdateRecordFetch 1件のレコードを更新する
KintoneRestApi_newCreateRecordsFetch 複数のレコードを登録する
KintoneRestApi_newReadRecordsFetch 複数のレコードを取得する
KintoneRestApi_newUpdateRecordsFetch 複数のレコードを更新する

いずれのPOUもAPIに対応してバイト列のレコードやクエリ文字列を引数とします。Fetchの生成は、APIクライアントサービスのリソースを消費しません。そのため、フォールバックとして障害時にFetchを蓄積し、復旧したら実行するということもできます。但し、APIクライアントサービスのリロードを行うとそれ以前に生成したFetchは実行しても失敗します。

生成したFetchはそれを実行・監視するPOUに渡して処理を実行します。実行・監視POUは以下です。

  • KintoneRestApi_fetch
    Fetchの状態を戻り値で列挙値(ATS_PENDING : 処理中、ATS_RESOLVED : 成功、ATS_REJECTED : 失敗)として返します。CASE文と合わせて使用します。

  • KintoneRestApi_fetchSignals
    Fetchの状態をBOOL値(Pending : 処理中、Resolved : 成功、Rejected : 失敗)として出力します。LDで使用します。

ユーザーは、これらの実行・制御POUが成否を返すまで実行します。API呼び出し処理はAPIクライアンサービス内の処理となるため、実行・制御POUは各サイクル、あるいは、レスポンスを処理可能なサイクルで1回だけ実行します。単一サイクルで複数回実行しても問題ありませんが、処理時間が短縮されるわけではありません。

実行・監視POUが成否を返すとき、内部的にレスポンスの複製を行います。そのため、他の処理でタスク時間が厳しい場合や、複数のFetchを同時使用してそれらが同時に完了した場合、タスクタイムオーバーとなる可能性があります。Fetchの監視は毎サイクル必要なわけではありません。レスポンスの処理が可能なサイクルで実行すれば十分です。

一度でも実行・監視POUで処理を実行したFetchは、必ず成否を確認する必要があります。実行・監視POUは成否を返す際に、APIクライアントサービスのタスクを解放します。そのため、成否を確認しない限りタスクリソースは占有されたままとなります。任意に実行するPOUでのFetchの使用には注意が必要です。また、Fetchは実行・監視POUが成否を返した時点で再実行可能な状態になります。再実行の意図が無い場合、以後はそのFetchを実行・監視POUで実行しないようにします。

成否が確定したFetchはその内容を取得して処理することになります。以下のPOUでFetchから必要とする値を取得します。

  • KintoneRestApiFetch_getError
    Fetchのエラー情報を取得します。失敗である場合、何らかのエラーが発生しています。エラー値(ErrorID)は以下です。
内容
0x1001 意図しないTCPコネクションの切断。
0x1003 成功ではない(非200番台)のHTTPレスポンスステータスコード。
0x2000 APIクライアントサービスのリロードによるタスク破棄。
0x2001 APIクライアントサービスのシャットダウンによるタスク破棄。
0x2002 kintoneアプリ情報が無効。
その他 内部エラー。
  • KintoneRestApiFetch_getResponse
    kintone REST APIのレスポンスを取得します。レスポンスは以下の構造体です。
メンバ データ型 内容
StatusCode UINT HTTPレスポンスステータスコード。
Request ARRAY[0..8191] OF BYTE レスポンスボディ。
Size UINT レスポンスボディのサイズ。
  • KintoneRestApiFetch_getRequest
    kintone REST APIを呼び出したリクエストを取得します。リクエストは以下の構造体です。
メンバ データ型 内容
Url STRING[256] kintone REST APIのエンドポイント。
HttpMethod STRING[8] HTTPリクエストのメソッド。
Request ARRAY[0..8191] OF BYTE リクエストボディ。
Size UINT リクエストボディのサイズ。

最後にFetchの状態遷移です。Fetchの状態は、以下のように遷移します。

実装

APIクライアントサービスは、kintone REST API呼び出しを処理するKintoneRestApiCallWorker FB、各機能を使用してサービスを提供するKintoneRestApiClientService FB、サービスをシングルトンとしてユーザーに提供するKintoneRestApiClientSingletonService FBの3つが主要なPOUです。このうちKintoneRestApiCallWorker FBだけ確認します。これを確認するのは、kintone REST API呼び出しがシンプルなHTTPリクエストであることを確認するためです。

KintoneRestApiCallWorker FB

どかかで見たような構造です。実際、殆どSlackPostサービスの使いまわしです。HTTPメソッドの指定は手抜きをしました。サイボウズ社には申し訳ないですが、Accept-Encodingはidentityだけです。kintone REST APIがどのようなレスポンスを返すのか確認していた際、Transfer-Encodingがchunkedだったのでヒヤッとしました。使えるかどうかを覚えていなかったからです。

POU/ファンクションブロック/KintoneRestApiCallWorker
Done := FALSE;
IF Execute AND NOT Busy THEN
    Busy := TRUE;
    
    iState := STATE_INIT;
END_IF;

CASE iState OF
    // STATE_INIT
    0:
        Clear(iError);
        Clear(iErrorID);
        Clear(iErrorIDEx);
        iActiveSettings := Settings;
        iTlsSession := TlsSession;
        TlsSession_getSessionName(
            TlsSession:=iTlsSession,
            SessionName=>iTlsSessionName);
    
        iHttpClientService.Enable := FALSE;
        
        Inc(iState);
    1:
        IF NOT iHttpClientService.Busy THEN
            InitHttpClientService(
                Context:=iHttpClientContext,
                TLSSessionName:=iTlsSessionName);

            iHttpClientService.Enable := TRUE;
            
            Inc(iState);
        END_IF;
    2:
        IF iHttpClientService.Busy THEN
            iTransState := STATE_SHUTDOWN;
            iState := STATE_CALL_API;
        END_IF;
        
    // STATE_CALL_API
    100:
        IF NewHttpClientTask(
               Context:=iHttpClientContext,
               ClientTask:=iHttpClientTask)
        THEN
            HttpPost(Context:=iHttpClientContext,
                     Url:=CallContext.Request.Url,
                     KeepAlive:=FALSE,
                     ClientTask:=iHttpClientTask);
            SetHttpHeader(Context:=iHttpClientContext,
                          Key:='X-Cybozu-API-Token',
                          Value:=iActiveSettings.Credential.ApiToken);
            IF CallContext.Request.HttpMethod <> 'POST' THEN
                SetHttpHeader(Context:=iHttpClientContext,
                              Key:='X-HTTP-Method-Override',
                              Value:=CallContext.Request.HttpMethod);
            END_IF;
            SetHttpHeader(Context:=iHttpClientContext,
                          Key:='User-Agent',
                          Value:=USER_AGENT);
            SetContent(Context:=iHttpClientContext,
                       ContentType:='application/json; charset=utf-8',
                       ContentLength:=CallContext.Request.Size,
                       Content:=CallContext.Request.Request,
                       Head:=0,
                       Size:=CallContext.Request.Size);

            InvokeHttpClientTask(Context:=iHttpClientContext,
                                 ClientTask:=iHttpClientTask);
            
            Inc(iState);
        END_IF;
    101:
        CASE HttpClientTaskState(Context:=iHttpClientContext,
                                 ClientTask:=iHttpClientTask) OF
            HTTP_CLIENT_TASK_STATE_CLOSED:
                // Unintended closing.
                iError := TRUE;
                iErrorID := ERROR_UNINTENDED_CLOSING;
                iErrorIDEx := SHL(UINT_TO_DWORD(iState), 16) OR TO_DWORD(iErrorID);
            
                iState := iState + 8;
            HTTP_CLIENT_TASK_STATE_RESPOND:
                iStatusCode := GetStatusCode(Context:=iHttpClientContext);
                CASE iStatusCode OF
                    // Unintended status.
                    0..199,300..999:
                        iError := TRUE;
                        iErrorID := ERROR_UNINTENDED_RESPONSE_STATUS;
                        iErrorIDEx := SHL(UINT_TO_DWORD(iState), 16) OR TO_DWORD(iErrorID);
                END_CASE;
                CallContext.Response.StatusCode := iStatusCode;
                GetContent(Context:=iHttpClientContext,
                           Out:=CallContext.Response.Response,
                           Head:=0,
                           Size=>CallContext.Response.Size);

                iState := iState + 8;
            HTTP_CLIENT_TASK_STATE_ERROR:
                GetHttpClientError(Context:=iHttpClientContext,
                                   Error=>iError,
                                   ErrorID=>iErrorID,
                                   ErrorIDEx=>iErrorIDEx);

                iState := iState + 8;
        END_CASE;
    109:
        // NOTE: Heavy processing.
        ClearRequest(iHttpClientContext);
        Clear(iActiveSettings);
        
        iState := iTransState;
    
    // STATE_SHUTDOWN
    900:
        iHttpClientService.Enable := FALSE;
    
        Inc(iState);
    901:
        IF NOT iHttpClientService.Busy THEN
            iState := iState + 8;
        END_IF;
    909:
        iState := STATE_DONE;
    
    // STATE_DONE
    1000:
        CallContext.Error := iError;
        CallContext.ErrorID := iErrorID;
        CallContext.ErrorIDEx := iErrorIDEx;
    
        Busy := FALSE;
        Done := TRUE;
        
        Inc(iState);
END_CASE;

iHttpClientService(Context:=iHttpClientCont);

既存プロジェクトで使用するには

既存プロジェクトでAPIクライアントサービスを使用する手順は以下です。ライブラリ操作を伴うため、必ず既存プロジェクトのコピーを作成します。

  1. プロジェクトでAPIクライアントサービスのライブラリと依存ライブラリを参照
    リポジトリのlib/が依存ライブラリです。
  2. サービス用変数をグローバル変数に登録
    gKintoneRestApiClientServiceSingleton : KintoneRestApiClientServiceSingletonContextを定義します。
  3. サービスランナー(KintoneRestApiClientSingletonService FBを実行するプログラム)の作成
    サンプルプロジェクトを参考にしてください。
  4. サービスランナーをタスクに追加
    サービスランナーは、API呼び出し処理を実行するので、適切なタスク時間のタスクに登録します。プライマリタスクは不適です。
  5. ビルドしてエラーが無いことを確認
    ライブラリの不足やグローバル変数定義に誤りがあればエラーが出ます。
  6. メモリ使用状況を確認
    メモリ使用状況に変更が反映されているか確認します。

使用環境が整ったらAPIクライアントサービスを使用するプログラム、kintoneアプリを作成し、サービスランナーに必要な情報を記述します。大きなプロジェクトに対してこれらの操作を行うと時間がかかります。可能であれば小さなプロジェクトで必要な機能を開発し、動作テストまで済ませてから必要とするプロジェクトに統合することを検討してください。

実際に使用するには

以下を考慮してください。

チームで取り組む

kintone、Sysmac Studio、場合によってVSCodeとツールと頭を切り替えながらの作業は心地よいものではありません。また、人によって得手不得手や好みはあると思いますが、特定の人に負荷が集中しないようにする必要があります。Sysmac Studioは特殊かもしれませんが、それ以外、特にkintoneについては誰でも一通りの作業が行えるとよいと思います。

kintoneについて理解する

まずはkintoneの外部連携に関連する事項を確認してください。

簡単なプログラムから始める

まずはTLSセッションを一つだけ登録し、手動の単発でAPI呼び出しを行うようなプログラムから始めてください。APIクライアントサービスはAPI呼び出しのレートリミット等の保護機能を実装していません。意図せず大量のリクエストを行うと、それらを実行しようとします。リソースが枯渇するような方向であれば実質的に停止するだけですが、リソースをフルに使用して持ちこたえるような状態に落ち着くとkintoneに対して短期間に大量のリクエストを行う可能性があります。

ロギングをする

簡単な方法でよいので、動作確認や監査のためにログを残すようにします。ロギングの手段は要件や用途に合わせて選択し、ログは機微な情報を含まないようにします。これは、シークレット情報を含まないというような単一ログに限るものではありません。例えば、量があることで十分に有意味で機微な情報になるのであれば、過度の蓄積を防ぐことも含みます。

フォールバックの実装と対応を策定する

システムは止まります。止まっても損失を許容できる程度に軽減する必要があります。kintoneは定期的メンテナンスがあるので必須です。定期メンテナンスでシステムが止まることをデメリットと考える必要はありません。フォールバックの定期検査です。システムが止まっても損失が生じないのであれば、フォールバックは必要ありません。

まとめ

簡単な治具を作成して作業の負担を軽減するように、アプリケーションを展開して負担を軽減できたらどうでしょうか。目的に特化したアプリケーションを迅速に展開でき、弾力的な運用ができるのであればそれが可能です。kintoneを対象にしましたが、同様の運用ができるのであれば、他のサービスでもコンテナ上のフルスクラッチでも構いません。

kintoneを対象としたのは、技術的な内容が控えめで基本的なアプリケーションの展開に必要な要素が少ないからです。そして、その軽快さを活かすためにはコントローラから直接、簡潔にアクセスすることができ、かつ、柔軟なデータ生成とアクセス制御のためにコードとして展開できる必要がありました。MQTTによるメッセージ基盤が整備されているのであれば、n8nでkintoneにブリッジが良いかもしれません。n8nは他の用途にも使えます。

kintoneはアプリケーションのライフサイクルを考えた場合、必要とする作業が簡単です。ライフサイクルではアプリケーションの作りやすさ、展開のしやすさ同様、終いやすさも重要です。不要になったアプリケーションは蓄積したデータを含め簡単に破棄したいのです。不必要なデータの保持は、インシデント時に不注意で済まされるものではありません。

今回のような内容になってくると、実機デモのような実演でなければ確認が大変かもしれません。機会があれば実機デモもできないことはないので、興味があればXにでもご連絡ください。

Discussion