/*
* Syncany, www.syncany.org
* Copyright (C) 2011-2016 Philipp C. Heckel <philipp.heckel@gmail.com>
*
* This program 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 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.syncany.operations.down;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.syncany.database.DatabaseVersion;
import org.syncany.database.DatabaseVersionHeader;
import org.syncany.database.MemoryDatabase;
import org.syncany.database.VectorClock;
/**
* The database reconciliator implements various parts of the sync down algorithm (see also:
* {@link DownOperation}). Its main responsibility is to compare the local database to the
* other clients' delta databases. The final goal of the algorithms described in this class is
* to determine a winning {@link MemoryDatabase} (or better: a winning {@link DatabaseBranch}) of
* a client.
*
* <p>All algorithm parts largely rely on the comparison of a client's database branch, i.e. its
* committed set of {@link DatabaseVersion}s. Instead of comparing the entire database versions
* of the different clients, however, the comparisons solely rely on the {@link DatabaseVersionHeader}s.
* In particular, most of them only compare the {@link VectorClock}. If the vector clocks are
* in conflict (= simultaneous), the local timestamp is used as a final decision (oldest wins).
*
* <p><b>Algorithm:</b>
* <ol>
* <li>Input: Local branch, unknown remote branches</li>
* <li>Sort the databaseversions by vectorclocks, tiebreaking with timestamps.</li>
* <li>Walk through the sorted list and construct the winning branch.
* </ol>
*
* @see DownOperation
* @see VectorClock
* @author Philipp C. Heckel <philipp.heckel@gmail.com>
* @author Pim Otte <otte.pim@gmail.com>
* @author Steffen Dangmann <steffen.dangmann@googlemail.com>
*/
public class DatabaseReconciliator {
private static final Logger logger = Logger.getLogger(DatabaseReconciliator.class.getSimpleName());
/**
* Implements the core synchronization algorithm as described {@link DatabaseReconciliator in the class description}.
*
* @param localMachineName Client name of the local machine (required for branch stitching)
* @param localBranch Local branch, created from the local database
* @param unknownRemoteBranches Newly downloaded unknown remote branches (incomplete branches; will be stitched)
* @return Returns the branch of the winning client
*/
public Map.Entry<String, DatabaseBranch> findWinnerBranch(DatabaseBranches allBranches)
throws Exception {
Entry<String, DatabaseBranch> winnersNameAndBranch = findWinnersNameAndBranch(allBranches);
if (winnersNameAndBranch != null) {
String winnersName = winnersNameAndBranch.getKey();
DatabaseBranch winnersBranch = winnersNameAndBranch.getValue();
if (logger.isLoggable(Level.INFO)) {
logger.log(Level.INFO, "- Winner is " + winnersName + " with branch: ");
for (DatabaseVersionHeader databaseVersionHeader : winnersBranch.getAll()) {
logger.log(Level.INFO, " + " + databaseVersionHeader);
}
}
return winnersNameAndBranch;
}
else {
return null;
}
}
public DatabaseBranch findLosersPruneBranch(DatabaseBranch losersBranch, DatabaseBranch winnersBranch) {
DatabaseBranch losersPruneBranch = new DatabaseBranch();
boolean pruneBranchStarted = false;
for (int i = 0; i < losersBranch.size(); i++) {
if (pruneBranchStarted) {
losersPruneBranch.add(losersBranch.get(i));
}
else if (i < winnersBranch.size() && !losersBranch.get(i).equals(winnersBranch.get(i))) {
pruneBranchStarted = true;
losersPruneBranch.add(losersBranch.get(i));
}
}
return losersPruneBranch;
}
public DatabaseBranch findWinnersApplyBranch(DatabaseBranch losersBranch, DatabaseBranch winnersBranch) {
logger.log(Level.INFO, "Finding winnersApplyBranch.");
logger.log(Level.INFO, "Losers Branch: " + losersBranch);
logger.log(Level.INFO, "Winners Branch: " + winnersBranch);
DatabaseBranch winnersApplyBranch = new DatabaseBranch();
boolean applyBranchStarted = false;
for (int i = 0; i < winnersBranch.size(); i++) {
if (!applyBranchStarted) {
if (i >= losersBranch.size() || !losersBranch.get(i).equals(winnersBranch.get(i))) {
applyBranchStarted = true;
}
}
if (applyBranchStarted) {
winnersApplyBranch.add(winnersBranch.get(i));
}
}
return winnersApplyBranch;
}
/**
* Algorithm to find the winner's database branch (client name and branch).
* The winner's branch is used to determine the local file system actions.
*
* <p>Basic algorithm: Sort all databaseversions by vectorclocks, tiebreaking with timestamps and machinenames.
* Iterate over this list, adding databaseversions to the winning branch if they are not simultaneous with the
* winning branch up until this point.
*
* <p><b>Illustration:</b><br />
* Suppose the following branches exist.
* Naming: <em>created-by / vector clock / local time</em>.
*
* <pre>
* A B C
* --|-------------------------------------------------
* 0 | A/(A1)/T=10 B/(A3,B1)/T=20 C/(A1,C1)/T=14
* 1 | A/(A2)/T=13 C/(A1,C2)/T=15
* 2 | A/(A3)/T=19
* 3 | A/(A4)/T=23
* </pre>
*
* <b>Sorted Database versions:</b>
* <ol>
* <li>A[0]:A/(A1)/T=10</li>
* <li>A[1]:A/(A2)/T=13</li>
* <li>C[0]:C/(A1,C1)/T=14</li>
* <li>C[1]:C/(A1,C2)/T=15</li>
* <li>A[2]:A/(A3)/T=19</li>
* <li>B[0]:B/(A3,B1)/T=20</li>
* <li>A[3]:A/(A4)/T=23</li>
* </ol>
*
* <b>Iterating through the list:</b>
* <ol>
* <li>A[0] is the first version. Add it.</li>
* <li>A[1] > A[0]. Add it.</li>
* <li>C[0] is simultaneous with A[1]. Ignore it.</li>
* <li>C[1] is simultaneous with A[1]. Ignore it.</li>
* <li>A[2] > A[1]. Add it.</li>
* <li>B[0] > A[2]. Add it.</li>
* <li>A[3] is simultaneous with B[0]. Ignore it.</li>
* </ol>
*
* <b>Winning branch:</b>
* <ol>
* <li>A[0]:A/(A1)/T=10</li>
* <li>A[1]:A/(A2)/T=13</li>
* <li>A[2]:A/(A3)/T=19</li>
* <li>B[0]:B/(A3,B1)/T=20</li>
* </ol>
*
* Last version matches last version of B. Hence B wins.
*
* @param allStitchedBranches All branches of all machines (including local)
* @return Returns the name and the branch of the winning machine
*/
private Entry<String, DatabaseBranch> findWinnersNameAndBranch(DatabaseBranches allBranches) {
List<DatabaseVersionHeader> databaseVersionHeaders = sortBranches(allBranches);
if (databaseVersionHeaders.size() == 0) {
return null;
}
// Determine winning branch
DatabaseBranch winnersBranch = new DatabaseBranch();
DatabaseVersionHeaderComparator databaseVersionHeaderComparator = new DatabaseVersionHeaderComparator(false);
for (DatabaseVersionHeader potentialWinner : databaseVersionHeaders) {
boolean emptyWinnerBranch = winnersBranch.size() == 0;
boolean potentialWinnerWins = !emptyWinnerBranch && databaseVersionHeaderComparator.compare(potentialWinner, winnersBranch.getLast()) > 0;
if (emptyWinnerBranch || potentialWinnerWins) {
logger.log(Level.INFO, "Adding database version to winning branch: " + potentialWinner);
winnersBranch.add(potentialWinner);
}
else {
logger.log(Level.INFO, "Ignoring databaseVersion: " + potentialWinner);
}
}
// Determine client name for winning branch
DatabaseVersionHeader winningLastDatabaseVersionHeader = winnersBranch.getLast();
for (String currentClient : allBranches.getClients()) {
DatabaseBranch currentBranch = allBranches.getBranch(currentClient);
DatabaseVersionHeader currentBranchLastDatabaseVersionHeader = currentBranch.getLast();
if (winningLastDatabaseVersionHeader.equals(currentBranchLastDatabaseVersionHeader)) {
return new AbstractMap.SimpleEntry<String, DatabaseBranch>(currentClient, winnersBranch);
}
}
return null;
}
private List<DatabaseVersionHeader> sortBranches(DatabaseBranches allBranches) {
List<DatabaseVersionHeader> databaseVersionHeaders = new ArrayList<DatabaseVersionHeader>();
for (String client : allBranches.getClients()) {
databaseVersionHeaders.addAll(allBranches.getBranch(client).getAll());
}
Collections.sort(databaseVersionHeaders, new DatabaseVersionHeaderComparator(true));
return databaseVersionHeaders;
}
}