mirror of
https://github.com/eclipse-sailing-analytics/sailing-analytics.git
synced 2026-09-15 02:08:37 +00:00
Merge remote-tracking branch 'masha/bug6226' into bug6226
This commit is contained in:
+1
-1
@@ -206,6 +206,6 @@ public enum FieldNames {
|
||||
MANEUVER_DISTANCE_SAILED_POMA, MANEUVER_DISTANCE_SAILED_INMPOMA, MANEUVER_SPEED_WITH_BEARING_BEFORE_DEGREES,
|
||||
MANEUVER_SPEED_WITH_BEARING_BEFORE_SPEED, MANEUVER_SPEED_WITH_BEARING_AFTER_DEGREES, MANEUVER_SPEED_WITH_BEARING_AFTER_SPEED_IN_KNOTS,
|
||||
MANEUVER_START_POSITION_LAT_RAD, MANEUVER_START_POSITION_LNG_RAD, MANEUVER_END_POSITION_LAT_RAD,
|
||||
MANEUVER_END_POSITION_LNG_RAD, MIDDLE_MAEUVER_ANGLE, MANEUVER_LOSS_DURATION
|
||||
MANEUVER_END_POSITION_LNG_RAD, MIDDLE_MAEUVER_ANGLE, MANEUVER_LOSS_DURATION, MANEUVER_PAGE_INDEX
|
||||
;
|
||||
}
|
||||
|
||||
+8
-2
@@ -3309,13 +3309,19 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
|
||||
@Override
|
||||
public Map<RaceIdentifier, ManeuverRaceFingerprint> loadFingerprintsForManeuverHashes() {
|
||||
final MongoCollection<Document> maneuversCollection = database.getCollection(CollectionNames.MANEUVERS.name());
|
||||
try {
|
||||
maneuversCollection.dropIndex("maneuversbyeventraceandcompetitor");
|
||||
} catch (final Exception e) {
|
||||
// index does not exist yet, nothing to drop
|
||||
}
|
||||
maneuversCollection.createIndex(new Document()
|
||||
.append(FieldNames.EVENT_NAME.name(), 1)
|
||||
.append(FieldNames.RACE_NAME.name(), 1)
|
||||
.append(FieldNames.COMPETITOR_ID.name(), 1),
|
||||
.append(FieldNames.COMPETITOR_ID.name(), 1)
|
||||
.append(FieldNames.MANEUVER_PAGE_INDEX.name(), 1),
|
||||
new IndexOptions()
|
||||
.unique(true)
|
||||
.name("maneuversbyeventraceandcompetitor")
|
||||
.name("maneuversbyeventracecompetitorandpage")
|
||||
.background(false));
|
||||
final Map<RaceIdentifier, ManeuverRaceFingerprint> fingerprintHashMap = new HashMap<>();
|
||||
for (final Document currentDocument : maneuversCollection.find()) {
|
||||
|
||||
+18
-6
@@ -171,6 +171,7 @@ import com.sap.sse.shared.media.VideoDescriptor;
|
||||
|
||||
public class MongoObjectFactoryImpl implements MongoObjectFactory {
|
||||
private static Logger logger = Logger.getLogger(MongoObjectFactoryImpl.class.getName());
|
||||
private static final int MANEUVERS_PER_PAGE = 1000;
|
||||
private final MongoDatabase database;
|
||||
private final CompetitorWithBoatRefJsonSerializer competitorWithBoatRefSerializer = CompetitorWithBoatRefJsonSerializer.create(/* serializeNonPublicCompetitorFields */ true);
|
||||
private final CompetitorJsonSerializer competitorSerializer = CompetitorJsonSerializer.create(
|
||||
@@ -2071,24 +2072,35 @@ public class MongoObjectFactoryImpl implements MongoObjectFactory {
|
||||
final JSONObject fingerprintjson = fingerprint.toJson();
|
||||
final Document fingerprintDoc = Document.parse(fingerprintjson.toString());
|
||||
for (final Entry<Competitor, List<Maneuver>> e : maneuvers.entrySet()) {
|
||||
storeCompetitorManeuvers(maneuverCollection, raceIdentifier, fingerprintDoc, course, e.getKey(), e.getValue());
|
||||
final List<Maneuver> competitorManeuvers = e.getValue() != null ? e.getValue() : new ArrayList<>();
|
||||
final int pageCount = Math.max(1, (int) Math.ceil((double) competitorManeuvers.size() / MANEUVERS_PER_PAGE));
|
||||
if (pageCount > 1) {
|
||||
logger.warning("Competitor " + e.getKey().getName() + " in race " + raceIdentifier
|
||||
+ " has " + competitorManeuvers.size() + " maneuvers, splitting into " + pageCount
|
||||
+ " documents of up to " + MANEUVERS_PER_PAGE + " maneuvers each (bug 6226).");
|
||||
}
|
||||
for (int pageIndex = 0; pageIndex < pageCount; pageIndex++) {
|
||||
final int from = pageIndex * MANEUVERS_PER_PAGE;
|
||||
final int to = Math.min(from + MANEUVERS_PER_PAGE, competitorManeuvers.size());
|
||||
storeCompetitorManeuvers(maneuverCollection, raceIdentifier, fingerprintDoc, course, e.getKey(), competitorManeuvers.subList(from, to), pageIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void storeCompetitorManeuvers(MongoCollection<Document> maneuverCollection, RaceIdentifier raceIdentifier,
|
||||
Document fingerprintDoc, Course course, Competitor competitor, List<Maneuver> competitorManeuvers) {
|
||||
Document fingerprintDoc, Course course, Competitor competitor, List<Maneuver> competitorManeuvers, final int pageIndex) {
|
||||
final Document query = new Document();
|
||||
DomainObjectFactoryImpl.addRaceIdentifierToQuery(query, raceIdentifier);
|
||||
query.put(FieldNames.COMPETITOR_ID.name(), competitor.getId());
|
||||
query.put(FieldNames.MANEUVER_PAGE_INDEX.name(), pageIndex);
|
||||
final Document result = new Document();
|
||||
result.put(FieldNames.MANEUVER_FINGERPRINT.name(), fingerprintDoc);
|
||||
storeRaceIdentifier(result, raceIdentifier);
|
||||
result.put(FieldNames.COMPETITOR_ID.name(), competitor.getId());
|
||||
result.put(FieldNames.MANEUVER_PAGE_INDEX.name(), pageIndex);
|
||||
final List<Document> maneuverList = new ArrayList<>();
|
||||
if (competitorManeuvers != null) {
|
||||
for (final Maneuver maneuver : competitorManeuvers) {
|
||||
maneuverList.add(generateManeuverDoc(maneuver, course));
|
||||
}
|
||||
for (final Maneuver maneuver : competitorManeuvers) {
|
||||
maneuverList.add(generateManeuverDoc(maneuver, course));
|
||||
}
|
||||
result.put(FieldNames.MANEUVERS.name(), maneuverList);
|
||||
try {
|
||||
|
||||
+58
-12
@@ -1,5 +1,6 @@
|
||||
package com.sap.sailing.mongodb.test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -15,6 +16,7 @@ import org.json.simple.JSONObject;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import com.mongodb.client.FindIterable;
|
||||
import com.mongodb.client.MongoClient;
|
||||
import com.mongodb.client.MongoClients;
|
||||
import com.mongodb.client.MongoCollection;
|
||||
@@ -26,6 +28,7 @@ import com.sap.sailing.domain.common.ManeuverType;
|
||||
import com.sap.sailing.domain.common.RegattaNameAndRaceName;
|
||||
import com.sap.sailing.domain.common.Tack;
|
||||
import com.sap.sailing.domain.maneuverhash.ManeuverRaceFingerprint;
|
||||
import com.sap.sailing.domain.persistence.FieldNames;
|
||||
import com.sap.sailing.domain.persistence.impl.MongoObjectFactoryImpl;
|
||||
import com.sap.sailing.domain.tracking.Maneuver;
|
||||
import com.sap.sailing.domain.tracking.ManeuverCurveBoundaries;
|
||||
@@ -40,19 +43,17 @@ import com.sap.sse.mongodb.MongoDBConfiguration;
|
||||
|
||||
/**
|
||||
* Regression test for bug 6226 (https://github.com/eclipse-sailing-analytics/sailing-analytics/issues/6226):
|
||||
* {@link BsonMaximumSizeExceededException} storing maneuvers for large races with many competitors.
|
||||
* {@link BsonMaximumSizeExceededException} storing maneuvers for large races.
|
||||
* <p>
|
||||
*
|
||||
* The old implementation stored all competitors' maneuvers in a single MongoDB document, which exceeded the 16MB BSON
|
||||
* limit for races with ~163 competitors. The fix stores one document per competitor instead.
|
||||
* <p>
|
||||
*
|
||||
* This test uses a synthetic dataset (200 competitors x 500 maneuvers each) to trigger the original issue and verify
|
||||
* that the new implementation handles it correctly.
|
||||
* The original fix stored one document per competitor instead of one per race. This test also covers the follow-up
|
||||
* case: a single competitor with enough maneuvers to exceed 16MB in one document. The implementation paginates
|
||||
* across multiple documents keyed by (EVENT_NAME, RACE_NAME, COMPETITOR_ID, MANEUVER_PAGE_INDEX).
|
||||
*/
|
||||
public class ManeuverStorageLimitTest {
|
||||
private static final int COMPETITOR_COUNT = 200;
|
||||
private static final int MANEUVERS_PER_COMPETITOR = 500;
|
||||
private static final int MANEUVERS_PER_PAGE = 1000;
|
||||
|
||||
private MongoDBConfiguration dbConfiguration;
|
||||
private MongoDatabase database;
|
||||
@@ -66,9 +67,7 @@ public class ManeuverStorageLimitTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the new per-competitor document storage does not exceed MongoDB's 16MB limit
|
||||
* even for a large race (200 competitors, 500 maneuvers each).
|
||||
* Verifies document count equals competitor count after storing.
|
||||
* Verifies that competitors with fewer than 1000 maneuvers each produce one document per competitor.
|
||||
*/
|
||||
@Test
|
||||
public void testNewImplementationStoresOneDocumentPerCompetitor() {
|
||||
@@ -78,11 +77,58 @@ public class ManeuverStorageLimitTest {
|
||||
final Course course = mock(Course.class);
|
||||
new MongoObjectFactoryImpl(database).storeManeuvers(raceIdentifier, fingerprint, course, maneuvers);
|
||||
final MongoCollection<Document> collection = database.getCollection("MANEUVERS");
|
||||
final long documentCount = collection.countDocuments();
|
||||
assertEquals(COMPETITOR_COUNT, documentCount,
|
||||
assertEquals(COMPETITOR_COUNT, collection.countDocuments(),
|
||||
"Expected one document per competitor in the MANEUVERS collection");
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that a competitor with 15000 maneuvers (enough to exceed the 16MB BSON limit in one document)
|
||||
* is split into 15 pages of 1000 maneuvers each, and all maneuvers are stored successfully.
|
||||
*/
|
||||
@Test
|
||||
public void testPaginationSplitsLargeCompetitorAcrossMultipleDocuments() {
|
||||
final int totalManeuvers = 15000;
|
||||
final RegattaNameAndRaceName raceIdentifier = new RegattaNameAndRaceName("505 Pre-Worlds 2014 synthetic", "Race 1");
|
||||
final Competitor competitor = mock(CompetitorImpl.class);
|
||||
when(competitor.getId()).thenReturn("competitor-0");
|
||||
final Map<Competitor, List<Maneuver>> maneuvers = new HashMap<>();
|
||||
maneuvers.put(competitor, buildManeuverList(totalManeuvers));
|
||||
final ManeuverRaceFingerprint fingerprint = buildMockFingerprint();
|
||||
final Course course = mock(Course.class);
|
||||
new MongoObjectFactoryImpl(database).storeManeuvers(raceIdentifier, fingerprint, course, maneuvers);
|
||||
final MongoCollection<Document> collection = database.getCollection("MANEUVERS");
|
||||
final int expectedPages = (int) Math.ceil((double) totalManeuvers / MANEUVERS_PER_PAGE);
|
||||
assertEquals(expectedPages, collection.countDocuments(), "Expected " + expectedPages + " page documents for one competitor with " + totalManeuvers + " maneuvers");
|
||||
final FindIterable<Document> docs = collection.find(new Document(FieldNames.COMPETITOR_ID.name(), "competitor-0"));
|
||||
int totalManeuversLoaded = 0;
|
||||
for (final Document doc : docs) {
|
||||
final List<Document> page = doc.getList(FieldNames.MANEUVERS.name(), Document.class);
|
||||
totalManeuversLoaded += page != null ? page.size() : 0;
|
||||
}
|
||||
assertEquals(totalManeuvers, totalManeuversLoaded, "Expected all maneuvers to be stored across pages");
|
||||
}
|
||||
|
||||
/**
|
||||
* Demonstrates that the pre-pagination implementation fails with BsonMaximumSizeExceededException
|
||||
* for a competitor with 15000 maneuvers (matching Axel's real-world case of 18,270). The old code
|
||||
* stored all maneuvers in one document with no page splitting, reproduced here via
|
||||
* OldSinglePageMongoObjectFactoryForBug6226Test.
|
||||
*/
|
||||
@Test
|
||||
public void testOldSinglePageImplementationFailsWithBsonLimitExceeded() {
|
||||
final RegattaNameAndRaceName raceIdentifier = new RegattaNameAndRaceName("505 Pre-Worlds 2014 synthetic", "Race 1");
|
||||
final Competitor competitor = mock(CompetitorImpl.class);
|
||||
when(competitor.getId()).thenReturn("competitor-0");
|
||||
final Map<Competitor, List<Maneuver>> maneuvers = new HashMap<>();
|
||||
maneuvers.put(competitor, buildManeuverList(15000));
|
||||
final ManeuverRaceFingerprint fingerprint = buildMockFingerprint();
|
||||
final Course course = mock(Course.class);
|
||||
assertThrows(BsonMaximumSizeExceededException.class, () ->
|
||||
new OldSinglePageMongoObjectFactoryForBug6226Test(database).storeManeuvers(raceIdentifier, fingerprint, course, maneuvers),
|
||||
"Old single-page implementation must throw BsonMaximumSizeExceededException for a competitor with 2500 maneuvers"
|
||||
);
|
||||
}
|
||||
|
||||
private ManeuverRaceFingerprint buildMockFingerprint() {
|
||||
final ManeuverRaceFingerprint fingerprint = mock(ManeuverRaceFingerprint.class);
|
||||
final JSONObject fingerprintJson = new JSONObject();
|
||||
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package com.sap.sailing.mongodb.test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import org.bson.Document;
|
||||
|
||||
import com.mongodb.client.MongoCollection;
|
||||
import com.mongodb.client.MongoDatabase;
|
||||
import com.mongodb.client.model.ReplaceOptions;
|
||||
import com.sap.sailing.domain.base.Competitor;
|
||||
import com.sap.sailing.domain.base.Course;
|
||||
import com.sap.sailing.domain.common.RaceIdentifier;
|
||||
import com.sap.sailing.domain.maneuverhash.ManeuverRaceFingerprint;
|
||||
import com.sap.sailing.domain.persistence.FieldNames;
|
||||
import com.sap.sailing.domain.persistence.impl.CollectionNames;
|
||||
import com.sap.sailing.domain.tracking.Maneuver;
|
||||
import com.sap.sailing.domain.tracking.ManeuverCurveBoundaries;
|
||||
|
||||
/**
|
||||
* Reproduces the pre-pagination behaviour of {@code MongoObjectFactoryImpl.storeCompetitorManeuvers()}:
|
||||
* all maneuvers for a competitor were stored in a single document with no page splitting. For competitors
|
||||
* with very large maneuver counts this document exceeds MongoDB's 16MB BSON limit.
|
||||
*
|
||||
* This class exists solely to let {@link ManeuverStorageLimitTest} assert that the old single-page code
|
||||
* path fails with {@code BsonMaximumSizeExceededException} for a competitor with 2500 maneuvers.
|
||||
* It must never be used in production.
|
||||
*/
|
||||
class OldSinglePageMongoObjectFactoryForBug6226Test {
|
||||
private final MongoDatabase database;
|
||||
|
||||
OldSinglePageMongoObjectFactoryForBug6226Test(final MongoDatabase database) {
|
||||
this.database = database;
|
||||
}
|
||||
|
||||
void storeManeuvers(final RaceIdentifier raceIdentifier, final ManeuverRaceFingerprint fingerprint,
|
||||
final Course course, final Map<Competitor, List<Maneuver>> maneuvers) {
|
||||
final MongoCollection<Document> maneuverCollection = database.getCollection(CollectionNames.MANEUVERS.name());
|
||||
final Document fingerprintDoc = Document.parse(fingerprint.toJson().toString());
|
||||
for (final Entry<Competitor, List<Maneuver>> e : maneuvers.entrySet()) {
|
||||
storeCompetitorManeuversSinglePage(maneuverCollection, raceIdentifier, fingerprintDoc, course, e.getKey(), e.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
private void storeCompetitorManeuversSinglePage(final MongoCollection<Document> maneuverCollection,
|
||||
final RaceIdentifier raceIdentifier, final Document fingerprintDoc, final Course course,
|
||||
final Competitor competitor, final List<Maneuver> competitorManeuvers) {
|
||||
final Document query = new Document();
|
||||
query.put(FieldNames.EVENT_NAME.name(), raceIdentifier.getRegattaName());
|
||||
query.put(FieldNames.RACE_NAME.name(), raceIdentifier.getRaceName());
|
||||
query.put(FieldNames.COMPETITOR_ID.name(), competitor.getId());
|
||||
final Document result = new Document();
|
||||
result.put(FieldNames.MANEUVER_FINGERPRINT.name(), fingerprintDoc);
|
||||
result.put(FieldNames.EVENT_NAME.name(), raceIdentifier.getRegattaName());
|
||||
result.put(FieldNames.RACE_NAME.name(), raceIdentifier.getRaceName());
|
||||
result.put(FieldNames.COMPETITOR_ID.name(), competitor.getId());
|
||||
final List<Document> maneuverList = new ArrayList<>();
|
||||
if (competitorManeuvers != null) {
|
||||
for (final Maneuver maneuver : competitorManeuvers) {
|
||||
maneuverList.add(buildManeuverDoc(maneuver));
|
||||
}
|
||||
}
|
||||
result.put(FieldNames.MANEUVERS.name(), maneuverList);
|
||||
maneuverCollection.replaceOne(query, result, new ReplaceOptions().upsert(true));
|
||||
}
|
||||
|
||||
private Document buildManeuverDoc(final Maneuver maneuver) {
|
||||
final Document doc = new Document();
|
||||
doc.put(FieldNames.SIMPLE_CLASS_NAME.name(), maneuver.getClass().getSimpleName());
|
||||
doc.put(FieldNames.TYPE.name(), maneuver.getType().name());
|
||||
doc.put(FieldNames.TACK.name(), maneuver.getNewTack() == null ? null : maneuver.getNewTack().name());
|
||||
doc.put(FieldNames.POSITION_LAT_RAD.name(), maneuver.getPosition().getLatRad());
|
||||
doc.put(FieldNames.POSITION_LNG_RAD.name(), maneuver.getPosition().getLngRad());
|
||||
doc.put(FieldNames.TIMEPOINT.name(), maneuver.getTimePoint().asMillis());
|
||||
doc.put(FieldNames.MAIN_CURVE_BOUNDARIES.name(), buildCurveBoundariesDoc(maneuver.getMainCurveBoundaries()));
|
||||
doc.put(FieldNames.MANEUVER_CURVE_WITH_STABLE_SPEED_AND_COURSE_BOUNDERIES.name(),
|
||||
buildCurveBoundariesDoc(maneuver.getManeuverCurveWithStableSpeedAndCourseBoundaries()));
|
||||
doc.put(FieldNames.MAX_TURNING_RATE_IN_DEGREE_PER_SECOUND.name(), maneuver.getMaxTurningRateInDegreesPerSecond());
|
||||
doc.put(FieldNames.INDEX_OF_PASSED_WAYPOINT.name(), -1);
|
||||
doc.put(FieldNames.TIME_AS_MILLIS.name(), maneuver.getDuration().asMillis());
|
||||
doc.put(FieldNames.MANEUVER_LOSS.name(), null);
|
||||
return doc;
|
||||
}
|
||||
|
||||
private Document buildCurveBoundariesDoc(final ManeuverCurveBoundaries f) {
|
||||
final Document d = new Document();
|
||||
d.put(FieldNames.MANEUVER_TIMEPOINT_BEFORE.name(), f.getTimePointBefore().asMillis());
|
||||
d.put(FieldNames.MANEUVER_TIMEPOINT_AFTER.name(), f.getTimePointAfter().asMillis());
|
||||
d.put(FieldNames.MANEUVER_SPEED_WITH_BEARING_BEFORE_DEGREES.name(), f.getSpeedWithBearingBefore().getBearing().getDegrees());
|
||||
d.put(FieldNames.MANEUVER_SPEED_WITH_BEARING_BEFORE_SPEED.name(), f.getSpeedWithBearingBefore().getKnots());
|
||||
d.put(FieldNames.MANEUVER_SPEED_WITH_BEARING_AFTER_DEGREES.name(), f.getSpeedWithBearingAfter().getBearing().getDegrees());
|
||||
d.put(FieldNames.MANEUVER_SPEED_WITH_BEARING_AFTER_SPEED_IN_KNOTS.name(), f.getSpeedWithBearingAfter().getKnots());
|
||||
d.put(FieldNames.MANEUVER_DIRECTION_CHANGE_IN_DEGREES.name(), f.getDirectionChangeInDegrees());
|
||||
d.put(FieldNames.MANEUVER_LOWEST_SPEED_IN_KNOTS.name(), f.getLowestSpeed().getKnots());
|
||||
d.put(FieldNames.MANEUVER_HIGHEST_SPEED_IN_KNOTS.name(), f.getHighestSpeed().getKnots());
|
||||
return d;
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,12 @@
|
||||
for any typical even seen so far.
|
||||
See also <a href="https://bugzilla.sapsailing.com/bugzilla/show_bug.cgi?id=6226">bug 6226</a>.
|
||||
</li>
|
||||
<li>Further fix for the same issue: a competitor tracked over an unusually long period could accumulate
|
||||
enough maneuvers to exceed the 16MB limit in their own document. Maneuver documents are now paginated —
|
||||
each document holds at most 1000 maneuvers, with additional pages stored as separate documents.
|
||||
A warning is logged when pagination kicks in.
|
||||
See also <a href="https://bugzilla.sapsailing.com/bugzilla/show_bug.cgi?id=6226">bug 6226</a>.
|
||||
</li>
|
||||
</ul>
|
||||
<h2 class="articleSubheadline">May 2026</h2>
|
||||
<ul class="bulletList">
|
||||
|
||||
Reference in New Issue
Block a user