Waypoints GET refetches every route after each best solution, one at a time, under one lock

オープン
#2,805 コメント 0 件 リアクション 0 件 担当者 1 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

@rsynek がすでに取り組んでいます。

2026年11月5日 から。

評価

この issue はまだ評価されていません。

説明

component/service

Describe the bug
GET /v1/route-plans/{id}/waypoints is slow while a run solves, and the map in the visualization waits for it.

WaypointsServiceImpl on main has three causes:

  1. onBestSolution (WaypointsServiceImpl.java:116) replaces the whole route map of the run. The next GET fetches every route from the map service again, also the routes whose stops did not change.
  2. getWaypoints (WaypointsServiceImpl.java:61) is synchronized on the bean, so one lock serves all runs. It calls the map service one route at a time, and it holds the lock through the retry delays.
  3. runIdsToWaypoints (WaypointsServiceImpl.java:49) never removes a run. It is also the only source of waypoints for a finished run, so memory grows with each run until a restart.

Think of a courier who throws away every drawn route after each new order. Then the courier redraws each route, also the unchanged ones, with the one pen that all couriers share.

Expected behavior
A GET after a best solution fetches only the routes whose stops changed, fetches them in parallel, and does not block the other runs. The service removes a finished run from memory, and a later GET reads the stored waypoints of that run.

Actual behavior
Measured with a fake map service at 150 ms per call, 50 vehicles, and a best solution that changes 5 routes:

Time Map service calls
GET after a best solution 7,829 ms 50
First GET 7,877 ms 50

Field Service Routing 2.7.0 in dev mode with a local OSRM (Open Source Routing Machine) server, 45 vehicles with about 175 vehicle shifts (one route for each shift): a GET after a best solution takes 1.13 s to 2.26 s.

To Reproduce

  1. Start a routing model with a map service, for example Field Service Routing with a local OSRM server.
  2. Solve a dataset with 45 vehicles or more.
  3. Call GET /v1/route-plans/{id}/waypoints every 2 seconds while the run solves, and log the time of each call.

Each GET after a best solution takes more than a second, and the map service log shows one call per route.

Environment

Timefold Solver Version or Git ref: 2.8.0 (main at 6741d1043)

Output of java -version: not relevant

Output of uname -a or ver: not relevant

Additional information

PR #2723 holds a solution for this bug. I closed it and opened this issue, so the owner of the service module can take over the change. Its approach:

  • Keep a route while its stops are equal, and fetch only the changed routes.
  • Fetch the missing routes in parallel on a dedicated pool (timefold.platform.map-service.waypoints-parallelism, default 8), without the global lock. Requests at the same time for the same route share one fetch.
  • For a viewed run, fetch the changed routes in the background after a best solution, at most one batch at a time for each run.
  • Release a run after SaveWaypointsPostProcessor stores its waypoints, or when the run fails. A later GET reads the waypoints from storage. A bounded list of released run ids stops a late event from adding a released run again.
  • Send FAILED with the run id when the run metadata cannot be read. Without this, a failed run is not released: SolverWorker.notifyOnFailure throws a NullPointerException when getMetadata returns null.

With the change of PR #2723, the same measurement with the fake map service gives 160 ms and 5 calls for a GET after a best solution. The Field Service Routing measurement gives 20 ms to 60 ms.

主要言語
Java
スター
1.8k
フォーク
243
平均マージ
2日 4時間
マージ済み PR(30日)
46

環境構築

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

TimefoldAI/timefold-solver のほかの issue

TimefoldAI/timefold-solver の issue をすべて見る

似ている issue

Java の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。