-
Notifications
You must be signed in to change notification settings - Fork 0
/
ApianVoting.cs
170 lines (152 loc) · 6.6 KB
/
ApianVoting.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
using System;
using System.Linq;
using System.Collections.Generic;
using UniLog;
namespace Apian
{
public enum VoteStatus
{
Voting,
Won,
Lost, // timed out
NotFound // Vote not found
}
public class VoteResult
{
// ReSharper disable MemberCanBePrivate.Global,UnusedMember.Global,NotAccessedField.Global
public readonly bool WasComplete; // if GetStatus is called without "viewOnly" when status was kWon
//or kLost then it is assumed that the voate has been
// acted upon and this is set to "true"
public readonly VoteStatus Status;
public readonly int YesVotes;
public readonly long TimeStamp;
public VoteResult(bool isComplete, VoteStatus status, int yesVotes, long timeStamp)
{
WasComplete = isComplete;
Status = status;
YesVotes = yesVotes;
TimeStamp = timeStamp;
}
// ReSharper enable MemberCanBePrivate.Global,UnusedMember.Global,NotAccessedField.Globala
}
public class ApianVoteMachine<T>
{
public const long DefaultExpireMs = 300;
public const long DefaultCleanupMs = 900;
private static long SysMs => DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond; // don;t use apian time for wait/expire stuff
public struct VoteData
{
public bool IsComplete {get; private set;}
public int NeededVotes {get;}
public long InitialMsgTime {get; private set;} // use this in any timestmped action resulting from the vote
public long ExpireTs {get;} // vote defaults to "no" after this
public long CleanupTs {get;} // VoteData gets removed after this
public VoteStatus Status {get; private set;}
public readonly List<string> PeerIds;
public void UpdateStatus(long nowMs)
{
if (Status == VoteStatus.Voting)
{
if (nowMs > ExpireTs)
Status = VoteStatus.Lost;
else if (PeerIds.Count >= NeededVotes)
Status = VoteStatus.Won;
}
}
public VoteData(int voteCnt, long msgTime, long expireTimeMs, long cleanupTimeMs)
{
IsComplete = false; // When GetResult() is called and the status is Won or Lost then this is set,
// indicating that if it gets read again (another yes vote comes in) it should
// NOT be acted upon. By the same token, we do not want to *delete* the vote,
// since a late vote will re-add it. We want the vote to be cleaned up automatically
// after a suitable time.
InitialMsgTime = msgTime;
NeededVotes = voteCnt;
ExpireTs = expireTimeMs;
CleanupTs = cleanupTimeMs;
Status = VoteStatus.Voting;
PeerIds = new List<string>();
}
public void AddVote(string peerId, long msgTime)
{
PeerIds.Add(peerId);
InitialMsgTime = msgTime < InitialMsgTime ? msgTime : InitialMsgTime; // use earliest
}
public void SetComplete() => IsComplete = true;
}
protected virtual int MajorityVotes(int peerCount) => peerCount / 2 + 1;
private Dictionary<T, VoteData> _voteDict;
protected long TimeoutMs {get;}
protected long CleanupMs {get;}
public readonly UniLogger Logger;
public ApianVoteMachine(long timeoutMs, long cleanupMs, UniLogger logger=null)
{
TimeoutMs = timeoutMs;
CleanupMs = cleanupMs;
Logger = logger ?? UniLogger.GetLogger("ApianVoteMachine");
_voteDict = new Dictionary<T, VoteData>();
}
protected void UpdateAllStatus()
{
// remove old and forgotten ones
_voteDict = _voteDict.Where(pair => pair.Value.CleanupTs >= SysMs)
.ToDictionary(pair => pair.Key, pair => pair.Value);
// if timed out set status to Lost
foreach (VoteData vote in _voteDict.Values)
vote.UpdateStatus(SysMs);
}
public void AddVote(T candidate, string votingPeer, long msgTime, int totalPeers)
{
UpdateAllStatus();
VoteData vd;
try {
vd = _voteDict[candidate];
if (vd.Status == VoteStatus.Voting)
{
vd.AddVote(votingPeer, msgTime);
vd.UpdateStatus(SysMs);
_voteDict[candidate] = vd; // VoteData is a struct (value) so must be re-added
Logger.Debug($"Vote.Add: +1 for: {candidate.ToString()}, Votes: {vd.PeerIds.Count}");
}
} catch (KeyNotFoundException) {
int majorityCnt = MajorityVotes(totalPeers);
vd = new VoteData(majorityCnt, msgTime, SysMs+TimeoutMs, SysMs+CleanupMs);
vd.PeerIds.Add(votingPeer);
vd.UpdateStatus(SysMs);
_voteDict[candidate] = vd;
Logger.Debug($"Vote.Add: New: {candidate.ToString()}, Majority: {majorityCnt}");
}
}
// public void DoneWithVote(T candidate)
// {
// try {
// voteDict.Remove(candidate);
// } catch (KeyNotFoundException) {}
// }
public VoteResult GetResult(T candidate, bool justPeeking=false)
{
// Have to get to it before it expires
// If the vote is finished (timed out or won)
// this will set the status to Done - which means it has been
// read
UpdateAllStatus();
VoteResult result = new VoteResult(false, VoteStatus.NotFound, 0, 0);
try {
VoteData vd = _voteDict[candidate];
result = new VoteResult(vd.IsComplete, vd.Status, vd.PeerIds.Count, vd.InitialMsgTime);
if (!justPeeking)
{
if (vd.Status == VoteStatus.Lost || vd.Status == VoteStatus.Won)
{
vd.SetComplete();
_voteDict[candidate] = vd;
}
}
} catch (KeyNotFoundException)
{
//logger.Warn($"GetStatus: Vote not found");
}
return result;
}
}
}