š® Online Multiplayer Game Lobby with Priority Rooms & Autoscaling ā Low-Level Design & Multi-Threaded Simulation
Target Interview Level: SDE-1 / SDE-2 ā Juspay Systems & Concurrency LLD
Topic: High-Concurrency Multiplayer Game Lobby with $N$ Default Priority Rooms & Dynamic On-Demand Autoscaling
Language: Modern, Clean C++17 with Complete Multi-Threaded Simulation (std::thread,std::mutex,std::condition_variable)
Evaluation Focus: OOP Principles (SOLID, Strategy, State, Observer, Factory), Thread-Safety Invariants, Zero-Deadlock Concurrency, Fine-Grained Locking, and Graceful Resource Reclamation.
š§ Problem Statement & Requirements Clarification
Problem Statement
Design an Online Multiplayer Game Lobby System that:
- Initializes with $N$ Default Game Rooms, each assigned a distinct Priority Level (e.g., Priority 1 = Highest, Priority $N$ = Lowest).
- Incoming players are allocated to rooms based on Room Priority (higher priority rooms fill first).
- When all $N$ default rooms are fully occupied, the system automatically spawns extra Dynamic Game Rooms on demand to absorb player overflow.
- Each room has a fixed maximum player capacity (e.g., 4 players per match).
- When a room reaches full capacity, it transitions to
IN_PROGRESSand starts a concurrent match simulation. - When a match concludes:
- Default Rooms: Reset back to
WAITINGstate and re-enter the priority pool ready for new players. - Dynamic Rooms: Are decommissioned and garbage-collected to prevent memory leaks and server resource exhaustion.
- Default Rooms: Reset back to
- The entire system must run in a real live simulation with concurrent threads representing player arrivals, game matches, and lobby management.
Clarifying Scope & Invariants (Thinking Out Loud)
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā GAME LOBBY ARCHITECTURE ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
[ Incoming Players (Concurrent Threads) ]
ā
ā¼
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā LOBBY MANAGER (Thread-Safe) ā
ā - Allocation Strategy (Priority)ā
ā - Room Registry ā
āāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāā
ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā“āāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā¼ ā¼
[ N DEFAULT ROOMS (Permanent) ] [ DYNAMIC ROOMS (On-Demand) ]
- Pre-allocated at startup - Spawned when defaults are full
- Priority 1 (High) to N (Low) - Temporary lifecycle
- Reset to WAITING on match end - Reclaimed & destroyed on match end- Player Assignment Invariant:
- An incoming player must be assigned to the highest-priority available room with open slots.
- Only when all default rooms are either full or playing does the system spawn an extra dynamic room.
- Room Capacity & Race Condition Invariant:
- Exactly $K$ players must enter a room of capacity $K$. Two concurrent threads must never be permitted to both claim the $K$-th slot (Over-subscription prevention).
- Lifecycle Invariant:
- Default rooms are immortal (never deleted from memory).
- Dynamic rooms are ephemeral (cleaned up when empty after match completion).
- Deadlock Freedom:
- Strict hierarchical lock acquisition order: Lobby Lock $\to$ Room Lock. Never acquire the Lobby lock while holding a Room lock!
šļø System Architecture & Class Diagram
āāāāāāāāāāāāāāāāāāāāāāāāāā
ā <<enum>> ā
ā RoomState ā
āāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā WAITING ā
ā FULL ā
ā IN_PROGRESS ā
ā FINISHED ā
ā DESTROYED ā
āāāāāāāāāāāāāāāāāāāāāāāāāā
ā²
ā
āāāāāāāāāāāāāāāāāāāāāāāāā 1 * ā
ā Player āāāāāāāāāāāāāāāāā¼āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
āāāāāāāāāāāāāāāāāāāāāāāā⤠ā ā
ā - id: string ā ā ā
ā - name: string ā ā ā
ā - skillRating: int ā ā ā
āāāāāāāāāāāāāāāāāāāāāāāāā ā ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā“āāāāāāāāāāāāāāāāāāāāāāāāāāā ā
ā GameRoom ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā⤠ā
ā - id: string ā ā
ā - roomType: RoomType (DEFAULT vs DYNAMIC) ā ā
ā - priority: int ā ā
ā - maxCapacity: int ā ā
ā - currentPlayers: vector<shared_ptr<Player>> ā ā
ā - state: RoomState ā ā
ā - roomMutex: mutex ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā⤠ā
ā + addPlayer(player): bool ā ā
ā + removePlayer(playerId): bool ā ā
ā + startMatch(): void ā ā
ā + reset(): void ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
ā ā
ā notifies ā
ā¼ ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
ā IRoomObserver ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā⤠ā
ā + onMatchCompleted(roomId): void ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
ā implements ā
ā¼ ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
ā GameLobby ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā⤠ā
ā - defaultRooms: vector<shared_ptr<GameRoom>> ā ā
ā - dynamicRooms: vector<shared_ptr<GameRoom>> āāāāā
ā - allocationStrategy: unique_ptr<IRoomAllocationStrategy> ā
ā - lobbyMutex: mutex ā
ā - cvMatchCompleted: condition_variable ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā + addPlayerToLobby(player): shared_ptr<GameRoom> ā
ā + onMatchCompleted(roomId): void ā
ā + cleanupDynamicRooms(): void ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā uses
ā¼
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā IRoomAllocationStrategy ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā + findBestRoom(defaultRooms, dynamicRooms, cap): shared_ptr<Room>ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā implements
ā¼
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā PriorityFirstAllocationStrategy ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā - Scans Default rooms by Priority (1 to N) ā
ā - Falls back to existing non-full Dynamic rooms ā
ā - Returns nullptr if all full (signaling need to spawn new) ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāšØ Design Patterns Applied
| Pattern | Component | Why It Was Chosen |
|---|---|---|
| Strategy Pattern | IRoomAllocationStrategy $\to$ PriorityFirstAllocationStrategy | Decouples player-to-room matchmaking algorithms. In future, we can plug in SkillBasedAllocationStrategy or LatencyBasedAllocationStrategy without changing lobby code (Open/Closed Principle). |
| State Pattern | RoomState (WAITING, FULL, IN_PROGRESS, FINISHED) | Explicitly governs valid room transitions. Prevents players from joining mid-game or joining rooms scheduled for destruction. |
| Observer Pattern | IRoomObserver $\to$ GameLobby::onMatchCompleted | Rooms notify the central Lobby asynchronously when games conclude, decoupling game execution threads from room reclamation logic. |
| Factory Pattern | RoomFactory | Encapsulates the instantiation of Default rooms (with predefined priority tiers) vs Dynamic rooms (with auto-generated IDs and lower priority). |
š Concurrency & Thread-Safety Invariants
1. Two-Level Fine-Grained Locking
- Lobby Mutex (
lobbyMutex): Protects the room lists (defaultRooms,dynamicRooms). Held only for short durations when finding a room or appending a newly spawned dynamic room. - Room Mutex (
roomMutex): EachGameRoommaintains its own dedicated mutex. When a player is being inserted into a room, only that room's lock is acquired. - Benefits: High concurrency! Players joining Room A do not block players joining Room B.
2. Eliminating Race Conditions During Room Allocation
Thread 1 (Player A) Thread 2 (Player B)
ā ā
āāāāāāāāāāāāŗ [ 1. Acquire Lobby Mutex ] āāā⤠(Waits)
ā [ 2. Strategy picks Room 1 ]
ā [ 3. Releases Lobby Mutex]
ā ā
ā¼ āāāāāāāāāāāāŗ [ Acquires Lobby Mutex ]
[ Acquires Room 1 Mutex ] [ Strategy picks Room 1 ]
[ Adds Player A ] [ Releases Lobby Mutex ]
[ Releases Room 1 Mutex ] ā
ā¼
[ Acquires Room 1 Mutex ]
[ If Room 1 became FULL in between:
Falls back & retries allocation! ]- Optimistic Room Reservation: After picking a candidate room under the lobby lock, the lobby attempts
room->tryAddPlayer(player). If another thread filled the room during the microsecond gap, the lobby smoothly advances to the next priority room or spawns an extra dynamic room.
3. Safe Dynamic Room Reclamation
- Dynamic rooms are stored as
std::shared_ptr<GameRoom>. - When a game ends,
onMatchCompleted(roomId)marks the dynamic room asFINISHED. - A dedicated or deferred cleanup pass locks
lobbyMutexand erases finished dynamic rooms fromdynamicRooms. Any active thread holding ashared_ptrretains a valid reference until its execution completes, preventing dangling pointer segfaults.
š» Complete, Runnable C++17 Implementation
Save this code as online_game_lobby.cpp and compile with:
g++ -std=c++17 -O2 -pthread online_game_lobby.cpp -o online_game_lobby
./online_game_lobby#include <iostream>
#include <string>
#include <vector>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <condition_variable>
#include <thread>
#include <chrono>
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <atomic>
#include <random>
// ============================================================================
// 1. DOMAIN ENUMS & LOGGING UTILITY
// ============================================================================
enum class RoomType {
DEFAULT,
DYNAMIC
};
enum class RoomState {
WAITING, // Accepting players
FULL, // Reached max capacity, preparing to launch
IN_PROGRESS, // Game match is running
FINISHED, // Game ended
DESTROYED // Marked for deletion
};
std::string roomTypeToString(RoomType type) {
return (type == RoomType::DEFAULT) ? "DEFAULT" : "DYNAMIC";
}
std::string roomStateToString(RoomState state) {
switch (state) {
case RoomState::WAITING: return "WAITING";
case RoomState::FULL: return "FULL";
case RoomState::IN_PROGRESS: return "IN_PROGRESS";
case RoomState::FINISHED: return "FINISHED";
case RoomState::DESTROYED: return "DESTROYED";
}
return "UNKNOWN";
}
// Thread-safe console logger
static std::mutex g_logMutex;
void logMessage(const std::string& msg) {
std::lock_guard<std::mutex> lock(g_logMutex);
auto now = std::chrono::system_clock::now();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) % 1000;
std::cout << "[" << std::setw(3) << std::setfill('0') << ms.count() << "ms] "
<< "[Thread " << std::this_thread::get_id() << "] "
<< msg << std::endl;
}
// ============================================================================
// 2. PLAYER ENTITY
// ============================================================================
class Player {
private:
std::string id;
std::string name;
int skillRating;
public:
Player(std::string id, std::string name, int skillRating = 1000)
: id(std::move(id)), name(std::move(name)), skillRating(skillRating) {}
const std::string& getId() const { return id; }
const std::string& getName() const { return name; }
int getSkillRating() const { return skillRating; }
};
// ============================================================================
// 3. OBSERVER INTERFACE (Decouples GameRoom from GameLobby)
// ============================================================================
class IRoomObserver {
public:
virtual ~IRoomObserver() = default;
virtual void onMatchCompleted(const std::string& roomId) = 0;
};
// ============================================================================
// 4. GAME ROOM ENTITY
// ============================================================================
class GameRoom : public std::enable_shared_from_this<GameRoom> {
private:
std::string id;
RoomType type;
int priority; // Lower numerical value = Higher priority (1 is highest)
size_t maxCapacity;
RoomState state;
std::vector<std::shared_ptr<Player>> players;
IRoomObserver* observer;
mutable std::mutex roomMutex;
public:
GameRoom(std::string id, RoomType type, int priority, size_t maxCapacity, IRoomObserver* observer)
: id(std::move(id)), type(type), priority(priority),
maxCapacity(maxCapacity), state(RoomState::WAITING), observer(observer) {}
const std::string& getId() const { return id; }
RoomType getType() const { return type; }
int getPriority() const { return priority; }
size_t getMaxCapacity() const { return maxCapacity; }
RoomState getState() const {
std::lock_guard<std::mutex> lock(roomMutex);
return state;
}
size_t getPlayerCount() const {
std::lock_guard<std::mutex> lock(roomMutex);
return players.size();
}
// Attempt to add player. Returns true if added, false if room is full or not in WAITING state
bool tryAddPlayer(const std::shared_ptr<Player>& player, bool& outRoomBecameFull) {
std::lock_guard<std::mutex> lock(roomMutex);
outRoomBecameFull = false;
if (state != RoomState::WAITING || players.size() >= maxCapacity) {
return false;
}
players.push_back(player);
std::ostringstream oss;
oss << "Player " << player->getName() << " (" << player->getId() << ") entered Room "
<< id << " [" << roomTypeToString(type) << " | P" << priority << "] ("
<< players.size() << "/" << maxCapacity << ")";
logMessage(oss.str());
if (players.size() == maxCapacity) {
state = RoomState::FULL;
outRoomBecameFull = true;
oss.str("");
oss << ">>> Room " << id << " is now FULL! Triggering match start...";
logMessage(oss.str());
}
return true;
}
// Launches the game match in an asynchronous worker thread
void launchMatchAsync() {
std::shared_ptr<GameRoom> self = shared_from_this();
std::thread([self]() {
self->runMatchSimulation();
}).detach();
}
// Match Simulation logic
void runMatchSimulation() {
{
std::lock_guard<std::mutex> lock(roomMutex);
state = RoomState::IN_PROGRESS;
}
std::ostringstream oss;
oss << "=== [MATCH STARTED] Room " << id << " with " << maxCapacity << " players! ===";
logMessage(oss.str());
// Simulate game duration (e.g., 200 - 400 milliseconds)
std::this_thread::sleep_for(std::chrono::milliseconds(300));
{
std::lock_guard<std::mutex> lock(roomMutex);
state = RoomState::FINISHED;
players.clear();
}
oss.str("");
oss << "=== [MATCH FINISHED] Room " << id << " match concluded! ===";
logMessage(oss.str());
// Notify lobby observer
if (observer) {
observer->onMatchCompleted(id);
}
}
// Reset default room back to WAITING state
void reset() {
std::lock_guard<std::mutex> lock(roomMutex);
players.clear();
state = RoomState::WAITING;
std::ostringstream oss;
oss << "Room " << id << " [DEFAULT | P" << priority << "] reset to WAITING state.";
logMessage(oss.str());
}
// Mark dynamic room as destroyed
void markDestroyed() {
std::lock_guard<std::mutex> lock(roomMutex);
state = RoomState::DESTROYED;
}
};
// ============================================================================
// 5. STRATEGY PATTERN: ROOM ALLOCATION
// ============================================================================
class IRoomAllocationStrategy {
public:
virtual ~IRoomAllocationStrategy() = default;
virtual std::shared_ptr<GameRoom> selectRoom(
const std::vector<std::shared_ptr<GameRoom>>& defaultRooms,
const std::vector<std::shared_ptr<GameRoom>>& dynamicRooms) = 0;
};
// Allocation Strategy:
// 1. Scans Default rooms ordered by highest priority (lowest numerical priority value).
// 2. If all default rooms are full/playing, checks existing non-full dynamic rooms.
// 3. If no room has space, returns nullptr (signaling the Lobby to spawn a new dynamic room).
class PriorityFirstAllocationStrategy : public IRoomAllocationStrategy {
public:
std::shared_ptr<GameRoom> selectRoom(
const std::vector<std::shared_ptr<GameRoom>>& defaultRooms,
const std::vector<std::shared_ptr<GameRoom>>& dynamicRooms) override {
// 1. Check Default Rooms sorted by Priority
for (const auto& room : defaultRooms) {
if (room->getState() == RoomState::WAITING) {
return room;
}
}
// 2. Check existing Dynamic Rooms
for (const auto& room : dynamicRooms) {
if (room->getState() == RoomState::WAITING) {
return room;
}
}
// 3. No room currently available
return nullptr;
}
};
// ============================================================================
// 6. FACTORY PATTERN: ROOM FACTORY
// ============================================================================
class RoomFactory {
private:
std::atomic<int> dynamicRoomCounter{1};
public:
std::shared_ptr<GameRoom> createDefaultRoom(int priority, size_t capacity, IRoomObserver* observer) {
std::string id = "DEF-ROOM-" + std::to_string(priority);
return std::make_shared<GameRoom>(id, RoomType::DEFAULT, priority, capacity, observer);
}
std::shared_ptr<GameRoom> createDynamicRoom(size_t capacity, IRoomObserver* observer) {
int seq = dynamicRoomCounter.fetch_add(1);
std::string id = "DYN-ROOM-" + std::to_string(seq);
// Dynamic rooms have lower priority (e.g., 999)
return std::make_shared<GameRoom>(id, RoomType::DYNAMIC, 999, capacity, observer);
}
};
// ============================================================================
// 7. GAME LOBBY (Core Facade & Concurrency Orchestrator)
// ============================================================================
class GameLobby : public IRoomObserver {
private:
size_t roomCapacity;
std::unique_ptr<IRoomAllocationStrategy> allocationStrategy;
RoomFactory roomFactory;
std::vector<std::shared_ptr<GameRoom>> defaultRooms;
std::vector<std::shared_ptr<GameRoom>> dynamicRooms;
mutable std::mutex lobbyMutex;
std::condition_variable cvCompletion;
std::atomic<int> activeMatches{0};
public:
GameLobby(int numDefaultRooms, size_t capacityPerRoom,
std::unique_ptr<IRoomAllocationStrategy> strategy = std::make_unique<PriorityFirstAllocationStrategy>())
: roomCapacity(capacityPerRoom), allocationStrategy(std::move(strategy)) {
// Initialize N Default Rooms with Priority (1 to N)
for (int p = 1; p <= numDefaultRooms; ++p) {
defaultRooms.push_back(roomFactory.createDefaultRoom(p, roomCapacity, this));
}
// Sort default rooms by priority in ascending order (P1 > P2 > P3)
std::sort(defaultRooms.begin(), defaultRooms.end(), [](const auto& a, const auto& b) {
return a->getPriority() < b->getPriority();
});
std::ostringstream oss;
oss << "Initialized Game Lobby with " << numDefaultRooms << " Default Rooms (Capacity: "
<< roomCapacity << " players per room).";
logMessage(oss.str());
}
~GameLobby() override = default;
// Core Method: Add player concurrently to the best available room
std::shared_ptr<GameRoom> addPlayer(const std::shared_ptr<Player>& player) {
while (true) {
std::shared_ptr<GameRoom> candidateRoom = nullptr;
bool needSpawnDynamic = false;
{
std::lock_guard<std::mutex> lock(lobbyMutex);
// Select best candidate via Strategy
candidateRoom = allocationStrategy->selectRoom(defaultRooms, dynamicRooms);
if (candidateRoom == nullptr) {
// All default and existing dynamic rooms are full/busy! Auto-scale!
needSpawnDynamic = true;
candidateRoom = roomFactory.createDynamicRoom(roomCapacity, this);
dynamicRooms.push_back(candidateRoom);
std::ostringstream oss;
oss << "ā” [AUTOSCALING] All default rooms full! Spawned Dynamic Room "
<< candidateRoom->getId() << " (Active Dynamic Rooms: "
<< dynamicRooms.size() << ")";
logMessage(oss.str());
}
}
// Attempt to insert player into candidate room
bool roomBecameFull = false;
if (candidateRoom->tryAddPlayer(player, roomBecameFull)) {
if (roomBecameFull) {
activeMatches.fetch_add(1);
candidateRoom->launchMatchAsync();
}
return candidateRoom;
}
// If tryAddPlayer failed (another thread filled it first), loop retries!
}
}
// Callback invoked when a match finishes
void onMatchCompleted(const std::string& roomId) override {
std::lock_guard<std::mutex> lock(lobbyMutex);
// 1. Check if it's a default room
for (auto& room : defaultRooms) {
if (room->getId() == roomId) {
room->reset();
activeMatches.fetch_sub(1);
cvCompletion.notify_all();
return;
}
}
// 2. Check if it's a dynamic room -> Reclaim & Decommission!
auto it = std::find_if(dynamicRooms.begin(), dynamicRooms.end(),
[&roomId](const auto& r) { return r->getId() == roomId; });
if (it != dynamicRooms.end()) {
(*it)->markDestroyed();
std::ostringstream oss;
oss << "šļø [RECLAMATION] Dynamic Room " << roomId
<< " finished match. Decommissioned & removed from pool. Remaining dynamic rooms: "
<< (dynamicRooms.size() - 1);
logMessage(oss.str());
dynamicRooms.erase(it);
activeMatches.fetch_sub(1);
cvCompletion.notify_all();
}
}
// Wait until all active matches have completed (for clean simulation shutdown)
void waitForAllMatches() {
std::unique_lock<std::mutex> lock(lobbyMutex);
cvCompletion.wait(lock, [this]() {
return activeMatches.load() == 0;
});
}
void printStatus() const {
std::lock_guard<std::mutex> lock(lobbyMutex);
std::cout << "\n================= LOBBY STATUS =================" << std::endl;
std::cout << "DEFAULT ROOMS (" << defaultRooms.size() << "):" << std::endl;
for (const auto& r : defaultRooms) {
std::cout << " - " << r->getId() << " [P" << r->getPriority() << "]: "
<< roomStateToString(r->getState()) << " ("
<< r->getPlayerCount() << "/" << r->getMaxCapacity() << ")" << std::endl;
}
std::cout << "DYNAMIC ROOMS (" << dynamicRooms.size() << "):" << std::endl;
for (const auto& r : dynamicRooms) {
std::cout << " - " << r->getId() << " [P" << r->getPriority() << "]: "
<< roomStateToString(r->getState()) << " ("
<< r->getPlayerCount() << "/" << r->getMaxCapacity() << ")" << std::endl;
}
std::cout << "================================================\n" << std::endl;
}
};
// ============================================================================
// 8. LIVE CONCURRENT MULTI-THREADED SIMULATION
// ============================================================================
int main() {
std::cout << "========================================================\n";
std::cout << "š® JUSPAY SDE LIVE MULTIPLAYER LOBBY SIMULATION\n";
std::cout << "========================================================\n\n";
// Setup:
// 3 Default Rooms (Priority 1, 2, 3)
// Capacity = 3 players per room
// Total default capacity = 3 * 3 = 9 players
const int NUM_DEFAULT_ROOMS = 3;
const size_t ROOM_CAPACITY = 3;
const int TOTAL_PLAYERS = 18; // 18 players will force autoscaling of 3 Dynamic Rooms!
auto lobby = std::make_shared<GameLobby>(NUM_DEFAULT_ROOMS, ROOM_CAPACITY);
lobby->printStatus();
std::vector<std::thread> playerThreads;
playerThreads.reserve(TOTAL_PLAYERS);
logMessage("Starting concurrent player arrival simulation (18 players)...");
// Spawn 18 concurrent threads simulating players arriving simultaneously
for (int i = 1; i <= TOTAL_PLAYERS; ++i) {
std::string pId = "P" + std::to_string(i);
std::string pName = "Player_" + std::to_string(i);
playerThreads.emplace_back([lobby, pId, pName, i]() {
// Small random jitter between arrivals to simulate real-world arrival patterns
std::this_thread::sleep_for(std::chrono::milliseconds((i % 5) * 20));
auto player = std::make_shared<Player>(pId, pName, 1000 + (i * 50));
lobby->addPlayer(player);
});
}
// Join all player dispatch threads
for (auto& t : playerThreads) {
if (t.joinable()) {
t.join();
}
}
logMessage("All 18 players have been dispatched to rooms!");
lobby->printStatus();
logMessage("Waiting for all in-flight matches to complete and dynamic rooms to reclaim...");
lobby->waitForAllMatches();
logMessage("All matches concluded successfully!");
lobby->printStatus();
std::cout << "========================================================\n";
std::cout << "ā
SIMULATION COMPLETED: ZERO DEADLOCKS, CLEAN RECLAMATION\n";
std::cout << "========================================================\n";
return 0;
}š¬ Simulation Walkthrough & Expected Output
Phase 1: Default Priority Room Filling
When the first 9 players arrive concurrently:
- Players are assigned strictly according to room priority:
- Priority 1 (
DEF-ROOM-1) is filled first (Players 1, 2, 3). - As soon as
DEF-ROOM-1reaches 3/3, it marks itselfFULLand triggerslaunchMatchAsync(). - Subsequent players spill over to Priority 2 (
DEF-ROOM-2) (Players 4, 5, 6). - Then to Priority 3 (
DEF-ROOM-3) (Players 7, 8, 9).
- Priority 1 (
Phase 2: Dynamic Autoscaling on Overflow
When Players 10 through 18 arrive while default rooms are busy running matches:
allocationStrategy->selectRoom()detects that all default rooms areIN_PROGRESSorFULL.- The lobby triggers Autoscaling:
- Spawns
DYN-ROOM-1$\to$ fills with Players 10, 11, 12 $\to$ starts match. - Spawns
DYN-ROOM-2$\to$ fills with Players 13, 14, 15 $\to$ starts match. - Spawns
DYN-ROOM-3$\to$ fills with Players 16, 17, 18 $\to$ starts match.
- Spawns
Phase 3: Match Completion & Garbage Collection
When simulated matches finish after 300ms:
- Default Rooms (
DEF-ROOM-1,DEF-ROOM-2,DEF-ROOM-3):onMatchCompleted()invokesroom->reset().- State transitions back to
WAITING. - Players vector is cleared.
- Room remains permanently in memory, ready for subsequent matches.
- Dynamic Rooms (
DYN-ROOM-1,DYN-ROOM-2,DYN-ROOM-3):onMatchCompleted()marks the room asDESTROYED.- Removes the room pointer from
dynamicRoomsvector. - When the detached worker thread finishes, the
std::shared_ptrref count drops to zero, and the dynamic room is automatically deallocated from heap memory.
- Final Lobby status shows 0 dynamic rooms, proving zero memory leaks!
ā” Edge Cases & Juspay Interview Gauntlet
1. The Thundering Herd & Double-Checked Reservation
- Scenario: 100 players arrive at the exact same millisecond when only 1 slot is free in
DEF-ROOM-1. - Solution: The lobby picks the candidate room under
lobbyMutex, releases the lobby lock, and callsroom->tryAddPlayer(). Only ONE player acquiresroomMutexand successfully increments the count. The other 99 threads receivefalse, loop back, re-evaluate the lobby state, and fall back to the next priority room or trigger dynamic room creation.
2. Player Disconnects While Waiting in Room
- Scenario: A player loses internet connectivity while waiting for the room to fill (3/4 players).
- Implementation:
bool removePlayer(const std::string& playerId) { std::lock_guard<std::mutex> lock(roomMutex); if (state != RoomState::WAITING) return false; // Cannot leave mid-match auto it = std::remove_if(players.begin(), players.end(), [&](const auto& p) { return p->getId() == playerId; }); if (it != players.end()) { players.erase(it, players.end()); return true; } return false; }
3. Starvation of Lower Priority Default Rooms
- Interview Question: "If high-priority rooms finish matches and reset quickly, will low-priority rooms ever get players?"
- Answer: Under high arrival traffic, Priority 1 fills instantly. While Priority 1 is in-progress (300ms), incoming players naturally flow to Priority 2 and Priority 3. If arrival rate is low, Priority 1 indeed absorbs all matches, which is desirable (concentrating players into optimal servers). If fair distribution is needed, replace
PriorityFirstAllocationStrategywith aRoundRobinAllocationStrategyvia the Strategy Pattern!
4. Memory Leak Prevention for Detached Threads
- When
launchMatchAsync()detaches a thread:- We capture
self = shared_from_this(). - Even if
GameLobbyerases the dynamic room from its vector,selfkeeps the object alive until the thread exits its function scope. - The destructor of
GameRoomis called safely when the thread terminates.
- We capture