Liliya Angelova
11/20/2025, 2:01 PM@task(cache_policy=NO_CACHE)
def processing_search_results(
search_results, distance, rabbitmq_secret_block, pg_secret_block, config, search_parameters
):
logger = get_run_logger()
<http://logger.info|logger.info>(f"Start processing: {datetime.now()}")
proc_pg_utils = build_pg_utils_from_block(pg_secret_block)
processed_batch = process_single.map(unmapped(config), search_results, distance).result(raise_on_failure=True)
save_requests_pg(processed_batch, logger, proc_pg_utils)
<http://logger.info|logger.info>("Requests data persistence completed.")
<http://logger.info|logger.info>(f"Publish message: {datetime.now()}")
publish_refresh_message(
rabbitmq_secret_block,
search_parameters["tab_id"],
search_parameters["job_id"],
"REFRESH",
)
@flow(task_runner=ThreadPoolTaskRunner())
def timocom_search_flow(
search_parameters: dict,
pg_secret_block: str,
timocom_secret_block: str,
rabbitmq_secret_block: str,
ors_server_variable: str,
price_calculation_server_variable: str,
predictive_scoring_server_variable: str,
here_maps_api_key_variable: str,
):
logger = get_run_logger()
<http://logger.info|logger.info>(f"Starting Timocom search with parameters: {search_parameters} at {datetime.now()}")
try:
sanitized_search_parameters = sanitize_and_validate_search_parameters(search_parameters)
pg_utils = build_pg_utils_from_block(pg_secret_block)
# Timocom credentials are stored as "tenant_id": {"user": "username", "pass": "password"}
creds = Secret.load(timocom_secret_block).get()
tenant_ref = search_parameters["tenant_ref"]
<http://logger.info|logger.info>(f"Using Timocom credentials for tenant: {tenant_ref}")
tenant_creds = creds.get(tenant_ref)
if not tenant_creds:
raise ValueError(f"No Timocom credentials found for tenant {tenant_ref}")
user = tenant_creds["user"]
password = tenant_creds["pass"]
# Initialize Timocom instance
timocom_instance = Timocom(logger, pg_utils, user, password)
# Create search tasks
search_tasks = timocom_instance.create_search_tasks(sanitized_search_parameters)
search_futures = timocom_instance.search.map(search_tasks)
<http://logger.info|logger.info>("Timocom search process completed.")
config = get_config(
ors_server_variable,
price_calculation_server_variable,
predictive_scoring_server_variable,
here_maps_api_key_variable,
)
# Process searches in batches of 5
for batch in chunked(search_futures, 5):
<http://logger.info|logger.info>(f"Processing batch of {len(batch)} results...")
distance_mapping = get_distance_mapping(batch, config)
processing_search_results.map(
batch,
distance_mapping,
unmapped(rabbitmq_secret_block),
unmapped(pg_secret_block),
unmapped(config),
unmapped(search_parameters),
).result(raise_on_failure=True)
<http://logger.info|logger.info>("Post-processing of freight requests completed.")
except Exception as e:
logger.error(f"Error during Timocom search initiation: {e}")
publish_refresh_message(
rabbitmq_secret_block, search_parameters["tab_id"], search_parameters["job_id"], "FAILED"
)
raiseMarvin
11/20/2025, 2:01 PMMarvin
11/20/2025, 2:05 PMprocessing_search_results) and each parent then calls process_single.map(...).result(...). While those parents wait on their children, they continue occupying worker threads. If your ThreadPoolTaskRunner doesn’t have enough threads, the child tasks won’t get scheduled and everything “looks parallel” but effectively runs serially.
- How to confirm: temporarily set a very low max_workers (e.g., 5) and watch everything stall. Then increase it a lot and see throughput improve.
- Fixes:
- Don’t map inside a task when you’ll also run many of those tasks in parallel. Move the inner .map up to the flow level or use a subflow to isolate a separate runner.
- If you keep the nested map, raise max_workers so there are enough threads for both the “outer” tasks and their “inner” mapped tasks:
@flow(task_runner=ThreadPoolTaskRunner(max_workers=32))
def timocom_search_flow(...):
...
- Avoid calling .result() on large collections inside a task; return the futures and resolve them in the flow, or break the follow‑up (save/publish) into separate tasks so the parent isn’t blocking a thread.
2) Heavy synchronous work done serially in the flow thread
- In your loop you call get_distance_mapping(batch, config) directly in the flow. If it does API calls or heavy computation, you’re blocking the flow thread and gating every batch.
- Fix: make it a task (and parallelize it if possible):
@task
def get_distance_mapping_task(batch, config):
return get_distance_mapping(batch, config)
distance_mapping = get_distance_mapping_task.submit(batch, config)
Then pass unmapped(distance_mapping) (a single future) into downstream mapping.
3) CPU-bound work under ThreadPoolTaskRunner
- If process_single or distance/scoring logic is CPU heavy, threads won’t help due to the GIL.
- Fix: move those CPU-bound pieces to ProcessPoolTaskRunner (ensure arguments/returns are picklable) or split them into separate CPU-focused tasks/work pool:
@flow(task_runner=ProcessPoolTaskRunner(max_workers=4))
def cpu_heavy_flow(...):
...
4) External bottlenecks (API rate limits, DB contention)
- If the upstream API or your database is rate limited, more concurrency won’t help and may worsen tail latency.
- Fixes:
- Tune batch size (your 5) to the service’s limits.
- Add client-side rate limiting/backoff.
- Bulk-insert to the DB and move save_requests_pg into its own task so it doesn’t block the parent thread. If you’re doing per-row inserts, switch to batched operations.
5) Large data marshaling and blocking waits
- Calling .result(raise_on_failure=True) inside processing_search_results forces all results to be materialized in that task thread. If the payloads are large, you’re paying serialization/memory costs and holding the worker.
- Fix: return futures to the flow and aggregate there, or do a bulk-save task that accepts futures and resolves inside it.
Concrete refactor sketch to avoid thread starvation:
- Make the pre-processing and post-processing separate tasks
- Keep mapping at the flow level (or isolate with a subflow)
```
@task
def get_distance_mapping_task(batch, config):
return get_distance_mapping(batch, config)
@task
def save_requests_pg_task(processed_batch, pg_secret_block):
proc_pg_utils = build_pg_utils_from_block(pg_secret_block)
save_requests_pg(processed_batch, get_run_logger(), proc_pg_utils)
@task
def publish_refresh_task(rabbitmq_secret_block, tab_id, job_id):
publish_refresh_message(rabbitmq_secret_block, tab_id, job_id, "REFRESH")
@flow(task_runner=ThreadPoolTaskRunner(max_workers=32))
def timocom_search_flow(...):
...Marvin
11/20/2025, 2:05 PMprocess_single.map(...) handle per search? And what does process_single actually do (API calls vs heavy compute)?
- Have you set max_workers explicitly yet?
- Does get_distance_mapping do network calls? If so, are they parallelizable?
- Are you seeing API rate limits or DB locks/contention?
If you share those details, I can suggest an exact sizing for max_workers and the best split between threads vs processes.
Related docs:
- Task runners overview: Task runners
- Concurrency and mapping: Concurrency and parallelism and Mapping (see notes on nested mapping)Liliya Angelova
11/20/2025, 2:16 PM