DATAKV-49

DATAKV-46
+ improved pubsub connection support
+ RJC integration still needs some work
This commit is contained in:
Costin Leau
2011-03-17 14:47:20 +02:00
parent 52fe4cd827
commit 202686a075
7 changed files with 371 additions and 250 deletions

View File

@@ -29,8 +29,8 @@ import org.springframework.data.keyvalue.UncategorizedKeyvalueStoreException;
import org.springframework.data.keyvalue.redis.connection.DataType;
import org.springframework.data.keyvalue.redis.connection.MessageListener;
import org.springframework.data.keyvalue.redis.connection.RedisConnection;
import org.springframework.data.keyvalue.redis.connection.SortParameters;
import org.springframework.data.keyvalue.redis.connection.RedisSubscribedConnectionException;
import org.springframework.data.keyvalue.redis.connection.SortParameters;
import org.springframework.data.keyvalue.redis.connection.Subscription;
import org.springframework.util.ReflectionUtils;
@@ -2221,6 +2221,7 @@ public class JedisConnection implements RedisConnection {
subscription = new JedisSubscription(listener, jedisPubSub, null, patterns);
jedis.psubscribe(jedisPubSub, patterns);
} catch (Exception ex) {
throw convertJedisAccessException(ex);
}
@@ -2245,6 +2246,7 @@ public class JedisConnection implements RedisConnection {
subscription = new JedisSubscription(listener, jedisPubSub, channels, null);
jedis.subscribe(jedisPubSub, channels);
} catch (Exception ex) {
throw convertJedisAccessException(ex);
}

View File

@@ -15,13 +15,8 @@
*/
package org.springframework.data.keyvalue.redis.connection.jedis;
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.data.keyvalue.redis.connection.MessageListener;
import org.springframework.data.keyvalue.redis.connection.Subscription;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.data.keyvalue.redis.connection.util.AbstractSubscription;
import redis.clients.jedis.BinaryJedisPubSub;
@@ -30,132 +25,48 @@ import redis.clients.jedis.BinaryJedisPubSub;
*
* @author Costin Leau
*/
class JedisSubscription implements Subscription {
class JedisSubscription extends AbstractSubscription {
private final MessageListener listener;
private final BinaryJedisPubSub jedisPubSub;
private final Collection<byte[]> channels = new ArrayList<byte[]>(2);
private final Collection<byte[]> patterns = new ArrayList<byte[]>(2);
JedisSubscription(MessageListener listener, BinaryJedisPubSub jedisPubSub, byte[][] channels, byte[][] patterns) {
Assert.notNull(listener);
this.listener = listener;
super(listener, channels, patterns);
this.jedisPubSub = jedisPubSub;
if (!ObjectUtils.isEmpty(channels)) {
synchronized (this.channels) {
for (byte[] bs : channels) {
this.channels.add(bs);
}
}
}
if (!ObjectUtils.isEmpty(patterns)) {
synchronized (this.patterns) {
for (byte[] bs : patterns) {
this.patterns.add(bs);
}
}
}
}
@Override
public Collection<byte[]> getChannels() {
synchronized (channels) {
return new ArrayList<byte[]>(channels);
}
}
@Override
public MessageListener getListener() {
return listener;
}
@Override
public Collection<byte[]> getPatterns() {
synchronized (patterns) {
return new ArrayList<byte[]>(patterns);
}
}
@Override
public void pSubscribe(byte[]... patterns) {
Assert.notEmpty(patterns, "at least one pattern required");
synchronized (this.patterns) {
for (byte[] bs : patterns) {
this.patterns.add(bs);
}
}
jedisPubSub.psubscribe(patterns);
}
@Override
public void pUnsubscribe() {
synchronized (patterns) {
patterns.clear();
}
protected void doClose() {
jedisPubSub.unsubscribe();
jedisPubSub.punsubscribe();
}
@Override
public void pUnsubscribe(byte[]... patterns) {
if (ObjectUtils.isEmpty(patterns)) {
unsubscribe();
protected void doPsubscribe(byte[]... patterns) {
jedisPubSub.psubscribe(patterns);
}
@Override
protected void doPUnsubscribe(boolean all, byte[]... patterns) {
if (all) {
jedisPubSub.punsubscribe();
}
else {
synchronized (this.patterns) {
for (byte[] bs : patterns) {
this.patterns.remove(bs);
}
}
jedisPubSub.punsubscribe(patterns);
}
}
@Override
public void subscribe(byte[]... channels) {
Assert.notEmpty(channels, "at least one channel required");
synchronized (this.channels) {
for (byte[] bs : channels) {
this.channels.add(bs);
}
}
protected void doSubscribe(byte[]... channels) {
jedisPubSub.subscribe(channels);
}
@Override
public void unsubscribe() {
synchronized (channels) {
channels.clear();
}
jedisPubSub.unsubscribe();
}
@Override
public void unsubscribe(byte[]... channels) {
if (ObjectUtils.isEmpty(channels)) {
unsubscribe();
protected void doUnsubscribe(boolean all, byte[]... channels) {
if (all) {
jedisPubSub.unsubscribe();
}
else {
synchronized (this.channels) {
for (byte[] bs : channels) {
this.channels.remove(bs);
}
}
jedisPubSub.unsubscribe(channels);
}
}
@Override
public boolean isAlive() {
return jedisPubSub.isSubscribed();
}
}

View File

@@ -15,135 +15,58 @@
*/
package org.springframework.data.keyvalue.redis.connection.rjc;
import java.util.ArrayList;
import java.util.Collection;
import org.idevlab.rjc.message.RedisSubscriber;
import org.idevlab.rjc.message.RedisNodeSubscriber;
import org.springframework.data.keyvalue.redis.connection.MessageListener;
import org.springframework.data.keyvalue.redis.connection.Subscription;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.data.keyvalue.redis.connection.util.AbstractSubscription;
/**
* Message subscription on top of RJC.
*
* @author Costin Leau
*/
class RjcSubscription implements Subscription {
class RjcSubscription extends AbstractSubscription {
private final MessageListener listener;
private final RedisSubscriber subscriber;
private final RedisNodeSubscriber subscriber;
private final RjcMessageListener listenerAdapter;
private final Object pubSubMonitor;
private final Collection<byte[]> channels = new ArrayList<byte[]>(2);
private final Collection<byte[]> patterns = new ArrayList<byte[]>(2);
RjcSubscription(MessageListener listener, RedisSubscriber subscriber) {
Assert.notNull(listener);
this.listener = listener;
RjcSubscription(MessageListener listener, RedisNodeSubscriber subscriber, Object pubSubMonitor) {
super(listener);
this.subscriber = subscriber;
this.listenerAdapter = new RjcMessageListener(listener);
this.pubSubMonitor = pubSubMonitor;
}
@Override
public Collection<byte[]> getChannels() {
synchronized (channels) {
return new ArrayList<byte[]>(channels);
protected void doClose() {
subscriber.close();
}
@Override
protected void doPsubscribe(byte[]... patterns) {
for (String str : RjcUtils.decodeMultiple(patterns)) {
subscriber.psubscribe(str, listenerAdapter);
}
}
@Override
public MessageListener getListener() {
return listener;
}
@Override
public Collection<byte[]> getPatterns() {
synchronized (patterns) {
return new ArrayList<byte[]>(patterns);
protected void doPUnsubscribe(boolean all, byte[]... patterns) {
for (String str : RjcUtils.decodeMultiple(patterns)) {
subscriber.punsubscribe(str);
}
}
@Override
public void pSubscribe(byte[]... patterns) {
Assert.notEmpty(patterns, "at least one pattern required");
synchronized (this.patterns) {
for (byte[] bs : patterns) {
this.patterns.add(bs);
}
}
for (String pattern : RjcUtils.decodeMultiple(patterns)) {
subscriber.psubscribe(pattern, listenerAdapter);
protected void doSubscribe(byte[]... channels) {
for (String str : RjcUtils.decodeMultiple(channels)) {
subscriber.subscribe(str, listenerAdapter);
}
}
@Override
public void pUnsubscribe() {
pUnsubscribe(null);
synchronized (patterns) {
patterns.clear();
protected void doUnsubscribe(boolean all, byte[]... channels) {
for (String str : RjcUtils.decodeMultiple(channels)) {
subscriber.unsubscribe(str);
}
}
@Override
public void pUnsubscribe(byte[]... patterns) {
if (ObjectUtils.isEmpty(patterns)) {
patterns = this.patterns.toArray(new byte[this.patterns.size()][]);
}
synchronized (this.patterns) {
for (byte[] bs : patterns) {
this.patterns.remove(bs);
}
}
subscriber.punsubscribe(RjcUtils.decodeMultiple(patterns));
}
@Override
public void subscribe(byte[]... channels) {
Assert.notEmpty(channels, "at least one channel required");
synchronized (this.channels) {
for (byte[] bs : channels) {
this.channels.add(bs);
}
}
for (String channel : RjcUtils.decodeMultiple(channels)) {
subscriber.subscribe(channel, listenerAdapter);
}
}
@Override
public void unsubscribe() {
unsubscribe(null);
synchronized (patterns) {
patterns.clear();
}
}
@Override
public void unsubscribe(byte[]... channels) {
if (ObjectUtils.isEmpty(channels)) {
channels = this.channels.toArray(new byte[this.channels.size()][]);
}
synchronized (this.channels) {
for (byte[] bs : channels) {
this.channels.remove(bs);
}
}
subscriber.punsubscribe(RjcUtils.decodeMultiple(channels));
}
@Override
public boolean isAlive() {
return (!channels.isEmpty() || !patterns.isEmpty());
}
}

View File

@@ -0,0 +1,253 @@
/*
* Copyright 2011 the original author or authors.
*
* 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 org.springframework.data.keyvalue.redis.connection.util;
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.atomic.AtomicBoolean;
import org.springframework.data.keyvalue.redis.connection.MessageListener;
import org.springframework.data.keyvalue.redis.connection.RedisInvalidSubscriptionException;
import org.springframework.data.keyvalue.redis.connection.Subscription;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* Base implementation for a subscription handling the channel/pattern registration so subclasses only have to deal
* with the actual registration/unregistration.
*
* @author Costin Leau
*/
public abstract class AbstractSubscription implements Subscription {
private final Collection<ByteArrayWrapper> channels = new ArrayList<ByteArrayWrapper>(2);
private final Collection<ByteArrayWrapper> patterns = new ArrayList<ByteArrayWrapper>(2);
private final AtomicBoolean alive = new AtomicBoolean(true);
private final MessageListener listener;
protected AbstractSubscription(MessageListener listener) {
this(listener, null, null);
}
/**
* Constructs a new <code>AbstractSubscription</code> instance. Allows channels and patterns to be added
* to the subscription w/o triggering a subscription action (as some clients (Jedis) require an initial call
* before entering into listening mode).
*
* @param listener
* @param channels
* @param patterns
*/
protected AbstractSubscription(MessageListener listener, byte[][] channels, byte[][] patterns) {
Assert.notNull(listener);
this.listener = listener;
synchronized (this.channels) {
remove(this.channels, channels);
}
synchronized (this.patterns) {
remove(this.patterns, patterns);
}
}
/**
* Subscribe to the given channels.
*
* @param channels channels to subscribe to
*/
protected abstract void doSubscribe(byte[]... channels);
/**
* Channel unsubscribe.
*
* @param all true if all the channels are unsubscribed (used as a hint for the underlying implementation).
* @param channels channels to be unsubscribed
*/
protected abstract void doUnsubscribe(boolean all, byte[]... channels);
/**
* Subscribe to the given patterns
*
* @param patterns patterns to subscribe to
*/
protected abstract void doPsubscribe(byte[]... patterns);
/**
* Pattern unsubscribe.
*
* @param all true if all the patterns are unsubscribed (used as a hint for the underlying implementation).
* @param patterns patterns to be unsubscribed
*/
protected abstract void doPUnsubscribe(boolean all, byte[]... patterns);
/**
* Shutdown the subscription and free any resources held.
*/
protected abstract void doClose();
@Override
public MessageListener getListener() {
return listener;
}
@Override
public Collection<byte[]> getChannels() {
synchronized (channels) {
return clone(channels);
}
}
@Override
public Collection<byte[]> getPatterns() {
synchronized (patterns) {
return clone(patterns);
}
}
@Override
public void pSubscribe(byte[]... patterns) {
checkPulse();
Assert.notEmpty(patterns, "at least one pattern required");
synchronized (this.patterns) {
add(this.patterns, patterns);
}
doPsubscribe(patterns);
}
@Override
public void pUnsubscribe() {
pUnsubscribe((byte[][]) null);
}
@Override
public void subscribe(byte[]... channels) {
checkPulse();
Assert.notEmpty(channels, "at least one channel required");
synchronized (this.channels) {
add(this.channels, channels);
}
doSubscribe(channels);
}
@Override
public void unsubscribe() {
unsubscribe((byte[][]) null);
}
@Override
public void pUnsubscribe(byte[]... patts) {
if (!isAlive()) {
return;
}
// shortcut for unsubscribing all patterns
if (ObjectUtils.isEmpty(patts)) {
if (!this.patterns.isEmpty()) {
patts = getPatterns().toArray(new byte[this.patterns.size()][]);
synchronized (this.patterns) {
this.patterns.clear();
}
}
}
else {
synchronized (this.patterns) {
remove(this.patterns, patts);
}
}
if (isWorking()) {
doPUnsubscribe(this.patterns.isEmpty(), patts);
}
}
@Override
public void unsubscribe(byte[]... chans) {
if (!isAlive()) {
return;
}
// shortcut for unsubscribing all channels
if (ObjectUtils.isEmpty(chans)) {
if (!this.channels.isEmpty()) {
chans = getPatterns().toArray(new byte[this.channels.size()][]);
synchronized (this.channels) {
this.channels.clear();
}
}
}
else {
synchronized (this.channels) {
remove(this.channels, chans);
}
}
if (isWorking()) {
doUnsubscribe(this.channels.isEmpty(), chans);
}
}
@Override
public boolean isAlive() {
return alive.get();
}
private void checkPulse() {
if (!isAlive()) {
throw new RedisInvalidSubscriptionException("Subscription has been unsubscribed and cannot be used anymore");
}
}
private boolean isWorking() {
if (channels.isEmpty() && patterns.isEmpty()) {
alive.set(false);
doClose();
}
return isAlive();
}
private static Collection<byte[]> clone(Collection<ByteArrayWrapper> col) {
Collection<byte[]> list = new ArrayList<byte[]>(col.size());
for (ByteArrayWrapper wrapper : col) {
list.add(wrapper.getArray().clone());
}
return list;
}
private static void add(Collection<ByteArrayWrapper> col, byte[]... bytes) {
if (!ObjectUtils.isEmpty(bytes)) {
for (byte[] bs : bytes) {
col.add(new ByteArrayWrapper(bs));
}
}
}
private static void remove(Collection<ByteArrayWrapper> col, byte[]... bytes) {
if (!ObjectUtils.isEmpty(bytes)) {
for (byte[] bs : bytes) {
col.remove(new ByteArrayWrapper(bs));
}
}
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2011 the original author or authors.
*
* 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 org.springframework.data.keyvalue.redis.connection.util;
import java.util.Arrays;
/**
* Simple wrapper class used for wrapping arrays so they can be used as keys inside maps.
*
* @author Costin Leau
*/
public class ByteArrayWrapper {
private final byte[] array;
private final int hashCode;
public ByteArrayWrapper(byte[] array) {
this.array = array;
this.hashCode = Arrays.hashCode(array);
}
@Override
public boolean equals(Object obj) {
if (obj instanceof ByteArrayWrapper) {
return Arrays.equals(array, ((ByteArrayWrapper) obj).array);
}
return false;
}
@Override
public int hashCode() {
return hashCode;
}
/**
* Returns the array.
*
* @return Returns the array
*/
public byte[] getArray() {
return array;
}
}

View File

@@ -0,0 +1,5 @@
/**
* Internal utility package for encoding/decoding Strings to byte[] (using Base64) library.
*/
package org.springframework.data.keyvalue.redis.connection.util;

View File

@@ -16,7 +16,6 @@
package org.springframework.data.keyvalue.redis.listener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -39,6 +38,7 @@ import org.springframework.data.keyvalue.redis.connection.MessageListener;
import org.springframework.data.keyvalue.redis.connection.RedisConnection;
import org.springframework.data.keyvalue.redis.connection.RedisConnectionFactory;
import org.springframework.data.keyvalue.redis.connection.Subscription;
import org.springframework.data.keyvalue.redis.connection.util.ByteArrayWrapper;
import org.springframework.data.keyvalue.redis.serializer.RedisSerializer;
import org.springframework.data.keyvalue.redis.serializer.StringRedisSerializer;
import org.springframework.scheduling.SchedulingAwareRunnable;
@@ -101,9 +101,9 @@ public class RedisMessageListenerContainer implements InitializingBean, Disposab
// to avoid creation of hashes for each message, the maps use raw byte arrays (wrapped to respect the equals/hashcode contract)
// lookup map between patterns and listeners
private final Map<ArrayHolder, Collection<MessageListener>> patternMapping = new ConcurrentHashMap<ArrayHolder, Collection<MessageListener>>();
private final Map<ByteArrayWrapper, Collection<MessageListener>> patternMapping = new ConcurrentHashMap<ByteArrayWrapper, Collection<MessageListener>>();
// lookup map between channels and listeners
private final Map<ArrayHolder, Collection<MessageListener>> channelMapping = new ConcurrentHashMap<ArrayHolder, Collection<MessageListener>>();
private final Map<ByteArrayWrapper, Collection<MessageListener>> channelMapping = new ConcurrentHashMap<ByteArrayWrapper, Collection<MessageListener>>();
private final SubscriptionTask subscriptionTask = new SubscriptionTask();
@@ -448,7 +448,7 @@ public class RedisMessageListenerContainer implements InitializingBean, Disposab
for (Topic topic : topics) {
ArrayHolder holder = new ArrayHolder(serializer.serialize(topic.getTopic()));
ByteArrayWrapper holder = new ByteArrayWrapper(serializer.serialize(topic.getTopic()));
if (topic instanceof ChannelTopic) {
Collection<MessageListener> collection = channelMapping.get(holder);
@@ -457,7 +457,7 @@ public class RedisMessageListenerContainer implements InitializingBean, Disposab
channelMapping.put(holder, collection);
}
collection.add(listener);
channels.add(holder.array);
channels.add(holder.getArray());
if (trace)
logger.trace("Adding listener '" + listener + "' on channel '" + topic.getTopic() + "'");
@@ -470,7 +470,7 @@ public class RedisMessageListenerContainer implements InitializingBean, Disposab
patternMapping.put(holder, collection);
}
collection.add(listener);
patterns.add(holder.array);
patterns.add(holder.getArray());
if (trace)
logger.trace("Adding listener '" + listener + "' for pattern '" + topic.getTopic() + "'");
@@ -598,7 +598,7 @@ public class RedisMessageListenerContainer implements InitializingBean, Disposab
}
}
private byte[][] unwrap(Collection<ArrayHolder> holders) {
private byte[][] unwrap(Collection<ByteArrayWrapper> holders) {
if (CollectionUtils.isEmpty(holders)) {
return new byte[0][];
}
@@ -606,8 +606,8 @@ public class RedisMessageListenerContainer implements InitializingBean, Disposab
byte[][] unwrapped = new byte[holders.size()][];
int index = 0;
for (ArrayHolder arrayHolder : holders) {
unwrapped[index++] = arrayHolder.array;
for (ByteArrayWrapper arrayHolder : holders) {
unwrapped[index++] = arrayHolder.getArray();
}
return unwrapped;
@@ -700,12 +700,12 @@ public class RedisMessageListenerContainer implements InitializingBean, Disposab
// do channel matching first
byte[] channel = message.getChannel();
Collection<MessageListener> ch = channelMapping.get(new ArrayHolder(channel));
Collection<MessageListener> ch = channelMapping.get(new ByteArrayWrapper(channel));
Collection<MessageListener> pt = null;
// followed by pattern matching
if (pattern != null && pattern.length > 0) {
pt = patternMapping.get(new ArrayHolder(pattern));
pt = patternMapping.get(new ByteArrayWrapper(pattern));
}
if (!CollectionUtils.isEmpty(ch)) {
@@ -739,34 +739,4 @@ public class RedisMessageListenerContainer implements InitializingBean, Disposab
}
}
}
/**
* Simple wrapper class used for wrapping arrays so they can be used as keys inside maps.
*
* @author Costin Leau
*/
private class ArrayHolder {
private final byte[] array;
private final int hashCode;
ArrayHolder(byte[] array) {
this.array = array;
this.hashCode = Arrays.hashCode(array);
}
@Override
public boolean equals(Object obj) {
if (obj instanceof ArrayHolder) {
return Arrays.equals(array, ((ArrayHolder) obj).array);
}
return false;
}
@Override
public int hashCode() {
return hashCode;
}
}
}