Coverage for /usr/local/lib/python3.12/site-packages/prefect/server/models/work_queues.py: 54%

253 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-07 02:04 +0000

1""" 

2Functions for interacting with work queue ORM objects. 

3Intended for internal use by the Prefect REST API. 

4""" 

5 

6import datetime 

7from typing import ( 

8 Any, 

9 Awaitable, 

10 Callable, 

11 Dict, 

12 Iterable, 

13 Optional, 

14 Sequence, 

15 Tuple, 

16 Union, 

17 cast, 

18) 

19from uuid import UUID 

20 

21import sqlalchemy as sa 

22from docket import Depends, Retry 

23from pydantic import TypeAdapter 

24from sqlalchemy import delete, select 

25from sqlalchemy.ext.asyncio import AsyncSession 

26 

27import prefect.server.models as models 

28import prefect.server.schemas as schemas 

29from prefect.server.database import ( 

30 PrefectDBInterface, 

31 db_injector, 

32 orm_models, 

33 provide_database_interface, 

34) 

35from prefect.server.events import clients 

36from prefect.server.events.clients import PrefectServerEventsClient 

37from prefect.server.exceptions import ObjectNotFoundError 

38from prefect.server.models.events import ( 

39 work_queue_created_event, 

40 work_queue_deleted_event, 

41 work_queue_status_event, 

42 work_queue_updated_event, 

43) 

44from prefect.server.models.workers import ( 

45 DEFAULT_AGENT_WORK_POOL_NAME, 

46 bulk_update_work_queue_priorities, 

47) 

48from prefect.server.schemas.states import StateType 

49from prefect.server.schemas.statuses import WorkQueueStatus 

50from prefect.server.utilities.database import UUID as PrefectUUID 

51from prefect.types._datetime import DateTime, now 

52 

53WORK_QUEUE_LAST_POLLED_TIMEOUT = datetime.timedelta(seconds=60) 

54 

55 

56@db_injector 

57async def create_work_queue( 

58 db: PrefectDBInterface, 

59 session: AsyncSession, 

60 work_queue: Union[schemas.core.WorkQueue, schemas.actions.WorkQueueCreate], 

61) -> orm_models.WorkQueue: 

62 """ 

63 Inserts a WorkQueue. 

64 

65 If a WorkQueue with the same name exists, an error will be thrown. 

66 

67 Args: 

68 session (AsyncSession): a database session 

69 work_queue (schemas.core.WorkQueue): a WorkQueue model 

70 

71 Returns: 

72 orm_models.WorkQueue: the newly-created or updated WorkQueue 

73 

74 """ 

75 data = work_queue.model_dump() 

76 

77 if data.get("work_pool_id") is None: 77 ↛ 104line 77 didn't jump to line 104 because the condition on line 77 was always true

78 # If no work pool is provided, get or create the default agent work pool 

79 default_agent_work_pool = await models.workers.read_work_pool_by_name( 

80 session=session, work_pool_name=DEFAULT_AGENT_WORK_POOL_NAME 

81 ) 

82 if default_agent_work_pool: 

83 data["work_pool_id"] = default_agent_work_pool.id 

84 else: 

85 default_agent_work_pool = await models.workers.create_work_pool( 

86 session=session, 

87 work_pool=schemas.actions.WorkPoolCreate( 

88 name=DEFAULT_AGENT_WORK_POOL_NAME, type="prefect-agent" 

89 ), 

90 ) 

91 if work_queue.name == "default": 

92 # If the desired work queue name is default, it was created when the 

93 # work pool was created. We can just return it. 

94 default_work_queue = await models.workers.read_work_queue( 

95 session=session, 

96 work_queue_id=default_agent_work_pool.default_queue_id, 

97 ) 

98 assert default_work_queue 

99 return default_work_queue 

100 data["work_pool_id"] = default_agent_work_pool.id 

101 

102 # Set the priority to be the max priority + 1 

103 # This will make the new queue the lowest priority 

104 if data["priority"] is None: 

105 # Set the priority to be the first priority value that isn't already taken 

106 priorities_query = sa.select(db.WorkQueue.priority).where( 

107 db.WorkQueue.work_pool_id == data["work_pool_id"] 

108 ) 

109 priorities = (await session.execute(priorities_query)).scalars().all() 

110 

111 priority = None 

112 for i, p in enumerate(sorted(priorities)): 

113 # if a rank was skipped (e.g. the set priority is different than the 

114 # enumerated priority) then we can "take" that spot for this work 

115 # queue 

116 if i + 1 != p: 

117 priority = i + 1 

118 break 

119 

120 # otherwise take the maximum priority plus one 

121 if priority is None: 

122 priority = max(priorities, default=0) + 1 

123 

124 data["priority"] = priority 

125 

126 model = db.WorkQueue(**data) 

127 

128 session.add(model) 

129 await session.flush() 

130 await session.refresh(model) 

131 

132 if work_queue.priority: 

133 await bulk_update_work_queue_priorities( 

134 session=session, 

135 work_pool_id=data["work_pool_id"], 

136 new_priorities={model.id: work_queue.priority}, 

137 ) 

138 

139 await emit_work_queue_created_event(session=session, work_queue=model) 

140 

141 return model 

142 

143 

144@db_injector 

145async def read_work_queue( 

146 db: PrefectDBInterface, 

147 session: AsyncSession, 

148 work_queue_id: Union[UUID, PrefectUUID], 

149) -> Optional[orm_models.WorkQueue]: 

150 """ 

151 Reads a WorkQueue by id. 

152 

153 Args: 

154 session (AsyncSession): A database session 

155 work_queue_id (str): a WorkQueue id 

156 

157 Returns: 

158 orm_models.WorkQueue: the WorkQueue 

159 """ 

160 

161 return await session.get(db.WorkQueue, work_queue_id) 

162 

163 

164# States counted against work queue concurrency by the ORM query in 

165# query_components.get_scheduled_flow_runs_from_work_queues. 

166SLOT_OCCUPYING_STATES = { 

167 StateType.PENDING, 

168 StateType.RUNNING, 

169 StateType.CANCELLING, 

170} 

171 

172# States counted against legacy tag-based work queue concurrency limits 

173# in _legacy_get_runs_in_work_queue. 

174LEGACY_SLOT_OCCUPYING_STATES = { 

175 StateType.PENDING, 

176 StateType.RUNNING, 

177} 

178 

179 

180async def _count_legacy_queue_active_slots( 

181 session: AsyncSession, 

182 queue_filter: schemas.core.QueueFilter, 

183) -> int: 

184 """Count active slots for a legacy tag-based queue using its filter criteria.""" 

185 return await models.flow_runs.count_flow_runs( 

186 session=session, 

187 flow_run_filter=schemas.filters.FlowRunFilter( 

188 tags=schemas.filters.FlowRunFilterTags(all_=queue_filter.tags), 

189 deployment_id=schemas.filters.FlowRunFilterDeploymentId( 

190 any_=queue_filter.deployment_ids, is_null_=False 

191 ), 

192 state=schemas.filters.FlowRunFilterState( 

193 type=schemas.filters.FlowRunFilterStateType( 

194 any_=list(LEGACY_SLOT_OCCUPYING_STATES) 

195 ) 

196 ), 

197 ), 

198 ) 

199 

200 

201@db_injector 

202async def count_work_queue_active_slots( 

203 db: PrefectDBInterface, 

204 session: AsyncSession, 

205 work_queue_id: UUID, 

206) -> int: 

207 """ 

208 Count flow runs occupying concurrency slots for a given work queue. 

209 

210 For standard queues (including pool-backed and default-agent queues), 

211 counts Pending/Running/Cancelling flow runs by work_queue_id FK. 

212 

213 For legacy tag-based queues, counts Pending/Running flow runs matching 

214 the queue's tag/deployment filter (matching _legacy_get_runs_in_work_queue). 

215 """ 

216 work_queue = await session.get(db.WorkQueue, work_queue_id) 

217 if work_queue is None: 217 ↛ 218line 217 didn't jump to line 218 because the condition on line 217 was never true

218 return 0 

219 

220 if work_queue.filter is not None: 

221 queue_filter = TypeAdapter(schemas.core.QueueFilter).validate_python( 

222 work_queue.filter 

223 ) 

224 return await _count_legacy_queue_active_slots(session, queue_filter) 

225 

226 query = ( 

227 select(sa.func.count()) 

228 .select_from(db.FlowRun) 

229 .where( 

230 db.FlowRun.work_queue_id == work_queue_id, 

231 db.FlowRun.state_type.in_(SLOT_OCCUPYING_STATES), 

232 ) 

233 ) 

234 result = await session.execute(query) 

235 return result.scalar_one() 

236 

237 

238@db_injector 

239async def count_work_queue_active_slots_bulk( 

240 db: PrefectDBInterface, 

241 session: AsyncSession, 

242 work_queue_ids: Sequence[UUID], 

243) -> dict[UUID, int]: 

244 """ 

245 Count active slots for multiple work queues. Standard queues are counted 

246 in a single bulk GROUP BY query; legacy tag-based queues fall back to 

247 per-queue counting since each has its own filter criteria. 

248 """ 

249 if not work_queue_ids: 249 ↛ 250line 249 didn't jump to line 250 because the condition on line 249 was never true

250 return {} 

251 

252 # Load only id and filter to classify queue types without full ORM hydration 

253 query = select(db.WorkQueue.id, db.WorkQueue.filter).where( 

254 db.WorkQueue.id.in_(work_queue_ids) 

255 ) 

256 result = await session.execute(query) 

257 rows = result.all() 

258 

259 standard_ids: list[UUID] = [] 

260 legacy_filters: list[tuple[UUID, Any]] = [] 

261 for wq_id, wq_filter in rows: 

262 if wq_filter is not None: 

263 legacy_filters.append((wq_id, wq_filter)) 

264 else: 

265 standard_ids.append(wq_id) 

266 

267 counts: dict[UUID, int] = {} 

268 

269 if standard_ids: 

270 bulk_query = ( 

271 select( 

272 db.FlowRun.work_queue_id, 

273 sa.func.count(db.FlowRun.id), 

274 ) 

275 .select_from(db.FlowRun) 

276 .where( 

277 db.FlowRun.work_queue_id.in_(standard_ids), 

278 db.FlowRun.state_type.in_(SLOT_OCCUPYING_STATES), 

279 ) 

280 .group_by(db.FlowRun.work_queue_id) 

281 ) 

282 bulk_result = await session.execute(bulk_query) 

283 counts.update(dict(bulk_result.all())) 

284 

285 for wq_id, wq_filter in legacy_filters: 

286 queue_filter = TypeAdapter(schemas.core.QueueFilter).validate_python(wq_filter) 

287 counts[wq_id] = await _count_legacy_queue_active_slots(session, queue_filter) 

288 

289 return counts 

290 

291 

292@db_injector 

293async def read_work_queue_by_name( 

294 db: PrefectDBInterface, session: AsyncSession, name: str 

295) -> Optional[orm_models.WorkQueue]: 

296 """ 

297 Reads a WorkQueue by name. 

298 

299 Args: 

300 session (AsyncSession): A database session 

301 name (str): a WorkQueue name 

302 

303 Returns: 

304 orm_models.WorkQueue: the WorkQueue 

305 """ 

306 default_work_pool = await models.workers.read_work_pool_by_name( 

307 session=session, work_pool_name=DEFAULT_AGENT_WORK_POOL_NAME 

308 ) 

309 # Logic to make sure this functionality doesn't break during migration 

310 if default_work_pool is not None: 

311 query = select(db.WorkQueue).filter_by( 

312 name=name, work_pool_id=default_work_pool.id 

313 ) 

314 else: 

315 query = select(db.WorkQueue).filter_by(name=name) 

316 result = await session.execute(query) 

317 return result.scalar() 

318 

319 

320@db_injector 

321async def read_work_queues( 

322 db: PrefectDBInterface, 

323 session: AsyncSession, 

324 offset: Optional[int] = None, 

325 limit: Optional[int] = None, 

326 work_queue_filter: Optional[schemas.filters.WorkQueueFilter] = None, 

327) -> Sequence[orm_models.WorkQueue]: 

328 """ 

329 Read WorkQueues. 

330 

331 Args: 

332 session: A database session 

333 offset: Query offset 

334 limit: Query limit 

335 work_queue_filter: only select work queues matching these filters 

336 Returns: 

337 Sequence[orm_models.WorkQueue]: WorkQueues 

338 """ 

339 

340 query = select(db.WorkQueue).order_by(db.WorkQueue.name) 

341 

342 if offset is not None: 342 ↛ 344line 342 didn't jump to line 344 because the condition on line 342 was always true

343 query = query.offset(offset) 

344 if limit is not None: 344 ↛ 346line 344 didn't jump to line 346 because the condition on line 344 was always true

345 query = query.limit(limit) 

346 if work_queue_filter: 

347 query = query.where(work_queue_filter.as_sql_filter()) 

348 

349 result = await session.execute(query) 

350 return result.scalars().unique().all() 

351 

352 

353def is_last_polled_recent(last_polled: Optional[DateTime]) -> bool: 

354 if last_polled is None: 

355 return False 

356 return (now("UTC") - last_polled) <= WORK_QUEUE_LAST_POLLED_TIMEOUT 

357 

358 

359@db_injector 

360async def update_work_queue( 

361 db: PrefectDBInterface, 

362 session: AsyncSession, 

363 work_queue_id: UUID, 

364 work_queue: schemas.actions.WorkQueueUpdate, 

365 emit_status_change: Optional[ 

366 Callable[[orm_models.WorkQueue], Awaitable[None]] 

367 ] = None, 

368) -> bool: 

369 """ 

370 Update a WorkQueue by id. 

371 

372 Args: 

373 session (AsyncSession): A database session 

374 work_queue: the work queue data 

375 work_queue_id (str): a WorkQueue id 

376 

377 Returns: 

378 bool: whether or not the WorkQueue was updated 

379 """ 

380 # exclude_unset=True allows us to only update values provided by 

381 # the user, ignoring any defaults on the model 

382 update_data = work_queue.model_dump_for_orm(exclude_unset=True) 

383 current_work_queue = await read_work_queue( 

384 session=session, work_queue_id=work_queue_id 

385 ) 

386 if current_work_queue is None: 386 ↛ 387line 386 didn't jump to line 387 because the condition on line 386 was never true

387 return False 

388 

389 session.expunge(current_work_queue) 

390 

391 if "is_paused" in update_data: 

392 # Only update the status to paused if it's not already paused. This ensures a work queue that is already 

393 # paused will not get a status update if it's paused again 

394 if ( 

395 update_data.get("is_paused") 

396 and current_work_queue.status != WorkQueueStatus.PAUSED 

397 ): 

398 update_data["status"] = WorkQueueStatus.PAUSED 

399 

400 # If unpausing, only update status if it's currently paused. This ensures a work queue that is already 

401 # unpaused will not get a status update if it's unpaused again 

402 if ( 402 ↛ 407line 402 didn't jump to line 407 because the condition on line 402 was never true

403 update_data.get("is_paused") is False 

404 and current_work_queue.status == WorkQueueStatus.PAUSED 

405 ): 

406 # Default status if unpaused 

407 update_data["status"] = WorkQueueStatus.NOT_READY 

408 

409 # Determine source of last_polled: update_data or database 

410 last_polled: Optional[DateTime] 

411 if "last_polled" in update_data: 

412 last_polled = cast(DateTime, update_data["last_polled"]) 

413 else: 

414 last_polled = current_work_queue.last_polled 

415 

416 # Check if last polled is recent and set status to READY if so 

417 if is_last_polled_recent(last_polled): 

418 update_data["status"] = schemas.statuses.WorkQueueStatus.READY 

419 

420 update_stmt = ( 

421 sa.update(db.WorkQueue) 

422 .where(db.WorkQueue.id == work_queue_id) 

423 .values(**update_data) 

424 ) 

425 result = await session.execute(update_stmt) 

426 updated = result.rowcount > 0 

427 

428 if updated: 

429 wq = await read_work_queue(session=session, work_queue_id=work_queue_id) 

430 assert wq is not None 

431 assert current_work_queue is not wq 

432 WORK_QUEUE_EVENT_FIELDS = { 

433 "name", 

434 "description", 

435 "concurrency_limit", 

436 "priority", 

437 # Exclude "is_paused" - handled with status 

438 # Exclude "last_polled" - usually auto-updated 

439 # Exclude "filter" - deprecated 

440 } 

441 # Detect which fields actually changed 

442 changed_fields = {} 

443 for field in update_data.keys(): 

444 if field not in WORK_QUEUE_EVENT_FIELDS or field == "status": 

445 continue 

446 

447 old_value = getattr(current_work_queue, field, None) 

448 new_value = getattr(wq, field, None) 

449 

450 if old_value != new_value: 

451 changed_fields[field] = { 

452 "from": old_value, 

453 "to": new_value, 

454 } 

455 

456 # Emit event for non-status field changes 

457 if changed_fields: 

458 await emit_work_queue_updated_event( 

459 session=session, 

460 work_queue=wq, 

461 changed_fields=changed_fields, 

462 ) 

463 

464 if "status" in update_data and emit_status_change: 

465 await emit_status_change(wq) 

466 

467 return updated 

468 

469 

470@db_injector 

471async def delete_work_queue( 

472 db: PrefectDBInterface, session: AsyncSession, work_queue_id: UUID 

473) -> bool: 

474 """ 

475 Delete a WorkQueue by id. 

476 

477 Args: 

478 session (AsyncSession): A database session 

479 work_queue_id (str): a WorkQueue id 

480 

481 Returns: 

482 bool: whether or not the WorkQueue was deleted 

483 """ 

484 work_queue = await session.get(db.WorkQueue, work_queue_id) 

485 if work_queue is None: 485 ↛ 486line 485 didn't jump to line 486 because the condition on line 485 was never true

486 return False 

487 

488 await emit_work_queue_deleted_event(session=session, work_queue=work_queue) 

489 

490 await session.execute(delete(db.WorkQueue).where(db.WorkQueue.id == work_queue_id)) 

491 return True 

492 

493 

494@db_injector 

495async def get_runs_in_work_queue( 

496 db: PrefectDBInterface, 

497 session: AsyncSession, 

498 work_queue_id: UUID, 

499 limit: Optional[int] = None, 

500 scheduled_before: Optional[datetime.datetime] = None, 

501) -> Tuple[orm_models.WorkQueue, Sequence[orm_models.FlowRun]]: 

502 """ 

503 Get runs from a work queue. 

504 

505 Args: 

506 session: A database session. work_queue_id: The work queue id. 

507 scheduled_before: Only return runs scheduled to start before this time. 

508 limit: An optional limit for the number of runs to return from the 

509 queue. This limit applies to the request only. It does not affect 

510 the work queue's concurrency limit. If `limit` exceeds the work 

511 queue's concurrency limit, it will be ignored. 

512 

513 """ 

514 work_queue = await read_work_queue(session=session, work_queue_id=work_queue_id) 

515 if not work_queue: 

516 raise ObjectNotFoundError(f"Work queue with id {work_queue_id} not found.") 

517 

518 if work_queue.filter is None: 

519 query = db.queries.get_scheduled_flow_runs_from_work_queues( 

520 limit_per_queue=limit, 

521 work_queue_ids=[work_queue_id], 

522 scheduled_before=scheduled_before, 

523 ) 

524 result = await session.execute(query) 

525 return work_queue, result.scalars().unique().all() 

526 

527 # if the work queue has a filter, it's a deprecated tag-based work queue 

528 # and uses an old approach 

529 else: 

530 return work_queue, await _legacy_get_runs_in_work_queue( 

531 session=session, 

532 work_queue_id=work_queue_id, 

533 scheduled_before=scheduled_before, 

534 limit=limit, 

535 ) 

536 

537 

538async def _legacy_get_runs_in_work_queue( 

539 session: AsyncSession, 

540 work_queue_id: UUID, 

541 scheduled_before: Optional[datetime.datetime] = None, 

542 limit: Optional[int] = None, 

543) -> Sequence[orm_models.FlowRun]: 

544 """ 

545 DEPRECATED method for getting runs from a tag-based work queue 

546 

547 Args: 

548 session: A database session. 

549 work_queue_id: The work queue id. 

550 scheduled_before: Only return runs scheduled to start before this time. 

551 limit: An optional limit for the number of runs to return from the queue. 

552 This limit applies to the request only. It does not affect the 

553 work queue's concurrency limit. If `limit` exceeds the work queue's 

554 concurrency limit, it will be ignored. 

555 

556 """ 

557 

558 work_queue = await read_work_queue(session=session, work_queue_id=work_queue_id) 

559 if not work_queue: 559 ↛ 560line 559 didn't jump to line 560 because the condition on line 559 was never true

560 raise ObjectNotFoundError(f"Work queue with id {work_queue_id} not found.") 

561 

562 if work_queue.is_paused: 562 ↛ 568line 562 didn't jump to line 568 because the condition on line 562 was always true

563 return [] 

564 

565 # ensure the filter object is fully hydrated 

566 # SQLAlchemy caching logic can result in a dict type instead 

567 # of the full pydantic model 

568 work_queue_filter = TypeAdapter(schemas.core.QueueFilter).validate_python( 

569 work_queue.filter 

570 ) 

571 flow_run_filter = dict( 

572 tags=dict(all_=work_queue_filter.tags), 

573 deployment_id=dict(any_=work_queue_filter.deployment_ids, is_null_=False), 

574 ) 

575 

576 # if the work queue has a concurrency limit, check how many runs are currently 

577 # executing and compare that count to the concurrency limit 

578 if work_queue.concurrency_limit is not None: 

579 # Note this does not guarantee race conditions won't be hit 

580 running_frs = await models.flow_runs.count_flow_runs( 

581 session=session, 

582 flow_run_filter=schemas.filters.FlowRunFilter( 

583 **flow_run_filter, 

584 state=dict(type=dict(any_=[StateType.PENDING, StateType.RUNNING])), 

585 ), 

586 ) 

587 

588 # compute the available concurrency slots 

589 open_concurrency_slots = max(0, work_queue.concurrency_limit - running_frs) 

590 

591 # if a limit override was given, ensure we return no more 

592 # than that limit 

593 if limit is not None: 

594 limit = min(open_concurrency_slots, limit) 

595 else: 

596 limit = open_concurrency_slots 

597 

598 return await models.flow_runs.read_flow_runs( 

599 session=session, 

600 flow_run_filter=schemas.filters.FlowRunFilter( 

601 **flow_run_filter, 

602 state=dict(type=dict(any_=[StateType.SCHEDULED])), 

603 next_scheduled_start_time=dict(before_=scheduled_before), 

604 ), 

605 limit=limit, 

606 sort=schemas.sorting.FlowRunSort.NEXT_SCHEDULED_START_TIME_ASC, 

607 ) 

608 

609 

610async def ensure_work_queue_exists( 

611 session: AsyncSession, name: str 

612) -> orm_models.WorkQueue: 

613 """ 

614 Checks if a work queue exists and creates it if it does not. 

615 

616 Useful when working with deployments, agents, and flow runs that automatically create work queues. 

617 

618 Will also create a work pool queue in the default agent pool to facilitate migration to work pools. 

619 """ 

620 # read work queue 

621 work_queue = await models.work_queues.read_work_queue_by_name( 

622 session=session, name=name 

623 ) 

624 if not work_queue: 

625 default_pool = await models.workers.read_work_pool_by_name( 

626 session=session, work_pool_name=DEFAULT_AGENT_WORK_POOL_NAME 

627 ) 

628 

629 if default_pool is None: 

630 work_queue = await models.work_queues.create_work_queue( 

631 session=session, 

632 work_queue=schemas.actions.WorkQueueCreate(name=name, priority=1), 

633 ) 

634 else: 

635 if name != "default": 

636 work_queue = await models.workers.create_work_queue( 

637 session=session, 

638 work_pool_id=default_pool.id, 

639 work_queue=schemas.actions.WorkQueueCreate(name=name, priority=1), 

640 ) 

641 else: 

642 work_queue = await models.work_queues.read_work_queue( 

643 session=session, work_queue_id=default_pool.default_queue_id 

644 ) 

645 assert work_queue, "Default work queue not found" 

646 

647 return work_queue 

648 

649 

650async def read_work_queue_status( 

651 session: AsyncSession, work_queue_id: UUID 

652) -> schemas.core.WorkQueueStatusDetail: 

653 """ 

654 Get work queue status by id. 

655 

656 Args: 

657 session (AsyncSession): A database session 

658 work_queue_id (str): a WorkQueue id 

659 

660 Returns: 

661 Information about the status of the work queue. 

662 """ 

663 

664 work_queue = await read_work_queue(session=session, work_queue_id=work_queue_id) 

665 if not work_queue: 665 ↛ 668line 665 didn't jump to line 668 because the condition on line 665 was always true

666 raise ObjectNotFoundError(f"Work queue with id {work_queue_id} not found") 

667 

668 work_queue_late_runs_count = await models.flow_runs.count_flow_runs( 

669 session=session, 

670 flow_run_filter=schemas.filters.FlowRunFilter( 

671 state=schemas.filters.FlowRunFilterState(name={"any_": ["Late"]}), 

672 ), 

673 work_queue_filter=schemas.filters.WorkQueueFilter( 

674 id=schemas.filters.WorkQueueFilterId(any_=[work_queue_id]) 

675 ), 

676 ) 

677 

678 # All work queues use the default policy for now 

679 health_check_policy = schemas.core.WorkQueueHealthPolicy( 

680 maximum_late_runs=0, maximum_seconds_since_last_polled=60 

681 ) 

682 

683 healthy = health_check_policy.evaluate_health_status( 

684 late_runs_count=work_queue_late_runs_count, 

685 last_polled=work_queue.last_polled, # type: ignore 

686 ) 

687 

688 return schemas.core.WorkQueueStatusDetail( 

689 healthy=healthy, 

690 late_runs_count=work_queue_late_runs_count, 

691 last_polled=work_queue.last_polled, 

692 health_check_policy=health_check_policy, 

693 ) 

694 

695 

696@db_injector 

697async def record_work_queue_polls( 

698 db: PrefectDBInterface, 

699 session: AsyncSession, 

700 polled_work_queue_ids: Sequence[UUID], 

701 ready_work_queue_ids: Sequence[UUID], 

702) -> None: 

703 """Record that the given work queues were polled, and also update the given 

704 ready_work_queue_ids to READY.""" 

705 polled = now("UTC") 

706 

707 if polled_work_queue_ids: 

708 await session.execute( 

709 sa.update(db.WorkQueue) 

710 .where(db.WorkQueue.id.in_(polled_work_queue_ids)) 

711 .values(last_polled=polled) 

712 ) 

713 

714 if ready_work_queue_ids: 714 ↛ exitline 714 didn't return from function 'record_work_queue_polls' because the condition on line 714 was always true

715 await session.execute( 

716 sa.update(db.WorkQueue) 

717 .where(db.WorkQueue.id.in_(ready_work_queue_ids)) 

718 .values(last_polled=polled, status=WorkQueueStatus.READY) 

719 ) 

720 

721 

722async def mark_work_queues_ready( 

723 *, 

724 db: PrefectDBInterface = Depends(provide_database_interface), 

725 polled_work_queue_ids: Sequence[UUID], 

726 ready_work_queue_ids: Sequence[UUID], 

727 retry: Retry = Retry(attempts=5, delay=datetime.timedelta(seconds=0.5)), 

728) -> None: 

729 async with db.session_context(begin_transaction=True) as session: 

730 await record_work_queue_polls( 

731 session=session, 

732 polled_work_queue_ids=polled_work_queue_ids, 

733 ready_work_queue_ids=ready_work_queue_ids, 

734 ) 

735 

736 # Emit events for any work queues that have transitioned to ready during this poll 

737 # Uses a separate transaction to avoid keeping locks open longer from the updates 

738 # in the previous transaction 

739 if not ready_work_queue_ids: 

740 return 

741 

742 async with db.session_context(begin_transaction=True) as session: 

743 newly_ready_work_queues = await session.execute( 

744 sa.select(db.WorkQueue).where(db.WorkQueue.id.in_(ready_work_queue_ids)) 

745 ) 

746 

747 events = [ 

748 await work_queue_status_event( 

749 session=session, 

750 work_queue=work_queue, 

751 occurred=now("UTC"), 

752 ) 

753 for work_queue in newly_ready_work_queues.scalars().all() 

754 ] 

755 

756 async with PrefectServerEventsClient() as events_client: 

757 for event in events: 

758 await events_client.emit(event) 

759 

760 

761@db_injector 

762async def mark_work_queues_not_ready( 

763 db: PrefectDBInterface, 

764 work_queue_ids: Iterable[UUID], 

765) -> None: 

766 if not work_queue_ids: 

767 return 

768 

769 async with db.session_context(begin_transaction=True) as session: 

770 await session.execute( 

771 sa.update(db.WorkQueue) 

772 .where(db.WorkQueue.id.in_(work_queue_ids)) 

773 .values(status=WorkQueueStatus.NOT_READY) 

774 ) 

775 

776 # Emit events for any work queues that have transitioned to ready during this poll 

777 # Uses a separate transaction to avoid keeping locks open longer from the updates 

778 # in the previous transaction 

779 

780 async with db.session_context(begin_transaction=True) as session: 

781 newly_unready_work_queues = await session.execute( 

782 sa.select(db.WorkQueue).where(db.WorkQueue.id.in_(work_queue_ids)) 

783 ) 

784 

785 events = [ 

786 await work_queue_status_event( 

787 session=session, 

788 work_queue=work_queue, 

789 occurred=now("UTC"), 

790 ) 

791 for work_queue in newly_unready_work_queues.scalars().all() 

792 ] 

793 

794 async with PrefectServerEventsClient() as events_client: 

795 for event in events: 

796 await events_client.emit(event) 

797 

798 

799@db_injector 

800async def emit_work_queue_status_event( 

801 db: PrefectDBInterface, 

802 work_queue: orm_models.WorkQueue, 

803) -> None: 

804 """Emit an event when work queue fields are updated.""" 

805 async with db.session_context() as session: 

806 event = await work_queue_status_event( 

807 session=session, 

808 work_queue=work_queue, 

809 occurred=now("UTC"), 

810 ) 

811 async with PrefectServerEventsClient() as events_client: 

812 await events_client.emit(event) 

813 

814 

815async def emit_work_queue_updated_event( 

816 session: AsyncSession, 

817 work_queue: orm_models.WorkQueue, 

818 changed_fields: Dict[str, Dict[str, Any]], 

819) -> None: 

820 if not changed_fields: 820 ↛ 821line 820 didn't jump to line 821 because the condition on line 820 was never true

821 return 

822 

823 async with PrefectServerEventsClient() as events_client: 

824 await events_client.emit( 

825 await work_queue_updated_event( 

826 session=session, 

827 work_queue=work_queue, 

828 changed_fields=changed_fields, 

829 occurred=now("UTC"), 

830 ) 

831 ) 

832 

833 

834async def emit_work_queue_created_event( 

835 session: AsyncSession, 

836 work_queue: orm_models.WorkQueue, 

837) -> None: 

838 """Emit an event when a work queue is created.""" 

839 async with clients.PrefectServerEventsClient() as events_client: 

840 await events_client.emit( 

841 await work_queue_created_event( 

842 session=session, 

843 work_queue=work_queue, 

844 occurred=now("UTC"), 

845 ) 

846 ) 

847 

848 

849async def emit_work_queue_deleted_event( 

850 session: AsyncSession, 

851 work_queue: orm_models.WorkQueue, 

852) -> None: 

853 """Emit an event when a work queue is deleted.""" 

854 async with clients.PrefectServerEventsClient() as events_client: 

855 await events_client.emit( 

856 await work_queue_deleted_event( 

857 session=session, 

858 work_queue=work_queue, 

859 occurred=now("UTC"), 

860 ) 

861 )