NAT の向こうでエージェントを常駐させる
NAT 配下の常駐エージェントを、外向き接続・独立した生存確認・必要時の SSH セッションで設計する方法を説明します。
NAT 配下の常駐エージェントを、外向き接続・独立した生存確認・必要時の SSH セッションで設計する方法を説明します。
複数言語が同居するコードベースで得た 5 つの手法をまとめます。既存の Go コードへの golangci-lint の段階導入、CI でのレースディテクタの常時実行、サブプロセスの寿命の制御、bootout の競合を避けた launchd サービスの再登録、そして外部キーを踏まえた PostgreSQL の行ロック強度の選び方です。
LangChain を使わずに Go で AI エージェント基盤を構築した理由と、クリーンアーキテクチャによってモデル、メモリ、ツール、ストリーミングの各関心事を独立して差し替えられるようにした方法を紹介します。
ブログへようこそ。CS、機械学習、エンジニアリングについてのメモを共有していく場所です。
Wi-Fi、VPN、コンテナのブリッジがあるマシンにはアドレスが複数あり、ホスト名の解決では間違ったものが返ることがあります。UDPソケットを宛先にconnectすると、カーネルがその宛先に使う送信元アドレスを決め、getsockname()でパケットを1つも送らずに読み出せます。Linuxで3つのインターフェースを使って確かめ、5つの宛先に対して5つの答えが得られ、IPv4とARPのフレームは流れませんでした。
稼働中のデーモンをリクエストを落とさずに入れ替える手順書です。小さなTCPサーバーを本物のsystemdで動かして試しました。単純なrestartは毎回、接続の拒否かリセットを起こしました。SO_REUSEPORTで新旧を並べる方法は拒否ゼロでしたが、一部の回でリセットが出て、カーネルの設定でそれが消えました。ソケットアクティベーションはエラーなしでした。処理中リクエストのドレインとTimeoutStopSecの関係、新バージョンを確認して巻き戻すリリース切り替えも扱います。
OTA アップデートは、変更できないネイティブのバイナリを呼び出す JavaScript です。ランタイムバージョンは、そのバイナリの ABI バージョンだと考えられます。@expo/fingerprint 0.20.13 を使った実験で、どの編集がハッシュを変え(extra、version、ビルド番号、ネイティブモジュール)、どれが変えないか(JavaScript、純 JS の依存)を確かめ、既定値を黙って落とすスキップリストの落とし穴も見ます。
自分自身を更新するデーモンが、更新用のヘルパーを起動してから自分のユニットを再起動すると、落とし穴があります。ヘルパーはサービスのcgroupの中で起動し、再起動時にsystemdがそのcgroupの中身を全部終了させるからです。使い捨てのsystemdで試すと、setsidしたヘルパーは最後のログ行を出す前に消え、KillMode=processでは生き残るものの次のインスタンスに紛れ込み、systemd-runなら独立したユニットとして最後まで動きました。証拠、直し方、試していないことを書きます。
ウェブサイトと React Native アプリで共有するクライアントライブラリが壊れるのは、fetch ではなく、Web プラットフォームの地味な部分であることが多いです。React Native 0.87.1 と Expo 57 のソースを読むと、ランタイムが3層あること、ウェブなら ‘/b/x’ になるところを ‘/b/c/d/../x’ と返す URL クラスがあること、Expo が置き換えない限り fetch にレスポンスのストリームがないことが分かります。必要なものを確認する小さなクライアントを、4つの模擬ランタイムでテストします。
Durable Object のアラームは1つだけで、少なくとも1回実行され、失敗するハンドラーはバックオフ付きで最大6回再試行されます。それが尽きると、オブジェクトを再び起こすものは何もありません。この3つに耐えるリース失効の台帳を、コードに注釈をつけて見ていきます。真実を持つ表、アラームを再設定するコンストラクター、2回実行されても困らない副作用です。ローカルの workerd で動かし、制限は Cloudflare の文書から引用しています。
CloudflareのドキュメントはWebSocket Hibernationについて、接続は維持され、メモリ上の状態は破棄される、と書いています。この記事では小さなDurable Objectをローカルのworkerdで動かし、それが具体的に何を意味するかを観察します。ソケットはOPENのまま、次のメッセージでコンストラクターが再実行され、クラスのフィールドは初期化され、アタッチメントはserializeしたものだけが残り、保留中のタイマーや標準のWebSocket APIを使うとハイバネーションがひそかに無効になります。本番の挙動や課金は試していません。
端末がオフラインのとき、アプリが終了されたとき、送信が多すぎるときにプッシュがどうなるかを、AppleとGoogleは文書で説明しています。通知は置き換えられ、捨てられ、順序が入れ替わり、遅れます。文書に書かれた失敗モードの表と、ペイロードを適用するクライアントが収束せず、カーソルから取得するクライアントが収束することを示す小さなシミュレーションを紹介します。
JWT の仕様は、検証側が理解できないクレームは無視するよう定めています。権限を与えるクレームなら無害ですが、制限するクレームでは危険です。クライアントの種類ごとに持てる権限の上限を決める4つの設計を、小さな発行者と3種類の検証側で試し、僕が選ぶ設計を示します。
RFC 9470 は、API がクライアントに「このトークンは、弱すぎる、または古すぎるログインで取得されたものだ」と伝える方法を定めています。RFC を節ごとに読み、試作のサーバーとクライアントを作ると、仕様が決めているもの(401 のチャレンジと2つのパラメーター)と、実装側に残されているもの(同時に届くチャレンジ、ループ、キャッシュ)が分かります。
AppleとAndroidの公式ドキュメントには、バックグラウンドのアプリは一時停止され、ネットワークが止められ、既存の接続が閉じられうると書かれています。つまり、フォアグラウンドに戻ったときの readyState === OPEN は、最後に処理したイベントの記憶にすぎません。この記事では、その記述から「確認する、作り直す、取りこぼしを取り戻す」という小さなルーチンを導き、Linux上の凍結プロセスという代役で動かします。実機では試していない点も、はっきり書きます。
WebSocket のハンドシェイクにある Origin ヘッダーは、ブラウザが付けるもので、それ以外のクライアントは好きな値を送れます。許可リストが守るのは、Cookie 認証のブラウザセッションが他のサイトから使われることで、接続してきた相手が誰かは証明しません。動くサーバー、実ブラウザでの攻撃、そこから導かれるハンドシェイクの規則を示します。
たくさんのリクエストを同時に流している1本の接続で、1つのリクエストの期限が来たとき、接続ごと閉じるべきか、そのリクエストだけを閉じるべきか。小さなNodeの実験では、HTTP/2のセッションを閉じると無関係な2本が失敗し、ストリームだけをキャンセルすると同じTCP接続のまま2本とも完了しました。ただし、TCPの1パケットの遅れは接続上の全ストリームを止めます。その場合に接続の生死を確かめる方法として、PINGを使った確認も試します。
WebSocketのsend()は待ってくれず、bufferedAmountが見ているのは自分のプロセスの中の待ち行列だけです。小さな実験では、bufferedAmountが減るのを待ってから送る送信側でも、受信側のメモリには、400件のほぼ全部が溜まりました。受信側が処理済みを返す「クレジット窓」(8件)なら、溜まるのは8件で止まりました。send()、bufferedAmount、メッセージサイズ、バックプレッシャーについてよくある4つの思い込みを実際に試し、使い分けの表にまとめます。
再接続したクライアントが、状態を取りこぼさず、重複せず、履歴を読み直さずに追いつく方法を、「全部読み直す」から「上限つきのログとスナップショットのフォールバック」まで、版0から版4まで段階的に組み立てます。ブラウザの EventSource が再接続時と non-200 応答のときに実際に何をするか、スナップショットの順序のバグ、決定木も扱います。
ヘッダーと本文を小さな write() 2回に分けて送り、返事を待つと、CPUもネットワークも空いているのに、1往復ごとに約40ms止まることがあります。Nagleアルゴリズムも遅延ACKも、単体では正しい仕様です。2つが組み合わさると、タイマーが切れるまで互いに待ち合ってしまいます。この記事では、停止を予想し、50行のスクリプトで再現し、パケットトレースで確かめ、4つの対処を比べます。
リフレッシュトークンのローテーションは、使用済みトークンの再利用を盗難の警報として扱います。期限切れのアクセストークンで3つのリクエストを同時に送り、401ごとに更新するクライアントは、同じリフレッシュトークンを3回提示して、自分で警報を鳴らします。この記事では、素朴なクライアントをあえて壊し、single-flight と stale check で直します。そのうえで、サーバー側の猶予期間、リクエストの待機、先回り更新、送信者制約付きトークンを比べます。
リクエストがタイムアウトしたとき、クライアントはリクエストが失われたのか、確認応答だけが失われたのかを区別できません。だから「ちょうど1回」の配送は作れず、実際にできるのは「少なくとも1回の配送」と「重複を除く受け手」の組み合わせです。不安定なネットワークでの実験、正しく見えて89件を重複させるバグ、原子的な重複排除ストア、ジッターのシミュレーションで確かめます。
epoll のエッジトリガーは部分的に読むと止まりますが、レベルトリガーは止まりません。同じ違いが、イベントの喪失・重複・順序入れ替えに耐えられるかを分けます。再現できる epoll の実験、損失のあるチャネル上での5通りのシミュレーション、そして見落とされがちな競合(再取得の最中に届いたイベント)を扱う40行ほどの invalidator を紹介します。
TCP は、相手がいなくなったことを教えてくれません。読み取り側は永遠にブロックし、書き込み側は十数分も再送を続け、SO_KEEPALIVE は未確認のデータがあるあいだ何もしません。この記事では、root 不要の1ファイルの Linux 実験環境を作り、何もしない、keepalive、TCP_USER_TIMEOUT、アプリケーションのハートビートという順に仕組みを積み上げて、気づくまでの時間を測ります。NAT と RFC がどこに関わるかも整理します。
101 レスポンスのあと、どのリクエストにも認証情報は載りません。サーバーが覚えているのは証拠ではなく判定結果だけです。1つのトークンをブラウザのハンドシェイクから追いかけ、ページから何が見えて何が見えないかを確かめたうえで、再接続・帯域内(in-band)・帯域外(out-of-band)の3つの更新方法を、動くサーバーとテストとヘッドレス Chrome の確認つきで比べます。
メッセージ単位で課金されるリレー越しに、モバイルクライアントがタイマー駆動のポーリングから共有の変更通知へ移った経緯を扱います。難しいのはストリームそのものではなく、保険のポーリングをいつ止めてよいかを正直に決めることです。
WebSocket のメッセージごとに課金され、フレームにも上限がある環境では、独立したフラッシュ条件を持つバッチ送信、上限より低い単一フレームのしきい値、上限付きの分割、混在バージョンのロールアウトを生き延びる能力フラグが必要です。難所は、終了時の順序と、ワイヤ上から消えるゼロ値です。
一定間隔の keepalive は、接続が忙しいときでも双方向に1メッセージずつ使います。受信したフレームをすべて生存の証拠として扱い、静かな接続だけを確認し、既存のハートビートで待機中のホストに切断してよいかを伝えれば、その大半を減らせます。代償は、上限のある復帰の遅延です。
自分のソケットの close イベントを待ってから後始末するクライアントは、そのイベントが届かないと、黙って止まることがあります。接続を終えると決めた時点で状態を確定させ、後始末を冪等にし、イベントは自分が始めていない切断のために残します。
共同編集の文書同期とスナップショット保存を分離し、初回同期前の空の文書による上書きを防ぐ設計を説明します。
分散タスクの同時実行制御として、作業面ごとの開始許可・投入時の宛先固定・マシン停止時の冪等な再送を設計する方法を説明します。
API の認証とロールベース認可を、主体・経路ごとの権限・人とマシンの認証情報に分けて設計する方法を説明します。
AI エージェントのメモリ設計として、検索した文脈とタスクの正本を分離し、検索インデックスの遅延を考慮して作成・更新を判断する方法を説明します。
会議 AI の処理を文字起こし・要約・タスク抽出に分け、発話の根拠を保持し、会議時刻から期限を解決する設計を説明します。
NAT 配下の常駐エージェントを、外向き接続・独立した生存確認・必要時の SSH セッションで設計する方法を説明します。
複数言語が同居するコードベースで得た 5 つの手法をまとめます。既存の Go コードへの golangci-lint の段階導入、CI でのレースディテクタの常時実行、サブプロセスの寿命の制御、bootout の競合を避けた launchd サービスの再登録、そして外部キーを踏まえた PostgreSQL の行ロック強度の選び方です。
LangChain を使わずに Go で AI エージェント基盤を構築した理由と、クリーンアーキテクチャによってモデル、メモリ、ツール、ストリーミングの各関心事を独立して差し替えられるようにした方法を紹介します。
ブログへようこそ。CS、機械学習、エンジニアリングについてのメモを共有していく場所です。