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.
This commit is contained in:
Axel Uhl committed 2026-07-26 23:39:36 +02:00
1 parent 8ce464986b
commit 610df7c5a4
1 file changed
+34 -5
@@ -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.
* <p>
*
* 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<Competitor, List<Maneuver>> maneuvers = new HashMap<>();
for (final Competitor competitor : race.getRace().getCompetitors()) {
cacheToUse.recalculate(competitor);