All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.lambdaworks.redis.RedisPublisher Maven / Gradle / Ivy

Go to download

Advanced and thread-safe Java Redis client for synchronous, asynchronous, and reactive usage. Supports Cluster, Sentinel, Pipelining, Auto-Reconnect, Codecs and much more.

The newest version!
/*
 * Copyright 2011-2016 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 com.lambdaworks.redis;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Objects;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;

import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;

import com.lambdaworks.redis.api.StatefulConnection;
import com.lambdaworks.redis.api.StatefulRedisConnection;
import com.lambdaworks.redis.internal.LettuceAssert;
import com.lambdaworks.redis.output.StreamingOutput;
import com.lambdaworks.redis.protocol.CommandWrapper;
import com.lambdaworks.redis.protocol.RedisCommand;

import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
import reactor.core.publisher.Operators;

/**
 * Reactive command {@link Publisher} using ReactiveStreams.
 *
 * This publisher handles command execution and response propagation to a {@link Subscriber}. Collections can be dissolved into
 * individual elements instead of emitting collections. This publisher allows multiple subscriptions if it's backed by a
 * {@link Supplier command supplier}.
 * 

* When using streaming outputs ({@link com.lambdaworks.redis.output.CommandOutput} that implement {@link StreamingOutput}) * elements are emitted as they are decoded. Otherwise, results are processed at command completion. * * @author Mark Paluch * @since 5.0 */ class RedisPublisher implements Publisher { private static final InternalLogger LOG = InternalLoggerFactory.getInstance(RedisPublisher.class); private final boolean traceEnabled = LOG.isTraceEnabled(); private final Supplier> commandSupplier; private final AtomicReference> ref; private final StatefulConnection connection; private final boolean dissolve; /** * Creates a new {@link RedisPublisher} for a static command. * * @param staticCommand static command, must not be {@literal null} * @param connection the connection, must not be {@literal null} * @param dissolve dissolve collections into particular elements */ public RedisPublisher(RedisCommand staticCommand, StatefulConnection connection, boolean dissolve) { this(() -> staticCommand, connection, dissolve); } /** * Creates a new {@link RedisPublisher} for a command supplier. * * @param commandSupplier command supplier, must not be {@literal null} * @param connection the connection, must not be {@literal null} * @param dissolve dissolve collections into particular elements */ public RedisPublisher(Supplier> commandSupplier, StatefulConnection connection, boolean dissolve) { LettuceAssert.notNull(commandSupplier, "CommandSupplier must not be null"); LettuceAssert.notNull(connection, "StatefulConnection must not be null"); this.commandSupplier = commandSupplier; this.connection = connection; this.dissolve = dissolve; this.ref = new AtomicReference<>(commandSupplier.get()); } @Override public void subscribe(Subscriber subscriber) { if (this.traceEnabled) { LOG.trace("subscribe: {}@{}", subscriber.getClass().getName(), Objects.hashCode(subscriber)); } // Reuse the first command but then discard it. RedisCommand command = ref.get(); if (command != null) { if (!ref.compareAndSet(command, null)) { command = commandSupplier.get(); } } else { command = commandSupplier.get(); } RedisSubscription redisSubscription = new RedisSubscription<>(connection, command, dissolve); redisSubscription.subscribe(subscriber); } /** * Implementation of {@link Subscription}. This subscription can receive demand for data signals with {@link #request(long)} * . It maintains a {@link State} to react on pull signals like demand for data or push signals as soon as data is * available. Subscription behavior and state transitions are kept inside the {@link State}. * * @param data element type */ private static class RedisSubscription implements Subscription, StreamingOutput.Subscriber { private static final InternalLogger LOG = InternalLoggerFactory.getInstance(RedisPublisher.class); private final boolean traceEnabled = LOG.isTraceEnabled(); private final AtomicLong demand = new AtomicLong(); private final Queue data = new ConcurrentLinkedQueue(); private final AtomicBoolean dispatched = new AtomicBoolean(); private volatile boolean allDataRead = false; private final StatefulConnection connection; private final RedisCommand command; private final boolean dissolve; private Subscriber subscriber; private final AtomicReference state = new AtomicReference<>(State.UNSUBSCRIBED); RedisSubscription(StatefulConnection connection, RedisCommand command, boolean dissolve) { LettuceAssert.notNull(connection, "Connection must not be null"); LettuceAssert.notNull(command, "RedisCommand must not be null"); this.connection = connection; this.command = command; this.dissolve = dissolve; } /** * Subscription procedure called by a {@link Publisher} * * @param subscriber the subscriber, must not be {@literal null}. */ void subscribe(Subscriber subscriber) { if (subscriber == null) { throw new NullPointerException("Subscriber must not be null"); } if (traceEnabled) { LOG.trace("{} subscribe: {}@{}", state(), subscriber.getClass().getName(), Objects.hashCode(subscriber)); } state().subscribe(this, subscriber); } /** * Signal for data demand. * * @param n number of requested elements */ @Override public final void request(long n) { if (traceEnabled) { LOG.trace("{} request: {}", state(), n); } state().request(this, n); } /** * Cancels a command. */ @Override public final void cancel() { if (traceEnabled) { LOG.trace("{} cancel", state()); } state().cancel(this); } private RedisPublisher.State state() { return this.state.get(); } /** * Called by {@link StreamingOutput} to dispatch data (push). * * @param t element */ @Override public void onNext(T t) { LettuceAssert.notNull(t, "Data must not be null"); data.add(t); onDataAvailable(); } /** * Called via a listener interface to indicate that reading is possible. * */ final void onDataAvailable() { if (traceEnabled) { LOG.trace("{} onDataAvailable()", state()); } this.state.get().onDataAvailable(this); } /** * Called via a listener interface to indicate that all data has been read. * */ final void onAllDataRead() { if (traceEnabled) { LOG.trace("{} onAllDataRead()", state()); } allDataRead = true; this.state.get().onAllDataRead(this); } /** * Called by a listener interface to indicate that as error has occured. * * @param t the error */ final void onError(Throwable t) { if (LOG.isErrorEnabled()) { LOG.trace("{} onError(): {}", state(), t.toString(), t); } this.state.get().onError(this, t); } /** * Reads and publishes data from the input. Continues until either there is no more demand, or until there is no more * data to be read. * * @return {@literal true} if there is more demand, {@literal false} otherwise */ private boolean readAndPublish() throws IOException { while (hasDemand()) { T data = read(); if (data != null) { BackpressureUtils.getAndSub(this.demand, 1L); this.subscriber.onNext(data); } else { return true; } } return false; } /** * Reads data from the input, if possible. * * @return the data that was read or {@literal null} */ protected T read() { return data.poll(); } private boolean hasDemand() { return this.demand.get() > 0; } private boolean changeState(State oldState, State newState) { return this.state.compareAndSet(oldState, newState); } void checkCommandDispatch() { if (!dispatched.get() && dispatched.compareAndSet(false, true)) { dispatchCommand(); } } @SuppressWarnings({"unchecked", "rawtypes"}) private void dispatchCommand() { if (command.getOutput() instanceof StreamingOutput) { StreamingOutput streamingOutput = (StreamingOutput) command.getOutput(); if (connection instanceof StatefulRedisConnection && ((StatefulRedisConnection) connection).isMulti()) { streamingOutput .setSubscriber(new CompositeSubscriber(Arrays.asList(this, streamingOutput.getSubscriber()))); } else { streamingOutput.setSubscriber(this); } } connection.dispatch(new SubscriptionCommand(command, this, dissolve)); } void checkOnDataAvailable() { if (!data.isEmpty()) { onDataAvailable(); } } } /** * Represents a state for the {@link Subscription} to be in. The following figure indicate the four different states that * exist, and the relationships between them. * *

     *       UNSUBSCRIBED
     *        |
     *        v
     * NO_DEMAND -------------------> DEMAND
     *    |    ^                      ^    |
     *    |    |                      |    |
     *    |    --------- READING <-----    |
     *    |                 |              |
     *    |                 v              |
     *    ------------> COMPLETED <---------
     * 
* * Refer to the individual states for more information. */ private enum State { /** * The initial unsubscribed state. Will respond to {@link #subscribe(RedisSubscription, Subscriber)} by changing state * to {@link #NO_DEMAND}. */ UNSUBSCRIBED { @SuppressWarnings("unchecked") @Override void subscribe(RedisSubscription subscription, Subscriber subscriber) { LettuceAssert.notNull(subscriber, "Subscriber must not be null"); if (subscription.changeState(this, NO_DEMAND)) { subscription.subscriber = (Subscriber) subscriber; subscriber.onSubscribe(subscription); } else { throw new IllegalStateException(toString()); } } }, /** * State that gets entered when there is no demand. Responds to {@link #request(RedisSubscription, long)} * (RedisPublisher, long)} by increasing the demand, changing state to {@link #DEMAND} and will check whether there is * data available for reading. */ NO_DEMAND { @Override void request(RedisSubscription subscription, long n) { if (Operators.checkRequest(n, subscription.subscriber)) { Operators.addAndGet(subscription.demand, n); if (subscription.changeState(this, DEMAND)) { try { subscription.checkCommandDispatch(); } catch (Exception ex) { subscription.onError(ex); } subscription.checkOnDataAvailable(); } } } }, /** * State that gets entered when there is demand. Responds to {@link #onDataAvailable(RedisSubscription)} by reading the * available data. The state will be changed to {@link #NO_DEMAND} if there is no demand. */ DEMAND { @Override void onDataAvailable(RedisSubscription subscription) { if (subscription.changeState(this, READING)) { try { boolean demandAvailable = subscription.readAndPublish(); if (demandAvailable) { subscription.changeState(READING, DEMAND); subscription.checkOnDataAvailable(); } else { if (subscription.allDataRead && subscription.data.isEmpty()) { subscription.onAllDataRead(); } else { subscription.changeState(READING, NO_DEMAND); } } } catch (IOException ex) { subscription.onError(ex); } } } @Override void request(RedisSubscription subscription, long n) { if (Operators.checkRequest(n, subscription.subscriber)) { Operators.addAndGet(subscription.demand, n); } } }, READING { @Override void request(RedisSubscription subscription, long n) { if (Operators.checkRequest(n, subscription.subscriber)) { Operators.addAndGet(subscription.demand, n); } } }, /** * The terminal completed state. Does not respond to any events. */ COMPLETED { @Override void request(RedisSubscription subscription, long n) { // ignore } @Override void cancel(RedisSubscription subscription) { // ignore } @Override void onAllDataRead(RedisSubscription subscription) { // ignore } @Override void onError(RedisSubscription subscription, Throwable t) { // ignore } }; void subscribe(RedisSubscription subscription, Subscriber subscriber) { throw new IllegalStateException(toString()); } void request(RedisSubscription subscription, long n) { throw new IllegalStateException(toString()); } void cancel(RedisSubscription subscription) { subscription.command.cancel(); subscription.changeState(this, COMPLETED); } void onDataAvailable(RedisSubscription subscription) { // ignore } void onAllDataRead(RedisSubscription subscription) { subscription.allDataRead = true; if (subscription.data.isEmpty() && subscription.changeState(this, COMPLETED)) { if (subscription.subscriber != null) { subscription.subscriber.onComplete(); } } } void onError(RedisSubscription subscription, Throwable t) { if (subscription.changeState(this, COMPLETED)) { if (subscription.subscriber != null) { subscription.subscriber.onError(t); } } } } /** * Command that emits it data after completion to a {@link RedisSubscription}. * * @param key type * @param value type * @param response type */ private static class SubscriptionCommand extends CommandWrapper { private final boolean dissolve; private final RedisSubscription subscription; private boolean completed = false; public SubscriptionCommand(RedisCommand command, RedisSubscription subscription, boolean dissolve) { super(command); this.subscription = subscription; this.dissolve = dissolve; } @Override @SuppressWarnings("unchecked") public void complete() { if (completed) { return; } try { super.complete(); if (getOutput() != null) { Object result = getOutput().get(); if (getOutput().hasError()) { onError(new RedisCommandExecutionException(getOutput().getError())); completed = true; return; } if (!(getOutput() instanceof StreamingOutput) && result != null) { if (dissolve && result instanceof Collection) { Collection collection = (Collection) result; for (T t : collection) { if (t != null) { subscription.onNext(t); } } } else { subscription.onNext((T) result); } } } subscription.onAllDataRead(); } finally { completed = true; } } @Override public void cancel() { if (completed) { return; } super.cancel(); completed = true; } @Override public boolean completeExceptionally(Throwable throwable) { if (completed) { return false; } boolean b = super.completeExceptionally(throwable); onError(throwable); completed = true; return b; } private void onError(Throwable throwable) { subscription.onError(throwable); } } /** * Composite {@link com.lambdaworks.redis.output.StreamingOutput.Subscriber} that can notify multiple nested subscribers. * * @param element type */ private static class CompositeSubscriber implements StreamingOutput.Subscriber { private final Collection> subscribers; CompositeSubscriber(Collection> subscribers) { this.subscribers = subscribers; } @Override public void onNext(T t) { subscribers.forEach(subscriber -> subscriber.onNext(t)); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy