com.tesora.dve.db.mysql.MysqlConnection.java Source code

Java tutorial

Introduction

Here is the source code for com.tesora.dve.db.mysql.MysqlConnection.java

Source

package com.tesora.dve.db.mysql;

/*
 * #%L
 * Tesora Inc.
 * Database Virtualization Engine
 * %%
 * Copyright (C) 2011 - 2014 Tesora Inc.
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License, version 3,
 * as published by the Free Software Foundation.
 * 
 * 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 Affero General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */

import com.tesora.dve.charset.NativeCharSetCatalog;
import com.tesora.dve.charset.NativeCharSetCatalogImpl;
import com.tesora.dve.common.DBType;
import com.tesora.dve.concurrent.CompletionHandle;
import com.tesora.dve.concurrent.DelegatingCompletionHandle;
import com.tesora.dve.db.mysql.portal.protocol.*;
import com.tesora.dve.exceptions.PECommunicationsException;
import com.tesora.dve.exceptions.PESQLStateException;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.buffer.UnpooledByteBufAllocator;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;

import java.net.InetSocketAddress;
import java.nio.charset.Charset;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Objects;
import com.tesora.dve.common.PEStringUtils;
import com.tesora.dve.common.PEUrl;
import com.tesora.dve.common.catalog.StorageSite;
import com.tesora.dve.concurrent.PEDefaultPromise;
import com.tesora.dve.db.CommandChannel;
import com.tesora.dve.db.DBConnection;
import com.tesora.dve.db.DBNative;
import com.tesora.dve.db.mysql.portal.protocol.MyBackendDecoder;
import com.tesora.dve.db.mysql.portal.protocol.MysqlClientAuthenticationHandler;
import com.tesora.dve.db.mysql.portal.protocol.StreamValve;
import com.tesora.dve.exceptions.PEException;
import com.tesora.dve.server.messaging.SQLCommand;
import com.tesora.dve.singleton.Singletons;
import com.tesora.dve.variables.KnownVariables;
import com.tesora.dve.variables.VariableHandler;
import com.tesora.dve.worker.DevXid;
import com.tesora.dve.worker.UserCredentials;

public class MysqlConnection implements DBConnection, DBConnection.Monitor, CommandChannel {

    public static final boolean USE_POOLED_BUFFERS = !(Boolean
            .getBoolean("com.tesora.dve.netty.disablePooledBuffers"));

    private static final boolean PACKET_LOGGER = Boolean.getBoolean("MysqlConnection.packetLogger");

    // TODO what's the general strategy for mapping MySQL error codes? (see PE-5)
    private static final int MYSQL_ERR_QUERY_INTERRUPTED = 1317;

    static Logger logger = LoggerFactory.getLogger(MysqlConnection.class);

    DeferredErrorHandle deferredErrorHandle = new DeferredErrorHandle();

    private long shutdownQuietPeriod = 2;
    private long shutdownTimeout = 15;

    public static class Factory implements DBConnection.Factory {
        @Override
        public DBConnection newInstance(EventLoopGroup eventLoop, StorageSite site) {
            return new MysqlConnection(eventLoop, site);
        }
    }

    Bootstrap mysqlBootstrap;
    EventLoopGroup connectionEventGroup;
    private UUID physicalID;
    private Channel channel;
    private ChannelFuture pendingConnection;
    private MysqlClientAuthenticationHandler authHandler;

    private Exception pendingException = null;
    private final StorageSite site;

    private boolean pendingUpdate = false;
    private boolean hasActiveTransaction = false;

    private AtomicReference<Charset> targetCharset = new AtomicReference<>();

    Map<String, String> currentSessionVariables = new HashMap<>();
    Map<String, String> sessionDefaults = new HashMap<>();

    public MysqlConnection(EventLoopGroup eventLoop, StorageSite site) {
        this.connectionEventGroup = eventLoop;
        this.site = site;
        setupDefaultSessionVars(site);
    }

    public MysqlConnection(StorageSite site) {
        this(SharedEventLoopHolder.getLoop(), site);
    }

    private void setupDefaultSessionVars(StorageSite site) {
        //provide some default session variables that can be overridden.
        sessionDefaults.put("@" + DBNative.DVE_SITENAME_VAR, site.getName());
        sessionDefaults.put("wait_timeout", getGlobalValueForVariable(KnownVariables.BACKEND_WAIT_TIMEOUT));
        sessionDefaults.put("character_set_connection",
                getGlobalValueForVariable(KnownVariables.CHARACTER_SET_CONNECTION));
        sessionDefaults.put("character_set_client", getGlobalValueForVariable(KnownVariables.CHARACTER_SET_CLIENT));
        sessionDefaults.put("character_set_results",
                getGlobalValueForVariable(KnownVariables.CHARACTER_SET_RESULTS));
    }

    private static String getGlobalValueForVariable(final VariableHandler<?> variable) {
        return variable.getGlobalValue(null).toString();
    }

    public void setShutdownQuietPeriod(final long seconds) {
        this.shutdownQuietPeriod = seconds;
    }

    public void setShutdownTimeout(final long seconds) {
        this.shutdownTimeout = seconds;
    }

    public long getShutdownQuietPeriod() {
        return this.shutdownQuietPeriod;
    }

    public long getShutdownTimeout() {
        return this.shutdownTimeout;
    }

    @Override
    public void connect(String url, final String userid, final String password, final long clientCapabilities)
            throws PEException {
        PEUrl peUrl = PEUrl.fromUrlString(url);

        if (!"mysql".equalsIgnoreCase(peUrl.getSubProtocol()))
            throw new PEException(MysqlConnection.class.getSimpleName()
                    + " does not support the sub protocol of url \"" + url + "\"");

        InetSocketAddress serverAddress = new InetSocketAddress(peUrl.getHost(), peUrl.getPort());
        final MyBackendDecoder.CharsetDecodeHelper charsetHelper = new CharsetDecodeHelper();
        mysqlBootstrap = new Bootstrap();
        mysqlBootstrap // .group(inboundChannel.eventLoop())
                .channel(NioSocketChannel.class).group(connectionEventGroup)
                .option(ChannelOption.ALLOCATOR,
                        USE_POOLED_BUFFERS ? PooledByteBufAllocator.DEFAULT : UnpooledByteBufAllocator.DEFAULT)
                .handler(new ChannelInitializer<Channel>() {
                    @Override
                    protected void initChannel(Channel ch) throws Exception {
                        authHandler = new MysqlClientAuthenticationHandler(new UserCredentials(userid, password),
                                clientCapabilities, NativeCharSetCatalogImpl.getDefaultCharSetCatalog(DBType.MYSQL),
                                targetCharset);

                        if (PACKET_LOGGER)
                            ch.pipeline().addLast(new LoggingHandler(LogLevel.INFO));

                        ch.pipeline().addLast(authHandler)
                                .addLast(MyBackendDecoder.class.getSimpleName(),
                                        new MyBackendDecoder(site.getName(), charsetHelper))
                                .addLast(StreamValve.class.getSimpleName(), new StreamValve())
                                .addLast(MysqlCommandSenderHandler.class.getSimpleName(),
                                        new MysqlCommandSenderHandler(site));
                    }
                });

        pendingConnection = mysqlBootstrap.connect(serverAddress);

        //      System.out.println("Create connection: Allocated " + totalConnections.incrementAndGet() + ", active " + activeConnections.incrementAndGet());
        channel = pendingConnection.channel();
        physicalID = UUID.randomUUID();

        //TODO: this was moved from execute to connect, which avoids blocking on the execute to be netty friendly, but causes lag on checkout.  Should make this event driven like everything else. -sgossard
        syncToServerConnect();
        authHandler.assertAuthenticated();
        //      channel.closeFuture().addListener(new GenericFutureListener<Future<Void>>() {
        //         @Override
        //         public void operationComplete(Future<Void> future) throws Exception {
        //            System.out.println(channel + " is closed");
        //         }
        //      });
    }

    //syntactic sugar for some of the inner utility calls.
    protected void execute(String sql, CompletionHandle<Boolean> promise) {
        this.execute(new SQLCommand(lookupCurrentConnectionCharset(), sql), promise);
    }

    //syntactic sugar for some of the inner utility calls.
    protected void execute(SQLCommand sql, CompletionHandle<Boolean> promise) {
        //TODO: it would be good to replace this with a simple command, especially since we don't care about the result set (but watch out for deferred exceptions). -sgossard
        if (promise == null)
            promise = getExceptionDeferringPromise();

        MysqlMessage message = MSPComQueryRequestMessage.newMessage(sql.getBytes());
        this.writeAndFlush(message, new PassFailProcessor(promise));
    }

    /**
     *  The main entrypoint for newer style calls that are just a protocol request and response processor.  Currently this
     *  wraps the request/response in the appropriate old-style objects.  When all methods operate through this method,
     *  it will be possible to clean up the pipeline and make it use the new types natively.
     * @param outboundMessage
     * @param resultsProcessor
     */
    @Override
    public void write(MysqlMessage outboundMessage, MysqlCommandResultsProcessor resultsProcessor) {
        this.sendCommand(SimpleMysqlCommandBundle.bundle(outboundMessage, resultsProcessor), false);
    }

    @Override
    public void writeAndFlush(MysqlMessage outboundMessage, MysqlCommandResultsProcessor resultsProcessor) {
        this.sendCommand(SimpleMysqlCommandBundle.bundle(outboundMessage, resultsProcessor), true);
    }

    /**
     * Currently the main entrypoint for dispatching old-style requests.  The MysqlCommand object holds both the
     * request and response, and writing the request and reading/dispatching the response is handled in the pipeline.
     * @param command
     */
    protected void sendCommand(MysqlCommandBundle command, boolean shouldFlush) {
        try {
            CommandChannel connection = this;

            if (connection.isOpen()) {//need prefer comm exception to pending exception, some HA behavior depends on that.
                Exception deferredException = connection.getAndClearPendingException();
                if (deferredException == null) {
                    if (shouldFlush)
                        channel.writeAndFlush(command);
                    else
                        channel.write(command);
                } else {
                    command.getResponseProcessor().failure(deferredException); //if we are using the deferred error handle again, we'll just defer the exception again.
                }
            } else {
                command.getResponseProcessor()
                        .failure(new PECommunicationsException("Channel closed: " + connection));
            }
        } catch (Throwable t) {
            command.getResponseProcessor().failure(PEException.wrapThrowableIfNeeded(t));
        }
    }

    public String getName() {
        return site.getName();
    }

    @Override
    public UUID getPhysicalID() {
        return physicalID;
    }

    public Charset getTargetCharset() {
        return targetCharset.get();
    }

    @Override
    public StorageSite getStorageSite() {
        return site;
    }

    @Override
    public Monitor getMonitor() {
        return this;
    }

    public CompletionHandle<Boolean> getExceptionDeferringPromise() {
        return deferredErrorHandle;
    }

    public Exception getAndClearPendingException() {
        Exception currentError = pendingException;
        pendingException = null;
        return currentError;
    }

    public boolean isOpen() {
        return channel.isOpen();
    }

    @Override
    public boolean isWritable() {
        return channel.isWritable();
    }

    private void syncToServerConnect() {
        if (pendingConnection != null) {
            pendingConnection.syncUninterruptibly();
            pendingConnection = null;
        }
    }

    public boolean isActive() {
        return (channel != null && channel.isActive());
    }

    @Override
    public synchronized void close() {
        if (connectionEventGroup != null) {
            if (channel.isOpen()) {
                try {
                    MysqlMessage message = MSPComQuitRequestMessage.newMessage();
                    this.writeAndFlush(message, new MysqlQuitCommand());
                } finally {
                    channel.close().syncUninterruptibly();
                }
            }
            //NOTE: we don't shutdown the event loop. Under the shared thread model, we don't know if someone else is using it. -sgossard
            connectionEventGroup = null;
        }
    }

    @Override
    public void start(DevXid xid, CompletionHandle<Boolean> promise) {
        hasActiveTransaction = true;
        execute("XA START " + xid.getMysqlXid(), promise);
    }

    @Override
    public void end(DevXid xid, CompletionHandle<Boolean> promise) {
        execute("XA END " + xid.getMysqlXid(), promise);
    }

    @Override
    public void prepare(DevXid xid, CompletionHandle<Boolean> promise) {
        execute("XA PREPARE " + xid.getMysqlXid(), promise);
    }

    @Override
    public void commit(DevXid xid, boolean onePhase, CompletionHandle<Boolean> promise) {
        String sql = "XA COMMIT " + xid.getMysqlXid();
        if (onePhase)
            sql += " ONE PHASE";
        execute(sql, new DelegatingCompletionHandle<Boolean>(promise) {
            @Override
            public void success(Boolean returnValue) {
                clearActiveState();
                super.success(returnValue);
            }
        });
    }

    @Override
    public void rollback(DevXid xid, CompletionHandle<Boolean> promise) {
        execute("XA ROLLBACK " + xid.getMysqlXid(), new DelegatingCompletionHandle<Boolean>(promise) {
            @Override
            public void success(Boolean returnValue) {
                clearActiveState();
                super.success(returnValue);
            }

            @Override
            public void failure(Exception e) {
                if (e instanceof PESQLStateException && backendErrorImpliesNoOpenXA((PESQLStateException) e)) {
                    logger.warn(
                            "tried to rollback transaction, but response implied no XA exists, " + e.getMessage());
                    clearActiveState();
                    super.success(true);
                } else {
                    super.failure(e);
                }
            }
        });
    }

    private static final AtomicLong updates = new AtomicLong(0);

    public void updateSessionVariables(Map<String, String> desiredVariables, SetVariableSQLBuilder setBuilder,
            CompletionHandle<Boolean> promise) {
        final SQLCommand updateCommand;
        final Map<String, String> updatesRequired = new HashMap<>();
        try {

            Set<String> mergedKeys = new HashSet<>(desiredVariables.keySet());
            mergedKeys.addAll(currentSessionVariables.keySet());
            mergedKeys.addAll(sessionDefaults.keySet());

            for (String variableName : mergedKeys) {
                String existingValue = currentSessionVariables.get(variableName);
                String desiredValue;
                if (desiredVariables.containsKey(variableName))
                    desiredValue = desiredVariables.get(variableName);
                else
                    desiredValue = sessionDefaults.get(variableName);
                if (desiredValue == null) {
                    updatesRequired.put(variableName, null);
                    setBuilder.remove(variableName, existingValue);
                } else if (existingValue == null) {
                    updatesRequired.put(variableName, desiredValue);
                    setBuilder.add(variableName, desiredValue);
                } else if (!desiredValue.equals(existingValue)) {
                    updatesRequired.put(variableName, desiredValue);
                    setBuilder.update(variableName, existingValue, desiredValue);
                } else
                    setBuilder.same(variableName, desiredValue);
            }

            updateCommand = setBuilder.generateSql(lookupCurrentConnectionCharset());
            //            System.out.println(updates.getAndIncrement() + ": " + updateCommand);
        } catch (Exception e) {
            callbackSetVariablesFailed(updatesRequired, null, e);
            promise.failure(e);
            return;
        }

        if (updateCommand == SQLCommand.EMPTY) {
            promise.success(true);
        } else {
            execute(updateCommand, new DelegatingCompletionHandle<Boolean>(promise) {
                @Override
                public void success(Boolean returnValue) {
                    callbackSetVariablesOK(updatesRequired, updateCommand);
                    super.success(returnValue);
                }

                @Override
                public void failure(Exception e) {
                    callbackSetVariablesFailed(updatesRequired, updateCommand, e);
                    super.failure(e);
                }
            });
        }
    }

    private void callbackSetVariablesFailed(Map<String, String> updatesRequired, SQLCommand updateCommand,
            Exception e) {
        currentSessionVariables.clear();//not sure if this is ideal, but it forces variables to get set next attempt.
        logger.warn("Problem updating session variables on connection {} via command {}",
                new Object[] { site, updateCommand, e });
    }

    private void callbackSetVariablesOK(Map<String, String> updatesRequired, SQLCommand updateCommand) {
        for (Map.Entry<String, String> updateEntry : updatesRequired.entrySet()) {
            String key = updateEntry.getKey();
            String value = updateEntry.getValue();
            if (value == null)
                currentSessionVariables.remove(key);
            else
                currentSessionVariables.put(key, value);
        }
    }

    private void clearActiveState() {
        pendingUpdate = false;
        hasActiveTransaction = false;
    }

    public boolean backendErrorImpliesNoOpenXA(PESQLStateException mysqlError) {
        return mysqlError.getErrorNumber() == 1397 || //XAER_NOTA, XA id is completely unknown.
                (mysqlError.getErrorNumber() == 1399 && mysqlError.getErrorMsg().contains("NON-EXISTING")) //XAER_RMFAIL, because we are in NON-EXISTING state
        ;
    }

    @Override
    public void setCatalog(String databaseName, CompletionHandle<Boolean> promise) {
        execute("use " + databaseName, promise);
    }

    @Deprecated
    @Override
    public void cancel() {
        logger.warn("Cancelling running query via MysqlConnection.cancel() currently not supported.");
        //KILL QUERY needs to be sent on a different socket than the target query.  Sent on the same socket, it pends until the target query finishes, oops.  -sgossard
        //      try {
        //            execute(new SQLCommand("KILL QUERY " + authHandler.getThreadID()), MysqlKillResultDiscarder.INSTANCE, wrapHandler(null));
        //        } catch (PESQLException e) {
        //         logger.warn("Failed to cancel active query", e);
        //      }
    }

    public void handleFailure(Exception e) {
        pendingException = new PEException("Unhandled exception received on server housekeeping sql statement", e);
    }

    @Override
    public boolean hasPendingUpdate() {
        return pendingUpdate;
    }

    @Override
    public void onUpdate() {
        pendingUpdate = true;
    }

    @Override
    public boolean hasActiveTransaction() {
        return hasActiveTransaction;
    }

    @Override
    public int getConnectionId() {
        return (authHandler != null) ? authHandler.getThreadID() : 0;
    }

    @Override
    public String toString() {
        return Objects.toStringHelper(this).add("site", this.site.getName()).add("id", this.getConnectionId())
                .toString();
    }

    private class DeferredErrorHandle extends PEDefaultPromise<Boolean> {
        //supply our own handler that will report failures on some call in the future.
        //NOTE: we only use this when the caller didn't provide a promise, so it is OK that we don't call the parent success/failure, no one should ever be waiting. -sgossard

        //TODO: since query results generally come back in order, it would be good for these handlers to signal the next provided handler, not some arbitrary execute in the future. -sgossard
        @Override
        public void success(Boolean returnValue) {
            //ignore, no one is waiting.
        }

        @Override
        public void failure(Exception e) {
            MysqlConnection.this.handleFailure(e);
        }

    }

    public Charset lookupCurrentConnectionCharset() {
        final NativeCharSetCatalog charSetcatalog = Singletons.require(NativeCharSetCatalog.class);
        final String currentConnectionCharsetName = PEStringUtils
                .dequote(currentSessionVariables.get(KnownVariables.CHARACTER_SET_CLIENT.getName()));
        if (currentConnectionCharsetName == null) {
            return KnownVariables.CHARACTER_SET_CLIENT.getGlobalValue(null).getJavaCharset();
        }

        return charSetcatalog.findCharSetByName(currentConnectionCharsetName).getJavaCharset();
    }

}