Skip to content

Commit

Permalink
better background tasks
Browse files Browse the repository at this point in the history
- improved/refactored user lookup filters
- add BaseTask, TaskCooldown, etc classes
- refactored BaseBatchRelationshipEmail
  • Loading branch information
jontsai committed Sep 13, 2015
1 parent 7af68d1 commit 1ceb22b
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 46 deletions.
35 changes: 35 additions & 0 deletions apps/accounts/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,41 @@

from htk.utils import utcnow

def active_users(users, active=True):
filtered = users.filter(is_active=active)
return filtered

def inactive_users(users):
filtered = users.filter(is_active=False)
return filtered

def users_with_attribute_value(users, key, value):
filtered = users.filter(
attributes__key=key,
attributes__value=value
)
return filtered

def users_currently_at_local_time(users, start_hour, end_hour, isoweekday=None):
"""Filters a QuerySet of `users` whose current local time is within a time range
Strategy 1 (inefficient):
enumerate through every User, and keep the ones whose current local time is within the range
Strategy 2:
- find all the timezones that are in the local time
- query users in that timezone
`start_hour` and `end_hour` are naive datetimes
If `isoweekday` is specified, also checks that it falls on that weekday (Monday = 1, Sunday = 7)
"""
from htk.utils.datetime_utils import get_timezones_within_current_local_time_bounds
timezones = get_timezones_within_current_local_time_bounds(start_hour, end_hour, isoweekday=isoweekday)
filtered = users.filter(
profile__timezone__in=timezones
)
return filtered

def users_logged_in_within_period(users, window=1):
"""Filter the queryset of users who logged in within the last `window` number of hours.
"""
Expand Down
31 changes: 13 additions & 18 deletions apps/accounts/utils/lookup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from django.contrib.auth import get_user_model

import htk.apps.accounts.filters as _filters
from htk.utils import utcnow
from htk.utils.datetime_utils import get_timezones_within_current_local_time_bounds

Expand All @@ -14,44 +15,38 @@ def get_all_users(active=True):
UserModel = get_user_model()
users = UserModel.objects.all()
if active is not None:
users = users.filter(is_active=active)
users = _filters.active_users(users, active=active)
return users

def get_inactive_users():
"""Returns all inactive users
"""
UserModel = get_user_model()
inactive_users = UserModel.objects.filter(is_active=False)
inactive_users = _filters.inactive_users(UserModel.objects)
return inactive_users

def get_users_with_attribute_value(key, value, active=True):
UserModel = get_user_model()
users = UserModel.objects.filter(
attributes__key=key,
attributes__value=value
)
users = _filters.users_with_attribute_value(UserModel.objects, key, value)
if active is not None:
users = users.filter(is_active=active)
users = _filters.active_users(users, active=active)
return users

def get_users_currently_at_local_time(start_hour, end_hour, isoweekday=None, active=True):
"""Returns a list of Users whose current local time is within a time range
Strategy 1 (inefficient):
enumerate through every User, and keep the ones whose current local time is within the range
Strategy 2:
- find all the timezones that are in the local time
- query users in that timezone
"""Returns a Queryset of Users whose current local time is within a time range
`start_hour` and `end_hour` are naive datetimes
If `isoweekday` is specified, also checks that it falls on that weekday (Monday = 1, Sunday = 7)
"""
timezones = get_timezones_within_current_local_time_bounds(start_hour, end_hour, isoweekday=isoweekday)
UserModel = get_user_model()
users = UserModel.objects.filter(
profile__timezone__in=timezones
users = _filters.users_currently_at_local_time(
UserModel.objects,
start_hour,
end_hour,
isoweekday=isoweekday
)

if active is not None:
users = users.filter(is_active=active)
users = _filters.active_users(users, active=active)
return users
11 changes: 10 additions & 1 deletion cachekeys.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,16 @@ def get_cache_duration(self):
duration = TIMEOUT_30_DAYS
return duration

class BatchRelationshipEmailCooldown(CustomCacheScheme):
class TaskCooldown(CustomCacheScheme):
"""Cache management object for not performing background tasks too frequently
Default cooldown: no more than once per day
"""
def get_cache_duration(self):
duration = TIMEOUT_24_HOURS
return duration

class BatchRelationshipEmailCooldown(TaskCooldown):
"""Cache management object for not sending out BatchRelationshipEmails too frequently
Default cooldown: no more than once per day
Expand Down
5 changes: 5 additions & 0 deletions constants/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,8 @@
BUSINESS_HOURS_END = 18 # 6pm
MORNING_HOURS_START = 6 # 6am
MORNING_HOURS_END = 10 # 10am
MID_MORNING_HOURS_START = 9 # 9am
MID_MORNING_HOURS_END = 11 # 11am
NOON_HOUR = 12 # 12pm
LUNCH_HOURS_START = 11 # 11am
LUNCH_HOURS_END = 13 # 2pm
33 changes: 6 additions & 27 deletions emails.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
from django.template import TemplateDoesNotExist
from django.template.loader import get_template

from htk.tasks import BaseTask
from htk.mailers import send_email

class BaseBatchRelationshipEmails(object):
class BaseBatchRelationshipEmails(BaseTask):
"""
Relationship emails are related to transactional emails, but usually happen asynchronously or offline, apart from direct web requests
Expand Down Expand Up @@ -37,34 +38,12 @@ def __init__(self, cooldown_class=None, template=None):
else:
raise TemplateDoesNotExist('Unspecified template')

def has_cooldown(self, user):
"""Checks whether cooldown timer is still going for `user`
"""
prekey = user.id
c = self.cooldown_class(prekey)
_has_cooldown = bool(c.get())
return _has_cooldown

def reset_cooldown(self, user):
"""Resets cooldown timer for this `user`
Returns whether cooldown was reset, False if timer was still running
"""
prekey = user.id
c = self.cooldown_class(prekey)
if c.get():
was_reset = False
else:
c.cache_store()
was_reset = True
return was_reset

def get_recipients(self):
"""Returns a list or QuerySet of User objects
Should be overridden
"""
users = []
users = self.get_users()
return users

def get_subject(self, recipient):
Expand Down Expand Up @@ -100,7 +79,7 @@ def _craft_email_params(self, recipient):
return email_params

def send_email(self, recipient):
"""Workhorse function called by `self.send_emails for`
"""Workhorse function called by `self.send_emails` for
sending to one `recipient`
Can be overridden
Expand All @@ -124,6 +103,6 @@ def send_emails(self):
pass
else:
self.send_email(recipient)
# cache right after we send, since we want to guarantee
# each send operation costs a non-zero overhead,
# cache right after we send, not before
# since each send operation costs a non-zero overhead
self.reset_cooldown(recipient)
69 changes: 69 additions & 0 deletions tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import inspect

class BaseTask(object):
"""Base class for background tasks
Examples:
- Daily or weekly updates
- Drip reminders
- Account status reports
- Shopping cart abandonment reminders
"""
def __init__(self, cooldown_class=None):
# set cooldown_class
from htk.cachekeys import TaskCooldown
if inspect.isclass(cooldown_class) and issubclass(cooldown_class, TaskCooldown):
self.cooldown_class = cooldown_class
else:
self.cooldown_class = TaskCooldown

def has_cooldown(self, user):
"""Checks whether cooldown timer is still going for `user`
"""
prekey = user.id
c = self.cooldown_class(prekey)
_has_cooldown = bool(c.get())
return _has_cooldown

def reset_cooldown(self, user):
"""Resets cooldown timer for this `user`
Returns whether cooldown was reset, False if timer was still running
"""
prekey = user.id
c = self.cooldown_class(prekey)
if c.get():
was_reset = False
else:
c.cache_store()
was_reset = True
return was_reset

def get_users(self):
"""Returns a list or QuerySet of User objects
Should be overridden
"""
users = []
return users

def execute(self, uesr):
"""Workhorse function called by `self.execute_batch`
Can be overriden
"""
pass

def execute_batch(self):
"""Batch execution
"""
users = self.get_users()
for user in users:
if self.has_cooldown(user):
# cooldown has not elapsed yet, don't execute too frequently
pass
else:
self.execute(user)
# cache right after execution, not before
# since each execution costs a non-zero overhead
self.reset_cooldown(user)

0 comments on commit 1ceb22b

Please sign in to comment.