1# Scheduled work on Cloud Run
2
3A Cloud Run service with two Cloud Scheduler cron handlers that write into BigQuery, and one public route that reads the rows back.
4
5- [`Monitor`](./src/Monitor.ts): a `GCP.Function` (Cloud Run service).
6 - Runs every minute. Records a heartbeat row whose `value` is the delivery lag in milliseconds.
7 - Runs daily at 00:00 UTC. Counts the last day's heartbeats and records the count.
8 - Serves `GET /heartbeats?kind=heartbeat|daily&limit=N`, which returns the newest rows first.
9- [`Heartbeats`](./src/resources.ts): the BigQuery table, in the `Monitoring` dataset.
10
11## Bindings and event sources
12
13| Binding | Grants the service's runtime account |
14| --- | --- |
15| `GCP.CloudScheduler.consumeSchedule` + `GCP.Run.ScheduleEventSource` (×2) | `roles/run.invoker` on the service. Each schedule gets one Cloud Scheduler job that `POST`s to `/__alchemy/scheduler/<id>` with an OIDC token for that account. |
16| `GCP.BigQuery.WriteTable` + `WriteTableHttp` | `roles/bigquery.dataEditor` on the table |
17| `GCP.BigQuery.ReadTable` + `ReadTableHttp` | `roles/bigquery.dataViewer` on the table and `roles/bigquery.jobUser` on the project, because query jobs can only be granted at project level |
18
19The service sets `invokerIamDisabled: true`, so `GET /heartbeats` is public. The schedule routes stay closed. The runtime verifies each delivery's OIDC token, checking both the audience and the service's own account, and answers `401` to anything else.
20
21## Deploy
22
23From the repository root:
24
25```sh
26pnpm install
27cd examples/gcp-cron
28pnpm deploy --profile <name>
29```
30
31This needs a local Docker daemon to build the image. Then:
32
33```sh
34curl "$URL/heartbeats?kind=heartbeat&limit=5"
35```
36
37Use the printed `url`. A heartbeat row appears within a minute. To run a schedule immediately, use `gcloud scheduler jobs run <job> --location <region>`.
38
39## Live test
40
41```sh
42ALCHEMY_PROFILE=<name> bun test
43```
44
45The test deploys the stack and forces one run of each job with `cloudscheduler.runProjectsLocationsJobs`. It polls `GET /heartbeats` until both rows appear and checks them with a direct BigQuery query. Then it destroys the stack and verifies the scheduler jobs and table are gone.
46
47## Destroy
48
49```sh
50pnpm destroy --profile <name>
51```