DefaultSubscriptionRegistry: Reduced thread contention
* DestinationCache is now synchronized on multiple 'destination' locks (previously a single shared lock) * DestinationCache keeps destinations without any subscriptions (previously such destinations were recomputed over and over) * SessionSubscriptionRegistry is now a 'sessionId -> subscriptionId -> (destination,selector)' map for faster lookups (previously 'sessionId -> destination -> set of (subscriptionId,selector)') closes gh-24395
This commit is contained in:
committed by
Rossen Stoyanchev
parent
b0570cd3a6
commit
524ca1a676
@@ -16,15 +16,17 @@
|
||||
|
||||
package org.springframework.messaging.simp.broker;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.Expression;
|
||||
@@ -34,6 +36,7 @@ import org.springframework.expression.TypedValue;
|
||||
import org.springframework.expression.spel.SpelEvaluationException;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.expression.spel.support.SimpleEvaluationContext;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
@@ -72,7 +75,7 @@ public class DefaultSubscriptionRegistry extends AbstractSubscriptionRegistry {
|
||||
|
||||
private PathMatcher pathMatcher = new AntPathMatcher();
|
||||
|
||||
private volatile int cacheLimit = DEFAULT_CACHE_LIMIT;
|
||||
private int cacheLimit = DEFAULT_CACHE_LIMIT;
|
||||
|
||||
@Nullable
|
||||
private String selectorHeaderName = "selector";
|
||||
@@ -106,6 +109,7 @@ public class DefaultSubscriptionRegistry extends AbstractSubscriptionRegistry {
|
||||
*/
|
||||
public void setCacheLimit(int cacheLimit) {
|
||||
this.cacheLimit = cacheLimit;
|
||||
this.destinationCache.ensureCacheLimit();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,14 +146,17 @@ public class DefaultSubscriptionRegistry extends AbstractSubscriptionRegistry {
|
||||
return this.selectorHeaderName;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void addSubscriptionInternal(
|
||||
String sessionId, String subsId, String destination, Message<?> message) {
|
||||
|
||||
protected void addSubscriptionInternal(@NonNull String sessionId, @NonNull String subscriptionId,
|
||||
@NonNull String destination, @NonNull Message<?> message) {
|
||||
Expression expression = getSelectorExpression(message.getHeaders());
|
||||
this.subscriptionRegistry.addSubscription(sessionId, subsId, destination, expression);
|
||||
this.destinationCache.updateAfterNewSubscription(destination, sessionId, subsId);
|
||||
boolean isAntPattern = this.pathMatcher.isPattern(destination);
|
||||
Subscription subscription = new Subscription(subscriptionId, expression, destination, isAntPattern);
|
||||
|
||||
Subscription previousValue = this.subscriptionRegistry.addSubscription(sessionId, subscriptionId, subscription);
|
||||
if (previousValue == null) {
|
||||
this.destinationCache.updateAfterNewSubscription(destination, isAntPattern, sessionId, subscriptionId);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -179,9 +186,9 @@ public class DefaultSubscriptionRegistry extends AbstractSubscriptionRegistry {
|
||||
protected void removeSubscriptionInternal(String sessionId, String subsId, Message<?> message) {
|
||||
SessionSubscriptionInfo info = this.subscriptionRegistry.getSubscriptions(sessionId);
|
||||
if (info != null) {
|
||||
String destination = info.removeSubscription(subsId);
|
||||
if (destination != null) {
|
||||
this.destinationCache.updateAfterRemovedSubscription(sessionId, subsId);
|
||||
Subscription subscription = info.removeSubscription(subsId);
|
||||
if (subscription != null) {
|
||||
this.destinationCache.updateAfterRemovedSubscription(sessionId, subscription);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -190,13 +197,13 @@ public class DefaultSubscriptionRegistry extends AbstractSubscriptionRegistry {
|
||||
public void unregisterAllSubscriptions(String sessionId) {
|
||||
SessionSubscriptionInfo info = this.subscriptionRegistry.removeSubscriptions(sessionId);
|
||||
if (info != null) {
|
||||
this.destinationCache.updateAfterRemovedSession(info);
|
||||
this.destinationCache.updateAfterRemovedSession(sessionId, info.getSubscriptions());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MultiValueMap<String, String> findSubscriptionsInternal(String destination, Message<?> message) {
|
||||
MultiValueMap<String, String> result = this.destinationCache.getSubscriptions(destination, message);
|
||||
MultiValueMap<String, String> result = this.destinationCache.getSubscriptions(destination);
|
||||
return filterSubscriptions(result, message);
|
||||
}
|
||||
|
||||
@@ -207,168 +214,181 @@ public class DefaultSubscriptionRegistry extends AbstractSubscriptionRegistry {
|
||||
return allMatches;
|
||||
}
|
||||
MultiValueMap<String, String> result = new LinkedMultiValueMap<>(allMatches.size());
|
||||
allMatches.forEach((sessionId, subIds) -> {
|
||||
for (String subId : subIds) {
|
||||
SessionSubscriptionInfo info = this.subscriptionRegistry.getSubscriptions(sessionId);
|
||||
if (info == null) {
|
||||
continue;
|
||||
}
|
||||
Subscription sub = info.getSubscription(subId);
|
||||
if (sub == null) {
|
||||
continue;
|
||||
}
|
||||
Expression expression = sub.getSelectorExpression();
|
||||
if (expression == null) {
|
||||
result.add(sessionId, subId);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
if (Boolean.TRUE.equals(expression.getValue(messageEvalContext, message, Boolean.class))) {
|
||||
result.add(sessionId, subId);
|
||||
allMatches.forEach((sessionId, subscriptionsIds) -> {
|
||||
SessionSubscriptionInfo subscriptions = this.subscriptionRegistry.getSubscriptions(sessionId);
|
||||
if (subscriptions != null) {
|
||||
for (String subscriptionId : subscriptionsIds) {
|
||||
Subscription subscription = subscriptions.getSubscription(subscriptionId);
|
||||
if (subscription != null && evaluateExpression(subscription.getSelectorExpression(), message)) {
|
||||
result.add(sessionId, subscription.getId());
|
||||
}
|
||||
}
|
||||
catch (SpelEvaluationException ex) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Failed to evaluate selector: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
logger.debug("Failed to evaluate selector", ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DefaultSubscriptionRegistry[" + this.destinationCache + ", " + this.subscriptionRegistry + "]";
|
||||
private boolean evaluateExpression(@Nullable Expression expression, Message<?> message) {
|
||||
boolean result = false;
|
||||
try {
|
||||
if (expression == null || Boolean.TRUE.equals(expression.getValue(messageEvalContext, message, Boolean.class))) {
|
||||
result = true;
|
||||
}
|
||||
}
|
||||
catch (SpelEvaluationException ex) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Failed to evaluate selector: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
logger.debug("Failed to evaluate selector", ex);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A cache for destinations previously resolved via
|
||||
* {@link DefaultSubscriptionRegistry#findSubscriptionsInternal(String, Message)}.
|
||||
*/
|
||||
private class DestinationCache {
|
||||
private final class DestinationCache {
|
||||
|
||||
/** Map from destination to {@code <sessionId, subscriptionId>} for fast look-ups. */
|
||||
private final Map<String, LinkedMultiValueMap<String, String>> accessCache =
|
||||
private final Map<String, LinkedMultiValueMap<String, String>> destinationCache =
|
||||
new ConcurrentHashMap<>(DEFAULT_CACHE_LIMIT);
|
||||
|
||||
/** Map from destination to {@code <sessionId, subscriptionId>} with locking. */
|
||||
@SuppressWarnings("serial")
|
||||
private final Map<String, LinkedMultiValueMap<String, String>> updateCache =
|
||||
new LinkedHashMap<String, LinkedMultiValueMap<String, String>>(DEFAULT_CACHE_LIMIT, 0.75f, true) {
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry<String, LinkedMultiValueMap<String, String>> eldest) {
|
||||
if (size() > getCacheLimit()) {
|
||||
accessCache.remove(eldest.getKey());
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
private final Queue<String> cacheEvictionPolicy = new ConcurrentLinkedQueue<>();
|
||||
|
||||
private final AtomicInteger cacheSize = new AtomicInteger();
|
||||
|
||||
public LinkedMultiValueMap<String, String> getSubscriptions(String destination, Message<?> message) {
|
||||
LinkedMultiValueMap<String, String> result = this.accessCache.get(destination);
|
||||
if (result == null) {
|
||||
synchronized (this.updateCache) {
|
||||
result = new LinkedMultiValueMap<>();
|
||||
for (SessionSubscriptionInfo info : subscriptionRegistry.getAllSubscriptions()) {
|
||||
for (String destinationPattern : info.getDestinations()) {
|
||||
if (getPathMatcher().match(destinationPattern, destination)) {
|
||||
for (Subscription sub : info.getSubscriptions(destinationPattern)) {
|
||||
result.add(info.sessionId, sub.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
public LinkedMultiValueMap<String, String> getSubscriptions(String destination) {
|
||||
LinkedMultiValueMap<String, String> subscriptions = this.destinationCache.get(destination);
|
||||
if (subscriptions == null) {
|
||||
subscriptions = this.destinationCache.computeIfAbsent(destination, dest -> {
|
||||
LinkedMultiValueMap<String, String> sessionSubscriptions = calculateSubscriptions(destination);
|
||||
this.cacheEvictionPolicy.add(destination);
|
||||
this.cacheSize.incrementAndGet();
|
||||
return sessionSubscriptions;
|
||||
});
|
||||
ensureCacheLimit();
|
||||
}
|
||||
return subscriptions;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private LinkedMultiValueMap<String, String> calculateSubscriptions(String destination) {
|
||||
LinkedMultiValueMap<String, String> sessionsToSubscriptions = new LinkedMultiValueMap<>();
|
||||
|
||||
DefaultSubscriptionRegistry.this.subscriptionRegistry.forEachSubscription((sessionId, subscriptionDetail) -> {
|
||||
if (subscriptionDetail.isAntPattern()) {
|
||||
if (pathMatcher.match(subscriptionDetail.getDestination(), destination)) {
|
||||
sessionsToSubscriptions.compute(sessionId, (s, subscriptions) ->
|
||||
addToList(subscriptionDetail.getId(), subscriptions));
|
||||
}
|
||||
if (!result.isEmpty()) {
|
||||
this.updateCache.put(destination, result.deepCopy());
|
||||
this.accessCache.put(destination, result);
|
||||
}
|
||||
else if (destination.equals(subscriptionDetail.getDestination())) {
|
||||
sessionsToSubscriptions.compute(sessionId, (s, subscriptions) ->
|
||||
addToList(subscriptionDetail.getId(), subscriptions));
|
||||
}
|
||||
});
|
||||
return sessionsToSubscriptions;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private List<String> addToList(String subscriptionId, @Nullable List<String> subscriptions) {
|
||||
if (subscriptions == null) {
|
||||
return Collections.singletonList(subscriptionId);
|
||||
}
|
||||
else {
|
||||
List<String> newSubscriptions = new ArrayList<>(subscriptions.size() + 1);
|
||||
newSubscriptions.addAll(subscriptions);
|
||||
newSubscriptions.add(subscriptionId);
|
||||
return newSubscriptions;
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureCacheLimit() {
|
||||
int size = this.cacheSize.get();
|
||||
if (size > cacheLimit) {
|
||||
do {
|
||||
if (this.cacheSize.compareAndSet(size, size - 1)) {
|
||||
this.destinationCache.remove(this.cacheEvictionPolicy.poll());
|
||||
}
|
||||
} while ((size = this.cacheSize.get()) > cacheLimit);
|
||||
}
|
||||
}
|
||||
|
||||
public void updateAfterNewSubscription(String destination, boolean isPattern, String sessionId, String subscriptionId) {
|
||||
if (isPattern) {
|
||||
for (String cachedDestination : this.destinationCache.keySet()) {
|
||||
if (pathMatcher.match(destination, cachedDestination)) {
|
||||
addToDestination(cachedDestination, sessionId, subscriptionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public void updateAfterNewSubscription(String destination, String sessionId, String subsId) {
|
||||
synchronized (this.updateCache) {
|
||||
this.updateCache.forEach((cachedDestination, subscriptions) -> {
|
||||
if (getPathMatcher().match(destination, cachedDestination)) {
|
||||
// Subscription id's may also be populated via getSubscriptions()
|
||||
List<String> subsForSession = subscriptions.get(sessionId);
|
||||
if (subsForSession == null || !subsForSession.contains(subsId)) {
|
||||
subscriptions.add(sessionId, subsId);
|
||||
this.accessCache.put(cachedDestination, subscriptions.deepCopy());
|
||||
}
|
||||
}
|
||||
});
|
||||
else {
|
||||
addToDestination(destination, sessionId, subscriptionId);
|
||||
}
|
||||
}
|
||||
|
||||
public void updateAfterRemovedSubscription(String sessionId, String subsId) {
|
||||
synchronized (this.updateCache) {
|
||||
Set<String> destinationsToRemove = new HashSet<>();
|
||||
this.updateCache.forEach((destination, sessionMap) -> {
|
||||
List<String> subscriptions = sessionMap.get(sessionId);
|
||||
if (subscriptions != null) {
|
||||
subscriptions.remove(subsId);
|
||||
if (subscriptions.isEmpty()) {
|
||||
sessionMap.remove(sessionId);
|
||||
}
|
||||
if (sessionMap.isEmpty()) {
|
||||
destinationsToRemove.add(destination);
|
||||
}
|
||||
else {
|
||||
this.accessCache.put(destination, sessionMap.deepCopy());
|
||||
}
|
||||
private void addToDestination(String destination, String sessionId, String subscriptionId) {
|
||||
this.destinationCache.computeIfPresent(destination, (dest, sessionsToSubscriptions) -> {
|
||||
sessionsToSubscriptions = sessionsToSubscriptions.clone();
|
||||
sessionsToSubscriptions.compute(sessionId, (s, subscriptions) -> addToList(subscriptionId, subscriptions));
|
||||
return sessionsToSubscriptions;
|
||||
});
|
||||
}
|
||||
|
||||
public void updateAfterRemovedSubscription(String sessionId, Subscription subscriptionDetail) {
|
||||
if (subscriptionDetail.isAntPattern()) {
|
||||
String patternDestination = subscriptionDetail.getDestination();
|
||||
for (String destination : this.destinationCache.keySet()) {
|
||||
if (pathMatcher.match(patternDestination, destination)) {
|
||||
removeInternal(destination, sessionId, subscriptionDetail.getId());
|
||||
}
|
||||
});
|
||||
for (String destination : destinationsToRemove) {
|
||||
this.updateCache.remove(destination);
|
||||
this.accessCache.remove(destination);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void updateAfterRemovedSession(SessionSubscriptionInfo info) {
|
||||
synchronized (this.updateCache) {
|
||||
Set<String> destinationsToRemove = new HashSet<>();
|
||||
this.updateCache.forEach((destination, sessionMap) -> {
|
||||
if (sessionMap.remove(info.getSessionId()) != null) {
|
||||
if (sessionMap.isEmpty()) {
|
||||
destinationsToRemove.add(destination);
|
||||
}
|
||||
else {
|
||||
this.accessCache.put(destination, sessionMap.deepCopy());
|
||||
}
|
||||
}
|
||||
});
|
||||
for (String destination : destinationsToRemove) {
|
||||
this.updateCache.remove(destination);
|
||||
this.accessCache.remove(destination);
|
||||
}
|
||||
else {
|
||||
removeInternal(subscriptionDetail.getDestination(), sessionId, subscriptionDetail.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "cache[" + this.accessCache.size() + " destination(s)]";
|
||||
private void removeInternal(String destination, String sessionId, String subscription) {
|
||||
this.destinationCache.computeIfPresent(destination, (dest, subscriptions) -> {
|
||||
subscriptions = subscriptions.clone();
|
||||
subscriptions.computeIfPresent(sessionId, (session, subs) -> {
|
||||
/* it is very likely that one session has only one subscription per one destination */
|
||||
if (subs.size() == 1 && subscription.equals(subs.get(0))) {
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
subs = new ArrayList<>(subs);
|
||||
subs.remove(subscription);
|
||||
return emptyListToNUll(subs);
|
||||
}
|
||||
});
|
||||
return subscriptions;
|
||||
});
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private <T> List<T> emptyListToNUll(@NonNull List<T> list) {
|
||||
return list.isEmpty() ? null : list;
|
||||
}
|
||||
|
||||
public void updateAfterRemovedSession(String sessionId, Collection<Subscription> subscriptionDetails) {
|
||||
for (Subscription subscriptionDetail : subscriptionDetails) {
|
||||
updateAfterRemovedSubscription(sessionId, subscriptionDetail);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Provide access to session subscriptions by sessionId.
|
||||
*/
|
||||
private static class SessionSubscriptionRegistry {
|
||||
private static final class SessionSubscriptionRegistry {
|
||||
|
||||
// sessionId -> SessionSubscriptionInfo
|
||||
// 'sessionId' -> 'subscriptionId' -> 'destination, selector expression'
|
||||
private final ConcurrentMap<String, SessionSubscriptionInfo> sessions = new ConcurrentHashMap<>();
|
||||
|
||||
@Nullable
|
||||
@@ -376,119 +396,51 @@ public class DefaultSubscriptionRegistry extends AbstractSubscriptionRegistry {
|
||||
return this.sessions.get(sessionId);
|
||||
}
|
||||
|
||||
public Collection<SessionSubscriptionInfo> getAllSubscriptions() {
|
||||
return this.sessions.values();
|
||||
public void forEachSubscription(BiConsumer<String, Subscription> consumer) {
|
||||
this.sessions.forEach((sessionId, subscriptions) ->
|
||||
subscriptions.getSubscriptions().forEach(subscriptionDetail ->
|
||||
consumer.accept(sessionId, subscriptionDetail)));
|
||||
}
|
||||
|
||||
public SessionSubscriptionInfo addSubscription(String sessionId, String subscriptionId,
|
||||
String destination, @Nullable Expression selectorExpression) {
|
||||
|
||||
SessionSubscriptionInfo info = this.sessions.get(sessionId);
|
||||
if (info == null) {
|
||||
info = new SessionSubscriptionInfo(sessionId);
|
||||
SessionSubscriptionInfo value = this.sessions.putIfAbsent(sessionId, info);
|
||||
if (value != null) {
|
||||
info = value;
|
||||
}
|
||||
}
|
||||
info.addSubscription(destination, subscriptionId, selectorExpression);
|
||||
return info;
|
||||
@Nullable
|
||||
public Subscription addSubscription(String sessionId, String subscriptionId, Subscription subscriptionDetail) {
|
||||
SessionSubscriptionInfo subscriptions = this.sessions.computeIfAbsent(sessionId, s -> new SessionSubscriptionInfo());
|
||||
return subscriptions.addSubscription(subscriptionId, subscriptionDetail);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public SessionSubscriptionInfo removeSubscriptions(String sessionId) {
|
||||
return this.sessions.remove(sessionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "registry[" + this.sessions.size() + " sessions]";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Hold subscriptions for a session.
|
||||
*/
|
||||
private static class SessionSubscriptionInfo {
|
||||
private static final class SessionSubscriptionInfo {
|
||||
|
||||
private final String sessionId;
|
||||
private final Map<String, Subscription> subscriptionLookup = new ConcurrentHashMap<>();
|
||||
|
||||
// destination -> subscriptions
|
||||
private final Map<String, Set<Subscription>> destinationLookup = new ConcurrentHashMap<>(4);
|
||||
|
||||
public SessionSubscriptionInfo(String sessionId) {
|
||||
Assert.notNull(sessionId, "'sessionId' must not be null");
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
public String getSessionId() {
|
||||
return this.sessionId;
|
||||
}
|
||||
|
||||
public Set<String> getDestinations() {
|
||||
return this.destinationLookup.keySet();
|
||||
}
|
||||
|
||||
public Set<Subscription> getSubscriptions(String destination) {
|
||||
return this.destinationLookup.get(destination);
|
||||
public Collection<Subscription> getSubscriptions() {
|
||||
return this.subscriptionLookup.values();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Subscription getSubscription(String subscriptionId) {
|
||||
for (Map.Entry<String, Set<DefaultSubscriptionRegistry.Subscription>> destinationEntry :
|
||||
this.destinationLookup.entrySet()) {
|
||||
for (Subscription sub : destinationEntry.getValue()) {
|
||||
if (sub.getId().equalsIgnoreCase(subscriptionId)) {
|
||||
return sub;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void addSubscription(String destination, String subscriptionId, @Nullable Expression selectorExpression) {
|
||||
Set<Subscription> subs = this.destinationLookup.get(destination);
|
||||
if (subs == null) {
|
||||
synchronized (this.destinationLookup) {
|
||||
subs = this.destinationLookup.get(destination);
|
||||
if (subs == null) {
|
||||
subs = new CopyOnWriteArraySet<>();
|
||||
this.destinationLookup.put(destination, subs);
|
||||
}
|
||||
}
|
||||
}
|
||||
subs.add(new Subscription(subscriptionId, selectorExpression));
|
||||
return this.subscriptionLookup.get(subscriptionId);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String removeSubscription(String subscriptionId) {
|
||||
for (Map.Entry<String, Set<DefaultSubscriptionRegistry.Subscription>> destinationEntry :
|
||||
this.destinationLookup.entrySet()) {
|
||||
Set<Subscription> subs = destinationEntry.getValue();
|
||||
if (subs != null) {
|
||||
for (Subscription sub : subs) {
|
||||
if (sub.getId().equals(subscriptionId) && subs.remove(sub)) {
|
||||
synchronized (this.destinationLookup) {
|
||||
if (subs.isEmpty()) {
|
||||
this.destinationLookup.remove(destinationEntry.getKey());
|
||||
}
|
||||
}
|
||||
return destinationEntry.getKey();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
public Subscription addSubscription(String subscriptionId, Subscription subscriptionDetail) {
|
||||
return this.subscriptionLookup.putIfAbsent(subscriptionId, subscriptionDetail);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "[sessionId=" + this.sessionId + ", subscriptions=" + this.destinationLookup + "]";
|
||||
@Nullable
|
||||
public Subscription removeSubscription(String subscriptionId) {
|
||||
return this.subscriptionLookup.remove(subscriptionId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static final class Subscription {
|
||||
|
||||
private final String id;
|
||||
@@ -496,16 +448,31 @@ public class DefaultSubscriptionRegistry extends AbstractSubscriptionRegistry {
|
||||
@Nullable
|
||||
private final Expression selectorExpression;
|
||||
|
||||
public Subscription(String id, @Nullable Expression selector) {
|
||||
private final String destination;
|
||||
|
||||
private final boolean isAntPattern;
|
||||
|
||||
public Subscription(String id, @Nullable Expression selector, String destination, boolean isAntPattern) {
|
||||
Assert.notNull(id, "Subscription id must not be null");
|
||||
Assert.notNull(destination, "Subscription destination must not be null");
|
||||
this.id = id;
|
||||
this.selectorExpression = selector;
|
||||
this.destination = destination;
|
||||
this.isAntPattern = isAntPattern;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public String getDestination() {
|
||||
return this.destination;
|
||||
}
|
||||
|
||||
public boolean isAntPattern() {
|
||||
return this.isAntPattern;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Expression getSelectorExpression() {
|
||||
return this.selectorExpression;
|
||||
|
||||
Reference in New Issue
Block a user