bug6275: merge

This commit is contained in:
Masha Kashirina
2026-08-12 11:44:30 +02:00
77 changed files with 3228 additions and 829 deletions
@@ -30,11 +30,6 @@ public abstract class AbstractCompactGPSFixMovingImpl extends AbstractCompactGPS
@Override
public abstract double getKnots();
@Override
public Position travelTo(Position pos, TimePoint from, TimePoint to) {
return pos.translateGreatCircle(getBearing(), this.travel(from, to));
}
@Override
public SpeedWithBearing applyCourseChange(CourseChange courseChange) {
return AbstractSpeedWithBearingImpl.applyCourseChange(this, courseChange);
@@ -360,7 +360,8 @@ public class RaceLogRaceTracker extends AbstractRaceTrackerBaseImpl<RaceLogConne
raceColumn.setRaceIdentifier(fleet, trackedRegatta.getRegatta().getRaceIdentifier(raceDef));
trackedRace = raceTrackingHandler.createTrackedRace(trackedRegatta, raceDef, sidelines, windStore,
params.getDelayToLiveInMillis(), WindTrack.DEFAULT_MILLISECONDS_OVER_WHICH_TO_AVERAGE_WIND,
boatClass.getApproximateManeuverDurationInMilliseconds(), null, /*useMarkPassingCalculator*/ true, raceLogResolver,
boatClass.getApproximateManeuverDurationInMilliseconds(), /* raceDefinitionSetToUpdate */ null,
/* useMarkPassingCalculator */ true, raceLogResolver,
/* Not needed because the RaceTracker is not active on a replica */ Optional.empty(),
new TrackingConnectorInfoImpl(RaceLogTrackingAdapter.NAME, RaceLogTrackingAdapter.DEFAULT_URL, /* no webUrl */ null),
markPassingRaceFingerprintRegistry, maneuverRaceFingerprintRegistry);
@@ -0,0 +1,129 @@
package com.sap.sailing.domain.test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import com.sap.sailing.domain.base.CompetitorWithBoat;
import com.sap.sailing.domain.common.TrackedRaceStatusEnum;
import com.sap.sailing.domain.tracking.DynamicTrackedRace;
import com.sap.sailing.domain.tracking.TrackingDataLoader;
import com.sap.sailing.domain.tracking.impl.DynamicTrackedRegattaImpl;
import com.sap.sailing.domain.tracking.impl.TrackedRaceStatusImpl;
import com.sap.sse.common.impl.MillisecondsTimePoint;
/**
* Tests {@link com.sap.sailing.domain.tracking.TrackedRace#runWhenPastLoading(Runnable)}.
* See bug 6241.
*
* @author Axel Uhl (d043530)
*/
public class TrackedRaceRunWhenPastLoadingTest extends TrackBasedTest {
private CompetitorWithBoat competitor;
private DynamicTrackedRace trackedRace;
@BeforeEach
public void setUp() {
competitor = createCompetitorWithBoat("Test Competitor");
trackedRace = createTestTrackedRace("Test Regatta", "Test Race", "505",
createCompetitorAndBoatsMap(competitor), MillisecondsTimePoint.now(),
/* useMarkPassingCalculator */ false);
}
/**
* When the race is already past LOADING at the time of the call, the callback must
* run immediately (synchronously on the caller's thread).
*/
@Test
public void testFiresImmediatelyWhenAlreadyPastLoading() {
final TrackingDataLoader loader = new TrackingDataLoader() {};
trackedRace.onStatusChanged(loader, new TrackedRaceStatusImpl(TrackedRaceStatusEnum.TRACKING, 1.0));
assertEquals(TrackedRaceStatusEnum.TRACKING, trackedRace.getStatus().getStatus());
final AtomicInteger firings = new AtomicInteger(0);
trackedRace.runWhenPastLoading(() -> firings.incrementAndGet());
assertEquals(1, firings.get(), "callback must fire immediately when race is already past LOADING");
}
/**
* When the race is in PREPARED (initial state) at the time of the call, the callback
* must not fire until the race transitions past LOADING. A PREPARED to TRACKING jump
* without ever entering LOADING must still fire the callback -- that's the whole
* point of "past LOADING" being the condition, matching, e.g., RaceLogRaceTracker
* races that go straight from PREPARED to TRACKING.
*/
@Test
public void testFiresOnPreparedToTrackingTransitionSkippingLoading() {
assertEquals(TrackedRaceStatusEnum.PREPARED, trackedRace.getStatus().getStatus());
final AtomicInteger firings = new AtomicInteger(0);
trackedRace.runWhenPastLoading(() -> firings.incrementAndGet());
assertEquals(0, firings.get(), "callback must not fire while race is in PREPARED");
final TrackingDataLoader loader = new TrackingDataLoader() {};
trackedRace.onStatusChanged(loader, new TrackedRaceStatusImpl(TrackedRaceStatusEnum.TRACKING, 1.0));
assertEquals(1, firings.get(), "callback must fire when race transitions past LOADING");
}
/**
* Firing happens exactly once even when multiple status transitions past LOADING occur.
*/
@Test
public void testFiresExactlyOnceAcrossMultipleTransitions() {
assertEquals(TrackedRaceStatusEnum.PREPARED, trackedRace.getStatus().getStatus());
final AtomicInteger firings = new AtomicInteger(0);
trackedRace.runWhenPastLoading(() -> firings.incrementAndGet());
final TrackingDataLoader loader = new TrackingDataLoader() {};
trackedRace.onStatusChanged(loader, new TrackedRaceStatusImpl(TrackedRaceStatusEnum.LOADING, 0.5));
assertEquals(TrackedRaceStatusEnum.LOADING, trackedRace.getStatus().getStatus());
assertEquals(0, firings.get(), "callback must not fire while race is still in LOADING");
trackedRace.onStatusChanged(loader, new TrackedRaceStatusImpl(TrackedRaceStatusEnum.TRACKING, 1.0));
assertEquals(1, firings.get(), "callback must fire once when race leaves LOADING");
trackedRace.onStatusChanged(loader, new TrackedRaceStatusImpl(TrackedRaceStatusEnum.FINISHED, 1.0));
assertEquals(1, firings.get(), "callback must not fire again on subsequent transitions");
}
/**
* When the race is removed from its regatta before ever transitioning past LOADING,
* the callback must not fire and the primitive must tear down its listeners.
*/
@Test
public void testDoesNotFireWhenRaceIsRemovedBeforeReachingPastLoading() throws InterruptedException {
// add the race to the regatta so that removeTrackedRace has an effect
final DynamicTrackedRegattaImpl regatta = (DynamicTrackedRegattaImpl) trackedRace.getTrackedRegatta();
regatta.addTrackedRace(trackedRace, Optional.empty());
assertEquals(TrackedRaceStatusEnum.PREPARED, trackedRace.getStatus().getStatus());
final AtomicInteger firings = new AtomicInteger(0);
trackedRace.runWhenPastLoading(() -> firings.incrementAndGet());
assertEquals(0, firings.get());
regatta.removeTrackedRace(trackedRace, Optional.empty());
// Give the asynchronous race-listener notification a chance to be processed.
// TrackedRegattaImpl uses AsynchronousRunnableExecutor for non-synchronous
// listeners, so the raceRemoved event fires on a background thread. We poll
// for a moment; the callback should never fire in either case.
final long deadline = System.currentTimeMillis() + 1000;
while (System.currentTimeMillis() < deadline && firings.get() == 0) {
Thread.sleep(20);
}
assertEquals(0, firings.get(), "callback must not fire when race was removed before reaching past LOADING");
}
/**
* When the race is removed <em>after</em> the callback has already fired (because
* the race reached past LOADING), removal is a no-op regarding the callback -- it
* must not fire a second time.
*/
@Test
public void testRemovalAfterFiringDoesNotCauseSecondFiring() {
final DynamicTrackedRegattaImpl regatta = (DynamicTrackedRegattaImpl) trackedRace.getTrackedRegatta();
regatta.addTrackedRace(trackedRace, Optional.empty());
final AtomicInteger firings = new AtomicInteger(0);
trackedRace.runWhenPastLoading(() -> firings.incrementAndGet());
final TrackingDataLoader loader = new TrackingDataLoader() {};
trackedRace.onStatusChanged(loader, new TrackedRaceStatusImpl(TrackedRaceStatusEnum.TRACKING, 1.0));
assertEquals(1, firings.get());
regatta.removeTrackedRace(trackedRace, Optional.empty());
assertEquals(1, firings.get(), "removal after firing must not cause a second firing");
}
}
@@ -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;
@@ -1408,6 +1413,10 @@ public class MockedTrackedRace implements DynamicTrackedRace {
public void runWhenDoneLoading(Runnable runnable) {
}
@Override
public void runWhenPastLoading(Runnable callback) {
}
@Override
public void runSynchronizedOnStatus(Runnable runnable) {
}
@@ -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;
@@ -921,6 +926,10 @@ public class MockedTrackedRaceWithStartTimeAndRanks implements TrackedRace {
public void runWhenDoneLoading(Runnable runnable) {
}
@Override
public void runWhenPastLoading(Runnable callback) {
}
@Override
public void runSynchronizedOnStatus(Runnable runnable) {
}
@@ -5,7 +5,6 @@ import com.sap.sse.common.CourseChange;
import com.sap.sse.common.Position;
import com.sap.sse.common.Speed;
import com.sap.sse.common.SpeedWithBearing;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.impl.AbstractSpeedWithAbstractBearingImpl;
import com.sap.sse.common.impl.AbstractSpeedWithBearingImpl;
import com.sap.sse.common.impl.KilometersPerHourSpeedImpl;
@@ -24,11 +23,6 @@ public class KilometersPerHourSpeedWithBearingImpl extends KilometersPerHourSpee
return bearing;
}
@Override
public Position travelTo(Position pos, TimePoint from, TimePoint to) {
return pos.translateGreatCircle(getBearing(), this.travel(from, to));
}
@Override
public SpeedWithBearing add(SpeedWithBearing other) {
return AbstractSpeedWithBearingImpl.add(this, other);
@@ -454,17 +454,18 @@ public class ManeuverDetectorWithEstimationDataSupportDecoratorImpl
Waypoint nextWaypoint = legAfter.getLeg().getTo();
for (Mark mark : nextWaypoint.getMarks()) {
Position nextMarkPosition = markPositionAtTimePointCache.getEstimatedPosition(mark);
Bearing absoluteBearing = maneuverEndPosition.getBearingGreatCircle(nextMarkPosition);
Bearing resultCandidate = absoluteBearing.getDifferenceTo(boatCourse);
if (result == null) {
result = resultCandidate;
} else if (Math.signum(result.getDegrees()) != Math.signum(resultCandidate.getDegrees())) {
result = new DegreeBearingImpl(0);
break;
} else if (Math.abs(result.getDegrees()) > Math.abs(resultCandidate.getDegrees())) {
result = resultCandidate;
if (nextMarkPosition != null) {
Bearing absoluteBearing = maneuverEndPosition.getBearingGreatCircle(nextMarkPosition);
Bearing resultCandidate = absoluteBearing.getDifferenceTo(boatCourse);
if (result == null) {
result = resultCandidate;
} else if (Math.signum(result.getDegrees()) != Math.signum(resultCandidate.getDegrees())) {
result = new DegreeBearingImpl(0);
break;
} else if (Math.abs(result.getDegrees()) > Math.abs(resultCandidate.getDegrees())) {
result = resultCandidate;
}
}
}
}
return result;
@@ -6,9 +6,11 @@ import java.io.ObjectOutputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.common.ManeuverType;
import com.sap.sailing.domain.maneuverhash.ManeuverCache;
import com.sap.sailing.domain.maneuverhash.ManeuverRaceFingerprint;
import com.sap.sailing.domain.maneuverhash.ManeuverRaceFingerprintFactory;
@@ -17,6 +19,7 @@ import com.sap.sailing.domain.maneuverhash.SerializableManeuverCache;
import com.sap.sailing.domain.tracking.Maneuver;
import com.sap.sailing.domain.tracking.impl.DynamicTrackedRaceImpl;
import com.sap.sailing.domain.tracking.impl.TrackedRaceImpl;
import com.sap.sailing.domain.windestimation.IncrementalWindEstimation;
public class ManeuverCacheDelegate implements SerializableManeuverCache {
private static final long serialVersionUID = 19872309587435L;
@@ -78,27 +81,222 @@ public class ManeuverCacheDelegate implements SerializableManeuverCache {
} else {
fingerprint = null;
}
// Self-heal check (bug6241): if the fingerprint matches but the loaded maneuvers cannot
// possibly produce a maneuver-based wind estimate, treat the DB record as a stale cache
// miss and fall through to the compute path. Two poisoned states are recognised:
//
// * "all empty" -- every competitor's list is null or empty. This is the original
// failure mode: the maneuver detector completed with no wind context at all and the
// storage step wrote empty lists.
//
// * "no classifiable maneuvers" -- lists are non-empty but every stored maneuver is
// typed as UNKNOWN or PENALTY_CIRCLE (i.e. none of TACK/JIBE/HEAD_UP/BEAR_AWAY is
// present anywhere in the race). This happens when the detector ran while
// getWind(pos, at) still returned null everywhere so createManeuverFromManeuverCurveAndWind
// hit the "wind == null" branch (ManeuverDetectorImpl line ~699) and typed every
// spot as UNKNOWN. Feeding such records back through
// IncrementalWindEstimation.alreadyClassifiedManeuversAvailable produces zero wind
// fixes because IncrementalMstHmmWindEstimationForTrackedRace.mapManeuverType maps
// UNKNOWN and PENALTY_CIRCLE to null -- the whole batch gets filtered out and the
// MBE wind track stays empty. That's what surfaces to callers as hasWindData=false.
//
// Either case is unambiguous evidence that the DB record was produced without a working
// wind estimator. The compute path now sequences its storage after the wind estimator
// has settled (see computeAndStore / retypeAndStoreAfterWindEstimationSettled), so
// recomputing here overwrites the poison and heals the DB.
final boolean useDbLoad;
final Map<Competitor, List<Maneuver>> loadedManeuvers;
if (fingerprint != null && fingerprint.matches(race)) {
logger.info("Maneuver fingerprints match for race "+race.getRaceIdentifier()+"; loading from DB instead of computing");
cacheToUse = new ManeuversFromDatabase(maneuverRaceFingerprintRegistry.loadManeuvers(race, race.getRace().getCourse()));
loadedManeuvers = maneuverRaceFingerprintRegistry.loadManeuvers(race, race.getRace().getCourse());
if (isAllEmpty(loadedManeuvers)) {
logger.info("Maneuver fingerprints match for race "+race.getRaceIdentifier()
+" but stored maneuvers are empty for every competitor; treating as stale cache miss and re-computing (see bug6241)");
useDbLoad = false;
} else if (hasNoClassifiableManeuver(loadedManeuvers)) {
logger.info("Maneuver fingerprints match for race "+race.getRaceIdentifier()
+" but no stored maneuver is classifiable as TACK/JIBE/HEAD_UP/BEAR_AWAY;"
+" the DB record cannot yield a maneuver-based wind estimate. Treating as stale"
+" cache miss and re-computing (see bug6241)");
useDbLoad = false;
} else {
logger.info("Maneuver fingerprints match for race "+race.getRaceIdentifier()+"; loading from DB instead of computing");
useDbLoad = true;
}
} else {
new Thread(()->{
logger.info("Maneuver fingerprints do not match for race "+race.getRaceIdentifier()+"; NOT loading from DB");
if (!cacheToUse.canBeUpdated()) {
cacheToUse = createUpdatableManeuverCache();
loadedManeuvers = null;
useDbLoad = false;
}
if (useDbLoad) {
cacheToUse = new ManeuversFromDatabase(loadedManeuvers);
} else {
new Thread(this::computeAndStore, "Waiting for maneuvers for "+race.getName()+" after having resumed to store the results in registry")
.start();
}
}
/**
* Returns {@code true} iff {@code maneuvers} is {@code null}, contains no entries, or contains
* only entries whose value is {@code null} or an empty list. Used by {@link #resume()} to
* self-heal from a previously-persisted "computed and empty" verdict (see bug6241).
*/
private boolean isAllEmpty(Map<Competitor, List<Maneuver>> maneuvers) {
boolean allEmpty = true;
if (maneuvers != null) {
for (final List<Maneuver> forCompetitor : maneuvers.values()) {
if (forCompetitor != null && !forCompetitor.isEmpty()) {
allEmpty = false;
break;
}
cacheToUse.resume();
if (maneuverRaceFingerprintRegistry != null) {
// wait for maneuvers to be computed by the default cache implementation (SmartFutureCache),
// then store persistently in registry
final Map<Competitor, List<Maneuver>> maneuvers = new HashMap<>();
for (final Competitor competitor : race.getRace().getCompetitors()) {
maneuvers.put(competitor, (List<Maneuver>) cacheToUse.get(competitor, /* waitForLatest */ true));
}
}
return allEmpty;
}
/**
* Returns {@code true} iff none of the stored maneuvers across all competitors is typed as
* {@link ManeuverType#TACK TACK}, {@link ManeuverType#JIBE JIBE}, {@link ManeuverType#HEAD_UP
* HEAD_UP} or {@link ManeuverType#BEAR_AWAY BEAR_AWAY}. Those four are the types that the
* maneuver-based wind estimator can turn into a wind fix; anything else
* ({@link ManeuverType#PENALTY_CIRCLE PENALTY_CIRCLE}, {@link ManeuverType#UNKNOWN UNKNOWN},
* {@code null}) is filtered out in {@code IncrementalMstHmmWindEstimationForTrackedRace
* .mapManeuverType}, so a stored record consisting only of those non-classifiable types
* would yield zero wind fixes when fed back via {@code alreadyClassifiedManeuversAvailable}.
* <p>
*
* Assumes the caller has already established that {@link #isAllEmpty} is {@code false}:
* this method exists to catch the second poisoned state where the lists are non-empty but
* every entry is unclassifiable (typically all-{@link ManeuverType#UNKNOWN UNKNOWN} from a
* detection pass that had no wind context; see the {@link #resume()} rationale).
*/
private boolean hasNoClassifiableManeuver(Map<Competitor, List<Maneuver>> maneuvers) {
boolean noneClassifiable = true;
if (maneuvers != null) {
outer:
for (final List<Maneuver> forCompetitor : maneuvers.values()) {
if (forCompetitor != null) {
for (final Maneuver maneuver : forCompetitor) {
if (maneuver != null && isClassifiableType(maneuver.getType())) {
noneClassifiable = false;
break outer;
}
}
maneuverRaceFingerprintRegistry.storeManeuvers(race.getRaceIdentifier(), ManeuverRaceFingerprintFactory.INSTANCE.createFingerprint(race), maneuvers, race.getRace().getCourse());
}
}, "Waiting for maneuvers for "+race.getName()+" after having resumed to store the results in registry")
.start();
}
}
return noneClassifiable;
}
/**
* The four maneuver types that {@code IncrementalMstHmmWindEstimationForTrackedRace
* .mapManeuverType} maps to a non-{@code null} {@code ManeuverTypeForClassification}, i.e.
* the ones that can feed a wind fix. Keep this list in sync with that mapping.
*/
private boolean isClassifiableType(ManeuverType type) {
return type == ManeuverType.TACK || type == ManeuverType.JIBE
|| type == ManeuverType.HEAD_UP || type == ManeuverType.BEAR_AWAY;
}
/**
* Runs the maneuver detector via the smart-future cache, then -- once a wind estimation has
* been installed on the tracked race and its inference has produced any wind fixes it can --
* recalculates each competitor's maneuvers so they get re-typed using the estimator's fixes
* (which since bug6274 are visible to the typing step via {@code trackedRace.getWind}).
* Finally snapshots the re-typed maneuvers and persists them via the fingerprint registry.
* <p>
*
* This deferred-store choreography (bug6241) is what prevents the DB from being poisoned with
* empty / UNKNOWN-typed maneuvers on the first server run for a race that has no other wind
* source: on subsequent server starts the fingerprint match then loads a properly-typed
* maneuver list, which fed through {@code feedAlreadyKnownManeuversToWindEstimation} produces
* wind fixes without needing to redetect.
*/
private void computeAndStore() {
logger.info("Maneuver fingerprints do not match for race "+race.getRaceIdentifier()+"; NOT loading from DB");
if (!cacheToUse.canBeUpdated()) {
cacheToUse = createUpdatableManeuverCache();
}
cacheToUse.resume();
if (maneuverRaceFingerprintRegistry != null) {
// First blocking pass: let the detector complete its initial run. This is what emits
// ManeuverSpots to the wind estimator via newManeuverSpotsDetected. Spots get typed
// using whatever wind is currently available -- typically nothing on the first pass
// for races with no non-estimation wind source.
for (final Competitor competitor : race.getRace().getCompetitors()) {
cacheToUse.get(competitor, /* waitForLatest */ true);
}
// Sequence the persistence step after the wind estimator has been installed and has
// finished the inference kicked off by our spots. runWhenWindEstimationInstalled
// fires synchronously here if the estimator was installed while we were computing;
// otherwise it registers a callback that fires once setWindEstimation is called with
// a non-null argument, and cancels silently if the race is removed before that
// happens. Because the setWindEstimation call typically happens on a shared
// background-executor thread and the follow-up work (waitUntilDone, recalculate,
// get(waitForLatest=true)) then blocks waiting for other tasks on the *same* shared
// pool, running it on the setWindEstimation caller's thread risks starving the pool
// (fatal with pool size 1). Detach onto a dedicated thread so the caller of
// setWindEstimation returns immediately. See bug6241 for the rationale.
race.runWhenWindEstimationInstalled(() ->
new Thread(this::retypeAndStoreAfterWindEstimationSettled,
"Retyping and storing maneuvers after wind estimation for "+race.getName())
.start());
}
}
/**
* 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 (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);
maneuvers.put(competitor, cacheToUse.get(competitor, /* waitForLatest */ true));
}
maneuverRaceFingerprintRegistry.storeManeuvers(race.getRaceIdentifier(),
ManeuverRaceFingerprintFactory.INSTANCE.createFingerprint(race),
maneuvers, race.getRace().getCourse());
}
}
@@ -130,8 +328,24 @@ public class ManeuverCacheDelegate implements SerializableManeuverCache {
return new ManeuversFromSmartFutureCache((DynamicTrackedRaceImpl) race);
}
/**
* Reflects the current {@link #cacheToUse inner cache}. Returns {@code true} when the delegate is
* currently backed by a compute-capable cache (a {@link ManeuversFromSmartFutureCache}) and
* {@code false} when it is currently backed by a passive {@link ManeuversFromDatabase}. The value
* switches at {@link #resume()} time depending on whether the fingerprint matched (DB-load) or not
* (compute), and again in {@link #triggerUpdate(Competitor)} if an update is requested on a
* currently-passive cache -- see the comment in {@code triggerUpdate}.
* <p>
*
* Callers use this to distinguish the two states, e.g., {@code TrackedRaceImpl
* .feedAlreadyKnownManeuversToWindEstimation} only feeds when the delegate is in the DB-load
* state (bug6241). Prior to bug6241 this method returned {@code true} unconditionally, which was
* a semantic bug: the delegate always <em>can</em> switch to a compute-capable cache on demand,
* but that's not what the {@link ManeuverCache#canBeUpdated} contract asks -- it asks whether
* the cache currently accepts {@link #triggerUpdate} without a mode switch.
*/
@Override
public boolean canBeUpdated() {
return true;
return cacheToUse.canBeUpdated();
}
}
@@ -1481,24 +1481,30 @@ public class CandidateFinderImpl implements CandidateFinder {
// passing instructions. If no passing instructions are given, construct two lines, one to each direction
// orthogonally to the adjacent leg:
if (w == race.getRace().getCourse().getFirstWaypoint()) {
if (instruction == PassingInstruction.None || instruction == PassingInstruction.Single_Unknown) {
b = markPositionCache.getLegBearing(race.getTrackedLegStartingAt(w)).add(new DegreeBearingImpl(90));
result.add(new Pair<>(p, b));
b = markPositionCache.getLegBearing(race.getTrackedLegStartingAt(w)).add(new DegreeBearingImpl(270));
} else {
b = markPositionCache.getLegBearing(race.getTrackedLegStartingAt(w)).add(new DegreeBearingImpl(instruction == PassingInstruction.Port ? 90 : 270));
final Bearing legBearingStartingAtW = markPositionCache.getLegBearing(race.getTrackedLegStartingAt(w));
if (legBearingStartingAtW != null) {
if (instruction == PassingInstruction.None || instruction == PassingInstruction.Single_Unknown) {
b = legBearingStartingAtW.add(new DegreeBearingImpl(90));
result.add(new Pair<>(p, b));
b = legBearingStartingAtW.add(new DegreeBearingImpl(270));
} else {
b = legBearingStartingAtW.add(new DegreeBearingImpl(instruction == PassingInstruction.Port ? 90 : 270));
}
}
} else if (w == race.getRace().getCourse().getLastWaypoint()) {
if (instruction == PassingInstruction.None || instruction == PassingInstruction.Single_Unknown) {
b = markPositionCache.getLegBearing(race.getTrackedLegFinishingAt(w)).add(new DegreeBearingImpl(90));
result.add(new Pair<>(p, b));
b = markPositionCache.getLegBearing(race.getTrackedLegFinishingAt(w)).add(new DegreeBearingImpl(270));
} else {
b = markPositionCache.getLegBearing(race.getTrackedLegFinishingAt(w)).add(new DegreeBearingImpl(instruction == PassingInstruction.Port ? 90 : 270));
final Bearing legBearingFinishingAtW = markPositionCache.getLegBearing(race.getTrackedLegFinishingAt(w));
if (legBearingFinishingAtW != null) {
if (instruction == PassingInstruction.None || instruction == PassingInstruction.Single_Unknown) {
b = legBearingFinishingAtW.add(new DegreeBearingImpl(90));
result.add(new Pair<>(p, b));
b = legBearingFinishingAtW.add(new DegreeBearingImpl(270));
} else {
b = legBearingFinishingAtW.add(new DegreeBearingImpl(instruction == PassingInstruction.Port ? 90 : 270));
}
}
} else {
Bearing before = markPositionCache.getLegBearing(race.getTrackedLegFinishingAt(w));
Bearing after = markPositionCache.getLegBearing(race.getTrackedLegStartingAt(w));
final Bearing before = markPositionCache.getLegBearing(race.getTrackedLegFinishingAt(w));
final Bearing after = markPositionCache.getLegBearing(race.getTrackedLegStartingAt(w));
if (before != null && after != null) {
b = before.middle(after.reverse());
}
@@ -154,7 +154,91 @@ public interface PolarDataService {
BearingWithConfidence<Void> getManeuverAngle(BoatClass boatClass, ManeuverType maneuverType, Speed windSpeed)
throws NotEnoughDataHasBeenAddedException;
void raceFinishedLoading(TrackedRace race);
void raceFinishedLoading(TrackedRace race, Runnable callbackWhenRaceChangingToTrackingOfFinishedStatus);
/**
* Registers {@code callback} to fire once the polar-data loading pipeline has fully drained.
* <p>
*
* IMPORTANT this waits for a <em>global</em> drain, not a per-race one. The loading pipeline
* is shared across all races, so the callback fires only when the fixes of <em>every</em>
* race ingested so far (not just {@code race}) have been processed. The {@code race} parameter
* does <em>not</em> scope the wait to that race; it only gates <em>when</em> the callback is
* allowed to start observing the drain, via two conditions that both must hold before the
* callback is attached to the pipeline's drain:
* <ol>
* <li>{@code race}'s fixes have actually been ingested into the pipeline (i.e.
* {@link #raceFinishedLoading(TrackedRace, Runnable)} has run for it). Without this gate the
* callback could fire on a pipeline that is merely momentarily idle because this race's
* fixes haven't been queued yet, producing an incomplete polar model for {@code race}.</li>
* <li>{@link #markLoadingOfAllRacesToRestoreStarted()} has been signalled, so a transient
* idle window between two startup races' ingestion bursts is not mistaken for a real
* drain.</li>
* </ol>
* The consequence is intended: a caller waiting on {@code race} effectively waits for the
* whole loaded-fix backlog to be processed, which on a cold start of a large archive can be
* substantial (the estimator install for an early race waits behind every other race's polar
* ingestion). This "wait for everything" behavior is deliberate we want the polar model to
* reflect all loaded data before any maneuver-based wind estimation is installed and the
* mild sequentiality it implies is accepted. It is <em>not</em> a per-race isolation
* guarantee; do not rely on this firing as soon as only {@code race}'s own fixes are done.
* <p>
*
* Unlike {@link #raceFinishedLoading(TrackedRace, Runnable)}, this method does <em>not</em>
* ingest the race's fixes; it only observes the pipeline. It is safe to call multiple times
* for the same race, and before or after {@link #raceFinishedLoading} has been called for it.
* See bug6241.
*
* @param callback
* must not be {@code null}
*/
void runWhenPolarLoadingFinishedFor(TrackedRace race, Runnable callback);
/**
* Releases any state the polar data service holds for {@code race}, allowing a removed race
* and its tracks to be garbage-collected. Callers of
* {@link #runWhenPolarLoadingFinishedFor(TrackedRace, Runnable)} MUST call this when the race
* is removed from its regatta / the racing event service: a callback parked for a race whose
* polar loading never completes is held strongly (and typically captures the race strongly
* itself), so without this call the race and all of its tracks would be pinned for the
* lifetime of the service. In this codebase the call is wired through
* {@code RacingEventServiceImpl.RaceAdditionListener.raceRemoved(TrackedRace)}.
* <p>
*
* Idempotent and safe to call for a race the service never tracked. See bug6241.
*
* @param race
* the race to forget; must not be {@code null}
*/
void raceRemoved(TrackedRace race);
/**
* Signals to this polar data service that its client has finished enumerating and triggering
* loading for every race that will be restored during startup. From this point on the client
* makes no further ingestion promises, so any transient idle window on the loading pipeline
* is a genuine drain of everything ingested so far. Callbacks registered via
* {@link #runWhenPolarLoadingFinishedFor(TrackedRace, Runnable)} whose race is already
* ingested but which arrived before this signal fire on the next drain after this call.
* <p>
*
* The signal is about the enumeration/triggering side only: it does <em>not</em> imply that
* the enumerated races have already progressed past {@code LOADING} some may still be
* loading, some may take a long time, some may never leave {@code LOADING} at all. The
* signal only promises that no new startup races will show up unannounced.
* <p>
*
* Whether calling this method has any effect depends on how the implementation was
* constructed. Implementations built for the gated (production) mode hold
* {@link #runWhenPolarLoadingFinishedFor} callbacks until this signal arrives; implementations
* built in the default non-gated mode ignore this call. It is always safe to call regardless
* of the implementation's mode.
* <p>
*
* Idempotent: subsequent calls after the first are logged (in gated mode) or silently
* ignored (in non-gated mode).
* See bug6241.
*/
void markLoadingOfAllRacesToRestoreStarted();
/**
* See {@link #getAverageSpeedWithBearing(BoatClass, Speed, LegType, Tack, boolean)}
@@ -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;
@@ -834,6 +839,10 @@ public class DummyTrackedRace extends TrackedRaceWithWindEssentials {
public void runWhenDoneLoading(Runnable runnable) {
}
@Override
public void runWhenPastLoading(Runnable callback) {
}
@Override
public void runSynchronizedOnStatus(Runnable runnable) {
}
@@ -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
@@ -1461,6 +1468,64 @@ public interface TrackedRace
*/
void runWhenDoneLoading(Runnable runnable);
/**
* Runs {@code callback} exactly once, at the first moment when the race's status reaches an
* {@link TrackedRaceStatus#getStatus() order} strictly greater than
* {@link TrackedRaceStatusEnum#LOADING} (i.e., {@link TrackedRaceStatusEnum#TRACKING},
* {@link TrackedRaceStatusEnum#FINISHED}, {@link TrackedRaceStatusEnum#ERROR}, or
* {@link TrackedRaceStatusEnum#REMOVED}), <em>provided</em> the race is still a member of its
* {@link TrackedRegatta} at that moment. If the race is
* {@link TrackedRegatta#removeTrackedRace removed} from its regatta before the status condition
* is met, {@code callback} is never invoked and any listeners registered by this method are
* torn down. This means an implementation must register a status listener on this race as well
* as a race-removal listener on the containing regatta and coordinate them so that both are
* removed regardless of which branch settles first.
* <p>
*
* Note the difference to {@link #runWhenDoneLoading(Runnable)}: this method treats any status
* strictly beyond LOADING as "done loading," including {@link TrackedRaceStatusEnum#ERROR} and
* {@link TrackedRaceStatusEnum#REMOVED} (via status transition), which makes it usable for
* races that transition, e.g., directly from {@link TrackedRaceStatusEnum#PREPARED} to
* {@link TrackedRaceStatusEnum#TRACKING} without ever entering LOADING.
*
* @param callback
* must not be {@code null}
*/
void runWhenPastLoading(Runnable callback);
/**
* Executes {@code callback} once a non-{@code null} {@link IncrementalWindEstimation} has been
* {@link #setWindEstimation(IncrementalWindEstimation) installed} on this race. If one is
* already installed at call time, the callback fires synchronously on the caller's thread;
* otherwise the callback is registered and fires on whichever thread first invokes
* {@code setWindEstimation} with a non-{@code null} argument.
* <p>
*
* The callback also cancels silently, without firing, if the race is removed before the
* estimation is installed (matching the cancellation semantics of
* {@link #runWhenPastLoading(Runnable)}), so that callers don't leak resources when a race
* disappears during startup or teardown.
* <p>
*
* The default implementation checks {@link #getWindEstimation()} once: if non-{@code null},
* the callback fires synchronously; otherwise it is discarded. This is only appropriate for
* test doubles and other implementations without a dynamic {@link #setWindEstimation(IncrementalWindEstimation)}
* lifecycle. Implementations with such a lifecycle (notably {@code TrackedRaceImpl}) must
* override to register the callback and fire it when the estimation is installed.
* <p>
*
* See bug6241: used to sequence maneuver-cache re-typing and persistence after the wind
* estimator has been installed and had a chance to produce wind fixes.
*
* @param callback
* must not be {@code null}
*/
default void runWhenWindEstimationInstalled(Runnable callback) {
if (getWindEstimation() != null) {
callback.run();
}
}
/**
* Executes the {@code callable} under synchronization with the {@link #getStatus() race status}; in other words,
* while the callable executes, the race status of this race cannot be updated.
@@ -35,6 +35,8 @@ import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.logging.Level;
@@ -196,6 +198,7 @@ import com.sap.sse.concurrent.NamedReentrantReadWriteLock;
import com.sap.sse.shared.util.impl.ApproximateTime;
import com.sap.sse.shared.util.impl.ArrayListNavigableSet;
import com.sap.sse.util.IdentityWrapper;
import com.sap.sse.util.ThreadPoolUtil;
import com.sap.sse.util.impl.FutureTaskWithTracingGet;
import difflib.DiffUtils;
@@ -216,6 +219,24 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl
private static final Logger logger = Logger.getLogger(TrackedRaceImpl.class.getName());
/**
* Dedicated executor for {@link #feedAlreadyKnownManeuversToWindEstimation(IncrementalWindEstimation)}
* waiter tasks. These tasks call {@code maneuverCache.get(competitor, true)} (with
* {@code waitForLatest} set to {@code true}), which may block on maneuver-detection futures
* that themselves run on the shared
* {@link ThreadPoolUtil#getDefaultBackgroundTaskThreadPoolExecutor()}. If we scheduled the
* waiters on that same shared pool, all pool threads would end up blocked in waits while
* the detection tasks they wait for sit queued behind them -- a classic pool-starvation
* deadlock (observed in CI runs where all "Default background executor" threads were stuck
* in FutureTaskWithCancelBlocking.get with 76 queued tasks and no active detection). Using a
* separate pool decouples the waiters from the pool that runs the tasks they wait for.
* See bug6241.
*/
private static final ScheduledExecutorService feedManeuversToWindEstimationExecutor =
ThreadPoolUtil.INSTANCE.createBackgroundTaskThreadPoolExecutor(
Math.max(2, ThreadPoolUtil.INSTANCE.getReasonableThreadPoolSize() / 4),
TrackedRaceImpl.class.getSimpleName() + " feedManeuversToWindEstimation");
private static final long DELAY_FOR_CACHE_CLEARING_IN_MILLISECONDS = 7500;
public static final Duration TIME_BEFORE_START_TO_TRACK_WIND_MILLIS = Duration.ONE_MINUTE.times(4); // let wind start four minutes before race
@@ -442,6 +463,23 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl
private transient volatile IncrementalWindEstimation windEstimation;
/**
* Callbacks awaiting the first non-{@code null} {@link #setWindEstimation(IncrementalWindEstimation)}
* invocation, registered via {@link #runWhenWindEstimationInstalled(Runnable)}. Guarded by
* {@link #windEstimationInstalledCallbacksLock}. Cleared once the wind estimation is installed;
* subsequent registrations invoke the callback synchronously on the caller's thread without
* adding to this list.
*/
private transient List<Runnable> windEstimationInstalledCallbacks;
/**
* Monitor guarding the {@link #windEstimationInstalledCallbacks} list and the read-then-decide
* against {@link #windEstimation} in {@link #runWhenWindEstimationInstalled(Runnable)}.
* Reinitialized in {@link #readObject(ObjectInputStream)} on replicas because both this field
* and the underlying list are transient.
*/
private transient Object windEstimationInstalledCallbacksLock = new Object();
private transient ShortTimeAfterLastHitCache<Competitor, IncrementalManeuverDetector> maneuverDetectorPerCompetitorCache;
/**
@@ -793,6 +831,14 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl
competitorRankingsLocks = createCompetitorRankingsLockMap();
directionFromStartToNextMarkCache = new ConcurrentHashMap<>();
maneuverDetectorPerCompetitorCache = createManeuverDetectorCache();
// bug6241: re-establish the "wait for wind estimation to be installed" callback plumbing
// on the replica. Both fields are transient; without this initialization,
// runWhenWindEstimationInstalled would NPE on the synchronized (windEstimationInstalledCallbacksLock)
// block. The list itself starts empty because no callbacks are pending on a fresh replica --
// any pending state on the master lives only in the master's memory. windEstimationInstalledCallbacks
// is left null and lazily created on first registration in runWhenWindEstimationInstalled.
windEstimationInstalledCallbacksLock = new Object();
windEstimationInstalledCallbacks = null;
logger.info("Deserialized race " + getRace().getName());
}
@@ -2867,10 +2913,10 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl
.approximate(from, to);
}
private void ensureManeuverCacheIsFilledForAllCompetitors() {
maneuverCache.ensureFilled();
}
public void triggerManeuverCacheRecalculationForAllCompetitors() {
if (cachesSuspended) {
triggerManeuverCacheInvalidationForAllCompetitors = true;
@@ -3217,6 +3263,164 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl
}
}
@Override
public void runWhenPastLoading(final Runnable callback) {
final int loadingOrder = TrackedRaceStatusEnum.LOADING.getOrder();
// Two listeners cooperate to settle the outcome exactly once: a status listener
// on this race that fires the callback when the race moves past LOADING, and a
// race-removal listener on the containing regatta that silently cancels if the
// race is removed first. The one-shot CAS on `settled` guarantees exactly one
// branch (fire or cancel) wins; both listeners are removed in either case.
// The status-notifier monitor is held only around the initial check and
// registration of the status listener so we don't cross-lock with the regatta
// when adding the regatta listener; the post-registration re-check catches any
// status transition that raced with us.
final AtomicBoolean settled = new AtomicBoolean(false);
final TrackedRegatta regatta = getTrackedRegatta();
final RaceListener[] regattaListenerHolder = new RaceListener[1];
final AbstractRaceChangeListener[] statusListenerHolder = new AbstractRaceChangeListener[1];
final Runnable tearDown = () -> {
if (statusListenerHolder[0] != null) {
removeListener(statusListenerHolder[0]);
}
if (regattaListenerHolder[0] != null) {
regatta.removeRaceListener(regattaListenerHolder[0]);
}
};
final Runnable settleAndFire = () -> {
if (settled.compareAndSet(false, true)) {
try {
tearDown.run();
} finally {
callback.run();
}
}
};
final Runnable settleWithoutFiring = () -> {
if (settled.compareAndSet(false, true)) {
tearDown.run();
}
};
statusListenerHolder[0] = new AbstractRaceChangeListener() {
@Override
public void statusChanged(final TrackedRaceStatus newStatus, final TrackedRaceStatus oldStatus) {
if (newStatus.getStatus().getOrder() > loadingOrder) {
settleAndFire.run();
}
}
};
regattaListenerHolder[0] = new RaceListener() {
@Override
public void raceAdded(final TrackedRace trackedRace) {
// not interested in additions
}
@Override
public void raceRemoved(final TrackedRace trackedRace) {
if (trackedRace == TrackedRaceImpl.this) {
settleWithoutFiring.run();
}
}
};
final boolean alreadyPast;
synchronized (getStatusNotifier()) {
if (getStatus().getStatus().getOrder() > loadingOrder) {
alreadyPast = true;
} else {
alreadyPast = false;
addListener(statusListenerHolder[0]);
}
}
if (alreadyPast) {
callback.run();
} else {
regatta.addRaceListener(regattaListenerHolder[0], Optional.empty(), /* synchronous */ false);
// Close the race between the initial check + listener registration and the
// regatta-listener registration: if we already transitioned in between, fire
// now. The CAS on `settled` makes this safe against concurrent status events
// that may already be delivering to statusListenerHolder[0].
if (getStatus().getStatus().getOrder() > loadingOrder) {
settleAndFire.run();
}
}
}
@Override
public void runWhenWindEstimationInstalled(final Runnable callback) {
// Same two-cooperating-listeners pattern as runWhenPastLoading, but the "fire" trigger is
// the first non-null setWindEstimation call (observed via the callback list drained inside
// updateManeuversAndWindWithNewWindEstimation) rather than a status transition. If a wind
// estimation is already installed when this method is called, we fire synchronously on the
// caller's thread; otherwise we register a callback and a regatta-removal listener so we
// silently cancel if the race disappears before installation. See bug6241.
final AtomicBoolean settled = new AtomicBoolean(false);
final TrackedRegatta regatta = getTrackedRegatta();
final RaceListener[] regattaListenerHolder = new RaceListener[1];
final Runnable[] installCallbackHolder = new Runnable[1];
final Runnable tearDown = () -> {
if (installCallbackHolder[0] != null) {
synchronized (windEstimationInstalledCallbacksLock) {
if (windEstimationInstalledCallbacks != null) {
windEstimationInstalledCallbacks.remove(installCallbackHolder[0]);
}
}
}
if (regattaListenerHolder[0] != null) {
regatta.removeRaceListener(regattaListenerHolder[0]);
}
};
final Runnable settleAndFire = () -> {
if (settled.compareAndSet(false, true)) {
try {
tearDown.run();
} finally {
callback.run();
}
}
};
final Runnable settleWithoutFiring = () -> {
if (settled.compareAndSet(false, true)) {
tearDown.run();
}
};
installCallbackHolder[0] = () -> settleAndFire.run();
regattaListenerHolder[0] = new RaceListener() {
@Override
public void raceAdded(final TrackedRace trackedRace) {
// not interested in additions
}
@Override
public void raceRemoved(final TrackedRace trackedRace) {
if (trackedRace == TrackedRaceImpl.this) {
settleWithoutFiring.run();
}
}
};
final boolean alreadyInstalled;
synchronized (windEstimationInstalledCallbacksLock) {
if (windEstimation != null) {
alreadyInstalled = true;
} else {
alreadyInstalled = false;
if (windEstimationInstalledCallbacks == null) {
windEstimationInstalledCallbacks = new ArrayList<>();
}
windEstimationInstalledCallbacks.add(installCallbackHolder[0]);
}
}
if (alreadyInstalled) {
callback.run();
} else {
regatta.addRaceListener(regattaListenerHolder[0], Optional.empty(), /* synchronous */ false);
// Close the race between the initial check + list append and the regatta-listener
// registration: if setWindEstimation ran in that window and drained our callback (or
// the field became non-null another way), fire now. The CAS on `settled` makes this
// safe against concurrent installations.
if (windEstimation != null) {
settleAndFire.run();
}
}
}
@Override
public void attachRaceLog(RaceLog raceLog) {
synchronized (TrackedRaceImpl.this) {
@@ -4090,6 +4294,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;
@@ -4100,18 +4309,162 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl
private void updateManeuversAndWindWithNewWindEstimation(IncrementalWindEstimation windEstimation,
IncrementalWindEstimation previousWindEstimation) {
WindSource windSource = new WindSourceImpl(WindSourceType.MANEUVER_BASED_ESTIMATION);
final WindSource windSource = new WindSourceImpl(WindSourceType.MANEUVER_BASED_ESTIMATION);
windTracks.remove(windSource);
if (windEstimation != null) {
windTracks.put(windSource, windEstimation.getWindTrack());
}
updateWindSourcesByType(windSource);
this.windEstimation = windEstimation;
// TODO Make more efficient by reusing the state of incremental maneuver detectors. The already computed
// complete maneuver curves can be fed directly into the windEstimation.
// Clear the maneuver-detector cache so future maneuver detections pick up the new
// WindEstimationInteraction from the current windEstimation. See also bug6184: maneuver
// detection itself does not consume MANEUVER_BASED_ESTIMATION, so the maneuvers won't
// change; only the side-channel notification to the wind estimator will now target the
// new estimator instance.
maneuverDetectorPerCompetitorCache.clearCache();
shortTimeWindCache.clearCache();
// no need to trigger maneuver recalculation because it is not using the MANEUVER_BASED_ESTIMATION; see also bug6184
// bug6241: if a new (non-null) wind estimation is being installed and we already have
// maneuvers for this race (either loaded from the persistent maneuver cache, or
// previously computed with a null / stale WindEstimationInteraction so they never
// reached the current estimator), feed those maneuvers to the new estimator now via
// alreadyClassifiedManeuversAvailable. The feed runs on a background task per
// competitor so that this method returns quickly and, importantly, does not block the
// setter's caller (typically the OSGi service-tracker thread or the
// RaceAdditionListener callback thread) on the potentially-slow
// maneuverCache.get(_, /* waitForLatest */ true) call that may still be waiting for
// maneuver detection to complete.
if (windEstimation != null) {
feedAlreadyKnownManeuversToWindEstimation(windEstimation);
// bug6241: fire "wind estimation installed" callbacks -- for example, from
// ManeuverCacheDelegate.resume()'s Path B thread that wants to re-type maneuvers
// after the estimator has had a chance to produce wind fixes. Snapshot and clear the
// list under the monitor so subsequent runWhenWindEstimationInstalled(...) calls
// observe the installed state and invoke synchronously.
final List<Runnable> callbacksToFire;
synchronized (windEstimationInstalledCallbacksLock) {
if (windEstimationInstalledCallbacks != null && !windEstimationInstalledCallbacks.isEmpty()) {
callbacksToFire = new ArrayList<>(windEstimationInstalledCallbacks);
windEstimationInstalledCallbacks.clear();
} else {
callbacksToFire = Collections.emptyList();
}
}
for (final Runnable callback : callbacksToFire) {
try {
callback.run();
} catch (Throwable t) {
logger.log(Level.WARNING,
"runWhenWindEstimationInstalled callback threw for race " + getRaceIdentifier(), t);
}
}
}
}
/**
* Schedules a per-competitor background task that reads the competitor's currently-known
* maneuvers from {@link #maneuverCache} (waiting, if necessary, for a pending detection to
* complete or for the DB-load path to have delivered its results) and, if any are present,
* hands them off to {@code newWindEstimation} via
* {@link IncrementalWindEstimation#alreadyClassifiedManeuversAvailable(Competitor, Iterable)}.
* <p>
*
* Only invoked when the {@link #maneuverCache} is <em>not</em> updatable via computation
* (i.e., the maneuvers were loaded from the persistent cache as
* {@link com.sap.sailing.domain.maneuverhash.impl.ManeuversFromDatabase}). In the compute
* path -- a {@link com.sap.sailing.domain.maneuverhash.impl.ManeuversFromSmartFutureCache}
* -- detection produces the same maneuvers and the maneuver detector will invoke
* {@link IncrementalWindEstimation#newManeuverSpotsDetected} on the newly installed
* estimator, which drives the NN+HMM graph path and produces the wind fixes with the
* proper cross-competitor spatial and temporal proximity awareness. Feeding the same
* maneuvers a second time via {@code alreadyClassifiedManeuversAvailable} would produce
* per-competitor slices without that MST aggregation and would clobber the correct
* fixes through the reconciliation step of {@code applyManeuverClassificationsToWindTrack}
* -- see bug6241.
* <p>
*
* The task guards against being obsoleted by a subsequent {@link #setWindEstimation} that
* replaces {@code newWindEstimation}: before performing the hand-off it checks that the
* race's current {@link #windEstimation} is still the same instance it was scheduled with.
*/
private void feedAlreadyKnownManeuversToWindEstimation(final IncrementalWindEstimation newWindEstimation) {
// Gate: only feed maneuvers to the newly-installed estimator when the maneuver cache
// is NOT updatable via computation. That is: only when this JVM's cache is a
// ManeuversFromDatabase (canBeUpdated() == false), meaning maneuvers were loaded from
// the persistent MANEUVERS collection and no maneuver detector on this JVM has (or
// will) invoke IncrementalWindEstimation#newManeuverSpotsDetected for them.
// <p>
//
// WHY THIS GATE MUST EXIST -- please leave it in place. Both the DB-load path and the
// compute path need the estimator to receive the maneuvers, but they do so through
// different channels and mixing them is harmful:
// <ul>
// <li>DB-load path (!canBeUpdated()): the maneuvers came from disk. No detector on
// this JVM ever ran, so no {@code newManeuverSpotsDetected} will ever fire for them.
// The only way to get them into the estimator is via
// {@code alreadyClassifiedManeuversAvailable}, which is exactly what this method
// does. Enqueues a {@code PreClassifiedUpdate} per competitor.</li>
//
// <li>Compute path (canBeUpdated()): the maneuver detector runs on this JVM (either
// under {@code ManeuverCacheDelegate.resume}'s Path B or during the follow-up
// recompute driven by {@code runWhenWindEstimationInstalled} in the retype-and-store
// choreography of {@code ManeuverCacheDelegate}). Freshly-built detectors,
// constructed AFTER the wind estimator is installed and after
// {@link #updateManeuversAndWindWithNewWindEstimation} cleared
// {@link #maneuverDetectorPerCompetitorCache}, capture the now-non-null
// {@code WindEstimationInteraction} and feed the estimator through the graph path:
// {@code newManeuverSpotsDetected} -> {@code NewSpotsUpdate}. That path correctly
// aggregates ALL competitors' spots into one MST/HMM inference with cross-competitor
// spatial and temporal proximity awareness -- essential for good wind estimates.</li>
// </ul>
// Feeding the compute-path maneuvers a SECOND time through this method (i.e., not
// gating on {@code !canBeUpdated()}) would produce per-competitor
// {@code PreClassifiedUpdate} slices of wind fixes without that cross-competitor
// aggregation. Worse, the estimator's reconciliation step
// {@code applyManeuverClassificationsToWindTrack} then reconciles the wind track down
// to whatever the current input batch produced, which for a per-competitor slice
// CLOBBERS the correct whole-race track computed by the graph path. Observable as a
// large drop in the ratio-of-matching-fixes assertion in
// {@code IncrementalMstHmmWindEstimationForTrackedRaceTest} (75% threshold), triggered
// by an ordering race between {@link #feedManeuversToWindEstimationExecutor} tasks
// and {@code triggerManeuverCacheRecalculationForAllCompetitors} in the test's setUp:
// if the feed task runs after the recalc trigger, it double-feeds; if it runs before,
// {@code maneuverCache.get(waitForLatest=true)} returns null and the feed is skipped.
// <p>
//
// The bug6241 "hold-back" concern (that after polar-data hold-back the wind estimator
// may be installed AFTER a first detection pass has already completed with a captured
// null {@code WindEstimationInteraction}) was previously cited as a reason to feed
// unconditionally. That concern is now handled explicitly by the retype-and-store
// choreography in {@code ManeuverCacheDelegate.computeAndStore}, which registers via
// {@code runWhenWindEstimationInstalled} and then re-drives the detector (whose cache
// was cleared here at install time) so freshly-built detectors emit spots to the newly
// installed estimator through the graph path -- naturally, with cross-competitor
// aggregation intact.
// <p>
//
// Safety of the gate: {@code ManeuverCacheDelegate.resume} -- which switches
// {@code cacheToUse} between the two variants -- runs synchronously inside the
// LOADING-to-post-LOADING transition handler. {@code setWindEstimation} is invoked
// AFTER that transition (either by {@code scheduleWindEstimationInstallation} waiting
// on {@link #runWhenPastLoading}, or by tests manually sequencing after wind sources
// are populated). So by the time we arrive here, {@code maneuverCache.canBeUpdated()}
// reliably reflects which path is in use.
if (!maneuverCache.canBeUpdated()) {
for (final Competitor competitor : getRace().getCompetitors()) {
feedManeuversToWindEstimationExecutor.execute(() -> {
try {
final List<Maneuver> maneuvers = maneuverCache.get(competitor, /* waitForLatest */ true);
if (maneuvers != null && !maneuvers.isEmpty()
&& TrackedRaceImpl.this.windEstimation == newWindEstimation) {
newWindEstimation.alreadyClassifiedManeuversAvailable(competitor, maneuvers);
}
} catch (Throwable e) {
logger.log(Level.WARNING, "Failed to feed already-known maneuvers of competitor " + competitor
+ " into the wind estimation of race " + getRaceIdentifier(), e);
}
});
}
}
}
/**
@@ -69,28 +69,46 @@ public abstract class TrackedRegattaImpl implements TrackedRegatta {
* that newly added listeners only receive events after the initial {@link TrackedRace} instances are delivered to
* this listener.</li>
* <li>Firing events for the already existing {@link TrackedRace} instances when adding a new listener (see
* {@link #addRaceListener(RaceListener)}). This ensures that all events are correctly fired to this listener that
* are triggered after the listener was added while suppressing inconsistent events before/while the initial
* {@link TrackedRace} instances are delivered to this listener.</li>
* {@link #addRaceListener(RaceListener, Optional, boolean)}). This ensures that all events are correctly fired to
* this listener that are triggered after the listener was added while suppressing inconsistent events before/while
* the initial {@link TrackedRace} instances are delivered to this listener.</li>
* <li>Completing the future returned by {@link #removeRaceListener(RaceListener)} to ensure that the receiver gets
* to know when it is guaranteed that no more event will be fired to the listener.
* </ul>
*
* <p>Concurrency contract: this is a {@link ConcurrentHashMap} and no dedicated lock guards it. Instead, the
* required serialization comes from {@link #trackedRacesLock}:
* <ul>
* <li>{@link #enqueEvent} is only ever called from {@link #addTrackedRace} and {@link #removeTrackedRace}, both of
* which hold {@link #trackedRacesLock} for write while calling it. During that write region, no thread can be
* inside {@link #addRaceListener} or {@link #removeRaceListener}, both of which need
* {@link #trackedRacesLock} for read at their entry (line {@code lockTrackedRacesForRead()}) -- read blocks on
* write. So the listener set is stable for the whole duration of a dispatch, without needing a dedicated
* {@code raceListenersLock}.</li>
* <li>{@link #addRaceListener} and {@link #removeRaceListener} do their own mutations through
* {@link ConcurrentHashMap#computeIfAbsent} / {@link ConcurrentHashMap#remove}, both of which are atomic per key
* on {@link ConcurrentHashMap}. Concurrent add/add or remove/remove for the same listener collapse safely; for
* different listeners the map's own synchronization handles it.</li>
* <li>Iterating this map from {@link #enqueEvent} uses {@link ConcurrentHashMap#forEach}, which is weakly
* consistent; combined with the outer {@link #trackedRacesLock} write hold, iteration observes a stable snapshot
* of the listener set.</li>
* </ul>
*
* <p>A dedicated {@code raceListenersLock} used to exist here. It was dropped as part of issue #6241 because it
* caused a read-then-write self-deadlock: {@link #enqueEvent} would hold {@code raceListenersLock} for read and
* dispatch synchronously to listeners; if a listener's callback in turn called
* {@link #addRaceListener} (e.g. via a wind-estimation installation primitive that registers a race-removal
* listener), the {@code raceListenersLock} write acquisition would block on the same thread's own read hold, and
* {@link java.util.concurrent.locks.ReentrantReadWriteLock} does not allow upgrading.
*/
private transient ConcurrentMap<RaceListener, RunnableExecutor> raceListeners;
/**
* Guards access to {@link #raceListeners}.
*/
private final NamedReentrantReadWriteLock raceListenersLock;
public TrackedRegattaImpl(Regatta regatta) {
super();
this.trackedRacesLock = new NamedReentrantReadWriteLock("trackeRaces lock for tracked regatta "+regatta.getName(), /* fair */ false);
this.regatta = regatta;
this.trackedRaces = new HashMap<RaceDefinition, TrackedRace>();
this.raceListeners = new ConcurrentHashMap<>();
this.raceListenersLock = new NamedReentrantReadWriteLock(
"raceListeners lock for tracked regatta " + regatta.getName(), /* fair */ false);
}
private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
@@ -157,13 +175,14 @@ public abstract class TrackedRegattaImpl implements TrackedRegatta {
* enqueues an event for each currently known listeners.
*/
protected void enqueEvent(Consumer<RaceListener> fireEventCallback, Optional<ThreadLocalTransporter> threadLocalTransporter) {
// No dedicated lock on raceListeners here: callers hold trackedRacesLock for write
// (via addTrackedRace / removeTrackedRace), which excludes addRaceListener / removeRaceListener
// (both of which need trackedRacesLock for read). See the raceListeners field's Javadoc.
threadLocalTransporter.ifPresent(ThreadLocalTransporter::rememberThreadLocalStates);
LockUtil.executeWithReadLock(raceListenersLock, () -> {
raceListeners.forEach((listener, eventQueue) -> {
eventQueue.addWork(() -> {
withBeforeAndAfterHandling(threadLocalTransporter, () -> {
fireEventCallback.accept(listener);
});
raceListeners.forEach((listener, eventQueue) -> {
eventQueue.addWork(() -> {
withBeforeAndAfterHandling(threadLocalTransporter, () -> {
fireEventCallback.accept(listener);
});
});
});
@@ -260,24 +279,26 @@ public abstract class TrackedRegattaImpl implements TrackedRegatta {
@Override
public void addRaceListener(RaceListener listener, Optional<ThreadLocalTransporter> threadLocalTransporter, boolean synchronous) {
assert synchronous == false || !threadLocalTransporter.isPresent(); // transporting thread locals doesn't make sense for synchronous listeners
// Hold trackedRacesLock for read so that no addTrackedRace / removeTrackedRace runs
// concurrently and races with our catch-up snapshot below. The tracked races cannot
// change while we hold this lock. Registration into raceListeners itself is atomic via
// ConcurrentHashMap.computeIfAbsent, which also collapses duplicate registrations of
// the same listener; no dedicated lock on raceListeners is needed.
lockTrackedRacesForRead();
try {
LockUtil.executeWithWriteLock(raceListenersLock, () -> {
// This prevents the creation of another WorkQueue if an already known listener is added a second time
raceListeners.computeIfAbsent(listener, listenerToAdd -> {
final RunnableExecutor eventQueue = synchronous ? new SynchronousRunnableExecutor() : new AsynchronousRunnableExecutor();
final List<TrackedRace> trackedRacesCopy = new ArrayList<>();
Util.addAll(getTrackedRaces(), trackedRacesCopy);
threadLocalTransporter.ifPresent(ThreadLocalTransporter::rememberThreadLocalStates);
eventQueue.addWork(() -> {
withBeforeAndAfterHandling(threadLocalTransporter, () -> {
for (TrackedRace trackedRace : trackedRacesCopy) {
listenerToAdd.raceAdded(trackedRace);
}
});
raceListeners.computeIfAbsent(listener, listenerToAdd -> {
final RunnableExecutor eventQueue = synchronous ? new SynchronousRunnableExecutor() : new AsynchronousRunnableExecutor();
final List<TrackedRace> trackedRacesCopy = new ArrayList<>();
Util.addAll(getTrackedRaces(), trackedRacesCopy);
threadLocalTransporter.ifPresent(ThreadLocalTransporter::rememberThreadLocalStates);
eventQueue.addWork(() -> {
withBeforeAndAfterHandling(threadLocalTransporter, () -> {
for (TrackedRace trackedRace : trackedRacesCopy) {
listenerToAdd.raceAdded(trackedRace);
}
});
return eventQueue;
});
return eventQueue;
});
} finally {
unlockTrackedRacesAfterRead();
@@ -287,10 +308,13 @@ public abstract class TrackedRegattaImpl implements TrackedRegatta {
@Override
public Future<Boolean> removeRaceListener(RaceListener listener) {
final CompletableFuture<Boolean> result = new CompletableFuture<Boolean>();
// Hold trackedRacesLock for read so that no addTrackedRace / removeTrackedRace runs
// concurrently while we're removing the listener; that keeps enqueEvent from possibly
// enqueuing an event on a listener we're about to consider gone. The remove itself is
// atomic via ConcurrentHashMap.remove; no dedicated lock on raceListeners is needed.
lockTrackedRacesForRead();
try {
final RunnableExecutor eventQueue = LockUtil.executeWithWriteLockAndResult(raceListenersLock,
() -> raceListeners.remove(listener));
final RunnableExecutor eventQueue = raceListeners.remove(listener);
if (eventQueue != null) {
eventQueue.addWork(() -> {
result.complete(Boolean.TRUE);
@@ -1,6 +1,8 @@
package com.sap.sailing.domain.windestimation;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.common.WindSource;
import com.sap.sailing.domain.tracking.Maneuver;
import com.sap.sailing.domain.tracking.TrackedRace;
import com.sap.sailing.domain.tracking.WindTrack;
@@ -10,7 +12,7 @@ import com.sap.sailing.domain.tracking.WindTrack;
* changes which are communicated to tracked race via its
* {@link TrackedRace#recordWind(com.sap.sailing.domain.common.Wind, WindSource, boolean)} and
* {@link TrackedRace#removeWind(com.sap.sailing.domain.common.Wind, WindSource)}.
*
*
* @author Vladislav Chumak (D069712)
*
*/
@@ -25,7 +27,39 @@ public interface IncrementalWindEstimation extends WindEstimationInteraction {
* Gets the produced wind track of this wind estimation
*/
WindTrack getWindTrack();
/**
* Feeds already-classified {@link Maneuver}s (typically loaded from the persistent maneuver
* cache after a fingerprint match, or produced by prior maneuver detection on this race)
* into the estimator. Unlike
* {@link WindEstimationInteraction#newManeuverSpotsDetected(Competitor, Iterable, com.sap.sailing.domain.maneuverdetection.TrackTimeInfo)},
* this hand-off skips the MST/HMM classification stages because the maneuver's type
* ({@link Maneuver#getType()}) is already known; the estimator only needs to convert each
* maneuver into a wind fix and merge those fixes into its wind track.
* <p>
*
* The typical caller is
* {@link com.sap.sailing.domain.tracking.impl.TrackedRaceImpl#setWindEstimation}: when a
* previously-null wind estimation transitions to a non-null one on a race whose maneuvers
* are already known (from DB load or from a prior computation), the maneuvers can be fed
* directly to the newly-installed estimator without re-running detection. See bug6241.
* <p>
*
* The default implementation does nothing, so existing implementors of
* {@link IncrementalWindEstimation} that don't need this hook remain source- and
* binary-compatible.
*
* @param competitor
* the competitor on whose track the maneuvers occurred; must not be {@code null}
* @param maneuvers
* the already-classified maneuvers to fold into the estimator's wind track; may
* be empty; must not be {@code null}. Only maneuvers whose
* {@link Maneuver#getType() type} maps to a classifiable maneuver type
* (TACK, JIBE, HEAD_UP, BEAR_AWAY) contribute a wind fix; others are skipped.
*/
default void alreadyClassifiedManeuversAvailable(Competitor competitor, Iterable<Maneuver> maneuvers) {
}
/**
* Allows test set-ups to wait until this estimator is done in case it performs tasks
* asynchronously.
@@ -51,7 +51,7 @@
<p ui:field="contentSailingAnalytics2" />
<g:Anchor ui:field="sailingAnalyticsDetailsAnchor"
styleName="{res.mainCss.button}">
<ui:text from='{i18n.sailingRaceManagerReadMore}' />
<ui:text from='{i18n.readMore}' />
</g:Anchor>
</div>
</div>
@@ -66,7 +66,7 @@
<sb:PlayStoreBadge targetUrl="{i18n.playstoreRacecommitteeApp}"/>
<g:Anchor ui:field="raceCommitteeAppDetailsAnchor"
styleName="{res.mainCss.button}">
<ui:text from='{i18n.sailingRaceManagerReadMore}' />
<ui:text from='{i18n.readMore}' />
</g:Anchor>
</div>
</div>
@@ -83,7 +83,7 @@
<sb:PlayStoreBadge targetUrl="{i18n.playstoreInsightApp}"/>
<g:Anchor ui:field="inSightAppDetailsAnchor"
styleName="{res.mainCss.button}">
<ui:text from='{i18n.sailInSightReadMore}' />
<ui:text from='{i18n.readMore}' />
</g:Anchor>
</div>
</div>
@@ -99,7 +99,7 @@
<sb:PlayStoreBadge targetUrl="{i18n.playStoreBuoyPingerApp}"/>
<g:Anchor ui:field="buoyPingerAppDetailsAnchor"
styleName="{res.mainCss.button}">
<ui:text from='{i18n.sailingBuoyPingerReadMore}' />
<ui:text from='{i18n.readMore}' />
</g:Anchor>
</div>
</div>
@@ -118,7 +118,7 @@
</p>
<g:Anchor ui:field="simulatorAppDetailsAnchor"
styleName="{res.mainCss.button}">
<ui:text from='{i18n.strategySimulatorReadMore}' />
<ui:text from='{i18n.readMore}' />
</g:Anchor>
</div>
</div>
@@ -23,7 +23,7 @@
<p ui:field="contentSailingAnalytics1" />
<p ui:field="contentSailingAnalytics2" />
<a class="{res.mainCss.button}" ui:field="sailingAnalyticsDetailsAnchor">
<ui:text from="{i18n.sailingRaceManagerReadMore}"/>
<ui:text from="{i18n.readMore}"/>
</a>
</g:HTMLPanel>
</s:content>
@@ -36,7 +36,7 @@
<p ui:field="contentSailingRaceManager" />
<sb:PlayStoreBadge targetUrl="{i18n.playstoreRacecommitteeApp}"/>
<a class="{res.mainCss.button}" ui:field="raceManagerAppDetailsAnchor">
<ui:text from="{i18n.sailingRaceManagerReadMore}"/>
<ui:text from="{i18n.readMore}"/>
</a>
</g:HTMLPanel>
</s:content>
@@ -50,7 +50,7 @@
<sb:AppStoreBadge targetUrl="{i18n.appstoreSapSailInsight}"/>
<sb:PlayStoreBadge targetUrl="{i18n.playstoreInsightApp}"/>
<a class="{res.mainCss.button}" ui:field="sailInSightAppDetailsAnchor">
<ui:text from="{i18n.sailInSightReadMore}"/>
<ui:text from="{i18n.readMore}"/>
</a>
</g:HTMLPanel>
</s:content>
@@ -63,7 +63,7 @@
<p ui:field="contentSailingBuoyPinger" />
<sb:PlayStoreBadge targetUrl="{i18n.playStoreBuoyPingerApp}"/>
<a class="{res.mainCss.button}" ui:field="buoyPingerAppDetailsAnchor">
<ui:text from="{i18n.sailInSightReadMore}"/>
<ui:text from="{i18n.readMore}"/>
</a>
</g:HTMLPanel>
</s:content>
@@ -77,7 +77,7 @@
<ui:text from='{i18n.contentStrategySimulator}' />
</p>
<a class="{res.mainCss.button}" ui:field="simulatorAppDetailsAnchor">
<ui:text from="{i18n.strategySimulatorReadMore}"/>
<ui:text from="{i18n.readMore}"/>
</a>
</g:HTMLPanel>
</s:content>
@@ -2565,17 +2565,14 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages,
String contentSailingAnalytics2(String brandName);
String sailingRaceManager(String brandName);
String contentSailingRaceManager(String brandName);
String sailingRaceManagerReadMore();
String sailInSight(String brandName);
String sailInSightName();
String contentSailInSight(String brandName);
String sailInSightReadMore();
String sailingBuoyPinger(String brandName);
String contentSailingBuoyPinger(String brandName);
String sailingBuoyPingerReadMore();
String strategySimulator();
String contentStrategySimulator();
String strategySimulatorReadMore();
String readMore();
String testConnection();
String tracTracConnectionTestFailed(String message);
String ipsLockedForBearerTokenAbuse();
@@ -1760,17 +1760,14 @@ contentSailingAnalytics1=By delivering real time analytics around race rankings,
contentSailingAnalytics2={0} Sailing Analytics provide insights and transparency to the world of sailing by utilizing Cloud and In-Memory Technology, processing GPS and wind measurement data in real time and visualizing contents in various frontends accessible from anywhere.
sailingRaceManager={0} Sailing Race Manager
contentSailingRaceManager=The {0} Sailing Race Manager delivers greater efficiency and control to race organizers by automating and simplifying their manual tasks and communication during running sailing regattas. The Race Committee App helps simplify the operation of running and managing race events to help race committees operate smarter.
sailingRaceManagerReadMore=Learn More
readMore=Learn More
sailInSight=Sail Insight powered by {0}
sailInSightName=Sail Insight
contentSailInSight=With the {0} App, sailboat racers can join, set up and manage the GPS tracking for regattas of various formats easier than ever before. One design, as well as single number handicap regattas, are currently supported with ORC Polar Curve Scoring (PCS) coming soon. The mobile app connects to the {0} Sailing Analytics cloud solution that empowers sailors, coaches and fans to analyze their performance with a rich and unparalleled set of features.
sailInSightReadMore=Learn More
sailingBuoyPinger={0} Sailing Buoy Pinger
contentSailingBuoyPinger=With the {0} Sailing Buoy Pinger mobile app for Android phones, sailboat race managers can simplify the positioning of race marks and communication with the mark layers anywhere and anytime. This app connects to the {0} Sailing Analytics solution and allows regatta managers to track the geographical position of their race marks, enabling the use of the {0} Sail Insight powered by {0} mobile app by sailors participating in the regatta.
sailingBuoyPingerReadMore=Learn More
strategySimulator=Strategy Simulator
contentStrategySimulator=Choosing the right strategy can provide great competitive advantage during a race. The Strategy Simulator simplifies determining the best sailing strategy for various wind conditions and water currents. Since wind and current can be changed in the simulation right away, the robustness of a strategy can be easily evaluated even for uncertain weather conditions.
strategySimulatorReadMore=Learn More
impressum=Imprint
lastFix=Last fix
filterDeviceMappings=Filter device mappings
@@ -1726,17 +1726,14 @@ contentSailingAnalytics1={0} nabízí analýzy v reálném čase, které sleduj
contentSailingAnalytics2=S pomocí cloudové a in-memory technologie, zpracování dat z GPS a anemometrů a vizualizací výsledků dostupných odkudkoli a v nejrůznějších zobrazovacích prostředích {0} Sailing Analytics zajišťuje transparentnost a informovanost v jachtařském sportu.
sailingRaceManager={0} Sailing Race Manager
contentSailingRaceManager={0} Sailing Race Manager přináší organizátorům závodů možnost účinnějšího řízení a lepší kontroly díky zjednodušení manuálních úkonů a automatizaci některých procesů včetně komunikace během regat. Aplikace Race Committee usnadňuje organizování a řízení závodů a umožňuje závodním komisím kvalitnější práci.
sailingRaceManagerReadMore=Další informace
readMore=Další informace
sailInSight=Sail Insight s technologií od společnosti {0}
sailInSightName=Sail Insight
contentSailInSight=S aplikací {0} se mohou závodníci snadno připojit k trasování lodí pomocí GPS při regatách nejrůznějších formátů a toto trasování si nastavit a spravovat dle potřeby. V nejbližší době bude doplněna funkce hodnocení podle polární křivky ORC, kterou bude možné využít v regatách třídy One-Design, jakož i v regatách s jednočíselnými handicapy. Mobilní aplikace se připojuje ke cloudovému řešení {0} Sailing Analytics, které umožňuje jachtařům, trenérům i fanouškům analyzovat výkon pomocí jedinečné bohaté sady funkcí.
sailInSightReadMore=Další informace
sailingBuoyPinger={0} Sailing Buoy Pinger
contentSailingBuoyPinger=Mobilní aplikace {0} Sailing Buoy Pinger pro Android zjednodušuje organizátorům jachtařských závodů umisťování závodních značek a komunikaci s týmy, které je umisťují. Aplikace se připojuje k řešení {0} Sailing Analytics a pořadatelé regat mohou jejím prostřednictvím zaznamenat geografické polohy značek a umožnit tak závodníkům účastnícím se regaty používání mobilní aplikace {0} Sail Insight powered by {0}.
sailingBuoyPingerReadMore=Další informace
strategySimulator=Simulátor strategie
contentStrategySimulator=Volba správné strategie je při závodě rozhodující. Simulátor strategie zjednodušuje rozhodování o nejlepší strategii při nejrůznějších podmínkách větru a vodních proudů. Díky možnosti okamžité změny nastavení větru i proudů lze snadno zhodnotit účinnost zvolené strategie i při nejistých povětrnostních podmínkách.
strategySimulatorReadMore=Další informace
impressum=Tiráž
lastFix=Poslední záznam polohy
filterDeviceMappings=Filtrovat mapování zařízení
@@ -1726,17 +1726,14 @@ contentSailingAnalytics1=Ved at levere analyser i realtid for kapsejladsernes ra
contentSailingAnalytics2={0} Sailing Analytics giver indsigt og synlighed i sejlsportens verden ved at udnytte Cloud- og In-Memory-teknologi, der behandler GPS- og vindmåledata i realtid og visualiserer indholdet på forskellige frontends, der kan tilgås hvor som helst.
sailingRaceManager={0} Sailing Race Manager
contentSailingRaceManager={0} Sailing Race Manager giver kapsejladsledere større effektivitet og kontrol ved at automatisere og forenkle deres manuelle opgaver og kommunikation under igangværende sejlregattaer. Appen til kapsejladskomitéer hjælper med at forenkle afholdelsen og administrationen af kapsejladsbegivenheder for at give kapsejladskomitéer mulighed for at gøre det smartere.
sailingRaceManagerReadMore=Få mere at vide
readMore=Få mere at vide
sailInSight=Sail Insight powered by {0}
sailInSightName=Sail Insight
contentSailInSight=Med appen {0} kan deltagere i sejlbådskapsejladser tilmelde sig, opsætte og administrere GPS-sporing for regattaer i forskellige formater nemmere end nogensinde før. One-design og regattaer med handicap med et enkelt ciffer, der i øjeblikket understøttes med ORC Polar Curve Scoring (PCS), kommer snart. Mobilappen opretter forbindelse til {0} Sailing Analytics-cloud-løsningen, der giver sejlere, trænere og fans mulighed for at analysere deres præstation med en indholdsrig og enestående række af funktioner.
sailInSightReadMore=Få mere at vide
sailingBuoyPinger={0} Sailing Buoy Pinger
contentSailingBuoyPinger=Med mobilappen {0} Sailing Buoy Pinger til Android-telefoner kan ledere af sejlbådskapsejladser forenkle deres placering af kapsejladsmærker og kommunikationen med mærkeudlæggere hvor som helst. Denne app opretter forbindelse til {0} Sailing Analytics-løsningen og giver regattaledere mulighed for at spore den geografiske position af deres kapsejladsmærker, hvilket giver sejlere, der deltager i regattaen, mulighed for at bruge mobilappen {0} Sail Insight powered by {0}.
sailingBuoyPingerReadMore=Få mere at vide
strategySimulator=Strategisimulator
contentStrategySimulator=Valget af den rette strategi kan give en stor konkurrencemæssig fordel under en kapsejlads. Strategisimulatoren forenkler bestemmelsen af den bedste sejlstrategi for forskellige vindforhold og vandstrømninger. Da vinden og vandstrømme kan ændres i simuleringen med det samme, kan robustheden af en strategi nemt evalueres, selv for usikre vejrforhold.
strategySimulatorReadMore=Få mere at vide
impressum=Impressum
lastFix=Seneste registrering
filterDeviceMappings=Filtrer enhedsallokeringer
@@ -1736,17 +1736,14 @@ contentSailingAnalytics1=Durch die Bereitstellung von Echtzeit-Analysen zu Rangl
contentSailingAnalytics2={0} Sailing Analytics nutzt Cloud- und In-Memory-Technologie, verarbeitet GPS- und Winddaten in Echtzeit und visualisiert Inhalte in verschiedenen Frontends, die von überall zugänglich sind, um transparente Einblicke in die Welt des Segelns zu liefern.
sailingRaceManager={0} Sailing Race Manager
contentSailingRaceManager=Der {0} Sailing Race Manager bietet Rennveranstaltern mehr Effizienz und Kontrolle, indem sie ihre manuellen Aufgaben und die Kommunikation während Regatten automatisiert und vereinfacht. Die Race Committee App unterstützt die Durchführung und Abwicklung von Segelrennen und hilft Rennleitungen dadurch, intelligenter zu arbeiten.
sailingRaceManagerReadMore=Mehr erfahren
readMore=Mehr erfahren
sailInSight=Sail Insight powered by {0}
sailInSightName=Sail Insight
contentSailInSight=Mit der {0} können Regattasegler jetzt einfacher denn je am GPS-Tracking von Regatten verschiedener Formate teilnehmen sowie selbst Regatten zum Tracken anlegen und verwalten. Es werden Regatten in Einheitsklassen sowie verschiedenen Handicap-Varianten unterstützt, wobei ORC Polar Curve Scoring (PCS) bald angeboten werden wird. Die App nutzt die {0} Sailing Analytics Cloud-Lösung, die Segler, Trainer und Fans gleichermaßen in die Lage versetzt, informative und umfassende Performance-Analysen durchzuführen.
sailInSightReadMore=Learn More
sailingBuoyPinger={0} Sailing Buoy Pinger
contentSailingBuoyPinger=Mit der {0} Sailing Buoy Pinger App für Android Telefone, können Wettfahrtleiter die Auslage der Kursmarken sowie die Kommunikation mit den Tonnenlegern vereinfachen überall und jederzeit. Diese App verbindet sich mit der {0} Sailing Analytics Lösung und ermöglicht Wettfahrtleitern die geografische Position ihrer Kursmarken zu erfassen. Dies ermöglicht es teilnehmenden Seglern die App Sail Insight powered by {0} bei einer Regatta zu verwenden.
sailingBuoyPingerReadMore=Learn More
strategySimulator=Strategie-Simulator
contentStrategySimulator=Während eines Rennens hängt viel von der Wahl der richtigen Strategie ab. Der Strategie-Simulator vereinfacht die Ermittlung der besten Segelstrategie für diverse Wind- und Strömungsbedingungen. Da Wind und Strömung sofort in der Simulation geändert werden können, lässt sich die Robustheit einer Strategie selbst für unsichere Wetterbedingungen problemlos bewerten.
strategySimulatorReadMore=Mehr erfahren
impressum=Impressum
lastFix=Letzter Fix
filterDeviceMappings=Gerätezuordnungen filtern
@@ -1726,17 +1726,14 @@ contentSailingAnalytics1=Al proporcionar análisis en tiempo real sobre clasific
contentSailingAnalytics2={0} Sailing Analytics proporciona conocimientos y transparencia en el mundo de la navegación con la tecnología Cloud e In-Memory, procesando datos de medida GPS y de viento en tiempo real y visualizando contenidos en diversos front ends accesibles desde cualquier lugar.
sailingRaceManager={0} Sailing Race Manager
contentSailingRaceManager=El {0} Sailing Race Manager proporciona una mayor eficacia y control a organizadores de pruebas automatizando y simplificando sus tareas manuales y la comunicación durante las regatas de navegación. La aplicación Race Commitee ayuda a simplificar la operación de ejecución y gestión de eventos regatistas para ayudar a los comités de prueba a operar de manera más inteligente.
sailingRaceManagerReadMore=Más información
readMore=Más información
sailInSight=Sail InSight powered by {0}
sailInSightName=Sail Insight
contentSailInSight=Con la aplicación {0}, los participantes en las pruebas de veleros podrán gestionar y configurar el rastreo por GPS para regatas de diversos formatos de forma más sencilla. Actualmente, la puntuación por curva polar (PCS) de ORC en breve solo admite un diseño, así como un único número de regatas de hándicap. Esta aplicación móvil conecta la solución en la nube de {0} Sailing Analytics y permite a los navegantes, entrenadores, fans analizar su rendimiento con un rico conjunto de inigualables características.
sailInSightReadMore=Más información
sailingBuoyPinger={0} Sailing Buoy Pinger
contentSailingBuoyPinger=Con la aplicación móvil {0} Sailing Buoy Pinger para teléfonos Android, los directores deportivos de pruebas de veleros podrán simplificar el posicionamiento de las marcas de prueba y la comunicación mediante las capas de marca en cualquier lugar y en cualquier momento. Esta aplicación conecta con la solución {0} Sailing Analytics y permite a los directores de regata rastrear la posición geográfica de sus propias marcas de prueba, haciendo posible el uso de la aplicación móvil {0} Sail InSight powered by {0} por los navegantes que participan en la regata.
sailingBuoyPingerReadMore=Más información
strategySimulator=Simulador de estrategias
contentStrategySimulator=Al seleccionar la estrategia de la derecha puede proporcionar una gran ventaja competitiva durante la prueba. El simulador de estrategias determina la mejor estrategia de navegación para determinadas condiciones de viento y corrientes de marinas. Como el viento y la corriente pueden modificarse en la simulación de forma inmediata, la robustez de una estrategia podrá evaluarse rápidamente incluso para condiciones meteorológicas variables.
strategySimulatorReadMore=Más información
impressum=Marca
lastFix=Última corrección
filterDeviceMappings=Filtrar asignaciones de dispositivos
@@ -1726,17 +1726,14 @@ contentSailingAnalytics1=Grâce aux analyses en temps réel des classements de c
contentSailingAnalytics2=Grâce aux analyses de la solution {0} Sailing Analytics, le monde de la voile gagne en transparence. Les éléments clés sont l''utilisation du Cloud et des technologies du traitement des données en mémoire vive, le traitement des données de GPS et de mesures de vent en temps réel ainsi que la visualisation de ces contenus sur différents types frontend, accessibles partout.
sailingRaceManager={0} Sailing Race Manager
contentSailingRaceManager=Grâce à la solution {0} Sailing Race Manager, les organisateurs de courses peuvent être plus efficaces et garder plus facilement la main. Leurs tâches manuelles sont automatisées et simplifiées, tout comme la communication pendant les régates de voile en cours. L''application destinée au comité de course contribue à faciliter la mise en œuvre et la gestion des manifestations de courses pour un déroulement plus souple des compétitions.
sailingRaceManagerReadMore=En savoir plus
readMore=En savoir plus
sailInSight=Sail Insight powered by {0}
sailInSightName=Sail Insight
contentSailInSight=Grâce à l''application {0}, les participants aux courses à la voile peuvent configurer et gérer la localisation via GPS pour différentes régates encore plus facilement. Une configuration tout comme des régates handicap à numéro unique sont actuellement prises en charge, et bientôt aussi l''attribution des points à l''aide de la courbe polaire ORC. Cette application mobile se connecte à la solution Cloud {0} Sailing Analytics et permet aux compétiteurs, à leurs coachs et à leurs supporteurs d''analyser leurs performances avec un ensemble de fonctionnalités complet et unique en son genre.
sailInSightReadMore=En savoir plus
sailingBuoyPinger={0} Sailing Buoy Pinger
contentSailingBuoyPinger=L''application mobile {0} Sailing Buoy Pinger pour Android permet aux organisateurs de courses à la voile de simplifier le positionnement des marques de course ainsi que la communication avec les positionneurs des marques, à tout moment, où qu''ils soient. Cette application se connecte à la solution {0} Sailing Analytics et permet aux organisateurs de régates de suivre la position géographique de leurs marques de course. Cette connexion permet en outre aux sportifs participant à la régate d''utiliser l''application mobile {0} Sail Insight powered by {0}.
sailingBuoyPingerReadMore=En savoir plus
strategySimulator=Simulateur de stratégies
contentStrategySimulator=Opter pour la bonne stratégie peut procurer un avantage significatif pendant une course. Le simulateur de stratégies simplifie la détermination de la meilleure stratégie de navigation pour différentes conditions de vent et courants d''eau. Comme les données de vent et de courant peuvent être modifiées directement au cours de la simulation, la solidité d''une stratégie peut être facilement évaluée même en cas de conditions météorologiques incertaines.
strategySimulatorReadMore=En savoir plus
impressum=Mentions légales
lastFix=Dernier point
filterDeviceMappings=Filtrer des mappages d''appareils
@@ -1726,17 +1726,14 @@ contentSailingAnalytics1=Offrendo lanalisi in tempo reale di classifiche dell
contentSailingAnalytics2={0} Sailing Analytics fornisce approfondimenti e dona trasparenza al mondo della vela, attraverso tecnologie cloud e in-memory, lelaborazione di dati GPS e di misurazione del vento in tempo reale e la visualizzazione di contenuti in vari front-end accessibili da qualsiasi luogo.
sailingRaceManager={0} Sailing Race Manager
contentSailingRaceManager={0} Sailing Race Manager offre maggiore efficienza e controllo agli organizzatori delle gare, automatizzandone e semplificandone operazioni manuali e comunicazioni durante lo svolgimento delle regate veliche. Lapp Race Committee contribuisce a semplificare le operazioni di svolgimento e gestione degli eventi delle gare per aiutare i comitati delle gare a operare in maniera più efficiente.
sailingRaceManagerReadMore=Maggiori informazioni
readMore=Maggiori informazioni
sailInSight=Sail Insight powered by {0}
sailInSightName=Sail Insight
contentSailInSight=Grazie allapp mobile {0} Sail Insight, i regatanti possono partecipare, configurare e gestire il proprio tracciamento GPS per regate di vari formati nel modo più semplice. Lapp supporta un design nonché regate con handicap a numero singolo e presto anche la curva polare ORC. L''app mobile si collega alla soluzione cloud {0} Sailing Analytics che consente a velisti, allenatori e appassionati di analizzare le proprie prestazioni mediante un ricco insieme di funzionalità senza precedenti.
sailInSightReadMore=Maggiori informazioni
sailingBuoyPinger={0} Sailing Buoy Pinger
contentSailingBuoyPinger=Grazie allapp mobile {0} Sailing Buoy Pinger per dispositivi Android, i responsabili di gara possono semplificare il posizionamento dei segnali di gara e le comunicazioni con i posaboe, ovunque e in qualsiasi momento. Lapp si connette con {0} Sailing Analytics e consente ai responsabili della regata di tracciare la posizione geografica dei segnali di gara, consentendo luso dellapp mobile {0} Sail Insight powered by {0} da parte dei velisti che partecipano alla regata.
sailingBuoyPingerReadMore=Maggiori informazioni
strategySimulator=Simulatore di strategia
contentStrategySimulator=Scegliere la strategia giusta può fornire un grande vantaggio competitivo durante una gara. Il Simulatore di strategia semplifica la determinazione della migliore strategia di navigazione per diverse condizioni di vento e correnti marine. Dal momento che il vento e le correnti possono essere immediatamente modificate nella simulazione, la forza di una strategia può essere facilmente valutata anche con condizioni meteorologiche incerte.
strategySimulatorReadMore=Maggiori informazioni
impressum=Sigla editoriale
lastFix=Ultimo punto nave
filterDeviceMappings=Filtra mappaggi dispositivo
@@ -1726,17 +1726,14 @@ contentSailingAnalytics1=レースランキング、艇速、マニューバー
contentSailingAnalytics2={0} Sailing Analytics は、クラウドおよびインメモリ技術を活用し、リアルタイムで GPS および風計測データを処理し、どこからでもアクセスできるさまざまなフロントエンドでコンテンツを視覚化することによってセーリングの世界にインサイトと透明性を提供します。
sailingRaceManager={0} Sailing Race Manager
contentSailingRaceManager={0} Sailing Race Manager は、セーリングレガッタ運営中にレース主催者が行うマニュアルタスクや連絡業務の自動化および簡略化により効率と統制を大幅に向上させます。Race Committee アプリは、レースイベントの運営管理業務を簡略化するのに役立ち、レース委員会がより円滑に運営されるよう支援します。
sailingRaceManagerReadMore=詳細情報
readMore=詳細情報
sailInSight=Sail Insight powered by {0}
sailInSightName=Sail Insight
contentSailInSight={0} アプリにより、ヨットレース競技者はこれまでよりさらに簡単にさまざまな形式のレガッタの GPS 追跡に参加し、設定を行い、管理することができます。One Design およびシングルナンバーハンディキャップのレガッタが近日公表の ORC 極座標曲線スコアリング方式 (PCS) で現在サポートされています。このモバイルアプリは、豊富で比類のない一連の機能によるパフォーマンスの分析でセーラー、コーチ、そしてファンを支援する {0} Sailing Analytics クラウドソリューションに接続するものです。
sailInSightReadMore=詳細情報
sailingBuoyPinger={0} Sailing Buoy Pinger
contentSailingBuoyPinger=Android フォン向け {0} Sailing Buoy Pinger モバイルアプリにより、ヨットレース運営者はいつでもどこでもレース用のマークの位置決めや、マーク設定担当者とのやり取りを簡略化することができます。このアプリは、{0} Sailing Analytics ソリューションに接続され、レガッタ運営者がレース用のマークの地理的位置を追跡することを可能にし、レガッタに参加しているセーラーが {0} Sail Insight powered by {0} モバイルアプリを使用できるようにします。
sailingBuoyPingerReadMore=詳細情報
strategySimulator=戦略シミュレータ
contentStrategySimulator=適切な戦略を選択すれば、レース中に大きな競争上の優位性を得ることが可能です。Strategy Simulator により、さまざまな風況および水流に対して最良のセーリング戦略を決定することが容易になります。風と水流をシミュレーションで直ちに変更することができるため、不確かな気象条件に対しても戦略の確かさを簡単に評価することができます。
strategySimulatorReadMore=詳細情報
impressum=刻印
lastFix=最終フィックス
filterDeviceMappings=デバイスマッピングのフィルタリング
@@ -1726,17 +1726,14 @@ contentSailingAnalytics1=Ao oferecer análises em tempo real de classificações
contentSailingAnalytics2={0} Sailing Analytics fornece análises e transparência ao mundo da vela utilizando tecnologia na nuvem e in-memory, processando dados de GPS e de medição do vento em tempo real e visualizando os conteúdos em vários front ends acessíveis a partir de qualquer ponto.
sailingRaceManager={0} Sailing Race Manager
contentSailingRaceManager=O {0} Sailing Race Manager fornece maior eficiência e controle aos organizadores das corridas automatizando e simplificando suas tarefas manuais e a comunicação durante as regatas a vela. O app da comissão da corrida ajuda a simplificar a operação de gerenciamento e administração dos eventos de corridas de forma a ajudar as comissões das corridas a funcionarem de forma mais inteligente.
sailingRaceManagerReadMore=Saber mais
readMore=Saber mais
sailInSight=Sail InSight suportado por {0}
sailInSightName=Sail Insight
contentSailInSight=Com o app {0}, os competidores de vela podem aderir, configurar e administrar o rastreamento GPS para regatas de vários formatos mais facilmente do que nunca. As regatas de "handicap" com design e número individual são atualmente suportadas, com a pontuação da curva polar ORC em breve. O app móvel estabelece a conexão à solução na nuvem {0} Sailing Analytics que permite que velejadores, treinadores e fãs analisem sua performance com um conjunto de funções completo e inigualável.
sailInSightReadMore=Saber mais
sailingBuoyPinger={0} Sailing Buoy Pinger
contentSailingBuoyPinger=Com o app móvel {0} Sailing Buoy Pinger para Android, os administradores de corridas a vela podem simplificar o posicionamento de marcas da corrida e a comunicação com os responsáveis pelas marcas em qualquer lugar e em qualquer altura. Este app estabelece a conexão à solução {0} Sailing Analytics e permite que os administradores de regatas rastreiem a posição geográfica das suas marcas da corrida, permitindo a utilização do app móvel {0} Sail InSight suportado pela {0} por velejadores participantes na regata.
sailingBuoyPingerReadMore=Saber mais
strategySimulator=Simulador de estratégia
contentStrategySimulator=A escolha da estratégia certa pode fornecer uma grande vantagem competitiva durante uma corrida. O simulador de estratégia simplifica determinando a melhor estratégia de vela para várias condições do vento e correntes marítimas. Uma vez que o vento e a corrente podem ser modificados de imediato na simulação, a confiança de uma estratégia pode ser avaliada facilmente mesmo com condições meteorológicas incertas.
strategySimulatorReadMore=Saber mais
impressum=Impressão
lastFix=Último ponto fixo
filterDeviceMappings=Filtrar mapeamentos de dispositivos
@@ -1726,17 +1726,14 @@ contentSailingAnalytics1=Предоставляя аналитику по дан
contentSailingAnalytics2={0} Sailing Analytics делает мир парусного спорта понятным и прозрачным, используя облако и технологию In-Memory, обработку данных GPS и параметров ветра в реальном времени, а также визуализацию контекста в разных пользовательских интерфейсах, доступных где угодно.
sailingRaceManager={0} Sailing Race Manager
contentSailingRaceManager=Приложение {0} Sailing Race Manager повышает уровень эффективности и контроля для организаторов гонки за счет автоматизации и упрощения выполняемых вручную задач и общения во время проведения парусных регат. Приложение Race Committee помогает упростить проведение событий и управление ими, освобождая для оргкомитета время на лучшую проработку деталей.
sailingRaceManagerReadMore=Узнать больше
readMore=Узнать больше
sailInSight=Sail Insight powered by {0}
sailInSightName=Sail Insight
contentSailInSight=С помощью приложения {0} участники парусных гонок могут настраивать GPS-отслеживание регат различных форматов и управлять им проще, чем когда-либо ранее. В настоящее время поддерживаются монотипные и гандикапные регаты, а скоро будет реализована поддержка системы оценки по кривой скорости ORC. Это мобильное приложение подключается к облачному решению {0} Sailing Analytics, что позволяет мореходам, тренерам и болельщикам анализировать эффективность с помощью беспрецедентно широкого набора функций.
sailInSightReadMore=Узнать больше
sailingBuoyPinger={0} Sailing Buoy Pinger
contentSailingBuoyPinger=Мобильное приложение {0} Sailing Buoy Pinger для смартфонов Android позволяет руководителям парусных гонок упростить позиционирование отметок и общение с установщиками отметок откуда и когда угодно. Это приложение подключается к решению {0} Sailing Analytics и позволяет руководителям гонок отслеживать географическое местоположение гоночных отметок, обеспечивая использование мобильного приложения {0} Sail Insight powered by {0} для яхтсменов, участвующих в регате.
sailingBuoyPingerReadMore=Узнать больше
strategySimulator=Симулятор стратегии
contentStrategySimulator=Выбор правильной стратегии может обеспечить большое конкурентное преимущество во время гонки. Симулятор стратегии упрощает определение лучшей стратегии хождения под парусом для разных атмосферных условий и водных течений. Возможность мгновенного изменения ветра и течения в симуляции позволяет легко оценить надежность стратегии даже для неопределенных погодных условий.
strategySimulatorReadMore=Узнать больше
impressum=Печать
lastFix=Последний замер
filterDeviceMappings=Фильтровать сопоставления устройств
@@ -1726,17 +1726,14 @@ contentSailingAnalytics1={0} pomaga poenostaviti in osvetliti zapleten svet jadr
contentSailingAnalytics2={0}Sailing Analytics zagotavlja vpoglede in vnaša preglednost v svet jadranja, za kar uporablja tehnologijo Cloud ter In-Memory, sprotno obdelavo GPS-podatkov in podatkov o meritvah vetra ter vizualizacijo vsebin v različnih frontendih, ki so dostopni povsod.
sailingRaceManager={0} Sailing Race Manager
contentSailingRaceManager={0} Sailing Race Manager organizatorjem plovov prinaša večjo učinkovitost in nadzor, saj avtomatizira in poenostavlja opravila, ki jih sicer izvajajo ročno, in komunikacijo med regatami. Aplikacija Race Committee pomaga poenostaviti izvajanje in upravljanje dogodkov med plovi, da lahko regatni odbori pametneje opravljajo svoje delo.
sailingRaceManagerReadMore=Več o tem
readMore=Več o tem
sailInSight=Sail Insight s tehnologijo {0}
sailInSightName=Sail Insight
contentSailInSight=Z aplikacijo {0} se lahko jadralci še lažje pridružijo, nastavijo in upravljajo GPS-sledenje za regate v različnih oblikah. Trenutno so podprte regate razreda One Design in regate z enomestnim hendikepom, kmalu pa prihaja še točkovanje glede na polarno krivuljo ORC (PCS). Mobilna aplikacija se poveže z rešitvijo v oblaku {0} Sailing Analytics, s katero lahko jadralci, trenerji in navdušenci analizirajo nastope z bogatim naborom funkcij brez primere.
sailInSightReadMore=Več o tem
sailingBuoyPinger={0} Sailing Buoy Pinger
contentSailingBuoyPinger=Z mobilno aplikacijo {0}Sailing Buoy Pinger za telefone s sistemom Android lahko vodje jadralnega tekmovanja poenostavijo postavitev oznak za plove ter komunikacijo s sloji oznak vedno in povsod. Ta aplikacija se poveže z rešitvijo {0} Sailing Analytics in vodjem regat omogoča, da sledijo geografskim položajem oznak za plove, jadralcem, ki sodelujejo v regati, pa omogoča uporabo {0} mobilne aplikacije {0} Sail Insight s tehnologijo.
sailingBuoyPingerReadMore=Več o tem
strategySimulator=Simulator strategij
contentStrategySimulator=Izbira prave strategije lahko pomeni veliko konkurenčno prednost med plovom. Simulator strategij poenostavi določitev najboljše jadralne strategije v različnih vetrovnih pogojih in vodnih tokovih. Ker je veter in tok mogoče sproti spreminjati v simulaciji, je mogoče tudi preprosto preveriti izvedljivost strategije za negotove vremenske razmere.
strategySimulatorReadMore=Več o tem
impressum=O nas
lastFix=Zadnji položaj
filterDeviceMappings=Filtriraj preslikave naprav
@@ -1726,17 +1726,14 @@ contentSailingAnalytics1=通过实时分析比赛轮次排名、速度、操作
contentSailingAnalytics2={0} Sailing Analytics 利用云和内存技术实时处理 GPS 和风力测量数据,并在可从任何地方访问的前端显示,让用户全面、透彻地了解航行运动。
sailingRaceManager={0} Sailing Race Manager
contentSailingRaceManager={0} Sailing Race Manager 通过自动化及简化赛事主办机构在航行比赛期间的人工作业和沟通,实现更高的效率和控制能力。Race Committee 应用简化举办和管理比赛活动的操作,使竞赛委员会的操作更加智能。
sailingRaceManagerReadMore=了解更多
readMore=了解更多
sailInSight=Sail Insight powered by {0}
sailInSightName=Sail Insight
contentSailInSight=使用 {0} 应用,帆船选手可以比之前更容易地加入、设置和管理各种形式比赛的 GPS 跟踪。当前支持统一设计级别以及单号障碍赛以及不久将会推出的 ORC 极曲线记分(PCS)。此移动应用与 {0} Sailing Analytics 云解决方案相连,这使水手、教练和粉丝能够通过丰富且无与伦比的功能组合分析其成绩。
sailInSightReadMore=了解更多
sailingBuoyPinger={0} Sailing Buoy Pinger
contentSailingBuoyPinger=使用适用于 Android 手机的 {0} Sailing Buoy Pinger 移动应用,帆船赛赛事经理可以随时随地简化比赛航标的定位和与布标裁判的沟通。此应用与 {0} Sailing Analytics 解决方案相连,比赛经理可以跟踪其比赛航标的地理位置,并允许参赛水手使用 {0} Sail Insight powered by {0} 移动应用。
sailingBuoyPingerReadMore=了解更多
strategySimulator=策略模拟器
contentStrategySimulator=选择正确的策略可在比赛期间极大地提高竞争优势。策略模拟器为不同的风力条件和水流简化确定最佳的航行策略。由于可以在模拟中即时更改风和水流,即便在不确定的天气条件下,也可以轻松地评估策略的稳健性。
strategySimulatorReadMore=了解更多
impressum=版本说明
lastFix=上次修复
filterDeviceMappings=筛选设备映射
@@ -20,6 +20,7 @@ Require-Bundle:
com.sap.sailing.polars.datamining,
com.sap.sailing.server.gateway.serialization.shared.android,
com.sap.sailing.server.gateway.serialization,
com.sap.sailing.server.interface,
com.sap.sse.datamining,
com.sap.sse.datamining.annotations,
com.sap.sse.datamining.shared,
@@ -11,18 +11,36 @@ import java.util.logging.Logger;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;
import org.osgi.util.tracker.ServiceTracker;
import com.sap.sailing.domain.base.DomainFactory;
import com.sap.sailing.domain.polars.PolarDataService;
import com.sap.sailing.polars.ReplicablePolarService;
import com.sap.sailing.polars.jaxrs.client.PolarDataClient;
import com.sap.sailing.server.interfaces.RacingEventService;
import com.sap.sse.replication.Replicable;
import com.sap.sse.util.ClearStateTestSupport;
/**
* Handles OSGi (de-)registration of the polar data service.
*
* @author D054528 (Frederik Petersen)
* Bundle activator that constructs and registers the {@link PolarDataService} in the OSGi
* registry exactly once, only after the service is fully initialized. Initialization consists of:
* <ol>
* <li>Waiting for the {@link RacingEventService} to be registered (via an OSGi
* {@link ServiceTracker}), then obtaining the {@link DomainFactory} from it.</li>
* <li>Installing that domain factory on the {@link PolarDataServiceImpl} instance so that
* subsequent deserialization (e.g. via {@link PolarDataService#runWithDomainFactory}) resolves
* boat classes against the same factory the rest of the server uses.</li>
* <li>Optionally fetching polar regression data from a remote URL (if
* {@value #POLAR_DATA_SOURCE_URL_PROPERTY_NAME} is set), which populates the miner's
* regressions before any consumer sees the service.</li>
* <li>Registering the service in the OSGi registry.</li>
* </ol>
*
* With this ordering, the OSGi contract "if it's registered, it's ready" holds. Consumers
* (notably {@code RacingEventServiceImpl.setPolarDataService}) are only invoked once with a
* usable service and the previous unregister/re-register dance around the remote fetch is gone.
* See bug6241.
*
* @author D054528 (Frederik Petersen)
*/
public class Activator implements BundleActivator {
@@ -32,66 +50,94 @@ public class Activator implements BundleActivator {
private static final Logger logger = Logger.getLogger(Activator.class.getName());
private final Set<ServiceRegistration<?>> registrations = new HashSet<>();
private volatile Thread initializerThread;
private volatile ServiceTracker<RacingEventService, RacingEventService> racingEventServiceTracker;
@Override
public void start(BundleContext context) throws Exception {
logger.info("Registering PolarDataService");
PolarDataServiceImpl service = new PolarDataServiceImpl();
final ServiceRegistration<PolarDataService> polarDataServiceRegistration = context.registerService(PolarDataService.class, service, null);
registrations.add(polarDataServiceRegistration);
final Dictionary<String, String> replicableServiceProperties = new Hashtable<>();
replicableServiceProperties.put(Replicable.OSGi_Service_Registry_ID_Property_Name, service.getId().toString());
registrations.add(context.registerService(Replicable.class, service, replicableServiceProperties));
registrations.add(context.registerService(ClearStateTestSupport.class.getName(), service, null));
logger.info("PolarDataService bundle started; awaiting RacingEventService before initializing and registering");
final String polarDataSourceURL = System.getProperty(POLAR_DATA_SOURCE_URL_PROPERTY_NAME);
final String polarDataBearerToken = System.getProperty(POLAR_DATA_SOURCE_BEARER_TOKEN_PROPERTY_NAME);
if (polarDataSourceURL != null && !polarDataSourceURL.isEmpty()) {
waitForRacingEventServiceToObtainDomainFactory(polarDataSourceURL,
Optional.ofNullable(polarDataBearerToken), service, context, polarDataServiceRegistration);
}
// We wait on the OSGi RacingEventService instead of relying on the previous pattern where
// PolarDataService was registered early and then unregistered/re-registered around the
// remote data fetch. Reversing the dependency direction (polar bundle actively pulls the
// domain factory from RacingEventService via a ServiceTracker) lets us register the
// PolarDataService exactly once, after full initialization. See bug6241.
racingEventServiceTracker = new ServiceTracker<>(context, RacingEventService.class, null);
racingEventServiceTracker.open();
final Thread thread = new Thread(() -> {
try {
Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); // classpath:... Shiro ini files
initializeAndRegisterPolarDataService(context, polarDataSourceURL,
Optional.ofNullable(polarDataBearerToken));
} catch (InterruptedException e) {
logger.log(Level.WARNING,
"Interrupted while waiting for RacingEventService; polar-data service will not be registered",
e);
} catch (Exception e) {
logger.log(Level.SEVERE, "Failed to initialize and register PolarDataService", e);
}
}, "PolarDataService initializer");
thread.setDaemon(true);
initializerThread = thread;
thread.start();
}
/**
* Spawns a daemon thread that waits for the domain factory to be registered with the {@link PolarDataService}, then
* unregisters the service from the OSGi registry because it will temporarily become unusable, runs the polar data
* import from the given URL and registers the service again, adding the service registration object to the set of
* {@link #registrations}. The domain factory is required to resolve boat classes during de-serialization.
* @param polarDataServiceRegistration
* used to remove the service registration temporarily while updating the service by a remote import
* @param registerPolarServiceCallback
* called when the polar data has successfully been obtained; expected to register the polar service as
* {@link Replicable} and as the {@link PolarDataService} with the OSGi registry
*/
private void waitForRacingEventServiceToObtainDomainFactory(final String polarDataSourceURL,
Optional<String> polarDataBearerToken, final ReplicablePolarService polarService,
final BundleContext context, ServiceRegistration<PolarDataService> polarDataServiceRegistration) {
final Thread t = new Thread(() -> {
private void initializeAndRegisterPolarDataService(final BundleContext context,
final String polarDataSourceURL, final Optional<String> polarDataBearerToken)
throws InterruptedException {
logger.info("Waiting for RacingEventService to become available...");
final RacingEventService racingEventService = racingEventServiceTracker.waitForService(0);
if (racingEventService == null) {
logger.warning("RacingEventService tracker returned null (bundle stopping?); aborting polar-data initialization");
} else {
final DomainFactory domainFactory = racingEventService.getBaseDomainFactory();
logger.info("Obtained DomainFactory from RacingEventService; constructing PolarDataService");
// Gated mode: RacingEventServiceImpl.restoreTrackedRaces() promises to call
// markLoadingOfAllRacesToRestoreStarted() once its enumeration loop has fired for every
// race to restore. Until then, runWhenPolarLoadingFinishedFor(...) callbacks are held
// to avoid firing on a transient idle window of the loading pipeline between races.
// See bug6241.Why
final PolarDataServiceImpl service = new PolarDataServiceImpl(
/* waitForLoadingOfAllRacesToRestoreToBeStarted */ true);
service.registerDomainFactory(domainFactory);
if (polarDataSourceURL != null && !polarDataSourceURL.isEmpty()) {
logger.info("Fetching polar regression data from " + polarDataSourceURL);
final PolarDataClient polarDataClient = new PolarDataClient(polarDataSourceURL, service,
polarDataBearerToken);
try {
Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); // ensure that classpath:... Shiro ini files are resolved properly
logger.info("Waiting for domain factory to be registered with PolarService...");
// Note: although the domainFactory parameter isn't used, using runWithDomainFactory ensures that the domain factory is there
polarService.runWithDomainFactory(domainFactory -> {
PolarDataClient polarDataClient = new PolarDataClient(polarDataSourceURL, polarService, polarDataBearerToken);
try {
polarDataServiceRegistration.unregister();
polarDataClient.updatePolarDataRegressions();
registrations.add(context.registerService(PolarDataService.class, polarService, null));
} catch (Exception e) {
logger.log(Level.SEVERE, "Exception while trying to import polar data from "+polarDataSourceURL, e);
}
});
} catch (InterruptedException e) {
logger.log(Level.SEVERE, "Interrupted while waiting for UserStore service", e);
polarDataClient.updatePolarDataRegressions();
logger.info("Polar regression data loaded from " + polarDataSourceURL);
} catch (Exception e) {
// Log SEVERE but continue with registration: an empty regression state is
// preferable to no service at all, and the polars can still be updated later
// through other channels (JAX-RS API, replication).
logger.log(Level.SEVERE, "Exception while trying to import polar data from " + polarDataSourceURL
+ "; registering PolarDataService anyway with whatever state it has now", e);
}
}, "PolarService activator waiting for domain factory to be registered");
t.setDaemon(true);
t.start();
}
logger.info("Registering PolarDataService");
registrations.add(context.registerService(PolarDataService.class, service, null));
final Dictionary<String, String> replicableServiceProperties = new Hashtable<>();
replicableServiceProperties.put(Replicable.OSGi_Service_Registry_ID_Property_Name,
service.getId().toString());
registrations.add(context.registerService(Replicable.class, service, replicableServiceProperties));
registrations.add(context.registerService(ClearStateTestSupport.class.getName(), service, null));
}
}
@Override
public void stop(BundleContext context) throws Exception {
logger.info("Unregistering PolarDataService");
for (ServiceRegistration<?> reg : registrations) {
final Thread thread = initializerThread;
if (thread != null) {
thread.interrupt();
}
final ServiceTracker<RacingEventService, RacingEventService> tracker = racingEventServiceTracker;
if (tracker != null) {
tracker.close();
}
for (final ServiceRegistration<?> reg : registrations) {
reg.unregister();
}
registrations.clear();
@@ -68,26 +68,59 @@ public class PolarDataServiceImpl extends AbstractReplicableWithObjectInputStrea
private DomainFactory domainFactory;
/**
* Constructs the polar data service with default generation settings.
* See {@link #PolarDataServiceImpl(boolean)}. Preserved across
* {@link #resetState() state resets}.
*/
private final boolean waitForLoadingOfAllRacesToRestoreToBeStarted;
/**
* Convenience constructor equivalent to
* {@code new PolarDataServiceImpl(false)}. See
* {@link #PolarDataServiceImpl(boolean)} for the meaning of the flag; briefly, this default
* does <em>not</em> enable the "wait until the client has finished triggering all startup
* races" gate on {@link #runWhenPolarLoadingFinishedFor(TrackedRace, Runnable)}. That gate
* is only needed by clients that batch-restore many races through this service and expect
* drain-callbacks to hold until that batch is fully queued. Ad-hoc uses and tests can rely
* on this default.
*/
public PolarDataServiceImpl() {
this(/* waitForLoadingOfAllRacesToRestoreToBeStarted */ false);
}
/**
* @param waitForLoadingOfAllRacesToRestoreToBeStarted
* when {@code true}, callbacks registered via
* {@link #runWhenPolarLoadingFinishedFor(TrackedRace, Runnable)} won't fire until
* the client has explicitly called {@link #markLoadingOfAllRacesToRestoreStarted()}
* <em>and</em> the race's fixes are in the loading pipeline. Constructing with
* {@code true} and never calling {@code markLoadingOfAllRacesToRestoreStarted()}
* will hold callbacks indefinitely used by the OSGi/production wiring which
* makes that promise. See bug6241 and
* {@link PolarDataMiner#PolarDataMiner(PolarSheetGenerationSettings, CubicRegressionPerCourseProcessor, SpeedRegressionPerAngleClusterProcessor, ClusterGroup, boolean)}.
*/
public PolarDataServiceImpl(boolean waitForLoadingOfAllRacesToRestoreToBeStarted) {
this.waitForLoadingOfAllRacesToRestoreToBeStarted = waitForLoadingOfAllRacesToRestoreToBeStarted;
resetState();
}
public PolarDataServiceImpl filterToBoatClasses(Iterable<BoatClass> boatClassesToFilterTo) {
final PolarDataMiner filteredPolarDataMiner = polarDataMiner.filterToBoatClasses(boatClassesToFilterTo);
// A filtered clone is a derived view, not driven by a startup-restore batch; keep the
// default (non-gated) mode regardless of this instance's setting.
final PolarDataServiceImpl filteredService = new PolarDataServiceImpl();
filteredService.polarDataMiner = filteredPolarDataMiner;
return filteredService;
}
@Override
public void resetState() {
PolarSheetGenerationSettings settings = PolarSheetGenerationSettingsImpl.createBackendPolarSettings();
ClusterGroup<Bearing> angleClusterGroup = createAngleClusterGroup();
CubicRegressionPerCourseProcessor cubicRegressionPerCourseProcessor = new CubicRegressionPerCourseProcessor();
SpeedRegressionPerAngleClusterProcessor speedRegressionPerAngleClusterProcessor = new SpeedRegressionPerAngleClusterProcessor(angleClusterGroup);
this.polarDataMiner = new PolarDataMiner(settings, cubicRegressionPerCourseProcessor, speedRegressionPerAngleClusterProcessor, angleClusterGroup);
this.polarDataMiner = new PolarDataMiner(settings, cubicRegressionPerCourseProcessor,
speedRegressionPerAngleClusterProcessor, angleClusterGroup,
waitForLoadingOfAllRacesToRestoreToBeStarted);
}
public boolean isCurrentlyActiveOrHasQueue() {
@@ -231,8 +264,23 @@ public class PolarDataServiceImpl extends AbstractReplicableWithObjectInputStrea
}
@Override
public void raceFinishedLoading(TrackedRace race) {
polarDataMiner.raceFinishedTracking(race);
public void raceFinishedLoading(TrackedRace race, Runnable callbackWhenRaceChangingToTrackingOfFinishedStatus) {
polarDataMiner.raceFinishedLoading(race, callbackWhenRaceChangingToTrackingOfFinishedStatus);
}
@Override
public void runWhenPolarLoadingFinishedFor(TrackedRace race, Runnable callback) {
polarDataMiner.runWhenPolarLoadingFinishedFor(race, callback);
}
@Override
public void raceRemoved(TrackedRace race) {
polarDataMiner.raceRemoved(race);
}
@Override
public void markLoadingOfAllRacesToRestoreStarted() {
polarDataMiner.markLoadingOfAllRacesToRestoreStarted();
}
@Override
@@ -308,7 +356,8 @@ public class PolarDataServiceImpl extends AbstractReplicableWithObjectInputStrea
CubicRegressionPerCourseProcessor cubicRegressionPerCourseProcessor = (CubicRegressionPerCourseProcessor) is.readObject();
SpeedRegressionPerAngleClusterProcessor speedRegressionPerAngleClusterProcessor = (SpeedRegressionPerAngleClusterProcessor) is.readObject();
polarDataMiner = new PolarDataMiner(backendPolarSettings, cubicRegressionPerCourseProcessor,
speedRegressionPerAngleClusterProcessor, speedRegressionPerAngleClusterProcessor.getAngleCluster());
speedRegressionPerAngleClusterProcessor, speedRegressionPerAngleClusterProcessor.getAngleCluster(),
waitForLoadingOfAllRacesToRestoreToBeStarted);
}
@Override
@@ -47,6 +47,10 @@ public class CubicRegressionPerCourseProcessor implements
*/
private transient ConcurrentMap<BoatClass, Set<PolarsChangedListener>> listeners;
private boolean isFinished;
private boolean isAborted;
public CubicRegressionPerCourseProcessor filterToBoatClasses(Iterable<BoatClass> boatClasses) {
final Set<BoatClass> allowedBoatClasses = Util.asSet(boatClasses);
final CubicRegressionPerCourseProcessor filteredProcessor = new CubicRegressionPerCourseProcessor();
@@ -219,41 +223,40 @@ public class CubicRegressionPerCourseProcessor implements
@Override
public Class<GroupedDataEntry<GPSFixMovingWithPolarContext>> getInputType() {
// TODO Auto-generated method stub
return null;
@SuppressWarnings("unchecked")
final Class<GroupedDataEntry<GPSFixMovingWithPolarContext>> result = (Class<GroupedDataEntry<GPSFixMovingWithPolarContext>>) (Class<?>) GroupedDataEntry.class;
return result;
}
@Override
public Class<Void> getResultType() {
// No result type here, since this is a special case of a processor. It's the end of the pipe so to say.
return null;
return Void.class;
}
@Override
public void finish() throws InterruptedException {
// Nothing to do here
isFinished = true;
}
@Override
public boolean isFinished() {
return false;
return isFinished;
}
@Override
public void abort() {
// TODO Auto-generated method stub
isAborted = true;
}
@Override
public boolean isAborted() {
// TODO Auto-generated method stub
return false;
return isAborted;
}
@Override
public AdditionalResultDataBuilder getAdditionalResultData(AdditionalResultDataBuilder additionalDataBuilder) {
// TODO Auto-generated method stub
return null;
return additionalDataBuilder;
}
public Map<GroupKey, AngleAndSpeedRegression> getRegressions() {
@@ -10,6 +10,7 @@ import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentMap;
@@ -91,8 +92,139 @@ public class PolarDataMiner {
private final ConcurrentMap<BoatClass, Set<PolarsChangedListener>> listeners = new ConcurrentHashMap<>();
/**
* Coordinates callbacks registered through {@link #runWhenPolarLoadingFinishedFor(TrackedRace, Runnable)} and
* {@link #raceFinishedLoading(TrackedRace, Runnable)} such that the callback for a given race is only registered on
* the {@link #preFilteringProcessorForLoadedFixes loading pipeline's} drain <em>after</em> the race's fixes have
* been queued into that pipeline. Otherwise, a caller of {@code runWhenPolarLoadingFinishedFor} could register a
* drain callback while the pipeline is momentarily idle (before ingestion for that race started), and the callback
* would fire immediately, before this race's fixes had even entered the pipeline.
* <p>
*
* Semantics: the map is keyed by races whose fixes have <em>not yet</em> been queued into the loading pipeline.
* Values are lists of callbacks parked pending that ingestion. When ingestion for a race completes queueing all its
* fixes (see {@link #raceFinishedLoading(TrackedRace, Runnable)}), the race is removed from this map and all parked
* callbacks, together with any callback passed to that {@code raceFinishedLoading} call itself, are registered on
* the drain of {@link #preFilteringProcessorForLoadedFixes}. A race is added to this map by
* {@link #runWhenPolarLoadingFinishedFor(TrackedRace, Runnable)} the first time a callback is registered for it
* before its ingestion has started; subsequent {@code
* runWhenPolarLoadingFinishedFor} calls for a race present in this map append to its list. Callers of
* {@link #runWhenPolarLoadingFinishedFor(TrackedRace, Runnable)} that arrive <em>after</em> ingestion has completed
* queueing (race not present in this map at that time) register their callback on the drain directly. See bug6241.
* <p>
*
* Keyed strongly ({@link HashMap}). An entry pins its {@link TrackedRace} key while callbacks
* are parked for it, which is intentional: the parked {@link Runnable}s typically capture the
* same {@code race} strongly anyway (e.g. the wind-estimation install closure in
* {@code RacingEventServiceImpl.scheduleWindEstimationInstallation}), so weak-keying this map
* would be defeated by its own values and give a false sense of safety. Entries are removed
* for live races in {@link #raceFinishedLoading} (once the parked callbacks are handed off to
* the drain) and, for races that are removed before their fixes were ever ingested, by
* {@link #raceRemoved(TrackedRace)}. Callers of
* {@link #runWhenPolarLoadingFinishedFor(TrackedRace, Runnable)} are contractually required to
* arrange for {@link #raceRemoved(TrackedRace)} to be called when the race goes away; see that
* method's contract.
*/
private final Map<TrackedRace, List<Runnable>> callbacksWaitingForFixIngestion = new HashMap<>();
/**
* Records races whose fixes have been fully queued into {@link #preFilteringProcessorForLoadedFixes}. Once a race
* is in this set, a caller of {@link #runWhenPolarLoadingFinishedFor(TrackedRace, Runnable)} for that race
* registers its callback on the drain directly (rather than parking it in {@link #callbacksWaitingForFixIngestion})
* provided {@link #loadingOfAllRacesToRestoreStarted} is already {@code true}. Guarded by the monitor of
* {@link #callbacksWaitingForFixIngestion}.
* <p>
*
* There is no natural point during normal operation at which an entry could be removed, because a
* {@link #runWhenPolarLoadingFinishedFor(TrackedRace, Runnable)} call for a race may legitimately arrive long after
* ingestion (e.g. when a wind-estimation factory swap reschedules the install per race). To avoid pinning every
* {@link TrackedRace} ever loaded -- and transitively all of its tracks -- for the lifetime of this miner (a real
* leak on long-running ARCHIVE servers that load tens of thousands of races), this set is held weakly
* ({@link Collections#newSetFromMap(Map) Collections.newSetFromMap(}{@link WeakHashMap
* new WeakHashMap<>())}). Unlike {@link #callbacksWaitingForFixIngestion} this set has no values, so nothing
* defeats the weak keys: an entry disappears once the race is no longer strongly reachable anywhere else, at which
* point no further {@link #runWhenPolarLoadingFinishedFor(TrackedRace, Runnable)} call for it can occur anyway, so
* losing the "already ingested" bit is harmless. As a belt-and-suspenders measure the entry is also removed
* eagerly in {@link #raceRemoved(TrackedRace)} rather than waiting for garbage collection. For any race that is
* still alive, the entry remains and the gate keeps working exactly as before. See bug6241.
*/
private final Set<TrackedRace> racesWithIngestedFixes = Collections.newSetFromMap(new WeakHashMap<>());
/**
* Set by {@link #markLoadingOfAllRacesToRestoreStarted()} to signal that the caller (typically
* {@code RacingEventServiceImpl.restoreTrackedRaces()}) has finished the enumeration loop that triggers loading for
* every race to be restored during startup. It does <em>not</em> imply that all those races have already progressed
* past {@code LOADING}: some may still be loading, some may take a long time, some may never leave {@code LOADING}
* at all. The flag only signals that no <em>new</em> startup races will show up unannounced.
* <p>
*
* Before this flag is set, the {@link #preFilteringProcessorForLoadedFixes loading pipeline} can transiently be
* idle (counter==0) between two races' ingestion bursts; registering a drain callback in such a window would fire
* it immediately, before other startup races have had a chance to feed fixes into the pipeline. Once this flag is
* set, any pipeline idle window is a genuine drain of everything that has been ingested so far. Combined with the
* {@link #racesWithIngestedFixes} gate, a callback for a specific race only fires once <em>that race's</em> fixes
* have made it in <em>and</em> the pipeline has drained everything ingested up to that point.
* <p>
*
* Callbacks registered via {@link #runWhenPolarLoadingFinishedFor(TrackedRace, Runnable)} whose race has already
* been ingested but which arrive while this flag is still {@code false} are parked in
* {@link #callbacksWaitingForLoadingOfAllRacesToRestoreToStart} until the flag flips.
* <p>
*
* The flag is <em>initialized</em> to {@code false} only when this miner was constructed with
* {@code waitForLoadingOfAllRacesToRestoreToBeStarted == true}. Otherwise (the default) it is initialized to
* {@code true} so that the second gate is effectively bypassed and this miner behaves exactly as before bug6241's
* second-gate addition; this suits ad-hoc clients and tests that instantiate a miner outside a startup-restore flow
* and never call {@link #markLoadingOfAllRacesToRestoreStarted()}. See bug6241.
*/
private volatile boolean loadingOfAllRacesToRestoreStarted;
/**
* Callbacks whose race has already been fully ingested but which arrived before
* {@link #markLoadingOfAllRacesToRestoreStarted()} was called. They are held here until the flag flips, at which
* point each is registered on {@link #preFilteringProcessorForLoadedFixes}'s drain. Guarded by the monitor of
* {@link #callbacksWaitingForFixIngestion}.
* <p>
*
* Lifecycle / leak-safety: this list is bounded and self-clearing under normal operation --
* {@link #markLoadingOfAllRacesToRestoreStarted()} drains it fully and clears it exactly once, moving every parked
* callback onto the pipeline drain. It is not keyed by {@link TrackedRace} and does not itself pin any race
* (individual callbacks may still capture a race, but only transiently, until the drain fires). The only way it
* can retain callbacks indefinitely is if {@link #markLoadingOfAllRacesToRestoreStarted()} is never called on a
* miner constructed in gated mode -- i.e. a broken startup contract on the client side, not a per-race leak. Once
* the flag is set, further callbacks bypass this list and go straight to the drain (see
* {@link #registerOnDrainOrWaitForRestoreStart(Runnable)}), so the list stays empty thereafter. Because it is not
* race-keyed, {@link #raceRemoved(TrackedRace)} does not prune it; a race removed after its callback landed here
* but before the flag flips will simply have its (now moot) callback fire once on the next drain.
*/
private final List<Runnable> callbacksWaitingForLoadingOfAllRacesToRestoreToStart = new ArrayList<>();
/**
* Snapshot of the constructor argument. Used only to distinguish a redundant call to
* {@link #markLoadingOfAllRacesToRestoreStarted()} on a gated miner (worth a WARN, because the client's contract is
* to call exactly once) from a call on a non-gated miner (silently ignored, because the client didn't request
* gating).
*/
private final boolean waitForLoadingOfAllRacesToRestoreToBeStarted;
/**
* Entry point to the data mining pipeline for incremental updates, usually by races in status
* {@link TrackedRaceStatusEnum#TRACKING}. It receives its data through the
* {@link #addFix(GPSFixMoving, Competitor, TrackedRace)} method and targets the same terminal processors
* {@link #cubicRegressionPerCourseProcessor} and {@link #speedRegressionPerAngleClusterProcessor} that the
* {@link #preFilteringProcessorForLoadedFixes} targets.
*/
private ParallelFilteringProcessor<GPSFixMovingWithOriginInfo> preFilteringProcessor;
/**
* Entry point to the data mining pipeline for bulk updates during loading races, usually from races in status
* {@link TrackedRaceStatusEnum#LOADING}. It receives its data through the
* {@link #raceFinishedLoading(TrackedRace, Runnable)} method and targets the same terminal processors
* {@link #cubicRegressionPerCourseProcessor} and {@link #speedRegressionPerAngleClusterProcessor} that the
* {@link #preFilteringProcessor} targets.
*/
private ParallelFilteringProcessor<GPSFixMovingWithOriginInfo> preFilteringProcessorForLoadedFixes;
private final PolarSheetGenerationSettings backendPolarSheetGenerationSettings;
/**
@@ -119,16 +251,49 @@ public class PolarDataMiner {
};
}
/**
* Convenience constructor equivalent to calling
* {@link #PolarDataMiner(PolarSheetGenerationSettings, CubicRegressionPerCourseProcessor, SpeedRegressionPerAngleClusterProcessor, ClusterGroup, boolean)}
* with {@code waitForLoadingOfAllRacesToRestoreToBeStarted == false}. Callbacks registered via
* {@link #runWhenPolarLoadingFinishedFor(TrackedRace, Runnable)} then only wait for the specific race's fixes to
* have been queued into the loading pipeline, not for a subsequent signal from the client. Suited for ad-hoc uses,
* tests, and other flows that don't have a distinguished "startup restore" phase driving many races through this
* miner.
*/
public PolarDataMiner(PolarSheetGenerationSettings backendPolarSettings,
CubicRegressionPerCourseProcessor cubicRegressionPerCourseProcessor,
SpeedRegressionPerAngleClusterProcessor speedRegressionPerAngleClusterProcessor,
ClusterGroup<Bearing> angleClusterGroup) {
this(backendPolarSettings, cubicRegressionPerCourseProcessor,
speedRegressionPerAngleClusterProcessor, angleClusterGroup,
/* waitForLoadingOfAllRacesToRestoreToBeStarted */ false);
}
/**
* @param waitForLoadingOfAllRacesToRestoreToBeStarted
* when {@code true}, this miner enters the "gated" mode described on
* {@link #loadingOfAllRacesToRestoreStarted}: callbacks registered via
* {@link #runWhenPolarLoadingFinishedFor(TrackedRace, Runnable)} will not fire until the client has
* explicitly called {@link #markLoadingOfAllRacesToRestoreStarted()} <em>and</em> the specific race's
* fixes have made it into the loading pipeline. This is the mode used by the OSGi/production wiring,
* where {@code RacingEventServiceImpl} makes that promise. Constructing with {@code true} without ever
* calling {@code markLoadingOfAllRacesToRestoreStarted()} will result in callbacks being held
* indefinitely. When {@code false} (the default via the shorter constructor), the second gate is
* bypassed, matching the behavior before bug6241's addition.
*/
public PolarDataMiner(PolarSheetGenerationSettings backendPolarSettings,
CubicRegressionPerCourseProcessor cubicRegressionPerCourseProcessor,
SpeedRegressionPerAngleClusterProcessor speedRegressionPerAngleClusterProcessor,
ClusterGroup<Bearing> angleClusterGroup,
boolean waitForLoadingOfAllRacesToRestoreToBeStarted) {
cubicRegressionPerCourseProcessor.setListeners(listeners);
speedRegressionPerAngleClusterProcessor.setListeners(listeners);
backendPolarSheetGenerationSettings = backendPolarSettings;
this.cubicRegressionPerCourseProcessor = cubicRegressionPerCourseProcessor;
this.speedRegressionPerAngleClusterProcessor = speedRegressionPerAngleClusterProcessor;
this.angleClusterGroup = angleClusterGroup;
this.waitForLoadingOfAllRacesToRestoreToBeStarted = waitForLoadingOfAllRacesToRestoreToBeStarted;
this.loadingOfAllRacesToRestoreStarted = !waitForLoadingOfAllRacesToRestoreToBeStarted;
try {
setUpWorkflow();
} catch (ClassCastException | NoSuchMethodException | SecurityException e) {
@@ -144,37 +309,48 @@ public class PolarDataMiner {
angleClusterGroup);
}
private void setUpWorkflow() throws ClassCastException, NoSuchMethodException, SecurityException {
Collection<Processor<GroupedDataEntry<GPSFixMovingWithPolarContext>, ?>> regressionPerCourseGrouperResultReceivers = new ArrayList<Processor<GroupedDataEntry<GPSFixMovingWithPolarContext>, ?>>();
/**
* Creates a data mining workflow from processors that start with filtering fixes for best competitors in a race,
* then enriching the fixes with context data required for further processing in the data mining pipeline,
* {@link PolarDataDimensionCollectionFactory grouping} and then passing into
* {@link #cubicRegressionPerCourseProcessor} and {@link #speedRegressionPerAngleClusterProcessor}, respectively.<p>
*
* This can be used to establish two separate pipelines ending at the same two processors, namely
* {@link #preFilteringProcessor} and {@link #preFilteringProcessorForLoadedFixes}, so that clients
* can wait separately for the processing of those fixes introduced by loading races, as compared to
* the continuous processing happening for live races.
*/
private ParallelFilteringProcessor<GPSFixMovingWithOriginInfo> createWorkflow() throws ClassCastException, NoSuchMethodException, SecurityException {
final Collection<Processor<GroupedDataEntry<GPSFixMovingWithPolarContext>, ?>> regressionPerCourseGrouperResultReceivers = new ArrayList<Processor<GroupedDataEntry<GPSFixMovingWithPolarContext>, ?>>();
regressionPerCourseGrouperResultReceivers.add(cubicRegressionPerCourseProcessor);
Collection<ParameterizedFunction<?>> parameterizedDimensionsForCubicRegression = new ArrayList<>();
final Collection<ParameterizedFunction<?>> parameterizedDimensionsForCubicRegression = new ArrayList<>();
for (Function<?> function : PolarDataDimensionCollectionFactory
.getCubicRegressionPerCourseClusterKeyDimensions()) {
parameterizedDimensionsForCubicRegression.add(new SimpleParameterizedFunction<>(function,
ParameterProvider.NULL));
}
Processor<GPSFixMovingWithPolarContext, GroupedDataEntry<GPSFixMovingWithPolarContext>> cubicRegressionPerCourseGroupingProcessor = new ParallelMultiDimensionsValueNestingGroupingProcessor<GPSFixMovingWithPolarContext>(
final Processor<GPSFixMovingWithPolarContext, GroupedDataEntry<GPSFixMovingWithPolarContext>> cubicRegressionPerCourseGroupingProcessor = new ParallelMultiDimensionsValueNestingGroupingProcessor<GPSFixMovingWithPolarContext>(
GPSFixMovingWithPolarContext.class, executor, regressionPerCourseGrouperResultReceivers,
parameterizedDimensionsForCubicRegression);
Collection<Processor<GroupedDataEntry<GPSFixMovingWithPolarContext>, ?>> regressionPerAngleClusterGrouperResultReceivers = new ArrayList<Processor<GroupedDataEntry<GPSFixMovingWithPolarContext>, ?>>();
final Collection<Processor<GroupedDataEntry<GPSFixMovingWithPolarContext>, ?>> regressionPerAngleClusterGrouperResultReceivers = new ArrayList<Processor<GroupedDataEntry<GPSFixMovingWithPolarContext>, ?>>();
regressionPerAngleClusterGrouperResultReceivers.add(speedRegressionPerAngleClusterProcessor);
Collection<ParameterizedFunction<?>> parameterizedDimensionsForRegressionPerAngleCluster = new ArrayList<>();
final Collection<ParameterizedFunction<?>> parameterizedDimensionsForRegressionPerAngleCluster = new ArrayList<>();
for (Function<?> function : PolarDataDimensionCollectionFactory
.getSpeedRegressionPerAngleClusterClusterKeyDimensions()) {
parameterizedDimensionsForRegressionPerAngleCluster.add(new SimpleParameterizedFunction<>(function,
ParameterProvider.NULL));
}
Processor<GPSFixMovingWithPolarContext, GroupedDataEntry<GPSFixMovingWithPolarContext>> regressionPerAngleClusterGroupingProcessor = new ParallelMultiDimensionsValueNestingGroupingProcessor<GPSFixMovingWithPolarContext>(
final Processor<GPSFixMovingWithPolarContext, GroupedDataEntry<GPSFixMovingWithPolarContext>> regressionPerAngleClusterGroupingProcessor = new ParallelMultiDimensionsValueNestingGroupingProcessor<GPSFixMovingWithPolarContext>(
GPSFixMovingWithPolarContext.class, executor, regressionPerAngleClusterGrouperResultReceivers,
parameterizedDimensionsForRegressionPerAngleCluster);
Collection<Processor<GPSFixMovingWithPolarContext, ?>> filteringResultReceivers = new ArrayList<>();
final Collection<Processor<GPSFixMovingWithPolarContext, ?>> filteringResultReceivers = new ArrayList<>();
filteringResultReceivers.add(cubicRegressionPerCourseGroupingProcessor);
filteringResultReceivers.add(regressionPerAngleClusterGroupingProcessor);
Processor<GPSFixMovingWithPolarContext, GPSFixMovingWithPolarContext> filteringProcessor = new ParallelFilteringProcessor<GPSFixMovingWithPolarContext>(
final Processor<GPSFixMovingWithPolarContext, GPSFixMovingWithPolarContext> filteringProcessor = new ParallelFilteringProcessor<GPSFixMovingWithPolarContext>(
GPSFixMovingWithPolarContext.class, executor, filteringResultReceivers, new PolarFixFilterCriteria(
backendPolarSheetGenerationSettings.getPctOfLeadingCompetitorsToInclude()));
Collection<Processor<GPSFixMovingWithPolarContext, ?>> enrichingResultReceivers = Arrays.asList(filteringProcessor);
AbstractEnrichingProcessor<GPSFixMovingWithOriginInfo, GPSFixMovingWithPolarContext> enrichingProcessor = new AbstractEnrichingProcessor<GPSFixMovingWithOriginInfo, GPSFixMovingWithPolarContext>(
final Collection<Processor<GPSFixMovingWithPolarContext, ?>> enrichingResultReceivers = Arrays.asList(filteringProcessor);
final AbstractEnrichingProcessor<GPSFixMovingWithOriginInfo, GPSFixMovingWithPolarContext> enrichingProcessor = new AbstractEnrichingProcessor<GPSFixMovingWithOriginInfo, GPSFixMovingWithPolarContext>(
GPSFixMovingWithOriginInfo.class, GPSFixMovingWithPolarContext.class, executor,
enrichingResultReceivers) {
@Override
@@ -185,8 +361,8 @@ public class PolarDataMiner {
return result;
}
};
Collection<Processor<GPSFixMovingWithOriginInfo, ?>> preFilterResultReceivers = Arrays.asList(enrichingProcessor);
preFilteringProcessor = new ParallelFilteringProcessor<GPSFixMovingWithOriginInfo>(
final Collection<Processor<GPSFixMovingWithOriginInfo, ?>> preFilterResultReceivers = Arrays.asList(enrichingProcessor);
return new ParallelFilteringProcessor<GPSFixMovingWithOriginInfo>(
GPSFixMovingWithOriginInfo.class, executor, preFilterResultReceivers,
new FilterCriterion<GPSFixMovingWithOriginInfo>() {
@Override
@@ -215,6 +391,11 @@ public class PolarDataMiner {
});
}
private void setUpWorkflow() throws ClassCastException, NoSuchMethodException, SecurityException {
preFilteringProcessor = createWorkflow();
preFilteringProcessorForLoadedFixes = createWorkflow();
}
public void addFix(GPSFixMoving fix, Competitor competitor, TrackedRace trackedRace) {
// don't process fixes while LOADING because wind data is loading at the same time, and
// unpredictable results may occur due to this
@@ -448,7 +629,23 @@ public class PolarDataMiner {
return speedRegressionPerAngleClusterProcessor.getSpeedRegressionFunction(boatClass, trueWindAngle);
}
public void raceFinishedTracking(final TrackedRace race) {
/**
* Ingests the fixes from all competitors into the "loading" pipeline. When that pipeline finishes processing
* the fixes ingested by this and other calls on this {@link PolarDataMiner} instance, it invokes all the
* {@code callbackWhenAllLoadedFixesHaveBeenProcessed} callbacks provided to this method.
*
* @param callbackWhenAllLoadedFixesHaveBeenProcessed
* if not {@code null}, this callback will be {@link Runnable#run() invoked} when the processing of all
* fixes ingested into this {@link PolarDataMiner} through this method have been processed. This assumes
* that background processes from the {@link #executor} continue to handle fixes ingested by a single call,
* and more calls for more races may be accepted in the meantime, thus holding back the invocation of
* the callback until the ingestions from <em>all</em> races have been processed. This is made possible
* by entertaining two "frontend" data mining workflow processor chains, both leading to the same terminal
* processors ({@link #cubicRegressionPerCourseProcessor} and {@link #speedRegressionPerAngleClusterProcessor}),
* one filled by this method, the other by {@link #addFix(GPSFixMoving, Competitor, TrackedRace)} used by
* incremental updates.
*/
public void raceFinishedLoading(final TrackedRace race, Runnable callbackWhenAllLoadedFixesHaveBeenProcessed) {
processRacesThatFinishedLoadingExecutor.execute(()->{ // no Subject association necessary here
logger.info("All queued fixes for newly loaded race will process now. "
+ (race.getRace() != null ? race.getRace().getName() : race.getRaceIdentifier().getRaceName()));
@@ -468,15 +665,195 @@ public class PolarDataMiner {
track.unlockAfterRead();
}
for (final GPSFixMoving fix : fixes) {
preFilteringProcessor.processElement(new GPSFixMovingWithOriginInfo(fix, race, competitor));
preFilteringProcessorForLoadedFixes.processElement(new GPSFixMovingWithOriginInfo(fix, race, competitor));
}
}
// All of this race's fixes have now been submitted to preFilteringProcessorForLoadedFixes.
// Any callbacks parked in callbacksWaitingForFixIngestion for this race, along with the
// one passed to this method (if any), can now safely be handed off to the
// "wait for global drain" stage. See bug6241.
final List<Runnable> callbacksToHandOff;
synchronized (callbacksWaitingForFixIngestion) {
racesWithIngestedFixes.add(race);
callbacksToHandOff = callbacksWaitingForFixIngestion.remove(race);
}
if (callbacksToHandOff != null) {
for (final Runnable parked : callbacksToHandOff) {
registerOnDrainOrWaitForRestoreStart(parked);
}
}
if (callbackWhenAllLoadedFixesHaveBeenProcessed != null) {
registerOnDrainOrWaitForRestoreStart(callbackWhenAllLoadedFixesHaveBeenProcessed);
}
logger.info("Finished injecting fixes for race "
+ (race.getRace() != null ? race.getRace().getName() : race.getRaceIdentifier().getRaceName())
+ "; stats: " + stats);
});
}
/**
* Registers {@code callback} to fire once the {@link #preFilteringProcessorForLoadedFixes
* loading pipeline} has fully drained <em>globally</em> (i.e. the fixes of <em>every</em>
* race ingested so far, not only {@code race}) <em>and</em> the caller has announced (via
* {@link #markLoadingOfAllRacesToRestoreStarted()}) that no further startup races will be
* added. The {@code race} parameter does not scope the wait to that race's fixes -- the
* terminal processors are shared and there is no per-race completion tracking; it only gates
* <em>when</em> the callback is allowed onto the shared drain (see the two conditions below).
* Waiting on {@code race} therefore effectively waits for the whole loaded-fix backlog; this
* "wait for everything" behavior is intended (we want the polar model complete before any
* maneuver-based wind estimation is installed) even though it introduces some sequentiality
* on a large cold start. Unlike {@link #raceFinishedLoading(TrackedRace, Runnable)}, this
* method does <em>not</em> ingest the race's fixes into the pipeline; it only observes the
* pipeline. It may be called any number of times for the same race, before or after
* {@code raceFinishedLoading} has been called for that race, and before or after the
* "loading started" signal has flipped.
* <p>
*
* Firing is gated by two conditions, both of which must hold:
* <ol>
* <li>{@code race}'s fixes have been fully queued into the pipeline (i.e.
* {@link #raceFinishedLoading} has ingested them and added the race to
* {@link #racesWithIngestedFixes}). This prevents firing while the pipeline is idle simply
* because this race's ingestion hasn't started yet.</li>
* <li>{@link #markLoadingOfAllRacesToRestoreStarted()} has been called. This prevents
* firing during a transient global idle window that occurs between two startup races'
* ingestion bursts.</li>
* </ol>
*
* Depending on which of these already hold at the time of the call, {@code callback} is
* either registered on the drain immediately (both hold), parked in
* {@link #callbacksWaitingForLoadingOfAllRacesToRestoreToStart} (fix ingestion done, signal
* pending), or parked in {@link #callbacksWaitingForFixIngestion} (fix ingestion pending;
* once ingestion completes, the callback moves to the drain or to the signal-pending list
* as appropriate). See bug6241.
* <p>
*
* LEAK CONTRACT -- read before calling. A {@code callback} parked here (in
* {@link #callbacksWaitingForFixIngestion}) is held strongly, keyed by {@code race}, until
* either its race's fixes are ingested ({@link #raceFinishedLoading}) or the race is
* explicitly forgotten. Parked callbacks also typically capture {@code race} strongly
* themselves. Consequently, if a race is registered here but its fixes are never ingested
* (e.g. it is removed while still in {@link TrackedRaceStatusEnum#LOADING}), the entry --
* and the whole {@link TrackedRace} with all its tracks -- would be pinned for the lifetime
* of this miner. To prevent that, the caller MUST call {@link #raceRemoved(TrackedRace)} when
* the race is removed from its regatta / the racing event service. In this codebase that is
* wired through {@code RacingEventServiceImpl.RaceAdditionListener.raceRemoved(TrackedRace)}.
* Do not rely on garbage collection to clean up {@link #callbacksWaitingForFixIngestion}: its
* keys are strong precisely because weak keys would be defeated by the callbacks' own
* strong references back to the race.
*
* @param callback
* must not be {@code null}
*/
public void runWhenPolarLoadingFinishedFor(final TrackedRace race, final Runnable callback) {
if (callback == null) {
throw new NullPointerException("callback must not be null");
}
final boolean fixIngestionAlreadyDone;
synchronized (callbacksWaitingForFixIngestion) {
if (racesWithIngestedFixes.contains(race)) {
fixIngestionAlreadyDone = true;
} else {
fixIngestionAlreadyDone = false;
callbacksWaitingForFixIngestion
.computeIfAbsent(race, r -> new ArrayList<>())
.add(callback);
}
}
if (fixIngestionAlreadyDone) {
registerOnDrainOrWaitForRestoreStart(callback);
}
}
/**
* Forgets all state this miner holds for {@code race}, so that a removed race and its tracks
* can be garbage-collected rather than pinned for the lifetime of the miner. Specifically it
* drops any callbacks still parked for {@code race} in {@link #callbacksWaitingForFixIngestion}
* (the install those callbacks would drive is for the now-removed race instance, so it needn't
* fire; note this does not abort any of {@code race}'s fixes that are already being drained
* through the loading pipeline) and removes the race from {@link #racesWithIngestedFixes}.
* <p>
*
* This is the removal side of the {@link #runWhenPolarLoadingFinishedFor(TrackedRace, Runnable)}
* leak contract: because parked callbacks are held strongly and typically capture the race
* strongly themselves, {@link #callbacksWaitingForFixIngestion} cannot rely on weak keys and
* must be pruned explicitly when a race goes away. Callers (in this codebase,
* {@code RacingEventServiceImpl.RaceAdditionListener.raceRemoved(TrackedRace)}) must invoke
* this when a race is removed from its regatta / the racing event service.
* {@link #racesWithIngestedFixes} is already held weakly and would clear on its own, but is
* pruned here too as a belt-and-suspenders measure so the memory is reclaimed promptly rather
* than at the next garbage collection.
* <p>
*
* Idempotent and safe to call for a race this miner never saw: a race with no state simply
* results in no-ops. Guarded by the same {@link #callbacksWaitingForFixIngestion} monitor as
* the registration methods.
*
* @param race
* the race to forget; must not be {@code null}
*/
public void raceRemoved(final TrackedRace race) {
if (race == null) {
throw new NullPointerException("race must not be null");
}
synchronized (callbacksWaitingForFixIngestion) {
callbacksWaitingForFixIngestion.remove(race);
racesWithIngestedFixes.remove(race);
}
}
/**
* Second-stage gate for callbacks whose race's fixes are already ingested (or whose fix-ingestion parking has just
* been drained by {@link #raceFinishedLoading(TrackedRace, Runnable)}). If
* {@link #loadingOfAllRacesToRestoreStarted} is already set the callback goes straight to the drain; otherwise it
* is parked in {@link #callbacksWaitingForLoadingOfAllRacesToRestoreToStart} where
* {@link #markLoadingOfAllRacesToRestoreStarted()} will pick it up. See bug6241.
*/
private void registerOnDrainOrWaitForRestoreStart(final Runnable callback) {
final boolean signalAlreadyGiven;
synchronized (callbacksWaitingForFixIngestion) {
if (loadingOfAllRacesToRestoreStarted) {
signalAlreadyGiven = true;
} else {
signalAlreadyGiven = false;
callbacksWaitingForLoadingOfAllRacesToRestoreToStart.add(callback);
}
}
if (signalAlreadyGiven) {
preFilteringProcessorForLoadedFixes.runWhenFinishedProcessing(callback);
}
}
/**
* Announces that the caller (typically {@code RacingEventServiceImpl.restoreTrackedRaces()}) has finished the
* enumeration loop that triggers loading for every race to be restored during startup. From now on, any transient
* idle window on {@link #preFilteringProcessorForLoadedFixes} is a genuine drain of everything that has been
* ingested up to that point; there won't be surprise ingestion bursts from previously unknown startup races.
* Callbacks that have been parked in {@link #callbacksWaitingForLoadingOfAllRacesToRestoreToStart} (because they
* were queued for a race whose fixes were already ingested, but arrived before this signal) are moved onto the
* drain now. Idempotent: subsequent calls are logged and ignored. See bug6241.
*/
public void markLoadingOfAllRacesToRestoreStarted() {
final Iterable<Runnable> toRegisterOnDrain;
synchronized (callbacksWaitingForFixIngestion) {
if (loadingOfAllRacesToRestoreStarted) {
if (waitForLoadingOfAllRacesToRestoreToBeStarted) {
logger.warning("markLoadingOfAllRacesToRestoreStarted() called more than once; ignoring.");
}
// otherwise: this miner is running without the second gate; the call is a
// harmless no-op (the client didn't request gating, so there's nothing parked).
toRegisterOnDrain = Collections.emptyList();
} else {
loadingOfAllRacesToRestoreStarted = true;
toRegisterOnDrain = new ArrayList<>(callbacksWaitingForLoadingOfAllRacesToRestoreToStart);
callbacksWaitingForLoadingOfAllRacesToRestoreToStart.clear();
}
}
for (final Runnable callback : toRegisterOnDrain) {
preFilteringProcessorForLoadedFixes.runWhenFinishedProcessing(callback);
}
}
public void registerListener(BoatClass boatClass, PolarsChangedListener listener) {
Set<PolarsChangedListener> listenersForBoatClass = listeners.get(boatClass);
if (listenersForBoatClass == null) {
@@ -61,6 +61,10 @@ public class SpeedRegressionPerAngleClusterProcessor implements
*/
private transient ConcurrentMap<BoatClass, Set<PolarsChangedListener>> listeners;
private boolean isFinished;
private boolean isAborted;
public SpeedRegressionPerAngleClusterProcessor(ClusterGroup<Bearing> angleClusterGroup) {
this.angleClusterGroup = angleClusterGroup;
}
@@ -110,7 +114,6 @@ public class SpeedRegressionPerAngleClusterProcessor implements
@Override
public boolean canProcessElements() {
// TODO Auto-generated method stub
return true;
}
@@ -269,41 +272,40 @@ public class SpeedRegressionPerAngleClusterProcessor implements
@Override
public Class<GroupedDataEntry<GPSFixMovingWithPolarContext>> getInputType() {
// TODO Auto-generated method stub
return null;
@SuppressWarnings("unchecked")
final Class<GroupedDataEntry<GPSFixMovingWithPolarContext>> result = (Class<GroupedDataEntry<GPSFixMovingWithPolarContext>>) (Class<?>) GroupedDataEntry.class;
return result;
}
@Override
public Class<Void> getResultType() {
// No result type here, since this is a special case of a processor. It's the end of the pipe so to say.
return null;
return Void.class;
}
@Override
public void finish() throws InterruptedException {
// Nothing to do here
isFinished = true;
}
@Override
public boolean isFinished() {
return false;
return isFinished;
}
@Override
public void abort() {
// TODO Auto-generated method stub
isAborted = true;
}
@Override
public boolean isAborted() {
// TODO Auto-generated method stub
return false;
return isAborted;
}
@Override
public AdditionalResultDataBuilder getAdditionalResultData(AdditionalResultDataBuilder additionalDataBuilder) {
// TODO Auto-generated method stub
return null;
return additionalDataBuilder;
}
public ClusterGroup<Bearing> getAngleCluster() {
@@ -27,7 +27,7 @@
<!--
In this section necessary system properties for the different web drivers can be provided if needed. For more
informations about available properties see the following sides:
information about available properties see the following sites:
- http://code.google.com/p/selenium/wiki/ChromeDriver
- http://code.google.com/p/selenium/wiki/FirefoxDriver
@@ -37,368 +37,363 @@
<stringAttribute key="profilingTraceType-PERFORMANCE_HOTSPOT_TRACE" value="KEY_IGNORE_SLEEPING_THREADS%CTX_KEY%false%CTX_ENTRY%KEY_APPLICATION_FILTER%CTX_KEY%*%CTX_ENTRY%KEY_SESSION_FILTER%CTX_KEY%*%CTX_ENTRY%KEY_ENABLEMENT%CTX_KEY%true%CTX_ENTRY%KEY_USER_FILTER%CTX_KEY%*%CTX_ENTRY%KEY_REQUEST_FILTER%CTX_KEY%*%CTX_ENTRY%KEY_TENANT_FILTER%CTX_KEY%*%CTX_ENTRY%"/>
<stringAttribute key="profilingTraceType-SYNCHRONIZATION_TRACE" value="KEY_APPLICATION_FILTER%CTX_KEY%*%CTX_ENTRY%KEY_SESSION_FILTER%CTX_KEY%*%CTX_ENTRY%KEY_ENABLEMENT%CTX_KEY%false%CTX_ENTRY%KEY_USER_FILTER%CTX_KEY%*%CTX_ENTRY%KEY_REQUEST_FILTER%CTX_KEY%*%CTX_ENTRY%KEY_TENANT_FILTER%CTX_KEY%*%CTX_ENTRY%"/>
<setAttribute key="selected_target_bundles">
<setEntry value="routeconverter@default:default"/>
<setEntry value="org.apache.commons.codec@default:default"/>
<setEntry value="org.apache.poi@default:default"/>
<setEntry value="org.apache.poi.ooxml@default:default"/>
<setEntry value="org.apache.poi.ooxml.schemas@default:default"/>
<setEntry value="org.apache.commons.collections4@default:default"/>
<setEntry value="org.apache.commons.compress@default:default"/>
<setEntry value="org.apache.poi.source@default:default"/>
<setEntry value="org.apache.poi.ooxml.source@default:default"/>
<setEntry value="org.apache.commons.collections4.source@default:default"/>
<setEntry value="org.apache.commons.compress.source@default:default"/>
<setEntry value="org.dom4j@default:default"/>
<setEntry value="org.apache.xmlbeans@default:default"/>
<setEntry value="org.apache.commons.math@default:default"/>
<setEntry value="org.apache.httpcomponents.httpclient@default:default"/>
<setEntry value="org.apache.httpcomponents.httpcore@default:default"/>
<setEntry value="org.hyperic.sigar@default:default"/>
<setEntry value="com.sun.jersey.contribs.jersey-multipart@default:default"/>
<setEntry value="javax.validation@default:default"/>
<setEntry value="org.apache.commons.fileupload@default:default"/>
<setEntry value="org.jvnet.mimepull@default:default"/>
<setEntry value="org.apache.commons.math3@default:default"/>
<setEntry value="org.mongodb.driver-core@default:default"/>
<setEntry value="org.mongodb.driver-core.source@default:default"/>
<setEntry value="org.mongodb.bson@default:default"/>
<setEntry value="org.mongodb.bson.source@default:default"/>
<setEntry value="org.mongodb.driver-sync@default:default"/>
<setEntry value="org.mongodb.driver-sync@default:default"/>
<setEntry value="org.eclipse.jetty.osgi.boot@3:true"/>
<setEntry value="org.eclipse.jetty.osgi.boot.warurl@default:default"/>
<setEntry value="org.hyperic.sigar@default:default"/>
<setEntry value="slf4j.jdk14@default:default"/>
<setEntry value="lz4-java@default:default"/>
<setEntry value="org.apache.felix.gogo.command@default:default"/>
<setEntry value="org.apache.felix.gogo.runtime@default:default"/>
<setEntry value="org.apache.felix.gogo.shell@default:default"/>
<setEntry value="org.eclipse.jetty.deploy@default:default"/>
<setEntry value="org.eclipse.jetty.deploy.source@default:default"/>
<setEntry value="org.eclipse.jetty.http@3:true"/>
<setEntry value="org.eclipse.jetty.http.source@default:default"/>
<setEntry value="org.eclipse.jetty.io@default:default"/>
<setEntry value="org.eclipse.jetty.io.source@default:default"/>
<setEntry value="org.eclipse.jetty.jmx@default:default"/>
<setEntry value="org.eclipse.jetty.jmx.source@default:default"/>
<setEntry value="org.eclipse.jetty.security@default:default"/>
<setEntry value="org.eclipse.jetty.security.source@default:default"/>
<setEntry value="org.eclipse.jetty.server@default:default"/>
<setEntry value="org.eclipse.jetty.server.source@default:default"/>
<setEntry value="org.eclipse.jetty.servlet@default:default"/>
<setEntry value="org.eclipse.jetty.servlet.source@default:default"/>
<setEntry value="org.eclipse.jetty.util@default:default"/>
<setEntry value="org.eclipse.jetty.util.source@default:default"/>
<setEntry value="org.eclipse.jetty.util.ajax@default:default"/>
<setEntry value="org.eclipse.jetty.util.ajax.source@default:default"/>
<setEntry value="org.eclipse.jetty.webapp@default:default"/>
<setEntry value="org.eclipse.jetty.webapp.source@default:default"/>
<setEntry value="org.eclipse.jetty.websocket.api@default:default"/>
<setEntry value="org.eclipse.jetty.websocket.api.source@default:default"/>
<setEntry value="org.eclipse.jetty.websocket.client@default:default"/>
<setEntry value="org.eclipse.jetty.websocket.client.source@default:default"/>
<setEntry value="org.eclipse.jetty.websocket.common@default:default"/>
<setEntry value="org.eclipse.jetty.websocket.common.source@default:default"/>
<setEntry value="org.eclipse.jetty.websocket.server@default:default"/>
<setEntry value="org.eclipse.jetty.websocket.server.source@default:default"/>
<setEntry value="org.eclipse.jetty.websocket.servlet@default:default"/>
<setEntry value="org.eclipse.jetty.websocket.servlet.source@default:default"/>
<setEntry value="org.eclipse.jetty.xml@default:default"/>
<setEntry value="org.eclipse.jetty.xml.source@default:default"/>
<setEntry value="slf4j.api@default:default"/>
<setEntry value="org.apache.servicemix.bundles.zxing@default:default"/>
<setEntry value="org.apache.commons.io@default:default"/>
<setEntry value="jcl.over.slf4j@default:default"/>
<setEntry value="jul.to.slf4j@default:default"/>
<setEntry value="com.sun.mail.javax.mail@default:default"/>
<setEntry value="com.rabbitmq.client@default:default"/>
<setEntry value="com.rabbitmq.client.source@default:default"/>
<setEntry value="org.apache.commons.lang@default:default"/>
<setEntry value="org.apache.commons.logging@default:default"/>
<setEntry value="jackson-jaxrs@default:default"/>
<setEntry value="com.sun.jersey@default:default"/>
<setEntry value="javax.ws.rs@default:default"/>
<setEntry value="org.apache.commons.beanutils@default:default"/>
<setEntry value="org.apache.commons.beanutils.source@default:default"/>
<setEntry value="org.apache.servicemix.bundles.ehcache@default:default"/>
<setEntry value="org.apache.servicemix.bundles.scribe@default:default"/>
<setEntry value="org.owasp.encoder@default:default"/>
<setEntry value="org.owasp.encoder.source@default:default"/>
<setEntry value="org.apache.shiro.core@default:default"/>
<setEntry value="org.apache.shiro.core.source@default:default"/>
<setEntry value="org.apache.shiro.ehcache@default:default"/>
<setEntry value="org.apache.shiro.ehcache.source@default:default"/>
<setEntry value="org.apache.shiro.web@default:default"/>
<setEntry value="org.apache.shiro.web.source@default:default"/>
<setEntry value="jackson-core-asl@default:default"/>
<setEntry value="jackson-mapper-asl@default:default"/>
<setEntry value="com.fasterxml.jackson.core.jackson-core@default:default"/>
<setEntry value="org.apache.commons.collections@default:default"/>
<setEntry value="org.eclipse.jetty.client@default:default"/>
<setEntry value="javax.xml@default:default"/>
<setEntry value="com.sun.activation.javax.activation@default:default"/>
<setEntry value="org.eclipse.equinox.common@2:true"/>
<setEntry value="org.eclipse.equinox.console@default:default"/>
<setEntry value="org.eclipse.equinox.http.service.api@default:default"/>
<setEntry value="org.eclipse.equinox.http.service.api.source@default:default"/>
<setEntry value="org.eclipse.equinox.launcher@default:default"/>
<setEntry value="org.eclipse.equinox.simpleconfigurator@2:true"/>
<setEntry value="org.eclipse.osgi@-1:true"/>
<setEntry value="org.eclipse.osgi.source@default:default"/>
<setEntry value="org.osgi.util.promise@default:default"/>
<setEntry value="org.osgi.util.promise.source@default:default"/>
<setEntry value="org.osgi.util.function@default:default"/>
<setEntry value="org.osgi.util.function.source@default:default"/>
<setEntry value="org.osgi.util.measurement@default:default"/>
<setEntry value="org.osgi.util.measurement.source@default:default"/>
<setEntry value="org.osgi.util.position@default:default"/>
<setEntry value="org.osgi.util.position.source@default:default"/>
<setEntry value="org.osgi.util.xml@default:default"/>
<setEntry value="org.osgi.util.xml.source@default:default"/>
<setEntry value="org.eclipse.osgi.services@default:default"/>
<setEntry value="org.eclipse.osgi.services.source@default:default"/>
<setEntry value="org.eclipse.equinox.cm@default:default"/>
<setEntry value="com.sun.istack.commons-runtime@default:default"/>
<setEntry value="com.sun.xml.bind.jaxb-impl@default:default"/>
<setEntry value="javax.xml.stream@default:default"/>
<setEntry value="javax.xml.ws@default:default"/>
<setEntry value="javax.xml.soap@default:default"/>
<setEntry value="org.eclipse.osgi.util@default:default"/>
<setEntry value="org.eclipse.osgi.util.source@default:default"/>
<setEntry value="com.chargebee.chargebee-java@default:default"/>
<setEntry value="com.github.mwiede.jsch@default:default"/>
<setEntry value="com.github.mwiede.jsch.source@default:default"/>
<setEntry value="bcprov-ext@default:default"/>
<setEntry value="com.amazon.aws.aws-java-api@default:default"/>
<setEntry value="com.amazon.aws.aws-java-api.source@default:default"/>
<setEntry value="org.objectweb.asm@default:default"/>
<setEntry value="org.objectweb.asm.commons@default:default"/>
<setEntry value="org.objectweb.asm.tree.analysis@default:default"/>
<setEntry value="org.objectweb.asm.tree.analysis.source@default:default"/>
<setEntry value="org.objectweb.asm.tree@default:default"/>
<setEntry value="org.objectweb.asm.tree.source@default:default"/>
<setEntry value="org.objectweb.asm.tree@default:default"/>
<setEntry value="org.objectweb.asm.util@default:default"/>
<setEntry value="org.objectweb.asm.source@default:default"/>
<setEntry value="org.objectweb.asm.commons.source@default:default"/>
<setEntry value="org.objectweb.asm.tree.source@default:default"/>
<setEntry value="org.objectweb.asm.util.source@default:default"/>
<setEntry value="org.eclipse.jetty.apache-jsp@4:true"/>
<setEntry value="org.eclipse.jetty.apache-jsp.source@default:default"/>
<setEntry value="org.eclipse.jetty.osgi.boot.jsp@default:default"/>
<setEntry value="org.eclipse.jetty.osgi.boot.jsp.source@default:default"/>
<setEntry value="org.apache.geronimo.specs.geronimo-jta_1.1_spec@default:default"/>
<setEntry value="org.apache.aries.spifly.dynamic.bundle@3:true"/>
<setEntry value="org.apache.aries.spifly.dynamic.bundle.source@default:default"/>
<setEntry value="org.mortbay.jasper.apache-el@default:default"/>
<setEntry value="org.mortbay.jasper.apache-jsp@default:default"/>
<setEntry value="org.mortbay.jasper.apache-el.source@default:default"/>
<setEntry value="org.mortbay.jasper.apache-jsp.source@default:default"/>
<setEntry value="org.eclipse.jdt.core.compiler.batch@default:default"/>
<setEntry value="org.apache.taglibs.standard-impl@default:default"/>
<setEntry value="org.apache.taglibs.taglibs-standard-spec@default:default"/>
<setEntry value="javax.annotation@default:default"/>
<setEntry value="org.eclipse.jetty.osgi-servlet-api@default:default"/>
<setEntry value="org.apache.xalan@default:default"/>
<setEntry value="org.apache.xml.serializer@default:default"/>
<setEntry value="org.eclipse.equinox.event@default:default"/>
<setEntry value="org.eclipse.jetty.annotations@4:true"/>
<setEntry value="org.eclipse.jetty.jndi@default:default"/>
<setEntry value="org.eclipse.jetty.plus@default:default"/>
<setEntry value="com.sap.db.jdbc@default:default"/>
<setEntry value="com.sap.db.jdbc.source@default:default"/>
<setEntry value="org.osgi.service.component@default:default"/>
<setEntry value="org.osgi.service.cm@default:default"/>
<setEntry value="org.osgi.service.event@default:default"/>
<setEntry value="org.osgi.service.metatype@default:default"/>
<setEntry value="org.osgi.service.metatype.source@default:default"/>
<setEntry value="org.osgi.service.packageadmin@default:default"/>
<setEntry value="org.osgi.service.provisioning@default:default"/>
<setEntry value="org.osgi.service.provisioning.source@default:default"/>
<setEntry value="org.osgi.service.upnp@default:default"/>
<setEntry value="org.osgi.service.upnp.source@default:default"/>
<setEntry value="org.osgi.service.useradmin@default:default"/>
<setEntry value="org.osgi.service.useradmin.source@default:default"/>
<setEntry value="org.osgi.service.wireadmin@default:default"/>
<setEntry value="org.osgi.service.wireadmin.source@default:default"/>
<setEntry value="org.osgi.service.device@default:default"/>
<setEntry value="org.osgi.service.device.source@default:default"/>
<setEntry value="org.osgi.service.http.whiteboard@default:default"/>
<setEntry value="org.osgi.service.http.whiteboard.source@default:default"/>
<setEntry value="org.osgi.service.url@default:default"/>
<setEntry value="redisson@default:default"/>
<setEntry value="com.esotericsoftware.kryo@default:default"/>
<setEntry value="com.esotericsoftware.reflectasm@default:default"/>
<setEntry value="com.esotericsoftware.minlog@default:default"/>
<setEntry value="com.fasterxml.jackson.core.jackson-annotations@default:default"/>
<setEntry value="com.fasterxml.jackson.core.jackson-databind@default:default"/>
<setEntry value="com.fasterxml.jackson.dataformat.jackson-dataformat-yaml@default:default"/>
<setEntry value="org.yaml.snakeyaml@default:default"/>
<setEntry value="io.netty.transport@default:default"/>
<setEntry value="io.netty.buffer@default:default"/>
<setEntry value="io.netty.common@default:default"/>
<setEntry value="io.netty.resolver@default:default"/>
<setEntry value="io.netty.transport-classes-epoll@default:default"/>
<setEntry value="io.netty.transport-native-unix-common@default:default"/>
<setEntry value="io.netty.transport-classes-kqueue@default:default"/>
<setEntry value="io.netty.codec@default:default"/>
<setEntry value="io.netty.handler@default:default"/>
<setEntry value="io.netty.resolver-dns@default:default"/>
<setEntry value="io.netty.codec-dns@default:default"/>
<setEntry value="io.netty.incubator.netty-incubator-transport-classes-io_uring@default:default"/>
<setEntry value="io.reactivex.rxjava3.rxjava@default:default"/>
<setEntry value="reactive-streams@default:default"/>
<setEntry value="javax.cache.api@default:default"/>
<setEntry value="com.github.haifengl.smile-core@default:default"/>
<setEntry value="com.github.haifengl.smile-core.source@default:default"/>
<setEntry value="com.github.haifengl.smile-data@default:default"/>
<setEntry value="com.github.haifengl.smile-data.source@default:default"/>
<setEntry value="com.github.haifengl.smile-graph@default:default"/>
<setEntry value="com.github.haifengl.smile-graph.source@default:default"/>
<setEntry value="com.github.haifengl.smile-math@default:default"/>
<setEntry value="com.github.haifengl.smile-math.source@default:default"/>
<setEntry value="com.google.gson@default:default"/>
<setEntry value="com.google.gson.source@default:default"/>
<setEntry value="jodd-bean@default:default"/>
<setEntry value="net.bytebuddy.byte-buddy@default:default"/>
<setEntry value="net.bytebuddy.byte-buddy.source@default:default"/>
<setEntry value="net.bytebuddy.byte-buddy-agent@default:default"/>
<setEntry value="net.bytebuddy.byte-buddy-agent.source@default:default"/>
<setEntry value="org.jboss.marshalling.jboss-marshalling-osgi@default:default"/>
<setEntry value="io.projectreactor.reactor-core@default:default"/>
<setEntry value="com.diffplug.osgi.extension.sun.misc@default:default"/>
<setEntry value="com.google.protobuf@default:default"/>
<setEntry value="com.google.protobuf.source@default:default"/>
<setEntry value="com.sun.jna@default:default"/>
<setEntry value="com.sun.jna.source@default:default"/>
<setEntry value="com.sun.jna.platform@default:default"/>
<setEntry value="com.sun.jna.platform.source@default:default"/>
<setEntry value="com.nulab-inc.zxcvbn@default:default"/>
<setEntry value="com.nulab-inc.zxcvbn.source@default:default"/>
</setAttribute>
<setEntry value="bcprov-ext@default:default"/>
<setEntry value="com.amazon.aws.aws-java-api.source"/>
<setEntry value="com.amazon.aws.aws-java-api@default:default"/>
<setEntry value="com.chargebee.chargebee-java@default:default"/>
<setEntry value="com.diffplug.osgi.extension.sun.misc@default:false"/>
<setEntry value="com.esotericsoftware.kryo@default:default"/>
<setEntry value="com.esotericsoftware.minlog@default:default"/>
<setEntry value="com.esotericsoftware.reflectasm@default:default"/>
<setEntry value="com.fasterxml.jackson.core.jackson-annotations@default:default"/>
<setEntry value="com.fasterxml.jackson.core.jackson-core@default:default"/>
<setEntry value="com.fasterxml.jackson.core.jackson-databind@default:default"/>
<setEntry value="com.fasterxml.jackson.dataformat.jackson-dataformat-yaml@default:default"/>
<setEntry value="com.github.haifengl.smile-core.source"/>
<setEntry value="com.github.haifengl.smile-core@default:default"/>
<setEntry value="com.github.haifengl.smile-data.source"/>
<setEntry value="com.github.haifengl.smile-data@default:default"/>
<setEntry value="com.github.haifengl.smile-graph.source"/>
<setEntry value="com.github.haifengl.smile-graph@default:default"/>
<setEntry value="com.github.haifengl.smile-math.source"/>
<setEntry value="com.github.haifengl.smile-math@default:default"/>
<setEntry value="com.github.mwiede.jsch.source"/>
<setEntry value="com.github.mwiede.jsch@default:default"/>
<setEntry value="com.google.gson.source"/>
<setEntry value="com.google.gson@default:default"/>
<setEntry value="com.google.protobuf.source"/>
<setEntry value="com.google.protobuf@default:default"/>
<setEntry value="com.nulab-inc.zxcvbn.source"/>
<setEntry value="com.nulab-inc.zxcvbn@default:default"/>
<setEntry value="com.rabbitmq.client.source"/>
<setEntry value="com.rabbitmq.client@default:default"/>
<setEntry value="com.sap.db.jdbc.source"/>
<setEntry value="com.sap.db.jdbc@default:default"/>
<setEntry value="com.sun.activation.javax.activation@default:default"/>
<setEntry value="com.sun.istack.commons-runtime@default:default"/>
<setEntry value="com.sun.jersey.contribs.jersey-multipart@default:default"/>
<setEntry value="com.sun.jersey@default:default"/>
<setEntry value="com.sun.jna.platform.source"/>
<setEntry value="com.sun.jna.platform@default:default"/>
<setEntry value="com.sun.jna.source"/>
<setEntry value="com.sun.jna@default:default"/>
<setEntry value="com.sun.mail.javax.mail@default:default"/>
<setEntry value="com.sun.xml.bind.jaxb-impl@default:default"/>
<setEntry value="io.netty.buffer@default:default"/>
<setEntry value="io.netty.codec-dns@default:default"/>
<setEntry value="io.netty.codec@default:default"/>
<setEntry value="io.netty.common@default:default"/>
<setEntry value="io.netty.handler@default:default"/>
<setEntry value="io.netty.incubator.netty-incubator-transport-classes-io_uring@default:default"/>
<setEntry value="io.netty.resolver-dns@default:default"/>
<setEntry value="io.netty.resolver@default:default"/>
<setEntry value="io.netty.transport-classes-epoll@default:default"/>
<setEntry value="io.netty.transport-classes-kqueue@default:default"/>
<setEntry value="io.netty.transport-native-unix-common@default:default"/>
<setEntry value="io.netty.transport@default:default"/>
<setEntry value="io.projectreactor.reactor-core@default:default"/>
<setEntry value="io.reactivex.rxjava3.rxjava@default:default"/>
<setEntry value="jackson-core-asl@default:default"/>
<setEntry value="jackson-jaxrs@default:default"/>
<setEntry value="jackson-mapper-asl@default:default"/>
<setEntry value="javax.annotation@default:default"/>
<setEntry value="javax.cache.api@default:default"/>
<setEntry value="javax.validation@default:default"/>
<setEntry value="javax.ws.rs@default:default"/>
<setEntry value="javax.xml.soap@default:default"/>
<setEntry value="javax.xml.stream@default:default"/>
<setEntry value="javax.xml.ws@default:default"/>
<setEntry value="javax.xml@default:default"/>
<setEntry value="jcl.over.slf4j@default:default"/>
<setEntry value="jodd-bean@default:default"/>
<setEntry value="jul.to.slf4j@default:default"/>
<setEntry value="lz4-java@default:default"/>
<setEntry value="net.bytebuddy.byte-buddy-agent.source"/>
<setEntry value="net.bytebuddy.byte-buddy-agent@default:default"/>
<setEntry value="net.bytebuddy.byte-buddy.source"/>
<setEntry value="net.bytebuddy.byte-buddy@default:default"/>
<setEntry value="org.apache.aries.spifly.dynamic.bundle.source"/>
<setEntry value="org.apache.aries.spifly.dynamic.bundle@3:true"/>
<setEntry value="org.apache.commons.beanutils.source@default:default"/>
<setEntry value="org.apache.commons.beanutils@default:default"/>
<setEntry value="org.apache.commons.codec@default:default"/>
<setEntry value="org.apache.commons.collections4.source"/>
<setEntry value="org.apache.commons.collections4@default:default"/>
<setEntry value="org.apache.commons.collections@default:default"/>
<setEntry value="org.apache.commons.compress.source"/>
<setEntry value="org.apache.commons.compress@default:default"/>
<setEntry value="org.apache.commons.fileupload@default:default"/>
<setEntry value="org.apache.commons.io@default:default"/>
<setEntry value="org.apache.commons.lang@default:default"/>
<setEntry value="org.apache.commons.logging@default:default"/>
<setEntry value="org.apache.commons.math3@default:default"/>
<setEntry value="org.apache.commons.math@default:default"/>
<setEntry value="org.apache.felix.gogo.command@default:default"/>
<setEntry value="org.apache.felix.gogo.runtime@default:default"/>
<setEntry value="org.apache.felix.gogo.shell@default:default"/>
<setEntry value="org.apache.geronimo.specs.geronimo-jta_1.1_spec@default:default"/>
<setEntry value="org.apache.httpcomponents.httpclient@default:default"/>
<setEntry value="org.apache.httpcomponents.httpcore@default:default"/>
<setEntry value="org.apache.poi.ooxml.schemas@default:default"/>
<setEntry value="org.apache.poi.ooxml.source"/>
<setEntry value="org.apache.poi.ooxml@default:default"/>
<setEntry value="org.apache.poi.source"/>
<setEntry value="org.apache.poi@default:default"/>
<setEntry value="org.apache.servicemix.bundles.ehcache@default:default"/>
<setEntry value="org.apache.servicemix.bundles.scribe@default:default"/>
<setEntry value="org.apache.servicemix.bundles.zxing@default:default"/>
<setEntry value="org.apache.shiro.core.source"/>
<setEntry value="org.apache.shiro.core@default:default"/>
<setEntry value="org.apache.shiro.ehcache.source"/>
<setEntry value="org.apache.shiro.ehcache@default:default"/>
<setEntry value="org.apache.shiro.web.source"/>
<setEntry value="org.apache.shiro.web@default:default"/>
<setEntry value="org.apache.taglibs.standard-impl@default:default"/>
<setEntry value="org.apache.taglibs.taglibs-standard-spec@default:default"/>
<setEntry value="org.apache.xalan@default:default"/>
<setEntry value="org.apache.xml.serializer@default:default"/>
<setEntry value="org.apache.xmlbeans@default:default"/>
<setEntry value="org.dom4j@default:default"/>
<setEntry value="org.eclipse.equinox.cm@default:default"/>
<setEntry value="org.eclipse.equinox.common@2:true"/>
<setEntry value="org.eclipse.equinox.console@default:default"/>
<setEntry value="org.eclipse.equinox.event@default:default"/>
<setEntry value="org.eclipse.equinox.http.service.api.source"/>
<setEntry value="org.eclipse.equinox.http.service.api@default:default"/>
<setEntry value="org.eclipse.equinox.launcher@default:default"/>
<setEntry value="org.eclipse.equinox.simpleconfigurator@2:true"/>
<setEntry value="org.eclipse.jdt.core.compiler.batch@default:default"/>
<setEntry value="org.eclipse.jetty.annotations@4:true"/>
<setEntry value="org.eclipse.jetty.apache-jsp.source"/>
<setEntry value="org.eclipse.jetty.apache-jsp@4:true"/>
<setEntry value="org.eclipse.jetty.client@default:default"/>
<setEntry value="org.eclipse.jetty.deploy.source"/>
<setEntry value="org.eclipse.jetty.deploy@default:default"/>
<setEntry value="org.eclipse.jetty.http.source"/>
<setEntry value="org.eclipse.jetty.http@3:true"/>
<setEntry value="org.eclipse.jetty.io.source"/>
<setEntry value="org.eclipse.jetty.io@default:default"/>
<setEntry value="org.eclipse.jetty.jmx.source"/>
<setEntry value="org.eclipse.jetty.jmx@default:default"/>
<setEntry value="org.eclipse.jetty.jndi@default:default"/>
<setEntry value="org.eclipse.jetty.osgi-servlet-api@default:default"/>
<setEntry value="org.eclipse.jetty.osgi.boot.jsp.source"/>
<setEntry value="org.eclipse.jetty.osgi.boot.jsp@default:false"/>
<setEntry value="org.eclipse.jetty.osgi.boot.warurl@default:default"/>
<setEntry value="org.eclipse.jetty.osgi.boot@3:true"/>
<setEntry value="org.eclipse.jetty.plus@default:default"/>
<setEntry value="org.eclipse.jetty.security.source"/>
<setEntry value="org.eclipse.jetty.security@default:default"/>
<setEntry value="org.eclipse.jetty.server.source"/>
<setEntry value="org.eclipse.jetty.server@default:default"/>
<setEntry value="org.eclipse.jetty.servlet.source"/>
<setEntry value="org.eclipse.jetty.servlet@default:default"/>
<setEntry value="org.eclipse.jetty.util.ajax.source"/>
<setEntry value="org.eclipse.jetty.util.ajax@default:default"/>
<setEntry value="org.eclipse.jetty.util.source"/>
<setEntry value="org.eclipse.jetty.util@default:default"/>
<setEntry value="org.eclipse.jetty.webapp.source"/>
<setEntry value="org.eclipse.jetty.webapp@default:default"/>
<setEntry value="org.eclipse.jetty.websocket.api.source"/>
<setEntry value="org.eclipse.jetty.websocket.api@default:default"/>
<setEntry value="org.eclipse.jetty.websocket.client.source"/>
<setEntry value="org.eclipse.jetty.websocket.client@default:default"/>
<setEntry value="org.eclipse.jetty.websocket.common.source"/>
<setEntry value="org.eclipse.jetty.websocket.common@default:default"/>
<setEntry value="org.eclipse.jetty.websocket.server.source"/>
<setEntry value="org.eclipse.jetty.websocket.server@default:default"/>
<setEntry value="org.eclipse.jetty.websocket.servlet.source"/>
<setEntry value="org.eclipse.jetty.websocket.servlet@default:default"/>
<setEntry value="org.eclipse.jetty.xml.source"/>
<setEntry value="org.eclipse.jetty.xml@default:default"/>
<setEntry value="org.eclipse.osgi.services.source"/>
<setEntry value="org.eclipse.osgi.services@default:default"/>
<setEntry value="org.eclipse.osgi.source"/>
<setEntry value="org.eclipse.osgi.util.source"/>
<setEntry value="org.eclipse.osgi.util@default:default"/>
<setEntry value="org.eclipse.osgi@-1:true"/>
<setEntry value="org.hyperic.sigar@default:default"/>
<setEntry value="org.jboss.marshalling.jboss-marshalling-osgi@default:default"/>
<setEntry value="org.jvnet.mimepull@default:default"/>
<setEntry value="org.mongodb.bson.source"/>
<setEntry value="org.mongodb.bson@default:default"/>
<setEntry value="org.mongodb.driver-core.source"/>
<setEntry value="org.mongodb.driver-core@default:default"/>
<setEntry value="org.mongodb.driver-sync@default:default"/>
<setEntry value="org.mortbay.jasper.apache-el.source"/>
<setEntry value="org.mortbay.jasper.apache-el@default:default"/>
<setEntry value="org.mortbay.jasper.apache-jsp.source"/>
<setEntry value="org.mortbay.jasper.apache-jsp@default:default"/>
<setEntry value="org.objectweb.asm.commons.source"/>
<setEntry value="org.objectweb.asm.commons@default:default"/>
<setEntry value="org.objectweb.asm.source"/>
<setEntry value="org.objectweb.asm.tree.analysis.source"/>
<setEntry value="org.objectweb.asm.tree.analysis@default:default"/>
<setEntry value="org.objectweb.asm.tree.source"/>
<setEntry value="org.objectweb.asm.tree@default:default"/>
<setEntry value="org.objectweb.asm.util.source"/>
<setEntry value="org.objectweb.asm.util@default:default"/>
<setEntry value="org.objectweb.asm@default:default"/>
<setEntry value="org.osgi.service.cm@default:default"/>
<setEntry value="org.osgi.service.component@default:default"/>
<setEntry value="org.osgi.service.device.source"/>
<setEntry value="org.osgi.service.device@default:default"/>
<setEntry value="org.osgi.service.event@default:default"/>
<setEntry value="org.osgi.service.http.whiteboard.source"/>
<setEntry value="org.osgi.service.http.whiteboard@default:default"/>
<setEntry value="org.osgi.service.metatype.source"/>
<setEntry value="org.osgi.service.metatype@default:default"/>
<setEntry value="org.osgi.service.packageadmin@default:default"/>
<setEntry value="org.osgi.service.provisioning.source"/>
<setEntry value="org.osgi.service.provisioning@default:default"/>
<setEntry value="org.osgi.service.upnp.source"/>
<setEntry value="org.osgi.service.upnp@default:default"/>
<setEntry value="org.osgi.service.url@default:default"/>
<setEntry value="org.osgi.service.useradmin.source"/>
<setEntry value="org.osgi.service.useradmin@default:default"/>
<setEntry value="org.osgi.service.wireadmin.source"/>
<setEntry value="org.osgi.service.wireadmin@default:default"/>
<setEntry value="org.osgi.util.function.source"/>
<setEntry value="org.osgi.util.function@default:default"/>
<setEntry value="org.osgi.util.measurement.source"/>
<setEntry value="org.osgi.util.measurement@default:default"/>
<setEntry value="org.osgi.util.position.source"/>
<setEntry value="org.osgi.util.position@default:default"/>
<setEntry value="org.osgi.util.promise.source"/>
<setEntry value="org.osgi.util.promise@default:default"/>
<setEntry value="org.osgi.util.xml.source"/>
<setEntry value="org.osgi.util.xml@default:default"/>
<setEntry value="org.owasp.encoder.source"/>
<setEntry value="org.owasp.encoder@default:default"/>
<setEntry value="org.yaml.snakeyaml@default:default"/>
<setEntry value="reactive-streams@default:default"/>
<setEntry value="redisson@default:default"/>
<setEntry value="routeconverter@default:default"/>
<setEntry value="slf4j.api@default:default"/>
<setEntry value="slf4j.jdk14@default:default"/>
</setAttribute>
<setAttribute key="selected_workspace_bundles">
<setEntry value="com.sap.sailing.geocoding@default:default"/>
<setEntry value="com.sap.sailing.domain.common@default:default"/>
<setEntry value="com.sap.sailing.domain@default:default"/>
<setEntry value="com.sap.sailing.news@4:true"/>
<setEntry value="com.sap.sailing.domain.tractracadapter@5:true"/>
<setEntry value="com.sap.sailing.expeditionconnector@default:default"/>
<setEntry value="com.sap.sailing.domain.vakarosadapter@4:true"/>
<setEntry value="com.sap.sailing.domain.windfinderadapter@4:true"/>
<setEntry value="com.sap.sailing.server@5:true"/>
<setEntry value="com.sap.sailing.server.gateway@5:true"/>
<setEntry value="com.sap.sailing.server.gateway.interfaces@default:default"/>
<setEntry value="com.sap.sailing.declination@default:default"/>
<setEntry value="com.sap.sailing.domain.persistence@default:default"/>
<setEntry value="com.sap.sailing.domain.swisstimingadapter@5:true"/>
<setEntry value="com.sap.sailing.domain.swisstimingadapter.persistence@4:true"/>
<setEntry value="com.sap.sailing.domain.swisstimingreplayadapter@4:true"/>
<setEntry value="com.sap.sailing.domain.tractracadapter.persistence@4:true"/>
<setEntry value="com.sap.sailing.gwt.ui@6:true"/>
<setEntry value="com.sap.sailing.udpconnector@default:default"/>
<setEntry value="com.sap.sailing.simulator@default:default"/>
<setEntry value="com.sap.sailing.www@5:true"/>
<setEntry value="com.sap.sailing.resultimport@4:true"/>
<setEntry value="com.sap.sailing.kiworesultimport@4:true"/>
<setEntry value="com.sap.sailing.ess40.resultimport@4:true"/>
<setEntry value="com.sap.sailing.freg.resultimport@4:true"/>
<setEntry value="com.sap.sailing.barbados.resultimport@4:true"/>
<setEntry value="com.sap.sailing.sailwave.resultimport@4:true"/>
<setEntry value="com.sap.sailing.sailwave.html.resultimport@4:true"/>
<setEntry value="com.sap.sailing.manage2sail.resultimport@4:true"/>
<setEntry value="com.sap.sailing.sailti.resultimport@4:true"/>
<setEntry value="com.sap.sailing.yachtscoring.resultimport@4:true"/>
<setEntry value="com.sap.sailing.velum.resultimport@4:true"/>
<setEntry value="com.sap.sailing.monitoring@7:true"/>
<setEntry value="com.sap.sailing.xrr.resultimport@4:true"/>
<setEntry value="com.sap.sailing.domain.igtimiadapter@default:default"/>
<setEntry value="com.sap.sailing.domain.igtimiadapter.server@4:true"/>
<setEntry value="com.sap.sailing.domain.igtimiadapter.persistence@default:default"/>
<setEntry value="com.sap.sailing.domain.racelogtrackingadapter@4:true"/>
<setEntry value="com.sap.sailing.domain.deckmanadapter@5:true"/>
<setEntry value="com.sap.sailing.domain.oceanraceadapter@5:true"/>
<setEntry value="com.sap.sailing.domain.yellowbrickadapter@5:true"/>
<setEntry value="com.sap.sailing.domain.yellowbrickadapter.persistence@4:true"/>
<setEntry value="com.sap.sailing.xrr.structureimport@default:default"/>
<setEntry value="com.sap.sailing.server.gateway.serialization.shared.android@default:default"/>
<setEntry value="com.sap.sailing.server.gateway.serialization@default:default"/>
<setEntry value="com.sap.sailing.dashboards.gwt@6:true"/>
<setEntry value="com.sap.sailing.dashboards.gwt@6:true"/>
<setEntry value="com.sap.sailing.datamining@5:true"/>
<setEntry value="com.sap.sailing.datamining.shared@default:default"/>
<setEntry value="com.sap.sailing.polars@5:true"/>
<setEntry value="com.sap.sailing.windestimation@5:true"/>
<setEntry value="com.sap.sailing.polars.datamining@5:true"/>
<setEntry value="com.sap.sailing.domain.shared.android@default:default"/>
<setEntry value="com.sap.sailing.manage2sail@default:default"/>
<setEntry value="com.sap.sailing.polars.datamining.shared@default:default"/>
<setEntry value="com.sap.sailing.xrr.schema@default:default"/>
<setEntry value="com.sap.sailing.server.trackfiles@default:default"/>
<setEntry value="com.sap.sailing.competitorimport@default:default"/>
<setEntry value="com.sap.sailing.datamining.provider@default:default"/>
<setEntry value="com.sap.sailing.grib@default:default"/>
<setEntry value="com.sap.sailing.nmeaconnector@default:default"/>
<setEntry value="com.sap.sailing.domain.expeditionadapter@5:true"/>
<setEntry value="com.sap.sailing.expeditionconnector.persistence@4:true"/>
<setEntry value="com.sap.sailing.expeditionconnector.common@default:default"/>
<setEntry value="com.sap.sailing.domain.bravoadapter@5:true"/>
<setEntry value="net.sf.marineapi@default:default"/>
<setEntry value="com.sap.sailing.routeconverterjava11extension@default:default"/>
<setEntry value="com.sap.sailing.server.interface@default:default"/>
<setEntry value="com.sap.sse.datamining.ui@default:default"/>
<setEntry value="com.sap.sailing.domain.igtimiadapter.gateway@5:true"/>
<setEntry value="com.sap.sailing.shared.server@5:true"/>
<setEntry value="com.sap.sailing.shared.server.gateway@5:true"/>
<setEntry value="com.sap.sailing.shared.persistence@default:default"/>
<setEntry value="com.sap.sailing.landscape@default:default"/>
<setEntry value="com.sap.sailing.landscape.gateway@5:true"/>
<setEntry value="com.sap.sailing.landscape.common@default:default"/>
<setEntry value="com.sap.sailing.landscape.ui@default:default"/>
<setEntry value="com.sap.sailing.hanaexport@5:true"/>
<setEntry value="com.sap.sailing.domain.queclinkadapter@5:true"/>
<setEntry value="com.sap.sailing.aiagent.interfaces@default:default"/>
<setEntry value="com.sap.sailing.aiagent@4:true"/>
<setEntry value="com.sap.sailing.aiagent.persistence@5:true"/>
<setEntry value="com.sap.sailing.aiagent.gateway@6:true"/>
<setEntry value="com.tractrac.clientmodule@default:default"/>
<setEntry value="com.sap.sse.gwt@default:default"/>
<setEntry value="com.google.gwt.servlet@default:default"/>
<setEntry value="com.sap.sse.security@default:default"/>
<setEntry value="com.sap.sse.security.ui@6:true"/>
<setEntry value="com.sap.sse.security.userstore.mongodb@4:true"/>
<setEntry value="com.sap.sse@default:default"/>
<setEntry value="com.sap.sse.common@default:default"/>
<setEntry value="com.sap.sse.datamining@default:default"/>
<setEntry value="com.sap.sse.datamining.annotations@default:default"/>
<setEntry value="com.sap.sse.datamining.shared@default:default"/>
<setEntry value="com.sap.sse.gwt.adminconsole@default:default"/>
<setEntry value="com.sap.sse.mongodb@default:default"/>
<setEntry value="com.sap.sse.operationaltransformation@default:default"/>
<setEntry value="com.sap.sse.replication@6:true"/>
<setEntry value="com.sap.sse.filestorage@4:true"/>
<setEntry value="com.sap.sse.shared.android@default:default"/>
<setEntry value="com.sap.sse.mail@5:true"/>
<setEntry value="com.sap.sse.threadmanager@default:default"/>
<setEntry value="com.sap.sse.security.common@default:default"/>
<setEntry value="org.json.simple@default:default"/>
<setEntry value="org.moxieapps.gwt.highcharts@default:default"/>
<setEntry value="com.googlecode.java-diff-utils@default:default"/>
<setEntry value="org.mp4parser.isoparser@default:default"/>
<setEntry value="com.sap.sse.replication.interfaces@default:default"/>
<setEntry value="com.sap.sse.security.datamining@5:true"/>
<setEntry value="com.sap.sse.security.persistence@default:default"/>
<setEntry value="com.sap.sse.security.interface@default:default"/>
<setEntry value="com.sap.sse.replication.persistence@default:default"/>
<setEntry value="com.sap.sse.landscape.common@default:default"/>
<setEntry value="com.sap.sse.landscape@default:default"/>
<setEntry value="com.sap.sse.landscape.aws@4:true"/>
<setEntry value="com.sap.sse.landscape.aws.common@default:default"/>
<setEntry value="com.sap.sse.landscape.aws.persistence@default:default"/>
<setEntry value="com.sap.sse.branding@default:default"/>
<setEntry value="com.sap.sse.branding.sap@5:true"/>
<setEntry value="com.sap.sse.aicore@default:default"/>
<setEntry value="elemental2@default:default"/>
</setAttribute>
<setEntry value="com.google.gwt.servlet@default:default"/>
<setEntry value="com.googlecode.java-diff-utils@default:default"/>
<setEntry value="com.sap.sailing.aiagent.gateway@6:true"/>
<setEntry value="com.sap.sailing.aiagent.interfaces@default:default"/>
<setEntry value="com.sap.sailing.aiagent.persistence@5:true"/>
<setEntry value="com.sap.sailing.aiagent@4:true"/>
<setEntry value="com.sap.sailing.barbados.resultimport@4:true"/>
<setEntry value="com.sap.sailing.competitorimport@default:default"/>
<setEntry value="com.sap.sailing.dashboards.gwt@6:true"/>
<setEntry value="com.sap.sailing.datamining.provider@default:default"/>
<setEntry value="com.sap.sailing.datamining.shared@default:default"/>
<setEntry value="com.sap.sailing.datamining@5:true"/>
<setEntry value="com.sap.sailing.declination@default:default"/>
<setEntry value="com.sap.sailing.domain.bravoadapter@5:true"/>
<setEntry value="com.sap.sailing.domain.common@default:default"/>
<setEntry value="com.sap.sailing.domain.deckmanadapter@5:true"/>
<setEntry value="com.sap.sailing.domain.expeditionadapter@5:true"/>
<setEntry value="com.sap.sailing.domain.igtimiadapter.gateway@5:true"/>
<setEntry value="com.sap.sailing.domain.igtimiadapter.persistence@default:default"/>
<setEntry value="com.sap.sailing.domain.igtimiadapter.server@4:true"/>
<setEntry value="com.sap.sailing.domain.igtimiadapter@default:default"/>
<setEntry value="com.sap.sailing.domain.oceanraceadapter@5:true"/>
<setEntry value="com.sap.sailing.domain.persistence@default:default"/>
<setEntry value="com.sap.sailing.domain.queclinkadapter@5:true"/>
<setEntry value="com.sap.sailing.domain.racelogtrackingadapter@4:true"/>
<setEntry value="com.sap.sailing.domain.shared.android@default:default"/>
<setEntry value="com.sap.sailing.domain.swisstimingadapter.persistence@4:true"/>
<setEntry value="com.sap.sailing.domain.swisstimingadapter@5:true"/>
<setEntry value="com.sap.sailing.domain.swisstimingreplayadapter@4:true"/>
<setEntry value="com.sap.sailing.domain.tractracadapter.persistence@4:true"/>
<setEntry value="com.sap.sailing.domain.tractracadapter@5:true"/>
<setEntry value="com.sap.sailing.domain.vakarosadapter@4:true"/>
<setEntry value="com.sap.sailing.domain.windfinderadapter@4:true"/>
<setEntry value="com.sap.sailing.domain.yellowbrickadapter.persistence@4:true"/>
<setEntry value="com.sap.sailing.domain.yellowbrickadapter@5:true"/>
<setEntry value="com.sap.sailing.domain@default:default"/>
<setEntry value="com.sap.sailing.ess40.resultimport@4:true"/>
<setEntry value="com.sap.sailing.expeditionconnector.common@default:default"/>
<setEntry value="com.sap.sailing.expeditionconnector.persistence@4:true"/>
<setEntry value="com.sap.sailing.expeditionconnector@default:default"/>
<setEntry value="com.sap.sailing.freg.resultimport@4:true"/>
<setEntry value="com.sap.sailing.geocoding@default:default"/>
<setEntry value="com.sap.sailing.grib@default:default"/>
<setEntry value="com.sap.sailing.gwt.ui@6:true"/>
<setEntry value="com.sap.sailing.hanaexport@5:true"/>
<setEntry value="com.sap.sailing.kiworesultimport@4:true"/>
<setEntry value="com.sap.sailing.landscape.common@default:default"/>
<setEntry value="com.sap.sailing.landscape.gateway@5:true"/>
<setEntry value="com.sap.sailing.landscape.ui@default:default"/>
<setEntry value="com.sap.sailing.landscape@default:default"/>
<setEntry value="com.sap.sailing.manage2sail.resultimport@4:true"/>
<setEntry value="com.sap.sailing.manage2sail@default:default"/>
<setEntry value="com.sap.sailing.monitoring@7:true"/>
<setEntry value="com.sap.sailing.news@4:true"/>
<setEntry value="com.sap.sailing.nmeaconnector@default:default"/>
<setEntry value="com.sap.sailing.polars.datamining.shared@default:default"/>
<setEntry value="com.sap.sailing.polars.datamining@5:true"/>
<setEntry value="com.sap.sailing.polars@5:true"/>
<setEntry value="com.sap.sailing.resultimport@4:true"/>
<setEntry value="com.sap.sailing.routeconverterjava11extension@default:false"/>
<setEntry value="com.sap.sailing.sailti.resultimport@4:true"/>
<setEntry value="com.sap.sailing.sailwave.html.resultimport@4:true"/>
<setEntry value="com.sap.sailing.sailwave.resultimport@4:true"/>
<setEntry value="com.sap.sailing.server.gateway.interfaces@default:default"/>
<setEntry value="com.sap.sailing.server.gateway.serialization.shared.android@default:default"/>
<setEntry value="com.sap.sailing.server.gateway.serialization@default:default"/>
<setEntry value="com.sap.sailing.server.gateway@5:true"/>
<setEntry value="com.sap.sailing.server.interface@default:default"/>
<setEntry value="com.sap.sailing.server.trackfiles@default:default"/>
<setEntry value="com.sap.sailing.server@5:true"/>
<setEntry value="com.sap.sailing.shared.persistence@default:default"/>
<setEntry value="com.sap.sailing.shared.server.gateway@5:true"/>
<setEntry value="com.sap.sailing.shared.server@5:true"/>
<setEntry value="com.sap.sailing.simulator@default:default"/>
<setEntry value="com.sap.sailing.udpconnector@default:default"/>
<setEntry value="com.sap.sailing.velum.resultimport@4:true"/>
<setEntry value="com.sap.sailing.windestimation@5:true"/>
<setEntry value="com.sap.sailing.www@5:true"/>
<setEntry value="com.sap.sailing.xrr.resultimport@4:true"/>
<setEntry value="com.sap.sailing.xrr.schema@default:default"/>
<setEntry value="com.sap.sailing.xrr.structureimport@default:default"/>
<setEntry value="com.sap.sailing.yachtscoring.resultimport@4:true"/>
<setEntry value="com.sap.sse.aicore@default:default"/>
<setEntry value="com.sap.sse.branding.sap@5:true"/>
<setEntry value="com.sap.sse.branding@default:default"/>
<setEntry value="com.sap.sse.common@default:default"/>
<setEntry value="com.sap.sse.datamining.annotations@default:default"/>
<setEntry value="com.sap.sse.datamining.shared@default:default"/>
<setEntry value="com.sap.sse.datamining.ui@default:default"/>
<setEntry value="com.sap.sse.datamining@default:default"/>
<setEntry value="com.sap.sse.filestorage@4:true"/>
<setEntry value="com.sap.sse.gwt.adminconsole@default:default"/>
<setEntry value="com.sap.sse.gwt@default:default"/>
<setEntry value="com.sap.sse.landscape.aws.common@default:default"/>
<setEntry value="com.sap.sse.landscape.aws.persistence@default:default"/>
<setEntry value="com.sap.sse.landscape.aws@4:true"/>
<setEntry value="com.sap.sse.landscape.common@default:default"/>
<setEntry value="com.sap.sse.landscape@default:default"/>
<setEntry value="com.sap.sse.mail@5:true"/>
<setEntry value="com.sap.sse.mongodb@default:default"/>
<setEntry value="com.sap.sse.operationaltransformation@default:default"/>
<setEntry value="com.sap.sse.replication.interfaces@default:default"/>
<setEntry value="com.sap.sse.replication.persistence@default:default"/>
<setEntry value="com.sap.sse.replication@6:true"/>
<setEntry value="com.sap.sse.security.common@default:default"/>
<setEntry value="com.sap.sse.security.datamining@5:true"/>
<setEntry value="com.sap.sse.security.interface@default:default"/>
<setEntry value="com.sap.sse.security.persistence@default:default"/>
<setEntry value="com.sap.sse.security.ui@6:true"/>
<setEntry value="com.sap.sse.security.userstore.mongodb@4:true"/>
<setEntry value="com.sap.sse.security@default:default"/>
<setEntry value="com.sap.sse.shared.android@default:default"/>
<setEntry value="com.sap.sse.threadmanager@default:default"/>
<setEntry value="com.sap.sse@default:default"/>
<setEntry value="com.tractrac.clientmodule@default:default"/>
<setEntry value="elemental2@default:default"/>
<setEntry value="net.sf.marineapi@default:default"/>
<setEntry value="org.json.simple@default:default"/>
<setEntry value="org.moxieapps.gwt.highcharts@default:default"/>
<setEntry value="org.mp4parser.isoparser@default:default"/>
</setAttribute>
<booleanAttribute key="show_selected_only" value="false"/>
<booleanAttribute key="tracing" value="false"/>
<booleanAttribute key="useCustomFeatures" value="false"/>
@@ -33,6 +33,7 @@ import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@@ -598,6 +599,18 @@ Replicator {
private final AtomicInteger numberOfTrackedRacesStillLoading;
/**
* Counts down when {@link #setPolarDataService(PolarDataService)} first receives a non-null
* polar-data service. Restoring tracked races (see {@link #restoreTrackedRaces()}) waits on
* this latch on a background thread so that every {@link TrackedRace} produced by restore
* has a non-null polar-data service already installed when it is added, per the invariant
* that a maneuver-based wind estimation should never be built for a race whose polar data
* hasn't at least started to become available. See bug6241 and
* {@link com.sap.sailing.windestimation.integration.WindEstimationFactoryServiceImpl#createIncrementalWindEstimationTrack}
* for the corresponding check on the other end.
*/
private final CountDownLatch polarDataServiceArrived = new CountDownLatch(1);
private final ServiceTracker<ResultUrlRegistry, ResultUrlRegistry> resultUrlRegistryServiceTracker;
private final ServiceTracker<ScoreCorrectionProvider, ScoreCorrectionProvider> scoreCorrectionProviderServiceTracker;
@@ -969,7 +982,28 @@ Replicator {
}
}
if (restoreTrackedRaces) {
restoreTrackedRaces();
// bug6241: defer restoration to a background thread that awaits polarDataService
// arrival. This way every tracked race added by the restore has a non-null polar
// service from the start, matching the invariant that a maneuver-based wind
// estimator should only ever be built for a race whose polar service is available.
// The alternative -- restoring synchronously in the constructor -- would race
// with the OSGi service tracker that installs the PolarDataService, which opens
// AFTER RacingEventServiceImpl is constructed (see Activator.start ordering).
final Thread deferredRestoreThread = new Thread(() -> {
try {
polarDataServiceArrived.await();
restoreTrackedRaces();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
logger.log(Level.WARNING,
"Deferred restoreTrackedRaces was interrupted before PolarDataService arrived; no races restored",
e);
} catch (Throwable e) {
logger.log(Level.SEVERE, "Deferred restoreTrackedRaces failed", e);
}
}, "RacingEventServiceImpl-deferred-restoreTrackedRaces");
deferredRestoreThread.setDaemon(true);
deferredRestoreThread.start();
} else {
getMongoObjectFactory().removeAllConnectivityParametersForRacesToRestore();
}
@@ -1150,6 +1184,15 @@ Replicator {
}
}
}).getNumberOfParametersToLoad();
// Enumeration complete: every race to restore has had its loading triggered (some may
// still be in LOADING state and some may never leave it; that's fine). Announce this to
// the polar data service, which if constructed in gated mode will now release any
// drain-callback waiters (see bug6241). Announcing to a non-gated instance is a harmless
// no-op. Since restoreTrackedRaces() only starts after the polarDataServiceArrived latch
// has counted down (see the deferred task in the constructor), and the polars bundle now
// registers its service exactly once and only after full initialization, polarDataService
// is guaranteed non-null at this point.
polarDataService.markLoadingOfAllRacesToRestoreStarted();
}
@Override
@@ -2346,6 +2389,15 @@ Replicator {
if (polarFixCacheUpdater != null) {
trackedRace.removeListener(polarFixCacheUpdater);
}
// bug6241: let the polar data service forget any wind-estimation-install callbacks it
// still has parked for this race (see PolarDataService.raceRemoved). Without this the
// parked callback -- which strongly captures trackedRace -- would pin the race and all
// of its tracks for the lifetime of the service if the race is removed before its
// polar loading completed. polarDataService may still be null if it has not been
// registered via its OSGi ServiceTracker yet.
if (polarDataService != null) {
polarDataService.raceRemoved(trackedRace);
}
trackedRace.runSynchronizedOnStatus(()->{
if (!trackedRace.hasFinishedLoading()) {
numberOfTrackedRacesStillLoading.decrementAndGet();
@@ -2356,23 +2408,37 @@ Replicator {
@Override
public void raceAdded(TrackedRace trackedRace) {
// replicate the addition of the tracked race:
CreateTrackedRace op = new CreateTrackedRace(trackedRace.getRaceIdentifier(), trackedRace.getWindStore(),
final CreateTrackedRace op = new CreateTrackedRace(trackedRace.getRaceIdentifier(), trackedRace.getWindStore(),
trackedRace.getDelayToLiveInMillis(), trackedRace.getMillisecondsOverWhichToAverageWind(),
trackedRace.getMillisecondsOverWhichToAverageSpeed(), trackedRace.getTrackingConnectorInfo());
replicate(op);
linkRaceToConfiguredLeaderboardColumns(trackedRace);
TrackedRaceReplicatorAndNotifier trackedRaceReplicator = new TrackedRaceReplicatorAndNotifier(trackedRace);
final TrackedRaceReplicatorAndNotifier trackedRaceReplicator = new TrackedRaceReplicatorAndNotifier(trackedRace);
trackedRaceReplicators.put(trackedRace, trackedRaceReplicator);
trackedRace.addListener(trackedRaceReplicator, /* fire wind already loaded */true, /* notifyAboutGPSFixesAlreadyLoaded */ true);
PolarFixCacheUpdater polarFixCacheUpdater = new PolarFixCacheUpdater(trackedRace);
trackedRace.addListener(trackedRaceReplicator, /* fire wind already loaded */ true, /* notifyAboutGPSFixesAlreadyLoaded */ true);
// 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) {
if (polarDataService != null) { // this shouldn't really be necessary after the bug6241 changes as we now
// require a valid PolarDataService prior to restoring/loading any races
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) {
trackedRace.setWindEstimation(
windEstimationFactoryService.createIncrementalWindEstimationTrack(trackedRace));
scheduleWindEstimationInstallation(trackedRace, windEstimationFactoryService);
}
numberOfTrackedRacesStillLoading.incrementAndGet();
trackedRace.runWhenDoneLoading(()->numberOfTrackedRacesStillLoading.decrementAndGet());
@@ -2480,6 +2546,7 @@ Replicator {
private class PolarFixCacheUpdater extends AbstractRaceChangeListener {
private final TrackedRace race;
private boolean ingestionTriggered;
public PolarFixCacheUpdater(TrackedRace race) {
this.race = race;
@@ -2494,14 +2561,22 @@ 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) {
polarDataService.raceFinishedLoading(race);
}
// 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);
}
}
}
/**
@@ -4708,8 +4783,17 @@ Replicator {
public void setPolarDataService(PolarDataService service) {
if (this.polarDataService == null && service != null) {
polarDataService = service;
// The polars bundle now registers its service only after it has already obtained the
// DomainFactory from the RacingEventService itself (see
// com.sap.sailing.polars.impl.Activator), so registering it back here is only kept
// as a defensive no-op for services registered by other paths (e.g. test harnesses).
polarDataService.registerDomainFactory(baseDomainFactory);
setPolarDataServiceOnAllTrackedRaces(service);
// bug6241: unblock the deferred restoreTrackedRaces() background task so it can
// now proceed with adding tracked races. Every race added after this point will
// observe a non-null polarDataService in this RacingEventServiceImpl and will have
// it installed by RaceAdditionListener.raceAdded.
polarDataServiceArrived.countDown();
}
}
@@ -4729,8 +4813,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);
@@ -4741,7 +4840,92 @@ 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/FINISHED 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 (see
* {@link PolarDataService#runWhenPolarLoadingFinishedFor(TrackedRace, Runnable)}).
* Note that this is a <em>global</em> drain of the shared loading pipeline, gated so
* it does not fire before this race's own fixes have been ingested; it waits for the
* fixes of all races loaded so far, not just this one. That is essential because the
* estimator captures the polar service at construction time and uses it for
* classification and wind-speed inference; installing the estimator before the polars
* had been fully mined would leave it permanently using a polar model that reflects an
* incomplete data set. Waiting for the global drain is deliberate and accepts some
* startup sequentiality in exchange for a complete polar model.</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, new Runnable() {
@Override
public void run() {
synchronized (trackedRace) {
if (trackedRace.getWindEstimation() == null
&& windEstimationFactoryService == service
&& trackedRace.getStatus().getStatus() != TrackedRaceStatusEnum.REMOVED
&& trackedRace.getStatus().getStatus() != TrackedRaceStatusEnum.ERROR) {
trackedRace.setWindEstimation(
service.createIncrementalWindEstimationTrack(trackedRace));
}
}
}
@Override
public String toString() {
return "Installing wind estimation for race "+trackedRace.getRaceIdentifier().toString();
}
});
}
});
}
private void setPolarDataServiceOnAllTrackedRaces(PolarDataService service) {
// Capture the wind factory at method entry: on replicas, initiallyFillFromInternal
// deserializes each DynamicTrackedRegatta with its trackedRaces map already populated
// and then invokes ensureRegattaHasRaceAdditionListener(...), which calls
// TrackedRegatta.addRaceListener(...) -- and that in turn synchronously replays
// raceAdded(...) for every pre-existing tracked race. At that moment
// polarDataService and windEstimationFactoryService may both still be null (their
// OSGi trackers have not fired yet). If the wind factory arrives before the polar
// service, setWindEstimationFactoryService -> setWindEstimationOnAllTrackedRaces
// schedules an install per race, and scheduleWindEstimationInstallation's inner
// callback (past-LOADING guard) fires immediately because replicated races typically
// arrive already past LOADING; it then sees polarDataService == null and drops out
// silently, without registering a runWhenPolarLoadingFinishedFor callback.
// We compensate here: once the polar service actually arrives, if the wind factory
// is already available, reschedule the install per race. scheduleWindEstimationInstallation
// is idempotent, guarded against duplicate installation, so this is a safe no-op
// for races that already got their estimator through another code path. See bug6241.
final WindEstimationFactoryService factoryAtMethodEntry = windEstimationFactoryService;
Iterable<Regatta> allRegattas = getAllRegattas();
for (Regatta regatta : allRegattas) {
DynamicTrackedRegatta trackedRegatta = getTrackedRegatta(regatta);
@@ -4753,6 +4937,9 @@ Replicator {
trackedRace.setPolarDataService(service);
if (service != null) {
service.insertExistingFixes(trackedRace);
if (factoryAtMethodEntry != null) {
scheduleWindEstimationInstallation(trackedRace, factoryAtMethodEntry);
}
}
}
} catch (Throwable e) {
@@ -4766,6 +4953,16 @@ Replicator {
public void unsetPolarDataService(PolarDataService service) {
if (polarDataService == service) {
// bug6241: losing the PolarDataService is a serious event -- the maneuver-based
// wind estimation depends on it and cannot be usefully constructed while it is
// absent (see WindEstimationFactoryServiceImpl.createIncrementalWindEstimationTrack).
// With the polars bundle now registering its service exactly once after full
// initialization (see com.sap.sailing.polars.impl.Activator), this call should only
// happen at bundle shutdown or on catastrophic OSGi framework events. Existing
// tracked-race estimators are torn down via setPolarDataServiceOnAllTrackedRaces(null)
// which invokes TrackedRace.setPolarDataService(null) on each race.
logger.log(Level.SEVERE, "PolarDataService has been unregistered from RacingEventService. "
+ "Maneuver-based wind estimation is now unavailable until it is re-registered.");
polarDataService = null;
setPolarDataServiceOnAllTrackedRaces(null);
}
@@ -0,0 +1,101 @@
package com.sap.sailing.windestimation.integration;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.Test;
import com.sap.sailing.domain.base.BoatClass;
import com.sap.sailing.domain.base.impl.BoatClassImpl;
import com.sap.sailing.domain.common.BoatClassMasterdata;
import com.sap.sailing.domain.common.ManeuverType;
import com.sap.sailing.domain.common.Tack;
import com.sap.sailing.domain.tracking.Maneuver;
import com.sap.sailing.domain.tracking.ManeuverLoss;
import com.sap.sailing.domain.tracking.WindWithConfidence;
import com.sap.sailing.domain.tracking.impl.ManeuverCurveBoundariesImpl;
import com.sap.sailing.domain.tracking.impl.ManeuverWithMainCurveBoundariesImpl;
import com.sap.sailing.windestimation.data.ManeuverTypeForClassification;
import com.sap.sailing.windestimation.data.ManeuverWithEstimatedType;
import com.sap.sailing.windestimation.data.SimpleManeuverForEstimation;
import com.sap.sailing.windestimation.data.SimpleManeuverForEstimationImpl;
import com.sap.sailing.windestimation.data.SimpleManeuverWithEstimatedTypeImpl;
import com.sap.sailing.windestimation.windinference.DummyBasedTwsCalculatorImpl;
import com.sap.sailing.windestimation.windinference.MiddleCourseBasedTwdCalculatorImpl;
import com.sap.sailing.windestimation.windinference.WindTrackCalculator;
import com.sap.sailing.windestimation.windinference.WindTrackCalculatorImpl;
import com.sap.sse.common.Duration;
import com.sap.sse.common.Position;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.Util.Pair;
import com.sap.sse.common.impl.DegreeBearingImpl;
import com.sap.sse.common.impl.DegreePosition;
import com.sap.sse.common.impl.KnotSpeedImpl;
import com.sap.sse.common.impl.KnotSpeedWithBearingImpl;
import com.sap.sse.common.impl.MeterDistance;
/**
* Deals with converting a {@link Maneuver} instance into a {@link ManeuverWithEstimatedType} object. This will be
* useful in producing an estimated wind track from maneuvers loaded from the database.
*
* @author Axel Uhl (d043530)
*
*/
public class ManeuverWithEstimatedTypeFromManeuverTest {
@Test
public void testProducingManeuverWithEstimatedTypeFromManeuver() {
final BoatClass boatClass = new BoatClassImpl(BoatClassMasterdata._5O5);
final ManeuverCurveBoundariesImpl mainCurveBoundaries = new ManeuverCurveBoundariesImpl(
TimePoint.now().minus(Duration.ONE_SECOND), TimePoint.now().plus(Duration.ONE_SECOND),
new KnotSpeedWithBearingImpl(10, new DegreeBearingImpl(10)),
new KnotSpeedWithBearingImpl(9, new DegreeBearingImpl(100)), 90, new KnotSpeedImpl(5),
new KnotSpeedImpl(7));
final ManeuverCurveBoundariesImpl maneuverCurveWithStableSpeedAndCourseBoundaries = new ManeuverCurveBoundariesImpl(
TimePoint.now().minus(Duration.ONE_SECOND), TimePoint.now().plus(Duration.ONE_SECOND),
new KnotSpeedWithBearingImpl(10, new DegreeBearingImpl(10)),
new KnotSpeedWithBearingImpl(9, new DegreeBearingImpl(100)), 90, new KnotSpeedImpl(5),
new KnotSpeedImpl(7));
final Maneuver maneuver = new ManeuverWithMainCurveBoundariesImpl(ManeuverType.TACK, Tack.PORT,
new DegreePosition(54, 8), TimePoint.now(), mainCurveBoundaries,
maneuverCurveWithStableSpeedAndCourseBoundaries, 5.0, /* markPassing */ null,
new ManeuverLoss(/* distanceSailedProjectedOnMiddleManeuverAngle */ new MeterDistance(10),
/* distanceSailedIfNotManeuveringProjectedOnMiddleManeuverAngle */ new MeterDistance(20),
new DegreePosition(55, 9), new DegreePosition(56, 10), Duration.ONE_SECOND.times(20),
new KnotSpeedWithBearingImpl(10, new DegreeBearingImpl(10)), new DegreeBearingImpl(15)));
final SimpleManeuverForEstimation maneuverForEstimation = getManeuverForEstimation(maneuver, boatClass);
final SimpleManeuverWithEstimatedTypeImpl<SimpleManeuverForEstimation> maneuverWithEstimatedType = new SimpleManeuverWithEstimatedTypeImpl<>(
maneuverForEstimation, mapManeuverType(maneuver.getType()), 0.7);
final WindTrackCalculator calculator = new WindTrackCalculatorImpl(new MiddleCourseBasedTwdCalculatorImpl(),
new DummyBasedTwsCalculatorImpl());
final List<WindWithConfidence<Pair<Position, TimePoint>>> track = calculator
.getWindTrackFromManeuverClassifications(Arrays.asList(maneuverWithEstimatedType));
assertEquals(1, track.size());
assertEquals(55., track.get(0).getObject().getFrom().getDegrees(), 0.0001); // 55deg is the middle between course in (10deg) and course out (100deg)
}
private SimpleManeuverForEstimation getManeuverForEstimation(Maneuver maneuver, BoatClass boatClass) {
return new SimpleManeuverForEstimationImpl(maneuver.getTimePoint(), maneuver.getPosition(), maneuver.getMainCurveBoundaries().getMiddleCourse(),
maneuver.getSpeedWithBearingBefore(), maneuver.getSpeedWithBearingAfter(), /* is clean */ true, boatClass);
}
private ManeuverTypeForClassification mapManeuverType(ManeuverType type) {
switch (type) {
case BEAR_AWAY:
return ManeuverTypeForClassification.BEAR_AWAY;
case HEAD_UP:
return ManeuverTypeForClassification.HEAD_UP;
case JIBE:
return ManeuverTypeForClassification.JIBE;
case PENALTY_CIRCLE:
return null;
case TACK:
return ManeuverTypeForClassification.TACK;
case UNKNOWN:
return null;
default:
return null;
}
}
}
@@ -9,6 +9,8 @@ import com.sap.sailing.windestimation.data.CompetitorTrackWithEstimationData;
import com.sap.sailing.windestimation.data.ManeuverForEstimation;
import com.sap.sailing.windestimation.data.ManeuverWithEstimatedType;
import com.sap.sailing.windestimation.data.RaceWithEstimationData;
import com.sap.sailing.windestimation.data.SimpleManeuverForEstimation;
import com.sap.sailing.windestimation.data.SimpleManeuverWithEstimatedType;
import com.sap.sailing.windestimation.model.classifier.maneuver.ManeuverClassifiersCache;
import com.sap.sailing.windestimation.model.classifier.maneuver.ManeuverWithProbabilisticTypeClassification;
import com.sap.sailing.windestimation.preprocessing.PreprocessingPipeline;
@@ -76,7 +78,7 @@ public class ManeuverBasedWindEstimationComponentImpl<InputType>
@Override
public List<WindWithConfidence<Pair<Position, TimePoint>>> estimateWindTrackAfterManeuverClassificationsAggregation(
List<ManeuverWithEstimatedType> improvedManeuverClassifications) {
List<? extends SimpleManeuverWithEstimatedType<? extends SimpleManeuverForEstimation>> improvedManeuverClassifications) {
List<WindWithConfidence<Pair<Position, TimePoint>>> windTrack = windTrackCalculator
.getWindTrackFromManeuverClassifications(improvedManeuverClassifications);
return windTrack;
@@ -12,8 +12,9 @@ import com.sap.sailing.domain.tracking.impl.WindWithConfidenceImpl;
import com.sap.sailing.windestimation.aggregator.ManeuverClassificationsAggregator;
import com.sap.sailing.windestimation.aggregator.polarsfitting.PolarsFittingWindEstimation;
import com.sap.sailing.windestimation.data.ManeuverForEstimation;
import com.sap.sailing.windestimation.data.ManeuverWithEstimatedType;
import com.sap.sailing.windestimation.data.RaceWithEstimationData;
import com.sap.sailing.windestimation.data.SimpleManeuverForEstimation;
import com.sap.sailing.windestimation.data.SimpleManeuverWithEstimatedType;
import com.sap.sailing.windestimation.model.classifier.maneuver.ManeuverClassifiersCache;
import com.sap.sailing.windestimation.model.classifier.maneuver.ManeuverWithProbabilisticTypeClassification;
import com.sap.sailing.windestimation.preprocessing.PreprocessingPipeline;
@@ -90,7 +91,7 @@ public class PolarsFittingBasedWindEstimationComponentImpl<InputType>
@Override
public List<WindWithConfidence<Pair<Position, TimePoint>>> estimateWindTrackAfterManeuverClassificationsAggregation(
List<ManeuverWithEstimatedType> improvedManeuverClassifications) {
List<? extends SimpleManeuverWithEstimatedType<? extends SimpleManeuverForEstimation>> improvedManeuverClassifications) {
throw new UnsupportedOperationException();
}
@@ -7,6 +7,8 @@ import com.sap.sailing.windestimation.aggregator.ManeuverClassificationsAggregat
import com.sap.sailing.windestimation.data.ManeuverForEstimation;
import com.sap.sailing.windestimation.data.ManeuverWithEstimatedType;
import com.sap.sailing.windestimation.data.RaceWithEstimationData;
import com.sap.sailing.windestimation.data.SimpleManeuverForEstimation;
import com.sap.sailing.windestimation.data.SimpleManeuverWithEstimatedType;
import com.sap.sailing.windestimation.model.classifier.maneuver.ManeuverClassifiersCache;
import com.sap.sailing.windestimation.model.classifier.maneuver.ManeuverWithProbabilisticTypeClassification;
import com.sap.sailing.windestimation.preprocessing.PreprocessingPipeline;
@@ -46,7 +48,7 @@ public interface WindEstimationComponentWithInternals<InputType> extends WindEst
RaceWithEstimationData<ManeuverWithProbabilisticTypeClassification> raceWithManeuverClassifications);
List<WindWithConfidence<Pair<Position, TimePoint>>> estimateWindTrackAfterManeuverClassificationsAggregation(
List<ManeuverWithEstimatedType> improvedManeuverClassifications);
List<? extends SimpleManeuverWithEstimatedType<? extends SimpleManeuverForEstimation>> improvedManeuverClassifications);
PreprocessingPipeline<InputType, RaceWithEstimationData<ManeuverForEstimation>> getPreprocessingPipeline();
@@ -18,6 +18,7 @@ import com.sap.sailing.domain.tracking.WindWithConfidence;
import com.sap.sailing.domain.tracking.impl.WindWithConfidenceImpl;
import com.sap.sailing.windestimation.data.CompetitorTrackWithEstimationData;
import com.sap.sailing.windestimation.data.ManeuverForEstimation;
import com.sap.sailing.windestimation.data.SimpleManeuverForEstimation;
import com.sap.sse.common.Bearing;
import com.sap.sse.common.Speed;
import com.sap.sse.common.SpeedWithBearing;
@@ -214,7 +215,7 @@ public class PolarsFittingWindEstimation {
return windWithConfidence;
}
public Speed getWindSpeed(ManeuverForEstimation maneuver, Bearing windCourse) {
public Speed getWindSpeed(SimpleManeuverForEstimation maneuver, Bearing windCourse) {
WindSpeedRange windSpeedRange = null;
BoatClass boatClass = maneuver.getBoatClass();
if (maneuver.isClean()) {
@@ -14,13 +14,8 @@ import com.sap.sse.common.TimePoint;
* @author Vladislav Chumak (D069712)
*
*/
public class ManeuverForEstimation implements Comparable<ManeuverForEstimation> {
public class ManeuverForEstimation extends SimpleManeuverForEstimationImpl {
private final TimePoint maneuverTimePoint;
private final Position maneuverPosition;
private final Bearing middleCourse;
private final SpeedWithBearing speedWithBearingBefore;
private final SpeedWithBearing speedWithBearingAfter;
private final double courseChangeInDegrees;
private final double courseChangeWithinMainCurveInDegrees;
private final double maxTurningRateInDegreesPerSecond;
@@ -29,7 +24,6 @@ public class ManeuverForEstimation implements Comparable<ManeuverForEstimation>
private final double speedLossRatio;
private final double speedGainRatio;
private final double lowestSpeedVsExitingSpeedRatio;
private final boolean clean;
private final ManeuverCategory maneuverCategory;
private final double scaledSpeedBefore;
private final double scaledSpeedAfter;
@@ -46,11 +40,7 @@ public class ManeuverForEstimation implements Comparable<ManeuverForEstimation>
double lowestSpeedVsExitingSpeedRatio, boolean clean, ManeuverCategory maneuverCategory,
double scaledSpeedBefore, double scaledSpeedAfter, boolean markPassing, BoatClass boatClass,
boolean markPassingDataAvailable, String competitorName) {
this.maneuverTimePoint = maneuverTimePoint;
this.maneuverPosition = maneuverPosition;
this.middleCourse = middleCourse;
this.speedWithBearingBefore = speedWithBearingBefore;
this.speedWithBearingAfter = speedWithBearingAfter;
super(maneuverTimePoint, maneuverPosition, middleCourse, speedWithBearingBefore, speedWithBearingAfter, clean, boatClass);
this.courseChangeInDegrees = courseChangeInDegrees;
this.courseChangeWithinMainCurveInDegrees = courseChangeWithinMainCurveInDegrees;
this.maxTurningRateInDegreesPerSecond = maxTurningRateInDegreesPerSecond;
@@ -59,7 +49,6 @@ public class ManeuverForEstimation implements Comparable<ManeuverForEstimation>
this.speedLossRatio = speedLossRatio;
this.speedGainRatio = speedGainRatio;
this.lowestSpeedVsExitingSpeedRatio = lowestSpeedVsExitingSpeedRatio;
this.clean = clean;
this.maneuverCategory = maneuverCategory;
this.scaledSpeedBefore = scaledSpeedBefore;
this.scaledSpeedAfter = scaledSpeedAfter;
@@ -69,26 +58,6 @@ public class ManeuverForEstimation implements Comparable<ManeuverForEstimation>
this.competitorName = competitorName;
}
public TimePoint getManeuverTimePoint() {
return maneuverTimePoint;
}
public Position getManeuverPosition() {
return maneuverPosition;
}
public Bearing getMiddleCourse() {
return middleCourse;
}
public SpeedWithBearing getSpeedWithBearingBefore() {
return speedWithBearingBefore;
}
public SpeedWithBearing getSpeedWithBearingAfter() {
return speedWithBearingAfter;
}
public double getCourseChangeInDegrees() {
return courseChangeInDegrees;
}
@@ -121,10 +90,6 @@ public class ManeuverForEstimation implements Comparable<ManeuverForEstimation>
return lowestSpeedVsExitingSpeedRatio;
}
public boolean isClean() {
return clean;
}
public ManeuverCategory getManeuverCategory() {
return maneuverCategory;
}
@@ -153,14 +118,9 @@ public class ManeuverForEstimation implements Comparable<ManeuverForEstimation>
return competitorName;
}
@Override
public int compareTo(ManeuverForEstimation o) {
return maneuverTimePoint.compareTo(o.maneuverTimePoint);
}
@Override
public String toString() {
return "Maneuver at " + maneuverTimePoint + ", "
+ maneuverPosition + ", middleCourse=" + middleCourse + ", courseChangeInDegrees=" + courseChangeInDegrees;
return "Maneuver at " + getManeuverTimePoint() + ", "
+ getManeuverPosition() + ", middleCourse=" + getMiddleCourse() + ", courseChangeInDegrees=" + courseChangeInDegrees;
}
}
@@ -6,39 +6,9 @@ package com.sap.sailing.windestimation.data;
* @author Vladislav Chumak (D069712)
*
*/
public class ManeuverWithEstimatedType implements Comparable<ManeuverWithEstimatedType> {
public class ManeuverWithEstimatedType extends SimpleManeuverWithEstimatedTypeImpl<ManeuverForEstimation> {
private final ManeuverForEstimation maneuver;
private final ManeuverTypeForClassification maneuverType;
private final double confidence;
public ManeuverWithEstimatedType(ManeuverForEstimation maneuver, ManeuverTypeForClassification maneuverType,
double confidence) {
this.maneuver = maneuver;
this.maneuverType = maneuverType;
this.confidence = confidence;
}
public ManeuverForEstimation getManeuver() {
return maneuver;
}
public ManeuverTypeForClassification getManeuverType() {
return maneuverType;
}
public double getConfidence() {
return confidence;
}
@Override
public int compareTo(ManeuverWithEstimatedType o) {
return maneuver.compareTo(o.maneuver);
}
@Override
public String toString() {
return "" + maneuver + " of type " + maneuverType + ", confidence="
+ confidence;
public ManeuverWithEstimatedType(ManeuverForEstimation maneuver, ManeuverTypeForClassification maneuverType, double confidence) {
super(maneuver, maneuverType, confidence);
}
}
@@ -0,0 +1,25 @@
package com.sap.sailing.windestimation.data;
import com.sap.sailing.domain.base.BoatClass;
import com.sap.sse.common.Bearing;
import com.sap.sse.common.Position;
import com.sap.sse.common.SpeedWithBearing;
import com.sap.sse.common.TimePoint;
public interface SimpleManeuverForEstimation extends Comparable<SimpleManeuverForEstimation> {
Bearing getMiddleCourse();
Position getManeuverPosition();
TimePoint getManeuverTimePoint();
BoatClass getBoatClass();
boolean isClean();
SpeedWithBearing getSpeedWithBearingBefore();
SpeedWithBearing getSpeedWithBearingAfter();
}
@@ -0,0 +1,70 @@
package com.sap.sailing.windestimation.data;
import com.sap.sailing.domain.base.BoatClass;
import com.sap.sse.common.Bearing;
import com.sap.sse.common.Position;
import com.sap.sse.common.SpeedWithBearing;
import com.sap.sse.common.TimePoint;
public class SimpleManeuverForEstimationImpl implements SimpleManeuverForEstimation {
private final TimePoint maneuverTimePoint;
private final Position maneuverPosition;
private final Bearing middleCourse;
private final SpeedWithBearing speedWithBearingBefore;
private final SpeedWithBearing speedWithBearingAfter;
private final boolean clean;
private final BoatClass boatClass;
public SimpleManeuverForEstimationImpl(TimePoint maneuverTimePoint, Position maneuverPosition, Bearing middleCourse,
SpeedWithBearing speedWithBearingBefore, SpeedWithBearing speedWithBearingAfter, boolean clean,
BoatClass boatClass) {
super();
this.maneuverTimePoint = maneuverTimePoint;
this.maneuverPosition = maneuverPosition;
this.middleCourse = middleCourse;
this.speedWithBearingBefore = speedWithBearingBefore;
this.speedWithBearingAfter = speedWithBearingAfter;
this.clean = clean;
this.boatClass = boatClass;
}
@Override
public Bearing getMiddleCourse() {
return middleCourse;
}
@Override
public Position getManeuverPosition() {
return maneuverPosition;
}
@Override
public TimePoint getManeuverTimePoint() {
return maneuverTimePoint;
}
@Override
public BoatClass getBoatClass() {
return boatClass;
}
@Override
public boolean isClean() {
return clean;
}
@Override
public SpeedWithBearing getSpeedWithBearingBefore() {
return speedWithBearingBefore;
}
@Override
public SpeedWithBearing getSpeedWithBearingAfter() {
return speedWithBearingAfter;
}
@Override
public int compareTo(SimpleManeuverForEstimation o) {
return getManeuverTimePoint().compareTo(o.getManeuverTimePoint());
}
}
@@ -0,0 +1,11 @@
package com.sap.sailing.windestimation.data;
public interface SimpleManeuverWithEstimatedType<T extends SimpleManeuverForEstimation> extends Comparable<SimpleManeuverWithEstimatedType<T>> {
ManeuverTypeForClassification getManeuverType();
T getManeuver();
double getConfidence();
}
@@ -0,0 +1,41 @@
package com.sap.sailing.windestimation.data;
public class SimpleManeuverWithEstimatedTypeImpl<T extends SimpleManeuverForEstimation> implements SimpleManeuverWithEstimatedType<T> {
private final ManeuverTypeForClassification maneuverType;
private final double confidence;
private final T maneuver;
public SimpleManeuverWithEstimatedTypeImpl(T maneuver, ManeuverTypeForClassification maneuverType, double confidence) {
super();
this.maneuverType = maneuverType;
this.confidence = confidence;
this.maneuver = maneuver;
}
@Override
public ManeuverTypeForClassification getManeuverType() {
return maneuverType;
}
@Override
public T getManeuver() {
return maneuver;
}
@Override
public double getConfidence() {
return confidence;
}
@Override
public int compareTo(SimpleManeuverWithEstimatedType<T> o) {
return getManeuver().compareTo(o.getManeuver());
}
@Override
public String toString() {
return "" + maneuver + " of type " + maneuverType + ", confidence="
+ confidence;
}
}
@@ -14,13 +14,16 @@ import java.util.concurrent.Executor;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.sap.sailing.domain.base.BoatClass;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.common.ManeuverType;
import com.sap.sailing.domain.common.Wind;
import com.sap.sailing.domain.common.WindSource;
import com.sap.sailing.domain.common.WindSourceType;
import com.sap.sailing.domain.maneuverdetection.TrackTimeInfo;
import com.sap.sailing.domain.polars.PolarDataService;
import com.sap.sailing.domain.tracking.CompleteManeuverCurve;
import com.sap.sailing.domain.tracking.Maneuver;
import com.sap.sailing.domain.tracking.TrackedRace;
import com.sap.sailing.domain.tracking.WindTrack;
import com.sap.sailing.domain.tracking.WindWithConfidence;
@@ -34,10 +37,16 @@ import com.sap.sailing.windestimation.aggregator.msthmm.MstBestPathsCalculatorIm
import com.sap.sailing.windestimation.aggregator.msthmm.MstGraphExportHelper;
import com.sap.sailing.windestimation.aggregator.msthmm.MstGraphLevel;
import com.sap.sailing.windestimation.aggregator.msthmm.MstManeuverGraphGenerator.MstManeuverGraphComponents;
import com.sap.sailing.windestimation.data.ManeuverCategory;
import com.sap.sailing.windestimation.data.ManeuverTypeForClassification;
import com.sap.sailing.windestimation.data.ManeuverWithEstimatedType;
import com.sap.sailing.windestimation.data.SimpleManeuverForEstimation;
import com.sap.sailing.windestimation.data.SimpleManeuverForEstimationImpl;
import com.sap.sailing.windestimation.data.SimpleManeuverWithEstimatedType;
import com.sap.sailing.windestimation.data.SimpleManeuverWithEstimatedTypeImpl;
import com.sap.sailing.windestimation.data.transformer.ManeuverForEstimationTransformer;
import com.sap.sailing.windestimation.model.classifier.maneuver.ManeuverClassifiersCache;
import com.sap.sailing.windestimation.model.regressor.twdtransition.GaussianBasedTwdTransitionDistributionCache;
import com.sap.sailing.windestimation.windinference.DummyBasedTwsCalculatorImpl;
import com.sap.sailing.windestimation.windinference.MiddleCourseBasedTwdCalculatorImpl;
import com.sap.sailing.windestimation.windinference.PolarsBasedTwsCalculatorImpl;
import com.sap.sailing.windestimation.windinference.WindTrackCalculator;
@@ -45,7 +54,6 @@ import com.sap.sailing.windestimation.windinference.WindTrackCalculatorImpl;
import com.sap.sse.common.Position;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.Util.Pair;
import com.sap.sse.common.Util.Triple;
import com.sap.sse.util.ThreadPoolUtil;
/**
@@ -64,6 +72,17 @@ public class IncrementalMstHmmWindEstimationForTrackedRace implements Incrementa
private static final Logger logger = Logger.getLogger(IncrementalMstHmmWindEstimationForTrackedRace.class.getName());
private static final double WIND_COURSE_TOLERANCE_IN_DEGREES_TO_IGNORE_FOR_REUSE = 1.0;
/**
* Used by {@link PreClassifiedUpdate} to apply the same maneuver-eligibility filter that the graph path applies via
* {@link CompleteManeuverCurveToManeuverForEstimationConverter}: maneuvers whose direction change (on either the
* main curve or the stable-speed-and-course boundaries) is not classified as {@link ManeuverCategory#REGULAR} are
* excluded from wind-track contribution. Without this filter, the DB-load / re-adaptation hand-off would contribute
* wind fixes for maneuvers that the graph path silently skips, causing the estimator's track to contain extras at
* positions/timepoints the graph path never produces. See bug6241.
*/
private static final ManeuverForEstimationTransformer maneuverEligibilityFilter = new ManeuverForEstimationTransformer();
private final IncrementalMstManeuverGraphGenerator mstManeuverGraphGenerator;
private final MstBestPathsCalculator bestPathsCalculator;
private final WindTrackCalculator windTrackCalculator;
@@ -75,14 +94,28 @@ public class IncrementalMstHmmWindEstimationForTrackedRace implements Incrementa
private final static Executor recalculator = ThreadPoolUtil.INSTANCE.getDefaultBackgroundTaskThreadPoolExecutor();
/**
* Contains update requests scheduled by {@link #newManeuverSpotsDetected(Competitor, Iterable, TrackTimeInfo)}. Adding
* and removing elements must {@code synchronize} on this {@link IncrementalMstHmmWindEstimationForTrackedRace} object.
* When adding an element and {@link #updateTask} is {@code null}, a new update task must be scheduled and assigned
* to {@link #updateTask} while holding this object's monitor ({@code synchronized}). When checking for the next
* element to be removed and then deciding to terminate and clear the {@link #updateTask}, also this object's
* monitor must be held.
* A pending update to be processed by a {@link GraphRecalculationTask}. Two flavors are supported:
* {@link NewSpotsUpdate} (from {@link #newManeuverSpotsDetected(Competitor, Iterable, TrackTimeInfo)}) which feeds
* raw {@link CompleteManeuverCurve}s through the MST/HMM graph before reconciliation, and
* {@link PreClassifiedUpdate} (from {@link #alreadyClassifiedManeuversAvailable(Competitor, Iterable)}) which skips
* the graph because the maneuvers already carry their type.
*/
private final ConcurrentLinkedDeque<Triple<Competitor, Iterable<CompleteManeuverCurve>, TrackTimeInfo>> updateQueue;
private interface PendingUpdate {
void apply();
}
/**
* Contains update requests scheduled by
* {@link #newManeuverSpotsDetected(Competitor, Iterable, TrackTimeInfo)} and
* {@link #alreadyClassifiedManeuversAvailable(Competitor, Iterable)}. Adding and removing
* elements must {@code synchronize} on this
* {@link IncrementalMstHmmWindEstimationForTrackedRace} object. When adding an element and
* {@link #updateTask} is {@code null}, a new update task must be scheduled and assigned to
* {@link #updateTask} while holding this object's monitor ({@code synchronized}). When
* checking for the next element to be removed and then deciding to terminate and clear the
* {@link #updateTask}, also this object's monitor must be held.
*/
private final ConcurrentLinkedDeque<PendingUpdate> updateQueue;
/**
* A task that is scheduled with the {@link #recalculator} and is set to a non-{@code null} task if and only if one
@@ -97,23 +130,32 @@ public class IncrementalMstHmmWindEstimationForTrackedRace implements Incrementa
PolarDataService polarDataService, long millisecondsOverWhichToAverage,
ManeuverClassifiersCache maneuverClassifiersCache,
GaussianBasedTwdTransitionDistributionCache gaussianBasedTwdTransitionDistributionCache) {
// bug6241: enforced by callers (WindEstimationFactoryServiceImpl and the
// per-race installation coordination in RacingEventServiceImpl) so that the
// estimator captures a live polar service. Without polars, classification and
// wind-speed inference degrade to null and Dummy-based, which is what produced
// the incorrect wind estimation from maneuvers described by this bug.
if (polarDataService == null) {
throw new IllegalArgumentException(
"polarDataService must not be null; see bug6241 and WindEstimationFactoryServiceImpl."
+ "createIncrementalWindEstimationTrack for the required precondition on the tracked race.");
}
this.estimatedWindTrack = new WindTrackWithConfidenceForEachWindFixImpl(millisecondsOverWhichToAverage,
WindSourceType.MANEUVER_BASED_ESTIMATION.getBaseConfidence(),
WindSourceType.MANEUVER_BASED_ESTIMATION.useSpeed() && polarDataService != null,
WindSourceType.MANEUVER_BASED_ESTIMATION.useSpeed(),
IncrementalMstHmmWindEstimationForTrackedRace.class.getSimpleName()+" "+
trackedRace.getRaceIdentifier(), false, windTrackWithConfidences);
this.updateQueue = new ConcurrentLinkedDeque<>();
this.trackedRace = trackedRace;
this.windSource = windSource;
DistanceAndDurationAwareWindTransitionProbabilitiesCalculator transitionProbabilitiesCalculator = new DistanceAndDurationAwareWindTransitionProbabilitiesCalculator(
final DistanceAndDurationAwareWindTransitionProbabilitiesCalculator transitionProbabilitiesCalculator = new DistanceAndDurationAwareWindTransitionProbabilitiesCalculator(
gaussianBasedTwdTransitionDistributionCache, true);
this.mstManeuverGraphGenerator = new IncrementalMstManeuverGraphGenerator(
new CompleteManeuverCurveToManeuverForEstimationConverter(trackedRace, polarDataService),
transitionProbabilitiesCalculator, maneuverClassifiersCache);
this.bestPathsCalculator = new MstBestPathsCalculatorImpl(transitionProbabilitiesCalculator);
this.windTrackCalculator = new WindTrackCalculatorImpl(new MiddleCourseBasedTwdCalculatorImpl(),
polarDataService == null ? new DummyBasedTwsCalculatorImpl()
: new PolarsBasedTwsCalculatorImpl(polarDataService));
new PolarsBasedTwsCalculatorImpl(polarDataService));
}
@Override
@@ -137,16 +179,18 @@ public class IncrementalMstHmmWindEstimationForTrackedRace implements Incrementa
* {@link IncrementalMstHmmWindEstimationForTrackedRace#mstManeuverGraphGenerator maneuver graph generator} before
* updating the wind estimations based on maneuvers. Adding a maneuver spot to the maneuver graph generator
* synchronizes on that generator and therefore doing the same for multiple competitors for the same race from
* multiple threads will block all but one of those threads.<p>
* multiple threads will block all but one of those threads.
* <p>
*
* With this task, a separate queue ({@link IncrementalMstHmmWindEstimationForTrackedRace#updateQueue}) is
* used to pull updates from that queue and add them to the maneuver graph generator, then processing the updates
* to generate new wind estimations.<p>
* With this task, a separate queue ({@link IncrementalMstHmmWindEstimationForTrackedRace#updateQueue}) is used to
* pull updates from that queue and add them to the maneuver graph generator, then processing the updates to
* generate new wind estimations.
* <p>
*
* A task of this type repeats this until the queue is empty and then terminates. Trying to fetch the next update
* from the queue, deciding whether to terminate this task, and adding the next update to the queue are all synchronized
* on the enclosing {@link IncrementalMstHmmWindEstimationForTrackedRace} object, ensuring that exactly one task
* exists for the enclosing instance whenever an update is enqueued or processing.
* from the queue, deciding whether to terminate this task, and adding the next update to the queue are all
* synchronized on the enclosing {@link IncrementalMstHmmWindEstimationForTrackedRace} object, ensuring that exactly
* one task exists for the enclosing instance whenever an update is enqueued or processing.
*
* @author Axel Uhl (d043530)
*
@@ -155,7 +199,7 @@ public class IncrementalMstHmmWindEstimationForTrackedRace implements Incrementa
@Override
public void run() {
logger.fine(()->"This is a new recalculation task for "+trackedRace.getRaceIdentifier());
Triple<Competitor, Iterable<CompleteManeuverCurve>, TrackTimeInfo> nextUpdate;
PendingUpdate nextUpdate;
do {
synchronized (IncrementalMstHmmWindEstimationForTrackedRace.this) {
nextUpdate = updateQueue.poll();
@@ -167,15 +211,34 @@ public class IncrementalMstHmmWindEstimationForTrackedRace implements Incrementa
}
if (nextUpdate != null) {
logger.fine(()->"Handling next update task for "+trackedRace.getRaceIdentifier()+"; still "+updateQueue.size()+" tasks in the queue");
updateGraphGenerator(nextUpdate.getA(), nextUpdate.getB(), nextUpdate.getC());
nextUpdate.apply();
}
} while (nextUpdate != null);
}
private void updateGraphGenerator(Competitor competitor, Iterable<CompleteManeuverCurve> newManeuvers, TrackTimeInfo trackTimeInfo) {
List<ManeuverWithEstimatedType> maneuversWithEstimatedType = new ArrayList<>();
}
/**
* The classic path used by incremental maneuver detection:
* {@link IncrementalMstHmmWindEstimationForTrackedRace#newManeuverSpotsDetected(Competitor, Iterable, TrackTimeInfo)}
* enqueues one of these per notification, and its {@link #apply()} runs the raw {@link CompleteManeuverCurve}s
* through the MST/HMM graph before reconciling the resulting wind fixes into the estimated wind track.
*/
private class NewSpotsUpdate implements PendingUpdate {
private final Competitor competitor;
private final Iterable<CompleteManeuverCurve> newManeuvers;
private final TrackTimeInfo trackTimeInfo;
NewSpotsUpdate(final Competitor competitor, final Iterable<CompleteManeuverCurve> newManeuvers,
final TrackTimeInfo trackTimeInfo) {
this.competitor = competitor;
this.newManeuvers = newManeuvers;
this.trackTimeInfo = trackTimeInfo;
}
@Override
public void apply() {
final MstManeuverGraphComponents graphComponents;
for (CompleteManeuverCurve newManeuverSpot : newManeuvers) {
for (final CompleteManeuverCurve newManeuverSpot : newManeuvers) {
// The add(...) method on IncrementalMstManeuverGraphGenerator is synchronized on the one instance per race.
// But this newManeuverSpotsDetected method may be called by separate threads for different competitors.
// If the calculation takes long, many pooled threads may block, reducing throughput to sequential
@@ -196,69 +259,230 @@ public class IncrementalMstHmmWindEstimationForTrackedRace implements Incrementa
}
}
if (graphComponents != null) {
Iterable<GraphLevelInference<MstGraphLevel>> bestPath = bestPathsCalculator.getBestNodes(graphComponents);
for (GraphLevelInference<MstGraphLevel> inference : bestPath) {
ManeuverWithEstimatedType maneuverWithEstimatedType = new ManeuverWithEstimatedType(
final List<SimpleManeuverWithEstimatedType<? extends SimpleManeuverForEstimation>> maneuversWithEstimatedType = new ArrayList<>();
final Iterable<GraphLevelInference<MstGraphLevel>> bestPath = bestPathsCalculator.getBestNodes(graphComponents);
for (final GraphLevelInference<MstGraphLevel> inference : bestPath) {
final SimpleManeuverWithEstimatedType<? extends SimpleManeuverForEstimation> maneuverWithEstimatedType = new ManeuverWithEstimatedType(
inference.getGraphLevel().getManeuver(), inference.getGraphNode().getManeuverType(),
inference.getConfidence());
maneuversWithEstimatedType.add(maneuverWithEstimatedType);
}
Collections.sort(maneuversWithEstimatedType);
List<WindWithConfidence<Pair<Position, TimePoint>>> newWindTrack = windTrackCalculator
.getWindTrackFromManeuverClassifications(maneuversWithEstimatedType);
Map<Pair<Position, TimePoint>, WindWithConfidence<Pair<Position, TimePoint>>> newWindTrackMap = new HashMap<>(
newWindTrack.size());
for (WindWithConfidence<Pair<Position, TimePoint>> wind : newWindTrack) {
newWindTrackMap.put(wind.getRelativeTo(), wind);
}
// Now we're adjusting windTrackWithConfidences and the estimatedWindTrack incrementally and consistently
// so that afterwards the contents match the newWindTrack
// FIXME bug6026: the "consistently" seems to be causing problems when a new wind estimation model is ingested
List<WindWithConfidence<Pair<Position, TimePoint>>> windFixesToAdd = new ArrayList<>();
estimatedWindTrack.lockForWrite();
try {
for (Iterator<WindWithConfidence<Pair<Position, TimePoint>>> previousWindFixesIterator = windTrackWithConfidences
.values().iterator(); previousWindFixesIterator.hasNext();) {
WindWithConfidence<Pair<Position, TimePoint>> previousWind = previousWindFixesIterator.next();
WindWithConfidence<Pair<Position, TimePoint>> newWind = newWindTrackMap
.get(previousWind.getRelativeTo());
if (newWind == null) {
previousWindFixesIterator.remove();
trackedRace.removeWind(previousWind.getObject(), windSource);
} else if (!isWindNearlySame(newWind.getObject(), previousWind.getObject())) {
previousWindFixesIterator.remove();
trackedRace.removeWind(previousWind.getObject(), windSource);
windFixesToAdd.add(newWind);
}
}
for (WindWithConfidence<Pair<Position, TimePoint>> newWind : newWindTrack) {
if (!windTrackWithConfidences.containsKey(newWind.getRelativeTo())) {
windFixesToAdd.add(newWind);
}
}
for (WindWithConfidence<Pair<Position, TimePoint>> windFixToAdd : windFixesToAdd) {
windTrackWithConfidences.put(windFixToAdd.getRelativeTo(), windFixToAdd);
trackedRace.recordWind(windFixToAdd.getObject(), windSource, false);
}
} finally {
estimatedWindTrack.unlockAfterWrite();
}
applyManeuverClassificationsToWindTrack(maneuversWithEstimatedType);
}
}
}
/**
* Enqueues an update into {@link #updateQueue} and ensures that a {@link GraphRecalculationTask} exists to handle it.
* The method is {@code synchronized} to implement the choreography with {@link GraphRecalculationTask} which also synchronizes
* on this object while trying to fetch the next update from the {@link #updateQueue} and if not having retrieved an element
* setting {@link #updateTask} to {@code null} and terminating the task.
*
* @see #updateTask
* The DB-load / re-adaptation path used by
* {@link IncrementalMstHmmWindEstimationForTrackedRace#alreadyClassifiedManeuversAvailable(Competitor, Iterable)}:
* each maneuver already carries its {@link Maneuver#getType() type}, so we skip the MST/HMM graph and go straight
* to converting them into wind fixes and adding them into the wind track. To stay consistent with what the graph
* path would produce for the same race, this update applies the same maneuver-eligibility filter that
* {@link CompleteManeuverCurveToManeuverForEstimationConverter#convertCleanManeuverSpotToManeuverForEstimation(CompleteManeuverCurve, CompleteManeuverCurve, CompleteManeuverCurve, Competitor, TrackTimeInfo)}
* applies (via {@link ManeuverForEstimationTransformer#isManeuverEligibleForAnalysis(double, double)}), and uses the same
* {@code middleCourse} accessor that the graph-path adapter uses (see
* {@link ConvertableManeuverForEstimationAdapterForCompleteManeuverCurve#getMiddleCourse()}, which reads from
* {@link Maneuver#getManeuverCurveWithStableSpeedAndCourseBoundaries()}).
* <p>
*
* Unlike {@link NewSpotsUpdate}, this update is scheduled <em>per competitor</em>: the caller (e.g.
* {@code TrackedRaceImpl.feedAlreadyKnownManeuversToWindEstimation}) fires one update per competitor with just that
* competitor's maneuvers. Consequently, the corresponding {@code newWindTrack} produced by
* {@link #windTrackCalculator} covers only that competitor's contribution -- not the whole race. The reconciliation
* body of {@link #applyManeuverClassificationsToWindTrack} is designed for whole-race inputs (produced by the MST
* graph across all competitors) and would remove all other competitors' fixes on every per-competitor call. This
* update therefore uses the additive-only helper {@link #addManeuverClassificationsToWindTrack} which inserts
* missing fixes without removing existing ones. See bug6241.
*/
@Override
public synchronized void newManeuverSpotsDetected(Competitor competitor, Iterable<CompleteManeuverCurve> newManeuvers, TrackTimeInfo trackTimeInfo) {
private class PreClassifiedUpdate implements PendingUpdate {
private final Competitor competitor;
private final Iterable<Maneuver> maneuvers;
PreClassifiedUpdate(final Competitor competitor, final Iterable<Maneuver> maneuvers) {
this.competitor = competitor;
this.maneuvers = maneuvers;
}
@Override
public void apply() {
final BoatClass boatClass = trackedRace.getRace().getBoatClass();
final List<SimpleManeuverWithEstimatedType<? extends SimpleManeuverForEstimation>> maneuversWithEstimatedType = new ArrayList<>();
for (final Maneuver maneuver : maneuvers) {
final ManeuverTypeForClassification classification = mapManeuverType(maneuver.getType());
final boolean isEligible = classification != null
&& maneuverEligibilityFilter.isManeuverEligibleForAnalysis(
maneuver.getMainCurveBoundaries().getDirectionChangeInDegrees(),
maneuver.getManeuverCurveWithStableSpeedAndCourseBoundaries().getDirectionChangeInDegrees());
if (isEligible) {
// DB-loaded maneuvers are considered clean: they are the ones that were persisted
// after having been accepted as valid, non-penalty-circle maneuvers.
// middleCourse is read from the stable-speed-and-course boundaries to match the
// graph-path adapter (see the class-level Javadoc).
final SimpleManeuverForEstimation forEstimation = new SimpleManeuverForEstimationImpl(
maneuver.getTimePoint(), maneuver.getPosition(),
maneuver.getManeuverCurveWithStableSpeedAndCourseBoundaries().getMiddleCourse(),
maneuver.getSpeedWithBearingBefore(), maneuver.getSpeedWithBearingAfter(),
/* clean */ true, boatClass);
// Full confidence: these maneuvers came from the persistent cache with their
// type already resolved by a prior computation that had all inputs available.
maneuversWithEstimatedType.add(new SimpleManeuverWithEstimatedTypeImpl<>(forEstimation,
classification, /* confidence */ 1.0));
}
}
if (!maneuversWithEstimatedType.isEmpty()) {
Collections.sort(maneuversWithEstimatedType);
logger.fine(()->"Feeding "+maneuversWithEstimatedType.size()+" pre-classified maneuvers from competitor "
+competitor+" into the wind estimation of race "+trackedRace.getRaceIdentifier());
addManeuverClassificationsToWindTrack(maneuversWithEstimatedType);
}
}
}
/**
* Turns a list of already-typed maneuvers into wind fixes via {@link #windTrackCalculator} and merges them into the
* {@link #estimatedWindTrack} incrementally and consistently, so that afterwards the contents match the
* newly-produced wind track. Extracted from the previous inline body of
* {@code GraphRecalculationTask.updateGraphGenerator} so that both the {@link NewSpotsUpdate MST/HMM path} and the
* {@link PreClassifiedUpdate DB-load path} share exactly the same reconciliation semantics.
* <p>
* FIXME bug6026: the "consistently" seems to be causing problems when a new wind estimation model is ingested.
*/
private void applyManeuverClassificationsToWindTrack(
final List<SimpleManeuverWithEstimatedType<? extends SimpleManeuverForEstimation>> maneuversWithEstimatedType) {
final List<WindWithConfidence<Pair<Position, TimePoint>>> newWindTrack = windTrackCalculator
.getWindTrackFromManeuverClassifications(maneuversWithEstimatedType);
final Map<Pair<Position, TimePoint>, WindWithConfidence<Pair<Position, TimePoint>>> newWindTrackMap = new HashMap<>(
newWindTrack.size());
for (final WindWithConfidence<Pair<Position, TimePoint>> wind : newWindTrack) {
newWindTrackMap.put(wind.getRelativeTo(), wind);
}
// bug6241: mutate the internal windTrackWithConfidences map under the write lock, but
// fire the corresponding trackedRace.removeWind/recordWind notifications OUTSIDE it.
// Those downstream calls trigger maneuver-cache recalculation, which is the intended
// bootstrap: NN+HMM pre-classifies spots -> wind fixes on the estimation track -> the
// recalc re-runs detection so existing spots can be re-typed against the updated wind
// picture. The re-run reads other wind sources and, in isManeuverSpotWindNearlySame,
// also this estimation track (that read is what triggers a re-typing when the
// estimation track changes). Holding the write lock across the downstream calls
// therefore creates a lock-order inversion with those readers via SmartFutureCache
// internals: the estimator holds write on the wind track and then acquires internal
// state inside SmartFutureCache.triggerUpdate, while concurrent detection tasks
// (running from SmartFutureCache callbacks) try to acquire the wind track's read
// lock. Releasing the wind-track write lock before firing the downstream side-effects
// removes the inversion. The invariant guarded by the lock -- the contents of
// windTrackWithConfidences -- is fully updated by the time we release.
final List<WindWithConfidence<Pair<Position, TimePoint>>> windFixesToRemove = new ArrayList<>();
final List<WindWithConfidence<Pair<Position, TimePoint>>> windFixesToAdd = new ArrayList<>();
estimatedWindTrack.lockForWrite();
try {
for (Iterator<WindWithConfidence<Pair<Position, TimePoint>>> previousWindFixesIterator = windTrackWithConfidences
.values().iterator(); previousWindFixesIterator.hasNext();) {
final WindWithConfidence<Pair<Position, TimePoint>> previousWind = previousWindFixesIterator.next();
final WindWithConfidence<Pair<Position, TimePoint>> newWind = newWindTrackMap
.get(previousWind.getRelativeTo());
if (newWind == null) {
previousWindFixesIterator.remove();
windFixesToRemove.add(previousWind);
} else if (!isWindNearlySame(newWind.getObject(), previousWind.getObject())) {
previousWindFixesIterator.remove();
windFixesToRemove.add(previousWind);
windFixesToAdd.add(newWind);
}
}
for (final WindWithConfidence<Pair<Position, TimePoint>> newWind : newWindTrack) {
if (!windTrackWithConfidences.containsKey(newWind.getRelativeTo())) {
windFixesToAdd.add(newWind);
}
}
for (final WindWithConfidence<Pair<Position, TimePoint>> windFixToAdd : windFixesToAdd) {
windTrackWithConfidences.put(windFixToAdd.getRelativeTo(), windFixToAdd);
}
} finally {
estimatedWindTrack.unlockAfterWrite();
}
// Apply the collected deltas to the tracked race now that we no longer hold the
// estimated wind track's write lock.
for (final WindWithConfidence<Pair<Position, TimePoint>> windFixToRemove : windFixesToRemove) {
trackedRace.removeWind(windFixToRemove.getObject(), windSource);
}
for (final WindWithConfidence<Pair<Position, TimePoint>> windFixToAdd : windFixesToAdd) {
trackedRace.recordWind(windFixToAdd.getObject(), windSource, false);
}
}
/**
* Additive-only counterpart of {@link #applyManeuverClassificationsToWindTrack}: inserts any wind fixes for
* {@code maneuversWithEstimatedType} that are not already present in {@link #windTrackWithConfidences}, and does
* <em>not</em> remove any pre-existing fixes. Used by the DB-load / re-adaptation path
* ({@link PreClassifiedUpdate}) which invokes per competitor: the corresponding {@code newWindTrack} covers only
* one competitor's contribution, so the full reconciliation of {@link #applyManeuverClassificationsToWindTrack}
* would clobber fixes belonging to other competitors on every call. Additive semantics are correct here because the
* DB load supplies the whole race's typed maneuvers across the successive per-competitor calls, and the union of
* those per-competitor wind fixes is the intended track content. See bug6241.
* <p>
*
* As with {@link #applyManeuverClassificationsToWindTrack}, the mutation of {@link #windTrackWithConfidences}
* happens under the wind track's write lock and the corresponding {@code trackedRace.recordWind} notifications are
* fired outside the lock to avoid a lock-order inversion with concurrent maneuver detection reading the wind track.
*/
private void addManeuverClassificationsToWindTrack(
final List<SimpleManeuverWithEstimatedType<? extends SimpleManeuverForEstimation>> maneuversWithEstimatedType) {
final List<WindWithConfidence<Pair<Position, TimePoint>>> newWindTrack = windTrackCalculator
.getWindTrackFromManeuverClassifications(maneuversWithEstimatedType);
final List<WindWithConfidence<Pair<Position, TimePoint>>> windFixesToAdd = new ArrayList<>();
estimatedWindTrack.lockForWrite();
try {
for (final WindWithConfidence<Pair<Position, TimePoint>> newWind : newWindTrack) {
if (!windTrackWithConfidences.containsKey(newWind.getRelativeTo())) {
windTrackWithConfidences.put(newWind.getRelativeTo(), newWind);
windFixesToAdd.add(newWind);
}
}
} finally {
estimatedWindTrack.unlockAfterWrite();
}
for (final WindWithConfidence<Pair<Position, TimePoint>> windFixToAdd : windFixesToAdd) {
trackedRace.recordWind(windFixToAdd.getObject(), windSource, false);
}
}
/**
* Maps a domain {@link ManeuverType} to its wind-estimation counterpart, or {@code null} if the type doesn't
* correspond to a maneuver kind the wind-track calculator can produce a wind fix from (e.g., PENALTY_CIRCLE or
* UNKNOWN). Matches the mapping in ManeuverWithEstimatedTypeFromManeuverTest.
*/
private static ManeuverTypeForClassification mapManeuverType(final ManeuverType type) {
final ManeuverTypeForClassification result;
switch (type) {
case BEAR_AWAY:
result = ManeuverTypeForClassification.BEAR_AWAY;
break;
case HEAD_UP:
result = ManeuverTypeForClassification.HEAD_UP;
break;
case JIBE:
result = ManeuverTypeForClassification.JIBE;
break;
case TACK:
result = ManeuverTypeForClassification.TACK;
break;
case PENALTY_CIRCLE:
case UNKNOWN:
default:
result = null;
break;
}
return result;
}
/**
* Enqueues the given {@link PendingUpdate} and ensures that a {@link GraphRecalculationTask} exists to process it.
* Callers must not hold this object's monitor.
*/
private synchronized void submit(final PendingUpdate update) {
final boolean queueWasEmpty = updateQueue.isEmpty();
updateQueue.add(new Triple<>(competitor, newManeuvers, trackTimeInfo));
updateQueue.add(update);
logger.fine(()->"Currently "+updateQueue.size()+" update jobs enqueued for race "+trackedRace.getRaceIdentifier());
if (queueWasEmpty && updateTask == null) {
logger.fine(()->"Creating a new recalculation task for "+trackedRace.getRaceIdentifier());
@@ -268,6 +492,29 @@ public class IncrementalMstHmmWindEstimationForTrackedRace implements Incrementa
}
}
/**
* Enqueues an update into {@link #updateQueue} and ensures that a {@link GraphRecalculationTask} exists to handle
* it. The method is {@code synchronized} to implement the choreography with {@link GraphRecalculationTask} which
* also synchronizes on this object while trying to fetch the next update from the {@link #updateQueue} and if not
* having retrieved an element setting {@link #updateTask} to {@code null} and terminating the task.
*
* @see #updateTask
*/
@Override
public void newManeuverSpotsDetected(final Competitor competitor, final Iterable<CompleteManeuverCurve> newManeuvers,
final TrackTimeInfo trackTimeInfo) {
submit(new NewSpotsUpdate(competitor, newManeuvers, trackTimeInfo));
}
/**
* Enqueues an already-classified-maneuvers hand-off; see
* {@link IncrementalWindEstimation#alreadyClassifiedManeuversAvailable(Competitor, Iterable)}.
*/
@Override
public void alreadyClassifiedManeuversAvailable(final Competitor competitor, final Iterable<Maneuver> maneuvers) {
submit(new PreClassifiedUpdate(competitor, maneuvers));
}
private boolean isWindNearlySame(Wind oneWind, Wind otherWind) {
double bearingInDegrees = oneWind.getBearing().getDifferenceTo(otherWind.getBearing()).abs().getDegrees();
if (bearingInDegrees > WIND_COURSE_TOLERANCE_IN_DEGREES_TO_IGNORE_FOR_REUSE) {
@@ -280,5 +527,4 @@ public class IncrementalMstHmmWindEstimationForTrackedRace implements Incrementa
public WindSource getWindSource() {
return windSource;
}
}
@@ -70,7 +70,24 @@ public class WindEstimationFactoryServiceImpl extends
@Override
public IncrementalWindEstimation createIncrementalWindEstimationTrack(TrackedRace trackedRace) {
IncrementalWindEstimation windEstimation = new IncrementalMstHmmWindEstimationForTrackedRace(trackedRace,
// bug6241: the incremental wind estimation is only useful when a polar-data service is
// available on the tracked race, because the estimator captures that service at
// construction time and uses it to classify maneuvers (via
// CompleteManeuverCurveToManeuverForEstimationConverter which asks the polar service
// for target tack and jibe angles) and to infer wind speed (via
// PolarsBasedTwsCalculatorImpl). Without polars the estimator would produce speedless
// wind fixes and use null polar angles for classification, which is what the
// symptoms of bug6241 described. Callers coordinating the estimator's construction
// are expected to have waited for the polar service to be installed on the race and
// for the polar-data loading pipeline to have drained this race's fixes (see
// RacingEventServiceImpl.scheduleWindEstimationInstallation) before invoking this
// factory method.
if (trackedRace.getPolarDataService() == null) {
throw new IllegalStateException(
"Cannot create IncrementalWindEstimation for race " + trackedRace.getRaceIdentifier()
+ " because its PolarDataService is not set. See bug6241.");
}
final IncrementalWindEstimation windEstimation = new IncrementalMstHmmWindEstimationForTrackedRace(trackedRace,
new WindSourceImpl(WindSourceType.MANEUVER_BASED_ESTIMATION), trackedRace.getPolarDataService(),
trackedRace.getMillisecondsOverWhichToAverageWind(), maneuverClassifiersCache,
gaussianBasedTwdTransitionDistributionCache);
@@ -1,6 +1,7 @@
package com.sap.sailing.windestimation.windinference;
import com.sap.sailing.windestimation.data.ManeuverForEstimation;
import com.sap.sailing.windestimation.data.SimpleManeuverForEstimation;
import com.sap.sse.common.Bearing;
import com.sap.sse.common.Speed;
@@ -14,7 +15,7 @@ import com.sap.sse.common.Speed;
public class DummyBasedTwsCalculatorImpl implements TwsFromManeuverCalculator {
@Override
public Speed getWindSpeed(ManeuverForEstimation maneuver, Bearing windCourse) {
public Speed getWindSpeed(SimpleManeuverForEstimation maneuver, Bearing windCourse) {
return Speed.NULL;
}
@@ -1,8 +1,8 @@
package com.sap.sailing.windestimation.windinference;
import com.sap.sailing.windestimation.data.ManeuverForEstimation;
import com.sap.sailing.windestimation.data.ManeuverTypeForClassification;
import com.sap.sailing.windestimation.data.ManeuverWithEstimatedType;
import com.sap.sailing.windestimation.data.SimpleManeuverForEstimation;
import com.sap.sailing.windestimation.data.SimpleManeuverWithEstimatedType;
import com.sap.sse.common.Bearing;
/**
@@ -17,11 +17,11 @@ public class MiddleCourseBasedTwdCalculatorImpl implements TwdFromManeuverCalcul
private static final long serialVersionUID = -7920503233105279148L;
@Override
public Bearing getTwd(ManeuverWithEstimatedType maneuverWithEstimatedType) {
public Bearing getTwd(SimpleManeuverWithEstimatedType<? extends SimpleManeuverForEstimation> maneuverWithEstimatedType) {
Bearing twd = null;
if (maneuverWithEstimatedType.getManeuverType() == ManeuverTypeForClassification.TACK
|| maneuverWithEstimatedType.getManeuverType() == ManeuverTypeForClassification.JIBE) {
ManeuverForEstimation maneuver = maneuverWithEstimatedType.getManeuver();
SimpleManeuverForEstimation maneuver = maneuverWithEstimatedType.getManeuver();
twd = maneuver.getMiddleCourse();
if (maneuverWithEstimatedType.getManeuverType() == ManeuverTypeForClassification.JIBE) {
twd = twd.reverse();
@@ -2,7 +2,7 @@ package com.sap.sailing.windestimation.windinference;
import com.sap.sailing.domain.polars.PolarDataService;
import com.sap.sailing.windestimation.aggregator.polarsfitting.PolarsFittingWindEstimation;
import com.sap.sailing.windestimation.data.ManeuverForEstimation;
import com.sap.sailing.windestimation.data.SimpleManeuverForEstimation;
import com.sap.sse.common.Bearing;
import com.sap.sse.common.Speed;
@@ -21,7 +21,7 @@ public class PolarsBasedTwsCalculatorImpl implements TwsFromManeuverCalculator {
}
@Override
public Speed getWindSpeed(ManeuverForEstimation maneuver, Bearing windCourse) {
public Speed getWindSpeed(SimpleManeuverForEstimation maneuver, Bearing windCourse) {
return polarsFittingWindEstimation.getWindSpeed(maneuver, windCourse);
}
@@ -2,7 +2,8 @@ package com.sap.sailing.windestimation.windinference;
import java.io.Serializable;
import com.sap.sailing.windestimation.data.ManeuverWithEstimatedType;
import com.sap.sailing.windestimation.data.SimpleManeuverForEstimation;
import com.sap.sailing.windestimation.data.SimpleManeuverWithEstimatedType;
import com.sap.sse.common.Bearing;
/**
@@ -20,6 +21,6 @@ public interface TwdFromManeuverCalculator extends Serializable {
* The maneuver with its estimated type from which TWD will be derived
* @return Inverted TWD or {@code null} if no TWD could be determined
*/
Bearing getTwd(ManeuverWithEstimatedType maneuverWithEstimatedType);
Bearing getTwd(SimpleManeuverWithEstimatedType<? extends SimpleManeuverForEstimation> maneuverWithEstimatedType);
}
@@ -1,6 +1,6 @@
package com.sap.sailing.windestimation.windinference;
import com.sap.sailing.windestimation.data.ManeuverForEstimation;
import com.sap.sailing.windestimation.data.SimpleManeuverForEstimation;
import com.sap.sse.common.Bearing;
import com.sap.sse.common.Speed;
@@ -22,6 +22,6 @@ public interface TwsFromManeuverCalculator {
* Inverted TWD
* @return TWS. If TWS is zero, then TWS could be determined.
*/
Speed getWindSpeed(ManeuverForEstimation maneuver, Bearing windCourse);
Speed getWindSpeed(SimpleManeuverForEstimation maneuver, Bearing windCourse);
}
@@ -3,7 +3,8 @@ package com.sap.sailing.windestimation.windinference;
import java.util.List;
import com.sap.sailing.domain.tracking.WindWithConfidence;
import com.sap.sailing.windestimation.data.ManeuverWithEstimatedType;
import com.sap.sailing.windestimation.data.SimpleManeuverForEstimation;
import com.sap.sailing.windestimation.data.SimpleManeuverWithEstimatedType;
import com.sap.sse.common.Position;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.Util.Pair;
@@ -17,6 +18,6 @@ import com.sap.sse.common.Util.Pair;
public interface WindTrackCalculator {
List<WindWithConfidence<Pair<Position, TimePoint>>> getWindTrackFromManeuverClassifications(
List<ManeuverWithEstimatedType> aggregatedManeuverClassifications);
List<? extends SimpleManeuverWithEstimatedType<? extends SimpleManeuverForEstimation>> aggregatedManeuverClassifications);
}
@@ -7,8 +7,8 @@ import com.sap.sailing.domain.common.Wind;
import com.sap.sailing.domain.common.impl.WindImpl;
import com.sap.sailing.domain.tracking.WindWithConfidence;
import com.sap.sailing.domain.tracking.impl.WindWithConfidenceImpl;
import com.sap.sailing.windestimation.data.ManeuverForEstimation;
import com.sap.sailing.windestimation.data.ManeuverWithEstimatedType;
import com.sap.sailing.windestimation.data.SimpleManeuverForEstimation;
import com.sap.sailing.windestimation.data.SimpleManeuverWithEstimatedType;
import com.sap.sailing.windestimation.util.WindUtil;
import com.sap.sse.common.Bearing;
import com.sap.sse.common.Position;
@@ -36,13 +36,13 @@ public class WindTrackCalculatorImpl implements WindTrackCalculator {
@Override
public List<WindWithConfidence<Pair<Position, TimePoint>>> getWindTrackFromManeuverClassifications(
List<ManeuverWithEstimatedType> improvedManeuverClassifications) {
List<? extends SimpleManeuverWithEstimatedType<? extends SimpleManeuverForEstimation>> improvedManeuverClassifications) {
List<WindWithConfidence<Pair<Position, TimePoint>>> windFixes = new ArrayList<>();
for (ManeuverWithEstimatedType maneuverWithEstimatedType : improvedManeuverClassifications) {
for (SimpleManeuverWithEstimatedType<? extends SimpleManeuverForEstimation> maneuverWithEstimatedType : improvedManeuverClassifications) {
Bearing windFrom = twdCalculator.getTwd(maneuverWithEstimatedType);
if (windFrom != null) {
final Bearing windTo = windFrom.reverse();
ManeuverForEstimation maneuver = maneuverWithEstimatedType.getManeuver();
SimpleManeuverForEstimation maneuver = maneuverWithEstimatedType.getManeuver();
Speed avgWindSpeed = twsCalculator.getWindSpeed(maneuver, windTo);
Wind wind = new WindImpl(maneuver.getManeuverPosition(), maneuver.getManeuverTimePoint(),
new KnotSpeedWithBearingImpl(avgWindSpeed.getKnots(), windTo));
@@ -3,8 +3,6 @@ package com.sap.sse.common;
import com.sap.sse.common.impl.KnotSpeedWithBearingImpl;
import com.sap.sse.datamining.annotations.Statistic;
public interface SpeedWithBearing extends Speed {
@Statistic(messageKey="bearing", resultDecimals=1)
Bearing getBearing();
@@ -14,7 +12,9 @@ public interface SpeedWithBearing extends Speed {
* </code>to</code>, how far have we traveled? If <code>to</code> is before </code>from</code>, the speed will be
* applied in reverse.
*/
Position travelTo(Position pos, TimePoint from, TimePoint to);
default Position travelTo(Position pos, TimePoint from, TimePoint to) {
return travelTo(pos, from.until(to));
}
default Position travelTo(Position from, Duration duration) {
return from.translateGreatCircle(getBearing(), travel(duration));
@@ -4,19 +4,14 @@ import com.sap.sse.common.AbstractSpeedImpl;
import com.sap.sse.common.Bearing;
import com.sap.sse.common.CourseChange;
import com.sap.sse.common.Distance;
import com.sap.sse.common.Duration;
import com.sap.sse.common.Position;
import com.sap.sse.common.Speed;
import com.sap.sse.common.SpeedWithBearing;
import com.sap.sse.common.TimePoint;
public abstract class AbstractSpeedWithAbstractBearingImpl extends AbstractSpeedImpl implements SpeedWithBearing {
private static final long serialVersionUID = 6136100417593538013L;
@Override
public Position travelTo(Position pos, TimePoint from, TimePoint to) {
return pos.translateGreatCircle(getBearing(), this.travel(from, to));
}
@Override
public String toString() {
return super.toString()+" to "+getBearing().getDegrees()+"°";
@@ -71,14 +66,11 @@ public abstract class AbstractSpeedWithAbstractBearingImpl extends AbstractSpeed
return new KnotSpeedWithBearingImpl(newSpeedInKnots, newBearing);
}
private final static TimePoint start = new MillisecondsTimePoint(0);
private final static TimePoint end = start.plus(60000);
public static Speed projectTo(SpeedWithBearing speedWithBearing, Position position, Bearing projectTo) {
Position traveledOneMinute = speedWithBearing.travelTo(position, start, end);
Position traveledOneMinute = speedWithBearing.travelTo(position, Duration.ONE_MINUTE);
Position traveledToProjected = traveledOneMinute.projectToLineThrough(position, projectTo);
Distance projectedDistance = position.getDistance(traveledToProjected);
return projectedDistance.inTime(end.asMillis() - start.asMillis());
return projectedDistance.inTime(Duration.ONE_MINUTE);
}
@Override
@@ -16,3 +16,4 @@ Require-Bundle: org.hamcrest;bundle-version="2.2.0",
junit-platform-engine;bundle-version="1.11.3",
org.opentest4j;bundle-version="1.3.0"
Automatic-Module-Name: com.sap.sse.datamining.test
Import-Package: com.sap.sailing.polars.mining
@@ -0,0 +1,262 @@
package com.sap.sse.datamining.impl.components;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import org.junit.jupiter.api.Test;
import com.sap.sailing.polars.mining.AbstractEnrichingProcessor;
import com.sap.sse.datamining.components.AdditionalResultDataBuilder;
import com.sap.sse.datamining.components.Processor;
import com.sap.sse.datamining.components.ProcessorInstruction;
import com.sap.sse.datamining.components.ProcessorInstructionHandler;
import com.sap.sse.datamining.test.util.components.NullProcessor;
import com.sap.sse.util.ThreadPoolUtil;
public class TestWaitingForInstructionsToFinish {
@Test
public void testSimpleProcessorRunsImmediately() {
final Processor<String, Integer> p = new NullProcessor<String, Integer>(String.class, Integer.class);
final boolean[] waitSucceeded = new boolean[1];
p.processElement("Humba");
p.runWhenFinishedProcessing(()->waitSucceeded[0] = true);
assertTrue(waitSucceeded[0]);
}
/**
* Implements a test with the first parallel processor taking a while in the executor to compute its result
* (e.g., blocked on a barrier/latch), but we register the callback on it immediately after calling
* processElement(...). The result receivers haven't yet received the result. This test verifies that the callback
* isn't fired before the result receivers have also processed the result. The test shall fail for
* 4cac492293155f2afd9b0650c7a30ed0914595ba on branch bug6241 because there we first wait for the result receivers
* to have a zero count of unfinished instructions, which they have immediately when they haven't been passed any
* results yet, and then immediately register the callback on the first (entry-point) processor upon reaching zero
* unfinished instructions, which is the case as soon as its result has been forwarded to the result receivers.<p>
*
* To solve this, the waiting would probably need to be reversed. We know that the entry-point processor will
* decrement its unfinished instructions count only *after* invoking processElement(...) on all result receivers,
* which will then already in turn increment theirs.
*/
@Test
public void testEarlyCallbackWhenResultReceiversHaventStartedYet() throws InterruptedException, BrokenBarrierException {
// Set-up:
final Collection<Processor<Integer, ?>> resultReceivers = new ArrayList<>();
final CyclicBarrier barrier = new CyclicBarrier(2);
final CyclicBarrier afterForwardingToResultReceivers = new CyclicBarrier(2);
final CyclicBarrier afterFinishingEntryPoint = new CyclicBarrier(2);
final CyclicBarrier resultReceiverBarrier = new CyclicBarrier(2);
// construct some parallel processor as result receiver, so it processes results it received in executor tasks:
resultReceivers.add(new AbstractEnrichingProcessor<Integer, Integer>(Integer.class, Integer.class,
ThreadPoolUtil.INSTANCE.getDefaultBackgroundTaskThreadPoolExecutor(), Collections.emptySet()) {
@Override
protected Integer enrich(Integer element) {
try {
resultReceiverBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
throw new RuntimeException(e);
}
return element * 2;
}
});
final ProcessorInstructionHandler<?>[] handler = new ProcessorInstructionHandler<?>[1];
final AbstractParallelProcessor<String, Integer> p = new AbstractParallelProcessor<String, Integer>(
String.class, Integer.class, ThreadPoolUtil.INSTANCE.getDefaultBackgroundTaskThreadPoolExecutor(),
resultReceivers) {
@Override
protected ProcessorInstruction<Integer> createInstruction(String element) {
@SuppressWarnings("unchecked")
final ProcessorInstructionHandler<Integer> h = (ProcessorInstructionHandler<Integer>) handler[0];
return new AbstractProcessorInstruction<Integer>(h) {
@Override
protected Integer computeResult() throws Exception {
barrier.await(); // hold back computation in entry-point processor until released by test
return element.length();
}
};
}
@Override
public void instructionSucceeded(Integer result) {
super.instructionSucceeded(result);
try {
afterForwardingToResultReceivers.await();
} catch (InterruptedException | BrokenBarrierException e) {
throw new RuntimeException(e);
}
}
@Override
public synchronized void afterInstructionFinished(ProcessorInstruction<Integer> instruction) {
super.afterInstructionFinished(instruction);
try {
afterFinishingEntryPoint.await();
} catch (InterruptedException | BrokenBarrierException e) {
throw new RuntimeException(e);
}
}
@Override
protected void setAdditionalData(AdditionalResultDataBuilder additionalDataBuilder) {
}
};
handler[0] = p;
// running the test now:
final boolean[] waitSucceeded = new boolean[1];
p.processElement("Humba"); // schedules a background executor task for the instruction that blocks on the barrier
// now register the callback while the result receiver hasn't received anything yet:
p.runWhenFinishedProcessing(()->{
synchronized (waitSucceeded) {
waitSucceeded[0] = true;
waitSucceeded.notifyAll();
}
});
barrier.await(); // this releases the entry-point task's computeResult, so no forwarding to result receivers has taken place so far
afterForwardingToResultReceivers.await(); // after this we know the result receivers have received the result
afterFinishingEntryPoint.await(); // after this we know the entry-point callback would have fired if no pending instructions anymore
assertFalse(waitSucceeded[0]); // because we haven't yet unblocked the barrier for the result receiver
resultReceiverBarrier.await(); // now the result receiver can continue
synchronized (waitSucceeded) {
while (!waitSucceeded[0]) {
waitSucceeded.wait();
}
}
assertTrue(waitSucceeded[0]);
}
@Test
public void testSimpleParallelProcessorWithBlockingResultReceiver() throws InterruptedException, BrokenBarrierException {
final Collection<Processor<Integer, ?>> resultReceivers = new ArrayList<>();
final CountDownLatch barrier = new CountDownLatch(2);
resultReceivers.add(createProcessorUnblockingCyclicBarrier(barrier));
final AbstractParallelProcessor<String, Integer> p = createAbstractParallelProcessor(resultReceivers);
final boolean[] waitSucceeded = new boolean[1];
p.processElement("Humba");
p.runWhenFinishedProcessing(()->new Thread(()->{
try {
barrier.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (waitSucceeded) {
waitSucceeded[0] = true;
waitSucceeded.notifyAll();
}
}).start());
assertFalse(waitSucceeded[0]); // because we haven't yet unblocked the barrier
barrier.countDown();
synchronized (waitSucceeded) {
while (!waitSucceeded[0]) {
waitSucceeded.wait();
}
}
assertTrue(waitSucceeded[0]);
}
@Test
public void testSimpleParallelProcessorWithManyBlockingResultReceivers() throws InterruptedException, BrokenBarrierException {
final int NUMBER_OF_RECEIVERS = 10000;
final int NUMBER_OF_ELEMENTS_TO_PROCESS = 1000;
final Collection<Processor<Integer, ?>> resultReceivers = new ArrayList<>();
final CountDownLatch barrier = new CountDownLatch(NUMBER_OF_RECEIVERS*NUMBER_OF_ELEMENTS_TO_PROCESS+1);
for (int i=0; i<NUMBER_OF_RECEIVERS; i++) {
resultReceivers.add(createProcessorUnblockingCyclicBarrier(barrier));
}
final AbstractParallelProcessor<String, Integer> p = createAbstractParallelProcessor(resultReceivers);
final boolean[] waitSucceeded = new boolean[1];
for (int i=0; i<NUMBER_OF_ELEMENTS_TO_PROCESS; i++) {
p.processElement("Humba");
}
p.runWhenFinishedProcessing(()->new Thread(()->{
try {
barrier.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (waitSucceeded) {
waitSucceeded[0] = true;
waitSucceeded.notifyAll();
}
}).start());
assertFalse(waitSucceeded[0]); // because we haven't yet unblocked the barrier
barrier.countDown();
synchronized (waitSucceeded) {
while (!waitSucceeded[0]) {
waitSucceeded.wait();
}
}
assertTrue(waitSucceeded[0]);
}
private AbstractParallelProcessor<String, Integer> createAbstractParallelProcessor(
final Collection<Processor<Integer, ?>> resultReceivers) {
final ProcessorInstructionHandler<?>[] handler = new ProcessorInstructionHandler<?>[1];
final AbstractParallelProcessor<String, Integer> result = new AbstractParallelProcessor<String, Integer>(
String.class, Integer.class, ThreadPoolUtil.INSTANCE.getDefaultBackgroundTaskThreadPoolExecutor(),
resultReceivers) {
@Override
protected ProcessorInstruction<Integer> createInstruction(String element) {
@SuppressWarnings("unchecked")
final ProcessorInstructionHandler<Integer> h = (ProcessorInstructionHandler<Integer>) handler[0];
return new AbstractProcessorInstruction<Integer>(h) {
@Override
protected Integer computeResult() throws Exception {
return element.length();
}
};
}
@Override
protected void setAdditionalData(AdditionalResultDataBuilder additionalDataBuilder) {
}
};
handler[0] = result;
return result;
}
private AbstractProcessor<Integer, Integer> createProcessorUnblockingCyclicBarrier(final CountDownLatch barrier) {
return new AbstractProcessor<Integer, Integer>(Integer.class, Integer.class) {
@Override
public boolean canProcessElements() {
return true;
}
@Override
public void processElement(Integer element) {
barrier.countDown();
}
@Override
public void onFailure(Throwable failure) {
}
@Override
public void finish() throws InterruptedException {
}
@Override
public boolean isFinished() {
return false;
}
@Override
public void abort() {
}
@Override
public boolean isAborted() {
return false;
}
@Override
public AdditionalResultDataBuilder getAdditionalResultData(AdditionalResultDataBuilder additionalDataBuilder) {
return additionalDataBuilder;
}
};
}
}
@@ -52,4 +52,19 @@ public interface Processor<InputType, ResultType> {
*/
public AdditionalResultDataBuilder getAdditionalResultData(AdditionalResultDataBuilder additionalDataBuilder);
/**
* Enqueues a callback for the event where this processor has finished processing what it has been
* provided so far through calls to {@link #processElement(Object)}. Subclasses, especially those
* working with result receivers and thread pools for parallel processing need to check their dependent
* processors for having finished as well before invoking the callback.<p>
*
* This default implementation immediately invokes the callback, assuming that {@link #processElement(Object)}
* is a synchronous method that does not spawn any background processing.
*
* @param callbackWhenAllLoadedFixesHaveBeenProcessed must not be {@code null}
*/
default void runWhenFinishedProcessing(Runnable callbackWhenAllLoadedFixesHaveBeenProcessed) {
callbackWhenAllLoadedFixesHaveBeenProcessed.run();
}
}
@@ -76,7 +76,6 @@ public abstract class ProcessorQuery<ResultType, DataSourceType> implements Quer
state = QueryState.NOT_STARTED;
this.resultType = resultType;
this.additionalData = additionalData;
resultReceiver = new ProcessResultReceiver();
firstProcessor = createChainAndReturnFirstProcessor(resultReceiver);
}
@@ -1,11 +1,15 @@
package com.sap.sse.datamining.impl.components;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.UnavailableSecurityManagerException;
import org.apache.shiro.subject.Subject;
@@ -19,17 +23,19 @@ public abstract class AbstractParallelProcessor<InputType, ResultType> extends A
implements ProcessorInstructionHandler<ResultType> {
private static final Logger LOGGER = Logger.getLogger(AbstractParallelProcessor.class.getName());
private static final int SLEEP_TIME_DURING_FINISHING = 100;
private static final int SLEEP_TIME_DURING_FINISHING_MILLIS = 100;
private final Processor<ResultType, ?>[] resultReceivers;
private final ExecutorService executor;
private final AtomicInteger unfinishedInstructionsCounter;
private final Set<Runnable> callbacksWhenNoMoreUnfinishedInstructions;
private boolean isFinished = false;
private boolean isAborted = false;
public AbstractParallelProcessor(Class<InputType> inputType, Class<ResultType> resultType, ExecutorService executor, Collection<Processor<ResultType, ?>> resultReceivers) {
super(inputType, resultType);
this.callbacksWhenNoMoreUnfinishedInstructions = Collections.newSetFromMap(new ConcurrentHashMap<Runnable, Boolean>());
this.executor = executor;
@SuppressWarnings("unchecked")
final Processor<ResultType, ?>[] resultReceiversAsArray = (Processor<ResultType, ?>[]) new Processor<?, ?>[resultReceivers.size()];
@@ -82,15 +88,72 @@ public abstract class AbstractParallelProcessor<InputType, ResultType> extends A
}
}
@Override
public void afterInstructionFinished(ProcessorInstruction<ResultType> instruction) {
unfinishedInstructionsCounter.getAndDecrement();
}
protected Processor<ResultType, ?>[] getResultReceivers() {
return resultReceivers;
}
@Override
public synchronized void afterInstructionFinished(ProcessorInstruction<ResultType> instruction) {
if (unfinishedInstructionsCounter.decrementAndGet() == 0) {
for (final Runnable callback : callbacksWhenNoMoreUnfinishedInstructions) {
callback.run();
}
callbacksWhenNoMoreUnfinishedInstructions.clear();
}
}
/**
* Enqueues a callback for the event where the {@link #unfinishedInstructionsCounter} on this processor is equal to
* or gets decremented to zero and all {@link #resultReceivers} have also called back into a runnable passed now
* to their {@link AbstractParallelProcessor#runWhenFinishedProcessing(Runnable)} method.
*
* @param callbackWhenAllLoadedFixesHaveBeenProcessed
* must not be {@code null}
*/
@Override
public void runWhenFinishedProcessing(final Runnable callbackWhenAllLoadedFixesHaveBeenProcessed) {
if (callbackWhenAllLoadedFixesHaveBeenProcessed == null) {
throw new NullPointerException("callbackWhenAllLoadedFixesHaveBeenProcessed must not be null");
}
invokeOrScheduleCallbackForWhenNoMoreUnfinishedInstructions(()->{
if (resultReceivers.length == 0) {
LOGGER.info("Running callback after done processing and no result receivers: "
+callbackWhenAllLoadedFixesHaveBeenProcessed);
callbackWhenAllLoadedFixesHaveBeenProcessed.run();
} else {
final AtomicInteger resultReceiverCallbackCounter = new AtomicInteger(resultReceivers.length);
for (final Processor<ResultType, ?> resultReceiver : resultReceivers) {
resultReceiver.runWhenFinishedProcessing(new Runnable() {
@Override
public void run() {
if (resultReceiverCallbackCounter.decrementAndGet() == 0) {
// this was the last result receiver we were waiting for; trigger callback
LOGGER.info("Running callback after done processing and result receivers finished too: "
+callbackWhenAllLoadedFixesHaveBeenProcessed);
callbackWhenAllLoadedFixesHaveBeenProcessed.run();
}
}
@Override
public String toString() {
return callbackWhenAllLoadedFixesHaveBeenProcessed.toString();
}
});
}
}
});
}
private void invokeOrScheduleCallbackForWhenNoMoreUnfinishedInstructions(final Runnable callbackWhenAllLoadedFixesHaveBeenProcessed) {
synchronized (this) {
if (unfinishedInstructionsCounter.get() == 0) {
callbackWhenAllLoadedFixesHaveBeenProcessed.run();
} else {
callbacksWhenNoMoreUnfinishedInstructions.add(callbackWhenAllLoadedFixesHaveBeenProcessed);
}
}
}
/**
* Forwards the given <code>result</code> to the result receivers, if it's {@link #isResultValid(Object) valid}
* and if the processor hasn't been {@link #abort() aborted}.
@@ -149,7 +212,7 @@ public abstract class AbstractParallelProcessor<InputType, ResultType> extends A
protected void sleepUntilAllInstructionsFinished() throws InterruptedException {
while (areUnfinishedInstructionsLeft() && !isAborted) {
try {
Thread.sleep(SLEEP_TIME_DURING_FINISHING);
Thread.sleep(SLEEP_TIME_DURING_FINISHING_MILLIS); // TODO shouldn't this better be handled by wait/notify on unfinishedInstructionsCounter changes?
} catch (InterruptedException e) {
if (!isAborted) {
onFailure(e);
@@ -50,5 +50,4 @@ public class ParallelFilteringProcessor<InputType> extends AbstractParallelProce
int retrievedDataAmount = additionalDataBuilder.getRetrievedDataAmount();
additionalDataBuilder.setRetrievedDataAmount(retrievedDataAmount - filteredDataAmount.get());
}
}
@@ -188,6 +188,12 @@
"text_join_with_boat_explainer_03": " abyste mohli upravit tuto loď, nebo vytvořit novou.",
"text_join_with_boat_choose": "Vyberte si loď, se kterou pojedete:",
"text_join_with_boat_select_label": "Loď ",
"caption_already_registered": "Již zaregistrováno",
"text_boat_selection_not_applied": "Toto zařízení je k této regatě již zaregistrováno. Váš výběr lodí se nepoužil použije se vaše dříve registrovaná loď.",
"text_join_already_registered": "K této regatě jste již zaregistrováni. Použije se vaše dříve registrovaná loď.",
"text_join_already_registered_as": "K této regatě jste již zaregistrováni s %{name}. Použije se vaše registrovaná loď.",
"text_join_boats_admin_assigned": "Přiřazení lodí do rozjížděk v této regatě spravuje správce regaty.",
"text_join_boat_fixed_hint": "Poznámka: Po registraci nelze loď již změnit.",
"text_add_boat_explainer": "Tuto loď můžete upravit později, nebo přidejte více lodí v uživatelském nastavení.",
"text_link_copied_to_clipboard": "Odkaz zkopírován do schránky",
"text_course_saved": "Dráha byla úspěšně uložena",
@@ -188,6 +188,12 @@
"text_join_with_boat_explainer_03": " for at redigere denne båd eller oprette en ny.",
"text_join_with_boat_choose": "Vælg den båd, du vil sejle med:",
"text_join_with_boat_select_label": "Båd ",
"caption_already_registered": "Allerede registreret",
"text_boat_selection_not_applied": "Denne enhed er allerede registreret til denne regatta. Dit valg af båd blev ikke anvendt din tidligere registrerede båd vil blive brugt.",
"text_join_already_registered": "Du er allerede registreret til denne regatta. Din tidligere registrerede båd vil blive brugt.",
"text_join_already_registered_as": "Du er allerede registreret til denne regatta hos %{name}. Din registrerede båd vil blive brugt.",
"text_join_boats_admin_assigned": "I denne regatta administreres bådtildelinger pr. kapsejlads af regattaadministratoren.",
"text_join_boat_fixed_hint": "Bemærk: Din båd kan ikke ændres efter registrering.",
"text_add_boat_explainer": "Du kan redigere denne båd senere eller tilføje flere både ved at gå til brugerindstillingerne.",
"text_link_copied_to_clipboard": "Link kopieret til udklipsholderen",
"text_course_saved": "Banen blev gemt",
@@ -188,6 +188,12 @@
"text_join_with_boat_explainer_03": " para tratar esta embarcación o crear una nueva.",
"text_join_with_boat_choose": "Seleccione la embarcación con la que navegará:",
"text_join_with_boat_select_label": "Embarcación ",
"caption_already_registered": "Ya registrado",
"text_boat_selection_not_applied": "Este dispositivo ya está registrado para esta regata. No se aplicó la selección de su embarcación. Se utilizará la embarcación registrada anteriormente.",
"text_join_already_registered": "Ya está registrado para esta regata. Se utilizará su embarcación registrada anteriormente.",
"text_join_already_registered_as": "Ya está registrado en esta regata con %{name}. Se utilizará su embarcación registrada.",
"text_join_boats_admin_assigned": "En esta regata, el administrador de regatas gestiona las asignaciones de embarcaciones por carrera.",
"text_join_boat_fixed_hint": "Nota: No puede modificar su embarcación después del registro.",
"text_add_boat_explainer": "Puede tratar esta embarcación más tarde o añadir más embarcaciones en las opciones de usuario.",
"text_link_copied_to_clipboard": "Enlace copiado en el portapapeles",
"text_course_saved": "Rumbo guardado correctamente",
@@ -188,6 +188,12 @@
"text_join_with_boat_explainer_03": " pour modifier ce bateau ou en créer un.",
"text_join_with_boat_choose": "Sélectionnez le bateau avec lequel vous naviguerez :",
"text_join_with_boat_select_label": "Bateau ",
"caption_already_registered": "Déjà inscrit",
"text_boat_selection_not_applied": "Cet appareil est déjà inscrit pour cette régate. Votre sélection de bateau n'a pas été appliquée, votre bateau précédemment inscrit sera donc utilisé.",
"text_join_already_registered": "Vous êtes déjà inscrit pour cette régate. Votre bateau précédemment inscrit sera utilisé.",
"text_join_already_registered_as": "Vous êtes déjà inscrit à cette régate avec %{name}. Votre bateau inscrit sera utilisé.",
"text_join_boats_admin_assigned": "Dans cette régate, les affectations de bateaux par course sont gérées par l'administrateur de la régate.",
"text_join_boat_fixed_hint": "Remarque : Votre bateau ne peut pas être modifié après l'inscription.",
"text_add_boat_explainer": "Vous pourrez modifier ce bateau plus tard et ajouter d'autres bateaux en accédant aux options utilisateur.",
"text_link_copied_to_clipboard": "Lien copié vers le presse-papiers",
"text_course_saved": "Parcours correctement enregistré",
@@ -188,6 +188,12 @@
"text_join_with_boat_explainer_03": " per modificare la barca o crearne una nuova.",
"text_join_with_boat_choose": "Seleziona la barca con cui navigherai:",
"text_join_with_boat_select_label": "Barca ",
"caption_already_registered": "Già registrato",
"text_boat_selection_not_applied": "Questo dispositivo è già registrato per questa regata. La selezione della barca non è stata applicata. Verrà utilizzata la barca precedentemente registrata.",
"text_join_already_registered": "L'utente è già registrato a questa regata. Verrà utilizzata la barca precedentemente registrata.",
"text_join_already_registered_as": "L'utente è già registrato a questa regata con il nome %{name}. Verrà utilizzata la barca registrata.",
"text_join_boats_admin_assigned": "In questa regata, le assegnazioni delle barche a ogni gara vengono gestite dall'amministratore della regata.",
"text_join_boat_fixed_hint": "Nota: la barca non può essere modificata dopo la registrazione.",
"text_add_boat_explainer": "Potrai modificare la barca in seguito o aggiungere altre barche accedendo alle impostazioni utente.",
"text_link_copied_to_clipboard": "Link copiato negli appunti",
"text_course_saved": "Rotta salvata correttamente",
@@ -188,6 +188,12 @@
"text_join_with_boat_explainer_03": "にアクセスしてください。",
"text_join_with_boat_choose": "帆走する艇を選択:",
"text_join_with_boat_select_label": "艇",
"caption_already_registered": "すでに登録済み",
"text_boat_selection_not_applied": "このデバイスはこのレガッタにすでに登録されています。自分の艇の選択は適用されませんでした — 以前に登録した艇が使用されます。",
"text_join_already_registered": "あなたはこのレガッタにすでに登録されています。以前に登録した自分の艇が使用されます。",
"text_join_already_registered_as": "あなたはすでに %{name} でこのレガッタに登録されています。登録した自分の艇が使用されます。",
"text_join_boats_admin_assigned": "このレガッタでは、レースごとの艇の割り当てがレガッタ管理者によって管理されます。",
"text_join_boat_fixed_hint": "注記: 自分の艇は登録後に変更することはできません。",
"text_add_boat_explainer": "ユーザ設定にアクセスすることによって、この艇を編集したり、さらに艇を追加することができます。",
"text_link_copied_to_clipboard": "リンクがクリップボードにコピーされました",
"text_course_saved": "コースが正常に保存されました",
@@ -188,6 +188,12 @@
"text_join_with_boat_explainer_03": "para editar este barco ou criar um novo.",
"text_join_with_boat_choose": "Selecione o barco com o qual irá velejar:",
"text_join_with_boat_select_label": "Barco",
"caption_already_registered": "Já registrado",
"text_boat_selection_not_applied": "Este dispositivo já está registrado para esta regata. Sua seleção de barcos não foi aplicada — seu barco registrado anteriormente será usado.",
"text_join_already_registered": "Você já está registrado para esta regata. Seu barco registrado anteriormente será usado.",
"text_join_already_registered_as": "Você já está registrado nesta regata com %{nome}. Seu barco registrado será usado.",
"text_join_boats_admin_assigned": "Nesta regata, as atribuições de barcos por corrida são administradas pelo administrador da regata.",
"text_join_boat_fixed_hint": "Observação: seu barco não pode ser alterado após o registro.",
"text_add_boat_explainer": "Você pode editar este barco mais tarde ou adicionar mais barcos, visitando as configurações do usuário.",
"text_link_copied_to_clipboard": "Link copiado para clipboard",
"text_course_saved": "Percurso gravado com êxito",
@@ -188,6 +188,12 @@
"text_join_with_boat_explainer_03": " чтобы отредактировать эту лодку или создать новую.",
"text_join_with_boat_choose": "Выберите лодку, с которой поплывете:",
"text_join_with_boat_select_label": "Лодка ",
"caption_already_registered": "Уже зарегистрировано",
"text_boat_selection_not_applied": "Это устройство уже зарегистрировано для выбранной регаты. Ваш выбор лодки не применен, будет использоваться ранее зарегистрированная лодка.",
"text_join_already_registered": "Вы уже зарегистрированы на эту регату. Будет использоваться ранее зарегистрированная лодка.",
"text_join_already_registered_as": "Вы уже зарегистрированы на эту регату с %{name}. Будет использоваться зарегистрированная лодка.",
"text_join_boats_admin_assigned": "В этой регате назначениями лодок на гонку управляет администратор регаты.",
"text_join_boat_fixed_hint": "Примечание. Изменить лодку после регистрации невозможно.",
"text_add_boat_explainer": "Вы можете отредактировать эту лодку позже или добавить дополнительные лодки, перейдя в пользовательские настройки.",
"text_link_copied_to_clipboard": "Ссылка скопирована в буфер обмена",
"text_course_saved": "Курс успешно сохранен",
@@ -188,6 +188,12 @@
"text_join_with_boat_explainer_03": " če želite urediti to jadrnico ali ustvariti novo.",
"text_join_with_boat_choose": "Izberite jadrnico, s katero boste jadrali:",
"text_join_with_boat_select_label": "Jadrnica ",
"caption_already_registered": "Že registrirano",
"text_boat_selection_not_applied": "Ta naprava je že registrirana za to regato. Vaša izbira jadrnice ni bila uporabljena uporabljena bo jadrnica, ki ste jo predhodno registrirali.",
"text_join_already_registered": "Za to regato ste že registrirani. Uporabljena bo vaša predhodno registrirana jadrnica.",
"text_join_already_registered_as": "Za to regato ste že registrirani z %{name}. Uporabljena bo vaša registrirana jadrnica.",
"text_join_boats_admin_assigned": "V tej regati dodelitve jadrnice na posamezni plov upravlja skrbnik regate.",
"text_join_boat_fixed_hint": "Opomba: vaše jadrnice po registraciji ni mogoče zamenjati.",
"text_add_boat_explainer": "To jadrnico lahko kasneje uredite ali dodate dodatne jadrnice v uporabniških nastavitvah.",
"text_link_copied_to_clipboard": "Povezava kopirana v odložišče",
"text_course_saved": "Kurz uspešno shranjen",
@@ -188,6 +188,12 @@
"text_join_with_boat_explainer_03": "以编辑此船只或新建船只。",
"text_join_with_boat_choose": "选取您将航行的船只:",
"text_join_with_boat_select_label": "船只 ",
"caption_already_registered": "已注册",
"text_boat_selection_not_applied": "此设备已注册此比赛。您的船只选择未应用 - 将使用您之前注册的船只。",
"text_join_already_registered": "您已注册此比赛。将使用您之前注册的船只。",
"text_join_already_registered_as": "您已经使用 %{name} 注册此比赛。将使用您注册的船只。",
"text_join_boats_admin_assigned": "在此比赛中,每个比赛轮次的船只分配由比赛管理员管理。",
"text_join_boat_fixed_hint": "注意:注册后无法更改船只。",
"text_add_boat_explainer": "您可以稍后编辑此船只或通过访问用户设置添加更多船只。",
"text_link_copied_to_clipboard": "链接已复制到剪贴板",
"text_course_saved": "场地已成功保存",