Skip to content

Commit b936d32

Browse files
authored
Merge pull request #2830 from britneywwc/discourse-response-cache
feat: add discourse response cache
2 parents 7b73d4e + 06beb2d commit b936d32

7 files changed

Lines changed: 50 additions & 36 deletions

File tree

.env

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,6 @@ RECAPTCHA_SCORE_THRESHOLD=0.5
2525
# Secret for sitemap endpoint
2626
SITEMAP_SECRET=somesecret
2727

28-
# Charmhub API
29-
CHARMHUB_DISCOURSE_API_KEY=charmhub_discourse_api_key
30-
CHARMHUB_DISCOURSE_API_USER=charmhub_discourse_api_user
31-
3228
# DiscourseAPI
3329
DISCOURSE_API_KEY=discourse_api_key
3430
DISCOURSE_API_USERNAME=discourse_api_username

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ djlint templates/path/to/file.html --lint --profile=jinja # djlint for html/ji
9898

9999
| Service | Purpose | Key Env Vars |
100100
| ------------- | -------------------------------- | ------------ |
101-
| Discourse API | Blog, takeovers, docs, tutorials | `DISCOURSE_API_KEY`, `DISCOURSE_API_USERNAME`, `CHARMHUB_DISCOURSE_API_KEY`, `CHARMHUB_DISCOURSE_API_USERNAME`, `MAAS_DISCOURSE_API_KEY`, `MAAS_DISCOURSE_API_USERNAME` |
101+
| Discourse API | Blog, takeovers, docs, tutorials | `DISCOURSE_API_KEY`, `DISCOURSE_API_USERNAME`, `MAAS_DISCOURSE_API_KEY`, `MAAS_DISCOURSE_API_USERNAME` |
102102
| Careers | Careers related | `HARVEST_API_KEY`, `APPLICATION_CRYPTO_SECRET_KEY` |
103103
| Greenhouse | Candidate applications | `GREENHOUSE_API_KEY` |
104104
| Google Calendar | Google Calendar service-account auth | `SERVICE_ACCOUNT_EMAIL`, `SERVICE_ACCOUNT_PRIVATE_KEY` |

charm/charmcraft.yaml

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,6 @@ config:
3030
type: string
3131
description: "Canonical CLA API URL"
3232
default: "https://cla.staging.canonical.com"
33-
charmhub-discourse-api-key:
34-
type: string
35-
description: "Charmhub Discourse API key"
36-
default: "charmhub_discourse_api_key"
37-
charmhub-discourse-api-user:
38-
type: string
39-
description: "Charmhub Discourse API user"
40-
default: "charmhub_discourse_api_user"
4133
directory-api-token:
4234
description: "Directory API token"
4335
default: "disrectory_api_token"

konf/site.yaml

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,6 @@ env:
6262
key: ubuntu-api-username
6363
name: discourse-api
6464

65-
- name: CHARMHUB_DISCOURSE_API_KEY
66-
secretKeyRef:
67-
key: charmhub-api-key
68-
name: discourse-api
69-
70-
- name: CHARMHUB_DISCOURSE_API_USERNAME
71-
secretKeyRef:
72-
key: charmhub-api-username
73-
name: discourse-api
74-
7565
- name: MAAS_DISCOURSE_API_KEY
7666
secretKeyRef:
7767
key: maas-api-key

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ canonicalwebteam.blog==6.8.4
33
canonicalwebteam.http==1.0.4
44
canonicalwebteam.image-template==1.9.0
55
canonicalwebteam.templatefinder==1.0.0
6-
canonicalwebteam.discourse==7.1.1
6+
canonicalwebteam.discourse==7.8.0
77
canonicalwebteam.search==2.1.2
88
canonicalwebteam.form-generator==2.2.0
99
canonicalwebteam.directory-parser==1.2.11

webapp/app.py

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
EngagePages,
3636
TutorialParser,
3737
Tutorials,
38+
ResponseCache,
3839
)
3940
from canonicalwebteam.flask_base.app import FlaskBase
4041
from canonicalwebteam.flask_base.env import get_flask_env
@@ -121,10 +122,6 @@
121122
# Serves any page as Markdown via ?format=md query parameter
122123
MarkdownResponse(app)
123124

124-
# Load env variables after the app is initialized
125-
CHARMHUB_DISCOURSE_API_KEY = os.getenv("CHARMHUB_DISCOURSE_API_KEY")
126-
CHARMHUB_DISCOURSE_API_USERNAME = os.getenv("CHARMHUB_DISCOURSE_API_USERNAME")
127-
128125
RECAPTCHA_CONFIG = load_recaptcha_config()
129126
RECAPTCHA_SITE_KEY = RECAPTCHA_CONFIG.get("site_key")
130127
if not RECAPTCHA_SITE_KEY:
@@ -146,14 +143,6 @@
146143

147144
# Loader supplied to jinja_loader overwrites default jinja_loader
148145
app.jinja_loader = loader
149-
150-
charmhub_discourse_api = DiscourseAPI(
151-
base_url="https://discourse.charmhub.io/",
152-
session=get_requests_session(),
153-
api_key=CHARMHUB_DISCOURSE_API_KEY,
154-
api_username=CHARMHUB_DISCOURSE_API_USERNAME,
155-
get_topics_query_id=2,
156-
)
157146
search_session = get_requests_session()
158147
discourse_session = get_requests_session()
159148

@@ -1144,6 +1133,7 @@ def allow_src(tag, name, value):
11441133
api=DiscourseAPI(
11451134
base_url="https://discourse.dqlite.io/",
11461135
session=discourse_session,
1136+
cache=ResponseCache(ttl=600),
11471137
),
11481138
index_topic_id=34,
11491139
url_prefix="/dqlite/docs",
@@ -1177,6 +1167,7 @@ def allow_src(tag, name, value):
11771167
base_url="https://discourse.maas.io/",
11781168
session=discourse_session,
11791169
get_topics_query_id=2,
1170+
cache=ResponseCache(ttl=600),
11801171
),
11811172
index_topic_id=6662,
11821173
url_prefix=maas_url_prefix,
@@ -1261,6 +1252,7 @@ def maas_docs_api():
12611252
api_key=MAAS_DISCOURSE_API_KEY,
12621253
api_username=MAAS_DISCOURSE_API_USERNAME,
12631254
get_topics_query_id=2,
1255+
cache=ResponseCache(ttl=600),
12641256
),
12651257
index_topic_id=1289,
12661258
url_prefix="/maas/tutorials",
@@ -1524,6 +1516,7 @@ def cred_exam_content(**_):
15241516
get_topics_query_id=14,
15251517
api_key=DISCOURSE_API_KEY,
15261518
api_username=DISCOURSE_API_USERNAME,
1519+
cache=ResponseCache(ttl=600),
15271520
)
15281521
engage_pages = EngagePages(
15291522
api=engage_pages_discourse_api,
@@ -1553,6 +1546,7 @@ def cred_exam_content(**_):
15531546
session=search_session,
15541547
api_key=DISCOURSE_API_KEY,
15551548
api_username=DISCOURSE_API_USERNAME,
1549+
cache=ResponseCache(ttl=600),
15561550
)
15571551

15581552

@@ -1588,6 +1582,7 @@ def cred_exam_content(**_):
15881582
api=DiscourseAPI(
15891583
base_url="https://discuss.kubernetes.io/",
15901584
session=get_requests_session(),
1585+
cache=ResponseCache(ttl=600),
15911586
),
15921587
index_topic_id=11243,
15931588
url_prefix=microk8s_url_prefix,

webapp/handlers.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import flask
1010
from canonicalwebteam.flask_base.env import get_flask_env
11+
from canonicalwebteam.discourse import RateLimitedError
1112

1213
logger = logging.getLogger(__name__)
1314

@@ -476,3 +477,43 @@ def get_csp_as_str(csp={}, nonce=None):
476477
if get_flask_env("FLASK_ENV", "production") != "production":
477478
response.headers["X-Robots-Tag"] = "none"
478479
return response
480+
481+
@app.errorhandler(503)
482+
def service_unavailable(error):
483+
"""
484+
Rendered when an upstream API (e.g. Discourse) is rate-limiting
485+
us and there is no cached response to fall back on. Reuses the
486+
styled 500 template (the directory_parser sitemap excludes it,
487+
and it is the app's standard "couldn't load this page" error)
488+
rather than leaking the internal reason to users.
489+
490+
JSON endpoints get a JSON body so their fetch() consumers don't
491+
choke on HTML, and Retry-After tells well-behaved clients and
492+
crawlers when to come back.
493+
"""
494+
accepts = flask.request.accept_mimetypes
495+
wants_json = flask.request.path.endswith(".json") or (
496+
accepts.accept_json and not accepts.accept_html
497+
)
498+
if wants_json:
499+
response = flask.make_response(
500+
flask.jsonify(error="Service temporarily unavailable"),
501+
503,
502+
)
503+
else:
504+
response = flask.make_response(
505+
flask.render_template("500.html"), 503
506+
)
507+
508+
retry_after = getattr(error, "retry_after", None)
509+
response.headers["Retry-After"] = str(retry_after or 60)
510+
return response
511+
512+
@app.errorhandler(RateLimitedError)
513+
def discourse_rate_limited(error):
514+
"""
515+
The discourse package raises RateLimitedError when Discourse
516+
returns 429 and no cached response is available; serve the same
517+
503 as any other upstream outage.
518+
"""
519+
return service_unavailable(error)

0 commit comments

Comments
 (0)