Skip to content

Commit 7c3ab1c

Browse files
feat(elections): scalable vote storage with atomic MongoDB ops and multi-server safety
Replace single-document read-modify-write with per-voter documents and atomic $inc tally counters for O(1) vote reads. Add StartElection and ResolveElection endpoints for idempotent multi-server election lifecycle. Remove SaveElectionData endpoint.
1 parent 28190f5 commit 7c3ab1c

12 files changed

Lines changed: 579 additions & 166 deletions
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package net.swofty.commons.protocol.objects.election;
2+
3+
import net.swofty.commons.protocol.ProtocolObject;
4+
import net.swofty.commons.protocol.Serializer;
5+
import org.json.JSONObject;
6+
7+
public class ResolveElectionProtocolObject
8+
extends ProtocolObject<ResolveElectionProtocolObject.ResolveElectionMessage,
9+
ResolveElectionProtocolObject.ResolveElectionResponse> {
10+
11+
@Override
12+
public Serializer<ResolveElectionMessage> getSerializer() {
13+
return new Serializer<>() {
14+
@Override
15+
public String serialize(ResolveElectionMessage value) {
16+
JSONObject json = new JSONObject();
17+
json.put("year", value.year());
18+
return json.toString();
19+
}
20+
21+
@Override
22+
public ResolveElectionMessage deserialize(String json) {
23+
JSONObject obj = new JSONObject(json);
24+
return new ResolveElectionMessage(obj.getInt("year"));
25+
}
26+
27+
@Override
28+
public ResolveElectionMessage clone(ResolveElectionMessage value) {
29+
return value;
30+
}
31+
};
32+
}
33+
34+
@Override
35+
public Serializer<ResolveElectionResponse> getReturnSerializer() {
36+
return new Serializer<>() {
37+
@Override
38+
public String serialize(ResolveElectionResponse value) {
39+
JSONObject json = new JSONObject();
40+
json.put("resolved", value.resolved());
41+
json.put("serializedData", value.serializedData());
42+
return json.toString();
43+
}
44+
45+
@Override
46+
public ResolveElectionResponse deserialize(String json) {
47+
JSONObject obj = new JSONObject(json);
48+
return new ResolveElectionResponse(
49+
obj.getBoolean("resolved"),
50+
obj.optString("serializedData", null)
51+
);
52+
}
53+
54+
@Override
55+
public ResolveElectionResponse clone(ResolveElectionResponse value) {
56+
return value;
57+
}
58+
};
59+
}
60+
61+
public record ResolveElectionMessage(int year) {}
62+
63+
public record ResolveElectionResponse(boolean resolved, String serializedData) {}
64+
}

commons/src/main/java/net/swofty/commons/protocol/objects/election/SaveElectionDataProtocolObject.java

Lines changed: 0 additions & 58 deletions
This file was deleted.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package net.swofty.commons.protocol.objects.election;
2+
3+
import net.swofty.commons.protocol.ProtocolObject;
4+
import net.swofty.commons.protocol.Serializer;
5+
import org.json.JSONObject;
6+
7+
public class StartElectionProtocolObject
8+
extends ProtocolObject<StartElectionProtocolObject.StartElectionMessage,
9+
StartElectionProtocolObject.StartElectionResponse> {
10+
11+
@Override
12+
public Serializer<StartElectionMessage> getSerializer() {
13+
return new Serializer<>() {
14+
@Override
15+
public String serialize(StartElectionMessage value) {
16+
JSONObject json = new JSONObject();
17+
json.put("year", value.year());
18+
json.put("candidatesJson", value.candidatesJson());
19+
return json.toString();
20+
}
21+
22+
@Override
23+
public StartElectionMessage deserialize(String json) {
24+
JSONObject obj = new JSONObject(json);
25+
return new StartElectionMessage(
26+
obj.getInt("year"),
27+
obj.getString("candidatesJson")
28+
);
29+
}
30+
31+
@Override
32+
public StartElectionMessage clone(StartElectionMessage value) {
33+
return value;
34+
}
35+
};
36+
}
37+
38+
@Override
39+
public Serializer<StartElectionResponse> getReturnSerializer() {
40+
return new Serializer<>() {
41+
@Override
42+
public String serialize(StartElectionResponse value) {
43+
JSONObject json = new JSONObject();
44+
json.put("started", value.started());
45+
json.put("serializedData", value.serializedData());
46+
return json.toString();
47+
}
48+
49+
@Override
50+
public StartElectionResponse deserialize(String json) {
51+
JSONObject obj = new JSONObject(json);
52+
return new StartElectionResponse(
53+
obj.getBoolean("started"),
54+
obj.optString("serializedData", null)
55+
);
56+
}
57+
58+
@Override
59+
public StartElectionResponse clone(StartElectionResponse value) {
60+
return value;
61+
}
62+
};
63+
}
64+
65+
public record StartElectionMessage(int year, String candidatesJson) {}
66+
67+
public record StartElectionResponse(boolean started, String serializedData) {}
68+
}

service.elections/src/main/java/net/swofty/service/election/ElectionDatabase.java

Lines changed: 99 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,21 @@
77
import com.mongodb.client.MongoCollection;
88
import com.mongodb.client.MongoDatabase;
99
import com.mongodb.client.model.Filters;
10+
import com.mongodb.client.model.ReplaceOptions;
1011
import com.mongodb.client.model.Updates;
1112
import net.swofty.service.generic.MongoDB;
1213
import org.bson.Document;
1314

15+
import java.util.HashMap;
16+
import java.util.List;
17+
import java.util.Map;
18+
1419
public record ElectionDatabase(String key) implements MongoDB {
1520
public static MongoClient client;
1621
public static MongoDatabase database;
1722
public static MongoCollection<Document> electionCollection;
23+
public static MongoCollection<Document> votesCollection;
24+
public static MongoCollection<Document> talliesCollection;
1825

1926
@Override
2027
public MongoDB connect(String connectionString) {
@@ -24,6 +31,8 @@ public MongoDB connect(String connectionString) {
2431

2532
database = client.getDatabase("Minestom");
2633
electionCollection = database.getCollection("elections");
34+
votesCollection = database.getCollection("election-votes");
35+
talliesCollection = database.getCollection("election-tallies");
2736
return this;
2837
}
2938

@@ -42,33 +51,25 @@ public boolean exists() {
4251
@Override
4352
public Object get(String key, Object def) {
4453
Document doc = electionCollection.find(Filters.eq("_id", this.key)).first();
45-
if (doc == null) {
46-
return def;
47-
}
54+
if (doc == null) return def;
4855
return doc.get(key);
4956
}
5057

5158
@Override
5259
public void insertOrUpdate(String key, Object value) {
53-
if (exists()) {
54-
Document query = new Document("_id", this.key);
55-
Document found = electionCollection.find(query).first();
56-
assert found != null;
57-
electionCollection.updateOne(found, Updates.set(key, value));
58-
return;
59-
}
60-
Document newDoc = new Document("_id", this.key);
61-
newDoc.append(key, value);
62-
electionCollection.insertOne(newDoc);
60+
Document doc = new Document("_id", this.key).append(key, value);
61+
electionCollection.replaceOne(
62+
Filters.eq("_id", this.key),
63+
doc,
64+
new ReplaceOptions().upsert(true)
65+
);
6366
}
6467

6568
@Override
6669
public boolean remove(String id) {
6770
Document query = new Document("_id", id);
6871
Document found = electionCollection.find(query).first();
69-
if (found == null) {
70-
return false;
71-
}
72+
if (found == null) return false;
7273
electionCollection.deleteOne(query);
7374
return true;
7475
}
@@ -80,14 +81,88 @@ public static String loadElectionData() {
8081
}
8182

8283
public static void saveElectionData(String serializedData) {
83-
Document query = new Document("_id", "election_data");
84-
Document existing = electionCollection.find(query).first();
85-
if (existing != null) {
86-
electionCollection.updateOne(query, Updates.set("data", serializedData));
87-
} else {
88-
Document newDoc = new Document("_id", "election_data");
89-
newDoc.append("data", serializedData);
90-
electionCollection.insertOne(newDoc);
84+
Document doc = new Document("_id", "election_data").append("data", serializedData);
85+
electionCollection.replaceOne(
86+
Filters.eq("_id", "election_data"),
87+
doc,
88+
new ReplaceOptions().upsert(true)
89+
);
90+
}
91+
92+
public static void castVote(String playerId, String candidateName, int electionYear) {
93+
String talliesDocId = "tallies_" + electionYear;
94+
95+
Document existingVote = votesCollection.find(
96+
Filters.and(Filters.eq("_id", playerId), Filters.eq("electionYear", electionYear))
97+
).first();
98+
99+
if (existingVote != null) {
100+
String oldCandidate = existingVote.getString("candidate");
101+
if (oldCandidate != null && !oldCandidate.equals(candidateName)) {
102+
talliesCollection.updateOne(
103+
Filters.eq("_id", talliesDocId),
104+
Updates.inc(oldCandidate, -1L)
105+
);
106+
}
107+
}
108+
109+
Document voteDoc = new Document("_id", playerId)
110+
.append("candidate", candidateName)
111+
.append("electionYear", electionYear);
112+
votesCollection.replaceOne(
113+
Filters.eq("_id", playerId),
114+
voteDoc,
115+
new ReplaceOptions().upsert(true)
116+
);
117+
118+
if (existingVote == null || !candidateName.equals(existingVote.getString("candidate"))) {
119+
talliesCollection.updateOne(
120+
Filters.eq("_id", talliesDocId),
121+
Updates.inc(candidateName, 1L),
122+
new com.mongodb.client.model.UpdateOptions().upsert(true)
123+
);
91124
}
92125
}
126+
127+
public static String getPlayerVote(String playerId, int electionYear) {
128+
Document doc = votesCollection.find(
129+
Filters.and(Filters.eq("_id", playerId), Filters.eq("electionYear", electionYear))
130+
).first();
131+
if (doc == null) return null;
132+
return doc.getString("candidate");
133+
}
134+
135+
public static Map<String, Long> getTallies(int electionYear) {
136+
String talliesDocId = "tallies_" + electionYear;
137+
Document doc = talliesCollection.find(Filters.eq("_id", talliesDocId)).first();
138+
if (doc == null) return new HashMap<>();
139+
140+
Map<String, Long> tallies = new HashMap<>();
141+
for (String key : doc.keySet()) {
142+
if (key.equals("_id")) continue;
143+
Object val = doc.get(key);
144+
if (val instanceof Number num) {
145+
tallies.put(key, num.longValue());
146+
}
147+
}
148+
return tallies;
149+
}
150+
151+
public static void initTallies(int electionYear, List<String> candidateNames) {
152+
String talliesDocId = "tallies_" + electionYear;
153+
Document doc = new Document("_id", talliesDocId);
154+
for (String name : candidateNames) {
155+
doc.append(name, 0L);
156+
}
157+
talliesCollection.replaceOne(
158+
Filters.eq("_id", talliesDocId),
159+
doc,
160+
new ReplaceOptions().upsert(true)
161+
);
162+
}
163+
164+
public static void clearVotesForYear(int electionYear) {
165+
votesCollection.deleteMany(Filters.eq("electionYear", electionYear));
166+
talliesCollection.deleteMany(Filters.eq("_id", "tallies_" + electionYear));
167+
}
93168
}

service.elections/src/main/java/net/swofty/service/election/ElectionService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
public class ElectionService implements SkyBlockService {
1111

12-
static void main() {
12+
public static void main(String[] args) {
1313
String mongoUri = ConfigProvider.settings().getMongodb();
1414
new ElectionDatabase(null).connect(mongoUri);
1515
SkyBlockService.init(new ElectionService());

0 commit comments

Comments
 (0)