FAkka.Proc.Supervisor 1.571.101.400

dotnet add package FAkka.Proc.Supervisor --version 1.571.101.400
                    
NuGet\Install-Package FAkka.Proc.Supervisor -Version 1.571.101.400
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="FAkka.Proc.Supervisor" Version="1.571.101.400" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="FAkka.Proc.Supervisor" Version="1.571.101.400" />
                    
Directory.Packages.props
<PackageReference Include="FAkka.Proc.Supervisor" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add FAkka.Proc.Supervisor --version 1.571.101.400
                    
#r "nuget: FAkka.Proc.Supervisor, 1.571.101.400"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package FAkka.Proc.Supervisor@1.571.101.400
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=FAkka.Proc.Supervisor&version=1.571.101.400
                    
Install as a Cake Addin
#tool nuget:?package=FAkka.Proc.Supervisor&version=1.571.101.400
                    
Install as a Cake Tool

Akka.Proc.Supervisor

Akka.Proc.Supervisor 是一個基於 Akka.NET 的進程管理與監督服務,專門用來管理與調度外部進程 (Processes),特別是 F# Interactive (FSI) 節點。它結合了 Akka Cluster Sharding、Quartz.NET 排程與 Suave HTTP REST API,提供了一個高可用、可擴展的分散式進程執行與監控環境。

系統架構

系統主要分為兩種執行模式 (Mode):Supervisor (監督者)ProcNode (進程節點)

1. Supervisor (監督者模式)

負責管理所有衍生的子進程。核心元件包括:

  • ProcRegistryActor: 維護所有子進程的狀態快照 (Snapshot),提供進程列表與狀態查詢。
  • ProcSupervisorActor: 作為對外的唯一入口,將指令路由至對應的 Sharding Region 或 Registry。
  • ProcNodeActor: 這是透過 Akka Cluster Sharding 動態建立的 Actor,每一個 ProcNodeActor 負責一對一管理一個底層的作業系統進程 (OS Process)。它負責:
    • 啟動與停止 System.Diagnostics.Process
    • 監控 stdout / stderr 輸出。
    • 定期 Probe (探測) 子節點的 FSI 服務健康狀態(支援 Quartz Cron 或固定間隔)。
    • 若探測失敗超過閥值 (probeFailureThreshold) 或進程意外崩潰,會自動重啟進程。
    • 作為橋樑,將 FSI 相關的指令 (如 ForwardMessage) 轉發給子節點的 FSI Supervisor。
  • REST API (ProcRest): 提供 HTTP 介面供外部控制進程的啟動、停止與訊息發送。

2. ProcNode (進程節點模式)

被 Supervisor 啟動的子進程。通常是執行同樣的執行檔,但加上 --mode procnode 參數。

  • 啟動時會加入 Akka Cluster。
  • 啟動內建的 Akka.FSI.Supervisor (F# 互動環境監督者)。
  • 啟動成功後,會透過 Actor Selection 回傳 ProcNodeReady 訊息給 Supervisor,告知自身的 PID 與 FSI Actor 路徑。

Journal / Snapshot / DB

Akka.Proc.Supervisor 預設使用 compatibility profile,不建立 durable lifecycle authority;此模式維持 win18 以前的行為。明確選擇 sql-server profile 時,PersistentLifecycleAuthorityActor 會以 Akka.Persistence.Sql 保存 registration receipt、desired state、generation、pending effect、effect result、 stop progress 與 release tombstone。

需要區分兩種「snapshot」:

  • ProcSnapshotProcRegistryActor 內的 live process observation,透過 REST API 查詢;不是 durable authority。
  • ProcLifecycleCheckpointV1:durable lifecycle snapshot;只能由 journal/snapshot recovery 重建,不由 ProcSnapshot 反向覆寫。
  • Akka Cluster Sharding state store:ProcHost.fs 使用 StateStoreMode.DData,不是 SQL journal/snapshot-store。

本專案沒有自訂 CREATE TABLE 語句。SQL table mapping 與 DDL 由 Akka.Persistence.Sql 1.5.67 擁有; default mapping 使用 event journal、tag、optional metadata 與 snapshot store tables。production 可明確啟用 -SqlAutoInitialize,或預先套用該 package 對應 SQL Server provider 的 DDL。完整設定、actor topology 與 failure semantics 見 doc/DurableLifecycle.SDK.md


Generic registered process / pod API(current: 1.569.101.302-win23

RFC-PROC-0005 將通用 process control 與 application command 分開:

  • RegisteredProcessCatalog 是 deployment-time allow-list;caller 只能給 process/pod ID。
  • proc PBM palette 擁有 start/status/stop/restart,不接受 executable、working directory、environment、credential 或 actor path。
  • named pod 只定義 member 與順序;沒有隱含 all-for-one、dependency graph 或 sibling restart。
  • application palette proxy 只傳 bounded opaque command/arguments,application schema 與 human/JSON rendering 由 child router 擁有。
  • LetItGo 只釋放 exact current ProcId + PID 的 ownership且不停止 child。compatibility profile 的 release 只在目前 process lifetime 有效;durable profile 會先 persist entry tombstone 再回覆,restart 後不得 reclaim。

Trusted catalog

Supervisor 以 --process-catalog <absolute-json-path> 載入 version 1 catalog。以下是最小結構:

{
  "version": 1,
  "processes": [
    {
      "processId": "quote-tool",
      "fileName": "C:\\Program Files\\QuoteTool\\QuoteTool.exe",
      "arguments": ["--managed"],
      "workingDirectory": "C:\\Program Files\\QuoteTool",
      "role": null,
      "probeMessage": null,
      "probeCron": null,
      "probeIntervalMs": null,
      "restartPolicy": "bounded-on-failure",
      "maxRestarts": 1,
      "managedStopActorPath": "akka.tcp://QuoteTool@127.0.0.1:9453/user/process-control",
      "readyTimeoutMs": 30000,
      "stopTimeoutMs": 15000
    }
  ],
  "pods": [
    {
      "podId": "quotes",
      "memberIds": ["quote-tool"],
      "startOrder": ["quote-tool"],
      "stopOrder": ["quote-tool"]
    }
  ],
  "applicationRoutes": [
    {
      "podId": "quotes",
      "memberId": "quote-tool",
      "palette": "quote",
      "commands": [{"name": "status", "description": "Application-owned status."}],
      "actorPath": "akka.tcp://QuoteTool@127.0.0.1:9453/user/application-router",
      "timeoutMs": 10000,
      "maxRequestBytes": 32768,
      "maxReplyBytes": 262144,
      "maxConcurrency": 8
    }
  ]
}

Catalog validation fail-closed:ID 不可重複、process executable/working directory 必須是存在的 absolute path、pod order 必須是 member permutation、Akka endpoint 必須是 absolute address,所有 timeout/size/concurrency 必須為正數。catalog 本身不得保存 production secret。

PBM surface

pbm 127.0.0.1:7089 proc start   --id quotes
pbm 127.0.0.1:7089 proc status  --id quotes
pbm 127.0.0.1:7089 proc stop    --id quotes
pbm 127.0.0.1:7089 proc restart --id quote-tool --human
pbm 127.0.0.1:7089 quote status --target quote-tool --arg current --human

proc 預設回 stable JSON;--human 只格式化 generic lifecycle 欄位。application palette 的 repeatable --arg 保持原順序,Supervisor 不解析 payload,也不把 payload 寫進 lifecycle response。

start/stop/restart由control actor的單一FIFO序列化;status是獨立bounded lifecycle read,長時間stop期間仍可 回覆current stopping/PID。pod mutation的outer deadline由selected member與operation timeout總和推導,不使用 固定5分鐘。managed stop以iterative absolute-deadline polling等待OS process exit,逾時才執行一次force fallback。

Managed child readiness confirmation

Process.Start成功後,generic lifecycle先保持starting。managed child的guardian完成自己的最低readiness 判定後,以configured proc ID與current PID送一次confirmation:

let confirmationClient =
    ManagedChildHealthConfirmationClient(
        actorSystem,
        supervisorActorPath,
        TimeSpan.FromSeconds 5.0)

let! outcome =
    confirmationClient.ConfirmAsync("quote-tool", Environment.ProcessId)

if not outcome.Accepted then
    // 不阻塞application data path;記錄degraded並以bounded backoff重試。
    ()

accepted固定要求reply的procIdpidstatus=running都吻合。wrong/stale PID、pending stop、stopped與 unmanaged都回negative outcome,且不得改變lifecycle。ConfirmProcHealthy是保留的wire type名稱;它只表示 one-shot readiness edge,不是heartbeat,也不代表Supervisor持續監控application health/Serving。domain health 仍由child palette/probe擁有。

standalone child不送confirmation。replacement process必須用自己的新PID重新confirm。

Duplicate start / attach semantics

StartProcStartRegisteredProc不是「重新宣告 live child」API。current managed PID仍存活時:

  • exact相同spec與restart policy,且lifecycle為starting|running:視為idempotent attach,回原 ProcSnapshot;不重設readiness、不spawn、不重配probe。
  • spec或restart policy不同、stop/restart pending、stopping/unmanaged或矛盾狀態:fail closed,回未修改的 current snapshot。要套用新spec必須走explicit stop/restart。
  • current PID已不存在:才使用incoming spec建立新generation,初始狀態為starting

MDCQ fixed package完成整合E2E前,operator仍須使用artifact start.ps1的pre-status/partial-start guard;不得 對active pod直接送generic proc start。guard是defense-in-depth,actor-side invariant才是正確性來源。

Classified slot-aligned restart

ClassifiedOnFailure適用於「child能發布一個審查過的non-secret exit marker,Supervisor只應對exact marker replacement」的process。ProcSupervisor不認識application id、role或marker常數;全部由trusted catalog宣告:

{
  "processId": "capture",
  "fileName": "C:\\Program Files\\QuoteTool\\QuoteTool.exe",
  "arguments": ["--managed"],
  "workingDirectory": "C:\\Program Files\\QuoteTool",
  "role": "quote-capture",
  "probeMessage": null,
  "probeCron": null,
  "probeIntervalMs": null,
  "restartPolicy": "classified-on-failure",
  "maxRestarts": null,
  "safeDiagnosticPrefixes": ["QUOTE_RETRYABLE ", "QUOTE_FATAL_REDACTED"],
  "retryableExitMarkers": ["QUOTE_RETRYABLE reason=initial-connection-unavailable"],
  "restartSchedule": "utc-second-slots",
  "restartUtcSecondSlots": [5, 15, 25, 35, 45, 55],
  "restartScheduler": "akka-quartz",
  "restartMaxLatenessMs": 2000,
  "managedStopActorPath": null,
  "readyTimeoutMs": 30000,
  "stopTimeoutMs": 15000
}

restartScheduler可用akka-schedulerakka-quartz。後者使用既有 FAkka.Quartz.Actor 1.569.101.302與Quartz.NET one-shot trigger;不是常駐cron。Quartz trigger只送出 RestartDue(generation,dueUtc),真正start仍由ProcNodeActor重驗generation、desired-running、current PID與 lateness。missed slot只排next strict-future slot,不catch-up,也不fallback另一個backend。

Package API等價寫法:

let policy =
    { SafeDiagnosticPrefixes = [ "QUOTE_RETRYABLE "; "QUOTE_FATAL_REDACTED" ]
      RetryableExitMarkers = [ "QUOTE_RETRYABLE reason=initial-connection-unavailable" ]
      Schedule =
        { Seconds = [ 5; 15; 25; 35; 45; 55 ]
          Scheduler = ProcRestartSchedulerBackend.AkkaQuartz
          MaxLatenessMs = 2000 }
      MaxRestarts = None }

let restartPolicy = ProcRestartPolicy.ClassifiedOnFailure policy
let next = ProcUtcSecondSlots.nextStrictlyAfter DateTimeOffset.UtcNow policy.Schedule.Seconds

ProcSnapshot.restartStatus提供desiredRunningschedulerBackendpendingGenerationnextRetryUtc及bounded lastClassifiedOutcome。只有safe allowlist接受的line可進lastError;arbitrary stdout/stderr不會投影成restart diagnostic。operator stop、PrepareStop、LetItGo與replacement都會取消logical/physical trigger。

Default ProcHost建立的是RAM-backed QuartzActor()。可在package composition注入QuartzActor(IScheduler); Quartz仍只負責 trigger。只有啟用 SQL durable lifecycle profile 時,desired-running/generation/effect ledger 才可跨 Windows Service restart 恢復;只 persist Quartz job 仍會形成錯誤 authority。

Child self-stop 與 Supervisor stop

Managed child 自行停止前,使用 package 的 bounded typed client:

let releaseClient =
    ProcessReleaseClient(actorSystem, supervisorActorPath, TimeSpan.FromSeconds 10.0)

let! decision = releaseClient.ReleaseAsync("quote-tool", Environment.ProcessId)
match decision with
| LetItGoReply.Released
| LetItGoReply.AlreadyReleased ->
    // ownership 已釋放,child 才能開始自己的 shutdown guardian。
    ()
| rejected ->
    invalidOp $"Process ownership release rejected: {rejected}"

Supervisor 執行 proc stop 時不呼叫 LetItGo。它保留 process handle,先送 deployment catalog 註冊的 internal SupervisorManagedStopRequest;child 接受後自行退出。StopTimeoutMs 到期仍在執行時,Supervisor 才 force-kill。這個 typed message 不會出現在 public PBM/proxy arguments:

actor.Receive<SupervisorManagedStopRequest>(fun request ->
    if request.ExpectedPid <> Environment.ProcessId then
        replyTo.Tell({ RequestId = request.RequestId; Accepted = false; ErrorCode = Some "pid-mismatch" })
    else
        replyTo.Tell({ RequestId = request.RequestId; Accepted = true; ErrorCode = None })
        shutdownGuardian.Tell(ShutdownRequested))

Standalone mode 不建立 ProcessReleaseClient。fatal exit 也不先 release,而是由 catalog 的 ProcRestartPolicy 分類。

完整決策與 current-state API 見 RFC-PROC-0005RFC-PROC-0006RFC-PROC-0009doc/SA.mddoc/SD.md。MDCQ consumer guidance見RFC-PROC-0006 Feedback


REST API 介面

Supervisor 提供了一系列基於 Suave 的 HTTP API (預設 Port 為 6001)。

系統與叢集資訊

  • GET /healthGET /healthcheck: 系統健康檢查。
  • GET /api/cluster/info: 取得叢集狀態與角色。
  • POST /api/cluster/shutdown: 優雅地關閉所有子進程並關閉 Supervisor。

進程管理

  • GET /api/proc/nodes: 取得所有進程的狀態清單 (快照)。
  • POST /api/proc/nodes/start-default: 啟動一個預設的 ProcNode (執行自身並帶入 --mode procnode 等參數)。
    • Payload (Optional): { "procId": "自訂ID" }
  • POST /api/proc/nodes/start: 啟動一個自訂進程。
    • Payload: { "procId": "...", "fileName": "...", "args": [...], "workingDir": "...", "probeMessage": "...", "probeCron": "...", "probeIntervalMs": 15000 }
  • POST /api/proc/nodes/{procId}/stop: 停止指定的進程。
    • Payload (Optional): { "force": true }
  • POST /api/proc/nodes/clean-stopped: 清除 Registry 中已停止進程的紀錄。

FSI 互動 (透過 Probe 與 Send)

  • GET /api/proc/nodes/{procId}/probe: 取得進程目前的探測設定。
  • POST /api/proc/nodes/{procId}/probe: 更新探測設定。
    • Payload: { "probeMessage": "...", "probeCron": "...", "probeIntervalMs": 15000 }
  • GET /api/proc/nodes/{procId}/sessions: 取得該進程內 FSI Supervisor 的所有會話 (Sessions) 列表。
  • POST /api/proc/nodes/{procId}/sessions/{sessionName}: 確保指定 FSI session 存在。
  • DELETE /api/proc/nodes/{procId}/sessions/{sessionName}: 刪除指定 FSI session,會 forward DeleteSession 到 fsi supervisor;不可用 reset 模擬 delete。
  • POST /api/proc/nodes/{procId}/sessions/{sessionName}/reset: reset 指定 FSI session,語意保留為重置 session 狀態,不等同於 UI 的 Delete Session。
  • POST /api/proc/nodes/{procId}/send: 傳送指令到目標進程的 FSI Supervisor。
    • Payload: { "message": "執行字串", "timeoutMs": 30000 }

Message 格式 (傳送至 FSI)

透過 /api/proc/nodes/{procId}/send 傳送的 message 會由 ProcMessageParser 解析。支援的字串指令包含:

  • 執行 F# 程式碼: exec --session <sessionName> --code "<fsharp code>" [--refs ...] [--loads ...]
  • 取得 Session 資訊: getsession <sessionName>
  • 列出所有 Sessions: listsessions [--all true|false]
  • 建立/確保 Session: ensuresession <sessionName>
  • 重置 Session: resetsession <sessionName>
  • 刪除 Session: deletesession <sessionName>
  • 建立 Checkpoint: checkpoint --session <sessionName> [--id <id>] [--comment <text>]
  • Fork Session: fork --fromsession <old> --newsession <new> [--checkpointid <id>]
  • Join Sessions: join --parentsession <parent> --childsessions <child1> <child2> [--reducer <code>]

啟動參數 (CLI Arguments)

Supervisor 模式啟動範例:

./Akka.Proc.Supervisor --mode supervisor --systemname "proc-system" --host 127.0.0.1 --port 5001 --spawndefault

這會啟動 Supervisor,並自動衍生一個預設的 ProcNode。

Windows Service / SCM 模式啟動範例:

./Akka.Proc.Supervisor --mode supervisor --windows-service --systemname "proc-system" --host 127.0.0.1 --port 5001 --webhost 127.0.0.1 --webport 6001 --spawnnone

--windows-service 只用於 Windows Service 安裝後的 supervisor process。它透過 Microsoft.Extensions.Hosting.WindowsServices 連接 SCM service lifetime;一般 console / foreground 驗證不需要此 flag。PTC RN/GW outer service wrapper 應安裝 Akka.Proc.Supervisor 並傳入 --windows-service,不要把純 console supervisor binary 直接當 SCM service。

Application-neutral runtime catalog

Windows Service只啟動ProcSupervisor本身及其package-owned ProcNode bootstrap, 不得在SCM command line放入任何application catalog或application executable。 --windows-service若同時出現--process-catalog--spawn--spawnarg--spawnworkdir會在建立ActorSystem前fail-closed。這使Service deployment 與每一個application release保持獨立。

application自己的launcher先以absolute regular catalog path與exact SHA-256 註冊immutable catalog;註冊成功後才可執行generic lifecycle command:

pbm 127.0.0.1:7089 proc register --id application-a --catalog C:\artifacts\application-a\process-catalog.json --sha256 <64-hex>
pbm 127.0.0.1:7089 proc catalog-status --id application-a
pbm 127.0.0.1:7089 proc start --id application-a
pbm 127.0.0.1:7089 proc status --id application-a
pbm 127.0.0.1:7089 proc stop --id application-a

registration本身不啟動process。相同id + hash + catalog可重入;同id換版 只有舊、新catalog涉及的process都inactive時才可替換,否則回 registration-active。不同registration之間若process id、pod id或route identity衝突則拒絕。ProcSupervisor只解讀通用process/pod/restart/managed-stop/ application-route contract,不知道application名稱、actor protocol、資料庫或 credential。

durable SQL profile 會在 boot 從 authority 取回非 RequireReregister receipt,以 persisted absolute path 與 SHA-256 重新載入 catalog。它不從 journal 還原 argv,也不繞過 catalog hash。RestoreRegistrationOnly 只恢復 control-plane;RestoreDesiredRunningAfterFence 只有在沒有 ambiguous reservation/ownership 時才建立下一個 generation。全為 RequireReregister 的 registration 仍必須由 application launcher 顯式重送 proc register

application-specific probe/status/stop由catalog allow-list的route透過generic app invoke轉送:

pbm 127.0.0.1:7089 app invoke --id application-a --member worker-a --palette operations --command probe

完整設計與migration gate見 RFC-PROC-0010

從NuGet獨立部署neutral Windows Service

ProcSupervisor Service由本package自己的installer負責,不由MdcQuote或其他application artifact部署:

& .\scripts\Install-ProcSupervisorService.ps1

installer支援in-box Windows PowerShell 5.1;formal verifier會強制以powershell.exe Desktop 5.1執行, 避免只在PowerShell 7/modern .NET成功而漏掉production operator host差異。

零參數會查NuGet.org registration metadata,選擇listed=truepublished時間最新者;這不是SemVer 最大值。需固定版本時使用:

& .\scripts\Install-ProcSupervisorService.ps1 -Version 1.569.101.302-win23

只看計畫或只建立verified deployment而不修改SCM:

& .\scripts\Install-ProcSupervisorService.ps1 -PlanOnly
& .\scripts\Install-ProcSupervisorService.ps1 -PrepareOnly

啟用 durable SQL Service 時,connection string 只能放 encrypted file;command line 只保存 encrypted file 與 private-key path,不保存明文:

& .\scripts\Install-ProcSupervisorService.ps1 `
  -SystemName AkkaFsiProcSystemHostA `
  -PersistenceProfile sql-server `
  -SqlConnectionStringEncryptedFile D:\secure\proc-sql-connection.enc.txt `
  -SqlPrivateKeyPath D:\secure\myKey.private.txt `
  -SqlProviderName SqlServer.2022 `
  -LifecycleMachineIdentity HOST-A `
  -LifecycleSnapshotEvery 100

-SqlAutoInitialize 需由 operator 明確選擇;未帶時要求既有 schema。installer 仍拒絕 --process-catalog、application profile 與任意 child executable。 同機隔離驗證或多個Service須給不同-SystemName;預設仍為AkkaFsiProcSystem,與既有部署相容。 durable profile的proc status自win22起合併authority projection與live ProcNode,restart後不再只顯示fresh ProcNode的idle/desired=false

win23+將Reliable Delivery queue/producer identity由lifecycle persistence id的完整SHA-256衍生。相同 authority在Service restart後接回同一queue;不同deployment、測試Service或persistence id不再共用固定 proc-durable-effect-queue,避免舊的unconfirmed effect阻塞新authority。

installer要求系統管理員權限才可進入SCM mutation;任何managed child仍有live PID時會fail-closed。 Service PathName固定不含application catalog/profile/executable。詳細決策與rollback見 RFC-PROC-0011Runbook

既有Service有live child時,先執行read-only migration inventory,不要直接replace SCM:

dotnet fsi --exec .\test_scripts\generate_durable_migration_manifest.fsx

腳本只GET /api/proc/nodes,不讀child argv、不呼叫mutation endpoint;它輸出stable-sorted JSON/Markdown, 並為每個live child保留owner、graceful stop、health、re-register與rollback待填欄。這些欄位未完成前, installer的live-child fail-closed不得繞過。

Owner可另提供fakka-proc-migration-contracts.v1 JSON,再以--contracts-file <path> sparse override重跑。 每筆必須使用目前snapshot內的exact procId,完整填入owner、graceful stop、health、re-register與rollback; 未知、重複、空白、TBD、control character或超過512字元的欄位會讓整次產生fail closed。overlay只投影 readiness,不執行contract;allLiveContractsReady=true也仍需maintenance approval才可replace SCM。

注意:

  1. 若你是直接執行 .dll,請使用 dotnet exec --runtimeconfig ... --depsfile ... Akka.Proc.Supervisor.dll ...,不要只寫 dotnet Akka.Proc.Supervisor.dll
  2. --spawndefault 依賴 bootstrap procnode;目前已驗證可用的最小 smoke 會顯式設定 --host/--port/--webhost/--webport
  3. CLI 實際支援的參數名稱是 --systemname--system-name 不是主要入口的 Argu 參數名稱。若你在隔離驗證或外部啟動器中傳錯成 --system-name,會造成你誤判成 sidecar / session chain 壞掉,但其實是 CLI parameter mismatch。
  4. POST /api/proc/nodes/{procId}/send 目前仍有一個已知限制,見文末「已知問題」。
  5. 若你在 deployed 環境排查 fsi-supervisor,不要直接拿既有 bootstrap/stale proc 的 fsiSupervisorPath 下結論。較可靠的順序是:
    • 先 direct ask proc-supervisor GetVesion
    • GetAllProcInfo
    • 必要時先 stop stale proc
    • 再顯式 StartProc 起一個 fresh procnode
    • 最後使用 fresh fsiSupervisorPath 做 direct ask / direct execution 驗證
  6. GetVesionfsi-supervisor timeout,不等於 fsi-supervisor 整體不可用;應至少再交叉驗證 ListSessions 或直接 ExecCode
  7. 上層啟動器若自行 parse arguments,必須保留 Windows path backslash;只有 \"\' 或 escaped whitespace 才應消耗 \。否則 G:\PulseTrade.fs\... 會變成 G:PulseTrade.fs...,造成 child proc 起不來。
  8. 呼叫 StartProc / GetAllProcInfo 的 remote client ActorSystem 必須套用與 server 相容的 FAkka contract serializer config;若 remote 端 log 出現 JObject,優先檢查 client-side serializer binding,而不是先假設 proc supervisor 壞掉。

Shared logging profile

版本 1.564.101.203-win6 起,Akka.Proc.Supervisor 支援 WS-14 shared logging profile seam;目前 1.564.101.203-win9 同步使用 FAkka.FSI.Supervisor [1.564.101.203-win6]PulseTrade.Infra.Logging [1.564.101.203-win4]

  • --logging-hocon <path>:載入 host 產生的 logging HOCON fragment,建議由 PulseTrade.Infra.Logging.AkkaHocon.renderNLogLogger 產生。
  • PULSETRADE_AKKA_LOGGING_HOCON:未傳 --logging-hocon 時可由環境變數提供同一個檔案路徑;child procnode 也會繼承此 env var。
  • PULSETRADE_NLOG_CONFIG_FILE:若需 SQL target,host 可用 PulseTrade.Infra.Logging.NLogRuntime.writeConfigFile 產生 NLog XML config,並透過此 env var 讓 supervisor / procnode process 載入。
  • --logging-profile console:使用 built-in console NLog profile,適合 smoke / diagnostic。
  • --logging-profile none 或不指定:保留原本 package 行為,不強制切換 Akka logger。

Package 層不 hardcode SQL Server connection string、NLog table 或正式環境。SQL target / NLog database 應由 Mgmt2、DevKit、WinAgent 或其他 final host 決定,再透過 HOCON/config 注入。


本機 singleton guard

--mode supervisor 會以 --systemname 建立 machine-wide named mutex:

Global\PulseTrade.ProcSupervisor.<systemname>

同一台 Windows 機器上,同一個 --systemname 只允許一顆 local proc supervisor 存活。若第二顆 supervisor 使用相同 --systemname 啟動,會在建立 Akka actor system 前被拒絕,stderr 會包含 Another local proc supervisor is already running,process exit code 為 2

這個 guard 的語意是避免 Mgmt2FSharp.MCP.DevKitWinAgent 各自偷起第二顆 local proc supervisor,破壞共用 procnode/fsi session execution plane。client 端應採 discovery-first / attach-first:先嘗試 REST GET /api/cluster/info 或已知 actor path,只有完全沒有 reachable local singleton 時才啟第一顆。

測試證據:Akka.Proc.Supervisor.TestsProc supervisor singleton guard rejects second local supervisor 會先啟第一顆 supervisor,再用相同 --systemname 啟第二顆並驗證第二顆以 exit code 2 被拒絕。


--mode 與自訂 supervisee

--mode 只對 Akka.Proc.Supervisor.dll 自己 有意義:

  • --mode supervisor
    • 啟動 Supervisor process
  • --mode procnode
    • 啟動 ProcNode process,並在該 process 內 bootstrap Akka.FSI.Supervisor

若你透過 POST /api/proc/nodes/start 啟動的是別的程式,--mode 通常不該出現在該程式的 args 裡。

啟動另一個 .NET dll

如果 supervisee 是另一個 framework-dependent .NET dll,建議用:

{
  "procId": "my-dotnet-app",
  "fileName": "dotnet",
  "args": [
    "exec",
    "--runtimeconfig", "/path/MyApp.runtimeconfig.json",
    "--depsfile", "/path/MyApp.deps.json",
    "/path/MyApp.dll",
    "--arg1", "value1"
  ]
}

除非你啟動的仍然是 Akka.Proc.Supervisor.dll 本身,否則不要再加 --mode

啟動 Python

如果 supervisee 是 Python:

{
  "procId": "my-python-app",
  "fileName": "python3",
  "args": ["/path/app.py", "--arg1", "value1"]
}

同樣不需要 --mode

什麼情況下才要 --mode procnode

只有當你要啟動的 child process 本身就是 Akka.Proc.Supervisor.dll,而且要把它當成可回報 fsiSupervisorPath 的 FSI host 時,才需要 --mode procnode


probeMessage / probeIntervalMs 的用途與限制

  • probeMessage
    • 要定期送給 child proc 的 probe 內容
  • probeIntervalMs
    • 固定週期 probe 的毫秒數
  • probeCron
    • 若使用 Quartz cron,則用這個欄位排程 probe

這套 probe 機制不是 generic health check,也不是 generic IPC。實際流程是:

  1. ProcSupervisor 定時觸發 probe
  2. 讀出 probeMessage
  3. ProcMessageParser 解析該字串
  4. 轉送到 child proc 內的 fsi-supervisor
  5. 依回應是否成功來判定 probe success / failure

所以:

  • supervisee 若是 procnode + Akka.FSI.Supervisor
    • probeMessage 有意義
  • supervisee 若是一般 .NET dll、Python、或其他外部程式
    • 不應使用 FSI probe
    • /send 也不成立

推薦 probe 設定:FSI host / procnode

建議使用不改動 session state、且能穩定反映 FSI 可用性的指令:

{
  "probeMessage": "listsessions --all true",
  "probeIntervalMs": 15000
}

這是目前最建議的預設 probe。

一般自訂 .NET dll supervisee

若 child process 不是 procnode,建議不要設 probeMessage

{
  "probeMessage": null,
  "probeCron": null,
  "probeIntervalMs": null
}

Python supervisee

同樣不建議設 probeMessage

{
  "probeMessage": null,
  "probeCron": null,
  "probeIntervalMs": null
}

若要監控這類 process,應另外定義 generic health contract,而不是重用 FSI probe。


StartProc ask timeout 與 GetProcInfo 補收斂

在上層 orchestration(例如 fsharp-devkit create_fsi_host)中,StartProc 有時會出現:

  • child proc 其實已成功啟動
  • ProcRegistry 也已看得到 snapshot
  • StartProc 這個 ask-reply 還沒在 timeout 前回來

因此:

  • StartProc ask timeout
    • 不一定等於 host 建立失敗

較穩定的做法是:

  1. 先送 StartProc
  2. StartProc ask timeout
  3. 立刻在短時間內輪詢 GetProcInfo(procId)
  4. 若很快查到 snapshot,將其視為「已成功建立,但命令回覆較慢」
  5. 只有在短時間輪詢後仍查不到 snapshot,才真正當作建立失敗

這就是「StartProc ask timeout 時,改用 GetProcInfo 短時間輪詢補收斂」的意思。


E2E 範例 (FSX 腳本)

以下是一個已驗證可跑的 smoke 範例,示範如何透過 REST API 啟動一個 ProcNode、查詢節點、送出 F# 程式碼,最後再停止該 Node。

執行前請確認已啟動 Supervisor。若是 repo 內 Debug build,可用:

dotnet exec \
  --runtimeconfig Libs/Akka.Proc.Supervisor/bin/Debug/net10.0/Akka.Proc.Supervisor.runtimeconfig.json \
  --depsfile Libs/Akka.Proc.Supervisor/bin/Debug/net10.0/Akka.Proc.Supervisor.deps.json \
  Libs/Akka.Proc.Supervisor/bin/Debug/net10.0/Akka.Proc.Supervisor.dll \
  --mode supervisor \
  --systemname proc-system \
  --host 127.0.0.1 \
  --port 5001 \
  --webhost 127.0.0.1 \
  --webport 6001 \
  --spawnnone
#r "nuget: FSharp.Data"

open System
open System.Threading
open FSharp.Data

// 設定 Supervisor 的 API 網址
let baseUrl = "http://localhost:6001/api/proc/nodes"

printfn "=== 1. 啟動預設的 ProcNode ==="
let startResp = Http.RequestString(
    baseUrl + "/start-default",
    httpMethod = "POST",
    headers = [ HttpRequestHeaders.ContentType "application/json" ],
    body = TextRequest """{"procId": "demo-node-01"}"""
)
printfn "啟動回應: %s" startResp

// 等待一下讓 Node 啟動並連上 Cluster
printfn "等待 Node 準備就緒..."
Thread.Sleep(3000)

printfn "=== 2. 查詢 Node 列表 ==="
let nodesResp = Http.RequestString(baseUrl, httpMethod = "GET")
printfn "Nodes: %s" nodesResp

printfn "=== 3. 查詢該 Node 的 sessions ==="
let sessionsResp =
    Http.RequestString(
        sprintf "%s/demo-node-01/sessions" baseUrl,
        httpMethod = "GET"
    )
printfn "Sessions: %s" sessionsResp

printfn "=== 4. 傳送 F# 程式碼到該 Node 執行 ==="
let fsiCommand = """exec --session mysession --code "let add a b = a + b\nadd 5 7" """
let sendPayload = sprintf """{"message": "%s"}""" fsiCommand

let sendResp =
    Http.RequestString(
        sprintf "%s/demo-node-01/send" baseUrl,
        httpMethod = "POST",
        headers = [ HttpRequestHeaders.ContentType "application/json" ],
        body = TextRequest sendPayload
    )
printfn "執行結果: %s" sendResp

printfn "=== 5. 關閉該 Node ==="
let stopResp = Http.RequestString(
    sprintf "%s/demo-node-01/stop" baseUrl,
    httpMethod = "POST",
    headers = [ HttpRequestHeaders.ContentType "application/json" ],
    body = TextRequest """{"force": true}"""
)
printfn "停止回應: %s" stopResp

printfn "=== 完成 ==="

send 指令的轉義規則

/send 目前是以 CLI-like 字串協定配合 ProcMessageParser 解析,因此若你要在 --code 內放多行 F#,請用轉義字元:

  • \\n 代表換行
  • \\r 代表 CR
  • \\t 代表 tab
  • \\\" 代表雙引號
  • \\\\ 代表反斜線

例如:

exec --session mysession --code "let add a b = a + b\nadd 5 7"

會在送進 FSI 前被還原成真正的兩行 F# 程式碼。

失敗案例現在的預期行為

若 F# 程式本身有語法錯誤或編譯錯誤,/send 現在的預期回應是:

  • HTTP 仍正常回應
  • ExecResult.ok = false
  • diagnostics 內有 FCS 診斷
  • error.detail.errorType = "FSharp.Compiler.Interactive.Shell+FsiCompilationException"

也就是說,失敗會被表達成正常的 FSI 執行結果,而不是 transport/REST 反序列化錯誤。

關於設定檔 (.hocon)

系統會嘗試讀取目錄下的 .hocon 檔案,若不存在則使用內建預設值。您可以透過 akka.proc 區塊來調整 probe 週期、sharding 設定等:

akka.proc {
  system-name = "proc-system"
  probe-interval-ms = 15000
  probe-failure-threshold = 3
  restart-delay-ms = 3000
  web {
    host = "0.0.0.0"
    port = 6001
  }
}

部署診斷建議

若 deployed 環境看起來像:

  • proc-supervisor 可回 GetVesion
  • GetAllProcInfo 也有資料
  • fsiSupervisorPath 存在
  • fsi-supervisor GetVesion timeout

不要直接推論成 fsi-supervisor 壞掉。先做下面這組最小驗證:

  1. 用 direct actor ask 驗 proc-supervisor GetVesion
  2. 停掉 stale proc
  3. StartProc 起一個 fresh procnode
  4. 直接對 fresh fsiSupervisorPath
    • ListSessions
    • ExecCode

fsharp-devkit 的實際 deployment 驗證中,remote host isolationsession isolation 最終都是透過這種 direct actor-level 驗證確認成立;先前使用 bootstrap/stale proc target 的 timeout 不能直接當成底層 runtime 壞掉的證據。

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on FAkka.Proc.Supervisor:

Package Downloads
PulseTrade.Shared.fs

Package Description

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.571.101.400 171 9/4/2026
1.571.101.400-win2 117 9/6/2026
1.571.101.400-win1 117 9/6/2026
1.569.101.302-win9 95 8/6/2026
1.569.101.302-win8 103 8/4/2026
1.569.101.302-win7 110 8/4/2026
1.569.101.302-win23 94 9/4/2026
1.569.101.302-win22 91 9/4/2026
1.569.101.302-win21 92 9/4/2026
1.569.101.302-win20 95 9/4/2026
1.569.101.302-win19 92 9/4/2026
1.569.101.302-win18 92 8/31/2026
1.569.101.302-win17 97 8/29/2026
1.569.101.302-win16 91 8/18/2026
1.569.101.302-win15 101 8/18/2026
1.569.101.302-win14 105 8/12/2026
1.569.101.302-win13 127 8/12/2026
1.569.101.302-win12 101 8/9/2026
1.569.101.302-win11 93 8/8/2026
1.569.101.302-win10 97 8/6/2026
Loading failed