1# -*- coding: utf-8 -*- 2 3# This file is part of Tautulli. 4# 5# Tautulli is free software: you can redistribute it and/or modify 6# it under the terms of the GNU General Public License as published by 7# the Free Software Foundation, either version 3 of the License, or 8# (at your option) any later version. 9# 10# Tautulli is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with Tautulli. If not, see <http://www.gnu.org/licenses/>. 17 18from __future__ import unicode_literals 19from future.builtins import object 20 21import future.moves.queue as queue 22import time 23import threading 24 25import plexpy 26if plexpy.PYTHON2: 27 import logger 28else: 29 from plexpy import logger 30 31 32class TimedLock(object): 33 """ 34 Enforce request rate limit if applicable. This uses the lock so there 35 is synchronized access to the API. When N threads enter this method, the 36 first will pass trough, since there there was no last request recorded. 37 The last request time will be set. Then, the second thread will unlock, 38 and see that the last request was X seconds ago. It will sleep 39 (request_limit - X) seconds, and then continue. Then the third one will 40 unblock, and so on. After all threads finish, the total time will at 41 least be (N * request_limit) seconds. If some request takes longer than 42 request_limit seconds, the next unblocked thread will wait less. 43 """ 44 45 def __init__(self, minimum_delta=0): 46 """ 47 Set up the lock 48 """ 49 self.lock = threading.Lock() 50 self.last_used = 0 51 self.minimum_delta = minimum_delta 52 self.queue = queue.Queue() 53 54 def __enter__(self): 55 """ 56 Called when with lock: is invoked 57 """ 58 self.lock.acquire() 59 delta = time.time() - self.last_used 60 sleep_amount = self.minimum_delta - delta 61 if sleep_amount >= 0: 62 # zero sleeps give the cpu a chance to task-switch 63 logger.debug('Sleeping %s (interval)', sleep_amount) 64 time.sleep(sleep_amount) 65 while not self.queue.empty(): 66 try: 67 seconds = self.queue.get(False) 68 logger.debug('Sleeping %s (queued)', seconds) 69 time.sleep(seconds) 70 except queue.Empty: 71 continue 72 self.queue.task_done() 73 74 def __exit__(self, type, value, traceback): 75 """ 76 Called when exiting the with block. 77 """ 78 self.last_used = time.time() 79 self.lock.release() 80 81 def snooze(self, seconds): 82 """ 83 Asynchronously add time to the next request. Can be called outside 84 of the lock context, but it is possible for the next lock holder 85 to not check the queue until after something adds time to it. 86 """ 87 # We use a queue so that we don't have to synchronize 88 # across threads and with or without locks 89 logger.info('Adding %s to queue', seconds) 90 self.queue.put(seconds) 91 92 93class FakeLock(object): 94 """ 95 If no locking or request throttling is needed, use this 96 """ 97 98 def __enter__(self): 99 """ 100 Do nothing on enter 101 """ 102 pass 103 104 def __exit__(self, type, value, traceback): 105 """ 106 Do nothing on exit 107 """ 108 pass 109