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

com.mysql.cj.protocol.x.AsyncMessageReader Maven / Gradle / Ivy

There is a newer version: 8.0.33
Show newest version
/*
 * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, version 2.0, as published by the
 * Free Software Foundation.
 *
 * This program is also distributed with certain software (including but not
 * limited to OpenSSL) that is licensed under separate terms, as designated in a
 * particular file or component or in included license documentation. The
 * authors of MySQL hereby grant you an additional permission to link the
 * program and your derivative works with the separately licensed software that
 * they have included with MySQL.
 *
 * Without limiting anything contained in the foregoing, this file, which is
 * part of MySQL Connector/J, is also subject to the Universal FOSS Exception,
 * version 1.0, a copy of which can be found at
 * http://oss.oracle.com/licenses/universal-foss-exception.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License, version 2.0,
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
 */

package com.mysql.cj.protocol.x;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousCloseException;
import java.nio.channels.CompletionHandler;
import java.util.Optional;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import com.google.protobuf.CodedInputStream;
import com.google.protobuf.GeneratedMessage;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.Parser;
import com.mysql.cj.conf.PropertyDefinitions;
import com.mysql.cj.conf.PropertySet;
import com.mysql.cj.conf.RuntimeProperty;
import com.mysql.cj.exceptions.AssertionFailedException;
import com.mysql.cj.exceptions.CJCommunicationsException;
import com.mysql.cj.exceptions.WrongArgumentException;
import com.mysql.cj.protocol.MessageListener;
import com.mysql.cj.protocol.MessageReader;
import com.mysql.cj.protocol.SocketConnection;
import com.mysql.cj.x.protobuf.Mysqlx.Error;
import com.mysql.cj.x.protobuf.Mysqlx.ServerMessages;
import com.mysql.cj.x.protobuf.MysqlxNotice.Frame;

/**
 * Asynchronous low-level message reader for Protocol buffers encoded messages delivered by X Protocol.
 * The MessageReader will generally be used in one of two ways (See note regarding exceptions for Error messages):
 * 
    *
  • The next message type is known and it's an error to read any other type of message. The caller will generally call the reader like so: * *
     * MessageType msg = reader.readPayload(null, 0, ServerMessages.Type.THE_TYPE_VALUE);
     * 
    * *
  • *
  • The next message type is not known and the caller must conditionally decide what to do based on the type of the next message. The {@link * #readHeader()} method supports this use case. The caller will generally call the reader like so: * *
     * XMessageHeader header = reader.readHeader();
     * if (header.getMessageType() == ServerMessages.Type.TYPE_1_VALUE) {
     *     MessageType1 msg1 = reader.readPayload(null, 0, ServerMessages.Type.TYPE_1_VALUE);
     *     // do something with msg1
     * } else if (header.getMessageType() == ServerMessages.Type.TYPE_2_VALUE) {
     *     MessageType2 msg2 = reader.readPayload(null, 0, ServerMessages.Type.TYPE_2_VALUE);
     *     // do something with msg2
     * }
     * 
    * *
  • *
*

* If the MessageReader encounters an Error message, it will throw a {@link XProtocolError} exception to indicate that an error was returned from * the server. *

*

* All external interaction should know about message types listed in com.mysql.cj.x.protobuf.Mysqlx.ServerMessages. *

* TODO: write about async usage */ public class AsyncMessageReader implements MessageReader { private static int READ_AHEAD_DEPTH = 10; CompletedRead currentReadResult; /** Dynamic buffer to store the message body. */ ByteBuffer messageBuf; private PropertySet propertySet; /** The channel that we operate on. */ SocketConnection sc; CompletionHandler headerCompletionHandler = new HeaderCompletionHandler(); CompletionHandler messageCompletionHandler = new MessageCompletionHandler(); RuntimeProperty asyncTimeout; /** * The current MessageListener. This is set to null immediately following the listener's indicator that it is done reading * messages. It is set again when the next message is read and the next MessageListener is taken from the queue. */ MessageListener currentMessageListener; /** Queue of MessageListeners waiting to process messages. */ private BlockingQueue> messageListenerQueue = new LinkedBlockingQueue<>(); BlockingQueue pendingCompletedReadQueue = new LinkedBlockingQueue<>(READ_AHEAD_DEPTH); CompletableFuture pendingMsgHeader; /** Lock to protect the pending message. */ Object pendingMsgMonitor = new Object(); /** Have we been signalled to stop after the next message? */ boolean stopAfterNextMessage = false; private static class CompletedRead { public XMessageHeader header = null; public GeneratedMessage message = null; public CompletedRead() { } } public AsyncMessageReader(PropertySet propertySet, SocketConnection socketConnection) { this.propertySet = propertySet; this.sc = socketConnection; this.asyncTimeout = this.propertySet.getIntegerProperty(PropertyDefinitions.PNAME_asyncResponseTimeout); } public void start() { this.headerCompletionHandler.completed(0, null); // initiates header read cycle } public void stopAfterNextMessage() { this.stopAfterNextMessage = true; } private void checkClosed() { if (!this.sc.getAsynchronousSocketChannel().isOpen()) { throw new CJCommunicationsException("Socket closed"); } } public void pushMessageListener(MessageListener l) { checkClosed(); this.messageListenerQueue.add(l); } /** * Get the current or next {@link MessageListener}. This method works according to the following algorithm: *
    *
  • If there's a "current" {@link MessageListener} (indicated on last dispatch that it wanted more messages), it is returned.
  • *
  • If there's no current {@link MessageListener}, the queue is checked for the next one. If there's one in the queue, it is returned.
  • *
  • If there's no current and none in the queue, we either return null if block is false or wait for one to be put * in the queue if block is true.
  • *
*

* This method assigns to "current" the returned message listener. * * @param block * whether to block waiting for a MessageListener * @return the new current MessageListener */ MessageListener getMessageListener(boolean block) { try { if (this.currentMessageListener == null) { this.currentMessageListener = block ? this.messageListenerQueue.take() : this.messageListenerQueue.poll(); } return this.currentMessageListener; } catch (InterruptedException ex) { throw new CJCommunicationsException(ex); } } private class HeaderCompletionHandler implements CompletionHandler { public HeaderCompletionHandler() { } /** * Handler for "read completed" event. Consume the header data in header.headerBuf and initiate the reading of the message body. */ @Override public void completed(Integer bytesRead, Void attachment) { if (bytesRead < 0) { // async socket closed onError(new CJCommunicationsException("Socket closed")); return; } try { if (AsyncMessageReader.this.currentReadResult == null) { AsyncMessageReader.this.currentReadResult = new CompletedRead(); AsyncMessageReader.this.currentReadResult.header = new XMessageHeader(); } if (AsyncMessageReader.this.currentReadResult.header.getBuffer().position() < 5) { AsyncMessageReader.this.sc.getAsynchronousSocketChannel().read(AsyncMessageReader.this.currentReadResult.header.getBuffer(), null, this); return; // loop to #completed() again if we're still waiting for more data } //AsyncMessageReader.this.state = ReadingState.READING_MESSAGE; // TODO: re-use buffers if possible. Note that synchronization will be necessary to prevent overwriting re-used buffers while still being parsed by // previous read. Also the buffer will have to be managed accordingly so that "remaining" isn't longer than the message otherwise it may consume // data from the next header+message AsyncMessageReader.this.messageBuf = ByteBuffer.allocate(AsyncMessageReader.this.currentReadResult.header.getMessageSize()); // if there's no message listener waiting, expose the message class as pending for the next read if (getMessageListener(false) == null) { synchronized (AsyncMessageReader.this.pendingMsgMonitor) { AsyncMessageReader.this.pendingMsgHeader = CompletableFuture.completedFuture(AsyncMessageReader.this.currentReadResult.header); AsyncMessageReader.this.pendingMsgMonitor.notify(); } } AsyncMessageReader.this.messageCompletionHandler.completed(0, null); // initiates message read cycle } catch (Throwable t) { onError(t); // error reading => illegal state, close connection } } /** * Handler for "read failed" event. */ @Override public void failed(Throwable exc, Void attachment) { if (getMessageListener(false) != null) { // force any error to unblock pending message listener synchronized (AsyncMessageReader.this.pendingMsgMonitor) { AsyncMessageReader.this.pendingMsgMonitor.notify(); } if (AsynchronousCloseException.class.equals(exc.getClass())) { AsyncMessageReader.this.currentMessageListener.error(new CJCommunicationsException("Socket closed", exc)); } else { AsyncMessageReader.this.currentMessageListener.error(exc); } } // it's "done" after sending a closed() or error() signal AsyncMessageReader.this.currentMessageListener = null; } } private class MessageCompletionHandler implements CompletionHandler { public MessageCompletionHandler() { } /** * Read and consume the message body and dispatch the message to the current/next {@link MessageListener}. */ @Override public void completed(Integer bytesRead, Void attachment) { if (bytesRead < 0) { // async socket closed onError(new CJCommunicationsException("Socket closed")); return; } try { if (AsyncMessageReader.this.messageBuf.position() < AsyncMessageReader.this.currentReadResult.header.getMessageSize()) { AsyncMessageReader.this.sc.getAsynchronousSocketChannel().read(AsyncMessageReader.this.messageBuf, null, this); return; // loop to #completed() again if we're still waiting for more data } // copy these before initiating the next read to prevent them being overwritten in another thread ByteBuffer buf = AsyncMessageReader.this.messageBuf; AsyncMessageReader.this.messageBuf = null; Class messageClass = MessageConstants .getMessageClassForType(AsyncMessageReader.this.currentReadResult.header.getMessageType()); // Capture this flag value before dispatching the message, otherwise we risk having a different value when using it later on. boolean localStopAfterNextMessage = AsyncMessageReader.this.stopAfterNextMessage; // dispatch the message to the listener before starting next read to ensure in-order delivery buf.flip(); AsyncMessageReader.this.currentReadResult.message = parseMessage(messageClass, buf); AsyncMessageReader.this.pendingCompletedReadQueue.add(AsyncMessageReader.this.currentReadResult); AsyncMessageReader.this.currentReadResult = null; dispatchMessage(); // As this is where the read loop begins, we can escape it here if requested. // But we always read the next message if the current one is a notice. if (localStopAfterNextMessage && messageClass != Frame.class) { AsyncMessageReader.this.stopAfterNextMessage = false; // TODO it's a suspicious action, can we really change the global variable value here after we stated that it may be reset after dispatchMessage() ? AsyncMessageReader.this.currentReadResult = null; return; } AsyncMessageReader.this.headerCompletionHandler.completed(0, null); // initiates header read cycle } catch (Throwable t) { onError(t); // error reading => illegal state, close connection } } @Override public void failed(Throwable exc, Void attachment) { if (getMessageListener(false) != null) { // force any error to unblock pending message listener synchronized (AsyncMessageReader.this.pendingMsgMonitor) { AsyncMessageReader.this.pendingMsgMonitor.notify(); } if (AsynchronousCloseException.class.equals(exc.getClass())) { AsyncMessageReader.this.currentMessageListener.error(new CJCommunicationsException("Socket closed", exc)); } else { AsyncMessageReader.this.currentMessageListener.error(exc); } } // it's "done" after sending a closed() or error() signal AsyncMessageReader.this.currentMessageListener = null; } /** * Parse a message. * * @param messageClass * class extending {@link GeneratedMessage} * @param buf * message buffer * @return {@link GeneratedMessage} */ private GeneratedMessage parseMessage(Class messageClass, ByteBuffer buf) { try { Parser parser = MessageConstants.MESSAGE_CLASS_TO_PARSER.get(messageClass); return parser.parseFrom(CodedInputStream.newInstance(buf)); } catch (InvalidProtocolBufferException ex) { throw AssertionFailedException.shouldNotHappen(ex); } } } /** * Dispatch a message to a listener or "peek-er" once it has been read and parsed. */ void dispatchMessage() { if (this.pendingCompletedReadQueue.isEmpty()) { return; } if (getMessageListener(true) != null) { CompletedRead res; try { res = this.pendingCompletedReadQueue.take(); } catch (InterruptedException e) { throw new CJCommunicationsException("Failed to peek pending message", e); } GeneratedMessage message = res.message; // we must ensure that the message has been delivered and the pending message is cleared atomically under the pending message lock. otherwise the // pending message may still be seen after the message has been delivered but before the pending message is cleared // // t1-nio-thread | t2-user-thread // ------------------------------------------------------+------------------------------------------------------ // pendingMsgClass exposed - no current listener | // | listener added // getMessageListener(true) returns | // dispatchMessage(), in currentMessageListener.apply() | // | getNextMessageClass(), pendingMsgClass != null // | pendingMsgClass returned, but already being delivered // | in other thread // pendingMsgClass = null | // synchronized (this.pendingMsgMonitor) { // we repeatedly deliver messages to the current listener until he yields control and we move on to the next boolean currentListenerDone = this.currentMessageListener.createFromMessage(new XMessage(message)); if (currentListenerDone) { this.currentMessageListener = null; } // clear this after the message is delivered this.pendingMsgHeader = null; } } } void onError(Throwable t) { try { this.sc.getAsynchronousSocketChannel().close(); } catch (Exception ex) { // ignore } // notify all listeners of error if (this.currentMessageListener != null) { try { this.currentMessageListener.error(t); } catch (Exception ex) { } this.currentMessageListener = null; } this.messageListenerQueue.forEach(l -> { try { l.error(t); } catch (Exception ex) { } }); // in case we have a getNextMessageClass() request pending synchronized (this.pendingMsgMonitor) { this.pendingMsgHeader = new CompletableFuture<>(); this.pendingMsgHeader.completeExceptionally(t); this.pendingMsgMonitor.notify(); } this.messageListenerQueue.clear(); } /** * Peek into the pending message for it's class/type. This method blocks until a message is available. A message will not become available to peek until * there are no pending message listeners on this reader. * * @return the header of the next message to be delivered */ @Override public XMessageHeader readHeader() throws IOException { XMessageHeader mh; synchronized (this.pendingMsgMonitor) { checkClosed(); while (this.pendingMsgHeader == null) { try { this.pendingMsgMonitor.wait(); } catch (InterruptedException ex) { throw new CJCommunicationsException(ex); } } try { // get the message header before releasing the lock (when the future may be overwritten) mh = this.pendingMsgHeader.get(); } catch (ExecutionException ex) { throw new CJCommunicationsException("Failed to peek pending message", ex.getCause()); } catch (InterruptedException ex) { throw new CJCommunicationsException(ex); } } if (mh.getMessageType() == ServerMessages.Type.ERROR_VALUE) { // this will cause a the error to be read and thrown as an exception. this can't be called under the lock as it will block the message delivery readMessage(null, mh); } return mh; } @Override public XMessage readMessage(Optional reuse, XMessageHeader hdr) throws IOException { return readMessage(reuse, hdr.getMessageType()); } @Override public XMessage readMessage(Optional reuse, int expectedType) throws IOException { Class expectedClass = MessageConstants.getMessageClassForType(expectedType); CompletableFuture future = new CompletableFuture<>(); SyncXMessageListener r = new SyncXMessageListener<>(future, expectedClass); pushMessageListener(r); try { return future.get(this.asyncTimeout.getValue(), TimeUnit.SECONDS); } catch (ExecutionException ex) { if (XProtocolError.class.equals(ex.getCause().getClass())) { // wrap the other thread's exception and include this thread's context throw new XProtocolError((XProtocolError) ex.getCause()); } throw new CJCommunicationsException(ex.getCause().getMessage(), ex.getCause()); } catch (InterruptedException | TimeoutException ex) { throw new CJCommunicationsException(ex); } } /** * Synchronously read a single message and propagate any errors to the current thread. * * @param * GeneratedMessage type */ private static final class SyncXMessageListener implements MessageListener { private CompletableFuture future; private Class expectedClass; public SyncXMessageListener(CompletableFuture future, Class expectedClass) { this.future = future; this.expectedClass = expectedClass; } @SuppressWarnings("unchecked") @Override public Boolean createFromMessage(XMessage msg) { Class msgClass = (Class) msg.getMessage().getClass(); if (Error.class.equals(msgClass)) { this.future.completeExceptionally(new XProtocolError(Error.class.cast(msg.getMessage()))); return true; /* done reading? */ } else if (this.expectedClass.equals(msgClass)) { this.future.complete(msg); return true; /* done reading? */ } this.future.completeExceptionally(new WrongArgumentException("Unhandled msg class (" + msgClass + ") + msg=" + msg.getMessage())); return true; /* done reading? */ } public void error(Throwable ex) { this.future.completeExceptionally(ex); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy