Skip to content

Commit eab2667

Browse files
committed
Tasks: inline bounded same-entity hook outputs
Ingestion hooks always sent generated tasks through the priority RabbitMQ path, even when they targeted the same entity and would return to the same worker thread. Mirrored TTL data therefore added a confirmed publish, broker delivery, parse, and acknowledgement cycle for every source task. Add a startup-configured bound for processing same-routing-key descendants through the complete TaskExecutor pipeline in a local FIFO. The default of zero preserves queue-only behavior; cross-entity work, overflow, and pending siblings after an inline failure continue through the priority queue. A saturated benchmark using the real data-mirroring TTL hook and a three-second master-buffer flush improved median throughput from 67 to 207 entities/s and reduced completion time from 29.70s to 9.68s. It also eliminated 2,000 priority publish/deliver/ack cycles and reduced median Mongo updates from 2,314 to 2,004; the database coalescing benefit remains workload-dependent.
1 parent 9440e6d commit eab2667

8 files changed

Lines changed: 362 additions & 10 deletions

File tree

config/processing_core.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ worker_processes: 1
2222
# external services via network)
2323
worker_threads: 16
2424

25+
# Maximum number of same-entity tasks generated by ingestion hooks to process inline.
26+
# Set to 0 to send all generated tasks through the priority queue. Changes require a worker restart.
27+
max_inline_generated_tasks: 0
28+
2529
# Path to directory with plug-in modules
2630
# (Relative path is evaluated relative to location of this configuration file)
2731
modules_dir: "../modules"

docs/configuration/processing_core.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ msg_broker:
1111
password: dp3_password
1212
worker_processes: 2
1313
worker_threads: 16
14+
max_inline_generated_tasks: 0
1415
modules_dir: "../dp3_modules"
1516
enabled_modules:
1617
- "module_one"
@@ -50,6 +51,22 @@ to utilize computational power of multiple CPUs (which Python cannot do well
5051
anyway due to the GIL), but to mask long I/O operations (e.g. queries to
5152
external services via network).
5253

54+
## Inline generated tasks
55+
56+
`max_inline_generated_tasks` is a non-negative integer that limits how many tasks generated by
57+
ingestion hooks may be processed inline for one task received from RabbitMQ. The default is `0`,
58+
which disables inline processing and sends every generated task through the priority queue.
59+
60+
Only generated tasks with the same complete entity key (entity type and EID) as the received task
61+
are eligible. The limit counts generated children, not the received source task, and is shared by
62+
the whole chain of children and descendants. Tasks for other entities and same-entity tasks above
63+
the limit are sent through the priority queue. An overflow task receives a fresh allowance if it is
64+
later consumed from RabbitMQ.
65+
66+
The setting is read when workers start, so changing it requires a worker restart. A small value such
67+
as `20` is recommended when first enabling the optimization; restore `0` and restart workers to
68+
return to queue-only processing.
69+
5370
## Modules directory
5471

5572
Path to directory with plug-in (secondary) modules.

docs/hooks.md

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,8 @@ Callable[[AnyEidT, DataPointTask], list[DataPointTask]]
193193
This hook is called once for a newly created entity, after `allow_entity_creation` has accepted creation and before the task's datapoints are staged into raw and master persistence.
194194
The callback receives the new `eid` together with the original `DataPointTask` that caused the entity to be created, so it can inspect the incoming datapoints through `task.data_points`.
195195
It should not assume that the entity's new state is already readable as a persisted master record, because the creation-triggering task has not been written through the normal persistence path yet.
196-
The hook may return a list of `DataPointTask` objects, and those tasks are queued back into the ingestion pipeline, where they trigger the usual hooks again.
197-
Because they originate from ingestion, they are pushed to the **priority** task queue.
196+
The hook may return a list of `DataPointTask` objects, and those tasks re-enter the ingestion pipeline, where they trigger the usual hooks again.
197+
Depending on `max_inline_generated_tasks`, same-entity outputs may be processed inline by the current worker; outputs for other entities and same-entity overflow are pushed to the **priority** task queue.
198198
This registration also supports `refresh=` and `may_change=` for recomputation during module-config refresh; see [Refresh-on-config-change behavior for ingestion hooks](#refresh-on-config-change-behavior-for-ingestion-hooks).
199199

200200
Real usage examples:
@@ -258,8 +258,9 @@ If `task.delete` is `True`, `on_task_start` still runs and entity deletion is pe
258258

259259
#### Returned tasks recurse through the same pipeline
260260

261-
Any `DataPointTask` returned by `on_entity_creation` or `on_new_attr` is sent back to the main task queue and later processed again by `TaskExecutor.process_task`.
262-
In other words, module-generated datapoints re-enter DP3 exactly like primary datapoints from the API, so they can trigger `on_task_start`, `allow_entity_creation`, `on_entity_creation`, `on_new_attr`, and later snapshot or updater hooks.
261+
Any `DataPointTask` returned by `on_entity_creation` or `on_new_attr` is processed again by `TaskExecutor.process_task`.
262+
Depending on worker configuration, a bounded number of same-entity tasks may run inline, while cross-entity tasks and same-entity overflow pass through the priority queue first.
263+
In either case, module-generated datapoints re-enter the complete ingestion pipeline, so they can trigger `on_task_start`, `allow_entity_creation`, `on_entity_creation`, `on_new_attr`, and later snapshot or updater hooks.
263264

264265
## Snapshot-time hooks: periodic processing over stored data
265266

@@ -610,17 +611,28 @@ This creates a feedback loop:
610611

611612
```text
612613
hook returns DataPointTask(s)
613-
-> task queue
614+
-> inline processing or task queue
614615
-> TaskExecutor.process_task
615616
-> ingestion hooks run again
616617
-> data reaches master/raw storage
617618
-> later snapshot / updater cycles can see it
618619
```
619620

620-
The queueing path differs slightly by hook family.
621-
Ingestion hooks such as `on_entity_creation` and `on_new_attr` push returned tasks to the priority queue.
622-
Snapshot hooks and updater hooks push returned tasks to the normal task queue.
623-
`scheduler_register` callbacks do not have an automatic task-return path.
621+
The re-entry path differs slightly by hook family.
622+
For ingestion hooks such as `on_entity_creation` and `on_new_attr`, workers may process a bounded
623+
number of same-entity outputs inline when `max_inline_generated_tasks` is positive. The source task
624+
does not count toward the limit, and the allowance is shared by all descendants generated while
625+
processing it. Outputs for another entity and same-entity outputs above the limit use the priority
626+
queue. Inline processing still calls the complete `TaskExecutor.process_task` pipeline; it does not
627+
make buffered writes immediately visible through independent database reads.
628+
629+
With the default `max_inline_generated_tasks: 0`, all ingestion-hook outputs use the priority queue,
630+
matching queue-only behavior. The value is read at worker startup. Enabling inline processing can
631+
change interleaving with tasks already assigned to the worker and removes the JSON serialization and
632+
RabbitMQ boundary for eligible outputs, so hooks must not depend on either boundary.
633+
634+
Snapshot hooks and updater hooks push returned tasks to the normal task queue and are not eligible for
635+
this inline path. `scheduler_register` callbacks do not have an automatic task-return path.
624636

625637

626638
## Type of `eid`

dp3/task_processing/task_distributor.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import sys
77
import threading
88
import time
9+
from collections import deque
910
from functools import partial
1011

1112
from dp3.common.config import PlatformConfig
@@ -66,6 +67,17 @@ def __init__(
6667
# List of worker threads for processing the update requests
6768
self._worker_threads = []
6869
self.num_threads = platform_config.config.get("processing_core.worker_threads", 8)
70+
self.max_inline_generated_tasks = platform_config.config.get(
71+
"processing_core.max_inline_generated_tasks", 0
72+
)
73+
if (
74+
isinstance(self.max_inline_generated_tasks, bool)
75+
or not isinstance(self.max_inline_generated_tasks, int)
76+
or self.max_inline_generated_tasks < 0
77+
):
78+
raise ValueError(
79+
"processing_core.max_inline_generated_tasks must be a non-negative integer"
80+
)
6981

7082
# Internal queues for each worker
7183
self._queues = [queue.Queue(10) for _ in range(self.num_threads)]
@@ -274,7 +286,7 @@ def _worker_func(self, thread_index):
274286
# Process the task
275287
start_time = time.time()
276288
try:
277-
created, new_tasks = self.task_executor.process_task(task)
289+
created = self._process_task_chain(task)
278290
except Exception:
279291
self.log.error(f"Error has occurred during processing task: {task}")
280292
raise
@@ -292,7 +304,61 @@ def _worker_func(self, thread_index):
292304
)
293305
)
294306

307+
def _process_task_chain(self, source_task: DataPointTask) -> bool:
308+
"""Process one broker task and its bounded same-key generated-task chain."""
309+
created, new_tasks = self.task_executor.process_task(source_task)
310+
if self.max_inline_generated_tasks == 0:
295311
self.push_new_tasks(new_tasks)
312+
return created
313+
314+
source_key = source_task.routing_key()
315+
pending = deque()
316+
admitted_count = 0
317+
inline_count = 0
318+
cross_key_count = 0
319+
overflow_count = 0
320+
321+
def partition_tasks(tasks: list[DataPointTask]) -> None:
322+
nonlocal admitted_count, cross_key_count, overflow_count
323+
324+
tasks_to_publish = []
325+
for task in tasks:
326+
if task.routing_key() != source_key:
327+
cross_key_count += 1
328+
tasks_to_publish.append(task)
329+
elif admitted_count < self.max_inline_generated_tasks:
330+
admitted_count += 1
331+
pending.append(task)
332+
else:
333+
overflow_count += 1
334+
tasks_to_publish.append(task)
335+
336+
if tasks_to_publish:
337+
self.push_new_tasks(tasks_to_publish)
338+
339+
partition_tasks(new_tasks)
340+
try:
341+
while pending:
342+
task = pending.popleft()
343+
_, new_tasks = self.task_executor.process_task(task)
344+
inline_count += 1
345+
partition_tasks(new_tasks)
346+
except Exception:
347+
if pending:
348+
self.push_new_tasks(list(pending))
349+
raise
350+
351+
if inline_count:
352+
self.log.debug(
353+
"Generated-task chain for %s: processed %d inline, "
354+
"published %d cross-key and %d overflow tasks",
355+
source_key,
356+
inline_count,
357+
cross_key_count,
358+
overflow_count,
359+
)
360+
361+
return created
296362

297363
def push_new_tasks(self, new_tasks):
298364
"""Push new tasks (resulting from hooks) to the priority queue.

dp3/template/app/config/processing_core.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ worker_processes: 1
2222
# external services via network)
2323
worker_threads: 16
2424

25+
# Maximum number of same-entity tasks generated by ingestion hooks to process inline.
26+
# Set to 0 to send all generated tasks through the priority queue. Changes require a worker restart.
27+
max_inline_generated_tasks: 0
28+
2529
# Path to directory with plug-in modules
2630
# (Relative path is evaluated relative to location of this configuration file)
2731
modules_dir: "../modules"

0 commit comments

Comments
 (0)