箱庭PDUアーキテクチャ(その3)


前回は、箱庭PDUの責務分離と設計上の境界、
そして実装レベルでのデータの流れについて説明しました。

じつは、これまで箱庭PDUについては、
「ここまで用意するので、あとはお好きに実装してください」
というスタンスを取ってきました。

具体的には、PDUの定義やデータ変換、通信プロトコルの選択については、
箱庭として最低限のデフォルトツール
(データ型変換、バイナリ変換)は用意していましたが、
基本的にはユーザの自由度を最優先する方針を貫いてきました。

自分がツラくなってきました。

そんなカッコイイことを言っておきながら、
いざ Runtime Delegation の実装を本気で考え始めると、
一番ツラくなったのは、他でもない自分自身だったのです。

自由度を高く保つ、という判断そのものは間違っていませんでした。
しかし、その自由度を前提にしたまま、
実装を進めるには限界があることも、はっきりしてきました。

そして。

Runtime Delegation の概念モデルをいろいろと検討していく中で、
どうしても、
PDUの入出力を概念どおりに扱える、
それこそ設定(config)一つで切り替えられる、
そんな通信の「単点(endpoint)」が必要だ、
と気づいたのです。

この「通信の単点」を、明示的な概念として切り出したものが、
hakoniwa-pdu-endpoint
です。

こちらで、その実装を一般公開しています。(まだ変わるかもしれませんが。)

クラス設計もやった。

さて、次はクラス設計の話です。
ここでは、先ほどの図をもとに、PduEndpoint の中身を説明します。

この設計に至るまでに、自分の中で大きな気づきがありました。

最初は「通信」しか見ていなかった

最初に考えていたのは、正直なところ 通信 だけでした。

PDUをどう送るか。
TCPか、UDPか、ROSか、Zenohか。

送信については、それで問題ありません。
しかし、実装を進めるうちに、すぐに気づいたのです。

受信したデータ、どこに置くんだ?

受信は必ず「溜まる」

受信したPDUは、その場で必ず処理できるとは限りません。
つまり、どこかに保持する必要がある

この「保持」をどう扱うかを考えている中で、
ChatGPT との対話も含めて整理していくうちに、
この問題は単なる受信処理ではなく、
キャッシュ(cache)の問題として捉えるのが自然だと気づきました。

こうして生まれたのが PduCache です。

PduCache:Queue と Buffer という選択肢

PduCache は、受信したPDUを一時的に保持するための仕組みです。
そして、その実装として、複数の選択肢を持たせました。

  • Queue
    • FIFOでPDUを保持
    • キュー長制御が可能
    • 非同期処理と相性が良い
  • Buffer
    • 最新データで上書き
    • 状態保持用途に向いている
    • 実装がシンプル

用途に応じて、
「溜めたいのか」「最新だけでいいのか」
を選べるようになっています。

通信の違いは想像以上に大きい

もう一つ大きな分岐点がありました。

通信方式には、大きく2種類あります。

  • Zenoh / ROS
    • トピックのような上位概念を持つ通信
    • PDU単位でのやり取りが前提
  • TCP / UDP
    • コネクションを張って直接I/O
    • バイト列の送受信が基本

この違いは、単なる実装差では済みません。
抽象度がまったく違うのです。

RawPduComm の役割

この差を吸収するために導入したのが RawPduComm です。

RawPduComm は、

  • TCP / UDP / WebSocket のような
  • 「バイト列しか扱えない通信」

を、PDUという上位概念に引き上げるための層です。

そのため、RawPduComm は PduComm を継承しています。

外から見ると、

  • ROS でも
  • Zenoh でも
  • TCP / UDP でも

すべて PduComm として扱える

これが、この設計の肝です。

PduEndpoint は「組み合わせの起点」

最終的に、PduEndpoint は次のような役割を持ちます。

  • 通信するか、キャッシュするかを選ぶ
  • 通信する場合は、PduComm / RawPduComm を切り替える
  • 受信したPDUは、PduCache に流す

PduEndpoint 自体は、

  • 通信方式の違い
  • キューかバッファか
  • 実装の細部

を知りません。

ただ、組み合わせるだけです。

この設計で得られたもの

この構造にしたことで、

  • 通信方式の追加が楽になる
  • 受信処理の整理が一気に進む
  • Runtime Delegation のような複雑な要件にも耐えられる

土台ができました。

ちなみに、PduComm の実装としては、
必ずしも「ネットワーク通信」である必要はありません。

たとえば、

  • デバッグ出力
    • 受信した PDU をそのままログに吐く
  • ストレージ保存
    • PDU をファイルやDBに書き出す
  • テスト用ダミー実装
    • 通信せずに PDU を注入・回収する

といったものも、
PduComm の一実装として自然に組み込めます。

ここで重要なのは

PduEndpoint から見ると、

それが「どこに送られているか」は問題ではない

という点です。

  • ネットワークか
  • ローカルか
  • デバッグか
  • 永続化か

PduEndpoint はそれを知りません。
ただ 「PDUを出す先」 を切り替えているだけです。

最後に。

ここまで細かく説明してきましたが、
実際にユーザが触るのは、もっとシンプルです。

これが PDU エンドポイントの定義です。
箱庭では、これを JSON ファイルで記述します。

endpoint.json

{
    "name": "tcp_server_buffer_endpoint",
    "cache": "cache/buffer.json",
    "comm": "comm/tcp_server_inout_comm.json"
}

cache/buffer.json

{
  "type": "buffer",
  "name": "default_latest_buffer",
  "store": {
    "mode": "latest"
  }
}

comm/tcp_server_inout_comm.json

{
  "protocol": "tcp",
  "name": "tcp_server_inout",
  "direction": "inout",
  "role": "server",
  "local": {
    "address": "0.0.0.0",
    "port": 54001
  },
  "options": {
    "read_timeout_ms": 1000,
    "write_timeout_ms": 1000
  }
}

重要なのは、これらの JSON が 通信処理の実装を一切含んでいない ことです。

  • TCP の socket API は出てこない
  • send / recv の話も出てこない
  • キューかバッファかという「方式」は指定しているが、
    その内部実装は、ここでは意識する必要がありません

これらの設定は、
箱庭PDUにおける一つの「端点(endpoint)」を
定義しているに過ぎないのです。

いやー、これは、われながら、

「楽しい!」

つづく。


コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

PAGE TOP