Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[flake8]
max-line-length = 120
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ jobs:

- name: Install package and dependencies
run: |
pip install -e ".[dev,fast]" "protobuf>=3.19.4,<3.20"
pip install -r requirements-test.txt

- name: Run tests with coverage
Expand Down
10 changes: 10 additions & 0 deletions tests/http/test_http_server_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from trpc_sdk.http import get_http_response
from trpc_sdk.http import register_user_http_decode_func
from trpc_sdk.http import _routes
from trpc_sdk.utils import set_default_close_wait


def test_http_server_transport():
Expand All @@ -58,6 +59,15 @@ def test_http_listen_serves_the_runner_on_one_loop(monkeypatch):
transport = HttpServerTransportFactory().create()
transport.bind(opts)

real_create_socket = transport.create_socket

def _create_socket(reuse_port, stream=True):
sock = real_create_socket(reuse_port, stream)
set_default_close_wait(sock)
return sock

monkeypatch.setattr(transport, "create_socket", _create_socket)

def _app():
app = web.Application()
app.router.add_get("/", lambda request: web.Response(text="ok"))
Expand Down
8 changes: 7 additions & 1 deletion tests/transport/test_thread_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from trpc_sdk.transport._server import serve_stream
from trpc_sdk.transport._server import _trpc_transport
from trpc_sdk.utils import create_new_socket
from trpc_sdk.utils import set_default_close_wait


@pytest.fixture
Expand Down Expand Up @@ -67,7 +68,12 @@ def attach_socket(self, sock, protocol_factory, ssl_context=None):


def _listen_socket() -> socket.socket:
return create_new_socket(("127.0.0.1", 0), "", False, True, 16)
# Linux inherits SO_LINGER(1, 0) from the listen socket, so closing an
# accepted fd sends RST and can fail the client's connect. Restore the
# default linger so this test sees a normal FIN.
sock = create_new_socket(("127.0.0.1", 0), "", False, True, 16)
set_default_close_wait(sock)
return sock


def test_default_model_is_rebuilt_on_the_calling_loop(loop):
Expand Down
13 changes: 7 additions & 6 deletions trpc_sdk/runtime/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
# Copyright (C) 2026 Tencent. All rights reserved.
#
# tRPC-Python is licensed under Apache-2.0.

"""tRPC-Python runtime: the process and thread model behind a server.

There are two kinds of process. The **worker** is the process the application
Expand All @@ -30,7 +29,8 @@
what a CPU-bound service needs; ``merge`` will not scale it across cores.
Only tRPC-over-TCP addresses pass through the worker; UDP, HTTP and
user-protocol addresses are bound by every rpc task with ``SO_REUSEPORT``.
- ``peer`` (``multi_process`` with ``io_handle_type: merge``) — the worker forks ``worker_num - 1`` equal workers; every one,
- ``peer`` (``multi_process`` with ``io_handle_type: merge``) — the worker
forks ``worker_num - 1`` equal workers; every one,
itself included, binds every address with ``SO_REUSEPORT`` and serves it
like a single-loop ``merge`` worker. Nothing is forwarded between
processes; the kernel spreads connections. Tasks are not supported.
Expand All @@ -56,7 +56,8 @@
``runtime/threadmodel`` and ``separate_runtime``); responses cross back
to the I/O loops through :class:`~trpc_sdk.transport.ConnWriter`,
re-exported here
- :mod:`~trpc_sdk.runtime._common` — SIGTERM callbacks, process metadata and the exit template every mode shares (trpc-cpp
- :mod:`~trpc_sdk.runtime._common` — SIGTERM callbacks, process metadata
and the exit template every mode shares (trpc-cpp
``runtime/common``)
- :mod:`~trpc_sdk.runtime._process` — task children and the pre-fork
socketpair mesh (trpc-cpp ``runtime/task``), plus the multi-process
Expand Down Expand Up @@ -116,9 +117,9 @@

# Called by Server.serve. Absent from __all__ on purpose: application code
# uses set_signal_handle, register_handler_thread_hook, and create_server.
from ._common import run_server_signal_handle
from ._modes import create_worker
from ._threadmodel import freeze_handler_thread_hooks
from ._common import run_server_signal_handle # noqa: F401
from ._modes import create_worker # noqa: F401
from ._threadmodel import freeze_handler_thread_hooks # noqa: F401

__all__ = [
# Process model
Expand Down
10 changes: 5 additions & 5 deletions trpc_sdk/runtime/_threadmodel/_loop_thread.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
# Copyright (C) 2026 Tencent. All rights reserved.
#
# tRPC-Python is licensed under Apache-2.0.

"""Event-loop threads that a merge-mode worker spreads its work over.

In ``merge`` mode one process both accepts connections and runs handlers.
Expand Down Expand Up @@ -331,8 +330,7 @@ def _join(self, timeout: float) -> bool:
return not self._thread.is_alive()


def stop_loop_threads(threads: Sequence[LoopThread],
before_stop: Optional[Callable[[int], Awaitable[None]]],
def stop_loop_threads(threads: Sequence[LoopThread], before_stop: Optional[Callable[[int], Awaitable[None]]],
timeout: float) -> bool:
"""Stop several loop threads in parallel, under one shared deadline.

Expand Down Expand Up @@ -360,8 +358,10 @@ def stop_loop_threads(threads: Sequence[LoopThread],
if before_stop is None or thread.loop is None or thread.loop.is_closed():
continue
try:
pending.append((thread, thread._schedule( # pylint: disable=protected-access
lambda index=thread.index: before_stop(index))))
pending.append((
thread,
thread._schedule( # pylint: disable=protected-access
lambda index=thread.index: before_stop(index))))
except RuntimeError:
continue
for thread, future in pending:
Expand Down
6 changes: 3 additions & 3 deletions trpc_sdk/runtime/_threadmodel/_separate_thread.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
# Copyright (C) 2026 Tencent. All rights reserved.
#
# tRPC-Python is licensed under Apache-2.0.

"""Worker of ``server.mode: merge`` with ``handle_thread_num > 0`` (``io_handle_type: separate``).

inside one process; not to be confused with ``server.mode: separate`` in
Expand Down Expand Up @@ -115,8 +114,9 @@ def _warn_io_threads_without_tls(self) -> None:
return
if any(t.ipc_forwardable and t.opts.tls_cert and t.opts.tls_key for t in self.transports):
return
logger.warning("[separate thread worker] io_thread_num=%s, but no tRPC TCP endpoint uses TLS; "
"extra I/O loops only pay off for TLS", self.io_thread_num)
logger.warning(
"[separate thread worker] io_thread_num=%s, but no tRPC TCP endpoint uses TLS; "
"extra I/O loops only pay off for TLS", self.io_thread_num)

def _build_thread_model(self) -> ThreadModel:
"""I/O on the main loop plus ``io_thread_num - 1`` I/O threads; handlers on the handle threads.
Expand Down
1 change: 0 additions & 1 deletion trpc_sdk/transport/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
# Copyright (C) 2026 Tencent. All rights reserved.
#
# tRPC-Python is licensed under Apache-2.0.

"""tRPC-Python transport package: how bytes travel, not what they mean.

The package holds both halves of the wire path.
Expand Down
1 change: 0 additions & 1 deletion trpc_sdk/transport/_server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
# Copyright (C) 2026 Tencent. All rights reserved.
#
# tRPC-Python is licensed under Apache-2.0.

"""Server half of the transport package: the per-service listen transports.

A server transport is constructed and bound in the parent (``bind`` only
Expand Down
15 changes: 6 additions & 9 deletions trpc_sdk/transport/_server/_datagram_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
# Copyright (C) 2026 Tencent. All rights reserved.
#
# tRPC-Python is licensed under Apache-2.0.

"""Datagram protocol of a tRPC-over-UDP listen socket.

:meth:`~trpc_sdk.transport.TrpcServerTransport.datagram_protocol_factory`
Expand Down Expand Up @@ -50,12 +49,8 @@ class DatagramPeer:

__slots__ = ("aio_conn", "local_addr", "remote_addr", "_writer")

def __init__(self,
loop: asyncio.AbstractEventLoop,
aio_conn: asyncio.DatagramTransport,
local_addr: Tuple[str, int],
remote_addr: Tuple[str, int],
source: Any) -> None:
def __init__(self, loop: asyncio.AbstractEventLoop, aio_conn: asyncio.DatagramTransport,
local_addr: Tuple[str, int], remote_addr: Tuple[str, int], source: Any) -> None:
"""Bind the peer to one datagram's source.

Args:
Expand Down Expand Up @@ -141,8 +136,10 @@ def datagram_received(self, data: bytes, addr: Any) -> None:
return
except Exception as err: # pylint: disable=broad-except
report_attr("trpc_datagram_connection_read_fatal_err", 1)
logger.error(f"[datagram_connection] fatal err while decoding datagram, remote_addr: {addr}, "
f"err: {err}", exc_info=True)
logger.error(
f"[datagram_connection] fatal err while decoding datagram, remote_addr: {addr}, "
f"err: {err}",
exc_info=True)
self.parser.reset()
return
if not frames:
Expand Down
1 change: 0 additions & 1 deletion trpc_sdk/transport/_server/_stream_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
# Copyright (C) 2026 Tencent. All rights reserved.
#
# tRPC-Python is licensed under Apache-2.0.

"""Streaming server transport that maps (session, stream) to a write handle.

Inherits :class:`TrpcServerTransport`, so ``bind`` (pre-fork, options only)
Expand Down
28 changes: 16 additions & 12 deletions trpc_sdk/transport/_server/_trpc_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
# Copyright (C) 2026 Tencent. All rights reserved.
#
# tRPC-Python is licensed under Apache-2.0.

"""tRPC server transport: record options before fork, listen after it.

:meth:`TrpcServerTransport.bind` runs in ``ServiceAdapter.init_transport`` and only keeps the
Expand Down Expand Up @@ -113,7 +112,8 @@ class TrpcServerTransport(BaseServerTransport):
default. A subclass serving another protocol over the same listen
machinery calls :meth:`set_io_connection`, overrides :meth:`_msg_handler`
and, when the framing differs, sets another ``opts.parser``; its
connections are then :class:`DefaultIoConnection` objects. Datagrams are always served by :class:`DefaultDatagramConnection`, which
connections are then :class:`DefaultIoConnection` objects. Datagrams
are always served by :class:`DefaultDatagramConnection`, which
hands every frame to :meth:`_msg_handler`.

Attributes:
Expand Down Expand Up @@ -248,11 +248,12 @@ def protocol_factory(self, shared_pool: Optional[Any] = None) -> Callable[[], An
"""
opts = self.opts
if self._io_connection is None:
return partial(TCPConnection,
TcpConnectionOption(parser=opts.parser,
rpc_handler=opts.rpc_handler,
timeout=opts.idle_timeout,
shared_pool=shared_pool))
return partial(
TCPConnection,
TcpConnectionOption(parser=opts.parser,
rpc_handler=opts.rpc_handler,
timeout=opts.idle_timeout,
shared_pool=shared_pool))
return partial(self._io_connection, ConnectionOption(parser=opts.parser, msg_handler=self._msg_handler))

async def _msg_handler(self, conn: ServerConnection, data: bytes) -> None:
Expand Down Expand Up @@ -309,9 +310,7 @@ def datagram_protocol_factory(self, shared_pool: Optional[Any] = None) -> Callab
"""
opts = self.opts
return partial(DefaultDatagramConnection,
ConnectionOption(parser=opts.parser,
msg_handler=self._msg_handler,
shared_pool=shared_pool))
ConnectionOption(parser=opts.parser, msg_handler=self._msg_handler, shared_pool=shared_pool))

def create_socket(self, reuse_port: bool, stream: bool = True) -> socket.socket:
"""Create and bind one socket on the configured address.
Expand Down Expand Up @@ -347,8 +346,13 @@ def _serve_stream(self,
"""
sock = self.create_socket(model.reuse_port)
try:
listener = serve_stream(model, group, sock, factory_for, self.opts.backlog,
ssl_context=ssl_context, address=self.opts.address)
listener = serve_stream(model,
group,
sock,
factory_for,
self.opts.backlog,
ssl_context=ssl_context,
address=self.opts.address)
except BaseException:
sock.close()
raise
Expand Down
Loading