Skip to content
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

Adding results caching #247

Merged
merged 2 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions crank/models/organization.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Copyright (c) 2024 Isaac Adams
# Licensed under the MIT License. See LICENSE file in the project root for full license information.
from django.conf import settings
from django.core.cache import cache
from django.db import models
from django.utils.translation import gettext_lazy as _
from django_extensions.db.models import TimeStampedModel, ActivatorModel
Expand Down Expand Up @@ -52,8 +54,9 @@ class RTOPolicy(TextChoices):
accelerated_vesting = models.BooleanField(default=False)

def avg_scores(self):
results = self.scores.values("type__name").annotate(avg_score=Avg('score'))
return results
cache_key = f'organization_{self.pk}_avg_scores'
return cache.get_or_set(cache_key, lambda: self.scores.values("type__name").annotate(avg_score=Avg('score')),
timeout=settings.CACHE_TIMEOUT)

@staticmethod
def get_funding_round_choices():
Expand Down
16 changes: 15 additions & 1 deletion crank/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""
import multiprocessing
import os
from pathlib import Path

Expand Down Expand Up @@ -147,7 +148,6 @@
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases


EXTENSIONS_MAX_UNIQUE_QUERY_ATTEMPTS = 1000

SESSION_ENGINE = "django.contrib.sessions.backends.cache"
Expand All @@ -156,6 +156,20 @@
SESSION_COOKIE_DOMAIN = ".crank.fyi"
SESSION_COOKIE_SECURE = False

CACHE_TIMEOUT = 20 # Timeout for Redis cached items in seconds
REDIS_URL = os.environ["REDIS_URL"]
# Optional: To use Redis for session storage
SESSION_CACHE_ALIAS = 'default'
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': REDIS_URL,
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
}
}
}

# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators

Expand Down
17 changes: 3 additions & 14 deletions crank/settings/dev.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Copyright (c) 2024 Isaac Adams
# Licensed under the MIT License. See LICENSE file in the project root for full license information.
import multiprocessing
from pathlib import Path
from django.core.cache.backends.redis import RedisCache
import os
Expand All @@ -8,6 +9,8 @@
BASE_DIR = Path(__file__).resolve().parent.parent.parent
DEBUG = True
SECRET_KEY = os.environ["SECRET_KEY"]
CPU_COUNT = multiprocessing.cpu_count()

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
Expand All @@ -18,20 +21,6 @@
}
}

REDIS_URL = os.environ["REDIS_URL"]
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': REDIS_URL,
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
}
}
}

# Optional: To use Redis for session storage
SESSION_CACHE_ALIAS = 'default'

CORS_ORIGIN_ALLOW_ALL = True
ALLOWED_HOSTS = ['*']
LOGGING = {
Expand Down
3 changes: 2 additions & 1 deletion crank/settings/prod.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@
DEBUG = False
# SECURE_SSL_REDIRECT = True
SECRET_KEY = os.environ.get('SECRET_KEY')
CPU_COUNT = multiprocessing.cpu_count()

SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
CSRF_COOKIE_SECURE = True
CPU_COUNT = multiprocessing.cpu_count()

DATABASES = {
'default': {
Expand Down
3 changes: 2 additions & 1 deletion crank/tests/views/test_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from django.contrib.sessions.middleware import SessionMiddleware
from django.core.serializers import serialize
from django.http import HttpResponseRedirect
from django.test import TestCase, Client, RequestFactory
from django.test import TestCase, Client, RequestFactory, override_settings
from django.urls import reverse
from django.utils.html import escape
from unittest.mock import patch
Expand All @@ -20,6 +20,7 @@
from crank.settings import DEFAULT_ALGORITHM_ID


@override_settings(CACHES={'default': {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'}})
class IndexViewTests(TestCase):

def setUp(self):
Expand Down
9 changes: 7 additions & 2 deletions crank/views/fundinground.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
# Copyright (c) 2024 Isaac Adams
# Licensed under the MIT License. See LICENSE file in the project root for full license information.
from django.core.cache import cache
from django.http import JsonResponse
from django.views import View
from crank.models.organization import Organization

class FundingRoundChoicesView(View):
def get(self, request, *args, **kwargs):
choices = Organization.get_funding_round_choices()
return JsonResponse(choices)
cache_key = 'funding_round_choices'
def fetch_results():
choices = Organization.get_funding_round_choices()
return JsonResponse(choices)

return cache.get_or_set(cache_key, fetch_results())
74 changes: 41 additions & 33 deletions crank/views/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
from django.shortcuts import redirect
from django.urls import reverse
from django.views import generic
from django.core.cache import cache
from django.conf import settings


from crank.models.organization import Organization
from crank.models.score import ScoreAlgorithm
Expand Down Expand Up @@ -67,41 +70,46 @@ def get_queryset(self):
self.object_list = []
return self.object_list

query = '''
SELECT id, name, type, rto_policy, funding_round, accelerated_vesting, avg_score, profile_completeness, RANK() OVER (ORDER BY avg_score desc) as ranking
FROM (
SELECT orgs.id, orgs.name, orgs.type, orgs.rto_policy, orgs.funding_round, orgs.accelerated_vesting,
SUM(orgs.avg_type_score * orgs.weight) / SUM(orgs.weight) AS avg_score,
(CAST(score_types.score_type_count AS REAL) / (SELECT COUNT(*) FROM crank_scoretype ct WHERE ct.status = 1) * 100) AS profile_completeness
def fetch_results():
query = '''
SELECT id, name, type, rto_policy, funding_round, accelerated_vesting, avg_score, profile_completeness, RANK() OVER (ORDER BY avg_score desc) as ranking
FROM (
SELECT co.id, co.name, co.type, co.rto_policy, co.funding_round, co.accelerated_vesting,
AVG(cs.score) AS avg_type_score, cw.weight, ct.name AS score_type
FROM crank_organization AS co
JOIN crank_score AS cs ON co.id = cs.target_id
JOIN crank_scoretype AS ct ON cs.type_id = ct.id
JOIN crank_scorealgorithmweight AS cw ON cs.type_id = cw.type_id
WHERE co.status = 1 AND cw.algorithm_id = %s
GROUP BY co.id, co.name, co.type, co.rto_policy, co.funding_round, co.accelerated_vesting, cw.weight, ct.name
) orgs
JOIN (
SELECT target_id, count(*) AS score_type_count
SELECT orgs.id, orgs.name, orgs.type, orgs.rto_policy, orgs.funding_round, orgs.accelerated_vesting,
SUM(orgs.avg_type_score * orgs.weight) / SUM(orgs.weight) AS avg_score,
(CAST(score_types.score_type_count AS REAL) / (SELECT COUNT(*) FROM crank_scoretype ct WHERE ct.status = 1) * 100) AS profile_completeness
FROM (
SELECT target_id, type_id, COUNT(type_id)
FROM crank_score
WHERE status = 1
GROUP BY target_id, type_id
) score_counts
GROUP BY score_counts.target_id
) score_types ON score_types.target_id = orgs.id
GROUP BY id, name, type, rto_policy, funding_round, accelerated_vesting
) scored_results
'''

with connection.cursor() as cursor:
cursor.execute(query, [self.algorithm_id])
columns = [col[0] for col in cursor.description]
self.object_list = [dict(zip(columns, row)) for row in cursor.fetchall()]

SELECT co.id, co.name, co.type, co.rto_policy, co.funding_round, co.accelerated_vesting,
AVG(cs.score) AS avg_type_score, cw.weight, ct.name AS score_type
FROM crank_organization AS co
JOIN crank_score AS cs ON co.id = cs.target_id
JOIN crank_scoretype AS ct ON cs.type_id = ct.id
JOIN crank_scorealgorithmweight AS cw ON cs.type_id = cw.type_id
WHERE co.status = 1 AND cw.algorithm_id = %s
GROUP BY co.id, co.name, co.type, co.rto_policy, co.funding_round, co.accelerated_vesting, cw.weight, ct.name
) orgs
JOIN (
SELECT target_id, count(*) AS score_type_count
FROM (
SELECT target_id, type_id, COUNT(type_id)
FROM crank_score
WHERE status = 1
GROUP BY target_id, type_id
) score_counts
GROUP BY score_counts.target_id
) score_types ON score_types.target_id = orgs.id
GROUP BY id, name, type, rto_policy, funding_round, accelerated_vesting
) scored_results
'''

with connection.cursor() as cursor:
cursor.execute(query, [self.algorithm_id])
columns = [col[0] for col in cursor.description]
object_list = [dict(zip(columns, row)) for row in cursor.fetchall()]

return object_list

cache_key = f'algorithm_{self.algorithm_id}_results'
self.object_list = cache.get_or_set(cache_key, fetch_results, timeout=settings.CACHE_TIMEOUT)
return self.object_list


Expand Down
8 changes: 8 additions & 0 deletions crank/views/organization.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Copyright (c) 2024 Isaac Adams
# Licensed under the MIT License. See LICENSE file in the project root for full license information.
from django.conf import settings
from django.core.cache import cache
from django.views import generic

# from crank.models.score import Score
Expand All @@ -13,3 +15,9 @@ class OrganizationView(generic.DetailView):
def get_queryset(self):
"""make sure we can't see inactive organizations."""
return Organization.objects.filter(status=1)

def get_object(self, queryset=None):
obj_id = self.kwargs.get('pk')
cache_key = f'organization_{obj_id}'
return cache.get_or_set(cache_key, lambda: super(OrganizationView, self).get_object(queryset),
timeout=settings.CACHE_TIMEOUT)
10 changes: 8 additions & 2 deletions crank/views/rtopolicy.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
# Copyright (c) 2024 Isaac Adams
# Licensed under the MIT License. See LICENSE file in the project root for full license information.
from django.core.cache import cache
from django.http import JsonResponse
from django.views import View
from crank.models.organization import Organization

class RTOPolicyChoicesView(View):
def get(self, request, *args, **kwargs):
choices = Organization.get_rto_policy_choices()
return JsonResponse(choices)
cache_key = 'policy_choices'

def fetch_results():
choices = Organization.get_rto_policy_choices()
return JsonResponse(choices)

return cache.get_or_set(cache_key, fetch_results())
Loading