はじめに
同時処理ができないデバイスなどで排他制御したいことがあります。
簡単なノードを自作すれば実現できますが、ソース管理やインストールが必要なのでそれなりに面倒です。
そこで標準ノードだけで実現する方法を考えました。ただし、以下の制約があります。
- コンテキストストレージは標準のメモリ(ファイルなどでは動かない)
- (無いと思いますが)仕様変更で動かなくなる可能性がある
なぜ公開するか?
コンテキストで実現するなら、context.get()してcontext.set()すれば良いと思われるでしょう。私も試してみましたが、msgが錯綜するような状況では、context.set()のタイミングで別のmsgを受け取ってしまい、うまく排他制御できませんでした。
コンテキストデータの作成
そこで、排他制御時のcontext.set()を無くすべく、functionノードの初期化処理でオブジェクトを作成し、そこに排他制御用のフラグを保存します。
// ここに記述したコードは、ノードをデプロイした時に
// 一度だけ実行されます。
context.set("busy", {"flag": false});
ここでオブジェクトにしているのは、単純な変数だとcontext.get()が値を返すからです。オブジェクトにしておくことでcontext.get()が参照を返すようになり、コンテキスト中のオブジェクトにあるflagを直接更新することができます。
排他制御
上記のfunctionノードのコードに以下のように書きます。
let busy = context.get("busy");
if (msg.reset) {
busy.flag = false;
return; // do nothing
}
if (busy.flag) { // busy
return [null, msg];
}
else {
busy.flag = true; // Don't use context.set()
return [msg, null];
}
context.get("busy")で最初の参照が返ってからreturnまで割り込みが入らないように以下の処理をしています。
- msg.resetがtrueなら排他制御をやめる(後続処理はしない)
- コンテキストのflagがturueならbusyとする
- コンテキストのflagがfalseなら後続の処理をする
これだけです。
サンプルフロー
排他制御したい処理の代わりにdelayノードで実現しました。
最初の要求を処理中に要求するとBUSYになり、最初の処理が終わると処理済みになり、次の処理を受け付けます。
おわりに
いかがでしょう?意外と出番があるのではないでしょうか?
コンテキストがメモリの場合、オブジェクトは参照が渡ることを利用して実現しました。はじめに書いたように、メモリにあるオブジェクトを別の領域にディープコピーして渡すように、context.get()の仕様変更があれば動かくなります。しかし、コンテキストを簡易なデータベースとして使うこともあるので、そのような改悪は無いと信じています(あったらごめんなさい)。
コード
[{"id":"2bd6afffdc5fc0d3","type":"tab","label":"排他制御","disabled":false,"info":"","env":[]},{"id":"a5cfe07370a5047c","type":"inject","z":"2bd6afffdc5fc0d3","name":"要求","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":110,"y":120,"wires":[["d216c55707a86c3d"]]},{"id":"4af94267e0acc87d","type":"debug","z":"2bd6afffdc5fc0d3","name":"処理済み","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":600,"y":100,"wires":[]},{"id":"d216c55707a86c3d","type":"function","z":"2bd6afffdc5fc0d3","name":"排他制御","func":"let busy = context.get(\"busy\");\nif (msg.reset) {\n busy.flag = false;\n return; // do nothing\n}\nif (busy.flag) { // busy\n return [null, msg];\n}\nelse {\n busy.flag = true; // Don't use context.set()\n return [msg, null];\n}","outputs":2,"timeout":0,"noerr":0,"initialize":"// ここに記述したコードは、ノードをデプロイした時に\n// 一度だけ実行されます。\ncontext.set(\"busy\", {\"flag\": false});","finalize":"","libs":[],"x":280,"y":120,"wires":[["652e1f4e2d7e6cc7"],["d7711988b505fc7a"]]},{"id":"d7711988b505fc7a","type":"debug","z":"2bd6afffdc5fc0d3","name":"BUSY","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":590,"y":140,"wires":[]},{"id":"652e1f4e2d7e6cc7","type":"delay","z":"2bd6afffdc5fc0d3","name":"","pauseType":"delay","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":440,"y":100,"wires":[["4af94267e0acc87d","ad9d4acb5b6f6eb6"]]},{"id":"ad9d4acb5b6f6eb6","type":"change","z":"2bd6afffdc5fc0d3","name":"","rules":[{"t":"set","p":"reset","pt":"msg","to":"true","tot":"bool"}],"action":"","property":"","from":"","to":"","reg":false,"x":260,"y":60,"wires":[["d216c55707a86c3d"]]}]