シャドウモジュールとグレーディングから本番への移行
シャドウ = 同じモジュールの新しいバージョン。これは、独立した所有者(例: roll_shadow)として、本番の旧バージョンと並存して試運転されます。シャドウは本物のイベントのコピーを受け取り、出力はトラッキングされ、記録されるだけで、実際には発行されません。shadow_diff で2つのバージョンの動作を比較し、問題がないことを確認した後、promote でワンクリックで本番に移行し、dismiss でいつでも取り消すことができます。モジュールのコードは一切変更せず、実行時 API によって駆動されます。load / unload / reload と同様の運用アクションで、ダッシュボードやカスタム管理モジュールから直接呼び出すだけで、設定項目を書く必要はありません。
{!--< tips >!--}
- 起動:
await sdk.module.shadow_start("roll", source="v2のパス")—— 新しいバージョンのコードは、独立した所有者(デフォルトはパス名)として、旧バージョンと並存します。 - シャドウは本物の配布や依存関係図に参加しません。同名のコマンドはシャドウディレクトリに進み、ルーティングは登録されるだけ、マウントされません。ライフサイクルのブロードキャストは静かに処理され、
module.callと依存解析は引き続き v1 を指します。 - 本番への移行は常に人間による確認が必要です:
await sdk.module.promote_shadow("roll")。失敗した場合は、自動的に旧インスタンスにロールバックしてサービスを継続します。dismiss_shadowでいつでもシャドウを放棄できます。 {!--< /tips >!--}
快速上手
# v2 代码:通常のモジュール書き方、シャドウを意識しない(任意のディレクトリ、例:downloads/roll_v2/)
# オンラインロボット環境で(ダッシュボード / 管理モジュールから呼び出し)、一回のコマンドでグレーディングを開始:
await sdk.module.shadow_start("roll", source="downloads/roll_v2")
# → シャドウは独立した所有者 "roll_v2" として v1 と並存し、出力はブロックされ、記録される
# 試運転期間中の挙動比較:
report = sdk.module.shadow_diff("roll")
# {"shadow_owner": "roll_v2", "count": 3, "aligned": [...]}
- v1 が実際に送信した内容:受信箱(transcript)からの bot 時系列
- v2 が意図した送信内容:シャドウの帳簿(出力ゲートで記録された「何を送信しようとしていたか」)
- 両者は
trace_idで対応している — 同じメッセージについて、2つのバージョンそれぞれが何をトリガーしたか、送信したか、送信しなかったかが一目瞭然
問題がなければ、正式に転換します:
await sdk.module.promote_shadow("roll") # 転換、失敗時は自動的に v1 にロールバック
await sdk.module.dismiss_shadow("roll") # または:シャドウを放棄
五つの隔離ゲート
| ゲート | 機制 |
|---|---|
| 事件コピー | シャドウプロセッサは独立したイベントのコピー(shadow マーク付き)を受け取ります。シャドウでの変更 / 所有権取得 / 伝播停止はコピーにのみ影響し、元のイベントチェーンには影響しません。 |
| 出力ゲート | シャドウの Send DSL と Api 呼び出しはすべて記録され、成功した仮のレスポンスが返されます。実際には送信されません。シャドウは重複して返信しません。 |
| ストレージ上書き層 | シャドウの KV 書き込みはメモリ上の上書き層に入り、永続化は行われません。読み込みは上書き層を優先し、ヒットしなければ本物のストレージを参照します(グレーディングは実際のデータで実行)。削除は墓石として記録されます。 |
| ルーティング遮断 | シャドウの HTTP/WS/SSE ルーティングは登録のみでマウントされません。同名のコマンドはシャドウのコマンドディレクトリに登録され、プラットフォームイベントメソッドの注入は禁止されます。 |
| ライフサイクル静音 | シャドウは自身のライフサイクルイベントをブロードキャストせず、エコシステムの依存グラフにも参加しません(module.call と依存解析は v1 を参照し続けます。半完成品が依存されることを避けるため)。 |
構成の継承: シャドウはデフォルトで元のモジュールの構成節を継承します(そうでなければグレーディングの歪みが発生します)。転正後は構成がその場で有効になります。
正直な境界(防ぎきれない)
- フレームワークの送信 / API / KV ストレージ / 統一 HTTP クライアントはすべて防げる;
フレームワークを迂回して
aiohttpを直接起動したり、スレッドで外部システムに書き込んだりする場合——フレームワークは防げない - ORM の読み書きはカバレッジの対象外(行単位のオーバーレイでは SQL 層で綺麗に実現できない)—— 影の期間中は ORM を使っての書き込み隔離に依存しないことを推奨
- 影のソースがローカルパスの場合:新しいコードはパスからインポートされ、独自の所有者でロードされる。同じ PyPI パッケージは、同一の Python 解釈器内では
sys.modulesの単一キー制限により、新旧の2つのバージョンを同時に存在させることはできない - 泄漏監査ツール(
sdk.module.audit)は影のリソースの所有者を確認可能。フレームワークを迂回する副作用は、少なくとも静かに起こることはない
転正とロールバック
promote プロセス:現在のバージョン(および連鎖する依存者を含む)をスナップショット → 完全にアンロード → シャドウを本物の名前で登録
ロード → いずれかのステップで失敗した場合、自動的にロールバックされ、古いインスタンスが引き続きサービスを提供します(ベストエフォートの意味:on_unload で実行された副作用は取り消せません。ロールバック後、古いインスタンスは終了済み状態になります)。転正に成功したシャドウリソースは回収され、バインドが解除されます。元のモジュールの設定セクションはその場で有効になります。
永続化の注意:promote はランタイムでの切り替えです。再起動後も v2 で動作し続けます。新しいバージョンを永続的にインストールする必要があります(pip install -U で新しいバージョンをインストール / プラグインファイルを置き換えます)。ランタイムでの切り替えはパッケージ管理を代行しません。
関連ドキュメント
- 所有権(owner)システム —— シャドウが独立した owner によって隔離されるメカニズムの基礎
- インタラクティブセッション ——
trace_idと受信箱(diff で同期されたデータソース) - スコープ(scope) —— イベントの入出力制御面