edu.umass.cs.gigapaxos.testing.TESTPaxosApp.java Source code

Java tutorial

Introduction

Here is the source code for edu.umass.cs.gigapaxos.testing.TESTPaxosApp.java

Source

/*
 * Copyright (c) 2015 University of Massachusetts
 * 
 * Licensed 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.
 * 
 * Initial developer(s): V. Arun
 */
package edu.umass.cs.gigapaxos.testing;

import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.json.JSONException;
import org.json.JSONObject;

import edu.umass.cs.gigapaxos.PaxosConfig.PC;
import edu.umass.cs.gigapaxos.PaxosManager;
import edu.umass.cs.gigapaxos.interfaces.ClientMessenger;
import edu.umass.cs.gigapaxos.interfaces.Replicable;
import edu.umass.cs.gigapaxos.interfaces.Request;
import edu.umass.cs.gigapaxos.paxospackets.PaxosPacket;
import edu.umass.cs.gigapaxos.paxospackets.ProposalPacket;
import edu.umass.cs.gigapaxos.paxospackets.RequestPacket;
import edu.umass.cs.gigapaxos.paxosutil.RequestInstrumenter;
import edu.umass.cs.nio.JSONNIOTransport;
import edu.umass.cs.nio.MessageNIOTransport;
import edu.umass.cs.nio.interfaces.IntegerPacketType;
import edu.umass.cs.nio.interfaces.InterfaceNIOTransport;
import edu.umass.cs.nio.interfaces.SSLMessenger;
import edu.umass.cs.reconfiguration.reconfigurationutils.RequestParseException;
import edu.umass.cs.utils.Config;
import edu.umass.cs.utils.Keyable;
import edu.umass.cs.utils.MultiArrayMap;
import edu.umass.cs.utils.Util;

/**
 * @author V. Arun
 * 
 *         The default no-op application for gigapaxos testing. It simply echoes
 *         back the request to the client. But it does a number of other
 *         instrumentations and asserts for testing.
 */
public class TESTPaxosApp implements Replicable, ClientMessenger {
    private static final int MAX_STORED_REQUESTS = 1000;
    private MessageDigest md = null;
    private InterfaceNIOTransport<Integer, JSONObject> niot = null;

    private final MultiArrayMap<String, PaxosState> allState = ABSOLUTE_NOOP ? null
            : new MultiArrayMap<String, PaxosState>(Config.getGlobalInt(PC.PINSTANCES_CAPACITY));

    private class PaxosState implements Keyable<String> {
        PaxosState(String paxosID) {
            this.paxosID = paxosID;
        }

        public String getKey() {
            return this.paxosID;
        }

        private final String paxosID;
        private int seqnum = -1;
        private String value = null;
        private int numExecuted = 0;
        private HashMap<Integer, String> committed = null;
    }

    private static Logger log = PaxosManager.getLogger();

    /**
     * @param nio
     */
    public TESTPaxosApp(InterfaceNIOTransport<Integer, JSONObject> nio) {
        this();
        setNIOTransport(nio);
        AllApps.addApp(this);
    }

    // private because nio is necessary for testing
    /**
     * 
     */
    public TESTPaxosApp() {
        // app uses nio only to send, not receive, so no PacketDemultiplexer
        try {
            md = MessageDigest.getInstance("SHA");
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
    }

    static {
        TESTPaxosConfig.load();
    }
    private static final boolean ABSOLUTE_NOOP = Config.getGlobalBoolean(TESTPaxosConfig.TC.ABSOLUTE_NOOP_APP);
    private static final long APP_DELAY = Config.getGlobalLong(TESTPaxosConfig.TC.TEST_APP_DELAY);

    private boolean wasteTime(long usDelay) {
        long nanoT = System.nanoTime();
        while (System.nanoTime() - nanoT < usDelay * 1000) {
            for (int i = 0; i < 100; i++)
                Math.random();
            try {
                if (usDelay > 10000)
                    Thread.sleep(1);
                else if (usDelay > 500)
                    Thread.sleep(0);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        log.info(this + " handled request in " + (System.nanoTime() - nanoT) / 1000 + "us");
        return true;
    }

    public String toString() {
        return this.getClass().getSimpleName() + this.getMyID();
    }

    /*
     * This is the main execution method. The app is supposed to be agnostic to
     * slot numbers, but this method takes as input a ProposalPacket as opposed
     * to a RequestPacket only for tetsing purposes.
     */
    private boolean handleDecision(ProposalPacket requestPacket, boolean doNotReplyToClient) {
        if (APP_DELAY > 0)
            return wasteTime(APP_DELAY);
        else if (ABSOLUTE_NOOP)
            return true;

        // else the older testing code
        try {
            String paxosID = requestPacket.getPaxosID();
            PaxosState state = this.allState.putIfAbsent(paxosID, state = new PaxosState(paxosID));

            /*
             * Initialize seqnum upon first decision. We know it is the first
             * decision if seqnum==-1 or if putState is true, i.e., checkpoint
             * recovery has just happened and no other request has been
             * executed.
             */
            if (state.seqnum == -1)
                state.seqnum = requestPacket.slot;
            else
                state.seqnum++;

            log.log(Level.FINE, "Node{0} executing {1}; seqnum={2}, prev_state={3}",
                    new Object[] { (this.niot != null ? getMyID() : "[?]"), requestPacket.getSummary(),
                            state.seqnum, Util.truncate(state.value, 16, 16) });

            /*
             * Set state to current request value concatenated with the hash of
             * the previous state. This allows us to easily compare using just
             * the current state value that two RSMs executed the exact same set
             * of state transitions to arrive at that state.
             */
            if (TESTPaxosConfig.shouldAssertRSMInvariant())
                state.value = requestPacket.requestValue + digest(state.value);

            /*
             * Assert that the next slot is always the next expected seqnum.
             * This ensures that paxos is making the app execute requests in the
             * correct slot number order. Note that state.seqnum is set exactly
             * once at initialization to the arriving request's slot above and
             * is then just incremented by one below for every executed
             * decision.
             */
            assert (state.seqnum == requestPacket.slot) : "state.seqnum = " + state.seqnum
                    + " , requestPacket.slot = " + requestPacket.slot;
            // maintain executed sequence if shouldAssertRSMInvariant
            if (TESTPaxosConfig.shouldAssertRSMInvariant()) {
                if (state.committed == null)
                    state.committed = new LinkedHashMap<Integer, String>() {
                        private static final long serialVersionUID = 1L;
                        private static final int MAX_ENTRIES = 1000;

                        protected boolean removeEldestEntry(Map.Entry<Integer, String> eldest) {
                            if (this.size() > MAX_ENTRIES)
                                return true;
                            return false;
                        }
                    };
                state.committed.put(state.seqnum, state.value);
            }
            state.numExecuted++;

            if (TESTPaxosConfig.shouldAssertRSMInvariant())
                assert (RSMInvariant(requestPacket.getPaxosID(), requestPacket.slot)) : requestPacket;
            state.committed.remove(state.seqnum - MAX_STORED_REQUESTS); // GC

            if (TESTPaxosConfig.PAXOS_MANAGER_UNIT_TEST)
                synchronized (this) {
                    this.notify();
                }
            assert (requestPacket.requestID >= 0) : requestPacket.toString();
            if (!doNotReplyToClient && niot != null) {
                this.sendResponseToClient(requestPacket);
            } else
                log.log(Level.FINE, "Node{0} not sending reply {1} to client",
                        new Object[] { getMyID(), requestPacket.getSummary() });
        } catch (JSONException je) {
            je.printStackTrace();
        } catch (IOException ioe) {
            ioe.printStackTrace();
        }
        return true;
    }

    private void sendResponseToClient(ProposalPacket requestPacket) throws JSONException, IOException {
        if (!TESTPaxosConfig.getSendReplyToClient())
            return;
        RequestInstrumenter.remove(requestPacket.requestID);
        /*
         * Entry replica check must be done here as requests with different
         * entry replicas can get batched when forwarded between replicas.
         */
        assert (requestPacket.getEntryReplica() != -1);
        if (requestPacket.getEntryReplica() == this.getMyID()) {
            log.log(Level.FINE, "App {0} sending response to client {1}",
                    new Object[] { getMyID(), requestPacket.getSummary() });
            niot.sendToAddress((requestPacket.getClientAddress()),
                    new ProposalPacket(0, requestPacket.getACK()).toJSONObject());
        }
        // send responses for batched requests as well
        if (requestPacket.getBatched() != null)
            for (RequestPacket batchedReq : requestPacket.getBatched())
                // single-level recursion as batches can not be deeper
                this.sendResponseToClient(new ProposalPacket(requestPacket.slot, batchedReq));
    }

    @Override
    public synchronized String checkpoint(String paxosID) {
        if (ABSOLUTE_NOOP)
            return paxosID + ":" + Long.toHexString(((long) (Math.random() * Long.MAX_VALUE)));
        PaxosState state = this.allState.get(paxosID);
        if (state != null)
            return state.value;
        return null;
    }

    @Override
    public synchronized boolean restore(String paxosID, String value) {
        if (ABSOLUTE_NOOP)
            return true;
        if (!allState.containsKey(paxosID))
            allState.putIfAbsent(paxosID, new PaxosState(paxosID));
        PaxosState state = allState.get(paxosID);
        state.value = value;
        state.seqnum = -1; // reset seqnum upon state transfer
        return true;
    }

    synchronized int digest(String s) {
        // null check needed if null checkpoints are enabled
        if (s == null)
            return 0;
        md.update(s.getBytes());
        byte[] digest = md.digest();
        int dig = 0;
        for (int i = 0; i < digest.length; i++) {
            dig += (int) digest[i];
        }
        return dig;
    }

    void shutdown() {
        this.allState.clear();
    }

    private InterfaceNIOTransport<Integer, JSONObject> setNIOTransport(
            InterfaceNIOTransport<Integer, JSONObject> nio) {
        niot = nio;
        return nio;
    }

    private int getMyID() {
        return (this.niot != null ? this.niot.getMyID() : -1);
    }

    /*
     * Testing methods below.
     */
    /**
     * @param paxosID
     * @return Number committed.
     */
    public synchronized int getNumCommitted(String paxosID) {
        PaxosState state = this.allState.get(paxosID);
        if (state != null)
            return state.seqnum;
        return 0;
    }

    /**
     * @param paxosID
     * @return Number executed that in general may be different from number
     *         committed in case of checkpoint transfers.
     */
    public synchronized int getNumExecuted(String paxosID) {
        PaxosState state = this.allState.get(paxosID);
        if (state != null)
            return state.numExecuted;
        return 0;
    }

    synchronized String getRequest(String paxosID, int reqnum) {
        PaxosState state = this.allState.get(paxosID);
        if (state != null)
            return state.committed.get(reqnum);
        return null;
    }

    /**
     * @param paxosID
     * @return Hash of current state.
     */
    public synchronized int getHash(String paxosID) {
        PaxosState state = this.allState.get(paxosID);
        if (state != null)
            return state.value.hashCode();
        return 0;
    }

    /**
     * @throws InterruptedException
     */
    public synchronized void waitToFinish() throws InterruptedException {
        this.wait();
    }

    /**
     * @param paxosID
     * @param slot
     * @throws InterruptedException
     */
    public synchronized void waitToFinish(String paxosID, int slot) throws InterruptedException {
        PaxosState state = null;
        while ((state = this.allState.get(paxosID)) == null || state.seqnum < slot)
            this.wait();
    }

    synchronized String toString(String paxosID) {
        String s = "";
        PaxosState state = this.allState.get(paxosID);
        s += "[App" + this.niot.getMyID() + ":" + paxosID + ": " + "seqnum=" + (state != null ? state.seqnum : -1)
                + "; state=" + (state != null ? state.value : "null") + ";]";
        return s;
    }

    boolean RSMInvariant(TESTPaxosApp app1, TESTPaxosApp app2, String paxosID, int seqnum) {
        // invariant makes sense only when not recovery
        if (!TESTPaxosConfig.getCleanDB())
            return true;
        String state1 = null, state2 = null;
        if (app1.allState.containsKey(paxosID))
            state1 = app1.allState.get(paxosID).committed.get(seqnum);
        if (app2.allState.containsKey(paxosID))
            state2 = app2.allState.get(paxosID).committed.get(seqnum);
        assert (state1 == null || state2 == null || state1.equals(state2)
                || hasHoles(app1, app2, paxosID, seqnum)) : (getTrace(app1, app2, paxosID, seqnum));
        return (state1 == null || state2 == null || state1.equals(state2));
    }

    // legitimate holes can be caused because of checkpoint transfers
    private boolean hasHoles(TESTPaxosApp app1, TESTPaxosApp app2, String paxosID, int seqnum) {
        for (int i = seqnum; i >= 0; i--)
            if (app1.allState.get(paxosID).committed.get(i) == null
                    && app2.allState.get(paxosID).committed.get(i) != null
                    || app2.allState.get(paxosID).committed.get(i) == null
                            && app1.allState.get(paxosID).committed.get(i) != null)
                return true;
        return false;
    }

    private String getTrace(TESTPaxosApp app1, TESTPaxosApp app2, String paxosID, int seqnum) {
        String s = "";
        for (int i = seqnum; i >= 0; i--) {
            s += app1.getMyID() + ":" + paxosID + ":" + i + ": " + app1.allState.get(paxosID).committed.get(i)
                    + " != " + app2.getMyID() + ":" + paxosID + ":" + i + ": "
                    + app2.allState.get(paxosID).committed.get(i) + "\n";
        }
        return s;
    }

    // check invariant at seqnum
    boolean RSMInvariant(String paxosID, int seqnum) {
        boolean invariant = true;
        TESTPaxosApp[] replicas = AllApps.getReplicas(paxosID).toArray(new TESTPaxosApp[0]);
        for (int i = 0; i < replicas.length; i++) {
            for (int j = i + 1; j < replicas.length; j++) {
                invariant = invariant && RSMInvariant(replicas[i], replicas[j], paxosID, seqnum);
            }
        }
        return invariant;
    }

    // check invariant at current frontier
    boolean RSMInvariant(String paxosID) {
        boolean invariant = true;
        TESTPaxosApp[] replicas = AllApps.getReplicas(paxosID).toArray(new TESTPaxosApp[0]);
        for (int i = 0; i < replicas.length; i++) {
            for (int j = i + 1; j < replicas.length; j++) {
                invariant = invariant && RSMInvariant(replicas[i], replicas[j], paxosID,
                        replicas[i].allState.get(paxosID).seqnum);
            }
        }
        return invariant;
    }

    // AllApps below is for testing
    static class AllApps {
        // private static Set<TESTPaxosReplicable> appMap = new
        // HashSet<TESTPaxosReplicable>();
        private static HashMap<Integer, TESTPaxosApp> appMap = new HashMap<Integer, TESTPaxosApp>();

        private synchronized static void addApp(TESTPaxosApp app) {
            // appMap.add(app);
            appMap.put(app.getMyID(), app);
        }

        private synchronized static Set<TESTPaxosApp> getReplicas(String paxosID) {
            Set<TESTPaxosApp> replicas = new HashSet<TESTPaxosApp>();
            for (TESTPaxosApp app : appMap.values()) {
                if (app.allState.containsKey(paxosID))
                    replicas.add(app);
            }
            return replicas;
        }

        public synchronized static String toString(String paxosID) {
            String s = "";
            for (TESTPaxosApp app : getReplicas(paxosID)) {
                s += "\n" + app.toString(paxosID);
            }
            return s;
        }
    }

    @Override
    public boolean execute(Request request) {
        return execute(request, false);
    }

    @Override
    public Request getRequest(String stringified) throws RequestParseException {
        try {
            JSONObject json = new JSONObject(stringified);
            if (PaxosPacket.getPaxosPacketType(json).equals(PaxosPacket.PaxosPacketType.REQUEST))
                return new RequestPacket(json);
            else
                return new ProposalPacket(json);
        } catch (JSONException e) {
            e.printStackTrace();
        }
        throw new RequestParseException(new RuntimeException("Can not parse request: " + stringified));
    }

    @Override
    public Set<IntegerPacketType> getRequestTypes() {
        IntegerPacketType[] types = { PaxosPacket.PaxosPacketType.PAXOS_PACKET };
        return new HashSet<IntegerPacketType>(Arrays.asList(types));
    }

    @Override
    public boolean execute(Request request, boolean doNotReplyToClient) {
        // no need to again stringify and unstringify
        if (request instanceof ProposalPacket)
            return this.handleDecision((ProposalPacket) request, doNotReplyToClient);
        else if (request instanceof RequestPacket)
            return this.sendEchoResponse((RequestPacket) request);
        else
            throw new RuntimeException("TESTPaxosReplicable received non-RequestPacket type request : " + request);
    }

    boolean sendEchoResponse(RequestPacket request) {
        try {
            // arbitrary slot number of 0
            ProposalPacket proposal = new ProposalPacket(0, request);
            this.sendResponseToClient(proposal);
        } catch (JSONException | IOException e) {
            log.severe("Node" + getMyID() + "encountered JSONException while decoding REQUEST: " + request);
            e.printStackTrace();
        }
        return true;
    }

    @SuppressWarnings("unchecked")
    @Override
    public void setClientMessenger(SSLMessenger<?, JSONObject> messenger) {
        setNIOTransport((InterfaceNIOTransport<Integer, JSONObject>) messenger);
    }
}