Skip to content

Commit 4079b79

Browse files
committed
[ChatAntiSpam] Rework cache storage: Holder for same SpamData instance, synchronized
1 parent 27c088c commit 4079b79

File tree

3 files changed

+84
-56
lines changed

3 files changed

+84
-56
lines changed

bukkit/src/main/kotlin/io/github/rothes/esu/bukkit/module/ChatAntiSpamModule.kt

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import io.github.rothes.esu.lib.configurate.objectmapping.meta.PostProcess
3131
import org.bukkit.Bukkit
3232
import org.incendo.cloud.component.DefaultValue
3333
import java.time.Duration
34-
import java.util.function.Consumer
3534
import kotlin.math.max
3635
import kotlin.math.pow
3736
import kotlin.time.Duration.Companion.milliseconds
@@ -45,7 +44,7 @@ object ChatAntiSpamModule: BukkitModule<ChatAntiSpamModule.ModuleConfig, ChatAnt
4544

4645
override fun onEnable() {
4746
CasDataManager
48-
purgeTask = Scheduler.asyncTicks(20, 5 * 60 * 20) { purgeCache(true) }
47+
purgeTask = Scheduler.asyncTicks(20, 5 * 60 * 20) { CasDataManager.purgeCache(true) }
4948
CasListeners.enable()
5049
Bukkit.getOnlinePlayers().map { it.user }.forEach {
5150
if (it.hasPerm("notify"))
@@ -58,7 +57,7 @@ object ChatAntiSpamModule: BukkitModule<ChatAntiSpamModule.ModuleConfig, ChatAnt
5857
).handler { context ->
5958
val sender = context.sender()
6059
val playerUser = context.get<PlayerUser>("player")
61-
val spamData = CasDataManager.cacheByIp[playerUser.addr]
60+
val spamData = CasDataManager.getCache(playerUser)
6261
if (spamData == null) {
6362
sender.message(lang, { command.data.noData }, user(playerUser), sender.msgPrefix)
6463
} else {
@@ -112,8 +111,7 @@ object ChatAntiSpamModule: BukkitModule<ChatAntiSpamModule.ModuleConfig, ChatAnt
112111
).handler { context ->
113112
val playerUser = context.get<PlayerUser>("player")
114113
CasDataManager.deleteAsync(playerUser.dbId)
115-
CasDataManager.cacheById.remove(playerUser.dbId)
116-
CasDataManager.cacheByIp.remove(playerUser.addr)
114+
CasDataManager.getHolder(playerUser).spamData = SpamData()
117115
context.sender().message(lang, { command.reset.resetPlayer },
118116
context.sender().msgPrefix,
119117
component("player", playerUser.player.displayName_)
@@ -132,7 +130,7 @@ object ChatAntiSpamModule: BukkitModule<ChatAntiSpamModule.ModuleConfig, ChatAnt
132130
purgeTask = null
133131
CasListeners.disable()
134132
try {
135-
purgeCache(false)
133+
CasDataManager.purgeCache(false)
136134
} catch (e: NoClassDefFoundError) {
137135
// Ehh.. Plugin Jar got deleted?
138136
plugin.err("Failed to purge cache while disabling module $name", e)
@@ -149,25 +147,6 @@ object ChatAntiSpamModule: BukkitModule<ChatAntiSpamModule.ModuleConfig, ChatAnt
149147
}
150148
}
151149

152-
private fun purgeCache(deleteDb: Boolean) {
153-
val time = System.currentTimeMillis()
154-
val toDel = mutableListOf<Any?>()
155-
val handler = Consumer<MutableMap<*, SpamData>> { map ->
156-
map.entries
157-
.filter { time - it.value.lastAccess > config.userDataExpiresAfter.toMillis() }
158-
.forEach {
159-
map.remove(it.key)
160-
if (deleteDb)
161-
toDel.add(it.key)
162-
}
163-
}
164-
handler.accept(CasDataManager.cacheById)
165-
handler.accept(CasDataManager.cacheByIp)
166-
if (toDel.isNotEmpty()) {
167-
CasDataManager.deleteExpiredAsync(keys = toDel)
168-
}
169-
}
170-
171150
val PlayerUser.spamData
172151
get() = CasDataManager[this].also { it.lastAccess = max(it.lastAccess, System.currentTimeMillis()) }
173152

bukkit/src/main/kotlin/io/github/rothes/esu/bukkit/module/chatantispam/user/CasDataManager.kt

Lines changed: 73 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ import org.jetbrains.exposed.v1.datetime.datetime
2020
import org.jetbrains.exposed.v1.jdbc.*
2121
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
2222
import org.jetbrains.exposed.v1.json.json
23+
import java.util.concurrent.locks.ReentrantReadWriteLock
24+
import kotlin.concurrent.read
25+
import kotlin.concurrent.write
2326

2427
object CasDataManager {
2528

@@ -29,17 +32,52 @@ object CasDataManager {
2932
val lastAccess = datetime("last_access")
3033
val data = json<SpamData>("data", { it.serialize() }, { it.deserialize() })
3134
}
32-
val cacheById = hashMapOf<Int, SpamData>()
33-
val cacheByIp = hashMapOf<String, SpamData>()
35+
private val cacheById = hashMapOf<Int, SpamDataHolder>()
36+
private val cacheByIp = hashMapOf<String, SpamDataHolder>()
37+
private val lock = ReentrantReadWriteLock()
3438

3539
operator fun get(user: PlayerUser): SpamData {
36-
cacheByIp[user.addr]?.let {
37-
return it
40+
return getHolder(user).spamData
41+
}
42+
43+
fun getHolder(user: PlayerUser): SpamDataHolder {
44+
lock.read {
45+
cacheByIp[user.addr]?.let {
46+
return it
47+
}
48+
}
49+
lock.write {
50+
val created = SpamDataHolder(SpamData())
51+
cacheById[user.dbId] = created
52+
cacheByIp[user.addr] = created
53+
return created
54+
}
55+
}
56+
57+
fun getCache(user: PlayerUser): SpamData? {
58+
return lock.read { cacheByIp[user.addr]?.spamData }
59+
}
60+
61+
fun purgeCache(deleteDb: Boolean) {
62+
val time = System.currentTimeMillis()
63+
val toDel = mutableListOf<Any>()
64+
val handler = { map: MutableMap<out Any, SpamDataHolder> ->
65+
val iterator = map.iterator()
66+
for ((key, value) in iterator) {
67+
if (time - value.spamData.lastAccess > config.userDataExpiresAfter.toMillis()) {
68+
if (deleteDb)
69+
toDel.add(key)
70+
iterator.remove()
71+
}
72+
}
73+
}
74+
lock.write {
75+
handler(cacheById)
76+
handler(cacheByIp)
77+
}
78+
if (toDel.isNotEmpty()) {
79+
deleteExpiredAsync(keys = toDel)
3880
}
39-
val created = SpamData()
40-
cacheById[user.dbId] = created
41-
cacheByIp[user.addr] = created
42-
return created
4381
}
4482

4583
init {
@@ -87,51 +125,55 @@ object CasDataManager {
87125
Bukkit.getOnlinePlayers().forEach { loadSpamData(it.user) }
88126
}
89127

90-
fun loadSpamData(where: PlayerUser, async: Boolean = true) {
128+
fun loadSpamData(where: PlayerUser) {
91129
val dbId = where.dbId
92130
val addr = where.addr
93131

94-
fun func() {
95-
var spamData = latest(cacheById[dbId], cacheByIp[addr]) // Current cached
132+
StorageManager.coroutineScope.launch {
96133
with(ChatSpamTable) {
97134
transaction(database) {
98135
selectAll().where { (ip eq addr) or (user eq dbId) }.orderBy(lastAccess, SortOrder.DESC)
99136
.limit(1).singleOrNull()?.let { row ->
100-
spamData = latest(spamData, row[data])!!.also { sd ->
137+
val cached = lock.read { latest(cacheById[dbId], cacheByIp[addr]) }
138+
latest(cached, row[data]).also { holder ->
101139
val ip = row[ip]
102-
cacheById[row[user]] = sd
103-
cacheByIp[ip] = sd
104-
Bukkit.getOnlinePlayers().filter { it.address!!.hostString == ip }.forEach { cacheById[it.user.dbId] = sd }
140+
lock.write {
141+
cacheById[row[user]] = holder
142+
cacheByIp[ip] = holder
143+
}
105144
}
106145
}
107146
}
108147
}
109-
spamData?.let { sd ->
110-
cacheById[dbId] = sd
111-
cacheByIp[addr] = sd
112-
Bukkit.getOnlinePlayers().filter { it.address!!.hostString == addr }.forEach { cacheById[it.user.dbId] = sd }
113-
}
114-
}
115-
116-
if (async) {
117-
StorageManager.coroutineScope.launch {
118-
func()
119-
}
120-
} else {
121-
func()
122148
}
123149
}
124150

125-
private fun latest(o1: SpamData?, o2: SpamData?): SpamData? {
151+
private fun latest(o1: SpamDataHolder?, o2: SpamDataHolder?): SpamDataHolder? {
126152
if (o1 == null)
127153
return o2
128154
if (o2 == null)
129155
return null
130-
return if (o1.lastAccess > o2.lastAccess) o1 else o2
156+
return if (o1.spamData.lastAccess > o2.spamData.lastAccess) {
157+
o2.spamData = o1.spamData
158+
o1
159+
} else {
160+
o1.spamData = o2.spamData
161+
o2
162+
}
163+
}
164+
165+
private fun latest(holder: SpamDataHolder?, data: SpamData): SpamDataHolder {
166+
if (holder == null)
167+
return SpamDataHolder(data)
168+
if (holder.spamData.lastAccess < data.lastAccess) {
169+
holder.spamData = data
170+
}
171+
return holder
131172
}
132173

133174
fun saveSpamDataNow(where: PlayerUser) {
134-
val spamData = latest(cacheById[where.dbId], cacheByIp[where.addr]) ?: return
175+
val holder = lock.read { latest(cacheById[where.dbId], cacheByIp[where.addr]) } ?: return
176+
val spamData = holder.spamData
135177
val lastAccessValue = kotlin.math.max(spamData.lastAccess, spamData.muteUntil).localDateTime
136178
with(ChatSpamTable) {
137179
transaction(database) {
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package io.github.rothes.esu.bukkit.module.chatantispam.user
2+
3+
data class SpamDataHolder(
4+
@get:Synchronized
5+
@set:Synchronized
6+
var spamData: SpamData
7+
)

0 commit comments

Comments
 (0)