1414 */
1515package org.modelix.model.server.store
1616
17- import com.google.common.collect.MultimapBuilder
17+ import mu.KotlinLogging
1818import org.apache.ignite.Ignite
1919import org.apache.ignite.IgniteCache
2020import org.apache.ignite.Ignition
2121import org.modelix.model.IKeyListener
22+ import org.modelix.model.persistent.HashUtil
2223import java.io.File
2324import java.io.FileReader
2425import java.io.IOException
2526import java.util.*
26- import java.util.concurrent.Executors
2727import java.util.stream.Collectors
2828
29+ private val LOG = KotlinLogging .logger { }
30+
2931class 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
3134 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+ }
3439
3540 /* *
3641 * Istantiate an IgniteStoreClient
@@ -68,6 +73,13 @@ class IgniteStoreClient(jdbcConfFile: File? = null, inmemory: Boolean = false) :
6873 // timer.scheduleAtFixedRate(() -> {
6974 // System.out.println("stats: " + cache.metrics());
7075 // }, 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+ }
7183 }
7284
7385 override fun get (key : String ): String? {
@@ -94,44 +106,27 @@ class IgniteStoreClient(jdbcConfFile: File? = null, inmemory: Boolean = false) :
94106 override fun putAll (entries : Map <String , String ?>, silent : Boolean ) {
95107 val deletes = entries.filterValues { it == null }
96108 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+ }
102117 }
103118 }
104119 }
105120
106121 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)
131126 }
132127
133128 override fun removeListener (key : String , listener : IKeyListener ) {
134- synchronized(listeners) { listeners.remove (key, listener) }
129+ changeNotifier.removeListener (key, listener)
135130 }
136131
137132 override fun generateId (key : String ): Long {
@@ -144,6 +139,7 @@ class IgniteStoreClient(jdbcConfFile: File? = null, inmemory: Boolean = false) :
144139 transactions.txStart().use { tx ->
145140 val result = body()
146141 tx.commit()
142+ pendingChangeMessages.flushChangeMessages()
147143 return result
148144 }
149145 } else {
@@ -160,3 +156,62 @@ class IgniteStoreClient(jdbcConfFile: File? = null, inmemory: Boolean = false) :
160156 dispose()
161157 }
162158}
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