23
23
import java .util .Arrays ;
24
24
import java .util .Collection ;
25
25
import java .util .HashMap ;
26
+ import java .util .HashSet ;
26
27
import java .util .List ;
27
28
import java .util .Map ;
28
29
import java .util .Set ;
29
30
import java .util .concurrent .ConcurrentHashMap ;
31
+ import java .util .concurrent .atomic .AtomicBoolean ;
30
32
import java .util .concurrent .atomic .AtomicLongFieldUpdater ;
31
33
import java .util .stream .Collectors ;
32
34
import java .util .stream .StreamSupport ;
41
43
import org .springframework .data .redis .connection .ReactiveSubscription .Message ;
42
44
import org .springframework .data .redis .connection .ReactiveSubscription .PatternMessage ;
43
45
import org .springframework .data .redis .connection .SubscriptionListener ;
46
+ import org .springframework .data .redis .connection .util .ByteArrayWrapper ;
44
47
import org .springframework .data .redis .serializer .RedisElementReader ;
45
48
import org .springframework .data .redis .serializer .RedisSerializationContext .SerializationPair ;
46
49
import org .springframework .data .redis .serializer .RedisSerializer ;
50
+ import org .springframework .data .redis .util .ByteUtils ;
47
51
import org .springframework .lang .Nullable ;
48
52
import org .springframework .util .Assert ;
49
53
import org .springframework .util .ObjectUtils ;
@@ -157,6 +161,28 @@ public Flux<Message<String, String>> receive(ChannelTopic... channelTopics) {
157
161
return receive (Arrays .asList (channelTopics ), stringSerializationPair , stringSerializationPair );
158
162
}
159
163
164
+ /**
165
+ * Subscribe to one or more {@link ChannelTopic}s and receive a stream of {@link ChannelMessage} once the returned
166
+ * {@link Mono} completes. Messages and channel names are treated as {@link String}. The message stream subscribes
167
+ * lazily to the Redis channels and unsubscribes if the inner {@link org.reactivestreams.Subscription} is
168
+ * {@link org.reactivestreams.Subscription#cancel() cancelled}.
169
+ * <p/>
170
+ * The returned {@link Mono} completes once the connection has been subscribed to the given {@link Topic topics}. Note
171
+ * that cancelling the returned {@link Mono} can leave the connection in a subscribed state.
172
+ *
173
+ * @param channelTopics the channels to subscribe.
174
+ * @return the message stream.
175
+ * @throws InvalidDataAccessApiUsageException if {@code patternTopics} is empty.
176
+ * @since 2.6
177
+ */
178
+ public Mono <Flux <Message <String , String >>> receiveLater (ChannelTopic ... channelTopics ) {
179
+
180
+ Assert .notNull (channelTopics , "ChannelTopics must not be null!" );
181
+ Assert .noNullElements (channelTopics , "ChannelTopics must not contain null elements!" );
182
+
183
+ return receiveLater (Arrays .asList (channelTopics ), stringSerializationPair , stringSerializationPair );
184
+ }
185
+
160
186
/**
161
187
* Subscribe to one or more {@link PatternTopic}s and receive a stream of {@link PatternMessage}. Messages, pattern,
162
188
* and channel names are treated as {@link String}. The message stream subscribes lazily to the Redis channels and
@@ -178,6 +204,30 @@ public Flux<PatternMessage<String, String, String>> receive(PatternTopic... patt
178
204
.map (m -> (PatternMessage <String , String , String >) m );
179
205
}
180
206
207
+ /**
208
+ * Subscribe to one or more {@link PatternTopic}s and receive a stream of {@link PatternMessage} once the returned
209
+ * {@link Mono} completes. Messages, pattern, and channel names are treated as {@link String}. The message stream
210
+ * subscribes lazily to the Redis channels and unsubscribes if the inner {@link org.reactivestreams.Subscription} is
211
+ * {@link org.reactivestreams.Subscription#cancel() cancelled}.
212
+ * <p/>
213
+ * The returned {@link Mono} completes once the connection has been subscribed to the given {@link Topic topics}. Note
214
+ * that cancelling the returned {@link Mono} can leave the connection in a subscribed state.
215
+ *
216
+ * @param patternTopics the channels to subscribe.
217
+ * @return the message stream.
218
+ * @throws InvalidDataAccessApiUsageException if {@code patternTopics} is empty.
219
+ * @since 2.6
220
+ */
221
+ @ SuppressWarnings ("unchecked" )
222
+ public Mono <Flux <PatternMessage <String , String , String >>> receiveLater (PatternTopic ... patternTopics ) {
223
+
224
+ Assert .notNull (patternTopics , "PatternTopic must not be null!" );
225
+ Assert .noNullElements (patternTopics , "PatternTopic must not contain null elements!" );
226
+
227
+ return receiveLater (Arrays .asList (patternTopics ), stringSerializationPair , stringSerializationPair )
228
+ .map (it -> it .map (m -> (PatternMessage <String , String , String >) m ));
229
+ }
230
+
181
231
/**
182
232
* Subscribe to one or more {@link Topic}s and receive a stream of {@link ChannelMessage}. The stream may contain
183
233
* {@link PatternMessage} if subscribed to patterns. Messages, and channel names are serialized/deserialized using the
@@ -281,6 +331,68 @@ private <C, B> Flux<Message<C, B>> doReceive(SerializationPair<C> channelSeriali
281
331
.map (message -> readMessage (channelSerializer .getReader (), messageSerializer .getReader (), message ));
282
332
}
283
333
334
+ /**
335
+ * Subscribe to one or more {@link Topic}s and receive a stream of {@link ChannelMessage}. The returned {@link Mono}
336
+ * completes once the connection has been subscribed to the given {@link Topic topics}. Note that cancelling the
337
+ * returned {@link Mono} can leave the connection in a subscribed state.
338
+ *
339
+ * @param topics the channels to subscribe.
340
+ * @param channelSerializer serialization pair to decode the channel/pattern name.
341
+ * @param messageSerializer serialization pair to decode the message body.
342
+ * @return the message stream.
343
+ * @throws InvalidDataAccessApiUsageException if {@code topics} is empty.
344
+ * @since 2.6
345
+ */
346
+ private <C , B > Mono <Flux <Message <C , B >>> receiveLater (Iterable <? extends Topic > topics ,
347
+ SerializationPair <C > channelSerializer , SerializationPair <B > messageSerializer ) {
348
+
349
+ Assert .notNull (topics , "Topics must not be null!" );
350
+ Assert .notNull (channelSerializer , "Channel serializer must not be null!" );
351
+ Assert .notNull (messageSerializer , "Message serializer must not be null!" );
352
+
353
+ verifyConnection ();
354
+
355
+ ByteBuffer [] patterns = getTargets (topics , PatternTopic .class );
356
+ ByteBuffer [] channels = getTargets (topics , ChannelTopic .class );
357
+
358
+ if (ObjectUtils .isEmpty (patterns ) && ObjectUtils .isEmpty (channels )) {
359
+ throw new InvalidDataAccessApiUsageException ("No channels or patterns to subscribe to." );
360
+ }
361
+
362
+ return Mono .defer (() -> {
363
+
364
+ SubscriptionReadyListener readyListener = SubscriptionReadyListener .create (topics , stringSerializationPair );
365
+
366
+ return doReceiveLater (channelSerializer , messageSerializer ,
367
+ getRequiredConnection ().pubSubCommands ().createSubscription (readyListener ), patterns , channels )
368
+ .delayUntil (it -> readyListener .getTrigger ());
369
+ });
370
+ }
371
+
372
+ private <C , B > Mono <Flux <Message <C , B >>> doReceiveLater (SerializationPair <C > channelSerializer ,
373
+ SerializationPair <B > messageSerializer , Mono <ReactiveSubscription > subscription , ByteBuffer [] patterns ,
374
+ ByteBuffer [] channels ) {
375
+
376
+ return subscription .flatMap (it -> {
377
+
378
+ Mono <Void > subscribe = subscribe (patterns , channels , it ).doOnSuccess (v -> getSubscribers (it ).registered ());
379
+
380
+ Sinks .One <Message <ByteBuffer , ByteBuffer >> terminalSink = Sinks .one ();
381
+
382
+ Flux <Message <C , B >> receiver = it .receive ().doOnCancel (() -> {
383
+
384
+ Subscribers subscribers = getSubscribers (it );
385
+ if (subscribers .unregister ()) {
386
+ subscriptions .remove (it );
387
+ it .cancel ().subscribe (v -> terminalSink .tryEmitEmpty (), terminalSink ::tryEmitError );
388
+ }
389
+ }).mergeWith (terminalSink .asMono ())
390
+ .map (message -> readMessage (channelSerializer .getReader (), messageSerializer .getReader (), message ));
391
+
392
+ return subscribe .then (Mono .just (receiver ));
393
+ });
394
+ }
395
+
284
396
private static Mono <Void > subscribe (ByteBuffer [] patterns , ByteBuffer [] channels , ReactiveSubscription it ) {
285
397
286
398
Assert .isTrue (!ObjectUtils .isEmpty (channels ) || !ObjectUtils .isEmpty (patterns ),
@@ -418,4 +530,54 @@ boolean unregister() {
418
530
return false ;
419
531
}
420
532
}
533
+
534
+ static class SubscriptionReadyListener extends AtomicBoolean implements SubscriptionListener {
535
+
536
+ private final Set <ByteArrayWrapper > toSubscribe ;
537
+ private final Sinks .Empty <Void > sink = Sinks .empty ();
538
+
539
+ private SubscriptionReadyListener (Set <ByteArrayWrapper > topics ) {
540
+ this .toSubscribe = topics ;
541
+ }
542
+
543
+ public static SubscriptionReadyListener create (Iterable <? extends Topic > topics ,
544
+ SerializationPair <String > serializationPair ) {
545
+
546
+ Set <ByteArrayWrapper > wrappers = new HashSet <>();
547
+
548
+ for (Topic topic : topics ) {
549
+ wrappers .add (new ByteArrayWrapper (ByteUtils .getBytes (serializationPair .getWriter ().write (topic .getTopic ()))));
550
+ }
551
+
552
+ return new SubscriptionReadyListener (wrappers );
553
+ }
554
+
555
+ @ Override
556
+ public void onChannelSubscribed (byte [] channel , long count ) {
557
+ removeRemaining (channel );
558
+ }
559
+
560
+ @ Override
561
+ public void onPatternSubscribed (byte [] pattern , long count ) {
562
+ removeRemaining (pattern );
563
+ }
564
+
565
+ private void removeRemaining (byte [] channel ) {
566
+
567
+ boolean done ;
568
+
569
+ synchronized (toSubscribe ) {
570
+ toSubscribe .remove (new ByteArrayWrapper (channel ));
571
+ done = toSubscribe .isEmpty ();
572
+ }
573
+
574
+ if (done && compareAndSet (false , true )) {
575
+ sink .emitEmpty (Sinks .EmitFailureHandler .FAIL_FAST );
576
+ }
577
+ }
578
+
579
+ public Mono <Void > getTrigger () {
580
+ return sink .asMono ();
581
+ }
582
+ }
421
583
}
0 commit comments