Skip to content

Commit b9397da

Browse files
committed
fix(sdks): 五语言入站处理统一有界并发——读循环只投递,默认 CPU 核数
用户指正:只有 Go 是 worker 池不合理,且默认数应为 CPU 核数。 全量审计后统一各语言入站模型(同一定式:读循环只投递 + 固定 worker 池(默认 = CPU 核数,可配)+ 队列容量 workers×4 + 满 快速回错误响应让 Agent failover 接管): - Go:默认 8 → runtime.NumCPU()(下限 2),队列 32 → workers×4 - JS:读循环内 await handleInbound(头部阻塞)→ dispatchInbound 投递 + 动态 worker 循环(默认 os.cpus().length) - Python:reader 线程同步调 handler(头部阻塞)→ ThreadPoolExecutor(默认 max(2, os.cpu_count()))+ 计数上限 - C#:fire-and-forget 无界 Task → SemaphoreSlim(默认 ProcessorCount)+ Interlocked 计数上限 + 满时立即回空响应 - Java:readLoop 只路由响应、请求帧直接丢弃(handleLocalRequest 生产零调用——Agent 调用根本到不了 handler)→ 补 InboundListener + ThreadPoolExecutor(默认 CPU 核数)+ CroupierClientImpl 接线 attachInboundListener 路由 handleLocalRequest - C++:同 Java 病(请求帧按 unknown req_id 丢弃)→ TCPTransport 补 SetInboundHandler + 条件变量 worker 池(默认 hardware_concurrency)+ CroupierClient 实现 handleAgentRequest (invoke + keepalive pong)并双接线(connect/reconnect) Java/C++ 属于功能性缺陷修复:此前这两个 SDK 的 provider 模式 收不到任何 Agent 调用(帧被丢弃)。 本地验证:Go test/JS tsc/Python import/Java gradle/C# dotnet 全过;C++ 无本地 cmake 由 CI - C++ SDK 验证。
1 parent 0698b52 commit b9397da

10 files changed

Lines changed: 408 additions & 21 deletions

File tree

sdks/cpp/include/croupier/sdk/tcp_transport.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,17 @@ class TCPTransport {
141141
}
142142
};
143143

144+
// ---- Inbound (Agent -> Provider calls) ----
145+
// Read loop only dispatches; handlers run on a bounded worker pool
146+
// (default = hardware concurrency, queue = workers * 4, overflow
147+
// fast-fails with an empty response so Agent failover takes over).
148+
using InboundHandler = std::function<std::vector<uint8_t>(uint32_t msg_id, uint32_t req_id, const std::vector<uint8_t>& body)>;
149+
150+
void SetInboundHandler(InboundHandler handler);
151+
void DispatchInbound(uint32_t msg_id, uint32_t req_id, std::vector<uint8_t> body);
152+
void WriteResponseSilently(uint32_t resp_msg_id, uint32_t req_id, const std::vector<uint8_t>& body);
153+
static int InboundWorkerCount();
154+
144155
void ReadLoop();
145156
int ReadFully(void* buf, size_t count);
146157
static void PutMsgId(uint8_t* buf, uint32_t msg_id);
@@ -159,6 +170,13 @@ class TCPTransport {
159170
std::unordered_map<uint32_t, std::unique_ptr<ResponseLatch>> pending_responses_;
160171
std::mutex pending_mutex_;
161172
std::thread read_thread_;
173+
InboundHandler inbound_handler_;
174+
std::mutex inbound_pool_mutex_;
175+
std::vector<std::thread> inbound_workers_;
176+
std::queue<std::tuple<uint32_t, uint32_t, std::vector<uint8_t>>> inbound_queue_;
177+
std::condition_variable inbound_cv_;
178+
std::atomic<int> inbound_queued_{0};
179+
bool inbound_pool_started_ = false;
162180

163181
static constexpr size_t FRAME_HEADER_BYTES = 4;
164182
static constexpr size_t PROTOCOL_HEADER_SIZE = 8;

sdks/cpp/src/croupier_client.cpp

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,9 @@ class CroupierClient::Impl {
404404
std::unique_ptr<TCPTransport> replacement =
405405
std::make_unique<TCPTransport>(agent_address.host, agent_address.port, config_.timeout_seconds * 1000);
406406
replacement->SetConnectTimeout(config_.connect_timeout_seconds * 1000);
407+
replacement->SetInboundHandler([this](uint32_t msg_id, uint32_t req_id, const std::vector<uint8_t>& body) {
408+
return handleAgentRequest(msg_id, req_id, body);
409+
});
407410
replacement->Connect();
408411
std::string session_id = registerWithAgent(*replacement);
409412

@@ -420,6 +423,35 @@ class CroupierClient::Impl {
420423
}
421424
}
422425

426+
// handleAgentRequest 处理 Agent -> Provider 调用(invoke / start task),
427+
// 由 TCPTransport 的有界 worker 池并发执行(读循环只投递)。
428+
std::vector<uint8_t> handleAgentRequest(uint32_t msg_id, uint32_t /*req_id*/, const std::vector<uint8_t>& body) {
429+
try {
430+
if (msg_id == protocol::MSG_INVOKE_REQUEST) {
431+
auto req = ParseMessage<::croupier::sdk::v1::InvokeRequest>(body, "InvokeRequest");
432+
auto it = handlers_.find(req.function_id());
433+
if (it == handlers_.end()) {
434+
SDK_LOG_ERROR("Agent invoke: function not found: " + req.function_id());
435+
return {};
436+
}
437+
std::string context = "{}";
438+
std::string payload(req.payload().begin(), req.payload().end());
439+
std::string result = it->second(context, payload);
440+
::croupier::sdk::v1::InvokeResponse resp;
441+
resp.set_payload(result);
442+
return SerializeMessage(resp);
443+
}
444+
if (msg_id == protocol::MSG_PROVIDER_HEARTBEAT_REQUEST) {
445+
// keepalive pong(agent 侧探针)
446+
::croupier::sdk::v1::ProviderHeartbeatResponse resp;
447+
return SerializeMessage(resp);
448+
}
449+
} catch (const std::exception& e) {
450+
SDK_LOG_ERROR(std::string("Agent request handling failed: ") + e.what());
451+
}
452+
return {};
453+
}
454+
423455
bool Connect() {
424456
if (connected_)
425457
return true;
@@ -433,6 +465,9 @@ class CroupierClient::Impl {
433465
const auto agent_address = ParseTCPAddress(config_.agent_addr);
434466
auto transport =
435467
std::make_unique<TCPTransport>(agent_address.host, agent_address.port, config_.timeout_seconds * 1000);
468+
transport->SetInboundHandler([this](uint32_t msg_id, uint32_t req_id, const std::vector<uint8_t>& body) {
469+
return handleAgentRequest(msg_id, req_id, body);
470+
});
436471
transport->Connect();
437472
std::string session_id = registerWithAgent(*transport);
438473

sdks/cpp/src/tcp_transport.cpp

Lines changed: 92 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -505,15 +505,21 @@ void TCPTransport::ReadLoop() {
505505
std::vector<uint8_t> body(body_size);
506506
std::memcpy(body.data(), payload.data() + PROTOCOL_HEADER_SIZE, body_size);
507507

508-
// Route to pending request
509-
std::lock_guard<std::mutex> lock(pending_mutex_);
510-
auto it = pending_responses_.find(req_id);
511-
if (it != pending_responses_.end()) {
512-
it->second->Signal(std::move(body), msg_id);
508+
if (protocol::IsRequest(msg_id)) {
509+
// Agent -> Provider call: dispatch to bounded worker pool.
510+
// (Read loop never executes handler logic — inline processing
511+
// would head-of-line block every request on one slow handler.)
512+
DispatchInbound(msg_id, req_id, std::move(body));
513513
} else {
514-
// Debug: log unmatched response
515-
std::cerr << "[DEBUG] TCPTransport: Received response for unknown req_id: " << req_id
516-
<< ", msg_id: " << msg_id << ", body_size: " << body_size << '\n';
514+
// Route response to pending request
515+
std::lock_guard<std::mutex> lock(pending_mutex_);
516+
auto it = pending_responses_.find(req_id);
517+
if (it != pending_responses_.end()) {
518+
it->second->Signal(std::move(body), msg_id);
519+
} else {
520+
std::cerr << "[DEBUG] TCPTransport: Received response for unknown req_id: " << req_id
521+
<< ", msg_id: " << msg_id << ", body_size: " << body_size << '\n';
522+
}
517523
}
518524
}
519525

@@ -908,5 +914,83 @@ bool TCPServer::SendMessage(socket_t sock, uint32_t msg_type, uint32_t req_id, c
908914
return sent == static_cast<ssize_t>(frame.size());
909915
}
910916

917+
// ---- Inbound dispatch ----
918+
919+
int TCPTransport::InboundWorkerCount() {
920+
unsigned int n = std::thread::hardware_concurrency();
921+
return n == 0 ? 2 : static_cast<int>(std::max(2u, n));
922+
}
923+
924+
void TCPTransport::SetInboundHandler(InboundHandler handler) {
925+
inbound_handler_ = std::move(handler);
926+
}
927+
928+
void TCPTransport::DispatchInbound(uint32_t msg_id, uint32_t req_id, std::vector<uint8_t> body) {
929+
if (!inbound_handler_) {
930+
return;
931+
}
932+
// 惰性启动固定 worker 池(默认 = 硬件并发数)
933+
{
934+
std::lock_guard<std::mutex> lock(inbound_pool_mutex_);
935+
if (!inbound_pool_started_) {
936+
int workers = InboundWorkerCount();
937+
for (int i = 0; i < workers; ++i) {
938+
inbound_workers_.emplace_back([this] {
939+
for (;;) {
940+
std::tuple<uint32_t, uint32_t, std::vector<uint8_t>> task;
941+
{
942+
std::unique_lock<std::mutex> lock(inbound_pool_mutex_);
943+
inbound_cv_.wait(lock, [this] { return !inbound_queue_.empty(); });
944+
task = std::move(inbound_queue_.front());
945+
inbound_queue_.pop();
946+
}
947+
auto [mid, rid, tbody] = std::move(task);
948+
std::vector<uint8_t> resp;
949+
try {
950+
resp = inbound_handler_(mid, rid, tbody);
951+
} catch (const std::exception& e) {
952+
std::cerr << "[ERROR] inbound handler: " << e.what() << '\n';
953+
resp.clear();
954+
}
955+
WriteResponseSilently(protocol::GetResponseMsgID(mid), rid, resp);
956+
inbound_queued_.fetch_sub(1);
957+
}
958+
});
959+
}
960+
inbound_pool_started_ = true;
961+
}
962+
}
963+
int workers = InboundWorkerCount();
964+
if (inbound_queued_.load() >= workers * 4) {
965+
// 队列满:立即回空响应,Agent 侧 failover 接管。
966+
std::cerr << "[WARN] inbound queue full, fast-failing req_id=" << req_id << '\n';
967+
WriteResponseSilently(protocol::GetResponseMsgID(msg_id), req_id, {});
968+
return;
969+
}
970+
inbound_queued_.fetch_add(1);
971+
{
972+
std::lock_guard<std::mutex> lock(inbound_pool_mutex_);
973+
inbound_queue_.emplace(msg_id, req_id, std::move(body));
974+
}
975+
inbound_cv_.notify_one();
976+
}
977+
978+
void TCPTransport::WriteResponseSilently(uint32_t resp_msg_id, uint32_t req_id, const std::vector<uint8_t>& body) {
979+
try {
980+
auto frame = protocol::NewMessage(resp_msg_id, req_id, body);
981+
std::vector<uint8_t> wrapped(4 + frame.size());
982+
wrapped[0] = static_cast<uint8_t>((frame.size() >> 24) & 0xFF);
983+
wrapped[1] = static_cast<uint8_t>((frame.size() >> 16) & 0xFF);
984+
wrapped[2] = static_cast<uint8_t>((frame.size() >> 8) & 0xFF);
985+
wrapped[3] = static_cast<uint8_t>(frame.size() & 0xFF);
986+
std::memcpy(wrapped.data() + 4, frame.data(), frame.size());
987+
ssize_t sent = send(socket_, reinterpret_cast<const char*>(wrapped.data()),
988+
static_cast<int>(wrapped.size()), 0);
989+
(void)sent;
990+
} catch (...) {
991+
// best-effort
992+
}
993+
}
994+
911995
} // namespace sdk
912996
} // namespace croupier

sdks/csharp/src/Croupier.Sdk/Transport/TCPTransport.cs

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
using System.Net.Sockets;
1616
using System.Collections.Concurrent;
17+
using System.Threading;
1718
using Croupier.Sdk.Logging;
1819

1920
namespace Croupier.Sdk.Transport;
@@ -54,6 +55,14 @@ public sealed class TCPTransport : IClientTransport
5455
// Handler for inbound requests from Agent (e.g., InvokeRequest)
5556
private Func<int, int, byte[], Task<byte[]>>? _inboundRequestHandler;
5657

58+
// Inbound worker pool: bounded concurrency (default = CPU cores).
59+
// Fire-and-forget per request is unbounded; a slow handler storm can
60+
// exhaust memory. Queue capacity = workers * 4; overflow fast-fails
61+
// with an empty response so the Agent-side failover takes over.
62+
private readonly SemaphoreSlim _inboundLimiter = new(
63+
Math.Max(2, Environment.ProcessorCount), Math.Max(2, Environment.ProcessorCount));
64+
private int _inboundQueued;
65+
5766
/// <summary>
5867
/// Gets whether the transport is connected.
5968
/// </summary>
@@ -334,7 +343,7 @@ private async Task ReadLoop(CancellationToken cancellationToken)
334343
// awaiting a response to its own request. Do not await the
335344
// callback in the sole read loop, otherwise its response
336345
// cannot be read and both peers time out.
337-
_ = HandleInboundRequestAsync(parsed.MsgId, parsed.ReqId, parsed.Body, cancellationToken);
346+
DispatchInbound(parsed.MsgId, parsed.ReqId, parsed.Body, cancellationToken);
338347
}
339348
}
340349
catch (OperationCanceledException)
@@ -372,6 +381,47 @@ private async Task ReadLoop(CancellationToken cancellationToken)
372381
_pending.Clear();
373382
}
374383

384+
private void DispatchInbound(int msgId, int reqId, byte[] body, CancellationToken cancellationToken)
385+
{
386+
int capacity = Math.Max(2, Environment.ProcessorCount) * 4;
387+
if (Interlocked.CompareExchange(ref _inboundQueued, 0, 0) >= capacity)
388+
{
389+
// Queue full: respond immediately (empty) so the Agent fails over.
390+
_ = WriteFrameAsync(
391+
Protocol.NewMessage(Protocol.GetResponseMsgId(msgId), reqId, new byte[0]),
392+
cancellationToken);
393+
return;
394+
}
395+
Interlocked.Increment(ref _inboundQueued);
396+
_ = Task.Run(async () =>
397+
{
398+
try
399+
{
400+
await _inboundLimiter.WaitAsync(cancellationToken).ConfigureAwait(false);
401+
try
402+
{
403+
await HandleInboundRequest(msgId, reqId, body, cancellationToken).ConfigureAwait(false);
404+
}
405+
finally
406+
{
407+
_ = _inboundLimiter.Release();
408+
}
409+
}
410+
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
411+
{
412+
// Normal shutdown.
413+
}
414+
catch (Exception ex)
415+
{
416+
_logger.LogError("TCPTransport", $"Inbound request processing failed: {ex.Message}", ex);
417+
}
418+
finally
419+
{
420+
Interlocked.Decrement(ref _inboundQueued);
421+
}
422+
});
423+
}
424+
375425
private async Task HandleInboundRequestAsync(int msgId, int reqId, byte[] body, CancellationToken cancellationToken)
376426
{
377427
try

sdks/go/pkg/croupier/transport/config.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,10 @@ type Config struct {
5959
// requests sent by the Agent over this connection are handled in place.
6060
InboundHandler InboundHandler
6161

62-
// InboundWorkers 是处理 Agent 入站请求的固定 worker 数(0=默认 8)。
63-
// 显式有界——每请求一个 goroutine 的无界模型在并发下会爆内存。
62+
// InboundWorkers 是处理 Agent 入站请求的固定 worker 数
63+
//(0=默认 runtime.NumCPU()——CPU 核数即并发处理上限,handler
64+
// 通常含 IO 等待时可在连接配置里显式调高)。显式有界——每请求
65+
// 一个 goroutine 的无界模型在并发下会爆内存。
6466
InboundWorkers int
6567
// InboundQLen 是入站请求队列长度(0=默认 32)。队列满时新请求
6668
// 立即回错误响应(Agent 侧 failover 接管),不排队积累内存。

sdks/go/pkg/croupier/transport/tcp_client.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"io"
1010
"net"
11+
"runtime"
1112
"strconv"
1213
"strings"
1314
"sync"
@@ -124,11 +125,15 @@ func NewTCPClient(config *Config) (*TCPClient, error) {
124125
if config.InboundHandler != nil {
125126
workers := config.InboundWorkers
126127
if workers <= 0 {
127-
workers = 8
128+
workers = runtime.NumCPU()
129+
if workers < 2 {
130+
workers = 2
131+
}
128132
}
129133
qlen := config.InboundQLen
130134
if qlen <= 0 {
131-
qlen = 32
135+
// 队列容量与 worker 数成比例:突发吸收 4 轮 worker 满载
136+
qlen = workers * 4
132137
}
133138
client.inbox = make(chan inboundTask, qlen)
134139
client.inboxWg.Add(workers)

sdks/java/src/main/java/io/github/cuihairu/croupier/sdk/CroupierClientImpl.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,7 @@ private void reconnectOnce() throws Exception {
485485
TransportClient old = transport;
486486
transport = nextTransport;
487487
sessionId = response.sessionId;
488+
attachInboundListener(nextTransport);
488489
if (old != null) {
489490
old.close();
490491
}
@@ -496,6 +497,13 @@ private void reconnectOnce() throws Exception {
496497
}
497498
}
498499

500+
/** 把 transport 的入站请求路由到本地 handler(Agent -> Provider 调用)。 */
501+
private void attachInboundListener(TransportClient client) {
502+
if (client instanceof io.github.cuihairu.croupier.sdk.transport.TCPTransport tcp) {
503+
tcp.setInboundListener((msgId, requestId, body) -> handleLocalRequest(msgId, requestId, body));
504+
}
505+
}
506+
499507
private byte[] handleLocalRequest(int msgType, int requestId, byte[] body) throws Exception {
500508
return switch (msgType) {
501509
case Protocol.MSG_INVOKE_REQUEST -> handleInvokeRequest(body);

0 commit comments

Comments
 (0)