Skip to content

Commit b77fb66

Browse files
committed
fix(server): ref-count cache event listeners
- Replace the per-transaction cache listener owner flag with a per-graph CacheListenerHolder shared by graph and schema cache transactions. - The holder keeps the EventHub listener registered while any transaction for the graph is alive, and unregisters/removes it only when the last transaction releases it. The registry update, ref-count decrement, and hub unlisten now run inside ConcurrentMap.compute() to avoid owner-closes-first invalidation gaps. Also add graph/schema regression coverage for owner-first close and last-close cleanup, including graph close/reopen handling for stale EventHub holders.
1 parent 4369458 commit b77fb66

6 files changed

Lines changed: 370 additions & 85 deletions

File tree

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.hugegraph.backend.cache;
19+
20+
import org.apache.hugegraph.event.EventHub;
21+
import org.apache.hugegraph.event.EventListener;
22+
23+
/*
24+
* Listener lifetime must cover all active transactions for the graph.
25+
* The holder is removed from the registry and unregistered from EventHub
26+
* only when the last transaction releases it.
27+
*/
28+
final class CacheListenerHolder {
29+
30+
final EventListener listener;
31+
final EventHub hub;
32+
int refCount;
33+
34+
CacheListenerHolder(EventListener listener, EventHub hub) {
35+
this.listener = listener;
36+
this.hub = hub;
37+
this.refCount = 1;
38+
}
39+
}

hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CachedGraphTransaction.java

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import java.util.Iterator;
2525
import java.util.List;
2626
import java.util.Set;
27+
import java.util.concurrent.ConcurrentHashMap;
28+
import java.util.concurrent.ConcurrentMap;
2729

2830
import org.apache.hugegraph.HugeGraphParams;
2931
import org.apache.hugegraph.backend.cache.CachedBackendStore.QueryId;
@@ -60,12 +62,20 @@ public final class CachedGraphTransaction extends GraphTransaction {
6062
private static final long AVG_VERTEX_ENTRY_SIZE = 40L;
6163
private static final long AVG_EDGE_ENTRY_SIZE = 100L;
6264

65+
/*
66+
* Listener lifetime must cover all active transactions for the graph.
67+
* The holder is removed from the registry and unregistered from EventHub
68+
* only when the last transaction releases it.
69+
*/
70+
private static final ConcurrentMap<String, CacheListenerHolder>
71+
graphCacheEventListeners = new ConcurrentHashMap<>();
72+
6373
private final Cache<Id, Object> verticesCache;
6474
private final Cache<Id, Object> edgesCache;
6575

6676
private EventListener storeEventListener;
6777
private EventListener cacheEventListener;
68-
private boolean registeredCacheEventListener;
78+
private CacheListenerHolder holder;
6979

7080
public CachedGraphTransaction(HugeGraphParams graph, BackendStore store) {
7181
super(graph, store);
@@ -186,28 +196,44 @@ private void listenChanges() {
186196
return false;
187197
};
188198
EventHub graphEventHub = this.params().graphEventHub();
189-
EventListener previous =
190-
graphCacheEventListeners.putIfAbsent(
191-
this.params().spaceGraphName(), listener);
192-
if (previous == null) {
193-
this.cacheEventListener = listener;
194-
this.registeredCacheEventListener = true;
195-
graphEventHub.listen(Events.CACHE, this.cacheEventListener);
196-
} else {
197-
this.cacheEventListener = previous;
198-
this.registeredCacheEventListener = false;
199-
}
199+
String graphName = this.params().spaceGraphName();
200+
CacheListenerHolder acquired = graphCacheEventListeners.compute(
201+
graphName, (key, existing) -> {
202+
if (existing == null || existing.hub != graphEventHub) {
203+
// Graph close/reopen creates a new EventHub for the
204+
// same graph name; replace the stale holder. Old
205+
// transactions skip decrement via identity check.
206+
if (existing != null) {
207+
existing.hub.unlisten(Events.CACHE,
208+
existing.listener);
209+
}
210+
graphEventHub.listen(Events.CACHE, listener);
211+
return new CacheListenerHolder(listener, graphEventHub);
212+
}
213+
existing.refCount++;
214+
return existing;
215+
});
216+
this.holder = acquired;
217+
this.cacheEventListener = acquired.listener;
200218
}
201219

202220
private void unlistenChanges() {
203221
String graphName = this.params().spaceGraphName();
204-
if (this.registeredCacheEventListener) {
205-
EventHub graphEventHub = this.params().graphEventHub();
206-
if (graphCacheEventListeners.remove(graphName,
207-
this.cacheEventListener)) {
208-
graphEventHub.unlisten(Events.CACHE, this.cacheEventListener);
209-
}
210-
this.registeredCacheEventListener = false;
222+
CacheListenerHolder ours = this.holder;
223+
if (ours != null) {
224+
graphCacheEventListeners.compute(graphName, (key, existing) -> {
225+
if (existing == null || existing != ours) {
226+
return existing;
227+
}
228+
existing.refCount--;
229+
if (existing.refCount == 0) {
230+
existing.hub.unlisten(Events.CACHE, existing.listener);
231+
return null;
232+
}
233+
return existing;
234+
});
235+
this.holder = null;
236+
this.cacheEventListener = null;
211237
}
212238
if (storeEventListenStatus.remove(graphName) != null) {
213239
this.store().provider().unlisten(this.storeEventListener);

hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CachedSchemaTransaction.java

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,12 @@
4343

4444
public final class CachedSchemaTransaction extends SchemaTransaction {
4545

46-
private static final ConcurrentMap<String, EventListener>
46+
/*
47+
* Listener lifetime must cover all active transactions for the graph.
48+
* The holder is removed from the registry and unregistered from EventHub
49+
* only when the last transaction releases it.
50+
*/
51+
private static final ConcurrentMap<String, CacheListenerHolder>
4752
SCHEMA_CACHE_EVENT_LISTENERS = new ConcurrentHashMap<>();
4853

4954
private final Cache<Id, Object> idCache;
@@ -53,7 +58,7 @@ public final class CachedSchemaTransaction extends SchemaTransaction {
5358

5459
private EventListener storeEventListener;
5560
private EventListener cacheEventListener;
56-
private boolean registeredCacheEventListener;
61+
private CacheListenerHolder holder;
5762

5863
public CachedSchemaTransaction(HugeGraphParams graph, BackendStore store) {
5964
super(graph, store);
@@ -138,29 +143,49 @@ private void listenChanges() {
138143
};
139144
EventHub schemaEventHub = this.params().schemaEventHub();
140145
String graph = this.params().spaceGraphName();
141-
EventListener previous =
142-
SCHEMA_CACHE_EVENT_LISTENERS.putIfAbsent(graph, listener);
143-
if (previous == null) {
144-
this.cacheEventListener = listener;
145-
this.registeredCacheEventListener = true;
146-
schemaEventHub.listen(Events.CACHE, this.cacheEventListener);
147-
} else {
148-
this.cacheEventListener = previous;
149-
this.registeredCacheEventListener = false;
150-
}
146+
CacheListenerHolder acquired = SCHEMA_CACHE_EVENT_LISTENERS.compute(
147+
graph, (key, existing) -> {
148+
if (existing == null || existing.hub != schemaEventHub) {
149+
// Graph close/reopen creates a new EventHub for the
150+
// same graph name; replace the stale holder. Old
151+
// transactions skip decrement via identity check.
152+
if (existing != null) {
153+
existing.hub.unlisten(Events.CACHE,
154+
existing.listener);
155+
}
156+
schemaEventHub.listen(Events.CACHE, listener);
157+
return new CacheListenerHolder(listener,
158+
schemaEventHub);
159+
}
160+
existing.refCount++;
161+
return existing;
162+
});
163+
this.holder = acquired;
164+
this.cacheEventListener = acquired.listener;
151165
}
152166

153167
private void unlistenChanges() {
154168
// Unlisten store event
155169
this.store().provider().unlisten(this.storeEventListener);
156170

157171
// Unlisten cache event
158-
EventHub schemaEventHub = this.params().schemaEventHub();
159-
if (this.registeredCacheEventListener) {
160-
schemaEventHub.unlisten(Events.CACHE, this.cacheEventListener);
161-
SCHEMA_CACHE_EVENT_LISTENERS.remove(this.params().spaceGraphName(),
162-
this.cacheEventListener);
163-
this.registeredCacheEventListener = false;
172+
CacheListenerHolder ours = this.holder;
173+
if (ours != null) {
174+
SCHEMA_CACHE_EVENT_LISTENERS.compute(
175+
this.params().spaceGraphName(), (key, existing) -> {
176+
if (existing == null || existing != ours) {
177+
return existing;
178+
}
179+
existing.refCount--;
180+
if (existing.refCount == 0) {
181+
existing.hub.unlisten(Events.CACHE,
182+
existing.listener);
183+
return null;
184+
}
185+
return existing;
186+
});
187+
this.holder = null;
188+
this.cacheEventListener = null;
164189
}
165190
}
166191

hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphTransaction.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@
6060
import org.apache.hugegraph.backend.store.BackendStore;
6161
import org.apache.hugegraph.config.CoreOptions;
6262
import org.apache.hugegraph.config.HugeConfig;
63-
import org.apache.hugegraph.event.EventListener;
6463
import org.apache.hugegraph.exception.LimitExceedException;
6564
import org.apache.hugegraph.exception.NotFoundException;
6665
import org.apache.hugegraph.iterator.BatchMapperIterator;
@@ -141,8 +140,6 @@ public class GraphTransaction extends IndexableTransaction {
141140

142141
private final int verticesCapacity;
143142
private final int edgesCapacity;
144-
protected static final ConcurrentHashMap<String, EventListener>
145-
graphCacheEventListeners = new ConcurrentHashMap<>();
146143
protected static final ConcurrentHashMap<String, Boolean> storeEventListenStatus =
147144
new ConcurrentHashMap<>();
148145

0 commit comments

Comments
 (0)