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」:

  • ProcSnapshot:ProcRegistryActor 內的 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的procId、pid與status=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

StartProc與StartRegisteredProc不是「重新宣告 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-scheduler或akka-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提供desiredRunning、schedulerBackend、pendingGeneration、nextRetryUtc及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-0005、RFC-PROC-0006、RFC-PROC-0009、doc/SA.md、doc/SD.md。MDCQ consumer guidance見RFC-PROC-0006 Feedback。


REST API 介面

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

系統與叢集資訊

  • GET /health 或 GET /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=true中published時間最新者;這不是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-0011與 Runbook。

既有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. GetVesion 對 fsi-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 的語意是避免 Mgmt2、FSharp.MCP.DevKit、WinAgent 各自偷起第二顆 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.Tests 的 Proc 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 isolation 與 session 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 194 9/4/2026
1.571.101.400-win9 71 9/30/2026
1.571.101.400-win8 65 9/30/2026
1.571.101.400-win7 64 9/30/2026
1.571.101.400-win6 68 9/30/2026
1.571.101.400-win5 108 9/23/2026
1.571.101.400-win4 83 9/22/2026
1.571.101.400-win2 133 9/6/2026
1.571.101.400-win19 40 10/5/2026
1.571.101.400-win18 40 10/5/2026
1.571.101.400-win17 32 10/5/2026
1.571.101.400-win16 64 10/3/2026
1.571.101.400-win15 39 10/2/2026
1.571.101.400-win14 43 10/2/2026
1.571.101.400-win13 51 10/1/2026
1.571.101.400-win12 84 9/30/2026
1.571.101.400-win11 66 9/30/2026
1.571.101.400-win10 68 9/30/2026
1.571.101.400-win1 132 9/6/2026
1.569.101.302-win23 106 9/4/2026
Loading failed