-
Notifications
You must be signed in to change notification settings - Fork 0
Running at Scale
NavigationSystem owns one, but you can drive it directly.
try (var paths = new AsyncEntityPathfindingService(
Runtime.getRuntime().availableProcessors(), 256)) {
CompletableFuture<PathResult> future =
paths.submitLatest(entity.getUuid(), request);
}Cancelling the future cooperatively stops its active A* search. submitLatest
cancels the previous unfinished request for that key, so a stale path can never
beat a newer target. Closing cancels running and queued work, then waits for
its workers.
Controllers created by NavigationSystem add a bounded admission layer ahead
of the executor: newest intent per actor, at most
maximumConcurrentControllerSearches(...) at once, and actors whose route is
exhausted preferred over speculative replanning. Direct service submissions
bypass all of it.
Submitting beyond capacity sheds the search. The future completes normally
with PathResult.SHED — an empty route, not an exception.
PathResult result = navigation.submit(request).join();
if (result.shed()) return; // keep the current route, retry later
if (result.found()) follow(result);A shed replan is routine backpressure: the mob keeps the route it already has
for another tick. One shared PathResult.SHED instance answers every shed, so
sustained backpressure allocates nothing, and each shed still increments
searches().rejected(). /nav shed in the demo shows the whole contract.
NavigationMetricsSnapshot before = navigation.metricsSnapshot();
// ... one sampling interval later
NavigationMetricsSnapshot after = navigation.metricsSnapshot();
double seconds = after.elapsedSecondsSince(before);
long tailMicros = after.latency().since(before.latency()).p99Micros();| Field | Contents |
|---|---|
searches() |
submitted, completed, failed, cancelled, rejected, superseded, terminated(), inFlight()
|
latency() |
submission to result, for completed searches |
queueWait() |
submission to execution start, for searches that ran |
controllerOutcomes() |
outcomes plus stallRate() and travelStallRate()
|
adaptiveSources() |
one counter per SharedMeshSource, plus sharedHitRate()
|
stalePlans() |
plans refused because the entity moved past the start tolerance |
queue() |
activeSearches, queuedSearches, queueCapacity, parallelism
|
Alarm on
travelStallRate(), notstallRate().
Both have stuck as the numerator; the denominators differ. total() also
counts a navigation issued to a mob already standing on its goal, which
completes on its first tick without moving. That is not a rare edge: a mob that
has arrived and replans once a second contributes one per replan, and in a
representative soak they are over half of all outcomes. So stallRate() reads
several times lower than the stalling actually observed.
travelStallRate() >= stallRate() always holds, so the travel-filtered rate
can never hide a stall the lifetime rate would have shown.
unreachableTargets() is deliberately not part of total(). The controller
stays PARTIAL and re-targetable, so a sustained rate there is a
goal-selection problem, not a navigation failure.
Counters are LongAdders and never reset, so a rate is a difference between
two snapshots. Latency buckets are powers of two microseconds and always round
up, so a reported tail is never optimistic.
Start here
Shaping behaviour
Going deeper
Reference