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");
21 for (std::size_t i = 0; i < initialServers; ++i) {
22 servers_.emplace_back(nextServerId_++, 0);
24 stats_.initialServers = initialServers;
25 stats_.minActiveServers = initialServers;
26 stats_.peakLiveServers = initialServers;
29std::string LoadBalancer::describe(
const Request& request)
const {
30 std::ostringstream text;
38 if (firewall_.isBlocked(request.
sourceIp)) {
43 if (maxQueue_ != 0 && pending_.size() >= maxQueue_) {
48 pending_.push(request);
50 if (pending_.size() > stats_.peakQueue) {
51 stats_.peakQueue = pending_.size();
52 stats_.peakQueueCycle = cycle;
56 describe(request) +
" queued, queue=" + std::to_string(pending_.size()));
62 stats_.startingQueue = pending_.size();
64 "starting with " + std::to_string(servers_.size()) +
" servers and " +
65 std::to_string(pending_.size()) +
" queued requests");
67 stats_.queueAfterFirstDispatch = pending_.size();
72 for (
const Request& request : arrivals) {
73 admit(request, cycle);
75 adjustCapacity(cycle);
77 updateExtremes(cycle);
80void LoadBalancer::completeWork(
unsigned long cycle) {
83 if (server.isIdle()) {
90 "server " + std::to_string(server.id()) +
" finished " +
91 describe(server.lastCompleted()));
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);
106void LoadBalancer::adjustCapacity(
unsigned long cycle) {
107 if (policy_.cooldownCycles == 0 || cycle % policy_.cooldownCycles != 0) {
110 const double queue =
static_cast<double>(pending_.size());
113 if (queue > policy_.upperFactor *
static_cast<double>(active)) {
115 for (WebServer& server : servers_) {
116 if (server.isRetiring()) {
117 server.setRetiring(
false);
123 if (servers_.size() < policy_.maxServers) {
129 if (queue < policy_.lowerFactor *
static_cast<double>(active) && active > policy_.minServers) {
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());
141 WebServer* candidate =
nullptr;
142 for (WebServer& server : servers_) {
143 if (!server.isRetiring() &&
144 (candidate ==
nullptr || server.remainingCycles() < candidate->
remainingCycles())) {
148 if (candidate !=
nullptr) {
156void LoadBalancer::addServer(
unsigned long cycle) {
157 servers_.emplace_back(nextServerId_++, cycle);
162void LoadBalancer::recordScale(
ScaleEvent::Kind kind,
unsigned long cycle,
unsigned int serverId) {
166 event.serverId = serverId;
167 event.queueSize = pending_.size();
169 scaleEvents_.push_back(event);
175 action =
"added server ";
179 action =
"reactivated retiring server ";
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());
192void LoadBalancer::dispatch(
unsigned long cycle) {
193 for (WebServer& server : servers_) {
194 if (pending_.empty()) {
197 if (!server.isIdle() || server.isRetiring()) {
200 const Request request = pending_.front();
202 const unsigned long wait = cycle - request.
arrivalCycle;
203 stats_.totalWait += wait;
204 stats_.maxWait = std::max(stats_.maxWait, wait);
206 server.assign(request, cycle);
208 describe(request) +
" -> server " + std::to_string(server.id()) +
" after waiting " +
209 std::to_string(wait) +
" cycles");
213void LoadBalancer::updateExtremes(
unsigned long ) {
214 stats_.minActiveServers = std::min(stats_.minActiveServers,
activeServers());
215 stats_.peakLiveServers = std::max(stats_.peakLiveServers, servers_.size());
220 snapshot.
cycle = cycle;
227 snapshot.
rejected = stats_.rejected;
228 snapshots_.push_back(snapshot);
232 std::ostringstream text;
233 text <<
"queue=" << std::setw(5) << pending_.size() <<
" | servers=" << std::setw(3) << servers_.size()
235 <<
") | completed=" << stats_.completed <<
" rejected=" << stats_.rejected
236 <<
" discarded=" << stats_.discarded <<
" | scale +" << stats_.scaleUps <<
"/-" << stats_.scaleDowns;
241 const auto row = [&out](
const std::string& label,
const std::string& value) {
242 out <<
" " << std::left << std::setw(46) << label << value <<
'\n';
244 const auto fixed = [](
double value,
int precision) {
245 std::ostringstream text;
246 text << std::fixed << std::setprecision(precision) << value;
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) +
")");
257 out <<
" -- Servers --\n";
258 row(
"Initial servers", std::to_string(stats_.initialServers));
259 row(
"Final live servers", std::to_string(servers_.size()));
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) +
"%");
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");
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"));
304 return static_cast<std::size_t
>(std::count_if(servers_.begin(), servers_.end(),
305 [](
const WebServer& s) { return !s.isRetiring(); }));
309 return static_cast<std::size_t
>(std::count_if(servers_.begin(), servers_.end(),
310 [](
const WebServer& s) { return !s.isIdle(); }));
316 return static_cast<std::size_t
>(std::count_if(servers_.begin(), servers_.end(),
317 [](
const WebServer& s) { return s.isRetiring(); }));
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.
@ Assign
A queued request was dispatched to a server.
@ Info
General information such as start-up messages.
@ Arrival
A request entered the queue.
@ ScaleDown
A server was removed or marked for retirement.
@ Complete
A server finished a request.
@ Discard
The queue was full and a request was dropped.
@ Reject
The firewall blocked a request.
@ ScaleUp
A server was added or returned to service.
char jobTypeCode(JobType type)
Returns the one-letter code for a job type ('P' or 'S').
Rejects requests whose source address falls in a blocked IPv4 range.
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.
A simulated web server that processes at most one request at a time.
unsigned int remainingCycles() const
void setRetiring(bool retiring)
Marks or unmarks the server for retirement.
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.
unsigned int taskCycles
Clock cycles of processing required (always at least 1).
unsigned long arrivalCycle
Clock cycle at which the request was generated.
JobType jobType
Processing ('P') or streaming ('S') work.
std::string destinationIp
IPv4 address the response is returned to.
unsigned long id
Sequential identifier assigned by the generator.
std::string sourceIp
IPv4 address of the client sending the request.
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.