mirror of
https://github.com/eclipse-sailing-analytics/sailing-analytics.git
synced 2026-11-02 01:00:46 +00:00
bug6241: per-race scheduleWindEstimationInstallation primitive
Introduces a single per-race primitive in RacingEventServiceImpl that
composes TrackedRace.runWhenPastLoading with
PolarDataService.runWhenPolarLoadingFinishedFor to install the
maneuver-based wind estimation on a race only once BOTH of these are
true: the race has reached a status strictly beyond LOADING (so
whatever loading was to happen has finished), AND the polar-data
mining pipeline has fully drained this race's loaded fixes.
The primitive is called from two places, replacing two separate
installation code paths with one:
1. RaceAdditionListener.raceAdded, for races added while
windEstimationFactoryService is already registered.
2. setWindEstimationFactoryService (via
setWindEstimationOnAllTrackedRaces), for races that were already
present when the wind-factory service arrives later.
Under either arrival order the same guard applies: the install only
runs if the wind-factory reference is still the one we were scheduled
with, the race hasn't been removed or errored, and no other estimator
has been installed on it in the meantime. This removes the ordering
sensitivity between OSGi service arrival and race creation that
comment #17 on issue 6241 identified as a root cause.
PolarFixCacheUpdater simplifies dramatically: its only remaining job is
to trigger ingestion of the race's fixes into the polar-mining loading
pipeline on the first transition to a status past LOADING. It no longer
holds an installation callback, and it now also fires ingestion for
races that go directly PREPARED->TRACKING without ever entering
LOADING (previously that fallback branch fired only the install
callback, not the ingestion). This ensures the polar-loading gate in
PolarDataMiner always opens for such races, so any pending
runWhenPolarLoadingFinishedFor callbacks can proceed.
Also adds TrackedRace.getWindEstimation() so the installation
primitive can guard against duplicate installation (it was previously
write-only).
This commit is contained in:
1 parent
e3c0e0f187
commit
35298e66a0
6 files changed
+135
-30
No files matched your search
+5
@@ -1399,6 +1399,11 @@ public class MockedTrackedRace implements DynamicTrackedRace {
|
||||
public void setWindEstimation(IncrementalWindEstimation windEstimation) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public IncrementalWindEstimation getWindEstimation() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrackingConnectorInfo getTrackingConnectorInfo() {
|
||||
return null;
|
||||
|
||||
+5
@@ -912,6 +912,11 @@ public class MockedTrackedRaceWithStartTimeAndRanks implements TrackedRace {
|
||||
public void setWindEstimation(IncrementalWindEstimation windEstimation) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public IncrementalWindEstimation getWindEstimation() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrackingConnectorInfo getTrackingConnectorInfo() {
|
||||
return null;
|
||||
|
||||
@@ -825,6 +825,11 @@ public class DummyTrackedRace extends TrackedRaceWithWindEssentials {
|
||||
public void setWindEstimation(IncrementalWindEstimation windEstimation) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public IncrementalWindEstimation getWindEstimation() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrackingConnectorInfo getTrackingConnectorInfo() {
|
||||
return null;
|
||||
|
||||
@@ -1383,6 +1383,13 @@ public interface TrackedRace
|
||||
*/
|
||||
void setWindEstimation(IncrementalWindEstimation windEstimation);
|
||||
|
||||
/**
|
||||
* Returns the currently-installed maneuver-based wind estimation for this race, or
|
||||
* {@code null} if none is installed. Reflects the most recent successful
|
||||
* {@link #setWindEstimation(IncrementalWindEstimation)} call.
|
||||
*/
|
||||
IncrementalWindEstimation getWindEstimation();
|
||||
|
||||
/**
|
||||
* Obtains a quick, rough summary of the wind conditions during this race, based on a few wind samples at the
|
||||
* beginning, in the middle and at the end of the race. This is summarized in a min and max wind speed as well
|
||||
|
||||
+5
@@ -4173,6 +4173,11 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IncrementalWindEstimation getWindEstimation() {
|
||||
return windEstimation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setWindEstimation(IncrementalWindEstimation windEstimation) {
|
||||
final IncrementalWindEstimation previousWindEstimation = this.windEstimation;
|
||||
|
||||
+108
-30
@@ -2346,23 +2346,29 @@ Replicator {
|
||||
final TrackedRaceReplicatorAndNotifier trackedRaceReplicator = new TrackedRaceReplicatorAndNotifier(trackedRace);
|
||||
trackedRaceReplicators.put(trackedRace, trackedRaceReplicator);
|
||||
trackedRace.addListener(trackedRaceReplicator, /* fire wind already loaded */ true, /* notifyAboutGPSFixesAlreadyLoaded */ true);
|
||||
// the PolarFixCacheUpdater is asked to activate the wind estimation only once the "mining" of polar
|
||||
// data from the LOADING phase of races has finished; this way, the wind estimation track
|
||||
final PolarFixCacheUpdater polarFixCacheUpdater = new PolarFixCacheUpdater(trackedRace, () -> {
|
||||
// the PolarFixCacheUpdater will only start computing polars when the race has finished LOADING and then takes a while;
|
||||
// we may want to wait for all polars from race loading to have completed computing prior to
|
||||
// activating the wind estimation from maneuvers; so we register this callback that will runn
|
||||
// when the specific LOADING workflow chain inside the PolarDataMiner is done. See also bug6241
|
||||
if (windEstimationFactoryService != null) {
|
||||
trackedRace.setWindEstimation(
|
||||
windEstimationFactoryService.createIncrementalWindEstimationTrack(trackedRace)); // because the wind estimation from maneuvers needs polars for good results (or sometimes any results at all)
|
||||
}
|
||||
});
|
||||
// The PolarFixCacheUpdater's only responsibility is to feed newly-arriving
|
||||
// competitor positions to the polar-data service and, on the first status
|
||||
// transition past LOADING, to trigger ingestion of the race's loaded fixes into
|
||||
// the polar-mining loading pipeline. Installation of the maneuver-based wind
|
||||
// estimation is a separate concern handled by
|
||||
// scheduleWindEstimationInstallation below and by setWindEstimationFactoryService
|
||||
// if the factory arrives later.
|
||||
final PolarFixCacheUpdater polarFixCacheUpdater = new PolarFixCacheUpdater(trackedRace);
|
||||
polarFixCacheUpdaters.put(trackedRace, polarFixCacheUpdater);
|
||||
trackedRace.addListener(polarFixCacheUpdater);
|
||||
if (polarDataService != null) {
|
||||
trackedRace.setPolarDataService(polarDataService);
|
||||
}
|
||||
// Schedule installation of the maneuver-based wind estimation on this race.
|
||||
// The installation waits for (a) the race to reach a status strictly past
|
||||
// LOADING (so that whatever loading was to happen has finished) and (b) the
|
||||
// polar-data mining pipeline to have fully drained this race's loaded fixes.
|
||||
// If the wind-factory service is not registered yet, the install is deferred to
|
||||
// setWindEstimationFactoryService, which then iterates all existing races and
|
||||
// schedules an install for each. See bug6241.
|
||||
if (windEstimationFactoryService != null) {
|
||||
scheduleWindEstimationInstallation(trackedRace, windEstimationFactoryService);
|
||||
}
|
||||
numberOfTrackedRacesStillLoading.incrementAndGet();
|
||||
trackedRace.runWhenDoneLoading(()->numberOfTrackedRacesStillLoading.decrementAndGet());
|
||||
}
|
||||
@@ -2469,12 +2475,10 @@ Replicator {
|
||||
private class PolarFixCacheUpdater extends AbstractRaceChangeListener {
|
||||
|
||||
private final TrackedRace race;
|
||||
private final Runnable callbackWhenRaceChangingToTrackingOrFinishedStatus;
|
||||
private boolean callbackExecutionScheduled;
|
||||
private boolean ingestionTriggered;
|
||||
|
||||
public PolarFixCacheUpdater(TrackedRace race, Runnable callbackWhenRaceChangingToTrackingOrFinishedStatus) {
|
||||
public PolarFixCacheUpdater(TrackedRace race) {
|
||||
this.race = race;
|
||||
this.callbackWhenRaceChangingToTrackingOrFinishedStatus = callbackWhenRaceChangingToTrackingOrFinishedStatus;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -2486,18 +2490,20 @@ Replicator {
|
||||
|
||||
@Override
|
||||
public void statusChanged(TrackedRaceStatus newStatus, TrackedRaceStatus oldStatus) {
|
||||
if (oldStatus.getStatus() == TrackedRaceStatusEnum.LOADING
|
||||
&& newStatus.getStatus() != TrackedRaceStatusEnum.LOADING && newStatus.getStatus() != TrackedRaceStatusEnum.REMOVED) {
|
||||
if (polarDataService != null) {
|
||||
callbackExecutionScheduled = true;
|
||||
polarDataService.raceFinishedLoading(race, callbackWhenRaceChangingToTrackingOrFinishedStatus);
|
||||
}
|
||||
}
|
||||
// also trigger the callback in case the race moves across the LOADING status without ever entering it
|
||||
if (!callbackExecutionScheduled && callbackWhenRaceChangingToTrackingOrFinishedStatus != null
|
||||
&& newStatus.getStatus().getOrder() > TrackedRaceStatusEnum.LOADING.getOrder()) {
|
||||
callbackExecutionScheduled = true;
|
||||
callbackWhenRaceChangingToTrackingOrFinishedStatus.run();
|
||||
// Trigger ingestion of the race's loaded fixes into the polar-data mining loading
|
||||
// pipeline exactly once, on the first transition to a status past LOADING. This
|
||||
// fires both for the normal LOADING -> TRACKING/FINISHED path and for races that
|
||||
// move directly from PREPARED to TRACKING (e.g., RaceLogRaceTracker-tracked races
|
||||
// without loadable data); in the latter case the ingestion loop iterates over
|
||||
// empty tracks and the pipeline drains immediately, but the "gate" inside
|
||||
// PolarDataMiner still opens so any pending
|
||||
// runWhenPolarLoadingFinishedFor(...) callbacks (e.g., from
|
||||
// scheduleWindEstimationInstallation) can proceed. See bug6241.
|
||||
if (!ingestionTriggered && newStatus.getStatus().getOrder() > TrackedRaceStatusEnum.LOADING.getOrder()
|
||||
&& newStatus.getStatus() != TrackedRaceStatusEnum.REMOVED
|
||||
&& polarDataService != null) {
|
||||
ingestionTriggered = true;
|
||||
polarDataService.raceFinishedLoading(race, /* callback */ null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4727,8 +4733,23 @@ Replicator {
|
||||
try {
|
||||
final Iterable<DynamicTrackedRace> trackedRaces = trackedRegatta.getTrackedRaces();
|
||||
for (final TrackedRace trackedRace : trackedRaces) {
|
||||
trackedRace.setWindEstimation(
|
||||
service == null ? null : service.createIncrementalWindEstimationTrack(trackedRace));
|
||||
if (service == null) {
|
||||
// Tearing down the estimator: setWindEstimation(null) drops the current
|
||||
// estimator from the race and removes its wind track from the
|
||||
// MANEUVER_BASED_ESTIMATION source. Any per-race install task that was
|
||||
// still waiting on runWhenPastLoading / runWhenPolarLoadingFinishedFor
|
||||
// will observe on its final check that windEstimationFactoryService no
|
||||
// longer matches the service it was scheduled with (see
|
||||
// scheduleWindEstimationInstallation) and skip its install.
|
||||
trackedRace.setWindEstimation(null);
|
||||
} else {
|
||||
// Bringing the estimator up: for each race that already exists, schedule
|
||||
// an install through the per-race primitive so we wait for the race to
|
||||
// reach past LOADING and for its polar-mining fixes to drain before
|
||||
// constructing the estimator. This is the "wind-factory arrived after
|
||||
// races were added" side of the coordination; see bug6241.
|
||||
scheduleWindEstimationInstallation(trackedRace, service);
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
logger.log(Level.SEVERE, "Error reconstructing the wind estimation models for tracked races", e);
|
||||
@@ -4739,6 +4760,63 @@ Replicator {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules installation of the maneuver-based wind estimation on {@code trackedRace}.
|
||||
* Waits for two conditions before actually constructing and installing the estimator:
|
||||
* <ul>
|
||||
* <li>the race has reached a status strictly past
|
||||
* {@link TrackedRaceStatusEnum#LOADING} (see
|
||||
* {@link TrackedRace#runWhenPastLoading(Runnable)}). This covers the normal
|
||||
* LOADING -> TRACKING transition and the direct PREPARED -> TRACKING transition
|
||||
* used by, e.g., RaceLogRaceTracker-tracked races without loadable data. If the
|
||||
* race is removed from its regatta before the transition, the install is silently
|
||||
* cancelled.</li>
|
||||
* <li>the polar-data mining pipeline has drained this race's loaded fixes (see
|
||||
* {@link PolarDataService#runWhenPolarLoadingFinishedFor(TrackedRace, Runnable)}).
|
||||
* This is essential because the estimator captures the polar service at
|
||||
* construction time and uses it for classification and wind-speed inference; if
|
||||
* we installed the estimator before the polars for this race had been fed to the
|
||||
* polar-data service, the estimator would be permanently using a polar model that
|
||||
* reflects an incomplete data set.</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
*
|
||||
* The install is guarded against being obsoleted by a subsequent
|
||||
* {@code setWindEstimationFactoryService(...)} call replacing the factory: at
|
||||
* install time it re-checks that {@link #windEstimationFactoryService} is still the
|
||||
* {@code service} it was scheduled with, and that no other estimator has been installed
|
||||
* on the race in the meantime. It is also guarded against races that reached ERROR
|
||||
* status, on which installing the estimator is pointless.
|
||||
* <p>
|
||||
*
|
||||
* Safe to call multiple times for the same race and/or the same service; the install
|
||||
* itself is guarded against duplicate installation.
|
||||
* <p>
|
||||
*
|
||||
* The method returns immediately after scheduling; the actual install runs on the
|
||||
* shared background executor once both conditions above are satisfied. See bug6241.
|
||||
*/
|
||||
private void scheduleWindEstimationInstallation(final TrackedRace trackedRace,
|
||||
final WindEstimationFactoryService service) {
|
||||
trackedRace.runWhenPastLoading(() -> {
|
||||
final TrackedRaceStatusEnum status = trackedRace.getStatus().getStatus();
|
||||
if (status != TrackedRaceStatusEnum.ERROR && polarDataService != null
|
||||
&& windEstimationFactoryService == service) {
|
||||
polarDataService.runWhenPolarLoadingFinishedFor(trackedRace, () -> {
|
||||
synchronized (trackedRace) {
|
||||
if (trackedRace.getWindEstimation() == null
|
||||
&& windEstimationFactoryService == service
|
||||
&& trackedRace.getStatus().getStatus() != TrackedRaceStatusEnum.REMOVED
|
||||
&& trackedRace.getStatus().getStatus() != TrackedRaceStatusEnum.ERROR) {
|
||||
trackedRace.setWindEstimation(
|
||||
service.createIncrementalWindEstimationTrack(trackedRace));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setPolarDataServiceOnAllTrackedRaces(PolarDataService service) {
|
||||
Iterable<Regatta> allRegattas = getAllRegattas();
|
||||
for (Regatta regatta : allRegattas) {
|
||||
|
||||
Reference in new issue
Block a user