com.github.lontime.shaded.org.redisson.pubsub.PublishSubscribeService Maven / Gradle / Ivy
The newest version!
/**
* Copyright (c) 2013-2021 Nikita Koksharov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.github.lontime.shaded.org.redisson.pubsub;
import io.netty.util.Timeout;
import com.github.lontime.shaded.org.redisson.PubSubPatternStatusListener;
import com.github.lontime.shaded.org.redisson.client.*;
import com.github.lontime.shaded.org.redisson.client.codec.Codec;
import com.github.lontime.shaded.org.redisson.client.protocol.pubsub.PubSubType;
import com.github.lontime.shaded.org.redisson.config.MasterSlaveServersConfig;
import com.github.lontime.shaded.org.redisson.connection.ConnectionManager;
import com.github.lontime.shaded.org.redisson.connection.MasterSlaveEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
/**
*
* @author Nikita Koksharov
*
*/
public class PublishSubscribeService {
public static class PubSubKey {
private final ChannelName channelName;
private final MasterSlaveEntry entry;
public PubSubKey(ChannelName channelName, MasterSlaveEntry entry) {
this.channelName = channelName;
this.entry = entry;
}
public ChannelName getChannelName() {
return channelName;
}
public MasterSlaveEntry getEntry() {
return entry;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PubSubKey key = (PubSubKey) o;
return Objects.equals(channelName, key.channelName) && Objects.equals(entry, key.entry);
}
@Override
public int hashCode() {
return Objects.hash(channelName, entry);
}
}
public static class PubSubEntry {
Set keys = Collections.newSetFromMap(new ConcurrentHashMap<>());
Queue entries = new ConcurrentLinkedQueue<>();
public Set getKeys() {
return keys;
}
public Queue getEntries() {
return entries;
}
}
private static final Logger log = LoggerFactory.getLogger(PublishSubscribeService.class);
private final ConnectionManager connectionManager;
private final MasterSlaveServersConfig config;
private final AsyncSemaphore[] locks = new AsyncSemaphore[50];
private final AsyncSemaphore freePubSubLock = new AsyncSemaphore(1);
private final ConcurrentMap name2PubSubConnection = new ConcurrentHashMap<>();
private final ConcurrentMap entry2PubSubConnection = new ConcurrentHashMap<>();
private final Queue emptyQueue = new LinkedList<>();
private final SemaphorePubSub semaphorePubSub = new SemaphorePubSub(this);
private final CountDownLatchPubSub countDownLatchPubSub = new CountDownLatchPubSub(this);
private final LockPubSub lockPubSub = new LockPubSub(this);
public PublishSubscribeService(ConnectionManager connectionManager, MasterSlaveServersConfig config) {
super();
this.connectionManager = connectionManager;
this.config = config;
for (int i = 0; i < locks.length; i++) {
locks[i] = new AsyncSemaphore(1);
}
}
public ConnectionManager getConnectionManager() {
return connectionManager;
}
public LockPubSub getLockPubSub() {
return lockPubSub;
}
public CountDownLatchPubSub getCountDownLatchPubSub() {
return countDownLatchPubSub;
}
public SemaphorePubSub getSemaphorePubSub() {
return semaphorePubSub;
}
public PubSubConnectionEntry getPubSubEntry(ChannelName channelName) {
return name2PubSubConnection.get(createKey(channelName));
}
public CompletableFuture> psubscribe(ChannelName channelName, Codec codec, RedisPubSubListener>... listeners) {
if (isMultiEntity(channelName)) {
Collection entrySet = connectionManager.getEntrySet();
AtomicInteger statusCounter = new AtomicInteger(entrySet.size());
RedisPubSubListener[] ls = Arrays.stream(listeners).map(l -> {
if (l instanceof PubSubPatternStatusListener) {
return new PubSubPatternStatusListener((PubSubPatternStatusListener) l) {
@Override
public boolean onStatus(PubSubType type, CharSequence channel) {
if (statusCounter.decrementAndGet() == 0) {
return super.onStatus(type, channel);
}
return true;
}
};
}
return l;
}).toArray(RedisPubSubListener[]::new);
List> futures = new ArrayList<>();
for (MasterSlaveEntry entry : entrySet) {
CompletableFuture future = subscribe(PubSubType.PSUBSCRIBE, codec, channelName, entry, ls);
futures.add(future);
}
CompletableFuture future = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
return future.thenApply(r -> {
return futures.stream().map(v -> v.getNow(null)).collect(Collectors.toList());
});
}
CompletableFuture f = subscribe(PubSubType.PSUBSCRIBE, codec, channelName, getEntry(channelName), listeners);
return f.thenApply(res -> Collections.singletonList(res));
}
private boolean isMultiEntity(ChannelName channelName) {
return connectionManager.isClusterMode()
&& (channelName.toString().startsWith("__keyspace@")
|| channelName.toString().startsWith("__keyevent@"));
}
public CompletableFuture subscribe(Codec codec, ChannelName channelName, RedisPubSubListener>... listeners) {
return subscribe(PubSubType.SUBSCRIBE, codec, channelName, getEntry(channelName), listeners);
}
public CompletableFuture ssubscribe(Codec codec, ChannelName channelName, RedisPubSubListener>... listeners) {
return subscribe(PubSubType.SSUBSCRIBE, codec, channelName, getEntry(channelName), listeners);
}
private CompletableFuture subscribe(PubSubType type, Codec codec, ChannelName channelName,
MasterSlaveEntry entry, RedisPubSubListener>... listeners) {
CompletableFuture promise = new CompletableFuture<>();
AsyncSemaphore lock = getSemaphore(channelName);
int timeout = config.getTimeout() + config.getRetryInterval() * config.getRetryAttempts();
Timeout lockTimeout = connectionManager.newTimeout(t -> {
promise.completeExceptionally(new RedisTimeoutException(
"Unable to acquire subscription lock after " + timeout + "ms. " +
"Try to increase 'timeout', 'subscriptionsPerConnection', 'subscriptionConnectionPoolSize' parameters."));
}, timeout, TimeUnit.MILLISECONDS);
lock.acquire(() -> {
if (!lockTimeout.cancel() || promise.isDone()) {
lock.release();
return;
}
subscribe(codec, channelName, entry, promise, type, lock, new AtomicInteger(), listeners);
});
return promise;
}
public CompletableFuture subscribeNoTimeout(Codec codec, String channelName,
AsyncSemaphore semaphore, RedisPubSubListener>... listeners) {
CompletableFuture promise = new CompletableFuture<>();
subscribeNoTimeout(codec, new ChannelName(channelName), getEntry(new ChannelName(channelName)), promise,
PubSubType.SUBSCRIBE, semaphore, new AtomicInteger(), listeners);
return promise;
}
public AsyncSemaphore getSemaphore(ChannelName channelName) {
return locks[Math.abs(channelName.hashCode() % locks.length)];
}
private PubSubKey createKey(ChannelName channelName) {
MasterSlaveEntry entry = getEntry(channelName);
return new PubSubKey(channelName, entry);
}
private void subscribe(Codec codec, ChannelName channelName, MasterSlaveEntry entry,
CompletableFuture promise, PubSubType type,
AsyncSemaphore lock, AtomicInteger attempts, RedisPubSubListener>... listeners) {
subscribeNoTimeout(codec, channelName, entry, promise, type, lock, attempts, listeners);
timeout(promise);
}
public void timeout(CompletableFuture> promise) {
int timeout = config.getTimeout() + config.getRetryInterval() * config.getRetryAttempts();
timeout(promise, timeout);
}
public void timeout(CompletableFuture> promise, long timeout) {
Timeout task = connectionManager.newTimeout(t -> {
promise.completeExceptionally(new RedisTimeoutException(
"Unable to acquire subscription lock after " + timeout + "ms. " +
"Try to increase 'timeout', 'subscriptionsPerConnection', 'subscriptionConnectionPoolSize' parameters."));
}, timeout, TimeUnit.MILLISECONDS);
promise.whenComplete((r, e) -> {
task.cancel();
});
}
private void subscribeNoTimeout(Codec codec, ChannelName channelName, MasterSlaveEntry entry,
CompletableFuture promise, PubSubType type,
AsyncSemaphore lock, AtomicInteger attempts, RedisPubSubListener>... listeners) {
PubSubConnectionEntry connEntry = name2PubSubConnection.get(new PubSubKey(channelName, entry));
if (connEntry != null) {
addListeners(channelName, promise, type, lock, connEntry, listeners);
return;
}
freePubSubLock.acquire(() -> {
if (promise.isDone()) {
lock.release();
freePubSubLock.release();
return;
}
MasterSlaveEntry msEntry = Optional.ofNullable(connectionManager.getEntry(entry.getClient())).orElse(entry);
PubSubEntry freePubSubConnections = entry2PubSubConnection.getOrDefault(msEntry, new PubSubEntry());
PubSubConnectionEntry freeEntry = freePubSubConnections.getEntries().peek();
if (freeEntry == null) {
freePubSubLock.release();
CompletableFuture connectFuture = connect(codec, channelName, msEntry, promise, type, lock, listeners);
connectionManager.newTimeout(t -> {
if (attempts.get() == config.getRetryAttempts()) {
connectFuture.completeExceptionally(new RedisTimeoutException(
"Unable to acquire connection for subscription after " + attempts.get() + " attempts. " +
"Increase 'subscriptionsPerConnection' and/or 'subscriptionConnectionPoolSize' parameters."));
return;
}
if (connectFuture.cancel(true)) {
subscribe(codec, channelName, entry, promise, type, lock, attempts, listeners);
attempts.incrementAndGet();
}
}, config.getRetryInterval(), TimeUnit.MILLISECONDS);
return;
}
int remainFreeAmount = freeEntry.tryAcquire();
if (remainFreeAmount == -1) {
throw new IllegalStateException();
}
PubSubKey key = new PubSubKey(channelName, msEntry);
PubSubConnectionEntry oldEntry = name2PubSubConnection.putIfAbsent(key, freeEntry);
if (oldEntry != null) {
freeEntry.release();
freePubSubLock.release();
addListeners(channelName, promise, type, lock, oldEntry, listeners);
return;
}
if (remainFreeAmount == 0) {
freePubSubConnections.getEntries().poll();
}
freePubSubLock.release();
CompletableFuture subscribeFuture = addListeners(channelName, promise, type, lock, freeEntry, listeners);
freeEntry.subscribe(codec, type, channelName, subscribeFuture);
subscribeFuture.whenComplete((r, e) -> {
if (e != null) {
unsubscribe(channelName, type);
}
});
});
}
private MasterSlaveEntry getEntry(ChannelName channelName) {
int slot = connectionManager.calcSlot(channelName.getName());
return connectionManager.getEntry(slot);
}
private CompletableFuture addListeners(ChannelName channelName, CompletableFuture promise,
PubSubType type, AsyncSemaphore lock, PubSubConnectionEntry connEntry,
RedisPubSubListener>... listeners) {
for (RedisPubSubListener> listener : listeners) {
connEntry.addListener(channelName, listener);
}
SubscribeListener list = connEntry.getSubscribeFuture(channelName, type);
CompletableFuture subscribeFuture = list.getSuccessFuture();
subscribeFuture.whenComplete((res, e) -> {
if (e != null) {
promise.completeExceptionally(e);
lock.release();
return;
}
if (!promise.complete(connEntry)) {
for (RedisPubSubListener> listener : listeners) {
connEntry.removeListener(channelName, listener);
}
if (!connEntry.hasListeners(channelName)) {
unsubscribe(type, channelName)
.whenComplete((r, ex) -> {
lock.release();
});
} else {
lock.release();
}
} else {
lock.release();
}
});
return subscribeFuture;
}
private CompletableFuture nextPubSubConnection(MasterSlaveEntry entry, ChannelName channelName) {
if (entry == null) {
int slot = connectionManager.calcSlot(channelName.getName());
RedisNodeNotFoundException ex = new RedisNodeNotFoundException("Node for slot: " + slot + " hasn't been discovered yet. Check cluster slots coverage using CLUSTER NODES command. Increase value of retryAttempts and/or retryInterval settings.");
CompletableFuture result = new CompletableFuture<>();
result.completeExceptionally(ex);
return result;
}
return entry.nextPubSubConnection();
}
private CompletableFuture connect(Codec codec, ChannelName channelName,
MasterSlaveEntry msEntry, CompletableFuture promise,
PubSubType type, AsyncSemaphore lock, RedisPubSubListener>... listeners) {
CompletableFuture connFuture = nextPubSubConnection(msEntry, channelName);
promise.whenComplete((res, e) -> {
if (e != null) {
connFuture.completeExceptionally(e);
}
});
connFuture.whenComplete((conn, ex) -> {
if (ex != null) {
// freePubSubLock.release();
lock.release();
if (!connFuture.isCancelled()) {
promise.completeExceptionally(ex);
}
return;
}
freePubSubLock.acquire(() -> {
PubSubConnectionEntry entry = new PubSubConnectionEntry(conn, connectionManager);
int remainFreeAmount = entry.tryAcquire();
PubSubKey key = new PubSubKey(channelName, msEntry);
PubSubConnectionEntry oldEntry = name2PubSubConnection.putIfAbsent(key, entry);
if (oldEntry != null) {
msEntry.returnPubSubConnection(conn);
freePubSubLock.release();
addListeners(channelName, promise, type, lock, oldEntry, listeners);
return;
}
if (remainFreeAmount > 0) {
addFreeConnectionEntry(channelName, entry);
}
freePubSubLock.release();
CompletableFuture subscribeFuture = addListeners(channelName, promise, type, lock, entry, listeners);
entry.subscribe(codec, type, channelName, subscribeFuture);
subscribeFuture.whenComplete((r, e) -> {
if (e != null) {
unsubscribe(channelName, type);
}
});
});
});
return connFuture;
}
public CompletableFuture unsubscribe(PubSubType topicType, ChannelName channelName) {
PubSubConnectionEntry entry = name2PubSubConnection.remove(createKey(channelName));
if (entry == null || connectionManager.isShuttingDown()) {
return CompletableFuture.completedFuture(null);
}
CompletableFuture result = new CompletableFuture<>();
BaseRedisPubSubListener listener = new BaseRedisPubSubListener() {
@Override
public boolean onStatus(PubSubType type, CharSequence channel) {
if (type == topicType && channel.equals(channelName)) {
if (entry.release() == 1) {
MasterSlaveEntry msEntry = getEntry(channelName);
msEntry.returnPubSubConnection(entry.getConnection());
}
result.complete(null);
return true;
}
return false;
}
};
entry.unsubscribe(topicType, channelName, listener);
return result;
}
public void remove(MasterSlaveEntry entry) {
entry2PubSubConnection.remove(entry);
}
public CompletableFuture unsubscribe(ChannelName channelName, PubSubType topicType) {
return unsubscribe(channelName, getEntry(channelName), topicType);
}
private CompletableFuture unsubscribe(ChannelName channelName, MasterSlaveEntry e, PubSubType topicType) {
if (connectionManager.isShuttingDown()) {
return CompletableFuture.completedFuture(null);
}
CompletableFuture result = new CompletableFuture<>();
AsyncSemaphore lock = getSemaphore(channelName);
lock.acquire(() -> {
PubSubConnectionEntry entry = name2PubSubConnection.remove(new PubSubKey(channelName, e));
if (entry == null) {
lock.release();
result.complete(null);
return;
}
freePubSubLock.acquire(() -> {
PubSubEntry ee = entry2PubSubConnection.getOrDefault(e, new PubSubEntry());
Queue freePubSubConnections = ee.getEntries();
freePubSubConnections.remove(entry);
freePubSubLock.release();
Codec entryCodec;
if (topicType == PubSubType.PUNSUBSCRIBE) {
entryCodec = entry.getConnection().getPatternChannels().get(channelName);
} else if (topicType == PubSubType.SUNSUBSCRIBE) {
entryCodec = entry.getConnection().getShardedChannels().get(channelName);
} else {
entryCodec = entry.getConnection().getChannels().get(channelName);
}
RedisPubSubListener