Skip to main content

Thread-pool powered extensions for Python XML-RPC servers.

Project description

xmlrpc_extended

CI Docs License: MIT Python 3.10+ Ruff Discussions

📖 Full documentation · Issues · Discussions

xmlrpc_extended is a small, dependency-light package that upgrades Python's stdlib XML-RPC server with bounded thread-pool concurrency and explicit overload handling.

Current package scope

  • ThreadPoolXMLRPCServer — drop-in SimpleXMLRPCServer replacement with a bounded ThreadPoolExecutor
  • bounded worker and pending-request limits with configurable overload policies (block, close, fault, HTTP 503)
  • graceful executor shutdown
  • request body size limiting through LimitedXMLRPCRequestHandler
  • max_pending counts queued requests beyond the active worker pool; total outstanding capacity is max_workers + max_pending
  • XMLRPCClient context manager with configurable timeout
  • SO_REUSEPORT scale-out helpers for Linux (xmlrpc_extended.multiprocess)
  • XMLRPCASGIApp — ASGI 3 adapter; deploy on uvicorn/hypercorn/granian with async def and sync handlers

Quickstart

from xmlrpc_extended import ServerOverloadPolicy, ThreadPoolXMLRPCServer


def add(left, right):
    return left + right


server = ThreadPoolXMLRPCServer(
    ("127.0.0.1", 8000),
    max_workers=8,
    max_pending=16,
    overload_policy=ServerOverloadPolicy.FAULT,
    logRequests=False,
)
server.register_introspection_functions()
server.register_function(add, "add")
server.serve_forever()

With max_workers=8, max_pending=16, up to 8 requests run concurrently and up to 16 additional requests may wait for a worker. After that, the configured overload policy is applied.

Overload semantics and queueing model

Capacity model

total outstanding capacity = max_workers + max_pending
Parameter Role
max_workers Maximum requests executing concurrently in the thread pool
max_pending Maximum requests waiting for a free worker slot
request_queue_size OS-level TCP accept backlog (before the application sees the request)

Default: max_pending=None resolves to max_workers, so by default the total capacity is 2 × max_workers. For latency-sensitive deployments set max_pending=0 to fail fast instead of queuing.

Overload policy behavior

Policy What happens when capacity is exhausted
BLOCK (default) The accept thread blocks until a worker slot opens. Requests queue at the OS level. Suitable for trusted, low-variance load.
CLOSE The connection is closed immediately without sending a response. The client receives a connection-reset error. Suitable for shedding load fast.
FAULT An XML-RPC fault response is returned with the configured overload_fault_code and overload_fault_string. Clients that speak XML-RPC can distinguish overload from method errors.
HTTP_503 An HTTP 503 Service Unavailable response is returned. Non-XML-RPC clients (e.g. HTTP health-checkers) can detect overload at the transport layer.

Recommended defaults

Deployment Suggested settings
Embedded / internal tool max_workers=4, max_pending=None (= 4), BLOCK
Service with SLA max_workers=N, max_pending=0, FAULT or HTTP_503
Behind a load balancer max_workers=N, max_pending=small, CLOSE or HTTP_503

Restricting accepted URL paths

By default the server accepts XML-RPC requests at / and /RPC2. Restrict this with rpc_paths:

server = ThreadPoolXMLRPCServer(
    ("127.0.0.1", 8000),
    rpc_paths=("/rpc",),  # only /rpc is accepted; all others return 404
)

Observability — server stats

ThreadPoolXMLRPCServer.stats() returns a ServerStats snapshot with the following counters:

Field Meaning
active Requests currently executing in worker threads
queued Requests submitted to the thread pool but not yet running
rejected_close Requests rejected with connection close (CLOSE policy)
rejected_fault Requests rejected with fault response (FAULT policy)
rejected_503 Requests rejected with HTTP 503 (HTTP_503 policy)
completed Requests that finished successfully
errored Requests that raised an exception in the handler
snap = server.stats()
print(f"active={snap.active} queued={snap.queued} completed={snap.completed}")

Client helpers

xmlrpc_extended.client provides XMLRPCClient, a context-manager wrapper with an explicit timeout:

from xmlrpc_extended.client import XMLRPCClient

with XMLRPCClient("http://127.0.0.1:8000/", timeout=5.0) as proxy:
    result = proxy.add(1, 2)

The default timeout is 30 seconds. Retrying failed requests is the caller's responsibility — XML-RPC methods are not necessarily idempotent.

Scale-out with SO_REUSEPORT (Linux only)

On Linux, multiple server processes can share the same port using SO_REUSEPORT:

from xmlrpc_extended.multiprocess import create_reuseport_socket, spawn_workers
from xmlrpc_extended import ThreadPoolXMLRPCServer

def run_worker():
    sock = create_reuseport_socket("0.0.0.0", 8000)
    server = ThreadPoolXMLRPCServer(("0.0.0.0", 0), bind_and_activate=False)
    server.socket = sock
    server.server_activate()
    server.register_function(lambda x: x + 1, "inc")
    server.serve_forever()

if __name__ == "__main__":
    processes = spawn_workers(run_worker, num_workers=4)
    for p in processes:
        p.join()

See xmlrpc_extended.multiprocess module docs for full details.

Development approach

This repository was bootstrapped with TDD:

  1. define server behavior in integration-style tests
  2. implement the public API to satisfy those tests
  3. keep the package small and compatible with stdlib XML-RPC patterns

Local validation

python -m pip install . --no-deps
python -m unittest discover -s tests -v

Benchmarks

pip install .
# All implementations compared side by side (Simple, Threaded, ThreadPool, ASGI)
python benchmarks/benchmark_all.py
# Focused ASGI in-process benchmark (higher-concurrency ASGI tuning)
python benchmarks/benchmark_asgi.py
# Thread-pool server vs stdlib SimpleXMLRPCServer (legacy script)
python benchmarks/benchmark_server.py --requests 200 --clients 8 --sleep 0.02

Security

⚠️ xmlrpc_extended is not suitable for untrusted public networks. See SECURITY.md for the full security posture, deployment checklist, and vulnerability reporting process.

  • Use max_request_size to reject oversized payloads early.
  • Restrict URL paths with rpc_paths.
  • Never set allow_dotted_names=True on handlers exposed to untrusted clients.

Request-size and request-shape expectations

LimitedXMLRPCRequestHandler enforces the following rules on every incoming POST request before any XML-RPC processing occurs:

Condition HTTP response
Content-Length header missing 411 Length Required
Content-Length non-integer / negative 400 Bad Request
Content-Length exceeds max_request_size 413 Payload Too Large
Transfer-Encoding: chunked 501 Not Implemented

Only requests with a valid, non-negative Content-Length within the configured limit are forwarded to the XML-RPC dispatcher. The default max_request_size is 1 MiB (1 048 576 bytes) and can be set via ThreadPoolXMLRPCServer's constructor.

Roadmap

Milestone Target Description
M2 0.3.0 Metrics hooks, observability examples, benchmarks ✅
M3 0.4.0 Multi-process / SO_REUSEPORT, HTTP 503 rejection, client helpers ✅
M4 0.5.0 ASGI adapter (XMLRPCASGIApp) ✅

ASGI / async integration

XMLRPCASGIApp is a drop-in ASGI 3 adapter bundled with the core package (no extra install required). Deploy on any ASGI server:

from xmlrpc_extended.asgi import XMLRPCASGIApp

app = XMLRPCASGIApp()

async def greet(name: str) -> str:
    return f"Hello, {name}!"

app.register_function(greet, "greet")
# uvicorn your_module:app --host 127.0.0.1 --port 8000

Sync handlers run in a thread pool via asyncio.to_thread; async def handlers execute directly in the event loop. See the ASGI Integration guide for deployment recipes, testing patterns, and migration notes from ThreadPoolXMLRPCServer.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

xmlrpc_extended-1.0.0rc1.tar.gz (34.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

xmlrpc_extended-1.0.0rc1-py3-none-any.whl (20.0 kB view details)

Uploaded Python 3

File details

Details for the file xmlrpc_extended-1.0.0rc1.tar.gz.

File metadata

  • Download URL: xmlrpc_extended-1.0.0rc1.tar.gz
  • Upload date:
  • Size: 34.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for xmlrpc_extended-1.0.0rc1.tar.gz
Algorithm Hash digest
SHA256 03d1bb58fde79c758fddaef4b958ec1056a73354fb7d79e37b7ada2353b130df
MD5 395f6fb6098768f3d3cb65c56068e963
BLAKE2b-256 04296e939198e6270e9b6b0a66f5f47b3e2a1991c4a576a772907558c295cd73

See more details on using hashes here.

File details

Details for the file xmlrpc_extended-1.0.0rc1-py3-none-any.whl.

File metadata

File hashes

Hashes for xmlrpc_extended-1.0.0rc1-py3-none-any.whl
Algorithm Hash digest
SHA256 395febdc67375b8a3878516784cc26271dbc3d0122b1d575f756f023c404ac17
MD5 92a05e1607c02d6cb4981e179ce2360f
BLAKE2b-256 2aff55dba9fbfdde627c916c0a7811ff351b3cf84eae800c7671c4ec368c12ef

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page