14
14
*/
15
15
package org.modelix.model.server.store
16
16
17
- import com.google.common.collect.MultimapBuilder
17
+ import mu.KotlinLogging
18
18
import org.apache.ignite.Ignite
19
19
import org.apache.ignite.IgniteCache
20
20
import org.apache.ignite.Ignition
21
21
import org.modelix.model.IKeyListener
22
+ import org.modelix.model.persistent.HashUtil
22
23
import java.io.File
23
24
import java.io.FileReader
24
25
import java.io.IOException
25
26
import java.util.*
26
- import java.util.concurrent.Executors
27
27
import java.util.stream.Collectors
28
28
29
+ private val LOG = KotlinLogging .logger { }
30
+
29
31
class IgniteStoreClient (jdbcConfFile : File ? = null , inmemory : Boolean = false ) : IStoreClient, AutoCloseable {
30
- private val ignite: Ignite
32
+ private val ENTRY_CHANGED_TOPIC = " entryChanged"
33
+ private lateinit var ignite: Ignite
31
34
private val cache: IgniteCache <String , String ?>
32
- private val timer = Executors .newScheduledThreadPool(1 )
33
- private val listeners = MultimapBuilder .hashKeys().hashSetValues().build<String , IKeyListener >()
35
+ private val changeNotifier = ChangeNotifier (this )
36
+ private val pendingChangeMessages = PendingChangeMessages {
37
+ ignite.message().send(ENTRY_CHANGED_TOPIC , it)
38
+ }
34
39
35
40
/* *
36
41
* Istantiate an IgniteStoreClient
@@ -68,6 +73,13 @@ class IgniteStoreClient(jdbcConfFile: File? = null, inmemory: Boolean = false) :
68
73
// timer.scheduleAtFixedRate(() -> {
69
74
// System.out.println("stats: " + cache.metrics());
70
75
// }, 10, 10, TimeUnit.SECONDS);
76
+
77
+ ignite.message().localListen(ENTRY_CHANGED_TOPIC ) { nodeId: UUID ? , key: Any? ->
78
+ if (key is String ) {
79
+ changeNotifier.notifyListeners(key)
80
+ }
81
+ true
82
+ }
71
83
}
72
84
73
85
override fun get (key : String ): String? {
@@ -94,44 +106,27 @@ class IgniteStoreClient(jdbcConfFile: File? = null, inmemory: Boolean = false) :
94
106
override fun putAll (entries : Map <String , String ?>, silent : Boolean ) {
95
107
val deletes = entries.filterValues { it == null }
96
108
val puts = entries.filterValues { it != null }
97
- if (deletes.isNotEmpty()) cache.removeAll(deletes.keys)
98
- if (puts.isNotEmpty()) cache.putAll(puts)
99
- if (! silent) {
100
- for ((key, value) in entries) {
101
- ignite.message().send(key, value ? : IKeyListener .NULL_VALUE )
109
+ runTransaction {
110
+ if (deletes.isNotEmpty()) cache.removeAll(deletes.keys)
111
+ if (puts.isNotEmpty()) cache.putAll(puts)
112
+ if (! silent) {
113
+ for (key in entries.keys) {
114
+ if (HashUtil .isSha256(key)) continue
115
+ pendingChangeMessages.entryChanged(key)
116
+ }
102
117
}
103
118
}
104
119
}
105
120
106
121
override fun listen (key : String , listener : IKeyListener ) {
107
- synchronized(listeners) {
108
- val wasSubscribed = listeners.containsKey(key)
109
- listeners.put(key, listener)
110
- if (! wasSubscribed) {
111
- ignite.message()
112
- .localListen(
113
- key,
114
- ) { nodeId: UUID ? , value: Any? ->
115
- if (value is String ) {
116
- synchronized(listeners) {
117
- for (l in listeners[key].toList()) {
118
- try {
119
- l.changed(key, if (value == IKeyListener .NULL_VALUE ) null else value)
120
- } catch (ex: Exception ) {
121
- println (ex.message)
122
- ex.printStackTrace()
123
- }
124
- }
125
- }
126
- }
127
- true
128
- }
129
- }
130
- }
122
+ // Entries where the key is the SHA hash over the value are not expected to change and listening is unnecessary.
123
+ require(! HashUtil .isSha256(key)) { " Listener for $key will never get notified." }
124
+
125
+ changeNotifier.addListener(key, listener)
131
126
}
132
127
133
128
override fun removeListener (key : String , listener : IKeyListener ) {
134
- synchronized(listeners) { listeners.remove (key, listener) }
129
+ changeNotifier.removeListener (key, listener)
135
130
}
136
131
137
132
override fun generateId (key : String ): Long {
@@ -144,6 +139,7 @@ class IgniteStoreClient(jdbcConfFile: File? = null, inmemory: Boolean = false) :
144
139
transactions.txStart().use { tx ->
145
140
val result = body()
146
141
tx.commit()
142
+ pendingChangeMessages.flushChangeMessages()
147
143
return result
148
144
}
149
145
} else {
@@ -160,3 +156,62 @@ class IgniteStoreClient(jdbcConfFile: File? = null, inmemory: Boolean = false) :
160
156
dispose()
161
157
}
162
158
}
159
+
160
+ class PendingChangeMessages (private val notifier : (String ) -> Unit ) {
161
+ private val pendingChangeMessages = Collections .synchronizedSet(HashSet <String >())
162
+
163
+ @Synchronized
164
+ fun flushChangeMessages () {
165
+ for (pendingChangeMessage in pendingChangeMessages) {
166
+ notifier(pendingChangeMessage)
167
+ }
168
+ pendingChangeMessages.clear()
169
+ }
170
+
171
+ @Synchronized
172
+ fun entryChanged (key : String ) {
173
+ pendingChangeMessages + = key
174
+ }
175
+ }
176
+
177
+ class ChangeNotifier (val store : IStoreClient ) {
178
+ private val changeNotifiers = HashMap <String , EntryChangeNotifier >()
179
+
180
+ @Synchronized
181
+ fun notifyListeners (key : String ) {
182
+ changeNotifiers[key]?.notifyIfChanged()
183
+ }
184
+
185
+ @Synchronized
186
+ fun addListener (key : String , listener : IKeyListener ) {
187
+ changeNotifiers.getOrPut(key) { EntryChangeNotifier (key) }.listeners.add(listener)
188
+ }
189
+
190
+ @Synchronized
191
+ fun removeListener (key : String , listener : IKeyListener ) {
192
+ val notifier = changeNotifiers[key] ? : return
193
+ notifier.listeners.remove(listener)
194
+ if (notifier.listeners.isEmpty()) {
195
+ changeNotifiers.remove(key)
196
+ }
197
+ }
198
+
199
+ private inner class EntryChangeNotifier (val key : String ) {
200
+ val listeners = HashSet <IKeyListener >()
201
+ private var lastNotifiedValue: String? = null
202
+
203
+ fun notifyIfChanged () {
204
+ val value = store.get(key)
205
+ if (value == lastNotifiedValue) return
206
+ lastNotifiedValue = value
207
+
208
+ for (listener in listeners) {
209
+ try {
210
+ listener.changed(key, value)
211
+ } catch (ex: Exception ) {
212
+ LOG .error(" Exception in listener of $key " , ex)
213
+ }
214
+ }
215
+ }
216
+ }
217
+ }
0 commit comments