/* * Copyright 2017 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.cassandra.core.session; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.Assert; import com.datastax.driver.core.*; import com.google.common.util.concurrent.ListenableFuture; /** * Default implementation of a {@link ReactiveSession}. This implementation bridges asynchronous {@link Session} methods * to reactive execution patterns. * <p> * Calls are deferred until a subscriber subscribes to the resulting {@link org.reactivestreams.Publisher}. The calls * are executed by subscribing to {@link ListenableFuture} and returning the result as calls complete. * <p> * {@link ResultSet} implements transparent paging that invokes in the middle of result streaming blocking calls to * Cassandra. {@link DefaultBridgedReactiveSession} uses therefore {@link ReactiveResultSet} to avoid client thread * blocking. Elements are emitted on netty EventLoop threads and transported by the provided {@link Scheduler}. However, * this is an intermediate solution until Datastax can provide a fully reactive driver. * <p> * All CQL operations performed by this class are logged at debug level, using * "org.springframework.cassandra.core.DefaultBridgedReactiveSession" as log category. * <p> * * @author Mark Paluch * @since 2.0 * @see Mono * @see ReactiveResultSet * @see Scheduler * @see ReactiveSession */ public class DefaultBridgedReactiveSession implements ReactiveSession { private final Logger logger = LoggerFactory.getLogger(getClass()); private final Session session; private final Scheduler scheduler; /** * Create a new {@link DefaultBridgedReactiveSession} for a {@link Session} and {@link Scheduler}. * * @param session must not be {@literal null}. * @param scheduler must not be {@literal null}. */ public DefaultBridgedReactiveSession(Session session, Scheduler scheduler) { Assert.notNull(session, "Session must not be null"); Assert.notNull(scheduler, "Scheduler must not be null"); this.session = session; this.scheduler = scheduler; } /* (non-Javadoc) * @see org.springframework.cassandra.core.ReactiveSession#execute(java.lang.String) */ @Override public Mono<ReactiveResultSet> execute(String query) { Assert.hasText(query, "Query must not be empty"); return execute(new SimpleStatement(query)); } /* (non-Javadoc) * @see org.springframework.cassandra.core.ReactiveSession#execute(java.lang.String, java.lang.Object[]) */ @Override public Mono<ReactiveResultSet> execute(String query, Object... values) { Assert.hasText(query, "Query must not be empty"); return execute(new SimpleStatement(query, values)); } /* (non-Javadoc) * @see org.springframework.cassandra.core.ReactiveSession#execute(java.lang.String, java.util.Map) */ @Override public Mono<ReactiveResultSet> execute(String query, Map<String, Object> values) { Assert.hasText(query, "Query must not be empty"); return execute(new SimpleStatement(query, values)); } /* (non-Javadoc) * @see org.springframework.cassandra.core.ReactiveSession#execute(com.datastax.driver.core.Statement) */ @Override public Mono<ReactiveResultSet> execute(Statement statement) { Assert.notNull(statement, "Statement must not be null"); return Mono.defer(() -> { try { if (logger.isDebugEnabled()) { logger.debug("Executing Statement [{}]", statement); } CompletableFuture<ReactiveResultSet> future = new CompletableFuture<>(); ResultSetFuture resultSetFuture = session.executeAsync(statement); resultSetFuture.addListener(() -> { if (resultSetFuture.isDone()) { try { future.complete(new DefaultReactiveResultSet(resultSetFuture.getUninterruptibly(), scheduler)); } catch (Exception e) { future.completeExceptionally(e); } } }, Runnable::run); return Mono.fromFuture(future); } catch (Exception e) { return Mono.error(e); } }).subscribeOn(scheduler); } /* (non-Javadoc) * @see org.springframework.cassandra.core.ReactiveSession#prepare(java.lang.String) */ @Override public Mono<PreparedStatement> prepare(String query) { Assert.hasText(query, "Query must not be empty"); return prepare(new SimpleStatement(query)); } /* (non-Javadoc) * @see org.springframework.cassandra.core.ReactiveSession#prepare(com.datastax.driver.core.RegularStatement) */ @Override public Mono<PreparedStatement> prepare(RegularStatement statement) { Assert.notNull(statement, "Statement must not be null"); return Mono.defer(() -> { try { if (logger.isDebugEnabled()) { logger.debug("Preparing Statement [{}]", statement); } CompletableFuture<PreparedStatement> future = new CompletableFuture<>(); ListenableFuture<PreparedStatement> resultSetFuture = session.prepareAsync(statement); resultSetFuture.addListener(() -> { if (resultSetFuture.isDone()) { try { future.complete(resultSetFuture.get()); } catch (Exception e) { future.completeExceptionally(e); } } }, Runnable::run); return Mono.fromFuture(future); } catch (Exception e) { return Mono.error(e); } }).subscribeOn(scheduler); } /* (non-Javadoc) * @see org.springframework.cassandra.core.ReactiveSession#close() */ @Override public void close() { session.close(); } /* (non-Javadoc) * @see org.springframework.cassandra.core.ReactiveSession#isClosed() */ @Override public boolean isClosed() { return session.isClosed(); } /* (non-Javadoc) * @see org.springframework.cassandra.core.ReactiveSession#getCluster() */ @Override public Cluster getCluster() { return session.getCluster(); } private static class DefaultReactiveResultSet implements ReactiveResultSet { private final ResultSet resultSet; private final Scheduler scheduler; DefaultReactiveResultSet(ResultSet resultSet, Scheduler scheduler) { this.resultSet = resultSet; this.scheduler = scheduler; } /* (non-Javadoc) * @see org.springframework.cassandra.core.ReactiveResultSet#rows() */ @Override public Flux<Row> rows() { int prefetch = Math.max(1, resultSet.getAvailableWithoutFetching()); return Flux.fromIterable(resultSet) // .subscribeOn(scheduler) // .publishOn(Schedulers.immediate(), prefetch); // limit prefetching to available size } /* (non-Javadoc) * @see org.springframework.cassandra.core.ReactiveResultSet#getColumnDefinitions() */ @Override public ColumnDefinitions getColumnDefinitions() { return resultSet.getColumnDefinitions(); } /* (non-Javadoc) * @see org.springframework.cassandra.core.ReactiveResultSet#wasApplied() */ @Override public boolean wasApplied() { return resultSet.wasApplied(); } /* (non-Javadoc) * @see org.springframework.cassandra.core.ReactiveResultSet#getExecutionInfo() */ @Override public ExecutionInfo getExecutionInfo() { return resultSet.getExecutionInfo(); } /* (non-Javadoc) * @see org.springframework.cassandra.core.ReactiveResultSet#getAllExecutionInfo() */ @Override public List<ExecutionInfo> getAllExecutionInfo() { return resultSet.getAllExecutionInfo(); } } }