프로세스와 메시징 입문

격리된 작업을 생성하고, 메시지를 교환하고, 수명 주기를 모니터링하고, 실패를 연결하고, 프로세스 이름을 등록하는 API를 알아봅니다.

개요

프로세스는 메시지 전달로 통신하는 격리된 실행 단위입니다. 각 프로세스는 자체 수신함을 가지며 특정 메시지 주제를 구독할 수 있습니다.

분류: 참조/API 입문서. 각 코드 조각은 하나의 연산을 독립적으로 보여 주며, 이 페이지 자체는 독립 실행형 프로젝트가 아닙니다. 생성, 모니터링, 메시징을 결합한 완전한 애플리케이션은 에코 서비스 튜토리얼을 참조하세요.

실행 환경과 의존성

권한

프로세스 작업은 호출하는 엔트리의 보안 정책에 대해 권한 검사를 거칩니다. 아래에서 사용하는 액션을 허용하는 security.policy 엔트리를 선언하고, 프로세스를 스폰하거나 메시지를 보내거나 모니터링하거나 연결하거나 이름을 등록하는 모든 엔트리에 연결하세요:

  - name: policy
    kind: security.policy
    policy:
      actions:
        - process.spawn
        - process.spawn.monitored
        - process.spawn.linked
        - process.host
        - process.send
        - process.monitor
        - process.unmonitor
        - process.link
        - process.unlink
        - process.registry.register
        - process.registry.unregister
      resources: "*"
      effect: allow

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

이 권한이 없으면 이러한 호출은 not allowed to spawn process: app.test.process:echo_worker와 같은 에러를 반환합니다. 전체 액션 목록은 권한 레퍼런스에 있습니다.

프로세스 스폰

생성, 보내기, 모니터링, 연결, 취소, 종료, 레지스트리 변경은 보호되는 연산입니다. 실행 엔트리에 액터를 지정하고 필요한 연산 및 리소스만 허용하는 정책을 부여하세요. 그렇지 않으면 엄격 모드가 해당 연산을 거부합니다.

핵심 개념:

  • process.spawn()과 그 변형으로 프로세스를 생성합니다.
  • PID나 등록된 이름으로 주제 기반 메시지를 보냅니다.
  • process.listen() 또는 process.inbox()로 메시지를 받습니다.
  • 이벤트로 프로세스 수명 주기를 모니터링합니다.
  • 프로세스를 연결하여 실패를 함께 처리합니다.

프로세스 생성

엔트리 참조에서 새 프로세스를 생성합니다.

local pid, err = process.spawn("app.test.process:echo_worker", "app:processes", "hello")
if err then
    return false, "spawn failed: " .. tostring(err)
end

-- pid is a string identifier for the spawned process
print("Started worker:", pid)

매개변수:

  • 엔트리 참조(예: "app.test.process:echo_worker")
  • 호스트 참조(예: "app:processes")
  • 워커의 main 함수로 전달하는 선택적 인수

자신의 PID 가져오기

local my_pid = process.pid()
-- Returns string PID of current process

메시지 전달

메시지는 주제 기반 라우팅 시스템을 사용합니다. 주제와 함께 PID로 메시지를 보낸 다음 주제 구독 또는 수신함을 통해 받습니다.

메시지 보내기

-- Send to process by PID
local sent, err = process.send(worker_pid, "messages", "hello from parent")
if err then
    return false, "send failed: " .. tostring(err)
end

-- send returns (bool, error)

주제 구독으로 받기

process.listen()으로 특정 주제를 구독합니다.

-- Worker that listens for messages on "messages" topic
local function main()
    local ch = process.listen("messages")

    local msg, ok = ch:receive()
    if ok then
        -- msg is the payload directly
        print("Received:", msg)
        return true
    end

    return false
end

return { main = main }

수신함으로 받기

수신함은 어떤 주제 리스너와도 일치하지 않는 메시지를 받습니다.

local function main()
    local inbox_ch = process.inbox()
    local specific_ch = process.listen("specific_topic")

    while true do
        local result = channel.select({
            specific_ch:case_receive(),
            inbox_ch:case_receive()
        })

        if result.channel == specific_ch then
            -- Messages to "specific_topic" arrive here
            local payload = result.value
        elseif result.channel == inbox_ch then
            -- Messages to any OTHER topic arrive here
            local msg = result.value
            print("Inbox got:", msg:topic(), msg:payload():data())
        end
    end
end

return { main = main }

송신자 정보를 위한 메시지 모드

송신자 PID와 주제에 접근하려면 { message = true }를 사용합니다.

-- Worker that echoes messages back to sender
local function main()
    local ch = process.listen("echo", { message = true })

    local msg = ch:receive()
    if msg then
        local sender = msg:from()
        local data = msg:payload():data()

        if sender then
            process.send(sender, "reply", data)
        end
        return true
    end

    return false
end

return { main = main }

프로세스 모니터링

프로세스를 모니터링하면 종료될 때 EXIT 이벤트를 받습니다.

모니터링과 함께 생성

local events_ch = process.events()

local worker_pid, err = process.spawn_monitored(
    "app.test.process:events_exit_worker",
    "app:processes"
)
if err then
    return false, "spawn failed: " .. tostring(err)
end

-- Wait for EXIT event
local timeout = time.after("3s")
local result = channel.select {
    events_ch:case_receive(),
    timeout:case_receive(),
}

if result.channel == timeout then
    return false, "timeout waiting for EXIT event"
end

local event = result.value
if event.kind == process.event.EXIT then
    print("Worker exited:", event.from)
    if event.result and event.result.error then
        print("Exit error:", event.result.error)
    elseif event.result then
        print("Return value:", event.result.value)
    end
end

명시적 모니터링

이미 실행 중인 프로세스를 모니터링합니다.

local events_ch = process.events()

-- Spawn without monitoring
local worker_pid, err = process.spawn("app.test.process:long_worker", "app:processes")
if err then
    return false, "spawn failed: " .. tostring(err)
end

-- Add monitoring explicitly
local ok, monitor_err = process.monitor(worker_pid)
if monitor_err then
    return false, "monitor failed: " .. tostring(monitor_err)
end

-- Now will receive EXIT events for this worker

모니터링을 중지합니다.

local ok, err = process.unmonitor(worker_pid)
if err then
    return false, "unmonitor failed: " .. tostring(err)
end

프로세스 연결

수명 주기를 함께 관리하려면 프로세스를 연결합니다. 기본적으로 비정상 종료는 연결된 동료 프로세스를 종료합니다. trap_links=true인 동료는 계속 실행되며 대신 LINK_DOWN 이벤트를 받습니다.

연결된 프로세스 생성

-- Child terminates if parent crashes (unless trap_links is set)
local pid, err = process.spawn_linked("app.test.process:child_worker", "app:processes")
if err then
    return false, "spawn_linked failed: " .. tostring(err)
end

명시적 연결

-- Link to existing process
local ok, err = process.link(target_pid)
if err then
    return false, "link failed: " .. tostring(err)
end

-- Unlink
local ok, err = process.unlink(target_pid)
if err then
    return false, "unlink failed: " .. tostring(err)
end

기본적으로 연결된 동료가 비정상 종료되면 현재 프로세스가 종료되며 Lua LINK_DOWN 이벤트는 전달되지 않습니다. 계속 실행하면서 해당 이벤트를 받으려면 trap_links를 활성화합니다.

local function main()
    -- Enable trap_links to receive LINK_DOWN events instead of crashing
    local ok, err = process.set_options({ trap_links = true })
    if not ok then
        return false, "set_options failed: " .. tostring(err)
    end

    -- Verify trap_links is enabled
    local opts = process.get_options()
    if not opts.trap_links then
        return false, "trap_links should be true"
    end

    local events_ch = process.events()

    -- Spawn a linked process that will fail
    local error_pid, err2 = process.spawn_linked(
        "app.test.process:error_exit_worker",
        "app:processes"
    )
    if err2 then
        return false, "spawn error worker failed: " .. tostring(err2)
    end

    -- Wait for LINK_DOWN event
    local timeout = time.after("2s")
    local result = channel.select {
        events_ch:case_receive(),
        timeout:case_receive(),
    }

    if result.channel == timeout then
        return false, "timeout waiting for LINK_DOWN"
    end

    local event = result.value
    if event.kind == process.event.LINK_DOWN then
        print("Linked process died:", event.from)
        -- Handle gracefully instead of crashing
        return true
    end

    return false, "expected LINK_DOWN, got: " .. tostring(event.kind)
end

return { main = main }

프로세스 레지스트리

이름 기반 조회와 메시징을 사용하려면 프로세스 이름을 등록합니다.

이름 등록

local function main()
    local test_name = "my_service_" .. tostring(os.time())

    -- Register current process with a name
    local ok, err = process.registry.register(test_name)
    if err then
        return false, "register failed: " .. tostring(err)
    end

    -- Lookup the registered name
    local pid, lookup_err = process.registry.lookup(test_name)
    if lookup_err then
        return false, "lookup failed: " .. tostring(lookup_err)
    end

    -- Verify it resolves to our PID
    if pid ~= process.pid() then
        return false, "lookup returned wrong pid"
    end

    return true
end

return { main = main }

이름 등록 해제

-- Unregister explicitly
local unregistered = process.registry.unregister(test_name)
if not unregistered then
    print("Name was not registered")
end

-- Lookup after unregister returns nil + error
local pid, err = process.registry.lookup(test_name)
-- pid will be nil, err will be non-nil

프로세스가 종료되면 이름은 자동으로 해제됩니다.

예제: 모니터링되는 워커 풀

이 부분 예제는 부모 프로세스가 모니터링되는 워커 여러 개를 생성하고 완료 상태를 추적하는 방법을 보여 줍니다. 사용하려면 부모와 app.test.process:task_worker 엔트리, app:processes 호스트, 필요한 프로세스 정책을 정의하고 두 엔트리의 모듈 목록에 time을 포함하세요.

-- Parent process
local time = require("time")

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

    -- Track spawned workers
    local workers = {}
    local worker_count = 5

    -- Spawn multiple monitored workers
    for i = 1, worker_count do
        local worker_pid, err = process.spawn_monitored(
            "app.test.process:task_worker",
            "app:processes",
            { task_id = i, value = i * 10 }
        )

        if err then
            return false, "spawn worker " .. i .. " failed: " .. tostring(err)
        end

        workers[worker_pid] = { task_id = i, started = os.time() }
    end

    -- Wait for all workers to complete
    local completed = 0
    local timeout = time.after("10s")

    while completed < worker_count do
        local result = channel.select {
            events_ch:case_receive(),
            timeout:case_receive(),
        }

        if result.channel == timeout then
            return false, "timeout waiting for workers"
        end

        local event = result.value
        if event.kind == process.event.EXIT then
            local worker = workers[event.from]
            if worker then
                if event.result and event.result.error then
                    print("Worker " .. worker.task_id .. " failed:", event.result.error)
                else
                    print("Worker " .. worker.task_id .. " completed:", event.result and event.result.value)
                end
                completed = completed + 1
            end
        end
    end

    return true
end

return { main = main }

워커 프로세스:

-- task_worker.lua
local time = require("time")

local function main(task)
    -- Simulate work
    time.sleep("100ms")

    -- Process task
    local result = task.value * 2

    return result
end

return { main = main }

다음 단계