view vm-byteman/client-swing/src/main/java/com/redhat/thermostat/vm/byteman/client/swing/internal/VmBytemanInformationController.java @ 2579:4823a5a908d7

Make byteman metrics tab work for offline VMs. Reviewed-by: almac Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-February/022119.html
author Severin Gehwolf <sgehwolf@redhat.com>
date Mon, 30 Jan 2017 18:53:09 +0100
parents e7fd8adb5f9c
children
line wrap: on
line source

/*
 * Copyright 2012-2017 Red Hat, Inc.
 *
 * This file is part of Thermostat.
 *
 * Thermostat is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published
 * by the Free Software Foundation; either version 2, or (at your
 * option) any later version.
 *
 * Thermostat 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 for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Thermostat; see the file COPYING.  If not see
 * <http://www.gnu.org/licenses/>.
 *
 * Linking this code with other modules is making a combined work
 * based on this code.  Thus, the terms and conditions of the GNU
 * General Public License cover the whole combination.
 *
 * As a special exception, the copyright holders of this code give
 * you permission to link this code with independent modules to
 * produce an executable, regardless of the license terms of these
 * independent modules, and to copy and distribute the resulting
 * executable under terms of your choice, provided that you also
 * meet, for each linked independent module, the terms and conditions
 * of the license of that module.  An independent module is a module
 * which is not derived from or based on this code.  If you modify
 * this code, you may extend this exception to your version of the
 * library, but you are not obligated to do so.  If you do not wish
 * to do so, delete this exception statement from your version.
 */

package com.redhat.thermostat.vm.byteman.client.swing.internal;

import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.redhat.thermostat.client.command.RequestQueue;
import com.redhat.thermostat.client.core.controllers.InformationServiceController;
import com.redhat.thermostat.client.core.views.BasicView.Action;
import com.redhat.thermostat.client.core.views.UIComponent;
import com.redhat.thermostat.common.ActionEvent;
import com.redhat.thermostat.common.ActionListener;
import com.redhat.thermostat.common.command.Request;
import com.redhat.thermostat.common.model.Range;
import com.redhat.thermostat.common.utils.LoggingUtils;
import com.redhat.thermostat.common.utils.StreamUtils;
import com.redhat.thermostat.shared.locale.LocalizedString;
import com.redhat.thermostat.shared.locale.Translate;
import com.redhat.thermostat.storage.core.AgentId;
import com.redhat.thermostat.storage.core.VmId;
import com.redhat.thermostat.storage.core.VmRef;
import com.redhat.thermostat.storage.dao.AgentInfoDAO;
import com.redhat.thermostat.storage.dao.VmInfoDAO;
import com.redhat.thermostat.storage.model.AgentInformation;
import com.redhat.thermostat.storage.model.VmInfo;
import com.redhat.thermostat.storage.model.VmInfo.AliveStatus;
import com.redhat.thermostat.vm.byteman.client.swing.internal.VmBytemanView.BytemanInjectState;
import com.redhat.thermostat.vm.byteman.client.swing.internal.VmBytemanView.GenerateAction;
import com.redhat.thermostat.vm.byteman.client.swing.internal.VmBytemanView.InjectAction;
import com.redhat.thermostat.vm.byteman.client.swing.internal.VmBytemanView.TabbedPaneAction;
import com.redhat.thermostat.vm.byteman.client.swing.internal.VmBytemanView.TabbedPaneContentAction;
import com.redhat.thermostat.vm.byteman.common.BytemanMetric;
import com.redhat.thermostat.vm.byteman.common.VmBytemanDAO;
import com.redhat.thermostat.vm.byteman.common.VmBytemanStatus;
import com.redhat.thermostat.vm.byteman.common.command.BytemanRequest;
import com.redhat.thermostat.vm.byteman.common.command.BytemanRequest.RequestAction;
import com.redhat.thermostat.vm.byteman.common.command.BytemanRequestResponseListener;

public class VmBytemanInformationController implements InformationServiceController<VmRef> {
    
    private static final String BYTEMAN_RULE_TEMPLATE = "/byteman_rule_template.btm";
    private static final Logger logger = LoggingUtils.getLogger(VmBytemanInformationController.class);
    private static final Translate<LocaleResources> t = LocaleResources.createLocalizer();
    private static final Charset UTF_8_CHARSET = Charset.forName("UTF-8");
    private static final String EMPTY_STR = "";
    private static final long ONE_SECOND = 1000L;
    static final String NO_RULES_LOADED = t.localize(LocaleResources.NO_RULES_LOADED).getContents();
    
    private final VmRef vm;
    private final AgentInfoDAO agentInfoDao;
    private final VmInfoDAO vmInfoDao;
    private final VmBytemanView view;
    private final VmBytemanDAO bytemanDao;
    private final RequestQueue requestQueue;
    private Timer timer;
    private List<BytemanMetric> previousPayload = Collections.emptyList();
    private boolean comboBoxSelected = false;
    private boolean isPolling = false;

    VmBytemanInformationController(final VmBytemanView view, VmRef vm,
                                   AgentInfoDAO agentInfoDao, VmInfoDAO vmInfoDao,
                                   VmBytemanDAO bytemanDao, RequestQueue requestQueue) {
        this.view = view;
        this.vm = vm;
        this.agentInfoDao = agentInfoDao;
        this.vmInfoDao = vmInfoDao;
        this.bytemanDao = bytemanDao;
        this.requestQueue = requestQueue;

        view.addActionListener(new ActionListener<Action>() {
            
            @Override
            public void actionPerformed(ActionEvent<Action> actionEvent) {
                Action id = actionEvent.getActionId();
                switch(id) {
                case HIDDEN:
                    // nothing
                    break;
                case VISIBLE:
                    updateRuleAndDetermineState();
                    break;
                default:
                    throw new AssertionError("Invalid action event: " + id);
                }
            }
        });
        view.addRuleChangeListener(new ActionListener<InjectAction>() {
            
            @Override
            public void actionPerformed(ActionEvent<InjectAction> actionEvent) {
                InjectAction id = actionEvent.getActionId();
                switch(id) {
                case INJECT_RULE:
                    loadRule();
                    break;
                case UNLOAD_RULE:
                    unloadRule();
                    break;
                default:
                    throw new AssertionError("Invalid action event: " + id);
                }
            }

        });
        view.addTabbedPaneChangeListener(new ActionListener<TabbedPaneAction>() {

            @Override
            public void actionPerformed(ActionEvent<TabbedPaneAction> actionEvent) {
                TabbedPaneAction id = actionEvent.getActionId();
                switch(id) {
                case METRICS_TAB_SELECTED:
                    updateMetrics();
                    break;
                case RULES_TAB_SELECTED:
                    updateRule(getVmBytemanStatus());
                    break;
                case GRAPH_TAB_SELECTED:
                    updateGraph();
                    break;
                default:
                    throw new AssertionError("Invalid action event: " + id);
                }
            }
        });
        view.addGenerateActionListener(new ActionListener<VmBytemanView.GenerateAction>() {

            @Override
            public void actionPerformed(ActionEvent<GenerateAction> actionEvent) {
                GenerateAction id = actionEvent.getActionId();
                switch(id) {
                case GENERATE_TEMPLATE:
                    generateTemplate();
                    break;
                case GENERATE_TABLE:
                    comboBoxSelected = true;
                    updateMetrics();
                    break;
                case GENERATE_GRAPH:
                    updateGraph();
                    break;
                default:
                    throw new AssertionError("Unknown action event: " + id);
                }
            }
        });
        view.setViewControlsEnabled(isAlive());
    }
    
    // main constructor
    VmBytemanInformationController(VmRef ref, AgentInfoDAO agentInfoDao,
                                   VmInfoDAO vmInfo, VmBytemanDAO vmBytemanDao,
                                   RequestQueue requestQueue) {
        this(new SwingVmBytemanView(), ref, agentInfoDao, vmInfo, vmBytemanDao, requestQueue);
    }

    static String generateTemplateForVM(String mainClass) {
        try(InputStream stream = VmBytemanInformationController.class.getResourceAsStream(BYTEMAN_RULE_TEMPLATE)) {
            byte[] allBytes = StreamUtils.readAll(stream);
            String format = new String(allBytes, UTF_8_CHARSET);
            return String.format(format, mainClass, mainClass, mainClass);
        } catch (IOException e) {
            logger.log(Level.WARNING, "Failed to read byteman rule template file", e);
            return null;
        }
    }
    
    private void generateTemplate() {
        VmInfo info = vmInfoDao.getVmInfo(new VmId(vm.getVmId()));
        String ruleTemplate = generateTemplateForVM(info.getMainClass());
        ActionEvent<TabbedPaneContentAction> event = new ActionEvent<>(this, TabbedPaneContentAction.RULES_CHANGED);
        event.setPayload(ruleTemplate);
        view.contentChanged(event);
    }

    private void updateRuleAndDetermineState() {
        VmBytemanStatus status = getVmBytemanStatus();
        setInitialInstrumentationState(status);
        updateRule(status);
    }

    private VmBytemanStatus getVmBytemanStatus() {
        VmId vmId = new VmId(vm.getVmId());
        return bytemanDao.findBytemanStatus(vmId);
    }

    private void setInitialInstrumentationState(VmBytemanStatus status) {
        if (status != null) {
            String rule = status.getRule();
            if (rule != null && !rule.isEmpty()) {
                view.setInjectState(BytemanInjectState.INJECTED);
            }
        }
        // Everything else means there is no rule injected
    }

    // Package-private for testing
    void updateRule(VmBytemanStatus status) {
        String rule;
        if (status == null || status.getRule() == null || status.getRule().isEmpty()) {
            rule = NO_RULES_LOADED;
        } else {
            rule = status.getRule();
        }
        ActionEvent<TabbedPaneContentAction> event = new ActionEvent<>(this, TabbedPaneContentAction.RULES_CHANGED);
        event.setPayload(rule);
        view.contentChanged(event);
    }

    // Package-private for testing
    synchronized void updateMetrics() {
        VmId vmId = new VmId(vm.getVmId());
        AgentId agentId = new AgentId(vm.getHostRef().getAgentId());
        long now = System.currentTimeMillis();
        long duration = view.getDurationMillisecs();
        long from = now - duration;
        Range<Long> timeRange = new Range<Long>(from, now);
        List<BytemanMetric> metrics = bytemanDao.findBytemanMetrics(timeRange, vmId, agentId);
        ActionEvent<TabbedPaneContentAction> event = new ActionEvent<>(this, TabbedPaneContentAction.METRICS_CHANGED);
        event.setPayload(metrics);
        if ((metrics.isEmpty() && view.getInjectState() == BytemanInjectState.UNLOADED) || !isAlive()) {
            // stop polling if new payload is empty and inject state is not injected, or if VM is dead
            stopPolling();
            view.contentChanged(event);
        } else if (previousPayload.isEmpty() || isNewPayload(metrics, previousPayload)) {
            // start or continue polling if it's the first or new distinct payload
            if (!isPolling) {
                startPolling();
            }
            view.contentChanged(event);
        } else if (comboBoxSelected) {
            // selecting a combo box option requires a payload to redraw the table
            comboBoxSelected = false;
            view.contentChanged(event);
        }
        previousPayload = metrics;
    }

    private boolean isNewPayload(List<BytemanMetric> newPayload, List<BytemanMetric> prevPayload) {
        boolean result = false;
        if (newPayload.size() == 0 && prevPayload.size() == 0) {
            return false;
        } else if (newPayload.size() != prevPayload.size()) {
            return true;
        } else {
            for (int i = 0; i < newPayload.size(); i++) {
                if (!Objects.equals(newPayload.get(i).getDataAsJson(), prevPayload.get(i).getDataAsJson())) {
                    result = true;
                    break;
                }
            }
        }
        return result;
    }

    void startPolling() {
        isPolling = true;
        timer = new Timer();
        timer.schedule(new TimerTask() {
            public void run() {
                updateMetrics();
            }
        }, 0, ONE_SECOND);
    }

    void stopPolling() {
        isPolling = false;
        if (timer != null) { // Dead VMs might never have had the timer started
            timer.cancel();
        }
    }

    void updateGraph() {
        // pass latest byteman metrics to grapher
        VmId vmId = new VmId(vm.getVmId());
        AgentId agentId = new AgentId(vm.getHostRef().getAgentId());
        long now = System.currentTimeMillis();
        long duration = view.getDurationMillisecs();
        long from = now - duration;
        long to = now;
        Range<Long> timeRange = new Range<Long>(from, to);
        List<BytemanMetric> metrics = bytemanDao.findBytemanMetrics(timeRange, vmId, agentId);
        ActionEvent<TabbedPaneContentAction> event = new ActionEvent<>(this, TabbedPaneContentAction.GRAPH_CHANGED);
        event.setPayload(metrics);
        view.contentChanged(event);
    }

    // Package-private for testing
    void loadRule() {
        // 1. Validate rule input (basic validation, i.e. rule != EMPTY)
        String rule = view.getRuleContent();
        if (isEmpty(rule)) {
            // unselect the toggle button. This is needed for repeated error
            // handling.
            view.setInjectState(BytemanInjectState.UNLOADED);
            view.handleError(t.localize(LocaleResources.RULE_EMPTY));
            return;
        }
        view.setInjectState(BytemanInjectState.INJECTING);
        view.setViewControlsEnabled(false);
        // 2. Load rule into agent, handle errors.
        BytemanRequestResponseListener listener = doLoadRule(rule);
        view.setViewControlsEnabled(true);
        boolean success = !listener.isError();
        if (success) {
            view.setInjectState(BytemanInjectState.INJECTED);
        } else {
            view.setInjectState(BytemanInjectState.UNLOADED);
            // Error message is already localized.
            view.handleError(new LocalizedString(listener.getErrorMessage()));
        }
    }
    
    // Package-private for testing
    void unloadRule() {
        view.setInjectState(BytemanInjectState.UNLOADING);
        view.setViewControlsEnabled(false);
        BytemanRequestResponseListener listener = doUnloadRule();
        boolean success = !listener.isError();
        view.setViewControlsEnabled(true);
        if (success) {
            // Update the status in gui
            updateRule(getVmBytemanStatus());
            view.setInjectState(BytemanInjectState.UNLOADED);
        } else {
            view.setInjectState(BytemanInjectState.INJECTED);
            // error message is already localized
            view.handleError(new LocalizedString(listener.getErrorMessage()));
        }
    }

    private BytemanRequestResponseListener doLoadRule(String rule) {
        Request request = createRequest(rule, RequestAction.LOAD_RULES);
        return submitRequest(request);
    }

    private Request createRequest(String rule, RequestAction action) {
        VmId vmId = new VmId(vm.getVmId());
        VmInfo vmInfo = createVmInfo(vm);
        VmBytemanStatus status = bytemanDao.findBytemanStatus(vmId);
        int listenPort = BytemanRequest.NOT_ATTACHED_PORT;
        if (status != null) {
            listenPort = status.getListenPort();
        }
        AgentInformation agentInfo = agentInfoDao.getAgentInformation(new AgentId(vm.getHostRef().getAgentId()));
        InetSocketAddress address = agentInfo.getRequestQueueAddress();
        Request request = null;
        if (action == RequestAction.LOAD_RULES) {
            request = BytemanRequest.create(address, vmInfo, RequestAction.LOAD_RULES, listenPort, rule);
        } else if (action == RequestAction.UNLOAD_RULES) {
            request = BytemanRequest.create(address, vmInfo, action, listenPort);
        } else {
            throw new AssertionError("Unknown action: " + action);
        }
        return request;
    }
    
    private VmInfo createVmInfo(VmRef vmRef) {
        // set up vmInfo with just enough data
        VmInfo vmInfo = new VmInfo();
        vmInfo.setAgentId(vm.getHostRef().getAgentId());
        vmInfo.setVmId(vm.getVmId());
        vmInfo.setVmPid(vm.getPid());
        return vmInfo;
    }

    private BytemanRequestResponseListener submitRequest(Request request) {
        CountDownLatch latch = new CountDownLatch(1);
        BytemanRequestResponseListener listener = new BytemanRequestResponseListener(latch);
        request.addListener(listener);
        requestQueue.putRequest(request);
        waitWithTimeOut(latch);
        return listener;
    }

    void waitWithTimeOut(CountDownLatch latch) {
        try {
            // wait for request to finish
            latch.await(5, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            // ignore
        }
    }

    private boolean isEmpty(String rule) {
        if (EMPTY_STR.equals(rule.trim())) {
            return true;
        }
        return NO_RULES_LOADED.equals(rule);
    }

    private BytemanRequestResponseListener doUnloadRule() {
        Request request = createRequest(null, RequestAction.UNLOAD_RULES);
        return submitRequest(request);
    }

    private boolean isAlive() {
        AgentId agent = new AgentId(vm.getHostRef().getAgentId());
        AgentInformation agentInfo = agentInfoDao.getAgentInformation(agent);
        if (!agentInfo.isAlive()) {
            return false;
        }
        return vmInfoDao.getVmInfo(vm).isAlive(agentInfo) == AliveStatus.RUNNING;
    }

    @Override
    public UIComponent getView() {
        return view;
    }

    @Override
    public LocalizedString getLocalizedName() {
        return t.localize(LocaleResources.VM_BYTEMAN_TAB_NAME);
    }

}