From 610df7c5a4d604e548eb5cabae4d356ea715f896 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Sun, 26 Jul 2026 23:39:36 +0200 Subject: [PATCH] bug6241: two-round retype-and-store to actually see estimator wind fixes The previous single-round retypeAndStoreAfterWindEstimationSettled was incorrect: it called waitUntilDone on the estimator before any spots had been re-emitted (the compute-path's initial detection ran with a null WindEstimationInteraction because the estimator hadn't been installed yet, so no spots were pushed). waitUntilDone then returned immediately on an empty queue, and the subsequent recalculate emitted spots but returned before the estimator could process them and produce wind fixes. Net effect: stored maneuvers were still typed with null wind (mostly UNKNOWN), reproducing the poisoned-DB pattern the fix was supposed to prevent. Split into two rounds: 1. First recalculate + get per competitor. Since setWindEstimation cleared maneuverDetectorPerCompetitorCache, this builds a fresh detector with a non-null WindEstimationInteraction, runs a full scan, and *does* emit spots to the estimator during this pass. 2. waitUntilDone -- now the estimator's queue has the spots from round 1 and this actually waits for the graph inference to produce wind fixes and flow them into the tracked race's MANEUVER_BASED_ESTIMATION track. 3. Second recalculate + get per competitor. The detector's lastManeuverDetectionResult is now populated and no new raw fixes have arrived, so it takes the re-type branch of detectManeuverSpots -- which routes through determineManeuversFromManeuverCurve and types the spots using trackedRace.getWind(...), which now sees the estimator's fixes (bug6274 removed the MANEUVER_BASED_ESTIMATION exclusion). The isManeuverSpotWindNearlySame check correctly detects that the previous typing had null wind and the current has non-null, so it doesn't short-circuit to the UNKNOWN spot. 4. Snapshot the classified maneuvers and store. For W1 in 49erFX (5 competitors), pre-fix Mongo had 1 competitor with 362 typed maneuvers (whose detection happened to run after setWindEstimation) and 1 with 628 all-UNKNOWN (whose detection ran before setWindEstimation). Post-fix, both should be typed. --- .../impl/ManeuverCacheDelegate.java | 39 ++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/maneuverhash/impl/ManeuverCacheDelegate.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/maneuverhash/impl/ManeuverCacheDelegate.java index b0a53288bad..69d3390ea1c 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/maneuverhash/impl/ManeuverCacheDelegate.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/maneuverhash/impl/ManeuverCacheDelegate.java @@ -176,23 +176,52 @@ public class ManeuverCacheDelegate implements SerializableManeuverCache { } /** - * Second phase of {@link #computeAndStore()}: waits for the wind estimator to drain, triggers - * per-competitor recalculation so maneuvers get re-typed with the estimator's wind fixes, - * then snapshots and persists. Invoked from - * {@link com.sap.sailing.domain.tracking.TrackedRace#runWhenWindEstimationInstalled(Runnable)}. + * Second phase of {@link #computeAndStore()}: sequences maneuver-cache re-typing so that the + * stored maneuvers reflect the wind fixes the estimator produces from the spots emitted by + * the detector. + *

+ * + * The choreography is two-round: the first round of {@link ManeuverCache#recalculate} + * triggers a fresh full-scan pass of the detector for each competitor (their per-competitor + * detectors were {@link TrackedRaceImpl#setWindEstimation cleared} when the wind estimator + * was installed, so the fresh detector emits spots into the now-non-null + * {@code WindEstimationInteraction}). The estimator asynchronously processes those spots and + * publishes wind fixes on its {@code MANEUVER_BASED_ESTIMATION} track. Once the estimator + * has drained (via {@code waitUntilDone}), a second round of recalculation re-types the + * spots using the newly-available wind and captures the classified maneuvers, which are then + * persisted. See bug6241; without this sequencing the initial recalc runs while the + * estimator has queued but not yet processed the spots, so typing still sees no wind and + * produces UNKNOWN maneuvers which then get persisted permanently. */ private void retypeAndStoreAfterWindEstimationSettled() { + // Round 1: trigger fresh detection per competitor. Their detector cache was cleared when + // setWindEstimation ran, so new detectors are built with a non-null wind-estimation + // interaction and emit spots to the estimator during this pass. + for (final Competitor competitor : race.getRace().getCompetitors()) { + cacheToUse.recalculate(competitor); + cacheToUse.get(competitor, /* waitForLatest */ true); + } + // Wait for the estimator to drain the spots emitted in Round 1 and produce its wind + // fixes. Those fixes flow into the tracked race's MANEUVER_BASED_ESTIMATION track and + // become visible to getWind(pos, at) lookups. final IncrementalWindEstimation windEstimation = race.getWindEstimation(); + boolean estimatorSettled = true; if (windEstimation != null) { try { windEstimation.waitUntilDone(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); + estimatorSettled = false; logger.log(Level.WARNING, "Interrupted while waiting for wind estimation to finish inference for race " +race.getRaceIdentifier()+"; skipping maneuver re-type and store"); } } - if (!Thread.currentThread().isInterrupted()) { + if (estimatorSettled) { + // Round 2: recalculate per competitor. This time the detector's + // lastManeuverDetectionResult is populated from Round 1 and no new raw fixes have + // arrived, so it takes the re-type branch of detectManeuverSpots (line 150 of + // IncrementalManeuverDetectorImpl) and re-types the existing spots using the current + // wind -- which now includes the estimator's fixes. final Map> maneuvers = new HashMap<>(); for (final Competitor competitor : race.getRace().getCompetitors()) { cacheToUse.recalculate(competitor);