Skip to content

Commit e9b99fa

Browse files
committed
fix: recording statistics randomly failing due to lack of ongoing DB session to join fetch stats history
1 parent 9f12ef9 commit e9b99fa

File tree

6 files changed

+37
-47
lines changed

6 files changed

+37
-47
lines changed

ambassador-application/src/main/kotlin/com/roche/ambassador/analysis/AnalysisService.kt

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,27 @@ import com.roche.ambassador.model.project.Project
1111
import com.roche.ambassador.model.source.ProjectSources
1212
import com.roche.ambassador.storage.project.ProjectEntity
1313
import com.roche.ambassador.storage.project.ProjectEntityRepository
14+
import com.roche.ambassador.storage.project.ProjectStatisticsHistory
15+
import com.roche.ambassador.storage.project.ProjectStatisticsHistoryRepository
1416
import kotlinx.coroutines.CoroutineScope
1517
import kotlinx.coroutines.launch
1618
import org.springframework.stereotype.Service
19+
import org.springframework.transaction.PlatformTransactionManager
1720
import org.springframework.transaction.annotation.Propagation
1821
import org.springframework.transaction.annotation.Transactional
22+
import org.springframework.transaction.support.TransactionTemplate
1923

2024
@Service
2125
internal class AnalysisService(
2226
private val scorecardConfiguration: ScorecardConfiguration,
2327
private val projectEntityRepository: ProjectEntityRepository,
28+
private val projectStatisticsHistoryRepository: ProjectStatisticsHistoryRepository,
2429
private val projectSources: ProjectSources,
30+
platformTransactionManager: PlatformTransactionManager,
2531
concurrencyProvider: ConcurrencyProvider
2632
) {
2733

34+
private val transactionTemplate: TransactionTemplate = TransactionTemplate(platformTransactionManager)
2835
private val analysisScope: CoroutineScope = CoroutineScope(concurrencyProvider.getSupportingDispatcher())
2936
private val calculator: ScorecardCalculator = ScorecardCalculator(scorecardConfiguration)
3037

@@ -61,8 +68,13 @@ internal class AnalysisService(
6168
try {
6269
entity.project = analyze(entity.project)
6370
entity.updateScore(entity.project)
64-
entity.recordStatistics()
65-
projectEntityRepository.save(entity)
71+
transactionTemplate.executeWithoutResult {
72+
val savedEntity = projectEntityRepository.save(entity)
73+
val historyEntry = ProjectStatisticsHistory.from(savedEntity)
74+
val entryDate = historyEntry.date.toLocalDate().atStartOfDay()
75+
projectStatisticsHistoryRepository.deleteByProjectIdAndDateBetween(historyEntry.projectId, entryDate, entryDate.plusDays(1))
76+
projectStatisticsHistoryRepository.save(historyEntry)
77+
}
6678
progressMonitor.success()
6779
} catch (e: Throwable) {
6880
log.error("Failed to analyze project '{}' (id={}).", entity.project.fullName, entity.project.id, e)

ambassador-storage/src/main/kotlin/com/roche/ambassador/storage/project/ProjectEntity.kt

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package com.roche.ambassador.storage.project
22

33
import com.roche.ambassador.model.project.Project
44
import com.vladmihalcea.hibernate.type.json.JsonBinaryType
5-
import org.hibernate.annotations.BatchSize
65
import org.hibernate.annotations.Type
76
import org.hibernate.annotations.TypeDef
87
import java.time.LocalDate
@@ -13,10 +12,6 @@ import javax.persistence.*
1312
@Entity
1413
@Table(name = "project")
1514
@TypeDef(name = "jsonb", typeClass = JsonBinaryType::class)
16-
@NamedEntityGraph(
17-
name = "Project.statsHistory",
18-
attributeNodes = [NamedAttributeNode("statsHistory")]
19-
)
2015
class ProjectEntity(
2116
@Id var id: Long? = null,
2217
var name: String? = null,
@@ -36,15 +31,6 @@ class ProjectEntity(
3631
var lastIndexedDate: LocalDateTime = LocalDateTime.now(),
3732
@Column(name = "last_analysis_date")
3833
var lastAnalysisDate: LocalDateTime? = null,
39-
@OneToMany(
40-
mappedBy = "project",
41-
cascade = [CascadeType.ALL],
42-
fetch = FetchType.LAZY,
43-
orphanRemoval = true
44-
)
45-
@BatchSize(size = 25)
46-
@OrderBy("date")
47-
var statsHistory: MutableList<ProjectStatisticsHistory> = mutableListOf(),
4834
var source: String? = null,
4935
@Column(name = "last_indexing_id")
5036
var lastIndexingId: UUID? = null, // mapping is not needed here yet, thus not adding it
@@ -64,12 +50,6 @@ class ProjectEntity(
6450

6551
fun wasIndexedBefore(otherDate: LocalDate): Boolean = lastIndexedDate.isBefore(otherDate.atStartOfDay())
6652

67-
fun recordStatistics(): ProjectStatisticsHistory {
68-
val historyEntry = ProjectStatisticsHistory.from(this)
69-
statsHistory.add(historyEntry)
70-
return historyEntry
71-
}
72-
7353
fun updateIndex(project: Project) {
7454
this.name = project.name
7555
this.project = project

ambassador-storage/src/main/kotlin/com/roche/ambassador/storage/project/ProjectEntityRepository.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package com.roche.ambassador.storage.project
22

33
import com.roche.ambassador.storage.Lookup
44
import org.hibernate.jpa.QueryHints.HINT_CACHEABLE
5-
import org.springframework.data.jpa.repository.EntityGraph
65
import org.springframework.data.jpa.repository.Modifying
76
import org.springframework.data.jpa.repository.Query
87
import org.springframework.data.jpa.repository.QueryHints
@@ -23,7 +22,6 @@ interface ProjectEntityRepository : PagingAndSortingRepository<ProjectEntity, Lo
2322
@Modifying
2423
override fun deleteAll()
2524

26-
@EntityGraph(value = "Project.statsHistory")
2725
override fun findById(id: Long): Optional<ProjectEntity>
2826

2927
fun countAllBySubscribed(subscribed: Boolean): Long

ambassador-storage/src/main/kotlin/com/roche/ambassador/storage/project/ProjectStatisticsHistory.kt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,8 @@ import javax.persistence.*
1212
@TypeDef(name = "jsonb", typeClass = JsonBinaryType::class)
1313
class ProjectStatisticsHistory(
1414
@Id @GeneratedValue var id: UUID? = null,
15-
@ManyToOne
16-
@JoinColumn(name = "project_id", nullable = false, updatable = false)
17-
var project: ProjectEntity,
15+
@Column(name = "project_id", nullable = false, updatable = false)
16+
var projectId: Long,
1817
@Type(type = "jsonb")
1918
@Column(name = "stats", columnDefinition = "jsonb", updatable = false)
2019
@Basic(fetch = FetchType.EAGER)
@@ -27,7 +26,7 @@ class ProjectStatisticsHistory(
2726
fun from(projectEntity: ProjectEntity): ProjectStatisticsHistory {
2827
val stats = ProjectStatistics.from(projectEntity.project)
2928
return ProjectStatisticsHistory(
30-
null, projectEntity, stats, projectEntity.lastIndexedDate
29+
null, projectEntity.id!!, stats, projectEntity.lastIndexedDate
3130
)
3231
}
3332
}
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.roche.ambassador.storage.project
22

3+
import org.springframework.data.jpa.repository.Modifying
34
import org.springframework.data.jpa.repository.Query
45
import org.springframework.data.repository.CrudRepository
56
import org.springframework.data.repository.query.Param
@@ -8,16 +9,19 @@ import java.util.*
89

910
interface ProjectStatisticsHistoryRepository : CrudRepository<ProjectStatisticsHistory, UUID> {
1011

11-
@Query("FROM ProjectStatisticsHistory ph WHERE ph.project.id = :id ORDER BY ph.date DESC")
12+
@Query("FROM ProjectStatisticsHistory ph WHERE ph.projectId = :id ORDER BY ph.date DESC")
1213
fun findByProjectId(@Param("id") id: Long): List<ProjectStatisticsHistory>
1314

14-
@Query("FROM ProjectStatisticsHistory ph WHERE ph.project.id = :id AND ph.date >= :startDate ORDER BY ph.date DESC")
15+
@Query("FROM ProjectStatisticsHistory ph WHERE ph.projectId = :id AND ph.date >= :startDate ORDER BY ph.date DESC")
1516
fun findByProjectIdAndDateGreaterThanEqual(@Param("id") id: Long, @Param("startDate") startDate: LocalDateTime): List<ProjectStatisticsHistory>
1617

17-
@Query(value = "FROM ProjectStatisticsHistory ph WHERE ph.project.id = :id AND ph.date < :endDate ORDER BY ph.date DESC")
18+
@Query(value = "FROM ProjectStatisticsHistory ph WHERE ph.projectId = :id AND ph.date < :endDate ORDER BY ph.date DESC")
1819
fun findByProjectIdAndDateLessThan(@Param("id") id: Long, @Param("endDate") endDate: LocalDateTime): List<ProjectStatisticsHistory>
1920

20-
@Query(value = "FROM ProjectStatisticsHistory ph WHERE ph.project.id = :id AND ph.date BETWEEN :startDate AND :endDate ORDER BY ph.date DESC")
21+
@Query(value = "FROM ProjectStatisticsHistory ph WHERE ph.projectId = :id AND ph.date BETWEEN :startDate AND :endDate ORDER BY ph.date DESC")
2122
fun findByProjectIdAndDateBetween(@Param("id") id: Long, @Param("startDate") startDate: LocalDateTime, @Param("endDate") endDate: LocalDateTime): List<ProjectStatisticsHistory>
2223

24+
@Query("DELETE FROM ProjectStatisticsHistory ph WHERE ph.projectId = :projectId AND ph.date BETWEEN :startDate AND :endDate")
25+
@Modifying
26+
fun deleteByProjectIdAndDateBetween(@Param("projectId") projectId: Long, @Param("startDate") startDate: LocalDateTime, @Param("endDate") endDate: LocalDateTime)
2327
}

ambassador-storage/src/test/kotlin/com/roche/ambassador/storage/project/ProjectEntityTest.kt

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,7 @@ import com.roche.ambassador.model.Visibility
44
import com.roche.ambassador.model.project.Permissions
55
import com.roche.ambassador.model.project.Project
66
import com.roche.ambassador.model.stats.Statistics
7-
import org.assertj.core.api.Assertions.assertThat
8-
import org.junit.jupiter.api.Test
97
import java.time.LocalDate
10-
import java.time.LocalDateTime
118

129
class ProjectEntityTest {
1310

@@ -21,16 +18,16 @@ class ProjectEntityTest {
2118
Permissions(true, true)
2219
)
2320

24-
@Test
25-
fun `should create new stats history entry when recording stats`() {
26-
val project = ProjectEntity(project = createProject(), lastIndexedDate = LocalDateTime.now(),)
27-
28-
val history = project.recordStatistics()
29-
30-
assertThat(project.statsHistory).hasSize(1)
31-
.containsExactly(history)
32-
assertThat(history)
33-
.extracting(ProjectStatisticsHistory::date, ProjectStatisticsHistory::project)
34-
.containsExactly(project.lastIndexedDate, project)
35-
}
21+
// @Test
22+
// fun `should create new stats history entry when recording stats`() {
23+
// val project = ProjectEntity(project = createProject(), lastIndexedDate = LocalDateTime.now(),)
24+
//
25+
// val history = project.recordStatistics()
26+
//
27+
// assertThat(project.statsHistory).hasSize(1)
28+
// .containsExactly(history)
29+
// assertThat(history)
30+
// .extracting(ProjectStatisticsHistory::date, ProjectStatisticsHistory::project)
31+
// .containsExactly(project.lastIndexedDate, project)
32+
// }
3633
}

0 commit comments

Comments
 (0)