-
Notifications
You must be signed in to change notification settings - Fork 2.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: support kafka consumer for pubsub scenario #6995
Changes from all commits
a0d1743
8a354ef
2f71053
a6292fb
d40d19a
32f2182
3b3625f
53c4c21
3a22a26
7058417
6f84c48
0eea32b
3cb6506
eacdc7a
0871a90
fd2dd12
07162a8
abf9bee
646a656
621be00
f6c60d3
37539f1
7a3ead7
5ac5b38
20eedff
be28226
ca99c68
1cf7b4c
d6434c2
b07dc70
dbec646
826f89a
d6c8628
7de4bce
464a028
4238e62
491a16b
0bdfe23
4eadcb7
a7b4b63
ef152fe
0efa8f6
448b9bc
0ce6fba
56bb7a6
dce5b4f
9227083
bf1020c
7a6854f
5a1f388
08bf6b8
0b8ed17
244a293
c75f918
3eace42
dc6832e
a16cf7f
501efb6
3d30ee9
4cecaf1
7529e7b
db34949
f423ae4
359418b
66b1759
a467ba4
5792c59
7384042
58a973d
c732581
0130cf3
78b601a
0ed9d7c
a572035
0a7a1f6
181d31d
ec377a1
b1ddf53
8d5b8f8
9eef154
8e15ebd
b7e288d
49e30b1
c8b6696
73bed0a
ad8facf
1d1206c
6d72f21
53d21e7
c9c88a6
4c75286
23c273d
d79d439
6fa0e26
78cd57b
6f651aa
9e9b7e2
f085dd9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,167 @@ | ||||||
-- | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add ldoc like other files under core/ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added |
||||||
-- Licensed to the Apache Software Foundation (ASF) under one or more | ||||||
-- contributor license agreements. See the NOTICE file distributed with | ||||||
-- this work for additional information regarding copyright ownership. | ||||||
-- The ASF licenses this file to You under the Apache License, Version 2.0 | ||||||
-- (the "License"); you may not use this file except in compliance with | ||||||
-- the License. You may obtain a copy of the License at | ||||||
-- | ||||||
-- http://www.apache.org/licenses/LICENSE-2.0 | ||||||
-- | ||||||
-- Unless required by applicable law or agreed to in writing, software | ||||||
-- distributed under the License is distributed on an "AS IS" BASIS, | ||||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
-- See the License for the specific language governing permissions and | ||||||
-- limitations under the License. | ||||||
-- | ||||||
|
||||||
--- Extensible framework to support publish-and-subscribe scenarios | ||||||
-- | ||||||
-- @module core.pubsub | ||||||
|
||||||
local log = require("apisix.core.log") | ||||||
local ws_server = require("resty.websocket.server") | ||||||
local protoc = require("protoc") | ||||||
local pb = require("pb") | ||||||
local setmetatable = setmetatable | ||||||
local pcall = pcall | ||||||
local pairs = pairs | ||||||
|
||||||
protoc.reload() | ||||||
pb.option("int64_as_string") | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will this call pollute the global option of pb? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's possible that this BTW, user-defined option lists are allowed in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we can solve with the pb_State like how we store the proto in grpc-transcode? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I need to research this further and will update it later if needed, you can review the other code first. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I help you to do it today, and put the result as: #7025 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed #7028 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. resolved as |
||||||
local pubsub_protoc = protoc.new() | ||||||
|
||||||
|
||||||
local _M = { version = 0.1 } | ||||||
local mt = { __index = _M } | ||||||
|
||||||
|
||||||
--- | ||||||
-- Create pubsub module instance | ||||||
-- | ||||||
-- @function core.pubsub.new | ||||||
-- @treturn pubsub module instance | ||||||
-- @treturn string|nil error message if present | ||||||
-- @usage | ||||||
-- local pubsub, err = core.pubsub.new() | ||||||
function _M.new() | ||||||
-- compile the protobuf file on initial load module | ||||||
-- ensure that each worker is loaded once | ||||||
if not pubsub_protoc.loaded["pubsub.proto"] then | ||||||
pubsub_protoc:addpath("apisix/include/apisix/model") | ||||||
local ok, err = pcall(pubsub_protoc.loadfile, pubsub_protoc, "pubsub.proto") | ||||||
if not ok then | ||||||
pubsub_protoc:reset() | ||||||
return nil, "failed to load pubsub protocol: "..err | ||||||
end | ||||||
end | ||||||
|
||||||
local ws, err = ws_server:new() | ||||||
if not ws then | ||||||
return nil, err | ||||||
end | ||||||
|
||||||
local obj = setmetatable({ | ||||||
ws_server = ws, | ||||||
cmd_handler = {}, | ||||||
}, mt) | ||||||
|
||||||
return obj | ||||||
end | ||||||
|
||||||
|
||||||
--- | ||||||
-- Add command callbacks to pubsub module instances | ||||||
-- | ||||||
-- The callback function prototype: function (params) | ||||||
-- The params in the parameters contain the data defined in the requested command. | ||||||
-- Its first return value is the data, which needs to contain the data needed for | ||||||
-- the particular resp, returns nil if an error exists. | ||||||
-- Its second return value is a string type error message, no need to return when | ||||||
-- no error exists. | ||||||
-- | ||||||
-- @function core.pubsub.on | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's add some There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed #7043 |
||||||
-- @usage | ||||||
-- pubsub:on(command, function (params) | ||||||
-- return data, err | ||||||
-- end) | ||||||
function _M.on(self, command, handler) | ||||||
self.cmd_handler[command] = handler | ||||||
end | ||||||
|
||||||
|
||||||
--- | ||||||
-- Put the pubsub instance into an event loop, waiting to process client commands | ||||||
-- | ||||||
-- @function core.pubsub.wait | ||||||
-- @treturn string|nil error message if present, will terminate the event loop | ||||||
-- @usage | ||||||
-- local err = pubsub:wait() | ||||||
function _M.wait(self) | ||||||
local ws = self.ws_server | ||||||
while true do | ||||||
-- read raw data frames from websocket connection | ||||||
local raw_data, raw_type, err = ws:recv_frame() | ||||||
if err then | ||||||
-- terminate the event loop when a fatal error occurs | ||||||
if ws.fatal then | ||||||
ws:send_close() | ||||||
return "websocket server: "..err | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Better to use break in the while loop and handle the error in the same place There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed |
||||||
end | ||||||
|
||||||
-- skip this loop for non-fatal errors | ||||||
log.error("failed to receive websocket frame: "..err) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed |
||||||
goto continue | ||||||
end | ||||||
|
||||||
-- handle client close connection | ||||||
if raw_type == "close" then | ||||||
ws:send_close() | ||||||
return | ||||||
end | ||||||
|
||||||
-- the pub-sub messages use binary, if the message is not | ||||||
-- binary, skip this message | ||||||
if raw_type ~= "binary" then | ||||||
goto continue | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Better to add warn log for unexpected input There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed |
||||||
end | ||||||
|
||||||
local data = pb.decode("PubSubReq", raw_data) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we check decode error? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. checkd There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed |
||||||
local sequence = data.sequence | ||||||
|
||||||
-- call command handler to generate response data | ||||||
for key, value in pairs(data) do | ||||||
-- There are sequence and command properties in the data, | ||||||
-- select the handler according to the command value. | ||||||
if key ~= "sequence" then | ||||||
local handler = self.cmd_handler[key] | ||||||
if not handler then | ||||||
log.error("callback handler not registered for the", | ||||||
" this command, command: ", key) | ||||||
goto continue | ||||||
end | ||||||
|
||||||
local resp, err = handler(value) | ||||||
if not resp then | ||||||
ws:send_binary(pb.encode("PubSubResp", { | ||||||
sequence = sequence, | ||||||
error_resp = { | ||||||
code = 0, | ||||||
message = err, | ||||||
}, | ||||||
})) | ||||||
goto continue | ||||||
end | ||||||
|
||||||
-- write back the sequence | ||||||
resp.sequence = sequence | ||||||
ws:send_binary(pb.encode("PubSubResp", resp)) | ||||||
end | ||||||
end | ||||||
|
||||||
::continue:: | ||||||
end | ||||||
end | ||||||
|
||||||
|
||||||
return _M |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
// | ||
// Licensed to the Apache Software Foundation (ASF) under one or more | ||
// contributor license agreements. See the NOTICE file distributed with | ||
// this work for additional information regarding copyright ownership. | ||
// The ASF licenses this file to You under the Apache License, Version 2.0 | ||
// (the "License"); you may not use this file except in compliance with | ||
// the License. You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
// | ||
|
||
syntax = "proto3"; | ||
|
||
option java_package = "org.apache.apisix.api.pubsub"; | ||
option java_outer_classname = "PubSubProto"; | ||
option java_multiple_files = true; | ||
option go_package = "github.com/apache/apisix/api/pubsub;pubsub"; | ||
|
||
/** | ||
* Get the offset of the specified topic partition from Apache Kafka. | ||
*/ | ||
message CmdKafkaListOffset { | ||
string topic = 1; | ||
int32 partition = 2; | ||
int64 timestamp = 3; | ||
} | ||
|
||
/** | ||
* Fetch messages of the specified topic partition from Apache Kafka. | ||
*/ | ||
message CmdKafkaFetch { | ||
string topic = 1; | ||
int32 partition = 2; | ||
int64 offset = 3; | ||
} | ||
|
||
/** | ||
* Client request definition for pubsub scenarios | ||
* | ||
* The sequence field is used to associate requests and responses. | ||
* Apache APISIX will set a consistent sequence for the associated | ||
* requests and responses, and the client can explicitly know the | ||
* response corresponding to any of the requests. | ||
* | ||
* The req field is the command data sent by the client, and its | ||
* type will be chosen from any of the lists in the definition. | ||
* | ||
* Field numbers 1 to 30 in the definition are used to define basic | ||
* information and future extensions, and numbers after 30 are used | ||
* to define commands. | ||
*/ | ||
message PubSubReq { | ||
int64 sequence = 1; | ||
oneof req { | ||
CmdKafkaFetch cmd_kafka_fetch = 31; | ||
CmdKafkaListOffset cmd_kafka_list_offset = 32; | ||
}; | ||
} | ||
|
||
/** | ||
* The definition of a message in Kafka with the current message | ||
* offset, production timestamp, Key, and message content. | ||
*/ | ||
message KafkaMessage { | ||
int64 offset = 1; | ||
int64 timestamp = 2; | ||
bytes key = 3; | ||
bytes value = 4; | ||
} | ||
|
||
/** | ||
* The response body of the service when an error occurs, | ||
* containing the error code and the error message. | ||
*/ | ||
message ErrorResp { | ||
int32 code = 1; | ||
string message = 2; | ||
} | ||
|
||
/** | ||
* The response of Fetch messages from Apache Kafka. | ||
*/ | ||
message KafkaFetchResp { | ||
repeated KafkaMessage messages = 1; | ||
} | ||
|
||
/** | ||
* The response of list offset from Apache Kafka. | ||
*/ | ||
message KafkaListOffsetResp { | ||
int64 offset = 1; | ||
} | ||
|
||
/** | ||
* Server response definition for pubsub scenarios | ||
* | ||
* The sequence field will be the same as the value in the | ||
* request, which is used to associate the associated request | ||
* and response. | ||
* | ||
* The resp field is the response data sent by the server, and | ||
* its type will be chosen from any of the lists in the definition. | ||
*/ | ||
message PubSubResp { | ||
int64 sequence = 1; | ||
oneof resp { | ||
ErrorResp error_resp = 31; | ||
KafkaFetchResp kafka_fetch_resp = 32; | ||
KafkaListOffsetResp kafka_list_offset_resp = 33; | ||
}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should add the t/pubsub to this as the t/plugin is the most time-consuming.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved to t/admin t/cli .... t/pubsub runner, it is currently the least time-consuming runner.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, the rule for dividing the test cases is not to make the time cost in balance, but to put t/plugin in a separate job, and arrange the remaining into two groups according to the lexicographic order
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed #7043
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolved #7043