Skip to content

Commit 9955487

Browse files
bdshadowclaude
andcommitted
perf: fix N+1 queries, unbounded purge, missing index, and redundant fetch
- Add left join fetch k.branch to findSoftDeletedByIdsAndProjectId to eliminate lazy-load per key in restoreKeys - Change purge scheduler to paginated ID-only query instead of loading all expired key entities into memory - Add partial index key_project_deleted_at_not_null for trash queries - Eliminate double-fetch in permanentlyDelete by using lightweight project ID check instead of full entity load Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e740394 commit 9955487

5 files changed

Lines changed: 48 additions & 18 deletions

File tree

backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/keys/KeyTrashController.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,10 @@ class KeyTrashController(
147147
fun permanentlyDelete(
148148
@PathVariable keyId: Long,
149149
) {
150-
val key =
151-
keyService.findSoftDeletedByIdsAndProjectId(listOf(keyId), projectHolder.project.id).firstOrNull()
152-
?: throw NotFoundException(Message.KEY_NOT_FOUND)
153-
keyService.hardDeleteMultiple(listOf(key.id))
150+
val projectIds = keyService.getSoftDeletedProjectIdsForKeyIds(listOf(keyId))
151+
if (projectIds.isEmpty() || projectIds.single() != projectHolder.project.id) {
152+
throw NotFoundException(Message.KEY_NOT_FOUND)
153+
}
154+
keyService.hardDeleteMultiple(listOf(keyId))
154155
}
155156
}

backend/data/src/main/kotlin/io/tolgee/repository/KeyRepository.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -511,13 +511,17 @@ interface KeyRepository : JpaRepository<Key, Long> {
511511
pageable: Pageable,
512512
): Page<KeySearchResultView>
513513

514-
@Query("from Key k where k.deletedAt is not null and k.deletedAt < :before")
515-
fun findAllSoftDeletedBefore(before: Date): List<Key>
514+
@Query("select k.id from Key k where k.deletedAt is not null and k.deletedAt < :before")
515+
fun findSoftDeletedIdsBefore(
516+
before: Date,
517+
pageable: Pageable,
518+
): Page<Long>
516519

517520
@Query(
518521
"""
519522
from Key k
520523
left join fetch k.namespace
524+
left join fetch k.branch
521525
where k.id in :ids and k.project.id = :projectId and k.deletedAt is not null
522526
""",
523527
)

backend/data/src/main/kotlin/io/tolgee/service/key/KeyService.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -418,8 +418,11 @@ class KeyService(
418418
return keyRepository.findSoftDeletedByProjectId(projectId, branch, pageable)
419419
}
420420

421-
fun findAllSoftDeletedBefore(before: Date): List<Key> {
422-
return keyRepository.findAllSoftDeletedBefore(before)
421+
fun findSoftDeletedIdsBefore(
422+
before: Date,
423+
pageable: Pageable,
424+
): Page<Long> {
425+
return keyRepository.findSoftDeletedIdsBefore(before, pageable)
423426
}
424427

425428
fun findSoftDeletedByIdsAndProjectId(
@@ -429,6 +432,10 @@ class KeyService(
429432
return keyRepository.findSoftDeletedByIdsAndProjectId(ids, projectId)
430433
}
431434

435+
fun getSoftDeletedProjectIdsForKeyIds(keyIds: List<Long>): List<Long> {
436+
return keyRepository.getSoftDeletedProjectIdsForKeyIds(keyIds)
437+
}
438+
432439
@Transactional
433440
fun deleteAllByProject(projectId: Long) {
434441
keyMetaService.deleteAllByProject(projectId)

backend/data/src/main/kotlin/io/tolgee/service/key/KeyTrashPurgeScheduler.kt

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import io.tolgee.util.logger
1010
import io.tolgee.util.runSentryCatching
1111
import org.springframework.boot.context.event.ApplicationReadyEvent
1212
import org.springframework.context.event.EventListener
13+
import org.springframework.data.domain.PageRequest
1314
import org.springframework.stereotype.Component
1415
import org.springframework.transaction.PlatformTransactionManager
1516
import java.time.Duration
@@ -38,21 +39,26 @@ class KeyTrashPurgeScheduler(
3839

3940
private fun purgeExpiredKeys() {
4041
val cutoffDate = currentDateProvider.date.addDays(-RETENTION_DAYS)
41-
val expiredKeys =
42-
executeInNewTransaction(transactionManager) {
43-
keyService.findAllSoftDeletedBefore(cutoffDate)
44-
}
42+
var totalPurged = 0
43+
44+
do {
45+
val batch =
46+
executeInNewTransaction(transactionManager) {
47+
keyService.findSoftDeletedIdsBefore(cutoffDate, PageRequest.of(0, BATCH_SIZE))
48+
}
4549

46-
if (expiredKeys.isEmpty()) return
50+
if (batch.isEmpty) break
4751

48-
expiredKeys.map { it.id }.chunked(BATCH_SIZE).forEach { batch ->
4952
executeInNewTransaction(transactionManager) {
50-
keyService.hardDeleteMultiple(batch)
53+
keyService.hardDeleteMultiple(batch.content)
5154
}
52-
logger.info("Purged {} expired trashed keys", batch.size)
53-
}
55+
totalPurged += batch.numberOfElements
56+
logger.info("Purged {} expired trashed keys", batch.numberOfElements)
57+
} while (batch.hasNext())
5458

55-
logger.info("Total purged {} expired trashed keys older than {}", expiredKeys.size, cutoffDate)
59+
if (totalPurged > 0) {
60+
logger.info("Total purged {} expired trashed keys older than {}", totalPurged, cutoffDate)
61+
}
5662
}
5763

5864
companion object {

backend/data/src/main/resources/db/changelog/schema.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5152,4 +5152,16 @@
51525152
<!-- Old indexes would need manual recreation if rolling back -->
51535153
</rollback>
51545154
</changeSet>
5155+
5156+
<!-- Key soft-delete: partial index for trash queries (deleted_at IS NOT NULL) -->
5157+
<changeSet author="dbocharov" id="1739900000000-5" runInTransaction="false">
5158+
<sql>
5159+
create index concurrently if not exists key_project_deleted_at_not_null
5160+
on public.key (project_id, deleted_at)
5161+
where deleted_at is not null;
5162+
</sql>
5163+
<rollback>
5164+
drop index concurrently if exists key_project_deleted_at_not_null;
5165+
</rollback>
5166+
</changeSet>
51555167
</databaseChangeLog>

0 commit comments

Comments
 (0)