brooklyn.entity.database.mysql.MySqlClusterImpl.java Source code

Java tutorial

Introduction

Here is the source code for brooklyn.entity.database.mysql.MySqlClusterImpl.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 brooklyn.entity.database.mysql;

import java.util.Collection;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.entity.basic.EntityLocal;
import org.apache.brooklyn.api.entity.proxying.EntitySpec;
import org.apache.brooklyn.api.event.AttributeSensor;
import org.apache.brooklyn.api.event.SensorEvent;
import org.apache.brooklyn.api.event.SensorEventListener;
import org.apache.brooklyn.api.location.Location;

import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.reflect.TypeToken;

import brooklyn.config.ConfigKey;
import brooklyn.enricher.Enrichers;
import brooklyn.entity.basic.Attributes;
import brooklyn.entity.basic.EntityInternal;
import brooklyn.entity.basic.EntityPredicates;
import brooklyn.entity.basic.ServiceStateLogic.ServiceNotUpLogic;
import brooklyn.entity.group.DynamicClusterImpl;
import brooklyn.event.basic.DependentConfiguration;
import brooklyn.event.basic.Sensors;
import brooklyn.event.feed.function.FunctionFeed;
import brooklyn.event.feed.function.FunctionPollConfig;
import brooklyn.util.collections.CollectionFunctionals;
import brooklyn.util.guava.Functionals;
import brooklyn.util.guava.IfFunctions;
import brooklyn.util.task.DynamicTasks;
import brooklyn.util.task.TaskBuilder;
import brooklyn.util.text.Identifiers;
import brooklyn.util.text.StringPredicates;
import brooklyn.util.time.Duration;

// https://dev.mysql.com/doc/refman/5.7/en/replication-howto.html

// TODO CREATION_SCRIPT_CONTENTS executed before replication setup so it is not replicated to slaves
// TODO Bootstrap slave from dump for the case where the binary log is purged
// TODO Promote slave to master
// TODO SSL connection between master and slave
// TODO DB credentials littered all over the place in file system
public class MySqlClusterImpl extends DynamicClusterImpl implements MySqlCluster {
    private static final AttributeSensor<Boolean> NODE_REPLICATION_INITIALIZED = Sensors
            .newBooleanSensor("mysql.replication_initialized");

    private static final String MASTER_CONFIG_URL = "classpath:///brooklyn/entity/database/mysql/mysql_master.conf";
    private static final String SLAVE_CONFIG_URL = "classpath:///brooklyn/entity/database/mysql/mysql_slave.conf";
    private static final int MASTER_SERVER_ID = 1;
    private static final Predicate<Entity> IS_MASTER = EntityPredicates.configEqualTo(MySqlNode.MYSQL_SERVER_ID,
            MASTER_SERVER_ID);

    @SuppressWarnings("serial")
    private static final AttributeSensor<Supplier<Integer>> SLAVE_NEXT_SERVER_ID = Sensors
            .newSensor(new TypeToken<Supplier<Integer>>() {
            }, "mysql.slave.next_server_id", "Returns the ID of the next slave server");
    @SuppressWarnings("serial")
    private static final AttributeSensor<Map<String, String>> SLAVE_ID_ADDRESS_MAPPING = Sensors
            .newSensor(new TypeToken<Map<String, String>>() {
            }, "mysql.slave.id_address_mapping",
                    "Maps slave entity IDs to SUBNET_ADDRESS, so the address is known at member remove time.");

    @Override
    public void init() {
        super.init();
        // Set id supplier in attribute so it is serialized
        setAttribute(SLAVE_NEXT_SERVER_ID, new NextServerIdSupplier());
        setAttribute(SLAVE_ID_ADDRESS_MAPPING, new ConcurrentHashMap<String, String>());
        if (getConfig(SLAVE_PASSWORD) == null) {
            setAttribute(SLAVE_PASSWORD, Identifiers.makeRandomId(8));
        } else {
            setAttribute(SLAVE_PASSWORD, getConfig(SLAVE_PASSWORD));
        }
        initSubscriptions();
    }

    @Override
    public void rebind() {
        super.rebind();
        initSubscriptions();
    }

    private void initSubscriptions() {
        subscribeToMembers(this, MySqlNode.SERVICE_PROCESS_IS_RUNNING, new NodeRunningListener(this));
        subscribe(this, MEMBER_REMOVED, new MemberRemovedListener());
    }

    @Override
    protected void initEnrichers() {
        super.initEnrichers();
        propagateMasterAttribute(MySqlNode.HOSTNAME);
        propagateMasterAttribute(MySqlNode.ADDRESS);
        propagateMasterAttribute(MySqlNode.SUBNET_HOSTNAME);
        propagateMasterAttribute(MySqlNode.SUBNET_ADDRESS);
        propagateMasterAttribute(MySqlNode.MYSQL_PORT);
        propagateMasterAttribute(MySqlNode.DATASTORE_URL);

        addEnricher(Enrichers.builder().aggregating(MySqlNode.DATASTORE_URL).publishing(SLAVE_DATASTORE_URL_LIST)
                .computing(Functions.<Collection<String>>identity()).entityFilter(Predicates.not(IS_MASTER))
                .fromMembers().build());

        addEnricher(Enrichers.builder().aggregating(MySqlNode.QUERIES_PER_SECOND_FROM_MYSQL)
                .publishing(QUERIES_PER_SECOND_FROM_MYSQL_PER_NODE).fromMembers().computingAverage()
                .defaultValueForUnreportedSensors(0d).build());
    }

    private void propagateMasterAttribute(AttributeSensor<?> att) {
        addEnricher(Enrichers.builder().aggregating(att).publishing(att)
                .computing(IfFunctions.ifPredicate(CollectionFunctionals.notEmpty())
                        .apply(CollectionFunctionals.firstElement()).defaultValue(null))
                .entityFilter(IS_MASTER).build());
    }

    @Override
    protected EntitySpec<?> getFirstMemberSpec() {
        final EntitySpec<?> firstMemberSpec = super.getFirstMemberSpec();
        if (firstMemberSpec != null) {
            return applyDefaults(firstMemberSpec, Suppliers.ofInstance(MASTER_SERVER_ID), MASTER_CONFIG_URL, false);
        }

        final EntitySpec<?> memberSpec = super.getMemberSpec();
        if (memberSpec != null) {
            if (!isKeyConfigured(memberSpec, MySqlNode.TEMPLATE_CONFIGURATION_URL.getConfigKey())) {
                return EntitySpec.create(memberSpec).configure(MySqlNode.MYSQL_SERVER_ID, MASTER_SERVER_ID)
                        .configure(MySqlNode.TEMPLATE_CONFIGURATION_URL, MASTER_CONFIG_URL);
            } else {
                return memberSpec;
            }
        }

        return EntitySpec.create(MySqlNode.class).displayName("MySql Master")
                .configure(MySqlNode.MYSQL_SERVER_ID, MASTER_SERVER_ID)
                .configure(MySqlNode.TEMPLATE_CONFIGURATION_URL, MASTER_CONFIG_URL);
    }

    @Override
    protected EntitySpec<?> getMemberSpec() {
        Supplier<Integer> serverIdSupplier = getAttribute(SLAVE_NEXT_SERVER_ID);

        EntitySpec<?> spec = super.getMemberSpec();
        if (spec != null) {
            return applyDefaults(spec, serverIdSupplier, SLAVE_CONFIG_URL, true);
        }

        return EntitySpec.create(MySqlNode.class).displayName("MySql Slave")
                .configure(MySqlNode.MYSQL_SERVER_ID, serverIdSupplier.get())
                .configure(MySqlNode.TEMPLATE_CONFIGURATION_URL, SLAVE_CONFIG_URL)
                // block inheritance, only master should execute the creation script
                .configure(MySqlNode.CREATION_SCRIPT_URL, (String) null)
                .configure(MySqlNode.CREATION_SCRIPT_CONTENTS, (String) null);
    }

    private EntitySpec<?> applyDefaults(EntitySpec<?> spec, Supplier<Integer> serverId, String configUrl,
            boolean resetCreationScript) {
        boolean needsServerId = !isKeyConfigured(spec, MySqlNode.MYSQL_SERVER_ID);
        boolean needsConfigUrl = !isKeyConfigured(spec, MySqlNode.TEMPLATE_CONFIGURATION_URL.getConfigKey());
        boolean needsCreationScriptUrl = resetCreationScript
                && !isKeyConfigured(spec, MySqlNode.CREATION_SCRIPT_URL);
        boolean needsCreationScriptContents = resetCreationScript
                && !isKeyConfigured(spec, MySqlNode.CREATION_SCRIPT_CONTENTS);
        if (needsServerId || needsConfigUrl || needsCreationScriptUrl || needsCreationScriptContents) {
            EntitySpec<?> clonedSpec = EntitySpec.create(spec);
            if (needsServerId) {
                clonedSpec.configure(MySqlNode.MYSQL_SERVER_ID, serverId.get());
            }
            if (needsConfigUrl) {
                clonedSpec.configure(MySqlNode.TEMPLATE_CONFIGURATION_URL, configUrl);
            }
            if (needsCreationScriptUrl) {
                clonedSpec.configure(MySqlNode.CREATION_SCRIPT_URL, (String) null);
            }
            if (needsCreationScriptContents) {
                clonedSpec.configure(MySqlNode.CREATION_SCRIPT_URL, (String) null);
            }
            return clonedSpec;
        } else {
            return spec;
        }
    }

    private boolean isKeyConfigured(EntitySpec<?> spec, ConfigKey<?> key) {
        return spec.getConfig().containsKey(key) || spec.getFlags().containsKey(key.getName());
    }

    @Override
    protected Entity createNode(Location loc, Map<?, ?> flags) {
        Entity node = super.createNode(loc, flags);
        if (!IS_MASTER.apply(node)) {
            ServiceNotUpLogic.updateNotUpIndicator((EntityLocal) node, MySqlSlave.SLAVE_HEALTHY,
                    "Replication not started");

            addFeed(FunctionFeed.builder().entity((EntityLocal) node).period(Duration.FIVE_SECONDS)
                    .poll(FunctionPollConfig.forSensor(MySqlSlave.SLAVE_HEALTHY)
                            .callable(new SlaveStateCallable(node)).checkSuccess(StringPredicates.isNonBlank())
                            .onSuccess(new SlaveStateParser(node)).setOnFailure(false)
                            .description("Polls SHOW SLAVE STATUS"))
                    .build());

            node.addEnricher(Enrichers.builder().updatingMap(Attributes.SERVICE_NOT_UP_INDICATORS)
                    .from(MySqlSlave.SLAVE_HEALTHY)
                    .computing(Functionals.ifNotEquals(true).value("Slave replication status is not healthy"))
                    .build());
        }
        return node;
    }

    public static class SlaveStateCallable implements Callable<String> {
        private Entity slave;

        public SlaveStateCallable(Entity slave) {
            this.slave = slave;
        }

        @Override
        public String call() throws Exception {
            if (Boolean.TRUE.equals(slave.getAttribute(MySqlNode.SERVICE_PROCESS_IS_RUNNING))) {
                return slave.invoke(MySqlNode.EXECUTE_SCRIPT, ImmutableMap.of("commands", "SHOW SLAVE STATUS \\G"))
                        .asTask().getUnchecked();
            } else {
                return null;
            }
        }

    }

    public static class SlaveStateParser implements Function<String, Boolean> {
        private Entity slave;

        public SlaveStateParser(Entity slave) {
            this.slave = slave;
        }

        @Override
        public Boolean apply(String result) {
            Map<String, String> status = MySqlRowParser.parseSingle(result);
            String secondsBehindMaster = status.get("Seconds_Behind_Master");
            if (secondsBehindMaster != null && !"NULL".equals(secondsBehindMaster)) {
                ((EntityLocal) slave).setAttribute(MySqlSlave.SLAVE_SECONDS_BEHIND_MASTER,
                        new Integer(secondsBehindMaster));
            }
            return "Yes".equals(status.get("Slave_IO_Running")) && "Yes".equals(status.get("Slave_SQL_Running"));
        }

    }

    private static class NextServerIdSupplier implements Supplier<Integer> {
        private AtomicInteger nextId = new AtomicInteger(MASTER_SERVER_ID + 1);

        @Override
        public Integer get() {
            return nextId.getAndIncrement();
        }
    }

    // ============= Member Init =============

    // The task is executed in inessential context (event handler) so
    // not visible in tasks UI. Better make it visible so the user can
    // see failures, currently accessible only from logs.
    private static final class InitReplicationTask implements Runnable {
        private final MySqlCluster cluster;
        private final MySqlNode node;

        private InitReplicationTask(MySqlCluster cluster, MySqlNode node) {
            this.cluster = cluster;
            this.node = node;
        }

        @Override
        public void run() {
            Integer serverId = node.getConfig(MySqlNode.MYSQL_SERVER_ID);
            if (serverId == MASTER_SERVER_ID) {
                initMaster(node);
            } else if (serverId > MASTER_SERVER_ID) {
                initSlave(node);
            }
        }

        private void initMaster(MySqlNode master) {
            String binLogInfo = executeScriptOnNode(master,
                    "FLUSH TABLES WITH READ LOCK;SHOW MASTER STATUS \\G UNLOCK TABLES;");
            Map<String, String> status = MySqlRowParser.parseSingle(binLogInfo);
            String file = status.get("File");
            if (file != null) {
                ((EntityInternal) master).setAttribute(MySqlMaster.MASTER_LOG_FILE, file);
            }
            String position = status.get("Position");
            if (position != null) {
                ((EntityInternal) master).setAttribute(MySqlMaster.MASTER_LOG_POSITION, new Integer(position));
            }
        }

        private void initSlave(MySqlNode slave) {
            MySqlNode master = (MySqlNode) Iterables.find(cluster.getMembers(), IS_MASTER);
            String masterLogFile = validateSqlParam(getAttributeBlocking(master, MySqlMaster.MASTER_LOG_FILE));
            Integer masterLogPos = getAttributeBlocking(master, MySqlMaster.MASTER_LOG_POSITION);
            String masterAddress = validateSqlParam(master.getAttribute(MySqlNode.SUBNET_ADDRESS));
            Integer masterPort = master.getAttribute(MySqlNode.MYSQL_PORT);
            String slaveAddress = validateSqlParam(slave.getAttribute(MySqlNode.SUBNET_ADDRESS));
            String username = validateSqlParam(cluster.getConfig(SLAVE_USERNAME));
            String password = validateSqlParam(cluster.getAttribute(SLAVE_PASSWORD));

            executeScriptOnNode(master,
                    String.format(
                            "CREATE USER '%s'@'%s' IDENTIFIED BY '%s';\n"
                                    + "GRANT REPLICATION SLAVE ON *.* TO '%s'@'%s';\n",
                            username, slaveAddress, password, username, slaveAddress));

            String slaveCmd = String.format(
                    "CHANGE MASTER TO " + "MASTER_HOST='%s', " + "MASTER_PORT=%d, " + "MASTER_USER='%s', "
                            + "MASTER_PASSWORD='%s', " + "MASTER_LOG_FILE='%s', " + "MASTER_LOG_POS=%d;\n"
                            + "START SLAVE;\n",
                    masterAddress, masterPort, username, password, masterLogFile, masterLogPos);
            executeScriptOnNode(slave, slaveCmd);

            cluster.getAttribute(SLAVE_ID_ADDRESS_MAPPING).put(slave.getId(),
                    slave.getAttribute(MySqlNode.SUBNET_ADDRESS));
        }

        private <T> T getAttributeBlocking(Entity masterNode, AttributeSensor<T> att) {
            return DynamicTasks.queue(DependentConfiguration.attributeWhenReady(masterNode, att)).getUnchecked();
        }

    }

    private static final class NodeRunningListener implements SensorEventListener<Boolean> {
        private MySqlCluster cluster;

        public NodeRunningListener(MySqlCluster cluster) {
            this.cluster = cluster;
        }

        @Override
        public void onEvent(SensorEvent<Boolean> event) {
            final MySqlNode node = (MySqlNode) event.getSource();
            if (Boolean.TRUE.equals(event.getValue()) &&
            // We are interested in SERVICE_PROCESS_IS_RUNNING only while haven't come online yet.
            // Probably will get several updates while replication is initialized so an additional
            // check is needed whether we have already seen this.
                    Boolean.FALSE.equals(node.getAttribute(MySqlNode.SERVICE_UP))
                    && !Boolean.TRUE.equals(node.getAttribute(NODE_REPLICATION_INITIALIZED))) {

                // Events executed sequentially so no need to synchronize here.
                ((EntityLocal) node).setAttribute(NODE_REPLICATION_INITIALIZED, Boolean.TRUE);

                DynamicTasks
                        .queueIfPossible(TaskBuilder.builder().name("Configure master-slave replication on node")
                                .body(new InitReplicationTask(cluster, node)).build())
                        .orSubmitAsync(node);
            }
        }

    }

    // ============= Member Remove =============

    public class MemberRemovedListener implements SensorEventListener<Entity> {
        @Override
        public void onEvent(SensorEvent<Entity> event) {
            MySqlCluster cluster = (MySqlCluster) event.getSource();
            Entity node = event.getValue();
            String slaveAddress = cluster.getAttribute(SLAVE_ID_ADDRESS_MAPPING).remove(node.getId());
            if (slaveAddress != null) {
                DynamicTasks
                        .queueIfPossible(TaskBuilder.builder().name("Remove slave access")
                                .body(new RemoveSlaveConfigTask(cluster, slaveAddress)).build())
                        .orSubmitAsync(cluster);
            }
        }
    }

    public class RemoveSlaveConfigTask implements Runnable {
        private MySqlCluster cluster;
        private String slaveAddress;

        public RemoveSlaveConfigTask(MySqlCluster cluster, String slaveAddress) {
            this.cluster = cluster;
            this.slaveAddress = validateSqlParam(slaveAddress);
        }

        @Override
        public void run() {
            // Could already be gone if stopping the entire app - let it throw an exception
            MySqlNode master = (MySqlNode) Iterables.find(cluster.getMembers(), IS_MASTER);
            String username = validateSqlParam(cluster.getConfig(SLAVE_USERNAME));
            executeScriptOnNode(master, String.format("DROP USER '%s'@'%s';", username, slaveAddress));
        }

    }

    // Can't call node.executeScript directly, need to change execution context, so use an effector task
    private static String executeScriptOnNode(MySqlNode node, String commands) {
        return node.invoke(MySqlNode.EXECUTE_SCRIPT, ImmutableMap.of(MySqlNode.EXECUTE_SCRIPT_COMMANDS, commands))
                .getUnchecked();
    }

    private static String validateSqlParam(String config) {
        // Don't go into escape madness, just deny any suspicious strings.
        // Would be nice to use prepared statements, but not worth pulling in the extra dependencies.
        if (config.contains("'") && config.contains("\\")) {
            throw new IllegalStateException("User provided string contains illegal SQL characters: " + config);
        }
        return config;
    }

}