Skip to content

Commit be84a65

Browse files
authored
Merge pull request #2534 from digma-ai/thread-safe-in-daily-engagement
thread-safe-in-daily-engagement
2 parents bfbd216 + 39cadba commit be84a65

File tree

5 files changed

+252
-48
lines changed

5 files changed

+252
-48
lines changed

ide-common/src/main/kotlin/org/digma/intellij/plugin/engagement/EngagementScorePersistence.kt

Lines changed: 129 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,62 +4,168 @@ import com.intellij.openapi.components.PersistentStateComponent
44
import com.intellij.openapi.components.Service
55
import com.intellij.openapi.components.State
66
import com.intellij.openapi.components.Storage
7+
import com.intellij.openapi.components.service
78
import com.intellij.util.xmlb.Converter
89
import com.intellij.util.xmlb.annotations.OptionTag
910
import com.intellij.util.xmlb.annotations.XMap
11+
import com.jetbrains.rd.util.ConcurrentHashMap
12+
import kotlinx.datetime.DatePeriod
1013
import kotlinx.datetime.LocalDate
14+
import kotlinx.datetime.plus
15+
import java.util.concurrent.locks.ReentrantLock
16+
import kotlin.concurrent.withLock
1117

18+
@Service(Service.Level.APP)
19+
class EngagementScorePersistenceService {
20+
21+
private val persistence = service<EngagementScorePersistence>()
22+
23+
fun removeOldEntries(today: LocalDate, periodToTrack: DatePeriod) {
24+
persistence.removeOldEntries(today, periodToTrack)
25+
}
26+
27+
fun getLastEventTime(): LocalDate? {
28+
return persistence.getLastEventTime()
29+
}
30+
31+
fun getDaysForAverage(today: LocalDate): Map<String, Int> {
32+
return persistence.getDaysForAverage(today)
33+
}
34+
35+
fun setLastEventTime(today: LocalDate) {
36+
persistence.setLastEventTime(today)
37+
}
38+
39+
fun setLatestRegisteredActiveDays(activeDays: Long) {
40+
persistence.setLatestRegisteredActiveDays(activeDays)
41+
}
42+
43+
fun setLatestRegisteredAverage(average: Long) {
44+
persistence.setLatestRegisteredAverage(average)
45+
}
46+
47+
fun increment(today: LocalDate) {
48+
persistence.increment(today)
49+
}
50+
51+
fun getLatestRegisteredActiveDays(): Long {
52+
return persistence.getLatestRegisteredActiveDays()
53+
}
54+
55+
fun getLatestRegisteredAverage(): Long {
56+
return persistence.getLatestRegisteredAverage()
57+
}
1258

59+
}
60+
61+
62+
//don't use directly, use EngagementScorePersistenceService for thread safety
1363
@State(
1464
name = "org.digma.intellij.plugin.engagement.EngagementScorePersistence",
1565
storages = [Storage("DigmaEngagementScorePersistence.xml")]
1666
)
1767
@Service(Service.Level.APP)
18-
class EngagementScorePersistence : PersistentStateComponent<EngagementScoreData> {
68+
private class EngagementScorePersistence : PersistentStateComponent<EngagementScorePersistence.EngagementScoreData> {
1969

2070
private var myPersistenceData = EngagementScoreData()
2171

72+
private val lock = ReentrantLock(true)
73+
74+
//don't use getState to get properties from EngagementScoreData, always use dedicated methods that use myPersistenceData.
75+
// getState is called by the intellij framework and because it has a map it may happen that we write
76+
// to the map while the framework is serializing. getState returns a copy to avoid corrupted serialization.
2277
override fun getState(): EngagementScoreData {
23-
return myPersistenceData
78+
lock.withLock {
79+
return EngagementScoreData(myPersistenceData)
80+
}
2481
}
2582

2683
override fun loadState(state: EngagementScoreData) {
27-
myPersistenceData = state
84+
lock.withLock {
85+
myPersistenceData = EngagementScoreData(state)
86+
}
2887
}
29-
}
3088

31-
class EngagementScoreData {
32-
@OptionTag(converter = LocalDateConverter::class)
33-
var lastEventTime: LocalDate? = null
89+
fun removeOldEntries(today: LocalDate, periodToTrack: DatePeriod) {
90+
lock.withLock {
91+
val oldEntries = myPersistenceData.meaningfulActionsCounters.keys.filter {
92+
LocalDate.parse(it).plus(periodToTrack) < today
93+
}
3494

35-
//using string as day and not LocalDate because it's a bit messy to serialise LocalDate as map keys
36-
// with this persistence framework.
37-
@get:XMap(keyAttributeName = "day", valueAttributeName = "count")
38-
var meaningfulActionsCounters = mutableMapOf<String, Int>()
39-
40-
var latestRegisteredActiveDays: Long = 0
41-
var latestRegisteredAverage: Long = 0
95+
oldEntries.forEach {
96+
myPersistenceData.meaningfulActionsCounters.remove(it)
97+
}
98+
}
99+
}
42100

101+
fun setLastEventTime(today: LocalDate) {
102+
lock.withLock {
103+
myPersistenceData.lastEventTime = today
104+
}
105+
}
43106

44-
fun put(date: LocalDate, count: Int) {
45-
meaningfulActionsCounters[date.toString()] = count
107+
fun setLatestRegisteredActiveDays(activeDays: Long) {
108+
lock.withLock {
109+
myPersistenceData.latestRegisteredActiveDays = activeDays
110+
}
46111
}
47112

48-
fun get(date: LocalDate): Int? {
49-
return meaningfulActionsCounters[date.toString()]
113+
fun setLatestRegisteredAverage(average: Long) {
114+
lock.withLock {
115+
myPersistenceData.latestRegisteredAverage = average
116+
}
50117
}
51118

52119
fun increment(date: LocalDate) {
53-
val count = meaningfulActionsCounters[date.toString()]
54-
meaningfulActionsCounters[date.toString()] = count.increment()
120+
lock.withLock {
121+
val count = myPersistenceData.meaningfulActionsCounters[date.toString()]
122+
myPersistenceData.meaningfulActionsCounters[date.toString()] = count.increment()
123+
}
124+
}
125+
126+
fun getLastEventTime(): LocalDate? {
127+
return myPersistenceData.lastEventTime
55128
}
56129

57-
fun remove(date: LocalDate) {
58-
meaningfulActionsCounters.remove(date.toString())
130+
fun getDaysForAverage(today: LocalDate): Map<String, Int> {
131+
return myPersistenceData.meaningfulActionsCounters.filter {
132+
LocalDate.parse(it.key) != today
133+
}
134+
}
135+
136+
fun getLatestRegisteredActiveDays(): Long {
137+
return myPersistenceData.latestRegisteredActiveDays
138+
}
139+
140+
fun getLatestRegisteredAverage(): Long {
141+
return myPersistenceData.latestRegisteredAverage
59142
}
60143

61144
fun remove(date: String) {
62-
meaningfulActionsCounters.remove(date)
145+
lock.withLock {
146+
myPersistenceData.meaningfulActionsCounters.remove(date)
147+
}
148+
}
149+
150+
class EngagementScoreData() {
151+
152+
constructor(toCopy: EngagementScoreData) : this() {
153+
this.lastEventTime = toCopy.lastEventTime
154+
this.latestRegisteredAverage = toCopy.latestRegisteredAverage
155+
this.latestRegisteredActiveDays = toCopy.latestRegisteredActiveDays
156+
this.meaningfulActionsCounters.putAll(toCopy.meaningfulActionsCounters)
157+
}
158+
159+
@OptionTag(converter = LocalDateConverter::class)
160+
var lastEventTime: LocalDate? = null
161+
162+
//using string as day and not LocalDate because it's a bit messy to serialise LocalDate as map keys
163+
// with this persistence framework.
164+
@get:XMap(keyAttributeName = "day", valueAttributeName = "count")
165+
var meaningfulActionsCounters = ConcurrentHashMap(mutableMapOf<String, Int>())
166+
167+
var latestRegisteredActiveDays: Long = 0
168+
var latestRegisteredAverage: Long = 0
63169
}
64170
}
65171

ide-common/src/main/kotlin/org/digma/intellij/plugin/engagement/EngagementScoreService.kt

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package org.digma.intellij.plugin.engagement
22

33
import com.intellij.openapi.components.Service
44
import com.intellij.openapi.components.service
5+
import com.intellij.openapi.diagnostic.Logger
56
import kotlinx.coroutines.CoroutineScope
67
import kotlinx.coroutines.delay
78
import kotlinx.coroutines.isActive
@@ -10,7 +11,6 @@ import kotlinx.datetime.Clock
1011
import kotlinx.datetime.DatePeriod
1112
import kotlinx.datetime.LocalDate
1213
import kotlinx.datetime.TimeZone
13-
import kotlinx.datetime.plus
1414
import kotlinx.datetime.todayIn
1515
import org.digma.intellij.plugin.common.DisposableAdaptor
1616
import org.digma.intellij.plugin.common.findActiveProject
@@ -25,6 +25,8 @@ import kotlin.time.Duration.Companion.minutes
2525
@Service(Service.Level.APP)
2626
class EngagementScoreService(private val cs: CoroutineScope) : DisposableAdaptor {
2727

28+
val logger = Logger.getInstance(this::class.java)
29+
2830
companion object {
2931

3032
@JvmStatic
@@ -50,7 +52,7 @@ class EngagementScoreService(private val cs: CoroutineScope) : DisposableAdaptor
5052
private val PERIOD_TO_TRACK = DatePeriod.parse("P21D")
5153
private val TIME_ZONE = TimeZone.currentSystemDefault()
5254

53-
fun today(): LocalDate {
55+
private fun today(): LocalDate {
5456
return Clock.System.todayIn(TIME_ZONE)
5557
}
5658
}
@@ -60,7 +62,7 @@ class EngagementScoreService(private val cs: CoroutineScope) : DisposableAdaptor
6062

6163
disposingPeriodicTask("DailyEngagementScore", 1.minutes.inWholeMilliseconds, 6.hours.inWholeMilliseconds, false) {
6264
try {
63-
removeOldEntries()
65+
service<EngagementScorePersistenceService>().removeOldEntries(today(), PERIOD_TO_TRACK)
6466
if (isDailyEventTime()) {
6567
sendEvent()
6668
}
@@ -73,9 +75,7 @@ class EngagementScoreService(private val cs: CoroutineScope) : DisposableAdaptor
7375
private fun sendEvent() {
7476

7577
//compute average that includes up to the last day, exclude today.
76-
val daysForAverage = service<EngagementScorePersistence>().state.meaningfulActionsCounters.filter {
77-
LocalDate.parse(it.key) != today()
78-
}
78+
val daysForAverage = service<EngagementScorePersistenceService>().getDaysForAverage(today())
7979

8080
if (daysForAverage.isEmpty()) {
8181
return
@@ -109,46 +109,40 @@ class EngagementScoreService(private val cs: CoroutineScope) : DisposableAdaptor
109109
project?.let {
110110

111111
//update only if really sent the event
112-
service<EngagementScorePersistence>().state.lastEventTime = today()
113-
service<EngagementScorePersistence>().state.latestRegisteredActiveDays = activeDays
114-
service<EngagementScorePersistence>().state.latestRegisteredAverage = average
112+
service<EngagementScorePersistenceService>().setLastEventTime(today())
113+
service<EngagementScorePersistenceService>().setLatestRegisteredActiveDays(activeDays)
114+
service<EngagementScorePersistenceService>().setLatestRegisteredAverage(average)
115115

116116
ActivityMonitor.getInstance(it).registerEngagementScore(activeDays, average)
117117
}
118118
}
119119
}
120120

121121
private fun isDailyEventTime(): Boolean {
122-
return service<EngagementScorePersistence>().state.lastEventTime?.let {
122+
return service<EngagementScorePersistenceService>().getLastEventTime()?.let {
123123
today() > it
124124
} ?: true
125125
}
126126

127127

128-
private fun removeOldEntries() {
129-
val oldEntries = service<EngagementScorePersistence>().state.meaningfulActionsCounters.keys.filter {
130-
LocalDate.parse(it).plus(PERIOD_TO_TRACK) < today()
131-
}
132128

133-
oldEntries.forEach {
134-
service<EngagementScorePersistence>().state.remove(it)
135-
}
136-
}
137129

138130

139131
fun addAction(action: String) {
140-
if (MEANINGFUL_ACTIONS.contains(action)) {
141-
service<EngagementScorePersistence>().state.increment(today())
132+
cs.launch {
133+
if (MEANINGFUL_ACTIONS.contains(action)) {
134+
service<EngagementScorePersistenceService>().increment(today())
135+
}
142136
}
143137
}
144138

145139

146140
fun getLatestRegisteredActiveDays(): Long {
147-
return service<EngagementScorePersistence>().state.latestRegisteredActiveDays
141+
return service<EngagementScorePersistenceService>().getLatestRegisteredActiveDays()
148142
}
149143

150144
fun getLatestRegisteredAverage(): Long {
151-
return service<EngagementScorePersistence>().state.latestRegisteredAverage
145+
return service<EngagementScorePersistenceService>().getLatestRegisteredAverage()
152146
}
153147

154148
}

0 commit comments

Comments
 (0)