Skip to content

Commit

Permalink
Merge pull request #5110 from gitcoinco/feature/celery-4976
Browse files Browse the repository at this point in the history
Feature/celery 4976-In Review
  • Loading branch information
octavioamu authored Nov 20, 2019
2 parents dd5dd87 + 4426cf3 commit b308659
Show file tree
Hide file tree
Showing 10 changed files with 202 additions and 58 deletions.
13 changes: 13 additions & 0 deletions app/app/redis_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from django.conf import settings
from redis import Redis


class RedisService:
def __new__(cls):
if not hasattr(cls, 'instance'):
cls.instance = super().__new__(cls)
return cls.instance

def __init__(self):
redis_url = settings.CELERY_BROKER_URL
self.redis = Redis.from_url(redis_url)
43 changes: 28 additions & 15 deletions app/app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from easy_thumbnails.conf import Settings as easy_thumbnails_defaults

import warnings

warnings.filterwarnings("ignore", category=UserWarning, module='psycopg2')

root = environ.Path(__file__) - 2 # Set the base directory to two levels.
Expand Down Expand Up @@ -64,6 +65,8 @@
INSTALLED_APPS = [
'corsheaders',
'django.contrib.admin',
'taskapp.celery.CeleryConfig',
'django_celery_beat',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
Expand Down Expand Up @@ -184,8 +187,8 @@
REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users.
'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend', ),
'DEFAULT_THROTTLE_CLASSES': ('rest_framework.throttling.AnonRateThrottle', ),
'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',),
'DEFAULT_THROTTLE_CLASSES': ('rest_framework.throttling.AnonRateThrottle',),
'DEFAULT_THROTTLE_RATES': {
'anon': '1000/day',
},
Expand All @@ -205,7 +208,7 @@
USE_TZ = env.bool('USE_TZ', default=True)
TIME_ZONE = env.str('TIME_ZONE', default='UTC')

LOCALE_PATHS = ('locale', )
LOCALE_PATHS = ('locale',)

LANGUAGES = [
('en', gettext_noop('English')),
Expand Down Expand Up @@ -258,7 +261,6 @@
if RELEASE:
RAVEN_CONFIG['release'] = RELEASE


LOGGING = {
'version': 1,
'disable_existing_loggers': False,
Expand Down Expand Up @@ -317,13 +319,13 @@
'format': '%(hostname)s %(name)-12s [%(levelname)-8s] %(message)s',
}
LOGGING['handlers']['watchtower'] = {
'level': AWS_LOG_LEVEL,
'class': 'watchtower.django.DjangoCloudWatchLogHandler',
'boto3_session': boto3_session,
'log_group': AWS_LOG_GROUP,
'stream_name': AWS_LOG_STREAM,
'filters': ['host_filter'],
'formatter': 'cloudwatch',
'level': AWS_LOG_LEVEL,
'class': 'watchtower.django.DjangoCloudWatchLogHandler',
'boto3_session': boto3_session,
'log_group': AWS_LOG_GROUP,
'stream_name': AWS_LOG_STREAM,
'filters': ['host_filter'],
'formatter': 'cloudwatch',
}
LOGGING['loggers']['django.db.backends']['level'] = AWS_LOG_LEVEL

Expand All @@ -349,7 +351,7 @@

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.11/howto/static-files/
STATICFILES_DIRS = env.tuple('STATICFILES_DIRS', default=('assets/', ))
STATICFILES_DIRS = env.tuple('STATICFILES_DIRS', default=('assets/',))
STATIC_ROOT = root('static')
STATICFILES_LOCATION = env.str('STATICFILES_LOCATION', default='static')
MEDIAFILES_LOCATION = env.str('MEDIAFILES_LOCATION', default='media')
Expand All @@ -374,7 +376,7 @@
COMPRESS_ROOT = STATIC_ROOT
COMPRESS_ENABLED = env.bool('COMPRESS_ENABLED', default=True)

THUMBNAIL_PROCESSORS = easy_thumbnails_defaults.THUMBNAIL_PROCESSORS + ('app.thumbnail_processors.circular_processor', )
THUMBNAIL_PROCESSORS = easy_thumbnails_defaults.THUMBNAIL_PROCESSORS + ('app.thumbnail_processors.circular_processor',)

THUMBNAIL_ALIASES = {
'': {
Expand All @@ -392,6 +394,7 @@

CACHEOPS_DEGRADE_ON_FAILURE = env.bool('CACHEOPS_DEGRADE_ON_FAILURE', default=True)
CACHEOPS_REDIS = env.str('CACHEOPS_REDIS', default='redis://redis:6379/0')

CACHEOPS_DEFAULTS = {
'timeout': 60 * 60
}
Expand Down Expand Up @@ -455,6 +458,16 @@
},
}


# http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-broker_url
CELERY_BROKER_URL = env('CELERY_BROKER_URL', default=CACHEOPS_REDIS)
# http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-accept_content
CELERY_ACCEPT_CONTENT = ['json']
# http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-task_serializer
CELERY_TASK_SERIALIZER = 'json'
# http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-result_serializer
CELERY_RESULT_SERIALIZER = 'json'

DJANGO_REDIS_IGNORE_EXCEPTIONS = env.bool('REDIS_IGNORE_EXCEPTIONS', default=True)
DJANGO_REDIS_LOG_IGNORED_EXCEPTIONS = env.bool('REDIS_LOG_IGNORED_EXCEPTIONS', default=True)
COLLECTFAST_CACHE = env('COLLECTFAST_CACHE', default='collectfast')
Expand Down Expand Up @@ -655,8 +668,8 @@
AWS_S3_OBJECT_PARAMETERS = {'CacheControl': f'max-age={AWS_S3_CACHE_MAX_AGE}', }

CORS_ORIGIN_ALLOW_ALL = False
CORS_ORIGIN_WHITELIST = ('sumo.com', 'load.sumo.com', 'googleads.g.doubleclick.net', 'gitcoin.co', 'github.com', )
CORS_ORIGIN_WHITELIST = CORS_ORIGIN_WHITELIST + (AWS_S3_CUSTOM_DOMAIN, MEDIA_CUSTOM_DOMAIN, )
CORS_ORIGIN_WHITELIST = ('sumo.com', 'load.sumo.com', 'googleads.g.doubleclick.net', 'gitcoin.co', 'github.com',)
CORS_ORIGIN_WHITELIST = CORS_ORIGIN_WHITELIST + (AWS_S3_CUSTOM_DOMAIN, MEDIA_CUSTOM_DOMAIN,)

S3_REPORT_BUCKET = env('S3_REPORT_BUCKET', default='') # TODO
S3_REPORT_PREFIX = env('S3_REPORT_PREFIX', default='') # TODO
Expand Down
50 changes: 50 additions & 0 deletions app/dashboard/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from django.conf import settings

from celery import app
from celery.utils.log import get_task_logger
from app.redis_service import RedisService
from dashboard.models import Profile
from marketing.mails import send_mail, func_name
from retail.emails import render_share_bounty

logger = get_task_logger(__name__)

redis = RedisService().redis

# Lock timeout of 2 minutes (just in the case that the application hangs to avoid a redis deadlock)
LOCK_TIMEOUT = 60 * 2


@app.shared_task(bind=True, max_retries=3)
def bounty_emails(self, emails, msg, profile_handle, invite_url=None, kudos_invite=False, retry: bool = True) -> None:
"""
:param self:
:param emails:
:param msg:
:param profile_handle:
:param invite_url:
:param kudos_invite:
:return:
"""
with redis.lock("tasks:bounty_email:%s" % invite_url, timeout=LOCK_TIMEOUT):
# need to look at how to send bulk emails with SG
profile = Profile.objects.get(handle=profile_handle)
try:
for email in emails:
to_email = email
from_email = settings.CONTACT_EMAIL
subject = "You have been invited to work on a bounty."
html, text = render_share_bounty(to_email, msg, profile, invite_url, kudos_invite)
send_mail(
from_email,
to_email,
subject,
text,
html,
from_name=f"@{profile.handle}",
categories=['transactional', func_name()],
)

except ConnectionError as exc:
print(exc)
self.retry(30)
87 changes: 44 additions & 43 deletions app/marketing/mails.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,15 @@ def send_mail(from_email, _to_email, subject, body, html=False,
try:
response = sg.client.mail.send.post(request_body=mail.get())
except UnauthorizedError as e:
logger.debug(f'-- Sendgrid Mail failure - {_to_email} / {categories} - Unauthorized - Check sendgrid credentials')
logger.debug(
f'-- Sendgrid Mail failure - {_to_email} / {categories} - Unauthorized - Check sendgrid credentials')
logger.debug(e)
except HTTPError as e:
logger.debug(f'-- Sendgrid Mail failure - {_to_email} / {categories} - {e}')

return response


def nth_day_email_campaign(nth, subscriber):
firstname = subscriber.email.split('@')[0]

Expand Down Expand Up @@ -369,7 +371,8 @@ def tip_email(tip, to_emails, is_new):
warning = '' if tip.network == 'mainnet' else "({})".format(tip.network)
subject = gettext("⚡️ New Tip Worth {} {} {}").format(round(tip.amount, round_decimals), warning, tip.tokenName)
if not is_new:
subject = gettext("🕐 Tip Worth {} {} {} Expiring Soon").format(round(tip.amount, round_decimals), warning, tip.tokenName)
subject = gettext("🕐 Tip Worth {} {} {} Expiring Soon").format(round(tip.amount, round_decimals), warning,
tip.tokenName)

for to_email in to_emails:
cur_language = translation.get_language()
Expand Down Expand Up @@ -548,7 +551,7 @@ def warn_account_out_of_eth(account, balance, denomination):
setup_lang(to_email)
subject = account + str(_(" is out of gas"))
body_str = _("is down to ")
body = f"{account } {body_str} {balance} {denomination}"
body = f"{account} {body_str} {balance} {denomination}"
if not should_suppress_notification_email(to_email, 'admin'):
send_mail(
from_email,
Expand All @@ -569,7 +572,7 @@ def warn_subscription_failed(subscription):
try:
setup_lang(to_email)
subject = str(subscription.pk) + str(_(" subscription failed"))
body = f"{settings.BASE_URL}{subscription.admin_url }\n{subscription.contributor_profile.email}, {subscription.contributor_profile.user.email}<pre>\n\n{subscription.subminer_comments}</pre>"
body = f"{settings.BASE_URL}{subscription.admin_url}\n{subscription.contributor_profile.email}, {subscription.contributor_profile.user.email}<pre>\n\n{subscription.subminer_comments}</pre>"
if not should_suppress_notification_email(to_email, 'admin'):
send_mail(
from_email,
Expand All @@ -583,7 +586,6 @@ def warn_subscription_failed(subscription):
translation.activate(cur_language)



def new_feedback(email, feedback):
to_email = '[email protected]'
from_email = settings.SERVER_EMAIL
Expand Down Expand Up @@ -660,20 +662,13 @@ def no_applicant_reminder(to_email, bounty):


def share_bounty(emails, msg, profile, invite_url=None, kudos_invite=False):
for email in emails:
to_email = email
from_email = settings.CONTACT_EMAIL
subject = f"@{profile.handle} invited you to to work on a bounty."
html, text = render_share_bounty(to_email, msg, profile, invite_url, kudos_invite)
send_mail(
from_email,
to_email,
subject,
text,
html,
from_name=f"@{profile.handle}",
categories=['transactional', func_name()],
)
from dashboard.tasks import bounty_emails
# attempt to delay bounty_emails task to a worker
# long on failure to queue
try:
bounty_emails.delay(emails, msg, profile.handle, invite_url, kudos_invite)
except Exception as e:
logger.error(str(e))


def new_reserved_issue(from_email, user, bounty):
Expand Down Expand Up @@ -717,6 +712,7 @@ def reject_faucet_request(fr):
finally:
translation.activate(cur_language)


def new_bounty_daily(bounties, old_bounties, to_emails=None):
if not bounties:
return
Expand Down Expand Up @@ -800,6 +796,7 @@ def weekly_recap(to_emails=None):
finally:
translation.activate(cur_language)


def unread_notification_email_weekly_roundup(to_emails=None):
if to_emails is None:
to_emails = []
Expand Down Expand Up @@ -945,7 +942,6 @@ def bounty_changed(bounty, to_emails=None):


def new_match(to_emails, bounty, github_username):

subject = gettext("⚡️ {} Meet {}: {}! ").format(github_username.title(), bounty.org_name.title(), bounty.title)

to_email = to_emails[0]
Expand Down Expand Up @@ -1018,7 +1014,8 @@ def bounty_expire_warning(bounty, to_emails=None):
unit = _('hour')
num = int(round((bounty.expires_date - timezone.now()).seconds / 3600 / 24, 0))
unit = unit + ("s" if num != 1 else "")
subject = gettext("😕 Your Funded Issue ({}) Expires In {} {} ... 😕").format(bounty.title_or_desc, num, unit)
subject = gettext("😕 Your Funded Issue ({}) Expires In {} {} ... 😕").format(bounty.title_or_desc, num,
unit)

from_email = settings.CONTACT_EMAIL
html, text = render_bounty_expire_warning(to_email, bounty)
Expand Down Expand Up @@ -1208,6 +1205,10 @@ def new_bounty_request(model):

try:
setup_lang(to_email)
subject = _("New Bounty Request")
body_str = _("New Bounty Request from")
body = f"{body_str} {model.requested_by}: " \
f"{settings.BASE_URL}_administrationbounty_requests/bountyrequest/{model.pk}/change"
html, text, subject = render_bounty_request(to_email, model, settings.BASE_URL)

send_mail(
Expand Down Expand Up @@ -1243,21 +1244,21 @@ def new_funding_limit_increase_request(profile, cleaned_data):
usdt_per_tx = cleaned_data.get('usdt_per_tx', 0)
usdt_per_week = cleaned_data.get('usdt_per_week', 0)
comment = cleaned_data.get('comment', '')
accept_link = f'{settings.BASE_URL}requestincrease?'\
f'profile_pk={profile.pk}&'\
f'usdt_per_tx={usdt_per_tx}&'\
f'usdt_per_week={usdt_per_week}'
accept_link = f'{settings.BASE_URL}requestincrease?' \
f'profile_pk={profile.pk}&' \
f'usdt_per_tx={usdt_per_tx}&' \
f'usdt_per_week={usdt_per_week}'

try:
setup_lang(to_email)
subject = _('New Funding Limit Increase Request')
body = f'New Funding Limit Request from {profile} ({profile.absolute_url}).\n\n'\
f'New Limit in USD per Transaction: {usdt_per_tx}\n'\
f'New Limit in USD per Week: {usdt_per_week}\n\n'\
f'To accept the Funding Limit, visit: {accept_link}\n'\
f'Administration Link: ({settings.BASE_URL}_administrationdashboard/profile/'\
f'{profile.pk}/change/#id_max_tip_amount_usdt_per_tx)\n\n'\
f'Comment:\n{comment}'
body = f'New Funding Limit Request from {profile} ({profile.absolute_url}).\n\n' \
f'New Limit in USD per Transaction: {usdt_per_tx}\n' \
f'New Limit in USD per Week: {usdt_per_week}\n\n' \
f'To accept the Funding Limit, visit: {accept_link}\n' \
f'Administration Link: ({settings.BASE_URL}_administrationdashboard/profile/' \
f'{profile.pk}/change/#id_max_tip_amount_usdt_per_tx)\n\n' \
f'Comment:\n{comment}'

send_mail(from_email, to_email, subject, body, from_name=_("No Reply from Gitcoin.co"))
finally:
Expand All @@ -1277,17 +1278,17 @@ def bounty_request_feedback(profile):
try:
setup_lang(to_email)
subject = _(f'Bounty Request Feedback, @{profile.username} <> Gitcoin')
body = f'Howdy @{profile.username},\n\n'\
'This is Vivek from Gitcoin. '\
'I noticed you made a funded Gitcoin Requests '\
'a few months ago and just wanted to check in. '\
'How\'d it go? Any feedback for us?\n\n'\
'Let us know if you have any bounties in your near future '\
'-- we\'ll pay attention to '\
'Gitcoin Requests (https://gitcoin.co/requests/) '\
'from you as we know you\'ve suggested good things '\
'in the past 🙂\n\n'\
'Best,\n\nV'
body = f'Howdy @{profile.username},\n\n' \
'This is Vivek from Gitcoin. ' \
'I noticed you made a funded Gitcoin Requests ' \
'a few months ago and just wanted to check in. ' \
'How\'d it go? Any feedback for us?\n\n' \
'Let us know if you have any bounties in your near future ' \
'-- we\'ll pay attention to ' \
'Gitcoin Requests (https://gitcoin.co/requests/) ' \
'from you as we know you\'ve suggested good things ' \
'in the past 🙂\n\n' \
'Best,\n\nV'

send_mail(
from_email,
Expand Down
Empty file added app/taskapp/__init__.py
Empty file.
25 changes: 25 additions & 0 deletions app/taskapp/celery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import os

from celery import Celery
from celery.signals import setup_logging
from django.apps import AppConfig, apps

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings')

app = Celery('app')


class CeleryConfig(AppConfig):
name = 'taskapp'
verbose_name = 'Celery Config'

# Use Django logging instead of celery logger
@setup_logging.connect
def on_celery_setup_logging(**kwargs):
pass

def ready(self):
# Using a string here means the worker will not have to
app.config_from_object('django.conf:settings', namespace='CELERY')
installed_apps = [app_config.name for app_config in apps.get_app_configs()]
app.autodiscover_tasks(lambda: installed_apps, force=True)
Loading

0 comments on commit b308659

Please sign in to comment.