~bigbes/tarantool

tarantool-protobuf

ref: 5de883e9079faacdcdccb853011e52142cbb16b9 tarantool-protobuf/docs/howto/03-grpc-loopback.md -rw-r--r-- 6.9 KiB
5de883e9 — Eugene Blikh beads: close 4ql (ibuf encoder not viable) + memory note on FFI boundary tax 2 months ago

#How-to: gRPC with the loopback transport

End-to-end Greeter service running entirely in-process: same Tarantool instance hosts the server, calls it through a generated client. The loopback transport (pb.grpc.loopback) bridges the two via fiber.channel — no network, no HTTP/2 library, no external dependencies.

This is the path for tests, same-process apps, and any scenario where you want gRPC's ergonomics without leaving the process.

For real (external) transports the contract is documented in reference/grpc-contract.md; a worked example of writing your own lives in how-to: custom transport.

#The proto

We use the Greeter service from examples/proto/hello.proto. All four streaming flavors:

service Greeter {
  rpc SayHello(HelloRequest) returns (HelloReply);
  rpc Echo(HelloRequest) returns (HelloRequest);
  rpc StreamHellos(HelloRequest) returns (stream HelloReply);
  rpc CollectHellos(stream HelloRequest) returns (HelloReply);
  rpc Chat(stream HelloRequest) returns (stream HelloReply);
}

message HelloRequest { string name = 1; }
message HelloReply   { string greeting = 1; }

The repo regenerates this on just gen. For a project of your own: protoc --tarantool_out=./gen hello.proto.

#The server

Plain Lua table with one function per RPC. Streaming functions receive a stream parameter that exposes :send, :recv, etc. (see grpc-contract.md → stream object — server view).

examples/grpc/server.lua:

local pb = require('pb')
local hello = require('full.hello.hello_pb')

local impl = {
    -- Unary: req in, reply out.
    SayHello = function(req, _ctx)
        return {greeting = 'Hi ' .. req.name}
    end,

    -- Server-stream: push N replies then return.
    StreamHellos = function(req, stream, _ctx)
        for i = 1, 3 do
            stream:send({greeting = ('Hi #%d %s'):format(i, req.name)})
        end
    end,

    -- Client-stream: pull until peer closes, return one reply.
    CollectHellos = function(stream, _ctx)
        local names = {}
        while true do
            local req, err = stream:recv()
            if req == nil then
                if err ~= nil then error(err, 0) end
                break
            end
            names[#names + 1] = req.name
        end
        return {greeting = 'Hi ' .. table.concat(names, ', ')}
    end,

    -- Bidi: pull a request, push a reply, repeat until peer closes.
    Chat = function(stream, _ctx)
        while true do
            local req, err = stream:recv()
            if req == nil then
                if err ~= nil then error(err, 0) end
                return
            end
            stream:send({greeting = 'Echo ' .. req.name})
        end
    end,
}

local server = hello.Greeter_server(impl)
return pb.grpc.loopback(server)

#The client

examples/grpc/client.lua:

local fiber = require('fiber')
local hello = require('full.hello.hello_pb')

local transport = dofile('examples/grpc/server.lua')  -- builds loopback
local client = hello.Greeter_client(transport)

-- Unary
print(client.SayHello({name = 'Alice'}, {}).greeting)

-- Server-stream
local s = client.StreamHellos({name = 'Bob'}, {})
while true do
    local msg, err = s:recv()
    if msg == nil then break end
    print(msg.greeting)
end

-- Client-stream: send N, close, recv one
local c = client.CollectHellos({})
c:send({name = 'Alice'}); c:send({name = 'Bob'}); c:send({name = 'Carol'})
c:close_send()
print(c:recv().greeting)

-- Bidi: send on one fiber, recv on another
local b = client.Chat({})
fiber.create(function()
    for _, name in ipairs({'X', 'Y', 'Z'}) do b:send({name = name}) end
    b:close_send()
end)
while true do
    local msg, err = b:recv()
    if msg == nil then break end
    print(msg.greeting)
end

Run it:

LUA_PATH="./runtime/?/init.lua;./runtime/?.lua;./examples/expected/?.lua;./examples/expected/?/init.lua;;" \
    tarantool examples/grpc/client.lua

Expected output:

--- unary ---
Hi Alice
--- server-stream ---
Hi #1 Bob
Hi #2 Bob
Hi #3 Bob
--- client-stream ---
Hi Alice, Bob, Carol
--- bidi ---
Echo X
Echo Y
Echo Z

#Streaming patterns

#Bidi from a single fiber

The example above splits the bidi call across two fibers (one sends, the main fiber receives). That's the safest pattern — :send blocks when the channel buffer fills, so doing both from one fiber risks self-deadlock if you're not careful.

Single-fiber bidi works when send/recv naturally interleave: send, recv, send, recv, …

local b = client.Chat({})
for _, name in ipairs({'X', 'Y', 'Z'}) do
    b:send({name = name})
    print(b:recv().greeting)
end
b:close_send()

#Cancellation

Either side can cancel mid-stream:

local s = client.StreamHellos({name = 'Bob'}, {})
local first = s:recv()
s:cancel()  -- drop the rest

Cancellation closes the channel; the server-side :send raises pb.grpc: stream canceled on the next attempt. Handlers that don't need to react gracefully can ignore the raise — the fiber unwinds and the call ends.

#Handler errors propagate

Errors raised in the server's handler surface as the err return on the client's next :recv():

StreamHellos = function(req, stream, _ctx)
    stream:send({greeting = 'first'})
    error('boom!', 0)
end

-- Client side:
local s = client.StreamHellos({name = 'x'}, {})
local r1 = s:recv()                  -- {greeting = 'first'}
local r2, err = s:recv()             -- nil, "boom!"

The first message goes through; the error surfaces when the channel drains.

#The ctx argument

The second-or-third argument to every RPC is ctx, an opaque table. Standard reserved keys (ctx.deadline, ctx.headers, ctx.trace_id, ctx.span_id, ctx.options) are documented in grpc-contract.md → context.

The loopback transport doesn't enforce deadlines today — it threads ctx to the handler unchanged. Real external transports that implement the contract are expected to honor the standard keys.

#Multiple services on one transport

local greeter = hello.Greeter_server(greeter_impl)
local catalog = catalog.Catalog_server(catalog_impl)

local transport = pb.grpc.multiplex({greeter, catalog})

local greeter_client = hello.Greeter_client(transport)
local catalog_client = catalog.Catalog_client(transport)

pb.grpc.multiplex errors on duplicate paths, so two services with the same package + service name will fail loudly at setup time, not silently at the first dispatch.

#What's next