Echoサービス

複数のWippyプロセス、チャネル、コルーチン、メッセージパッシング、プロセス監視を使うCLI Echoサービスを構築します。

分類: 実行可能なチュートリアルです。ローカルの単一ノードCLIアプリケーションに必要な レジストリとLuaソース一式に加え、起動・検証手順も掲載しています。

概要

このチュートリアルでは、リレーサービスにメッセージを送信するCLIクライアントを作成し、リレーは各メッセージを処理するワーカーを生成します。以下を実演します:

  • プロセス生成 — 子プロセスを動的に作成
  • メッセージパッシング — send/receive操作でプロセス間通信
  • チャネルとselect — 複数のイベントソースを待機
  • コルーチン — プロセス内で並行処理を実行
  • プロセス登録 — 名前でプロセスを検索
  • モニタリング — 子プロセスのライフサイクルを追跡

前提条件

  • wippyとして実行できるWippyランタイムv0.3.32awippy version --shortで確認してください。

  • 対話型ターミナル。

  • 空の作業ディレクトリ。以下のファイルを追加する前に、プロジェクトとソースディレクトリを作成します:

    mkdir echo-service
    cd echo-service
    mkdir src
    

アーキテクチャ

flowchart TB
    subgraph terminal["terminal.host"]
        CLI["CLI Process"]
    end

    subgraph processes["process.host"]
        Relay["Relay Process
(+ stats coroutine)"] W1["Worker 1"] W2["Worker 2"] W3["Worker N"] end CLI -->|"send('relay', 'echo', msg)"| Relay Relay -->|"spawn_monitored(worker)"| W1 Relay -->|"spawn_monitored(...)"| W2 Relay -->|"spawn_monitored(...)"| W3 W1 -->|"send(sender, 'echo_response')"| CLI W2 -->|"send(...)"| CLI W3 -->|"send(...)"| CLI

プロジェクト構造

echo-service/
├── wippy.lock
└── src/
    ├── _index.yaml
    ├── cli.lua
    ├── relay.lua
    └── worker.lua

エントリ定義

src/_index.yamlを作成:

version: "1.0"
namespace: app

entries:
  - name: policy
    kind: security.policy
    policy:
      actions:
        - process.send
        - process.spawn
        - process.spawn.monitored
        - process.registry.register
      resources: "*"
      effect: allow

  - name: terminal
    kind: terminal.host
    lifecycle:
      auto_start: true

  - name: processes
    kind: process.host
    lifecycle:
      auto_start: true

  - name: cli
    kind: process.lua
    source: file://cli.lua
    method: main
    modules:
      - io
      - time
    security:
      policies: [app:policy]

  - name: relay
    kind: process.lua
    source: file://relay.lua
    method: main
    modules:
      - logger
      - time
    security:
      policies: [app:policy]

  - name: relay-service
    kind: process.service
    process: app:relay
    host: app:processes
    lifecycle:
      auto_start: true

  - name: worker
    kind: process.lua
    source: file://worker.lua
    method: main
    modules:
      - time
    security:
      policies: [app:policy]

セキュリティはデフォルト拒否のため、各プロセスは実行するアクション(名前の登録、メッセージの送信、監視付きワーカーの生成)を許可するポリシーを指定するsecurity:ブロックを持ちます。

リレープロセス

リレーは自身を登録し、メッセージを処理し、ワーカーを生成し、statsコルーチンを実行します。

src/relay.luaを作成:

local logger = require("logger")
local time = require("time")

local stats = {
    messages = 0,
    workers_spawned = 0
}

local function stats_reporter()
    while true do
        time.sleep("5s")
        logger:info("stats", {
            messages = stats.messages,
            workers_spawned = stats.workers_spawned
        })
    end
end

local function main()
    local inbox = process.inbox()
    local events = process.events()

    local _, register_err = process.registry.register("relay")
    if register_err then
        error("cannot register relay: " .. tostring(register_err))
    end
    logger:info("relay started", {pid = process.pid()})

    coroutine.spawn(stats_reporter)

    while true do
        local r = channel.select {
            inbox:case_receive(),
            events:case_receive()
        }

        if r.channel == events then
            local event = r.value
            if event.kind == process.event.CANCEL then
                logger:info("relay stopping", stats)
                return
            elseif event.kind == process.event.EXIT then
                logger:info("worker exited", {
                    from = event.from,
                    result = event.result
                })
            end
        else
            local msg = r.value
            if msg:topic() == "echo" then
                local echo = msg:payload():data()
                stats.messages = stats.messages + 1

                local worker_pid, err = process.spawn_monitored(
                    "app:worker",
                    "app:processes",
                    echo.sender,
                    echo.data
                )

                if err then
                    logger:error("spawn failed", {error = tostring(err)})
                else
                    stats.workers_spawned = stats.workers_spawned + 1
                end
            end
        end
    end
end

return { main = main }

主要パターン {id="relay-key-patterns"}

コルーチンの生成

coroutine.spawn(stats_reporter)

メイン関数とメモリを共有するコルーチンを起動します。コルーチンはtime.sleepなどのI/O操作でyieldします。

チャネルselect

local r = channel.select {
    inbox:case_receive(),
    events:case_receive()
}

複数のチャネルを待機します。r.channelでどれが発火したかを識別し、r.valueにデータが含まれます。ランタイムがサービスをシャットダウンすると、同じイベントチャネルにCANCELイベントが届きます。そこでmainから戻ることで、ホストは停止タイムアウトを待たずにクリーンに停止できます。

ペイロードの抽出

local echo = msg:payload():data()

メッセージにはトピック文字列用のmsg:topic()とペイロード用のmsg:payload():data()があります。

モニタリング付き生成

local worker_pid, err = process.spawn_monitored("app:worker", "app:processes", ...)

ワーカーを生成して監視を開始します。ワーカーが終了すると、リレーがEXITイベントを受信します。

ワーカープロセス

ワーカーは引数を直接受け取り、送信者にレスポンスを送信します。

src/worker.luaを作成:

local function main(sender_pid, data)
    local response = {
        data = string.upper(data),
        worker = process.pid()
    }

    local _, send_err = process.send(sender_pid, "echo_response", response)
    if send_err then
        error("cannot send echo response: " .. tostring(send_err))
    end

    return 0
end

return { main = main }

CLIプロセス

CLIはリレーの登録名にメッセージを送信し、各レスポンスをタイムアウト付きで待機します。

src/cli.luaを作成:

local io = require("io")
local time = require("time")

local reset = "\027"
local function dim(s) return "\027" .. s .. reset end
local function green(s) return "\027" .. s .. reset end
local function yellow(s) return "\027" .. s .. reset end
local function cyan(s) return "\027" .. s .. reset end

local function main()
    local inbox = process.inbox()

    -- Wait for relay to register its name
    local deadline = time.after("5s")
    while not process.registry.lookup("relay") do
        local tick = time.after("50ms")
        local r = channel.select { deadline:case_receive(), tick:case_receive() }
        if r.channel == deadline then
            io.print("relay not ready")
            return 1
        end
    end

    io.print(cyan("Echo Client"))
    io.print(dim("Type messages to echo. Ctrl+C to exit.\n"))

    while true do
        local _, write_err = io.write(yellow("> "))
        if write_err then
            io.eprint("cannot write prompt:", write_err)
            return 1
        end

        local _, flush_err = io.flush()
        if flush_err then
            io.eprint("cannot flush prompt:", flush_err)
            return 1
        end

        local input, read_err = io.readline()
        if read_err then
            io.eprint("cannot read input:", read_err)
            return 1
        end

        if not input or #input == 0 then
            break
        end

        local msg = {
            sender = process.pid(),
            data = input
        }
        local _, err = process.send("relay", "echo", msg)
        if err then
            io.print(dim("  error: " .. tostring(err)))
        else
            local timeout = time.after("2s")
            local r = channel.select {
                inbox:case_receive(),
                timeout:case_receive()
            }

            if r.channel == timeout then
                io.print(dim("  timeout"))
            else
                local msg = r.value
                if msg:topic() == "echo_response" then
                    local resp = msg:payload():data()
                    io.print(green("  " .. resp.data))
                    io.print(dim("  from worker: " .. resp.worker))
                end
            end
        end
    end

    io.print("\nGoodbye!")
    return 0
end

return { main = main }

主要パターン {id="cli-key-patterns"}

名前で送信

process.send("relay", "echo", msg)

process.sendは登録名を送信先として受け付け、その名前を解決できない場合はエラーを返します。

タイムアウトパターン

local timeout = time.after("2s")
local r = channel.select {
    inbox:case_receive(),
    timeout:case_receive()
}
if r.channel == timeout then
    -- timed out
end

実行

wippy init
wippy run -x app:cli

出力例:

Echo Client
Type messages to echo. Ctrl+C to exit.

> hello world
  HELLO WORLD
  from worker: {c49e0627-fcdf-53ec-a95d-6f84bc3715f3@app:processes|0x00005}

ワーカーPIDは実行時に生成されるため、表示される値は異なります。複数行を入力し、各レスポンスが 大文字になることを確認してください。空行を送信すると正常に終了します。

トラブルシューティングとクリーンアップ

  • relay not readyは、自動起動したリレーが5秒以内に登録されなかったことを示します。 ランタイムログでリレーの起動、ポリシー、レジストリエラーを確認してください。
  • not allowed to spawnまたはnot allowed to sendは、プロセスエントリに上記の app:process-policyセキュリティコンテキストがないことを示します。
  • no terminal host foundterminal.hostエントリがないことを示します。複数のターミナルホストがある場合は、 実行コマンドに--host app:terminalを追加してください。
  • 送信後のタイムアウトは、ワーカーがレスポンスを返さなかったことを示します。リレーログで生成エラーを確認し、 app:workerapp:processesがエントリ名と一致していることを確認してください。
  • 空行を送信するとCLIが終了します。ランタイムが動作し続ける場合はCtrl+Cを押してください。 使い捨ての演習であれば、ディレクトリを離れた後にecho-service/を削除してください。

次のステップ