Aggie Student Toolkit — Load Balancer CSCE 412 Project 4
Arnav Fotedar · Fall 2026 · C++ load balancer simulation
Loading...
Searching...
No Matches
LoadBalancer.cpp
Go to the documentation of this file.
1
6
7#include "LoadBalancer.h"
8
9#include <algorithm>
10#include <iomanip>
11#include <ostream>
12#include <sstream>
13#include <stdexcept>
14
15LoadBalancer::LoadBalancer(std::string name, std::size_t initialServers, const ScalingPolicy& policy,
16 const Firewall& firewall, std::size_t maxQueue, Logger& logger)
17 : name_(std::move(name)), policy_(policy), firewall_(firewall), maxQueue_(maxQueue), logger_(logger) {
18 if (initialServers == 0) {
19 throw std::invalid_argument("LoadBalancer needs at least one server");
20 }
21 for (std::size_t i = 0; i < initialServers; ++i) {
22 servers_.emplace_back(nextServerId_++, 0);
23 }
24 stats_.initialServers = initialServers;
25 stats_.minActiveServers = initialServers;
26 stats_.peakLiveServers = initialServers;
27}
28
29std::string LoadBalancer::describe(const Request& request) const {
30 std::ostringstream text;
31 text << "req#" << request.id << " " << request.sourceIp << " -> " << request.destinationIp << " ("
32 << jobTypeCode(request.jobType) << ", " << request.taskCycles << " cycles)";
33 return text.str();
34}
35
36bool LoadBalancer::admit(const Request& request, unsigned long cycle) {
37 ++stats_.received;
38 if (firewall_.isBlocked(request.sourceIp)) {
39 ++stats_.rejected;
40 logger_.log(LogCategory::Reject, cycle, name_, describe(request) + " blocked by firewall");
41 return false;
42 }
43 if (maxQueue_ != 0 && pending_.size() >= maxQueue_) {
44 ++stats_.discarded;
45 logger_.log(LogCategory::Discard, cycle, name_, describe(request) + " dropped, queue full");
46 return false;
47 }
48 pending_.push(request);
49 ++stats_.admitted;
50 if (pending_.size() > stats_.peakQueue) {
51 stats_.peakQueue = pending_.size();
52 stats_.peakQueueCycle = cycle;
53 }
54 if (cycle > 0) { // the initial queue is summarized instead of logged one by one
55 logger_.log(LogCategory::Arrival, cycle, name_,
56 describe(request) + " queued, queue=" + std::to_string(pending_.size()));
57 }
58 return true;
59}
60
62 stats_.startingQueue = pending_.size();
63 logger_.log(LogCategory::Info, 0, name_,
64 "starting with " + std::to_string(servers_.size()) + " servers and " +
65 std::to_string(pending_.size()) + " queued requests");
66 dispatch(0);
67 stats_.queueAfterFirstDispatch = pending_.size();
68}
69
70void LoadBalancer::processCycle(unsigned long cycle, const std::vector<Request>& arrivals) {
71 completeWork(cycle);
72 for (const Request& request : arrivals) {
73 admit(request, cycle);
74 }
75 adjustCapacity(cycle);
76 dispatch(cycle);
77 updateExtremes(cycle);
78}
79
80void LoadBalancer::completeWork(unsigned long cycle) {
81 stats_.serverCycles += servers_.size();
82 for (WebServer& server : servers_) {
83 if (server.isIdle()) {
84 continue;
85 }
86 ++stats_.busyCycles;
87 if (server.tick()) {
88 ++stats_.completed;
89 logger_.log(LogCategory::Complete, cycle, name_,
90 "server " + std::to_string(server.id()) + " finished " +
91 describe(server.lastCompleted()));
92 }
93 }
94 // Remove retiring servers that have now finished their last request.
95 for (auto it = servers_.begin(); it != servers_.end();) {
96 if (it->isRetiring() && it->isIdle()) {
97 const unsigned int id = it->id();
98 it = servers_.erase(it);
99 recordScale(ScaleEvent::Kind::Retired, cycle, id);
100 } else {
101 ++it;
102 }
103 }
104}
105
106void LoadBalancer::adjustCapacity(unsigned long cycle) {
107 if (policy_.cooldownCycles == 0 || cycle % policy_.cooldownCycles != 0) {
108 return;
109 }
110 const double queue = static_cast<double>(pending_.size());
111 const std::size_t active = activeServers();
112
113 if (queue > policy_.upperFactor * static_cast<double>(active)) {
114 // Prefer returning a retiring server to service: it already exists.
115 for (WebServer& server : servers_) {
116 if (server.isRetiring()) {
117 server.setRetiring(false);
118 ++stats_.scaleUps;
119 recordScale(ScaleEvent::Kind::Reactivated, cycle, server.id());
120 return;
121 }
122 }
123 if (servers_.size() < policy_.maxServers) {
124 addServer(cycle);
125 }
126 return;
127 }
128
129 if (queue < policy_.lowerFactor * static_cast<double>(active) && active > policy_.minServers) {
130 // Prefer removing an idle server immediately (search from the newest).
131 for (auto it = servers_.rbegin(); it != servers_.rend(); ++it) {
132 if (it->isIdle() && !it->isRetiring()) {
133 const unsigned int id = it->id();
134 servers_.erase(std::next(it).base());
135 ++stats_.scaleDowns;
136 recordScale(ScaleEvent::Kind::Removed, cycle, id);
137 return;
138 }
139 }
140 // Otherwise retire the busy server that will finish soonest.
141 WebServer* candidate = nullptr;
142 for (WebServer& server : servers_) {
143 if (!server.isRetiring() &&
144 (candidate == nullptr || server.remainingCycles() < candidate->remainingCycles())) {
145 candidate = &server;
146 }
147 }
148 if (candidate != nullptr) {
149 candidate->setRetiring(true);
150 ++stats_.scaleDowns;
151 recordScale(ScaleEvent::Kind::Retiring, cycle, candidate->id());
152 }
153 }
154}
155
156void LoadBalancer::addServer(unsigned long cycle) {
157 servers_.emplace_back(nextServerId_++, cycle);
158 ++stats_.scaleUps;
159 recordScale(ScaleEvent::Kind::Added, cycle, servers_.back().id());
160}
161
162void LoadBalancer::recordScale(ScaleEvent::Kind kind, unsigned long cycle, unsigned int serverId) {
163 ScaleEvent event;
164 event.cycle = cycle;
165 event.kind = kind;
166 event.serverId = serverId;
167 event.queueSize = pending_.size();
168 event.activeServers = activeServers();
169 scaleEvents_.push_back(event);
170
171 std::string action;
173 switch (kind) {
175 action = "added server ";
176 category = LogCategory::ScaleUp;
177 break;
179 action = "reactivated retiring server ";
180 category = LogCategory::ScaleUp;
181 break;
182 case ScaleEvent::Kind::Removed: action = "removed idle server "; break;
183 case ScaleEvent::Kind::Retiring: action = "retiring busy server "; break;
184 case ScaleEvent::Kind::Retired: action = "retired server "; break;
185 }
186 std::ostringstream text;
187 text << action << serverId << " | queue=" << pending_.size() << " active=" << activeServers()
188 << " live=" << servers_.size();
189 logger_.log(category, cycle, name_, text.str());
190}
191
192void LoadBalancer::dispatch(unsigned long cycle) {
193 for (WebServer& server : servers_) {
194 if (pending_.empty()) {
195 return;
196 }
197 if (!server.isIdle() || server.isRetiring()) {
198 continue;
199 }
200 const Request request = pending_.front();
201 pending_.pop();
202 const unsigned long wait = cycle - request.arrivalCycle;
203 stats_.totalWait += wait;
204 stats_.maxWait = std::max(stats_.maxWait, wait);
205 ++stats_.dispatched;
206 server.assign(request, cycle);
207 logger_.log(LogCategory::Assign, cycle, name_,
208 describe(request) + " -> server " + std::to_string(server.id()) + " after waiting " +
209 std::to_string(wait) + " cycles");
210 }
211}
212
213void LoadBalancer::updateExtremes(unsigned long /*cycle*/) {
214 stats_.minActiveServers = std::min(stats_.minActiveServers, activeServers());
215 stats_.peakLiveServers = std::max(stats_.peakLiveServers, servers_.size());
216}
217
218void LoadBalancer::recordSnapshot(unsigned long cycle) {
219 Snapshot snapshot;
220 snapshot.cycle = cycle;
221 snapshot.queueSize = pending_.size();
222 snapshot.liveServers = servers_.size();
223 snapshot.busyServers = busyServers();
224 snapshot.idleServers = idleServers();
225 snapshot.retiringServers = retiringServers();
226 snapshot.completed = stats_.completed;
227 snapshot.rejected = stats_.rejected;
228 snapshots_.push_back(snapshot);
229}
230
231std::string LoadBalancer::statusLine(unsigned long /*cycle*/) const {
232 std::ostringstream text;
233 text << "queue=" << std::setw(5) << pending_.size() << " | servers=" << std::setw(3) << servers_.size()
234 << " (busy " << busyServers() << ", idle " << idleServers() << ", retiring " << retiringServers()
235 << ") | completed=" << stats_.completed << " rejected=" << stats_.rejected
236 << " discarded=" << stats_.discarded << " | scale +" << stats_.scaleUps << "/-" << stats_.scaleDowns;
237 return text.str();
238}
239
240void LoadBalancer::summarize(std::ostream& out) const {
241 const auto row = [&out](const std::string& label, const std::string& value) {
242 out << " " << std::left << std::setw(46) << label << value << '\n';
243 };
244 const auto fixed = [](double value, int precision) {
245 std::ostringstream text;
246 text << std::fixed << std::setprecision(precision) << value;
247 return text.str();
248 };
249
250 out << "Load balancer " << name_ << '\n';
251 out << " -- Queue --\n";
252 row("Starting queue size (t=0, before dispatch)", std::to_string(stats_.startingQueue));
253 row("Queue size after first dispatch (t=0)", std::to_string(stats_.queueAfterFirstDispatch));
254 row("Ending queue size", std::to_string(pending_.size()));
255 row("Peak queue size", std::to_string(stats_.peakQueue) + " (cycle " + std::to_string(stats_.peakQueueCycle) + ")");
256
257 out << " -- Servers --\n";
258 row("Initial servers", std::to_string(stats_.initialServers));
259 row("Final live servers", std::to_string(servers_.size()));
260 row(" Active (busy) servers at end", std::to_string(busyServers() - std::min(busyServers(), retiringServers())));
261 row(" Inactive (idle) servers at end", std::to_string(idleServers()));
262 row(" Retiring servers at end (still busy)", std::to_string(retiringServers()));
263 row("Fewest active / most live servers", std::to_string(stats_.minActiveServers) + " / " +
264 std::to_string(stats_.peakLiveServers));
265 row("Scale-up events (added + reactivated)", std::to_string(stats_.scaleUps));
266 row("Scale-down events (removed + retiring)", std::to_string(stats_.scaleDowns));
267 const double utilization =
268 stats_.serverCycles == 0 ? 0.0 : 100.0 * static_cast<double>(stats_.busyCycles) / stats_.serverCycles;
269 row("Server utilization (busy / available)", fixed(utilization, 1) + "%");
270
271 out << " -- Requests --\n";
272 row("Received (initial queue + arrivals)", std::to_string(stats_.received));
273 row("Admitted to queue", std::to_string(stats_.admitted));
274 row("Rejected by firewall", std::to_string(stats_.rejected));
275 row("Discarded (queue full)", std::to_string(stats_.discarded));
276 row("Dispatched to servers", std::to_string(stats_.dispatched));
277 row("Completed", std::to_string(stats_.completed));
278 row("In service at end (unfinished)", std::to_string(inService()));
279 row("Remaining in queue at end", std::to_string(pending_.size()));
280 const double averageWait =
281 stats_.dispatched == 0 ? 0.0 : static_cast<double>(stats_.totalWait) / stats_.dispatched;
282 row("Average / longest wait before dispatch", fixed(averageWait, 1) + " / " + std::to_string(stats_.maxWait) + " cycles");
283
284 const bool receivedBalanced = stats_.received == stats_.admitted + stats_.rejected + stats_.discarded;
285 const bool admittedBalanced = stats_.admitted == stats_.completed + pending_.size() + inService();
286 out << " -- Consistency checks --\n";
287 row("received = admitted + rejected + discarded",
288 std::to_string(stats_.received) + " = " + std::to_string(stats_.admitted) + " + " +
289 std::to_string(stats_.rejected) + " + " + std::to_string(stats_.discarded) +
290 (receivedBalanced ? " PASS" : " FAIL"));
291 row("admitted = completed + queued + in service",
292 std::to_string(stats_.admitted) + " = " + std::to_string(stats_.completed) + " + " +
293 std::to_string(pending_.size()) + " + " + std::to_string(inService()) +
294 (admittedBalanced ? " PASS" : " FAIL"));
295}
296
297const std::string& LoadBalancer::name() const { return name_; }
298
299std::size_t LoadBalancer::queueSize() const { return pending_.size(); }
300
301std::size_t LoadBalancer::liveServers() const { return servers_.size(); }
302
303std::size_t LoadBalancer::activeServers() const {
304 return static_cast<std::size_t>(std::count_if(servers_.begin(), servers_.end(),
305 [](const WebServer& s) { return !s.isRetiring(); }));
306}
307
308std::size_t LoadBalancer::busyServers() const {
309 return static_cast<std::size_t>(std::count_if(servers_.begin(), servers_.end(),
310 [](const WebServer& s) { return !s.isIdle(); }));
311}
312
313std::size_t LoadBalancer::idleServers() const { return servers_.size() - busyServers(); }
314
315std::size_t LoadBalancer::retiringServers() const {
316 return static_cast<std::size_t>(std::count_if(servers_.begin(), servers_.end(),
317 [](const WebServer& s) { return s.isRetiring(); }));
318}
319
320std::size_t LoadBalancer::inService() const { return busyServers(); }
321
322const LoadBalancerStats& LoadBalancer::stats() const { return stats_; }
323
324const std::vector<ScaleEvent>& LoadBalancer::scaleEvents() const { return scaleEvents_; }
325
326const std::vector<Snapshot>& LoadBalancer::snapshots() const { return snapshots_; }
327
328const std::vector<WebServer>& LoadBalancer::servers() const { return servers_; }
329
330const ScalingPolicy& LoadBalancer::policy() const { return policy_; }
331
332const Firewall& LoadBalancer::firewall() const { return firewall_; }
Declares the LoadBalancer class, which queues requests, dispatches them and scales its server pool.
LogCategory
Kind of event being logged; selects the color and whether quiet mode shows it.
Definition Logger.h:16
@ Assign
A queued request was dispatched to a server.
Definition Logger.h:18
@ Info
General information such as start-up messages.
Definition Logger.h:25
@ Arrival
A request entered the queue.
Definition Logger.h:17
@ ScaleDown
A server was removed or marked for retirement.
Definition Logger.h:23
@ Complete
A server finished a request.
Definition Logger.h:19
@ Discard
The queue was full and a request was dropped.
Definition Logger.h:21
@ Reject
The firewall blocked a request.
Definition Logger.h:20
@ ScaleUp
A server was added or returned to service.
Definition Logger.h:22
char jobTypeCode(JobType type)
Returns the one-letter code for a job type ('P' or 'S').
Definition Request.h:29
Rejects requests whose source address falls in a blocked IPv4 range.
Definition Firewall.h:22
void start()
Records the starting queue size and performs the first dispatch at time 0.
const ScalingPolicy & policy() const
std::size_t queueSize() const
const std::string & name() const
const LoadBalancerStats & stats() const
std::size_t busyServers() const
void summarize(std::ostream &out) const
Writes the end-of-run summary for this load balancer.
std::size_t activeServers() const
void processCycle(unsigned long cycle, const std::vector< Request > &arrivals)
Runs one clock cycle: complete work, admit arrivals, adjust capacity, dispatch.
const std::vector< Snapshot > & snapshots() const
std::size_t inService() const
std::size_t idleServers() const
std::string statusLine(unsigned long cycle) const
Builds a one-line status: queue, servers (busy/idle/retiring), completed, rejected.
void recordSnapshot(unsigned long cycle)
Saves a Snapshot of the current state for the periodic table in the log.
std::size_t retiringServers() const
std::size_t liveServers() const
LoadBalancer(std::string name, std::size_t initialServers, const ScalingPolicy &policy, const Firewall &firewall, std::size_t maxQueue, Logger &logger)
Creates a load balancer with an initial server pool.
const std::vector< ScaleEvent > & scaleEvents() const
const Firewall & firewall() const
const std::vector< WebServer > & servers() const
bool admit(const Request &request, unsigned long cycle)
Offers a request to the load balancer.
Writes simulation events to the console and, optionally, to a file.
Definition Logger.h:40
A simulated web server that processes at most one request at a time.
Definition WebServer.h:26
unsigned int remainingCycles() const
Definition WebServer.cpp:67
void setRetiring(bool retiring)
Marks or unmarks the server for retirement.
Definition WebServer.cpp:26
unsigned int id() const
Definition WebServer.cpp:14
Counters accumulated over a whole run.
unsigned long long serverCycles
Sum over cycles of live servers (capacity offered).
A single web request waiting for, or receiving, service.
Definition Request.h:41
unsigned int taskCycles
Clock cycles of processing required (always at least 1).
Definition Request.h:45
unsigned long arrivalCycle
Clock cycle at which the request was generated.
Definition Request.h:46
JobType jobType
Processing ('P') or streaming ('S') work.
Definition Request.h:47
std::string destinationIp
IPv4 address the response is returned to.
Definition Request.h:44
unsigned long id
Sequential identifier assigned by the generator.
Definition Request.h:42
std::string sourceIp
IPv4 address of the client sending the request.
Definition Request.h:43
Kind
What happened to the server.
@ Retiring
A busy server was marked to be removed after its current request.
@ Removed
An idle server was removed immediately.
@ Reactivated
A retiring server was returned to service instead of creating one.
@ Retired
A retiring server finished its request and was removed.
@ Added
A new server was created.
Rules for growing and shrinking the server pool.
Point-in-time picture of the load balancer, recorded at regular intervals.
unsigned long rejected
Requests blocked by the firewall so far.
std::size_t liveServers
All servers, including retiring ones.
std::size_t queueSize
Requests waiting in the queue.
unsigned long completed
Requests completed so far.
std::size_t idleServers
Servers with no request (inactive).
std::size_t busyServers
Servers processing a request.
unsigned long cycle
Clock cycle of the snapshot.
std::size_t retiringServers
Servers finishing work before removal.