Fix rate limit accuracy

Batching multiple updates to the leaky bucket at a fixed interval
improves the accuracy of the rate limiter. Previously the rate would
drop substantially over the course of the sync operation.
This commit is contained in:
Tim Rae
2024-06-08 12:47:25 +02:00
parent ecc642ba7d
commit bb0f3cffd0
2 changed files with 11 additions and 4 deletions

View File

@@ -16,4 +16,4 @@ spotify:
# increasing these parameters should increase the search speed, while decreasing reduces likelihood of 429 errors # increasing these parameters should increase the search speed, while decreasing reduces likelihood of 429 errors
max_concurrency: 10 # max concurrent connections at any given time max_concurrency: 10 # max concurrent connections at any given time
rate_limit: 12 # max sustained connections per second rate_limit: 10 # max sustained connections per second

View File

@@ -2,6 +2,7 @@
import asyncio import asyncio
from .cache import failure_cache, track_match_cache from .cache import failure_cache, track_match_cache
import datetime
from functools import partial from functools import partial
from typing import List, Sequence, Set, Mapping from typing import List, Sequence, Set, Mapping
import math import math
@@ -224,10 +225,16 @@ def get_tracks_for_new_tidal_playlist(spotify_tracks: Sequence[t_spotify.Spotify
async def sync_playlist(spotify_session: spotipy.Spotify, tidal_session: tidalapi.Session, spotify_playlist, tidal_playlist: tidalapi.Playlist | None, config): async def sync_playlist(spotify_session: spotipy.Spotify, tidal_session: tidalapi.Session, spotify_playlist, tidal_playlist: tidalapi.Playlist | None, config):
async def _run_rate_limiter(semaphore): async def _run_rate_limiter(semaphore):
''' Leaky bucket algorithm for rate limiting. Periodically releases an item from semaphore at rate_limit''' ''' Leaky bucket algorithm for rate limiting. Periodically releases items from semaphore at rate_limit'''
_sleep_time = config.get('max_concurrency', 10)/config.get('rate_limit', 10)/4 # aim to sleep approx time to drain 1/4 of 'bucket'
t0 = datetime.datetime.now()
while True: while True:
await asyncio.sleep(1/config.get('rate_limit', 12)) # sleep for min time between new function executions await asyncio.sleep(_sleep_time)
semaphore.release() # leak one item from the 'bucket' t = datetime.datetime.now()
dt = (t - t0).total_seconds()
new_items = round(config.get('rate_limit', 10)*dt)
t0 = t
[semaphore.release() for i in range(new_items)] # leak new_items from the 'bucket'
# Create a new Tidal playlist if required # Create a new Tidal playlist if required
if not tidal_playlist: if not tidal_playlist: