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