add README.md
This commit is contained in:
parent
50df898e62
commit
63e01c5b6d
1 changed files with 133 additions and 0 deletions
133
README.md
Normal file
133
README.md
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
# st-agent-worker
|
||||
|
||||
Assigns Solidarity Tech agents to department members automatically.
|
||||
|
||||
When someone is given the Department Rep role in Solidarity Tech, an ST
|
||||
automation calls this service. The service looks up that person's chapter
|
||||
(campus) and department, finds every eligible member of the same
|
||||
chapter/department pair, and assigns the rep as their agent.
|
||||
|
||||
## Rules
|
||||
|
||||
- Only members whose appointment type is Senate or Recall are assigned.
|
||||
Set `ELIGIBLE_APPOINTMENTS` to change this.
|
||||
- Matching is on primary chapteri (UCLAFA, BFA, SCFA...) plus Department 1.
|
||||
A rep at one campus is never assigned members at another.
|
||||
- Reps are excluded from assignment. Organizer leads (OC members, organizing
|
||||
staff) assign their agents by hand in the ST web interface.
|
||||
- **Fill-only**: a member who already has an agent is left alone and
|
||||
recorded in the `skips` table. No agent is ever reassigned automatically.
|
||||
See below for consequences of this.
|
||||
|
||||
### Known consequence of fill-only
|
||||
|
||||
Some departments currently have two reps. Whichever rep runs first takes
|
||||
the whole department; the second gets nobody. The reconciler logs a warning
|
||||
in both cases that can be seen in the logs and reconciled manually.
|
||||
|
||||
_Potential fix_ In the future, round-robin splitting could be implemented. `RepsFor` already returns all reps for a department sorted by ID, so adding it is a
|
||||
change to how the agent is picked, and wouldn't require a code restructuring.
|
||||
|
||||
## Architecture
|
||||
|
||||
Four components do the work:
|
||||
|
||||
- `server.go` — HTTP handler. Verifies the shared secret, reads the rep's
|
||||
ST user ID from the query string, looks up their chapter and department
|
||||
live, takes a per-department lock, returns 200, and runs the
|
||||
reconciliation in the background. The 200 is returned before any work
|
||||
happens, because ST times out and retries otherwise.
|
||||
- `client.go` — Solidarity Tech API client. Holds the rate limiter and
|
||||
handles 429 responses. Nothing above it builds a URL.
|
||||
- `store.go` — SQLite database handle. Four tables: `people` (cached roster),
|
||||
`runs` (locks and progress), `skips` (members left alone), `sync_state` (last
|
||||
sync timestamp).
|
||||
- `reconcile.go` — the loop. Reads department members from the cache,
|
||||
checks each one's current agent live against the API, assigns or skips.
|
||||
|
||||
Roster data is cached because it changes more slowly, and API rate limits would make a read/write of the whole roster take two hours. Instead, agent assignments are read live on every check, so an assignment made by hand in the dashboard
|
||||
during a run can be respected right away. See _Rate limits_ below.
|
||||
|
||||
### Locking
|
||||
|
||||
`runs` has a partial unique index on `(chapter_id, department)` where
|
||||
status is `running`. A second webhook for the same department fails the
|
||||
insert and exits. On startup, any row still marked `running` belongs to a
|
||||
dead process and is marked `crashed`.
|
||||
|
||||
### Rate limits
|
||||
|
||||
The ST API allows 60 requests per 30 seconds per API key, which averages
|
||||
2 per second and permits bursts. The client is configured to match this.
|
||||
|
||||
## Configuration
|
||||
|
||||
| Variable | Meaning |
|
||||
| --- | --- |
|
||||
| `ST_API_KEY` | Solidarity Tech API key |
|
||||
| `WEBHOOK_SECRET` | Shared secret, passed as `?s=` on the webhook URL |
|
||||
| `DB_PATH` | SQLite file. `/data/worker.db` in the container |
|
||||
| `DRY_RUN` | Any non-empty value logs writes instead of sending them |
|
||||
| `ELIGIBLE_APPOINTMENTS` | Comma-separated labels, defaults to `senate,recall` |
|
||||
|
||||
The ST webhook action has no support for custom headers, so the secret
|
||||
travels in the query string. Do not log raw query strings.
|
||||
|
||||
## Endpoints
|
||||
|
||||
- `POST /webhook?id={user_id}&s={secret}` — triggered by the ST automation
|
||||
when someone is assigned the Department Rep role.
|
||||
- `POST /debug/sync?s={secret}` — forces a roster sync. Useful after a bulk
|
||||
import rather than waiting for the hourly update.
|
||||
- `GET /healthz` — returns 200 once the server is listening.
|
||||
|
||||
## Health checks
|
||||
|
||||
The startup sync runs before the server binds, and a cold start with an
|
||||
empty volume pulls the full roster, which takes about 90 seconds. Give the
|
||||
container a `start_period` of at least 120 seconds or it will be marked
|
||||
unhealthy before it comes up.
|
||||
|
||||
After deploying, check:
|
||||
|
||||
curl https://$HOST/healthz
|
||||
|
||||
and confirm the log shows a completed sync. Then:
|
||||
|
||||
sqlite3 /data/worker.db "
|
||||
SELECT COUNT(*) total, SUM(is_eligible) eligible, SUM(is_rep) reps
|
||||
FROM people;"
|
||||
|
||||
Should give roughly 17,500 total, 14,700 eligible.
|
||||
|
||||
Log lines to watch for:
|
||||
|
||||
- `WARNING: run N: assigned nobody` — every member already had an agent.
|
||||
Expected for the second rep in a co-repped department, unexpected
|
||||
otherwise.
|
||||
- `N eligible users synced with no department` — those members cannot be
|
||||
assigned to anyone. These are likely Senate or Recall duplicates, or
|
||||
people we don't have information for.
|
||||
|
||||
## Initial backfill
|
||||
|
||||
The webhook only fires when a rep is newly assigned the role, so reps who
|
||||
already had it when this was deployed never trigger it. To assign their
|
||||
departments, call the endpoint for each:
|
||||
|
||||
sqlite3 worker.db "SELECT id FROM people WHERE is_rep = 1;" | while read id; do
|
||||
curl -s -o /dev/null -X POST "https://$HOST/webhook?id=$id&s=$WEBHOOK_SECRET"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
Run it with `DRY_RUN` set first and count the assignments in the log.
|
||||
|
||||
## Not built yet
|
||||
|
||||
- A nightly sweep to reassign reps. Without it, a rep who steps down keeps
|
||||
their assignments and their replacement inherits nobody. Every member added
|
||||
after their department's rep signed up also goes unassigned until
|
||||
something triggers that department again. **FIRST PRIORITY**
|
||||
- Schema migrations. The schema is created with `CREATE TABLE IF NOT
|
||||
EXISTS`, which does not add columns to an existing table. Changing the
|
||||
schema currently means deleting the database and re-syncing.
|
||||
Loading…
Add table
Add a link
Reference in a new issue