-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make Hue and Spotify setup tasks singletons #16
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
e4fff1a
make setup tasks singleton tasks, move backend dev requirements to ba…
BeeStag 6a6d2d0
tidy up a little, make singleton task locks' TTL an adjustable param
BeeStag ae9d1b0
lengthen Spotify oauth timeout
BeeStag 43d2d7f
battling React dev mode
BeeStag 3657f57
Merge branch 'main' into setup-singleton-tasks
BeeStag d229c7e
add clarifying comment to decorator wrapper, make SPOTIFY_OAUTH_TIME…
BeeStag File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,105 @@ | ||
from contextlib import contextmanager | ||
from functools import wraps | ||
import logging | ||
import random | ||
import time | ||
from typing import List | ||
from typing import Generator, List, Optional | ||
|
||
from celery.app import task | ||
from phue import PhueException | ||
from redis import Redis | ||
from spotipy.oauth2 import SpotifyOauthError | ||
|
||
from . import celery_app, constants, redis_client, spotihue | ||
from . import celery_app, constants, oauth, redis_client, spotihue | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class SingletonTask(task.Task): | ||
|
||
@staticmethod | ||
def run_singleton_task(bound_task_function): | ||
"""Meant to decorate a bound celery task function such that there can only be 1 instance of the | ||
task running at any given point. | ||
Ex: | ||
|
||
@celery_app.task(base=SingletonTask, bind=True) | ||
@run_singleton_task | ||
def my_celery_task(self, other_param): | ||
pass | ||
|
||
Args: | ||
bound_task_function (Callable): a celery task function which is bound (ie. receives itself | ||
as its 1st argument). | ||
|
||
Returns: decorator for a celery @task-decorated function/Task subclass .run() implementation. | ||
|
||
""" | ||
@wraps(bound_task_function) | ||
def wrapper(*args, **kwargs): | ||
# Because this decorator is invoked statically above task declarations, we access | ||
# the Task object's self via the following derpy line... | ||
me = args[0] | ||
|
||
# ...so that we can collect the task's name and ID. | ||
my_task_name = me.name | ||
my_task_id = me.request.id | ||
|
||
my_task_lock = SingletonTaskLock(lock_id=my_task_name, redis=redis_client) | ||
|
||
with my_task_lock.acquire_for(my_task_id) as acquired: | ||
if acquired is True: | ||
logger.info(f'Acquired lock for {my_task_name}; running task {my_task_id}') | ||
return bound_task_function(*args, **kwargs) | ||
else: | ||
logger.info(f'Another {my_task_name} task is already being invoked; exiting task {my_task_id}') | ||
|
||
return wrapper | ||
|
||
|
||
class SingletonTaskLock: | ||
""" | ||
lock_id (key) = a task's name. | ||
lock values = task IDs. | ||
""" | ||
DEFAULT_MAX_TTL = 60 * 5 # 5 minutes | ||
|
||
def __init__(self, lock_id: str, redis: Redis, lock_max_ttl: Optional[int] = DEFAULT_MAX_TTL): | ||
self.lock_id = lock_id | ||
self.redis = redis | ||
self.lock_max_ttl = lock_max_ttl | ||
|
||
@contextmanager | ||
def acquire_for(self, task_id: str) -> Generator[str, None, None]: | ||
"""Attempts to acquire a simple Redis lock for a "singleton" celery task with ID task_id. | ||
|
||
Args: | ||
task_id (str): ID of a celery task that worker is trying to run. | ||
|
||
Returns: | ||
Generator[str, None, None]: context manager to get lock acquisition status for task, | ||
then subsequently relinquish that acquired lock (if the initial acquisition | ||
was successful). | ||
""" | ||
lock = self.redis.get(self.lock_id) | ||
|
||
if lock is None: | ||
# if there are 2 dueling tasks (queued within a few hundredths of a second | ||
# apart), let one of them win. | ||
time.sleep(random.uniform(.01, .1)) | ||
lock = self.redis.get(self.lock_id) | ||
|
||
lock_acquired = bool(lock is None or (isinstance(lock, bytes) and lock.decode('utf-8') == task_id)) | ||
|
||
try: | ||
if lock_acquired: | ||
self.redis.setex(self.lock_id, self.lock_max_ttl, task_id) | ||
yield lock_acquired | ||
finally: | ||
if lock_acquired: | ||
self.redis.delete(self.lock_id) | ||
|
||
|
||
def is_spotihue_running() -> bool: | ||
"""Queries for the SpotiHue task by its cached spotihue_task_id. There should only be one | ||
run_spotihue task running at any given time. | ||
|
@@ -89,9 +178,10 @@ def run_spotihue(lights: List[str], current_track_retries: int = 0) -> None: | |
time.sleep(sleep_duration) | ||
|
||
|
||
@celery_app.task | ||
def setup_hue(backoff_seconds: int = 5, retries: int = 5) -> None: | ||
logger.info("Attempting to connect to Hue bridge...") | ||
@celery_app.task(base=SingletonTask, bind=True) | ||
@SingletonTask.run_singleton_task | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. run_singleton_task is static method |
||
def setup_hue(self, backoff_seconds: int = 5, retries: int = 5) -> None: | ||
logger.info('Attempting to connect to Hue bridge...') | ||
|
||
for attempt in range(retries): | ||
try: | ||
|
@@ -109,9 +199,10 @@ def setup_hue(backoff_seconds: int = 5, retries: int = 5) -> None: | |
raise | ||
|
||
|
||
@celery_app.task | ||
def listen_for_spotify_redirect() -> None: | ||
logger.info(f"Waiting to receive user authorization from Spotify...") | ||
@celery_app.task(base=SingletonTask, bind=True, throws=(oauth.SpotifyOauthSocketTimeout,)) | ||
@SingletonTask.run_singleton_task | ||
def listen_for_spotify_redirect(self) -> None: | ||
logger.info('Waiting to receive user authorization from Spotify...') | ||
spotify_oauth = spotihue.spotify_oauth | ||
|
||
try: | ||
|
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add a comment here explaining this line based on our convo?