Module server.matchmaker.pop_timer

Classes

class PopTimer (queue: MatchmakerQueue)
Expand source code
@with_logger
class PopTimer(object):
    """ Calculates when the next pop should happen based on the rate of players
    queuing.

        timer = PopTimer(some_queue)
        # Pauses the coroutine until the next queue pop
        await timer.next_pop()

    The timer will adjust the pop times in an attempt to maintain a fixed queue
    size on each pop. So generally, the more people are in the queue, the
    shorter the time will be.

    The player queue rate is based on a moving average over the last few pops.
    The exact size can be set in config.
    """

    def __init__(self, queue: "MatchmakerQueue"):
        self.queue = queue
        # Set up deque's for calculating a moving average
        self.last_queue_amounts: deque[int] = deque(maxlen=config.QUEUE_POP_TIME_MOVING_AVG_SIZE)
        self.last_queue_times: deque[float] = deque(maxlen=config.QUEUE_POP_TIME_MOVING_AVG_SIZE)

        self._last_queue_pop = time()
        # Optimistically schedule first pop for half of the max pop time
        self.next_queue_pop = self._last_queue_pop + (config.QUEUE_POP_TIME_MAX / 2)
        self._wait_task = None

    async def next_pop(self):
        """ Wait for the timer to pop. """

        time_remaining = self.next_queue_pop - time()
        self._logger.info("Next %s wave happening in %is", self.queue.name, time_remaining)
        metrics.matchmaker_queue_pop.labels(self.queue.name).set(int(time_remaining))

        self._wait_task = asyncio.create_task(asyncio.sleep(time_remaining))
        await self._wait_task

        num_players = self.queue.num_players
        metrics.matchmaker_players.labels(self.queue.name).set(num_players)

        self._last_queue_pop = time()
        self.next_queue_pop = self._last_queue_pop + self.time_until_next_pop(
            num_players, time_remaining
        )

    def time_until_next_pop(self, num_queued: int, time_queued: float) -> float:
        """ Calculate how long we should wait for the next queue to pop based
        on the current rate of ladder queues
        """
        # Calculate moving average of player queue rate
        self.last_queue_amounts.append(num_queued)
        self.last_queue_times.append(time_queued)

        total_players = sum(self.last_queue_amounts)
        if total_players == 0:
            return config.QUEUE_POP_TIME_MAX

        total_times = sum(self.last_queue_times)
        if total_times:
            self._logger.debug(
                "Queue rate for %s: %f/s", self.queue.name,
                total_players / total_times
            )

        players_per_match = self.queue.team_size * 2
        desired_players = config.QUEUE_POP_DESIRED_MATCHES * players_per_match
        # Obtained by solving $ NUM_PLAYERS = rate * time $ for time.
        next_pop_time = desired_players * total_times / total_players
        if next_pop_time > config.QUEUE_POP_TIME_MAX:
            self._logger.info(
                "Required time (%.2fs) for %s is larger than max pop time (%ds). "
                "Consider increasing the max pop time",
                next_pop_time, self.queue.name, config.QUEUE_POP_TIME_MAX
            )
            return config.QUEUE_POP_TIME_MAX

        if next_pop_time < config.QUEUE_POP_TIME_MIN:
            self._logger.warning(
                "Required time (%.2fs) for %s is lower than min pop time (%ds). ",
                next_pop_time, self.queue.name, config.QUEUE_POP_TIME_MIN
            )
            return config.QUEUE_POP_TIME_MIN

        return next_pop_time

    def cancel(self):
        if self._wait_task:
            self._wait_task.cancel()

Calculates when the next pop should happen based on the rate of players queuing.

timer = PopTimer(some_queue)
# Pauses the coroutine until the next queue pop
await timer.next_pop()

The timer will adjust the pop times in an attempt to maintain a fixed queue size on each pop. So generally, the more people are in the queue, the shorter the time will be.

The player queue rate is based on a moving average over the last few pops. The exact size can be set in config.

Methods

def cancel(self)
Expand source code
def cancel(self):
    if self._wait_task:
        self._wait_task.cancel()
async def next_pop(self)
Expand source code
async def next_pop(self):
    """ Wait for the timer to pop. """

    time_remaining = self.next_queue_pop - time()
    self._logger.info("Next %s wave happening in %is", self.queue.name, time_remaining)
    metrics.matchmaker_queue_pop.labels(self.queue.name).set(int(time_remaining))

    self._wait_task = asyncio.create_task(asyncio.sleep(time_remaining))
    await self._wait_task

    num_players = self.queue.num_players
    metrics.matchmaker_players.labels(self.queue.name).set(num_players)

    self._last_queue_pop = time()
    self.next_queue_pop = self._last_queue_pop + self.time_until_next_pop(
        num_players, time_remaining
    )

Wait for the timer to pop.

def time_until_next_pop(self, num_queued: int, time_queued: float) ‑> float
Expand source code
def time_until_next_pop(self, num_queued: int, time_queued: float) -> float:
    """ Calculate how long we should wait for the next queue to pop based
    on the current rate of ladder queues
    """
    # Calculate moving average of player queue rate
    self.last_queue_amounts.append(num_queued)
    self.last_queue_times.append(time_queued)

    total_players = sum(self.last_queue_amounts)
    if total_players == 0:
        return config.QUEUE_POP_TIME_MAX

    total_times = sum(self.last_queue_times)
    if total_times:
        self._logger.debug(
            "Queue rate for %s: %f/s", self.queue.name,
            total_players / total_times
        )

    players_per_match = self.queue.team_size * 2
    desired_players = config.QUEUE_POP_DESIRED_MATCHES * players_per_match
    # Obtained by solving $ NUM_PLAYERS = rate * time $ for time.
    next_pop_time = desired_players * total_times / total_players
    if next_pop_time > config.QUEUE_POP_TIME_MAX:
        self._logger.info(
            "Required time (%.2fs) for %s is larger than max pop time (%ds). "
            "Consider increasing the max pop time",
            next_pop_time, self.queue.name, config.QUEUE_POP_TIME_MAX
        )
        return config.QUEUE_POP_TIME_MAX

    if next_pop_time < config.QUEUE_POP_TIME_MIN:
        self._logger.warning(
            "Required time (%.2fs) for %s is lower than min pop time (%ds). ",
            next_pop_time, self.queue.name, config.QUEUE_POP_TIME_MIN
        )
        return config.QUEUE_POP_TIME_MIN

    return next_pop_time

Calculate how long we should wait for the next queue to pop based on the current rate of ladder queues