From aaeab2f0a361e5729b89f42f800bf24e131a4b47 Mon Sep 17 00:00:00 2001 From: Brutus5000 Date: Wed, 24 Jun 2026 19:57:24 +0200 Subject: [PATCH 1/3] Make map folderName an indexable column to fix slow map filters `/data/map` requests filtering on `versions.folderName` took a flat ~5s each. `folderName` was a transient @ComputedAttribute derived in-memory from `filename` in a @PostLoad enricher, so Elide could not push `versions.folderName == X` into SQL and instead loaded and enriched the whole map_version table on every request, filtering in memory. Map `folderName` is now a real, indexed column (DB migration faf-db V144), making the filter a sargable SQL predicate. `filename` becomes a DB-generated column (read-only in the entity), kept for backwards compatibility with faf-server, the replay server and existing API clients; its value (maps/.zip) and the derived download/thumbnail URLs are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- compose.yaml | 4 ++-- .../com/faforever/api/config/MainDbTestContainers.java | 2 +- src/inttest/resources/sql/prepMapData.sql | 6 +++--- src/inttest/resources/sql/prepMapVersion.sql | 4 ++-- .../java/com/faforever/api/data/domain/MapVersion.java | 9 ++++++--- .../faforever/api/data/listeners/MapVersionEnricher.java | 9 ++++----- src/main/java/com/faforever/api/map/MapService.java | 3 +-- src/test/java/com/faforever/api/map/MapServiceTest.java | 2 +- 8 files changed, 20 insertions(+), 19 deletions(-) diff --git a/compose.yaml b/compose.yaml index 8cb21feab..a889acf94 100644 --- a/compose.yaml +++ b/compose.yaml @@ -16,7 +16,7 @@ services: - "3306:3306" faf-db-migrations: - image: faforever/faf-db-migrations:v140 + image: faforever/faf-db-migrations:v144 command: migrate environment: FLYWAY_URL: jdbc:mysql://faf-db/faf?useSSL=false @@ -35,7 +35,7 @@ services: condition: service_completed_successfully minio: - image: docker.io/bitnami/minio:2025 + image: docker.io/alpine/minio:latest-release ports: - '9000:9000' - '9001:9001' diff --git a/src/inttest/java/com/faforever/api/config/MainDbTestContainers.java b/src/inttest/java/com/faforever/api/config/MainDbTestContainers.java index 335e6bbea..c9866a4de 100644 --- a/src/inttest/java/com/faforever/api/config/MainDbTestContainers.java +++ b/src/inttest/java/com/faforever/api/config/MainDbTestContainers.java @@ -21,7 +21,7 @@ @Configuration public class MainDbTestContainers { private static final MariaDBContainer fafDBContainer = new MariaDBContainer<>("mariadb:11.7"); - private static final GenericContainer flywayMigrationsContainer = new GenericContainer<>("faforever/faf-db-migrations:v140"); + private static final GenericContainer flywayMigrationsContainer = new GenericContainer<>("faforever/faf-db-migrations:v144"); private static final Network sharedNetwork = Network.newNetwork(); @Bean diff --git a/src/inttest/resources/sql/prepMapData.sql b/src/inttest/resources/sql/prepMapData.sql index afabf4334..6f57debbe 100644 --- a/src/inttest/resources/sql/prepMapData.sql +++ b/src/inttest/resources/sql/prepMapData.sql @@ -2,9 +2,9 @@ INSERT INTO map (id, display_name, map_type, battle_type, author, license) VALUE (1, 'SCMP_001', 'FFA', 'skirmish', 1, 1), (2, 'SCMP_002', 'FFA', 'skirmish', 1, 1); -INSERT INTO map_version (id, description, max_players, width, height, version, filename, hidden, map_id) VALUES - (1, 'SCMP 001', 8, 5, 5, 1, 'maps/scmp_001.v0001.zip', 0, 1), - (2, 'SCMP 002', 8, 5, 5, 1, 'maps/scmp_002.v0001.zip', 0, 2); +INSERT INTO map_version (id, description, max_players, width, height, version, folder_name, hidden, map_id) VALUES + (1, 'SCMP 001', 8, 5, 5, 1, 'scmp_001.v0001', 0, 1), + (2, 'SCMP 002', 8, 5, 5, 1, 'scmp_002.v0001', 0, 2); INSERT INTO map_pool (id, name) VALUES (1, 'Ladder 1v1 <300'), diff --git a/src/inttest/resources/sql/prepMapVersion.sql b/src/inttest/resources/sql/prepMapVersion.sql index e384c87d0..ede6b0279 100644 --- a/src/inttest/resources/sql/prepMapVersion.sql +++ b/src/inttest/resources/sql/prepMapVersion.sql @@ -1,6 +1,6 @@ INSERT INTO map (id, display_name, map_type, battle_type, author, recommended, license) VALUES (1, 'display name', 'mtype', 'btype', 1, false, 1); -INSERT INTO map_version (id, description, max_players, width, height, version, filename, map_id, hidden, ranked) -VALUES (1, 'des', 2, 2, 2, 1, 'map/ghb.zip', 1, 0, 1); +INSERT INTO map_version (id, description, max_players, width, height, version, folder_name, map_id, hidden, ranked) +VALUES (1, 'des', 2, 2, 2, 1, 'ghb', 1, 0, 1); INSERT INTO map_reviews_summary (id, map_id, positive, negative, score, reviews, lower_bound) VALUES (1, 1, 0, 0, 2, 1, 0); INSERT INTO map_version_reviews_summary (id, map_version_id, positive, negative, score, reviews, lower_bound) diff --git a/src/main/java/com/faforever/api/data/domain/MapVersion.java b/src/main/java/com/faforever/api/data/domain/MapVersion.java index 5fab0d669..312de0dff 100644 --- a/src/main/java/com/faforever/api/data/domain/MapVersion.java +++ b/src/main/java/com/faforever/api/data/domain/MapVersion.java @@ -11,6 +11,8 @@ import com.yahoo.elide.annotation.Include; import com.yahoo.elide.annotation.UpdatePermission; import lombok.Setter; +import org.hibernate.annotations.Generated; +import org.hibernate.generator.EventType; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -82,7 +84,8 @@ public int getVersion() { return version; } - @Column(name = "filename") + @Column(name = "filename", insertable = false, updatable = false) + @Generated(event = EventType.INSERT) @NotNull public String getFilename() { return filename; @@ -138,8 +141,8 @@ public String getDownloadUrl() { return downloadUrl; } - @Transient - @ComputedAttribute + @Column(name = "folder_name") + @NotNull public String getFolderName() { return folderName; } diff --git a/src/main/java/com/faforever/api/data/listeners/MapVersionEnricher.java b/src/main/java/com/faforever/api/data/listeners/MapVersionEnricher.java index 8c1e6f230..49d628f55 100644 --- a/src/main/java/com/faforever/api/data/listeners/MapVersionEnricher.java +++ b/src/main/java/com/faforever/api/data/listeners/MapVersionEnricher.java @@ -25,11 +25,10 @@ public void init(FafApiProperties apiProperties, EntityCacheEvictor cacheEvictor @PostLoad public void enhance(MapVersion mapVersion) { - String filename = mapVersion.getFilename(); - mapVersion.setDownloadUrl(String.format(apiProperties.getMap().getDownloadUrlFormat(), filename.replace("maps/", ""))); - mapVersion.setThumbnailUrlLarge(String.format(apiProperties.getMap().getLargePreviewsUrlFormat(), filename.replace("maps/", "").replace(".zip", ".png"))); - mapVersion.setThumbnailUrlSmall(String.format(apiProperties.getMap().getSmallPreviewsUrlFormat(), filename.replace("maps/", "").replace(".zip", ".png"))); - mapVersion.setFolderName(filename.substring(filename.indexOf('/') + 1, filename.indexOf(".zip"))); + String folderName = mapVersion.getFolderName(); + mapVersion.setDownloadUrl(String.format(apiProperties.getMap().getDownloadUrlFormat(), folderName + ".zip")); + mapVersion.setThumbnailUrlLarge(String.format(apiProperties.getMap().getLargePreviewsUrlFormat(), folderName + ".png")); + mapVersion.setThumbnailUrlSmall(String.format(apiProperties.getMap().getSmallPreviewsUrlFormat(), folderName + ".png")); } @PostUpdate diff --git a/src/main/java/com/faforever/api/map/MapService.java b/src/main/java/com/faforever/api/map/MapService.java index 27bb1eb2b..683081eb4 100644 --- a/src/main/java/com/faforever/api/map/MapService.java +++ b/src/main/java/com/faforever/api/map/MapService.java @@ -85,7 +85,6 @@ public class MapService { }; private static final Charset MAP_CHARSET = StandardCharsets.ISO_8859_1; - private static final String LEGACY_FOLDER_PREFIX = "maps/"; private final FafApiProperties fafApiProperties; private final MapRepository mapRepository; private final LicenseRepository licenseRepository; @@ -413,7 +412,7 @@ private Map updateHibernateMapEntities(MapLuaAccessor mapLua, Optional exis .setMaxPlayers(standardTeamsConfig.get(CONFIGURATION_STANDARD_TEAMS_ARMIES).length()) .setVersion(mapLua.getMapVersion$()) .setMap(map) - .setFilename(LEGACY_FOLDER_PREFIX + mapNameBuilder.buildFinalZipName(mapLua.getMapVersion$())); + .setFolderName(mapNameBuilder.buildFolderName(mapLua.getMapVersion$())); map.getVersions().add(version); diff --git a/src/test/java/com/faforever/api/map/MapServiceTest.java b/src/test/java/com/faforever/api/map/MapServiceTest.java index adc4985c3..6b59fa828 100644 --- a/src/test/java/com/faforever/api/map/MapServiceTest.java +++ b/src/test/java/com/faforever/api/map/MapServiceTest.java @@ -410,7 +410,7 @@ void positiveUploadTest() throws Exception { assertEquals(256, mapVersion.getHeight()); assertEquals(256, mapVersion.getWidth()); assertEquals(8, mapVersion.getMaxPlayers()); - assertEquals("maps/command_conquer_rush.v0007.zip", mapVersion.getFilename()); + assertEquals("command_conquer_rush.v0007", mapVersion.getFolderName()); assertFalse(Files.exists(tmpDir)); From 919e98a9ddf945708b92ef6d26e974da9be73d75 Mon Sep 17 00:00:00 2001 From: Brutus5000 Date: Wed, 24 Jun 2026 21:38:55 +0200 Subject: [PATCH 2/3] Drop @NotNull on generated filename filename is a DB-generated column, but Bean Validation runs on pre-insert before MariaDB populates it, so @NotNull failed every map upload. The schema guarantees non-null, so the constraint is removed from the entity. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/main/java/com/faforever/api/data/domain/MapVersion.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/faforever/api/data/domain/MapVersion.java b/src/main/java/com/faforever/api/data/domain/MapVersion.java index 312de0dff..8eafb68b5 100644 --- a/src/main/java/com/faforever/api/data/domain/MapVersion.java +++ b/src/main/java/com/faforever/api/data/domain/MapVersion.java @@ -84,9 +84,10 @@ public int getVersion() { return version; } + // DB-generated from folderName (CONCAT('maps/', folder_name, '.zip')); read-only here. + // No @NotNull: bean validation runs pre-insert before the DB populates this value. @Column(name = "filename", insertable = false, updatable = false) @Generated(event = EventType.INSERT) - @NotNull public String getFilename() { return filename; } From 2e288e038da222a8c053784a8ab3bec7b7bc1cef Mon Sep 17 00:00:00 2001 From: Brutus5000 Date: Wed, 24 Jun 2026 22:08:31 +0200 Subject: [PATCH 3/3] Make folderName immutable after insert folderName is the source for the DB-generated filename and the derived download/thumbnail URLs, and map versions are immutable, so it must not change after creation. Mark the folder_name column updatable = false. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/main/java/com/faforever/api/data/domain/MapVersion.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/faforever/api/data/domain/MapVersion.java b/src/main/java/com/faforever/api/data/domain/MapVersion.java index 8eafb68b5..e8322c370 100644 --- a/src/main/java/com/faforever/api/data/domain/MapVersion.java +++ b/src/main/java/com/faforever/api/data/domain/MapVersion.java @@ -142,7 +142,9 @@ public String getDownloadUrl() { return downloadUrl; } - @Column(name = "folder_name") + // Immutable after insert: the DB-generated filename and the derived download/ + // thumbnail URLs are all computed from this, so it must not change post-creation. + @Column(name = "folder_name", updatable = false) @NotNull public String getFolderName() { return folderName;