com.surgeplay.visage.master.VisageMaster.java Source code

Java tutorial

Introduction

Here is the source code for com.surgeplay.visage.master.VisageMaster.java

Source

/*
 * Visage
 * Copyright (c) 2015-2016, Aesen Vismea <aesen@unascribed.com>
 *
 * The MIT License
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.surgeplay.visage.master;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.ObjectInputStream;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.zip.DeflaterOutputStream;

import org.eclipse.jetty.server.AsyncNCSARequestLog;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.ResourceHandler;
import org.eclipse.jetty.server.handler.gzip.GzipHandler;
import org.eclipse.jetty.util.log.Log;
import org.spacehq.mc.auth.GameProfile;
import org.spacehq.mc.auth.util.URLUtils;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

import com.google.common.collect.Maps;
import com.google.common.io.ByteStreams;
import com.google.common.io.Closer;
import com.rabbitmq.client.AMQP.BasicProperties;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.client.QueueingConsumer.Delivery;
import com.surgeplay.visage.RenderMode;
import com.surgeplay.visage.Visage;
import com.surgeplay.visage.VisageRunner;
import com.surgeplay.visage.master.exception.NoSlavesAvailableException;
import com.surgeplay.visage.master.exception.RenderFailedException;
import com.surgeplay.visage.master.glue.HeaderHandler;
import com.surgeplay.visage.master.glue.LogShim;
import com.surgeplay.visage.slave.VisageSlave;
import com.surgeplay.visage.util.Profiles;
import com.typesafe.config.Config;

public class VisageMaster extends Thread implements VisageRunner {
    public VisageSlave fallback;
    public Config config;
    public Connection conn;
    public Channel channel;
    public byte[] steve, alex;
    private JedisPool pool;
    private int resolverNum, skinNum;
    private String password;
    private boolean run = true;

    public VisageMaster(Config config) {
        super("Master thread");
        this.config = config;
    }

    public Jedis getResolverJedis() {
        Jedis j = getJedis();
        j.select(resolverNum);
        return j;
    }

    public Jedis getSkinJedis() {
        Jedis j = getJedis();
        j.select(skinNum);
        return j;
    }

    public Jedis getJedis() {
        Jedis j = pool.getResource();
        if (password != null) {
            j.auth(password);
        }
        return j;
    }

    @Override
    public void run() {
        try {
            Log.setLog(new LogShim(Visage.log));
            long total = Runtime.getRuntime().totalMemory();
            long max = Runtime.getRuntime().maxMemory();
            if (Visage.debug)
                Visage.log.finer("Current heap size: " + humanReadableByteCount(total, false));
            if (Visage.debug)
                Visage.log.finer("Max heap size: " + humanReadableByteCount(max, false));
            if (total < max) {
                Visage.log.warning(
                        "You have set your minimum heap size (Xms) lower than the maximum heap size (Xmx) - this can cause GC thrashing. It is strongly recommended to set them both to the same value.");
            }
            if (max < (1000 * 1000 * 1000)) {
                Visage.log.warning(
                        "The heap size (Xmx) is less than one gigabyte; it is recommended to run Visage with a gigabyte or more. Use -Xms1G and -Xmx1G to do this.");
            }
            Visage.log.info("Setting up Jetty");
            Server server = new Server(
                    new InetSocketAddress(config.getString("http.bind"), config.getInt("http.port")));

            List<String> expose = config.getStringList("expose");
            String poweredBy;
            if (expose.contains("server")) {
                if (expose.contains("version")) {
                    poweredBy = "Visage v" + Visage.VERSION;
                } else {
                    poweredBy = "Visage";
                }
            } else {
                poweredBy = null;
            }

            ResourceHandler resource = new ResourceHandler();
            resource.setResourceBase(config.getString("http.static"));
            resource.setDirectoriesListed(false);
            resource.setWelcomeFiles(new String[] { "index.html" });
            resource.setHandler(new VisageHandler(this));

            if (!"/dev/null".equals(config.getString("log"))) {
                new File(config.getString("log")).getParentFile().mkdirs();
                server.setRequestLog(new AsyncNCSARequestLog(config.getString("log")));
            }
            GzipHandler gzip = new GzipHandler();
            gzip.setHandler(new HeaderHandler("X-Powered-By", poweredBy, resource));
            server.setHandler(gzip);

            String redisHost = config.getString("redis.host");
            int redisPort = config.getInt("redis.port");
            Visage.log.info("Connecting to Redis at " + redisHost + ":" + redisPort);
            resolverNum = config.getInt("redis.resolver-db");
            skinNum = config.getInt("redis.skin-db");
            JedisPoolConfig jpc = new JedisPoolConfig();
            jpc.setMaxIdle(config.getInt("redis.max-idle-connections"));
            jpc.setMaxTotal(config.getInt("redis.max-total-connections"));
            jpc.setMinIdle(config.getInt("redis.min-idle-connections"));
            if (config.hasPath("redis.password")) {
                password = config.getString("redis.password");
            }
            pool = new JedisPool(jpc, redisHost, redisPort);

            Visage.log.info("Connecting to RabbitMQ at " + config.getString("rabbitmq.host") + ":"
                    + config.getInt("rabbitmq.port"));
            ConnectionFactory factory = new ConnectionFactory();
            factory.setHost(config.getString("rabbitmq.host"));
            factory.setPort(config.getInt("rabbitmq.port"));
            factory.setRequestedHeartbeat(10);
            if (config.hasPath("rabbitmq.user")) {
                factory.setUsername(config.getString("rabbitmq.user"));
                factory.setPassword(config.getString("rabbitmq.password"));
            }
            String queue = config.getString("rabbitmq.queue");

            Closer closer = Closer.create();
            steve = ByteStreams.toByteArray(closer.register(ClassLoader.getSystemResourceAsStream("steve.png")));
            alex = ByteStreams.toByteArray(closer.register(ClassLoader.getSystemResourceAsStream("alex.png")));
            closer.close();

            conn = factory.newConnection();
            channel = conn.createChannel();
            if (Visage.debug)
                Visage.log.finer("Setting up queue '" + queue + "'");
            channel.queueDeclare(queue, false, false, true, null);
            channel.basicQos(1);

            if (Visage.debug)
                Visage.log.finer("Setting up reply queue");
            replyQueue = channel.queueDeclare().getQueue();
            consumer = new QueueingConsumer(channel);
            channel.basicConsume(replyQueue, consumer);

            if (config.getBoolean("slave.enable")) {
                Visage.log.info("Starting fallback slave");
                fallback = new VisageSlave(
                        config.getConfig("slave").withValue("rabbitmq", config.getValue("rabbitmq")));
                fallback.start();
            }
            Visage.log.info("Starting Jetty");
            server.start();
            Visage.log.info("Listening for finished jobs");
            try {
                while (run) {
                    Delivery delivery = consumer.nextDelivery();
                    if (Visage.trace)
                        Visage.log.finest("Got delivery");
                    try {
                        String corrId = delivery.getProperties().getCorrelationId();
                        if (queuedJobs.containsKey(corrId)) {
                            if (Visage.trace)
                                Visage.log.finest("Valid");
                            responses.put(corrId, delivery.getBody());
                            Runnable run = queuedJobs.get(corrId);
                            queuedJobs.remove(corrId);
                            if (Visage.trace)
                                Visage.log.finest("Removed from queue");
                            run.run();
                            if (Visage.trace)
                                Visage.log.finest("Ran runnable");
                            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
                            if (Visage.trace)
                                Visage.log.finest("Ack'd");
                        } else {
                            Visage.log.warning("Unknown correlation ID?");
                            channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false);
                        }
                    } catch (Exception e) {
                        Visage.log.log(Level.WARNING,
                                "An unexpected error occured while attempting to process a response.", e);
                    }
                }
            } catch (InterruptedException e) {
            } catch (Exception e) {
                Visage.log.log(Level.SEVERE, "An unexpected error occured in the master run loop.", e);
                System.exit(2);
            }
            try {
                Visage.log.info("Shutting down master");
                server.stop();
                pool.destroy();
                conn.close(5000);
            } catch (Exception e) {
                Visage.log.log(Level.SEVERE, "A fatal error has occurred while shutting down the master.", e);
            }
        } catch (Exception e) {
            Visage.log.log(Level.SEVERE, "An unexpected error occured while initializing the master.", e);
            System.exit(1);
        }
    }

    private String replyQueue;
    private QueueingConsumer consumer;
    private Map<String, Runnable> queuedJobs = Maps.newHashMap();
    private Map<String, byte[]> responses = Maps.newHashMap();

    public RenderResponse renderRpc(RenderMode mode, int width, int height, int supersampling, GameProfile profile,
            byte[] skin, Map<String, String[]> switches) throws RenderFailedException, NoSlavesAvailableException {
        if (mode == RenderMode.SKIN)
            return null;
        try {
            byte[] response = null;
            String corrId = UUID.randomUUID().toString();
            BasicProperties props = new BasicProperties.Builder().correlationId(corrId).replyTo(replyQueue).build();
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            DeflaterOutputStream defos = new DeflaterOutputStream(baos);
            DataOutputStream dos = new DataOutputStream(defos);
            dos.writeByte(mode.ordinal());
            dos.writeShort(width);
            dos.writeShort(height);
            dos.writeByte(supersampling);
            Profiles.writeGameProfile(dos, profile);
            dos.writeShort(switches.size());
            for (Entry<String, String[]> en : switches.entrySet()) {
                dos.writeUTF(en.getKey());
                dos.writeByte(en.getValue().length);
                for (String s : en.getValue()) {
                    dos.writeUTF(s);
                }
            }
            dos.writeInt(skin.length);
            dos.write(skin);
            dos.flush();
            defos.finish();
            channel.basicPublish("", config.getString("rabbitmq.queue"), props, baos.toByteArray());
            if (Visage.debug)
                Visage.log.finer("Requested a " + width + "x" + height + " " + mode.name().toLowerCase()
                        + " render (" + supersampling + "x supersampling) for "
                        + (profile == null ? "null" : profile.getName()));
            final Object waiter = new Object();
            queuedJobs.put(corrId, new Runnable() {
                @Override
                public void run() {
                    if (Visage.debug)
                        Visage.log.finer("Got response");
                    synchronized (waiter) {
                        waiter.notify();
                    }
                }
            });
            long start = System.currentTimeMillis();
            long timeout = config.getDuration("render.timeout", TimeUnit.MILLISECONDS);
            synchronized (waiter) {
                while (queuedJobs.containsKey(corrId) && (System.currentTimeMillis() - start) < timeout) {
                    if (Visage.trace)
                        Visage.log.finest("Waiting...");
                    waiter.wait(timeout);
                }
            }
            if (queuedJobs.containsKey(corrId)) {
                if (Visage.trace)
                    Visage.log.finest("Queue still contains this request, assuming timeout");
                queuedJobs.remove(corrId);
                throw new RenderFailedException("Request timed out");
            }
            response = responses.get(corrId);
            responses.remove(corrId);
            if (response == null)
                throw new RenderFailedException("Response was null");
            ByteArrayInputStream bais = new ByteArrayInputStream(response);
            String slave = new DataInputStream(bais).readUTF();
            int type = bais.read();
            byte[] payload = ByteStreams.toByteArray(bais);
            if (type == 0) {
                if (Visage.trace)
                    Visage.log.finest("Got type 0, success");
                RenderResponse resp = new RenderResponse();
                resp.slave = slave;
                resp.png = payload;
                Visage.log.info("Receieved a " + mode.name().toLowerCase() + " render from " + resp.slave);
                return resp;
            } else if (type == 1) {
                if (Visage.trace)
                    Visage.log.finest("Got type 1, failure");
                ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(payload));
                Throwable t = (Throwable) ois.readObject();
                throw new RenderFailedException("Slave reported error", t);
            } else
                throw new RenderFailedException(
                        "Malformed response from '" + slave + "' - unknown response id " + type);
        } catch (Exception e) {
            if (e instanceof RenderFailedException)
                throw (RenderFailedException) e;
            throw new RenderFailedException("Unexpected error", e);
        }
    }

    /**
     * <a href="http://stackoverflow.com/questions/3758606/how-to-convert-byte-size-into-human-readable-format-in-java">Source</a>
     * @author aioobe
     */
    public static String humanReadableByteCount(long bytes, boolean si) {
        int unit = si ? 1000 : 1024;
        if (bytes < unit)
            return bytes + " B";
        int exp = (int) (Math.log(bytes) / Math.log(unit));
        String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp - 1) + (si ? "" : "i");
        return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre);
    }

    @Override
    public void shutdown() {
        run = false;
        interrupt();
    }

}