Proposal.java

  1. /*
  2.  * Copyright (C) 2016, Google Inc.
  3.  * and other copyright owners as documented in the project's IP log.
  4.  *
  5.  * This program and the accompanying materials are made available
  6.  * under the terms of the Eclipse Distribution License v1.0 which
  7.  * accompanies this distribution, is reproduced below, and is
  8.  * available at http://www.eclipse.org/org/documents/edl-v10.php
  9.  *
  10.  * All rights reserved.
  11.  *
  12.  * Redistribution and use in source and binary forms, with or
  13.  * without modification, are permitted provided that the following
  14.  * conditions are met:
  15.  *
  16.  * - Redistributions of source code must retain the above copyright
  17.  *   notice, this list of conditions and the following disclaimer.
  18.  *
  19.  * - Redistributions in binary form must reproduce the above
  20.  *   copyright notice, this list of conditions and the following
  21.  *   disclaimer in the documentation and/or other materials provided
  22.  *   with the distribution.
  23.  *
  24.  * - Neither the name of the Eclipse Foundation, Inc. nor the
  25.  *   names of its contributors may be used to endorse or promote
  26.  *   products derived from this software without specific prior
  27.  *   written permission.
  28.  *
  29.  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
  30.  * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
  31.  * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
  32.  * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  33.  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  34.  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  35.  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
  36.  * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  37.  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  38.  * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
  39.  * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  40.  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
  41.  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  42.  */

  43. package org.eclipse.jgit.internal.ketch;

  44. import static org.eclipse.jgit.internal.ketch.Proposal.State.ABORTED;
  45. import static org.eclipse.jgit.internal.ketch.Proposal.State.EXECUTED;
  46. import static org.eclipse.jgit.internal.ketch.Proposal.State.NEW;
  47. import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
  48. import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;

  49. import java.io.IOException;
  50. import java.util.ArrayList;
  51. import java.util.Collection;
  52. import java.util.Collections;
  53. import java.util.List;
  54. import java.util.concurrent.CopyOnWriteArrayList;
  55. import java.util.concurrent.TimeUnit;
  56. import java.util.concurrent.atomic.AtomicReference;

  57. import org.eclipse.jgit.annotations.Nullable;
  58. import org.eclipse.jgit.errors.MissingObjectException;
  59. import org.eclipse.jgit.internal.storage.reftree.Command;
  60. import org.eclipse.jgit.lib.ObjectId;
  61. import org.eclipse.jgit.lib.PersonIdent;
  62. import org.eclipse.jgit.lib.Ref;
  63. import org.eclipse.jgit.revwalk.RevWalk;
  64. import org.eclipse.jgit.transport.PushCertificate;
  65. import org.eclipse.jgit.transport.ReceiveCommand;
  66. import org.eclipse.jgit.util.time.ProposedTimestamp;

  67. /**
  68.  * A proposal to be applied in a Ketch system.
  69.  * <p>
  70.  * Pushing to a Ketch leader results in the leader making a proposal. The
  71.  * proposal includes the list of reference updates. The leader attempts to send
  72.  * the proposal to a quorum of replicas by pushing the proposal to a "staging"
  73.  * area under the {@code refs/txn/stage/} namespace. If the proposal succeeds
  74.  * then the changes are durable and the leader can commit the proposal.
  75.  * <p>
  76.  * Proposals are executed by
  77.  * {@link org.eclipse.jgit.internal.ketch.KetchLeader#queueProposal(Proposal)},
  78.  * which runs them asynchronously in the background. Proposals are thread-safe
  79.  * futures allowing callers to {@link #await()} for results or be notified by
  80.  * callback using {@link #addListener(Runnable)}.
  81.  */
  82. public class Proposal {
  83.     /** Current state of the proposal. */
  84.     public enum State {
  85.         /** Proposal has not yet been given to a {@link KetchLeader}. */
  86.         NEW(false),

  87.         /**
  88.          * Proposal was validated and has entered the queue, but a round
  89.          * containing this proposal has not started yet.
  90.          */
  91.         QUEUED(false),

  92.         /** Round containing the proposal has begun and is in progress. */
  93.         RUNNING(false),

  94.         /**
  95.          * Proposal was executed through a round. Individual results from
  96.          * {@link Proposal#getCommands()}, {@link Command#getResult()} explain
  97.          * the success or failure outcome.
  98.          */
  99.         EXECUTED(true),

  100.         /** Proposal was aborted and did not reach consensus. */
  101.         ABORTED(true);

  102.         private final boolean done;

  103.         private State(boolean done) {
  104.             this.done = done;
  105.         }

  106.         /** @return true if this is a terminal state. */
  107.         public boolean isDone() {
  108.             return done;
  109.         }
  110.     }

  111.     private final List<Command> commands;
  112.     private PersonIdent author;
  113.     private String message;
  114.     private PushCertificate pushCert;

  115.     private List<ProposedTimestamp> timestamps;
  116.     private final List<Runnable> listeners = new CopyOnWriteArrayList<>();
  117.     private final AtomicReference<State> state = new AtomicReference<>(NEW);

  118.     /**
  119.      * Create a proposal from a list of Ketch commands.
  120.      *
  121.      * @param cmds
  122.      *            prepared list of commands.
  123.      */
  124.     public Proposal(List<Command> cmds) {
  125.         commands = Collections.unmodifiableList(new ArrayList<>(cmds));
  126.     }

  127.     /**
  128.      * Create a proposal from a collection of received commands.
  129.      *
  130.      * @param rw
  131.      *            walker to assist in preparing commands.
  132.      * @param cmds
  133.      *            list of pending commands.
  134.      * @throws org.eclipse.jgit.errors.MissingObjectException
  135.      *             newId of a command is not found locally.
  136.      * @throws java.io.IOException
  137.      *             local objects cannot be accessed.
  138.      */
  139.     public Proposal(RevWalk rw, Collection<ReceiveCommand> cmds)
  140.             throws MissingObjectException, IOException {
  141.         commands = asCommandList(rw, cmds);
  142.     }

  143.     private static List<Command> asCommandList(RevWalk rw,
  144.             Collection<ReceiveCommand> cmds)
  145.                     throws MissingObjectException, IOException {
  146.         List<Command> commands = new ArrayList<>(cmds.size());
  147.         for (ReceiveCommand cmd : cmds) {
  148.             commands.add(new Command(rw, cmd));
  149.         }
  150.         return Collections.unmodifiableList(commands);
  151.     }

  152.     /**
  153.      * Get commands from this proposal.
  154.      *
  155.      * @return commands from this proposal.
  156.      */
  157.     public Collection<Command> getCommands() {
  158.         return commands;
  159.     }

  160.     /**
  161.      * Get optional author of the proposal.
  162.      *
  163.      * @return optional author of the proposal.
  164.      */
  165.     @Nullable
  166.     public PersonIdent getAuthor() {
  167.         return author;
  168.     }

  169.     /**
  170.      * Set the author for the proposal.
  171.      *
  172.      * @param who
  173.      *            optional identity of the author of the proposal.
  174.      * @return {@code this}
  175.      */
  176.     public Proposal setAuthor(@Nullable PersonIdent who) {
  177.         author = who;
  178.         return this;
  179.     }

  180.     /**
  181.      * Get optional message for the commit log of the RefTree.
  182.      *
  183.      * @return optional message for the commit log of the RefTree.
  184.      */
  185.     @Nullable
  186.     public String getMessage() {
  187.         return message;
  188.     }

  189.     /**
  190.      * Set the message to appear in the commit log of the RefTree.
  191.      *
  192.      * @param msg
  193.      *            message text for the commit.
  194.      * @return {@code this}
  195.      */
  196.     public Proposal setMessage(@Nullable String msg) {
  197.         message = msg != null && !msg.isEmpty() ? msg : null;
  198.         return this;
  199.     }

  200.     /**
  201.      * Get optional certificate signing the references.
  202.      *
  203.      * @return optional certificate signing the references.
  204.      */
  205.     @Nullable
  206.     public PushCertificate getPushCertificate() {
  207.         return pushCert;
  208.     }

  209.     /**
  210.      * Set the push certificate signing the references.
  211.      *
  212.      * @param cert
  213.      *            certificate, may be null.
  214.      * @return {@code this}
  215.      */
  216.     public Proposal setPushCertificate(@Nullable PushCertificate cert) {
  217.         pushCert = cert;
  218.         return this;
  219.     }

  220.     /**
  221.      * Get timestamps that Ketch must block for.
  222.      *
  223.      * @return timestamps that Ketch must block for. These may have been used as
  224.      *         commit times inside the objects involved in the proposal.
  225.      */
  226.     public List<ProposedTimestamp> getProposedTimestamps() {
  227.         if (timestamps != null) {
  228.             return timestamps;
  229.         }
  230.         return Collections.emptyList();
  231.     }

  232.     /**
  233.      * Request the proposal to wait for the affected timestamps to resolve.
  234.      *
  235.      * @param ts
  236.      *            a {@link org.eclipse.jgit.util.time.ProposedTimestamp} object.
  237.      * @return {@code this}.
  238.      */
  239.     public Proposal addProposedTimestamp(ProposedTimestamp ts) {
  240.         if (timestamps == null) {
  241.             timestamps = new ArrayList<>(4);
  242.         }
  243.         timestamps.add(ts);
  244.         return this;
  245.     }

  246.     /**
  247.      * Add a callback to be invoked when the proposal is done.
  248.      * <p>
  249.      * A proposal is done when it has entered either
  250.      * {@link org.eclipse.jgit.internal.ketch.Proposal.State#EXECUTED} or
  251.      * {@link org.eclipse.jgit.internal.ketch.Proposal.State#ABORTED} state. If
  252.      * the proposal is already done {@code callback.run()} is immediately
  253.      * invoked on the caller's thread.
  254.      *
  255.      * @param callback
  256.      *            method to run after the proposal is done. The callback may be
  257.      *            run on a Ketch system thread and should be completed quickly.
  258.      */
  259.     public void addListener(Runnable callback) {
  260.         boolean runNow = false;
  261.         synchronized (state) {
  262.             if (state.get().isDone()) {
  263.                 runNow = true;
  264.             } else {
  265.                 listeners.add(callback);
  266.             }
  267.         }
  268.         if (runNow) {
  269.             callback.run();
  270.         }
  271.     }

  272.     /** Set command result as OK. */
  273.     void success() {
  274.         for (Command c : commands) {
  275.             if (c.getResult() == NOT_ATTEMPTED) {
  276.                 c.setResult(OK);
  277.             }
  278.         }
  279.         notifyState(EXECUTED);
  280.     }

  281.     /** Mark commands as "transaction aborted". */
  282.     void abort() {
  283.         Command.abort(commands, null);
  284.         notifyState(ABORTED);
  285.     }

  286.     /**
  287.      * Read the current state of the proposal.
  288.      *
  289.      * @return read the current state of the proposal.
  290.      */
  291.     public State getState() {
  292.         return state.get();
  293.     }

  294.     /**
  295.      * Whether the proposal was attempted
  296.      *
  297.      * @return {@code true} if the proposal was attempted. A true value does not
  298.      *         mean consensus was reached, only that the proposal was considered
  299.      *         and will not be making any more progress beyond its current
  300.      *         state.
  301.      */
  302.     public boolean isDone() {
  303.         return state.get().isDone();
  304.     }

  305.     /**
  306.      * Wait for the proposal to be attempted and {@link #isDone()} to be true.
  307.      *
  308.      * @throws java.lang.InterruptedException
  309.      *             caller was interrupted before proposal executed.
  310.      */
  311.     public void await() throws InterruptedException {
  312.         synchronized (state) {
  313.             while (!state.get().isDone()) {
  314.                 state.wait();
  315.             }
  316.         }
  317.     }

  318.     /**
  319.      * Wait for the proposal to be attempted and {@link #isDone()} to be true.
  320.      *
  321.      * @param wait
  322.      *            how long to wait.
  323.      * @param unit
  324.      *            unit describing the wait time.
  325.      * @return true if the proposal is done; false if the method timed out.
  326.      * @throws java.lang.InterruptedException
  327.      *             caller was interrupted before proposal executed.
  328.      */
  329.     public boolean await(long wait, TimeUnit unit) throws InterruptedException {
  330.         synchronized (state) {
  331.             if (state.get().isDone()) {
  332.                 return true;
  333.             }
  334.             state.wait(unit.toMillis(wait));
  335.             return state.get().isDone();
  336.         }
  337.     }

  338.     /**
  339.      * Wait for the proposal to exit a state.
  340.      *
  341.      * @param notIn
  342.      *            state the proposal should not be in to return.
  343.      * @param wait
  344.      *            how long to wait.
  345.      * @param unit
  346.      *            unit describing the wait time.
  347.      * @return true if the proposal exited the state; false on time out.
  348.      * @throws java.lang.InterruptedException
  349.      *             caller was interrupted before proposal executed.
  350.      */
  351.     public boolean awaitStateChange(State notIn, long wait, TimeUnit unit)
  352.             throws InterruptedException {
  353.         synchronized (state) {
  354.             if (state.get() != notIn) {
  355.                 return true;
  356.             }
  357.             state.wait(unit.toMillis(wait));
  358.             return state.get() != notIn;
  359.         }
  360.     }

  361.     void notifyState(State s) {
  362.         synchronized (state) {
  363.             state.set(s);
  364.             state.notifyAll();
  365.         }
  366.         if (s.isDone()) {
  367.             for (Runnable callback : listeners) {
  368.                 callback.run();
  369.             }
  370.             listeners.clear();
  371.         }
  372.     }

  373.     /** {@inheritDoc} */
  374.     @Override
  375.     public String toString() {
  376.         StringBuilder s = new StringBuilder();
  377.         s.append("Ketch Proposal {\n"); //$NON-NLS-1$
  378.         s.append("  ").append(state.get()).append('\n'); //$NON-NLS-1$
  379.         if (author != null) {
  380.             s.append("  author ").append(author).append('\n'); //$NON-NLS-1$
  381.         }
  382.         if (message != null) {
  383.             s.append("  message ").append(message).append('\n'); //$NON-NLS-1$
  384.         }
  385.         for (Command c : commands) {
  386.             s.append("  "); //$NON-NLS-1$
  387.             format(s, c.getOldRef(), "CREATE"); //$NON-NLS-1$
  388.             s.append(' ');
  389.             format(s, c.getNewRef(), "DELETE"); //$NON-NLS-1$
  390.             s.append(' ').append(c.getRefName());
  391.             if (c.getResult() != ReceiveCommand.Result.NOT_ATTEMPTED) {
  392.                 s.append(' ').append(c.getResult()); // $NON-NLS-1$
  393.             }
  394.             s.append('\n');
  395.         }
  396.         s.append('}');
  397.         return s.toString();
  398.     }

  399.     private static void format(StringBuilder s, @Nullable Ref r, String n) {
  400.         if (r == null) {
  401.             s.append(n);
  402.         } else if (r.isSymbolic()) {
  403.             s.append(r.getTarget().getName());
  404.         } else {
  405.             ObjectId id = r.getObjectId();
  406.             if (id != null) {
  407.                 s.append(id.abbreviate(8).name());
  408.             }
  409.         }
  410.     }
  411. }