~bigbes/tarantool

tarantool-protobuf

ref: d6570ec6e105e3986ee544a06804e44649afc356 tarantool-protobuf/runtime/pb/grpc.lua -rw-r--r-- 10.6 KiB
d6570ec6 — Eugene Blikh docs: PLAN.md lazy perf numbers after SoA refactor 3 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
-- gRPC transport interface + reference loopback / multiplex implementations.
--
-- # Transport contract
--
-- Unary:
--   transport:unary(path, req_bytes, ctx) -> resp_bytes
--
-- Streaming (three flavors). Each returns a `client_view` stream object
-- — see "Stream object" below.
--   transport:server_stream(path, req_bytes, ctx) -> stream
--   transport:client_stream(path, ctx)            -> stream
--   transport:bidi(path, ctx)                     -> stream
--
-- # Stream object (client view)
--
-- Methods that may be called by the caller side (the gRPC client):
--   stream:send(bytes)           -- push a message (client_stream, bidi)
--   stream:close_send()          -- signal "no more outgoing messages"
--   stream:recv() -> bytes, err  -- pull next reply; (nil, err_or_nil) ends
--   stream:cancel()              -- abort the call, drop pending messages
--
-- For server_stream, `send` and `close_send` are no-ops (initial request
-- is conveyed via the call's req_bytes argument).
--
-- # Server-side stream view
--
-- Generated server code passes a "server_view" object to the user handler:
--   server_view:send(bytes)           -- push a reply (server_stream, bidi)
--   server_view:recv() -> bytes, err  -- pull next request (client_stream, bidi)
--
-- The handler signals end-of-stream by returning. Errors thrown via
-- `error(...)` propagate to the client as the `err` returned by `recv()`.
--
-- # Real transports
--
-- HTTP/2, net.box-tunneled, IProto — those live in separate packages.
-- This module ships only loopback + multiplex, intended for tests and
-- in-process apps.
local fiber = require('fiber')

local M = {}

-- Default channel buffer size for in-process streams. Senders block when
-- the buffer is full; receivers block when it's empty. 16 messages is a
-- compromise between sender/receiver decoupling and memory footprint for
-- payload backlog. Override per call site via new_stream_pair(buf_size).
local DEFAULT_BUFFER = 16

-- Internal: shared state between the two stream views.
local function new_state()
    return {
        -- Recorded by server-side fiber when handler raises. Surfaced to
        -- client via the `err` return of recv() once the reply channel
        -- drains.
        server_err = nil,
        -- Set by client cancel(); the server side checks this on send and
        -- treats it as an abort signal.
        canceled = false,
    }
end

-- new_stream_pair(buf_size?) -> client_view, server_view, internal_state
--
-- Returns two opposed views of a bidirectional message pipe. Used by
-- loopback to bridge an in-process server fiber with a client caller.
-- The returned `internal_state` is exposed so the transport (not the
-- caller) can flag errors and trigger close.
function M.new_stream_pair(buf_size)
    buf_size = buf_size or DEFAULT_BUFFER
    local c2s = fiber.channel(buf_size)  -- client -> server
    local s2c = fiber.channel(buf_size)  -- server -> client
    local state = new_state()

    -- Client-facing view
    local client = {}

    function client:send(bytes)
        if c2s:is_closed() then
            error('pb.grpc: send after close_send', 0)
        end
        if state.canceled then
            error('pb.grpc: stream canceled', 0)
        end
        c2s:put(bytes)
    end

    function client:close_send()
        if not c2s:is_closed() then c2s:close() end
    end

    function client:recv()
        local b = s2c:get()
        if b == nil then
            -- Either drained naturally or a server-side error closed it.
            return nil, state.server_err
        end
        return b, nil
    end

    function client:cancel()
        state.canceled = true
        if not c2s:is_closed() then c2s:close() end
        -- We deliberately do NOT close s2c here. The handler fiber may
        -- still write to it; closing under their feet would raise. Drain
        -- on the next recv (which will see `canceled`).
    end

    -- Server-facing view (used by the handler running on a worker fiber)
    local server = {}

    function server:recv()
        if state.canceled then return nil, 'canceled' end
        local b = c2s:get()
        if b == nil then return nil, nil end
        return b, nil
    end

    function server:send(bytes)
        if state.canceled then
            -- Caller gave up — silently drop, don't error in the handler.
            return false
        end
        if s2c:is_closed() then return false end
        s2c:put(bytes)
        return true
    end

    -- Internal: invoked by the transport, not by user code.
    function server:_finish(err)
        if err ~= nil then state.server_err = tostring(err) end
        if not s2c:is_closed() then s2c:close() end
    end

    function server:_force_close_recv()
        -- Used by server_stream call where there's no client->server
        -- channel; closing c2s up-front means server:recv() returns nil
        -- immediately if (erroneously) called.
        if not c2s:is_closed() then c2s:close() end
    end

    return client, server, state
end

-- ---------------------------------------------------------------------------
-- Internal helpers used by loopback + multiplex
-- ---------------------------------------------------------------------------

local function dispatch_stream(streams, path, kind, req_bytes, ctx)
    local entry = streams and streams[path]
    if entry == nil then
        error(('pb.grpc: no streaming method registered for %q'):format(path), 0)
    end
    if entry.kind ~= kind then
        error(('pb.grpc: method %q is %s, called as %s')
            :format(path, entry.kind, kind), 0)
    end

    local client_view, server_view = M.new_stream_pair()
    if kind == 'server_stream' then
        -- No client->server messages after the initial request.
        server_view:_force_close_recv()
        client_view:close_send()
    end

    fiber.create(function()
        local ok, err = pcall(entry.handler, req_bytes, server_view, ctx or {})
        if ok then
            server_view:_finish(nil)
        else
            server_view:_finish(err)
        end
    end)

    return client_view
end

local function dispatch_unary(methods, path, req_bytes, ctx)
    local handler = methods and methods[path]
    if handler == nil then
        error(('pb.grpc: unknown unary method %q'):format(path), 0)
    end
    return handler(req_bytes, ctx or {})
end

-- ---------------------------------------------------------------------------
-- Public transports
-- ---------------------------------------------------------------------------

-- loopback(server) bridges an in-process M.<Service>_server(impl) result
-- into the transport contract. Streaming methods run their handler on a
-- worker fiber and communicate via fiber.channel.
function M.loopback(server)
    if type(server) ~= 'table' or type(server.methods) ~= 'table' then
        error("pb.grpc.loopback: expected a server table from M.<Service>_server()", 0)
    end
    local methods = server.methods
    local streams = server.streams or {}
    return {
        unary = function(_, path, req_bytes, ctx)
            return dispatch_unary(methods, path, req_bytes, ctx)
        end,
        server_stream = function(_, path, req_bytes, ctx)
            return dispatch_stream(streams, path, 'server_stream', req_bytes, ctx)
        end,
        client_stream = function(_, path, ctx)
            return dispatch_stream(streams, path, 'client_stream', nil, ctx)
        end,
        bidi = function(_, path, ctx)
            return dispatch_stream(streams, path, 'bidi', nil, ctx)
        end,
    }
end

-- multiplex({server1, server2, ...}) merges several servers' methods +
-- streams under a single transport. Errors on duplicate paths.
function M.multiplex(servers)
    local methods, streams = {}, {}
    for _, srv in ipairs(servers) do
        for path, handler in pairs(srv.methods or {}) do
            if methods[path] ~= nil then
                error(("pb.grpc.multiplex: duplicate unary route %q"):format(path), 0)
            end
            methods[path] = handler
        end
        for path, entry in pairs(srv.streams or {}) do
            if streams[path] ~= nil then
                error(("pb.grpc.multiplex: duplicate streaming route %q"):format(path), 0)
            end
            streams[path] = entry
        end
    end
    return {
        unary = function(_, path, req_bytes, ctx)
            return dispatch_unary(methods, path, req_bytes, ctx)
        end,
        server_stream = function(_, path, req_bytes, ctx)
            return dispatch_stream(streams, path, 'server_stream', req_bytes, ctx)
        end,
        client_stream = function(_, path, ctx)
            return dispatch_stream(streams, path, 'client_stream', nil, ctx)
        end,
        bidi = function(_, path, ctx)
            return dispatch_stream(streams, path, 'bidi', nil, ctx)
        end,
    }
end

-- ---------------------------------------------------------------------------
-- Helpers used by generated client code
-- ---------------------------------------------------------------------------
--
-- These wrap a transport-level (bytes) stream in a typed (decoded
-- messages) facade. Living in pb.grpc keeps the generated code small and
-- means we can refactor the streaming surface without re-running protoc.

-- Wrap a server-streaming call: caller calls stream:recv() until nil.
function M.wrap_server_stream(raw, output_decode)
    return {
        recv = function(_)
            local bytes, err = raw:recv()
            if bytes == nil then return nil, err end
            return output_decode(bytes), nil
        end,
        cancel = function(_) raw:cancel() end,
    }
end

-- Wrap a client-streaming or bidi call: caller sends + recvs.
function M.wrap_call(raw, input_encode, output_decode)
    return {
        send = function(_, msg)
            raw:send(input_encode(msg))
        end,
        close_send = function(_) raw:close_send() end,
        recv = function(_)
            local bytes, err = raw:recv()
            if bytes == nil then return nil, err end
            return output_decode(bytes), nil
        end,
        cancel = function(_) raw:cancel() end,
    }
end

-- Wrap a server-side stream view for the generated server handler:
-- the user-supplied impl is called with a stream that speaks decoded
-- messages, hiding the per-message encode/decode boundary.
function M.wrap_server_view(raw, input_decode, output_encode)
    local wrapped = {}
    if input_decode ~= nil then
        function wrapped:recv()
            local bytes, err = raw:recv()
            if bytes == nil then return nil, err end
            return input_decode(bytes), nil
        end
    end
    if output_encode ~= nil then
        function wrapped:send(msg) raw:send(output_encode(msg)) end
    end
    return wrapped
end

return M