20const char*
const kRule =
"==============================================================================";
21const char*
const kThinRule =
"------------------------------------------------------------------------------";
23std::string fixed(
double value,
int precision) {
24 std::ostringstream text;
25 text << std::fixed << std::setprecision(precision) << value;
29void heading(std::ostream& out,
const std::string& title) {
30 out <<
'\n' << kThinRule <<
'\n' << title <<
'\n' << kThinRule <<
'\n';
33void row(std::ostream& out,
const std::string& label,
const std::string& value) {
34 out <<
" " << std::left << std::setw(46) << label << value <<
'\n';
82 generator_(buildGeneratorSettings(config), config.seed),
83 statusEvery_(config.statusEvery),
84 snapshotEvery_(std::max<unsigned long>(1, config.cycles / 20)) {
85 if (statusEvery_ == 0) {
86 statusEvery_ = config.
verbose ? 100 : std::max<unsigned long>(1, config.
cycles / 20);
88 const Firewall firewall = buildFirewall(config);
92 std::size_t streamingServers =
static_cast<std::size_t
>(std::lround(config.
servers * config.
streamingShare));
93 streamingServers = std::clamp<std::size_t>(streamingServers, 1, config.
servers - 1);
94 const std::size_t processingServers = config.
servers - streamingServers;
95 switch_ = std::make_unique<Switch>(
96 std::make_unique<LoadBalancer>(
"LB-P", processingServers, policy, firewall, config.
maxQueue, logger),
97 std::make_unique<LoadBalancer>(
"LB-S", streamingServers, policy, firewall, config.
maxQueue, logger));
99 single_ = std::make_unique<LoadBalancer>(
"LB", config.
servers, policy, firewall, config.
maxQueue, logger);
104 const auto started = std::chrono::steady_clock::now();
105 const std::size_t initialCount =
static_cast<std::size_t
>(config_.servers) * config_.queuePerServer;
107 "generating initial queue of " + std::to_string(initialCount) +
" requests (seed " +
108 std::to_string(config_.seed) +
")");
109 for (
const Request& request : generator_.makeInitialBatch(initialCount, 0)) {
111 switch_->route(request, 0);
113 single_->admit(request, 0);
122 balancer->recordSnapshot(0);
126 for (
unsigned long cycle = 1; cycle <= config_.cycles; ++cycle) {
127 const std::vector<Request> arrivals = generator_.arrivalsFor(cycle);
129 switch_->processCycle(cycle, arrivals);
131 single_->processCycle(cycle, arrivals);
133 const bool snapshotDue = cycle % snapshotEvery_ == 0 || cycle == config_.cycles;
134 const bool statusDue = cycle % statusEvery_ == 0 || cycle == config_.cycles;
137 balancer->recordSnapshot(cycle);
144 runSeconds_ = std::chrono::duration<double>(std::chrono::steady_clock::now() - started).count();
149 return {&switch_->processing(), &switch_->streaming()};
151 return {single_.get()};
154std::vector<LoadBalancer*> Simulation::mutableLoadBalancers() {
156 return {&switch_->processing(), &switch_->streaming()};
158 return {single_.get()};
165void Simulation::writeSettings(std::ostream& out)
const {
166 heading(out,
"1. RUN SETTINGS (user input)");
167 row(out,
"Initial web servers", std::to_string(config_.
servers));
168 row(out,
"Clock cycles simulated", std::to_string(config_.
cycles));
169 row(out,
"Output mode", config_.
verbose ?
"verbose" :
"quiet");
170 row(out,
"Routing", config_.
useSwitch ?
"Switch: P jobs -> LB-P, S jobs -> LB-S (bonus)" :
"single load balancer");
171 row(out,
"Random seed", std::to_string(config_.
seed));
172 row(out,
"Task time range (uniform)", std::to_string(config_.
minTask) +
" to " + std::to_string(config_.
maxTask) +
" cycles");
173 row(out,
"Arrivals per cycle",
"Poisson, average " + fixed(config_.
arrivalRate, 2));
178 std::to_string(wavePeriod) +
" cycles");
180 row(out,
"Traffic wave",
"off (steady rate)");
182 row(out,
"Streaming (S) job share", fixed(config_.streamingShare * 100.0, 0) +
"%");
183 row(out,
"Initial queue", std::to_string(config_.queuePerServer) +
" x servers = " +
184 std::to_string(
static_cast<unsigned long>(config_.servers) * config_.queuePerServer) +
186 row(out,
"Scale up when",
"queue > " + fixed(config_.upperFactor, 0) +
" x active servers");
187 row(out,
"Scale down when",
"queue < " + fixed(config_.lowerFactor, 0) +
" x active servers");
188 row(out,
"Scaling check interval",
"every " + std::to_string(config_.cooldown) +
" cycles, one server at a time");
189 row(out,
"Server limits", std::to_string(config_.minServers) +
" to " + std::to_string(config_.maxServers));
190 row(out,
"Queue capacity", config_.maxQueue == 0 ?
"unlimited" : std::to_string(config_.maxQueue));
192 for (
const std::string& range : config_.blockedRanges) {
193 ranges += (ranges.empty() ?
"" :
", ") + range;
195 row(out,
"Firewall blocked source ranges", ranges.empty() ?
"none" : ranges);
198void Simulation::writeRandomness(std::ostream& out)
const {
199 const GeneratorStats& stats = generator_.stats();
200 heading(out,
"3. RANDOMNESS IN REQUEST GENERATION");
201 row(out,
"Requests generated", std::to_string(stats.
total) +
" (" + std::to_string(stats.
initial) +
202 " initial + " + std::to_string(stats.
total - stats.
initial) +
" during run)");
203 const double total = stats.
total == 0 ? 1.0 :
static_cast<double>(stats.
total);
204 row(out,
"Processing (P) / Streaming (S) jobs",
205 std::to_string(stats.
processing) +
" (" + fixed(100.0 * stats.
processing / total, 1) +
"%) / " +
206 std::to_string(stats.
streaming) +
" (" + fixed(100.0 * stats.
streaming / total, 1) +
"%)");
207 row(out,
"Observed task time min / mean / max",
208 std::to_string(stats.
minTaskSeen) +
" / " + fixed(
static_cast<double>(stats.
taskCycleSum) / total, 1) +
" / " +
210 const double runCycles = config_.cycles == 0 ? 1.0 :
static_cast<double>(config_.cycles);
211 row(out,
"Observed arrivals per cycle (mean / max)",
214 out <<
"\n Task time histogram (cycles)\n";
215 const GeneratorSettings& settings = generator_.settings();
225 const std::size_t bar = largest == 0 ? 0 :
static_cast<std::size_t
>(30.0 * count / largest + 0.5);
226 std::ostringstream label;
227 label << std::setw(4) << low <<
"-" << std::left << std::setw(4) << high;
228 out <<
" " << label.str() <<
" " << std::right << std::setw(7) << count <<
" " << std::string(bar,
'#') <<
'\n';
231 out <<
"\n New arrivals per cycle (Poisson)\n";
235 out <<
" " << std::left << std::setw(3) << labels[bin] << std::right << std::setw(8) << count
236 <<
" cycles (" << fixed(100.0 * count / runCycles, 2) <<
"%)\n";
239 out <<
"\n First generated requests (sample)\n";
240 out <<
" " << std::left << std::setw(6) <<
"id" << std::setw(17) <<
"source IP" << std::setw(17)
241 <<
"destination IP" << std::setw(6) <<
"type" <<
"task cycles\n";
242 for (
const Request& request : generator_.samples()) {
243 out <<
" " << std::left << std::setw(6) << request.id << std::setw(17) << request.sourceIp
244 << std::setw(17) << request.destinationIp << std::setw(6) <<
jobTypeCode(request.jobType)
245 << request.taskCycles <<
'\n';
249void Simulation::writeSnapshots(std::ostream& out,
const LoadBalancer& balancer)
const {
250 out <<
"\n " << balancer.
name() <<
" status every " << snapshotEvery_ <<
" cycles\n";
251 out <<
" " << std::right << std::setw(7) <<
"cycle" << std::setw(8) <<
"queue" << std::setw(8) <<
"live"
252 << std::setw(7) <<
"busy" << std::setw(7) <<
"idle" << std::setw(7) <<
"retire" << std::setw(11)
253 <<
"completed" << std::setw(10) <<
"rejected" <<
'\n';
254 for (
const Snapshot& snapshot : balancer.
snapshots()) {
255 out <<
" " << std::setw(7) << snapshot.cycle << std::setw(8) << snapshot.queueSize << std::setw(8)
256 << snapshot.liveServers << std::setw(7) << snapshot.busyServers << std::setw(7) << snapshot.idleServers
257 << std::setw(7) << snapshot.retiringServers << std::setw(11) << snapshot.completed << std::setw(10)
258 << snapshot.rejected <<
'\n';
262void Simulation::writeScaleTimeline(std::ostream& out,
const LoadBalancer& balancer)
const {
263 const std::vector<ScaleEvent>& events = balancer.
scaleEvents();
264 out <<
"\n " << balancer.
name() <<
": " << events.size() <<
" server pool changes\n";
265 if (events.empty()) {
269 const std::size_t head = events.size() <= 500 ? events.size() : 300;
270 const std::size_t tail = events.size() <= 500 ? 0 : 100;
271 const auto print = [&out](
const ScaleEvent& event) {
272 out <<
" t=" << std::left << std::setw(8) <<
event.cycle << std::setw(17) << kindName(event.kind)
273 <<
"server " << std::setw(5) <<
event.serverId <<
"queue " << std::setw(7) <<
event.queueSize
274 <<
"active servers now " <<
event.activeServers <<
'\n';
276 for (std::size_t i = 0; i < events.size() && i < head; ++i) {
279 if (events.size() > head + tail) {
280 out <<
" ... " << (events.size() - head - tail)
281 <<
" more changes omitted (see the status table above or run with --verbose) ...\n";
283 for (std::size_t i = std::max(head, events.size() > tail ? events.size() - tail : 0); i < events.size(); ++i) {
289 const std::time_t now = std::time(
nullptr);
291 std::strftime(stamp,
sizeof(stamp),
"%Y-%m-%d %H:%M:%S", std::localtime(&now));
294 <<
" AGGIE STUDENT TOOLKIT - LOAD BALANCER LOG SUMMARY\n"
295 <<
" CSCE 412 Project 4 | Arnav Fotedar | generated " << stamp <<
'\n'
300 const std::vector<const LoadBalancer*> balancers =
loadBalancers();
301 std::size_t startingQueue = 0;
302 std::size_t endingQueue = 0;
307 heading(out,
"2. REQUIRED SUMMARY");
308 row(out,
"Starting queue size", std::to_string(startingQueue) +
" requests (t=0, after firewall, before dispatch)");
309 row(out,
"Ending queue size", std::to_string(endingQueue) +
" requests (t=" + std::to_string(config_.cycles) +
")");
310 row(out,
"Range for task times", std::to_string(config_.minTask) +
" to " + std::to_string(config_.maxTask) +
313 writeRandomness(out);
315 heading(out,
"4. END STATUS AND STATISTICS");
317 row(out,
"Requests routed to LB-P / LB-S",
318 std::to_string(switch_->routedProcessing()) +
" / " + std::to_string(switch_->routedStreaming()));
325 row(out,
"Simulation wall-clock time", fixed(runSeconds_, 2) +
" s");
327 heading(out,
"5. SERVERS AT THE END OF THE RUN");
329 out <<
" " << balancer->
name() <<
'\n';
330 out <<
" " << std::left << std::setw(8) <<
"server" << std::setw(10) <<
"state" << std::setw(26)
331 <<
"current request" << std::setw(11) <<
"remaining" << std::setw(11) <<
"completed"
334 std::string state = server.isIdle() ?
"idle" : (server.isRetiring() ?
"retiring" :
"busy");
335 std::string current =
"-";
336 if (server.current()) {
337 current =
"req#" + std::to_string(server.current()->id) +
" (" +
338 std::string(1,
jobTypeCode(server.current()->jobType)) +
", " +
339 std::to_string(server.current()->taskCycles) +
" cyc)";
341 out <<
" " << std::left << std::setw(8) << server.id() << std::setw(10) << state << std::setw(26)
342 << current << std::setw(11) << server.remainingCycles() << std::setw(11) << server.completedCount()
343 <<
"t=" << server.createdCycle() <<
'\n';
348 heading(out,
"6. DYNAMIC SERVER COUNT AND QUEUE OVER TIME");
350 writeSnapshots(out, *balancer);
353 heading(out,
"7. SCALING TIMELINE (server pool changes)");
355 writeScaleTimeline(out, *balancer);
357 out <<
'\n' << kRule <<
"\n END OF LOG\n" << kRule <<
'\n';
@ Info
General information such as start-up messages.
@ Status
Periodic queue and server status line.
char jobTypeCode(JobType type)
Returns the one-letter code for a job type ('P' or 'S').
Declares the Simulation class, which drives the clock and writes the run log.
Rejects requests whose source address falls in a blocked IPv4 range.
bool addRange(const std::string &cidr)
Adds a blocked range.
Distributes queued requests across a dynamically sized pool of WebServer objects.
std::size_t queueSize() const
const std::string & name() const
const LoadBalancerStats & stats() const
void summarize(std::ostream &out) const
Writes the end-of-run summary for this load balancer.
const std::vector< Snapshot > & snapshots() const
const std::vector< ScaleEvent > & scaleEvents() const
const std::vector< WebServer > & servers() const
Writes simulation events to the console and, optionally, to a file.
Produces random requests from a single seeded Mersenne Twister engine.
const GeneratorSettings & settings() const
void writeReport(std::ostream &out) const
Writes the full log summary: settings, randomness, results, snapshots and scaling timeline.
std::vector< const LoadBalancer * > loadBalancers() const
Simulation(const Config &config, Logger &logger)
Builds the components described by a validated configuration.
const RequestGenerator & generator() const
void run()
Runs every clock cycle of the simulation.
A simulated web server that processes at most one request at a time.
Every user-adjustable setting for one simulation run.
double arrivalRate
Average new requests per cycle.
double lowerFactor
Scale down when queue < lowerFactor x servers.
unsigned int servers
Initial number of web servers.
bool useSwitch
Bonus: route P and S jobs to two load balancers.
unsigned long cycles
Clock cycles to simulate.
unsigned int maxTask
Longest task time, in cycles.
bool verbose
Print every event instead of only status lines.
double waveAmplitude
Traffic wave size, 0 (steady) to 1.
double streamingShare
Fraction of requests that are streaming ('S') jobs.
std::uint32_t seed
Random seed (logged so a run can be repeated).
unsigned int cooldown
Cycles between scaling checks.
std::size_t maxQueue
Queue capacity; 0 means unlimited.
unsigned int minServers
Lower bound on active servers.
unsigned int minTask
Shortest task time, in cycles.
double upperFactor
Scale up when queue > upperFactor x servers.
std::vector< std::string > blockedRanges
Firewall CIDR ranges.
bool autoWavePeriod
Use cycles / 4 (four busy/quiet waves) unless –wave-period is given.
unsigned int maxServers
Upper bound on live servers.
Tunable parameters for random request generation.
unsigned int minTaskCycles
Shortest possible task, in clock cycles.
double waveAmplitude
Traffic wave size: the rate swings between (1 - a) and (1 + a) x arrivalRate.
unsigned long wavePeriod
Cycles in one busy-to-quiet traffic wave; 0 disables the wave.
double arrivalRate
Average new requests per clock cycle (Poisson mean).
double streamingShare
Probability that a request is a streaming ('S') job.
unsigned int maxTaskCycles
Longest possible task, in clock cycles.
static constexpr std::size_t kArrivalBins
Arrival-count buckets: 0, 1, 2, 3, 4+.
unsigned long streaming
Generated requests with JobType::Streaming.
unsigned int minTaskSeen
Shortest task actually generated.
std::array< unsigned long, kTaskBins > taskHistogram
Task lengths split into equal-width bins.
unsigned long initial
Requests generated for the initial queue.
unsigned int maxTaskSeen
Longest task actually generated.
std::array< unsigned long, kArrivalBins > arrivalHistogram
Cycles with 0, 1, 2, 3 and 4+ arrivals.
static constexpr std::size_t kTaskBins
Number of task-length histogram bins.
unsigned long maxArrivalsInCycle
Largest burst generated in a single cycle.
unsigned long processing
Generated requests with JobType::Processing.
unsigned long total
Requests generated, including the initial queue.
unsigned long long taskCycleSum
Sum of all task lengths, for the mean.
std::size_t startingQueue
Queue size at time 0, before the first dispatch.
A single web request waiting for, or receiving, service.
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.
unsigned int maxServers
Never more live servers (active + retiring) than this.
double lowerFactor
Scale down when queue < lowerFactor x active servers.
unsigned int cooldownCycles
Cycles between scaling evaluations.
unsigned int minServers
Never fewer active servers than this.
double upperFactor
Scale up when queue > upperFactor x active servers.