Skip to content

Commit

Permalink
Optionally, allow collaborators involved in a conflict to vote
Browse files Browse the repository at this point in the history
  • Loading branch information
ekuiter committed Jun 10, 2019
1 parent 7fef130 commit 4e4571f
Show file tree
Hide file tree
Showing 10 changed files with 109 additions and 35 deletions.
25 changes: 19 additions & 6 deletions client/src/components/overlays/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ interface ArgumentDescriptor {
items?: PaletteItemsFunction,
allowFreeform?: boolean,
transformFreeform?: (value: string) => string,
title?: string
title?: string,
skipArguments?: (value: string) => number
};

function clearLocalStorage() {
Expand Down Expand Up @@ -128,12 +129,16 @@ export default class extends React.Component<Props, State> {
toPaletteItemsFunction = (items?: PaletteItemsFunction) => items || (() => []),
argumentDescriptor: ArgumentDescriptor = toArgumentDescriptor(args[0]),
// bind current argument and recurse (until all arguments are bound)
recurse = (value: string) =>
this.actionWithArguments(
args.slice(1).map(toArgumentDescriptor).map(argument => ({
recurse = (value: string) => {
const currentArgument = toArgumentDescriptor(args[0]);
return this.actionWithArguments(
args.slice(currentArgument.skipArguments ? currentArgument.skipArguments(value) + 1 : 1)
.map(toArgumentDescriptor)
.map(argument => ({
...argument, items: toPaletteItemsFunction(argument.items).bind(undefined, value)
})),
action.bind(undefined, value));
};

this.setState({
rerenderPalette: +new Date(),
Expand Down Expand Up @@ -595,9 +600,17 @@ export default class extends React.Component<Props, State> {
[{
title: i18n.t('commandPalette.featureDiagram.votingStrategy'),
items: () => Object.values(VotingStrategy).map(votingStrategy =>
({text: i18n.t('commandPalette.featureDiagram', votingStrategy), key: votingStrategy}))
({text: i18n.t('commandPalette.featureDiagram', votingStrategy), key: votingStrategy})),
skipArguments: (votingStrategy: string) => votingStrategy === VotingStrategy.reject ? 1 : 0
}, {
title: i18n.t('commandPalette.featureDiagram.votingStrategy'),
items: () => [
{text: i18n.t('commandPalette.featureDiagram.everyone'), key: 'false'},
{text: i18n.t('commandPalette.featureDiagram.onlyInvolved'), key: 'true'}
]
}],
votingStrategy => this.props.onSetVotingStrategy({votingStrategy}))
(votingStrategy, onlyInvolved) => this.props.onSetVotingStrategy(
{votingStrategy, onlyInvolved: onlyInvolved === 'true'}))
}, {
text: i18n.t('commandPalette.developer.debug'),
icon: 'DeveloperTools',
Expand Down
2 changes: 2 additions & 0 deletions client/src/i18n.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ const translationMap = {
},
votingStrategy: 'Voting strategy',
setVotingStrategy: 'Set voting strategy',
onlyInvolved: 'Only collaborators involved in a conflict may vote',
everyone: 'Everyone may vote (default)',
reject: 'Reject conflicts',
firstVote: 'First vote wins',
plurality: 'Plurality vote',
Expand Down
4 changes: 2 additions & 2 deletions client/src/store/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,8 @@ const actions = {
},

vote: createMessageAction(({versionID}: {versionID?: string}) => ({type: MessageType.VOTE, versionID})),
setVotingStrategy: createMessageAction(({votingStrategy}: {votingStrategy: string}) =>
({type: MessageType.SET_VOTING_STRATEGY, votingStrategy}))
setVotingStrategy: createMessageAction(({votingStrategy, onlyInvolved}: {votingStrategy: string, onlyInvolved: boolean}) =>
({type: MessageType.SET_VOTING_STRATEGY, votingStrategy, onlyInvolved}))
}
}
};
Expand Down
2 changes: 1 addition & 1 deletion client/src/store/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export type OnToggleFeatureGroupTypeFunction = (payload: {feature: Feature}) =>
export type OnSetUserProfileFunction = (payload: {name: string}) => Promise<void>;
export type OnResetFunction = () => Promise<void>;
export type OnVoteFunction = (payload: {versionID?: string}) => Promise<void>;
export type OnSetVotingStrategyFunction = (payload: {votingStrategy: string}) => Promise<void>;
export type OnSetVotingStrategyFunction = (payload: {votingStrategy: string, onlyInvolved: boolean}) => Promise<void>;

// Props that may derived from the state to use in React components.
// This enforces the convention that a prop called 'on...' has the same type in all components.
Expand Down
8 changes: 4 additions & 4 deletions kernel/src/kernel/api.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,9 @@
[message]
(profile
{}
(let [[voting? message] (server/forward-message! (helpers/decode message))]
(let [[involved-site-IDs message] (server/forward-message! (helpers/decode message))]
(into-array Object
[voting?
[(when involved-site-IDs (into-array involved-site-IDs))
(helpers/encode message)]))))

(defn serverSiteJoined
Expand Down Expand Up @@ -184,9 +184,9 @@
[site-ID]
(profile
{}
(let [[voting? message] (server/site-left! site-ID)]
(let [[involved-site-IDs message] (server/site-left! site-ID)]
(into-array Object
[voting?
[(when involved-site-IDs (into-array involved-site-IDs))
(helpers/encode message)]))))


Expand Down
13 changes: 12 additions & 1 deletion kernel/src/kernel/core/conflict_resolution.cljc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
(ns kernel.core.conflict-resolution
"Utilities for assisting in conflict resolution."
(:require [kernel.core.vector-clock :as VC]
(:require [clojure.set :as set]
[kernel.core.vector-clock :as VC]
[kernel.core.history-buffer :as HB]
[kernel.core.conflict-cache :as CC]
[kernel.core.garbage-collector :as GC]
Expand Down Expand Up @@ -97,6 +98,16 @@
(let [_conflict-descriptor combined-effect]
(_conflict-descriptor :synchronized)))))

(defn involved-site-IDs
"Returns all sites which are involved in any conflict.
This is useful for only allowing those sites to participate in the voting phase."
[MCGS HB combined-effect]
(p ::involved-site-IDs
(when (voting? MCGS combined-effect)
; these are all operations involved in some conflict (as they are not in the neutral CG)
(let [conflicting-operations (set/difference (MOVIC/get-all-operations MCGS) (MOVIC/neutral-CG MCGS))]
(distinct (map #(CO/get-site-ID (HB/lookup HB %)) conflicting-operations))))))

(defn resolved-MCG
"For an agreed resolved version, extracts the according MCG from an MCGS."
[MCGS MCG-ID]
Expand Down
4 changes: 2 additions & 2 deletions kernel/src/kernel/shell/server.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
(site/receive-message! new-message) ; ignore returned feature model on the server
(generate-heartbeat!)
(swap! (*context* :GC) #(GC/insert % :server (GC-filter @(*context* :VC))))
[(conflict-resolution/voting? @(*context* :MCGS) @(*context* :combined-effect))
[(conflict-resolution/involved-site-IDs @(*context* :MCGS) @(*context* :HB) @(*context* :combined-effect))
(message/with-server-VC new-message (GC-filter @(*context* :VC)))])))

(defn site-joined!
Expand Down Expand Up @@ -113,7 +113,7 @@
(let [message (message/make-leave site-ID)]
(site/receive-leave! message)
(swap! (*context* :offline-sites) #(conj % site-ID))
[(conflict-resolution/voting? @(*context* :MCGS) @(*context* :combined-effect))
[(conflict-resolution/involved-site-IDs @(*context* :MCGS) @(*context* :HB) @(*context* :combined-effect))
message])))

(defn resolve-conflict!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import org.pmw.tinylog.Logger;

import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* A collaborative session consists of a set of collaborators that view and edit a artifact together.
Expand Down Expand Up @@ -60,6 +62,7 @@ void onMessage(Collaborator collaborator, Message message) throws Message.Invali
static class FeatureModel extends CollaborativeSession {
private Kernel kernel;
private String votingStrategy = "consensus";
private boolean onlyInvolved = false;
private VotingPhase votingPhase;

FeatureModel(Artifact.Path artifactPath, IFeatureModel initialFeatureModel) {
Expand All @@ -68,12 +71,21 @@ static class FeatureModel extends CollaborativeSession {
this.kernel = new Kernel(artifactPath, initialFeatureModel);
}

private void broadcastResponse(Collaborator collaborator, Object[] votingAndMessage) {
boolean isVoting = (boolean) votingAndMessage[0];
String newMessage = (String) votingAndMessage[1];
private void broadcastResponse(Collaborator collaborator, Object[] involvedSiteIDsAndMessage) {
String[] involvedSiteIDs = (String[]) involvedSiteIDsAndMessage[0];
String newMessage = (String) involvedSiteIDsAndMessage[1];
CollaboratorUtils.broadcastToOtherCollaborators(collaborators, new Api.Kernel(artifactPath, newMessage), collaborator);
if (isVoting && votingPhase == null) {
votingPhase = new VotingPhase(VotingPhase.VotingStrategy.fromString(votingStrategy, collaborators));
if (involvedSiteIDs != null && votingPhase == null) {
Logger.info("{} collaborators involved in the conflict", involvedSiteIDs.length);
Collection<Collaborator> involvedCollaborators =
Stream.of(involvedSiteIDs)
.map(siteID -> collaborators.stream()
.filter(_collaborator -> _collaborator.getSiteID().equals(UUID.fromString(siteID)))
.findFirst()
.get())
.collect(Collectors.toCollection(HashSet::new));
votingPhase = new VotingPhase(VotingPhase.VotingStrategy.createInstance(
votingStrategy, onlyInvolved, collaborators, involvedCollaborators));
broadcastVoters();
updateVotingPhase();
}
Expand Down Expand Up @@ -102,8 +114,10 @@ protected boolean _onMessage(Collaborator collaborator, Message.IDecodable messa
if (votingPhase != null)
throw new RuntimeException("can not change voting strategy while in voting phase");
String votingStrategy = ((Api.SetVotingStrategy) message).votingStrategy;
Logger.info("setting voting strategy to {}", votingStrategy);
boolean onlyInvolved = ((Api.SetVotingStrategy) message).onlyInvolved;
Logger.info("setting voting strategy to {} ({})", votingStrategy, onlyInvolved ? "only involved" : "everyone");
this.votingStrategy = votingStrategy;
this.onlyInvolved = onlyInvolved;
return true;
}

Expand Down
57 changes: 44 additions & 13 deletions server/src/main/java/de/ovgu/spldev/varied/VotingPhase.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,32 @@ public Collection<Collaborator> getVoters() {
return voters;
}
}

class OnlyInvolved implements IVoters {
Set<Collaborator> involvedCollaborators, voters;

OnlyInvolved(Collection<Collaborator> involvedCollaborators) {
this.involvedCollaborators = new HashSet<>(involvedCollaborators);
voters = new HashSet<>(involvedCollaborators);
}

public void onJoin(Collaborator collaborator) {
if (involvedCollaborators.contains(collaborator))
voters.add(collaborator);
}

public void onLeave(Collaborator collaborator) {
voters.remove(collaborator);
}

public boolean isEligible(Collaborator collaborator) {
return voters.contains(collaborator);
}

public Collection<Collaborator> getVoters() {
return voters;
}
}
}

interface IResolutionCriterion {
Expand Down Expand Up @@ -173,46 +199,51 @@ static VotingStrategy reject() {
new IResolutionOutcome.Neutral());
}

static VotingStrategy firstVote(Collection<Collaborator> collaborators) {
static VotingStrategy firstVote(IVoters voters) {
return new VotingStrategy(
new IVoters.Everyone(collaborators),
voters,
new IResolutionCriterion.OnFirstVote(),
new IResolutionOutcome.Any());
}

static VotingStrategy plurality(Collection<Collaborator> collaborators) {
static VotingStrategy plurality(IVoters voters) {
return new VotingStrategy(
new IVoters.Everyone(collaborators),
voters,
new IResolutionCriterion.OnLastVote(),
new IResolutionOutcome.Plurality());
}

static VotingStrategy majority(Collection<Collaborator> collaborators) {
static VotingStrategy majority(IVoters voters) {
return new VotingStrategy(
new IVoters.Everyone(collaborators),
voters,
new IResolutionCriterion.OnLastVote(),
new IResolutionOutcome.Majority());
}

static VotingStrategy consensus(Collection<Collaborator> collaborators) {
static VotingStrategy consensus(IVoters voters) {
return new VotingStrategy(
new IVoters.Everyone(collaborators),
voters,
new IResolutionCriterion.OnLastVoteOrDissent(),
new IResolutionOutcome.Consensus());
}

static VotingStrategy fromString(String votingStrategy, Collection<Collaborator> collaborators) {
static VotingStrategy createInstance(
String votingStrategy, boolean onlyInvolved,
Collection<Collaborator> collaborators, Collection<Collaborator> involvedCollaborators) {
IVoters voters = onlyInvolved
? new IVoters.OnlyInvolved(involvedCollaborators)
: new IVoters.Everyone(collaborators);
switch (votingStrategy) {
case "reject":
return reject();
case "firstVote":
return firstVote(collaborators);
return firstVote(voters);
case "plurality":
return plurality(collaborators);
return plurality(voters);
case "majority":
return majority(collaborators);
return majority(voters);
case "consensus":
return consensus(collaborators);
return consensus(voters);
default:
throw new RuntimeException("invalid voting strategy given");
}
Expand Down
3 changes: 3 additions & 0 deletions server/src/main/java/de/ovgu/spldev/varied/messaging/Api.java
Original file line number Diff line number Diff line change
Expand Up @@ -157,5 +157,8 @@ public ResolutionOutcome(Artifact.Path artifactPath, String versionID) {
public static class SetVotingStrategy extends Message implements Message.IDecodable {
@Expose
public String votingStrategy;

@Expose
public boolean onlyInvolved;
}
}

0 comments on commit 4e4571f

Please sign in to comment.