$limit Rule limit: {writes: int, window: seconds}. * @param DateTimeImmutable $now Reference time. * @return bool True when within the limit; false = LIMIT_EXCEEDED. */ public function check( int $agent_id, array $limit, DateTimeImmutable $now ): bool { $writes = isset( $limit['writes'] ) ? (int) $limit['writes'] : 0; $window = isset( $limit['window'] ) ? (int) $limit['window'] : 3600; if ( $writes <= 0 || $window <= 0 ) { return true; } $since = $now->modify( sprintf( '-%d seconds', $window ) ); return $this->ledger->count_writes_since( $agent_id, $since ) < $writes; } /** * Seconds until the window frees a slot. * * The window length is an upper bound and usually a poor one: with an hour * window and the oldest write 59 minutes old, "wait an hour" wastes 59 * minutes of it. A sliding window frees a slot when its oldest counted * write ages out, so that is what the agent is told. Never less than one * second - a hint of "retry now" would invite the loop the denial message * exists to prevent. * * @param int $agent_id Acting agent. * @param array $limit Rule limit. * @param DateTimeImmutable $now Reference time. */ public function retry_after( int $agent_id, array $limit, DateTimeImmutable $now ): int { $window = isset( $limit['window'] ) ? (int) $limit['window'] : 3600; if ( $window < 1 ) { $window = 3600; } $oldest = $this->ledger->oldest_write_since( $agent_id, $now->modify( sprintf( '-%d seconds', $window ) ) ); if ( null === $oldest ) { return $window; } return max( 1, $oldest->getTimestamp() + $window - $now->getTimestamp() ); } }