Skip to content

Commit 897d242

Browse files
committed
better caching
1 parent 200f3ae commit 897d242

37 files changed

Lines changed: 972 additions & 267 deletions

‎.env.example‎

Lines changed: 0 additions & 16 deletions
This file was deleted.

‎Dockerfile‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ CMD ["python", "-m", "feedback"]
3131

3232
FROM development AS test
3333

34-
ENV FEEDBACK_ENV=test
3534
COPY pyproject.toml ./
3635
COPY src ./src
3736
COPY tests ./tests
@@ -53,10 +52,7 @@ ENV VIRTUAL_ENV=/opt/venv \
5352
PATH=/opt/venv/bin:$PATH \
5453
PYTHONUNBUFFERED=1 \
5554
PYTHONDONTWRITEBYTECODE=1 \
56-
PYTHONHASHSEED=random \
57-
FEEDBACK_HOST=0.0.0.0 \
58-
FEEDBACK_PORT=8080 \
59-
FEEDBACK_WORKERS=1
55+
PYTHONHASHSEED=random
6056

6157
RUN apt-get update \
6258
&& apt-get install --yes --no-install-recommends ca-certificates \

‎README.md‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ The secret files must be readable by UID 10001 in the container. Root-owned mode
2424
`0444` is suitable because Docker bind-mounts them read-only. The application
2525
configuration itself is committed as `config/sites.toml`.
2626

27+
The process is configured with explicit command-line arguments rather than
28+
environment variables. `python -m feedback --help` lists the config, secret-file,
29+
listener, proxy-trust, concurrency, and keep-alive options. Docker Compose passes
30+
those arguments directly and uses fixed read-only mounts.
31+
2732
The `vps-8def0ca8` NixOS configuration imports and enables the feedback service.
2833
Rebuilding that host builds the image from `/srv/feedback`, starts it with Docker
2934
Compose, and provisions the `feedback-api.cpp.social` nginx virtual host and ACME
@@ -36,6 +41,77 @@ systemctl status feedback-service
3641
curl --fail --silent --show-error https://feedback-api.cpp.social/
3742
```
3843

44+
## Site mappings and metadata
45+
46+
Each site maps a browser resource to one GitHub Discussion. The configured
47+
`mapping` selects the value used as the discussion lookup term and title:
48+
49+
- `key`: the stable application-provided `resource.key` (recommended when URLs or
50+
titles may change).
51+
- `title`: the resource title.
52+
- `url`: the complete canonical URL, including its origin.
53+
- `pathname`: only the canonical URL path, allowing equivalent pages on multiple
54+
origins to share a discussion.
55+
- `custom`: an arbitrary consumer-provided string for custom routing schemes.
56+
- `number`: the numeric number of an already existing GitHub Discussion. Unlike
57+
`key`, it is not a resource identifier and missing discussions are never created.
58+
59+
The browser package exports `resourceFromDocument()`. By default it reads the
60+
first non-empty title from `meta[property="og:title"]` and then `<title>`, reads
61+
`link[rel="canonical"]` when present, and otherwise uses the current location.
62+
Consumers can override `titleSelectors` and `canonicalSelector`, or construct a
63+
`Resource` directly and supply `custom`, `pathname`, or `number`. Metadata
64+
selection is intentionally client-side; the service only receives validated
65+
resource values and applies the site mapping.
66+
67+
```ts
68+
const resource = resourceFromDocument({
69+
key: "articles/stable-id",
70+
titleSelectors: ['meta[name="feedback-title"]', 'meta[property="og:title"]', "title"],
71+
canonicalSelector: 'link[rel="canonical"]',
72+
custom: document.body.dataset.feedbackKey ?? "default-feedback-key",
73+
});
74+
```
75+
76+
## Counter cache
77+
78+
`cache_fresh_seconds` is the age at which a requested tracked counter needs an
79+
authoritative GitHub refresh. The default is five seconds. A batched request
80+
refreshes only stale resources; recently refreshed resources in the same request
81+
are served from SQLite. For example, if A was last refreshed 30 seconds ago and B
82+
three seconds ago, requesting `[A, B]` sends only A's discussion ID to GitHub and
83+
returns B from SQLite in the same response.
84+
85+
`refresh_cooldown_seconds` is the minimum delay before retrying a refresh attempt
86+
for the same resource. It primarily prevents repeated GitHub calls after a failed
87+
or concurrent attempt. Concurrent requests also join the same in-flight site
88+
batch. The default is five seconds.
89+
90+
`refresh_sweep_seconds` controls the low-priority full maintenance cycle. The
91+
default is 86400 seconds (daily). The service walks only discussions whose last
92+
authoritative snapshot is that old, in batches of 50 with pacing between full
93+
batches. Targeted requests and successful votes continue independently.
94+
95+
Votes made through the runtime update SQLite immediately using an atomic,
96+
confirmed delta. These local values are tentative: they do not change the last
97+
GitHub-refresh timestamp, and the next successful targeted or maintenance refresh
98+
replaces them with GitHub's absolute counts. Counter responses use `no-cache`, so
99+
browsers revalidate with this service; that does not imply a GitHub request while
100+
the relevant snapshot remains fresh.
101+
102+
The browser runtime keeps the last counter snapshot in local storage for up to
103+
seven days. Consumers can render it synchronously while the API request is in
104+
flight, avoiding a flash of zero counters. Snapshots contain only the site/resource
105+
key, discussion node ID, counts, and save time; authentication tokens are not part
106+
of this cache. Storage is optional and failures fall back to the network normally.
107+
108+
Operational logs are emitted at GitHub boundaries rather than for every HTTP
109+
request. Reaction-refresh lines include the site, trigger (`requested` or
110+
`sweep`), batch size, updated-row count, and duration. Failures include safe
111+
GitHub status/request IDs where available. Discussion discovery/creation, OAuth
112+
failures, vote failures, startup, and sweep summaries are also logged. Client
113+
IPs, origins, resource URLs, authorization codes, and tokens are not logged.
114+
39115
## Local checks
40116

41117
```sh

‎compose.deploy.yaml‎

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ services:
66
context: .
77
target: production
88
image: feedback-service:local
9+
command:
10+
- --config=/run/config/sites.toml
11+
- --github-app-private-key-file=/run/secrets/github-app-private-key
12+
- --github-client-secret-file=/run/secrets/github-client-secret
13+
- --oauth-state-hmac-key-file=/run/secrets/oauth-state-hmac-key
14+
- --forwarded-allow-ips=172.17.0.1
15+
- --limit-concurrency=64
16+
- --keep-alive-seconds=5
917
restart: unless-stopped
1018
init: true
1119
read_only: true
@@ -18,19 +26,10 @@ services:
1826
mem_limit: 256m
1927
mem_reservation: 128m
2028
cpus: 1.0
21-
environment:
22-
FEEDBACK_CONFIG: /run/config/sites.toml
23-
FEEDBACK_DATA_DIRECTORY: /data/sites
24-
FEEDBACK_GITHUB_APP_PRIVATE_KEY_FILE: /run/secrets/github-app-private-key
25-
FEEDBACK_GITHUB_CLIENT_SECRET_FILE: /run/secrets/github-client-secret
26-
FEEDBACK_OAUTH_STATE_HMAC_KEY_FILE: /run/secrets/oauth-state-hmac-key
27-
FEEDBACK_FORWARDED_ALLOW_IPS: ${FEEDBACK_FORWARDED_ALLOW_IPS:-127.0.0.1}
28-
FEEDBACK_LIMIT_CONCURRENCY: ${FEEDBACK_LIMIT_CONCURRENCY:-64}
29-
FEEDBACK_KEEP_ALIVE_SECONDS: ${FEEDBACK_KEEP_ALIVE_SECONDS:-5}
3029
ports:
31-
- "127.0.0.1:${FEEDBACK_HOST_PORT:-18080}:8080"
30+
- "127.0.0.1:18080:8080"
3231
volumes:
33-
- ${FEEDBACK_CONFIG_FILE:-./config/sites.toml}:/run/config/sites.toml:ro
32+
- ./config/sites.toml:/run/config/sites.toml:ro
3433
- feedback-data:/data
3534
secrets:
3635
- github-app-private-key
@@ -61,11 +60,11 @@ services:
6160

6261
secrets:
6362
github-app-private-key:
64-
file: ${FEEDBACK_GITHUB_APP_PRIVATE_KEY_FILE:-./config/secrets/github-app-private-key.pem}
63+
file: ./config/secrets/github-app-private-key.pem
6564
github-client-secret:
66-
file: ${FEEDBACK_GITHUB_CLIENT_SECRET_FILE:-./config/secrets/github-client-secret}
65+
file: ./config/secrets/github-client-secret
6766
oauth-state-hmac-key:
68-
file: ${FEEDBACK_OAUTH_STATE_HMAC_KEY_FILE:-./config/secrets/oauth-state-hmac-key}
67+
file: ./config/secrets/oauth-state-hmac-key
6968

7069
volumes:
7170
feedback-data:

‎compose.dev.yaml‎

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,11 @@ services:
1010
- -c
1111
- >-
1212
pip install --no-deps --editable . &&
13-
exec uvicorn feedback.app:app --host=0.0.0.0 --port=8080
14-
--reload --no-server-header
15-
environment:
16-
FEEDBACK_CONFIG: /workspace/config/sites.dev.toml
17-
FEEDBACK_DATA_DIRECTORY: /workspace/config/data
18-
FEEDBACK_GITHUB_APP_PRIVATE_KEY_FILE: /workspace/config/secrets/github-app-private-key.pem
19-
FEEDBACK_GITHUB_CLIENT_SECRET_FILE: /workspace/config/secrets/github-client-secret
20-
FEEDBACK_OAUTH_STATE_HMAC_KEY_FILE: /workspace/config/secrets/oauth-state-hmac-key
13+
exec python -m feedback --config=/workspace/config/sites.dev.toml
14+
--github-app-private-key-file=/workspace/config/secrets/github-app-private-key.pem
15+
--github-client-secret-file=/workspace/config/secrets/github-client-secret
16+
--oauth-state-hmac-key-file=/workspace/config/secrets/oauth-state-hmac-key
17+
--host=0.0.0.0 --port=8080
2118
init: true
2219
ports:
2320
- "127.0.0.1:8080:8080"
@@ -31,8 +28,6 @@ services:
3128
context: .
3229
target: test
3330
command: ["pytest", "-q"]
34-
environment:
35-
FEEDBACK_ENV: test
3631
init: true
3732
profiles: ["test"]
3833
read_only: true

‎config/sites.dev.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ http_request_timeout_seconds = 10
1212

1313
[sites.feedback-cpp-social]
1414
origins = ["https://feedback.cpp.social"]
15-
mapping = "id"
15+
mapping = "key"
1616
repository = "cppsocial/feedback"
1717
repository_id = "R_kgDOUc9png"
1818
installation_id = 162105624

‎config/sites.example.toml‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,15 @@ http_request_timeout_seconds = 10
1010

1111
[sites.example]
1212
origins = ["https://www.example.com"]
13-
mapping = "id"
13+
mapping = "key"
1414
repository = "example/site"
1515
repository_id = "replace-with-repository-node-id"
1616
installation_id = 1
1717
category = "Resources"
1818
category_id = "replace-with-category-node-id"
19+
20+
# Optional cache tuning; these are the defaults.
21+
# cache_fresh_seconds = 5
22+
# refresh_cooldown_seconds = 5
23+
# refresh_sweep_seconds = 86400
24+
# max_batch_size = 100

‎config/sites.toml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ http_request_timeout_seconds = 10
1010

1111
[sites.feedback-cpp-social]
1212
origins = ["https://feedback.cpp.social"]
13-
mapping = "id"
13+
mapping = "key"
1414
repository = "cppsocial/feedback"
1515
repository_id = "R_kgDOUc9png"
1616
installation_id = 162105624
@@ -19,7 +19,7 @@ category_id = "DIC_kwDOUc9pns4DFwQt"
1919

2020
[sites.cpp-social]
2121
origins = ["https://cpp.social"]
22-
mapping = "id"
22+
mapping = "key"
2323
repository = "cppsocial/site"
2424
repository_id = "R_kgDOSOfNQQ"
2525
installation_id = 162105624

‎deploy/nixos-module.nix‎

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,6 @@ in
1313
default = "/srv/feedback";
1414
};
1515

16-
port = mkOption {
17-
type = types.port;
18-
default = 18080;
19-
};
20-
21-
forwardedAllowIps = mkOption {
22-
type = types.str;
23-
default = "172.17.0.1";
24-
description = "Addresses trusted to supply forwarded headers to Uvicorn.";
25-
};
26-
2716
proxy = {
2817
enable = mkEnableOption "the feedback nginx virtual host" // { default = true; };
2918

@@ -48,10 +37,6 @@ in
4837
requires = [ "docker.service" ];
4938
wants = [ "network-online.target" ];
5039
wantedBy = [ "multi-user.target" ];
51-
environment = {
52-
FEEDBACK_FORWARDED_ALLOW_IPS = cfg.forwardedAllowIps;
53-
FEEDBACK_HOST_PORT = toString cfg.port;
54-
};
5540
unitConfig.ConditionPathExists = "${cfg.repositoryDirectory}/compose.deploy.yaml";
5641
serviceConfig = {
5742
Type = "simple";
@@ -89,7 +74,7 @@ in
8974
'';
9075
locations = {
9176
"~ ^/v1/sites/[a-z0-9-]+/(?:oauth/(?:authorize|exchange)|discussions/ensure)$" = {
92-
proxyPass = "http://127.0.0.1:${toString cfg.port}";
77+
proxyPass = "http://127.0.0.1:18080";
9378
extraConfig = ''
9479
limit_req zone=feedback_sensitive burst=5 nodelay;
9580
proxy_connect_timeout 3s;
@@ -98,7 +83,7 @@ in
9883
'';
9984
};
10085
"/" = {
101-
proxyPass = "http://127.0.0.1:${toString cfg.port}";
86+
proxyPass = "http://127.0.0.1:18080";
10287
extraConfig = ''
10388
limit_req zone=feedback_general burst=30 nodelay;
10489
proxy_connect_timeout 3s;

‎runtime/pages/example/index.html‎

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,9 @@
1111
<body>
1212
<main>
1313
<h1>Feedback integration example</h1>
14-
<p>This small page exercises cached counts, GitHub authentication, direct voting, and local stars.</p>
14+
<p>This page exercises batched cached counts, targeted refreshes, GitHub authentication, voting, and local stars.</p>
1515
<p>API: <code id="api-origin"></code></p>
16-
<div class="controls" aria-label="Feedback controls">
17-
<button id="up" type="button">▲ 0</button>
18-
<button id="down" type="button">▼ 0</button>
19-
<button id="star" type="button" aria-pressed="false">★ Star</button>
20-
</div>
16+
<div id="cards" class="cards"></div>
2117
<p id="status" role="status">Loading…</p>
2218
</main>
2319
<script src="{{EXAMPLE_SCRIPT}}"></script>

0 commit comments

Comments
 (0)