org.voltdb.AdmissionControlGroup.java Source code

Java tutorial

Introduction

Here is the source code for org.voltdb.AdmissionControlGroup.java

Source

/* This file is part of VoltDB.
 * Copyright (C) 2008-2013 VoltDB Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * 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 VoltDB.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.voltdb;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;

import org.voltcore.logging.VoltLogger;
import org.voltdb.dtxn.InitiatorStats;
import org.voltdb.dtxn.InitiatorStats.InvocationInfo;

import com.google.common.collect.ImmutableMap;

/**
 * Manage admission control for incoming requests by tracking the size of outstanding requests
 * as well as the number of outstanding requests in order to bound the total amount of memory
 * used to service the requests.
 *
 * Responses are also tracked by size although not by count and use the same resource pool
 * as incoming requests size wise. Count of responses is not tracked because they are flattened
 * and coalesced to byte buffers so the metadata overhead is minimal and not worth tracking separately
 *
 * Admission control only limits the amount of work each group is willing to accept into the cluster.
 * Because there is no coordination between groups it is possible for all the work to end up at one node.
 * This is guaranteed to happen if one node is slow enough that it can't keep up with the workload.
 */
public class AdmissionControlGroup implements org.voltcore.network.QueueMonitor {
    private static final VoltLogger networkLog = new VoltLogger("NETWORK");

    /*
     * Maximum values for each group are configured when the group is constructed
     */
    final private int MAX_DESIRED_PENDING_BYTES;
    final private int LESS_THAN_MAX_DESIRED_PENDING_BYTES;
    final private int MAX_DESIRED_PENDING_TXNS;
    final private int LESS_THAN_MAX_DESIRED_PENDING_TXNS;

    private static final VoltLogger hostLog = new VoltLogger("HOST");

    private int m_pendingTxnCount = 0;
    private long m_pendingTxnBytes = 0;
    private boolean m_hadBackPressure = false;

    /*
     * If for some reason ACG logs a negative transaction count or outstanding bytes,
     * only do it once to avoid flooding. Not going to make it a fatal error, but
     * don't want it to fail silently either.
     */
    private boolean m_haveLoggedACGNegativeFailure = false;

    /*
     * The ACG is accessed lock free from the network thread that owns the group.
     * Track the thread id of callers of this class to ensure that it is always the right
     * thread using assertions.
     */
    private final long m_expectedThreadId = Thread.currentThread().getId();

    /*
     * Members of the admission control group implement this interface and are expected
     * to stop accepting new requests when after onBackpressure is invoked and are allowed
     * to start accepting requests again once offBackpressure is invoked
     */
    public interface ACGMember {
        public void onBackpressure();

        public void offBackpressure();

        public long connectionId();
    }

    private final HashSet<ACGMember> m_members = new HashSet<ACGMember>();

    /*
     * There will be unsynchronized reads of the map hence volatile to ensure
     * new versions of the map are completely constructed on read.
     *
     * Reads/writes to the actual InvocationInfo are unsynchronized. There is a single writer
     * so no issues there, but the reader is unprotected.
     */
    private volatile ImmutableMap<String, org.voltdb.dtxn.InitiatorStats.InvocationInfo> m_connectionStats = ImmutableMap.<String, org.voltdb.dtxn.InitiatorStats.InvocationInfo>builder()
            .build();

    public AdmissionControlGroup(int maxBytes, int maxRequests) {
        MAX_DESIRED_PENDING_BYTES = maxBytes;
        LESS_THAN_MAX_DESIRED_PENDING_BYTES = (int) (MAX_DESIRED_PENDING_BYTES * .8);
        MAX_DESIRED_PENDING_TXNS = maxRequests;
        LESS_THAN_MAX_DESIRED_PENDING_TXNS = (int) (MAX_DESIRED_PENDING_TXNS * .8);
    }

    public static AdmissionControlGroup getDummy() {
        return new AdmissionControlGroup(Integer.MAX_VALUE, Integer.MAX_VALUE) {
            @Override
            public void addMember(ACGMember member) {
            }

            @Override
            public void removeMember(ACGMember member) {
            }

            @Override
            public void increaseBackpressure(int messageSize) {
            }

            @Override
            public void reduceBackpressure(int messageSize) {
            }

            @Override
            public boolean queue(int bytes) {
                return false;
            }
        };
    }

    public void addMember(ACGMember member) {
        assert (m_expectedThreadId == Thread.currentThread().getId());
        m_members.add(member);
    }

    public void removeMember(ACGMember member) {
        assert (m_expectedThreadId == Thread.currentThread().getId());
        m_members.remove(member);
        ImmutableMap.Builder<String, InitiatorStats.InvocationInfo> builder = ImmutableMap.builder();
        String endsWith = "$" + member.connectionId();
        for (Map.Entry<String, InitiatorStats.InvocationInfo> entry : m_connectionStats.entrySet()) {
            final String key = entry.getKey();
            if (!key.endsWith(endsWith)) {
                builder.put(entry);
            }
        }
        m_connectionStats = builder.build();
    }

    /*
     * Invoked when accepting a new transaction. Increments pending txn count in addition
     * to tracking the number of request bytes accepted. Can invoke onBackpressure
     * on all group members if there isn't already a backpressure condition.
     */
    public void increaseBackpressure(int messageSize) {
        assert (m_expectedThreadId == Thread.currentThread().getId());
        if (messageSize < 1) {
            throw new IllegalArgumentException("Message size must be > 0 but was " + messageSize);
        }
        m_pendingTxnBytes += messageSize;
        m_pendingTxnCount++;

        checkAndLogInvariants();
        if (m_pendingTxnBytes > MAX_DESIRED_PENDING_BYTES || m_pendingTxnCount > MAX_DESIRED_PENDING_TXNS) {
            if (!m_hadBackPressure) {
                hostLog.debug("TXN back pressure began");
                m_hadBackPressure = true;
                for (ACGMember m : m_members) {
                    m.onBackpressure();
                }
            }
        }
    }

    /*
     * Check that various invariants are maintained. If they aren't log the error at most once,
     * and take corrective action to maintain the invariants
     */
    private void checkAndLogInvariants() {
        if (m_pendingTxnCount < 0 || m_pendingTxnBytes < 0) {
            boolean badTxnCount = m_pendingTxnCount < 0 ? true : false;
            boolean badPendingBytes = m_pendingTxnBytes < 0 ? true : false;
            if (!m_haveLoggedACGNegativeFailure) {
                m_haveLoggedACGNegativeFailure = true;
                if (badTxnCount) {
                    networkLog.error("Admission control error, negative outstanding transaction count. "
                            + "This is error is not fatal, but it does indicate that admission control "
                            + "is not correctly tracking transaction resource usage. This message will not repeat "
                            + "the next time the condition occurs to avoid log spam");
                }
                if (badPendingBytes) {
                    networkLog.error("Admission control error, negative outstanding transaction byte count ("
                            + m_pendingTxnBytes + "). "
                            + "This is error is not fatal, but it does indicate that admission control "
                            + "is not correctly tracking transaction resource usage. This message will not repeat "
                            + "the next time the condition occurs to avoid log spam");
                }
            }

            /*
             * Repair both. It's possible that repairing it will trigger a repair cascade
             * effectively rendering the ACG always permissive, but it should right itself
             * once all requests associated with the ACG have left the system and the correct values are indeed 0.
             */
            m_pendingTxnCount = 0;
            m_pendingTxnBytes = 0;
        }
    }

    /*
     * Invoked when receiving a response to a transaction. Decrements pending txn count in addition
     * to tracking the number of request bytes accepted. Can invoke offBackpressure
     * on all group members if there is a backpressure condition that has ended
     */
    public void reduceBackpressure(int messageSize) {
        assert (m_expectedThreadId == Thread.currentThread().getId());
        if (messageSize < 1) {
            throw new IllegalArgumentException("Message size must be > 0 but was " + messageSize);
        }
        m_pendingTxnBytes -= messageSize;
        m_pendingTxnCount--;
        checkAndLogInvariants();
        if (m_pendingTxnBytes < LESS_THAN_MAX_DESIRED_PENDING_BYTES
                && m_pendingTxnCount < LESS_THAN_MAX_DESIRED_PENDING_TXNS) {
            if (m_hadBackPressure) {
                hostLog.debug("TXN backpressure ended");
                m_hadBackPressure = false;
                for (ACGMember m : m_members) {
                    m.offBackpressure();
                }
            }
        }
    }

    /*
     * When accepting new connections this flag is used to decide whether read selection should be enabled.
     * Read selection should not be enabled if there is currently backpressure
     */
    public boolean hasBackPressure() {
        return m_hadBackPressure;
    }

    /*
     * Invoked when queueing response bytes back to a connection. Can be invoked with positive/negative
     * values to indicate whether data is being flushed or added. The same resource pool counter is used
     * to track pending responses as is used to track outstanding requests although only the bytes are being
     * counted.
     *
     * Can signal start/stop backpressure when appropriate. The return value is not used and will be removed
     * in the future.
     */
    @Override
    public boolean queue(int bytes) {
        if (bytes > 0) {
            m_pendingTxnBytes += bytes;
            checkAndLogInvariants();
            if (m_pendingTxnBytes > MAX_DESIRED_PENDING_BYTES) {
                if (!m_hadBackPressure) {
                    hostLog.debug("TXN back pressure began");
                    m_hadBackPressure = true;
                    for (ACGMember m : m_members) {
                        m.onBackpressure();
                    }
                }
            }
        } else {
            m_pendingTxnBytes += bytes;
            checkAndLogInvariants();
            if (m_pendingTxnBytes < LESS_THAN_MAX_DESIRED_PENDING_BYTES
                    && m_pendingTxnCount < LESS_THAN_MAX_DESIRED_PENDING_TXNS) {
                if (m_hadBackPressure) {
                    hostLog.debug("TXN backpressure ended");
                    m_hadBackPressure = false;
                    for (ACGMember m : m_members) {
                        m.offBackpressure();
                    }
                }
            }
        }

        return false;
    }

    public void logTransactionCompleted(long connectionId, String connectionHostname, String procedureName,
            int delta, byte status) {
        //Allocate enough space to store the proc name + 8 characters of connection id
        final StringBuilder key = new StringBuilder(procedureName.length() + 9);
        key.append(procedureName).append('$').append(connectionId);
        //StringBuilder.toString(), why you gotta make a copy?
        final String keyString = key.toString();
        InvocationInfo info = m_connectionStats.get(keyString);
        if (info == null) {
            info = new InvocationInfo(connectionHostname);
            ImmutableMap.Builder<String, InitiatorStats.InvocationInfo> builder = ImmutableMap.builder();
            builder.putAll(m_connectionStats);
            builder.put(keyString, info);
            m_connectionStats = builder.build();
        }
        info.processInvocation(delta, status);
    }

    public Iterator<Map.Entry<String, InvocationInfo>> getInitiationStatsIterator() {
        return m_connectionStats.entrySet().iterator();
    }
}