| 1 | # Scheduled work on Cloud Run |
| 2 | |
| 3 | A 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 | |
| 19 | The 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 | |
| 23 | From the repository root: |
| 24 | |
| 25 | ```sh |
| 26 | pnpm install |
| 27 | cd examples/gcp-cron |
| 28 | pnpm deploy --profile <name> |
| 29 | ``` |
| 30 | |
| 31 | This needs a local Docker daemon to build the image. Then: |
| 32 | |
| 33 | ```sh |
| 34 | curl "$URL/heartbeats?kind=heartbeat&limit=5" |
| 35 | ``` |
| 36 | |
| 37 | Use 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 |
| 42 | ALCHEMY_PROFILE=<name> bun test |
| 43 | ``` |
| 44 | |
| 45 | The 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 |
| 50 | pnpm destroy --profile <name> |
| 51 | ``` |