Queued jobs start after a request answers, so the caller leaving cannot cancel their start #185
Mergeds
syntaqx merged start-after-answer into main
2 files+40−40/2 viewed
| 185 | 185 | limits: limits::Limits, | |
| 186 | 186 | /// Whether repositories' Actions are on, read once per request. | |
| 187 | 187 | actions_on: protection::ActionsOn, | |
| 188 | + | /// In a request, queued jobs are not started inline: whoever made the | |
| 189 | + | /// request may be gone before a sandbox starts, which can take seconds | |
| 190 | + | /// while Cloudflare has no container free, and the start would be | |
| 191 | + | /// cancelled with it, after the jobs were marked as running. They are | |
| 192 | + | /// started after the answer instead (`fetch`, `wait_until`). | |
| 193 | + | defer_starts: bool, | |
| 194 | + | /// Whether this request queued jobs or freed room for them. | |
| 195 | + | starts_wanted: std::cell::Cell<bool>, | |
| 188 | 196 | } | |
| 189 | 197 | ||
| 190 | 198 | impl Actions { | |
| ⋯ | |||
| 205 | 213 | blob_key: env.secret("ACTIONS_KEY").ok().map(|key| runtime::blob_key(&key.to_string())), | |
| 206 | 214 | limits: limits::Limits::from_env(env), | |
| 207 | 215 | actions_on: protection::ActionsOn::default(), | |
| 216 | + | defer_starts: false, | |
| 217 | + | starts_wanted: std::cell::Cell::new(false), | |
| 208 | 218 | }) | |
| 209 | 219 | } | |
| 210 | 220 | ||
| ⋯ | |||
| 284 | 294 | } | |
| 285 | 295 | ||
| 286 | 296 | #[event(fetch)] | |
| 287 | − | async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> { | |
| 297 | + | async fn fetch(mut request: Request, env: Env, ctx: Context) -> Result<Response> { | |
| 288 | 298 | let Some(method) = rpc_method(&request) else { | |
| 289 | 299 | return Response::error("Not found", 404); | |
| 290 | 300 | }; | |
| 291 | 301 | let body: Value = request.json().await?; | |
| 292 | − | let service = Actions::new(&env)?; | |
| 293 | − | match method.as_str() { | |
| 302 | + | let service = Actions { defer_starts: true, ..Actions::new(&env)? }; | |
| 303 | + | let response = answer(&service, &method, body).await; | |
| 304 | + | // The jobs this request queued or made room for start after the | |
| 305 | + | // answer, where the caller leaving cannot cancel them. A start that is | |
| 306 | + | // cut short anyway is tried again (FIRST_REPORT_MS). | |
| 307 | + | if service.starts_wanted.get() { | |
| 308 | + | ctx.wait_until(async move { | |
| 309 | + | match Actions::new(&env) { | |
| 310 | + | Ok(service) => { | |
| 311 | + | if let Err(error) = service.start_queued().await { | |
| 312 | + | worker::console_error!("actions: starting queued jobs after {method} failed: {error}"); | |
| 313 | + | } | |
| 314 | + | } | |
| 315 | + | Err(error) => worker::console_error!("actions: could not start: {error}"), | |
| 316 | + | } | |
| 317 | + | }); | |
| 318 | + | } | |
| 319 | + | response | |
| 320 | + | } | |
| 321 | + | ||
| 322 | + | /// One RPC's answer. | |
| 323 | + | async fn answer(service: &Actions, method: &str, body: Value) -> Result<Response> { | |
| 324 | + | match method { | |
| 294 | 325 | "workflows" => reply(&service.workflows(args(body)?).await?), | |
| 295 | 326 | "workflow" => reply(&service.workflow(args(body)?).await?), | |
| 296 | 327 | "runs" => reply(&service.runs(args(body)?).await?), | |
| 1597 | 1597 | Ok(()) | |
| 1598 | 1598 | } | |
| 1599 | 1599 | ||
| 1600 | − | /// Starts queued jobs, oldest first, while their workspace has room. | |
| 1600 | + | /// Starts queued jobs, oldest first, while their workspace has room. In | |
| 1601 | + | /// a request, only notes that they want starting (`defer_starts`). | |
| 1601 | 1602 | pub async fn start_queued(&self) -> Result<()> { | |
| 1603 | + | if self.defer_starts { | |
| 1604 | + | self.starts_wanted.set(true); | |
| 1605 | + | return Ok(()); | |
| 1606 | + | } | |
| 1602 | 1607 | self.start_queued_except(&std::collections::HashSet::new()).await | |
| 1603 | 1608 | } | |
| 1604 | 1609 |