English | 中文
In tRPC-Python, a task plugin is a set of child processes for background batch computation. A worker that handles network requests can send work that blocks, or that should be batched, such as model inference or CPU-heavy computation, to a task child process. The task process calls the user function on a batch, then returns each result to the worker asynchronously.
A task plugin differs from an ordinary plugin in a few ways:
- It does not subclass
BasePlugin. It has noinit()/start()lifecycle. Register it withregister_task. That function always usesPluginType.TASK, butPluginSpec.clsisNone, soget_plugin(PluginType.TASK, name)does not return a callable object. - There is no
plugins.taskconfiguration. A task exists only ifregister_taskran when the module was imported. The service entry must import the module that defines the task before it callsserve(). - It is not a timer or an RPC child process. A scheduled job uses
TimerTask. An RPC child process undermulti_process+separateis a network worker.register_taskis for a batch-computation child process, or for other compute-heavy work.
The data path is:
flowchart LR
worker[Worker handler coroutine] --> send["send_to_task"]
send --> queue[Task request queue]
queue --> consumer[TaskConsumer thread]
consumer --> handler["handler(batch)"]
handler --> rspQueue[Response queue]
rspQueue --> future[Worker Future]
The public type is trpc_sdk.abc.TaskInfo. The function is trpc_sdk.plugin.register_task:
from trpc_sdk.abc import TaskInfo
from trpc_sdk.plugin import register_task
def handler(reqs):
return [req.upper() for req in reqs]
def initializer():
return None
register_task(
"normal_task",
TaskInfo(
name="normal_task",
handler=handler,
initializer=initializer,
batch_size=5,
),
)register_task overwrites task_info.name with the name passed as the first argument. Keep the two strings the same.
TaskConsumer requires the following of handler:
- The output list matches the input, index by index.
reqsis alist, and the return value must be alistof the same length, with each index corresponding to the input. If the return value is not alist, the lengths differ, orhandlerraises, every caller in that batch receivesTaskHandleError. - It may be a synchronous function or a coroutine function. The consumer thread uses
inspect.iscoroutinefunctionto choose the synchronous or asynchronous entry, and the thread has its own event loop. Do not use the worker process's event loop insidehandler. - It receives business objects directly. Each element of the batch is the
reqpassed tosend_to_task. The framework serializes and deserializes across processes with the task codec. The business function does not handle bytes itself.
handler(required): the batch function.initializer(optional): called once in each task child process, afterforkand before it starts receiving requests from the pipe. SeeTaskProcess._prepare_to_serve. This is the place to load a model or initialize other process-local resources. It runs in the task child process, not in the worker. If it raises, that task child process does not start.batch_size: the maximum number of requests in one batch for this task. When<= 0, the server settingserver.task_batch_sizeis used instead.task_num: how many task child processes to start. The default is1. With more than one, the process names look likenormal_task:0andnormal_task:1.send_to_taskstill uses the registered name"normal_task". The framework chooses the connection.pause_heartbeat: defaultFalse. WhenTrue,build_task_infospauses naming-service heartbeats beforeforkand resumes them after the child process'sinitializerreturns. Turn this on only when the service should become discoverable after the task child has finished initializing. Ordinary cases keep the defaultFalse.
Only worker-side processes can send to a task. Task processes cannot call each other.
When the service starts, the framework brings up and connects the task child processes in this order:
- Register the task. Importing the service entry runs
register_task, which writes the task into the registry.get_registered_tasks()can list them. - Build the process topology. After
serve(), and beforefork,_spawn_children()runs.build_task_infos()makes one process record for eachtask_num, and callspause_registry_heartbeat()when needed. - Create the inter-process pipes.
TaskIpcMeshcreates the worker-to-tasksocketpairpipes beforefork. - Start the child and initialize plugins. After
fork, theTaskProcesschild callstask_init(queue_size)to set up the underlying queue, theninit_task_plugins(). That call initializes only plugins markedPluginInitPos.TASKthat are notUSER_DEFINED. - Run the consumers. The child closes unrelated file descriptors, calls
initializer(), then startsTaskConsumerthreads according toserver.task_consumer_num, and runsconsume_responseon the main event loop to send results back. At the same time, the worker callsregister_task_connon its own service event loop to register the task connections.
Inside a worker RPC handler, await send_to_task(ctx, task_name, req) to send the request to a task child process:
from trpc_sdk.task import send_to_task
async def SayHello(self, ctx, request):
rsp = await send_to_task(ctx, "normal_task", request.message)
return rsp- How a batch forms.
TaskConsumertakes at mostbatch_sizerequests from the queue and passes that list tohandler.batch_sizeis an upper bound: a single waiting request is still handled as its own batch. When the queue is empty, the consumer thread sleepstask_wait_intervalmilliseconds and checks again. - Common errors:
RuntimeError: the current thread has no task connection table.send_to_taskmust run on the worker's service event loop. Whenhandle_thread_num > 0, a separate I/O thread has no task pipe and cannot call it.TaskHandleError:task_nameis not registered (the resolved connection isNone), orhandlerin the task child process raises or returns a list of the wrong length.
Queue and consumer settings live under server, not under plugins. The defaults are in trpc_sdk/config/_types.py and _constants.py:
server:
task_batch_size: 1
task_wait_interval: 5
task_queue_size: 1000
task_consumer_num: 1task_batch_size: the global default batch limit, default1. It applies only when that task'sTaskInfo.batch_size <= 0. A positive value set at registration, such asbatch_size=5in the example above, wins over this setting.task_wait_interval: how long the consumer thread sleeps when the task queue is empty, default5, in milliseconds.task_queue_size: the request-queue capacity of each task child process, default1000.task_consumer_num: how manyTaskConsumerthreads each task child process starts, default1.