Coverage for /usr/local/lib/python3.12/site-packages/prefect/server/worker_communication/cleanup_queue/memory.py: 21%

321 statements  

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

1from __future__ import annotations 

2 

3import asyncio 

4from collections.abc import Iterable, Mapping 

5from copy import deepcopy 

6from dataclasses import dataclass 

7from datetime import timedelta 

8from secrets import token_urlsafe 

9from typing import Any, TypeVar 

10from uuid import UUID 

11 

12from prefect.client.schemas.worker_channel import CleanupKind 

13from prefect.server.worker_communication.cleanup_queue import ( 

14 CleanupQueueDeadLetter, 

15 CleanupQueueLeaseExpiryResult, 

16 CleanupQueueMessage, 

17 CleanupQueueOperation, 

18 CleanupQueueOperationResult, 

19 CleanupQueueReservation, 

20 CleanupQueueWakeup, 

21 record_cleanup_queue_dead_letter, 

22 record_cleanup_queue_lease_expiry_result, 

23 record_cleanup_queue_operation, 

24) 

25from prefect.server.worker_communication.cleanup_queue import ( 

26 WorkerCleanupQueue as _WorkerCleanupQueue, 

27) 

28from prefect.settings.context import get_current_settings 

29from prefect.types import DateTime 

30from prefect.types._datetime import now 

31 

32_T = TypeVar("_T", bound=CleanupQueueMessage | CleanupQueueDeadLetter) 

33 

34 

35@dataclass(frozen=True) 

36class _Reservation: 

37 token: str 

38 lease_expires_at: DateTime 

39 reserved_at: DateTime 

40 

41 

42@dataclass(frozen=True) 

43class _AckedMessage: 

44 message: CleanupQueueMessage 

45 completed_at: DateTime 

46 

47 

48@dataclass(frozen=True) 

49class _QueuePolicy: 

50 lease_duration: timedelta 

51 max_delivery_attempts: int 

52 completed_idempotency_retention: timedelta | None 

53 

54 

55def _copy_model(model: _T) -> _T: 

56 return model.model_copy(deep=True) 

57 

58 

59class WorkerCleanupQueue(_WorkerCleanupQueue): 

60 """ 

61 Singleton in-memory cleanup queue storage for a single server process. 

62 """ 

63 

64 _DEFAULT_EXPIRE_LEASE_LIMIT = 100 

65 _instance: "WorkerCleanupQueue | None" = None 

66 _initialized = False 

67 

68 def __new__(cls) -> "WorkerCleanupQueue": 

69 if cls._instance is None: 69 ↛ 71line 69 didn't jump to line 71 because the condition on line 69 was always true

70 cls._instance = super().__new__(cls) 

71 return cls._instance 

72 

73 def __init__(self) -> None: 

74 if self.__class__._initialized: 74 ↛ 75line 74 didn't jump to line 75 because the condition on line 74 was never true

75 return 

76 

77 self._messages: dict[UUID, CleanupQueueMessage] = {} 

78 self._reservations: dict[UUID, _Reservation] = {} 

79 self._dead_letters: dict[UUID, CleanupQueueDeadLetter] = {} 

80 self._acked_messages: dict[UUID, _AckedMessage] = {} 

81 self._idempotency_keys: dict[tuple[UUID, str], UUID] = {} 

82 self._wakeup_sequences: dict[UUID, int] = {} 

83 self._lock = asyncio.Lock() 

84 self._condition = asyncio.Condition() 

85 self.__class__._initialized = True 

86 

87 def clear(self) -> None: 

88 self._messages.clear() 

89 self._reservations.clear() 

90 self._dead_letters.clear() 

91 self._acked_messages.clear() 

92 self._idempotency_keys.clear() 

93 self._wakeup_sequences.clear() 

94 self._lock = asyncio.Lock() 

95 self._condition = asyncio.Condition() 

96 

97 async def enqueue( 

98 self, 

99 *, 

100 message_id: UUID, 

101 idempotency_key: str, 

102 work_pool_id: UUID, 

103 kind: CleanupKind, 

104 target: Mapping[str, Any], 

105 data: Mapping[str, Any] | None = None, 

106 work_queue_id: UUID | None = None, 

107 ) -> CleanupQueueMessage: 

108 if not idempotency_key: 

109 raise ValueError("idempotency_key must be non-empty") 

110 

111 should_wake = False 

112 policy = self._policy() 

113 async with self._lock: 

114 current_time = now("UTC") 

115 self._prune_completed_idempotency_locked( 

116 current_time=current_time, policy=policy 

117 ) 

118 existing = self._read_existing_message_locked( 

119 message_id=message_id, 

120 idempotency_key=idempotency_key, 

121 work_pool_id=work_pool_id, 

122 ) 

123 if existing is not None: 

124 should_wake = ( 

125 existing.message_id in self._messages 

126 and existing.message_id not in self._reservations 

127 ) 

128 result = _copy_model(existing) 

129 record_cleanup_queue_operation( 

130 "enqueue", 

131 status="duplicate", 

132 work_pool_id=work_pool_id, 

133 message_id=message_id, 

134 cleanup_kind=str(kind), 

135 ) 

136 else: 

137 message = CleanupQueueMessage( 

138 message_id=message_id, 

139 idempotency_key=idempotency_key, 

140 work_pool_id=work_pool_id, 

141 work_queue_id=work_queue_id, 

142 kind=kind, 

143 target=deepcopy(dict(target)), 

144 data=deepcopy(dict(data or {})), 

145 created_at=current_time, 

146 updated_at=current_time, 

147 ) 

148 self._messages[message_id] = message 

149 self._idempotency_keys[(work_pool_id, idempotency_key)] = message_id 

150 result = _copy_model(message) 

151 should_wake = True 

152 record_cleanup_queue_operation( 

153 "enqueue", 

154 status="accepted", 

155 work_pool_id=work_pool_id, 

156 message_id=message_id, 

157 cleanup_kind=str(kind), 

158 ) 

159 

160 if should_wake: 

161 await self.wake_dispatchers(work_pool_id) 

162 return result 

163 

164 async def reserve( 

165 self, 

166 *, 

167 work_pool_id: UUID, 

168 cleanup_kinds: Iterable[CleanupKind] | None = None, 

169 preferred_work_queue_ids: Iterable[UUID] | None = None, 

170 allow_fallback_to_any_queue: bool = True, 

171 ) -> CleanupQueueReservation | None: 

172 policy = self._policy() 

173 cleanup_kind_filter = set(cleanup_kinds) if cleanup_kinds is not None else None 

174 queue_preference_passes = self._queue_preference_passes( 

175 preferred_work_queue_ids=preferred_work_queue_ids, 

176 allow_fallback_to_any_queue=allow_fallback_to_any_queue, 

177 ) 

178 

179 async with self._lock: 

180 current_time = now("UTC") 

181 expiry_result = self._expire_due_leases_locked( 

182 current_time=current_time, 

183 policy=policy, 

184 limit=self._DEFAULT_EXPIRE_LEASE_LIMIT, 

185 work_pool_id=work_pool_id, 

186 ) 

187 wake_work_pool_ids = { 

188 message.work_pool_id for message in expiry_result.redelivered 

189 } 

190 

191 # Queue affinity is advisory: reserve preferred queue work first, then 

192 # fall back to any eligible work in the pool when fallback is enabled. 

193 result: CleanupQueueReservation | None = None 

194 for queue_filter in queue_preference_passes: 

195 for message in tuple(self._messages.values()): 

196 if message.work_pool_id != work_pool_id: 

197 continue 

198 if message.message_id in self._reservations: 

199 continue 

200 if ( 

201 cleanup_kind_filter is not None 

202 and message.kind not in cleanup_kind_filter 

203 ): 

204 continue 

205 if ( 

206 queue_filter is not None 

207 and message.work_queue_id not in queue_filter 

208 ): 

209 continue 

210 if message.delivery_count >= policy.max_delivery_attempts: 

211 self._move_to_dead_letter_locked( 

212 message_id=message.message_id, 

213 reason="max_delivery_attempts_reached", 

214 current_time=current_time, 

215 ) 

216 continue 

217 

218 lease_expires_at = current_time + policy.lease_duration 

219 updated_message = message.model_copy( 

220 update={ 

221 "delivery_count": message.delivery_count + 1, 

222 "updated_at": current_time, 

223 } 

224 ) 

225 self._messages[message.message_id] = updated_message 

226 reservation = _Reservation( 

227 token=token_urlsafe(32), 

228 lease_expires_at=lease_expires_at, 

229 reserved_at=current_time, 

230 ) 

231 self._reservations[message.message_id] = reservation 

232 result = CleanupQueueReservation( 

233 **updated_message.model_dump(), 

234 reservation_token=reservation.token, 

235 lease_expires_at=lease_expires_at, 

236 ) 

237 break 

238 if result is not None: 

239 break 

240 

241 for wake_work_pool_id in wake_work_pool_ids: 

242 await self.wake_dispatchers(wake_work_pool_id) 

243 record_cleanup_queue_lease_expiry_result(expiry_result) 

244 

245 if result is not None: 

246 record_cleanup_queue_operation( 

247 "reserve", 

248 status="accepted", 

249 work_pool_id=work_pool_id, 

250 message_id=result.message_id, 

251 cleanup_kind=str(result.kind), 

252 ) 

253 

254 return result 

255 

256 async def ack( 

257 self, 

258 *, 

259 work_pool_id: UUID, 

260 message_id: UUID, 

261 reservation_token: str, 

262 ) -> CleanupQueueOperationResult: 

263 policy = self._policy() 

264 result: CleanupQueueOperationResult 

265 wake_work_pool_id: UUID | None = None 

266 async with self._lock: 

267 current_time = now("UTC") 

268 operation_result = self._validate_current_reservation_locked( 

269 operation="ack", 

270 work_pool_id=work_pool_id, 

271 message_id=message_id, 

272 reservation_token=reservation_token, 

273 current_time=current_time, 

274 policy=policy, 

275 ) 

276 if operation_result is not None: 

277 result = operation_result 

278 wake_work_pool_id = self._expired_result_wakeup_work_pool_id_locked( 

279 message_id=message_id, 

280 result=result, 

281 ) 

282 else: 

283 message = self._messages.pop(message_id) 

284 self._reservations.pop(message_id, None) 

285 self._acked_messages[message_id] = _AckedMessage( 

286 message=message.model_copy(update={"updated_at": current_time}), 

287 completed_at=current_time, 

288 ) 

289 result = CleanupQueueOperationResult( 

290 message_id=message_id, 

291 operation="ack", 

292 status="accepted", 

293 ) 

294 

295 record_cleanup_queue_operation( 

296 "ack", 

297 status=result.status, 

298 work_pool_id=work_pool_id, 

299 message_id=message_id, 

300 ) 

301 if wake_work_pool_id is not None: 

302 await self.wake_dispatchers(wake_work_pool_id) 

303 return result 

304 

305 async def release( 

306 self, 

307 *, 

308 work_pool_id: UUID, 

309 message_id: UUID, 

310 reservation_token: str, 

311 reason: str, 

312 ) -> CleanupQueueOperationResult: 

313 if not reason: 

314 raise ValueError("release reason must be non-empty") 

315 

316 policy = self._policy() 

317 result: CleanupQueueOperationResult 

318 wake_work_pool_id: UUID | None = None 

319 async with self._lock: 

320 current_time = now("UTC") 

321 operation_result = self._validate_current_reservation_locked( 

322 operation="release", 

323 work_pool_id=work_pool_id, 

324 message_id=message_id, 

325 reservation_token=reservation_token, 

326 current_time=current_time, 

327 policy=policy, 

328 ) 

329 if operation_result is not None: 

330 result = operation_result 

331 wake_work_pool_id = self._expired_result_wakeup_work_pool_id_locked( 

332 message_id=message_id, 

333 result=result, 

334 ) 

335 else: 

336 message = self._messages[message_id] 

337 if message.delivery_count >= policy.max_delivery_attempts: 

338 dead_letter = self._move_to_dead_letter_locked( 

339 message_id=message_id, 

340 reason="max_delivery_attempts_reached", 

341 current_time=current_time, 

342 release_reason=reason, 

343 ) 

344 result = CleanupQueueOperationResult( 

345 message_id=message_id, 

346 operation="release", 

347 status="dead_lettered", 

348 reason="max_delivery_attempts_reached", 

349 dead_letter=dead_letter, 

350 ) 

351 else: 

352 self._reservations.pop(message_id, None) 

353 self._messages[message_id] = message.model_copy( 

354 update={"updated_at": current_time} 

355 ) 

356 wake_work_pool_id = message.work_pool_id 

357 result = CleanupQueueOperationResult( 

358 message_id=message_id, 

359 operation="release", 

360 status="accepted", 

361 ) 

362 

363 record_cleanup_queue_operation( 

364 "release", 

365 status=result.status, 

366 work_pool_id=work_pool_id, 

367 message_id=message_id, 

368 ) 

369 if wake_work_pool_id is not None: 

370 await self.wake_dispatchers(wake_work_pool_id) 

371 return result 

372 

373 async def renew( 

374 self, 

375 *, 

376 work_pool_id: UUID, 

377 message_id: UUID, 

378 reservation_token: str, 

379 ) -> CleanupQueueOperationResult: 

380 policy = self._policy() 

381 result: CleanupQueueOperationResult 

382 wake_work_pool_id: UUID | None = None 

383 async with self._lock: 

384 current_time = now("UTC") 

385 operation_result = self._validate_current_reservation_locked( 

386 operation="renew", 

387 work_pool_id=work_pool_id, 

388 message_id=message_id, 

389 reservation_token=reservation_token, 

390 current_time=current_time, 

391 policy=policy, 

392 ) 

393 if operation_result is not None: 

394 result = operation_result 

395 wake_work_pool_id = self._expired_result_wakeup_work_pool_id_locked( 

396 message_id=message_id, 

397 result=result, 

398 ) 

399 else: 

400 lease_expires_at = current_time + policy.lease_duration 

401 reservation = self._reservations[message_id] 

402 self._reservations[message_id] = _Reservation( 

403 token=reservation.token, 

404 lease_expires_at=lease_expires_at, 

405 reserved_at=reservation.reserved_at, 

406 ) 

407 self._messages[message_id] = self._messages[message_id].model_copy( 

408 update={"updated_at": current_time} 

409 ) 

410 result = CleanupQueueOperationResult( 

411 message_id=message_id, 

412 operation="renew", 

413 status="accepted", 

414 lease_expires_at=lease_expires_at, 

415 ) 

416 

417 record_cleanup_queue_operation( 

418 "renew", 

419 status=result.status, 

420 work_pool_id=work_pool_id, 

421 message_id=message_id, 

422 ) 

423 if wake_work_pool_id is not None: 

424 await self.wake_dispatchers(wake_work_pool_id) 

425 return result 

426 

427 async def expire_leases( 

428 self, 

429 *, 

430 limit: int = _DEFAULT_EXPIRE_LEASE_LIMIT, 

431 work_pool_id: UUID | None = None, 

432 ) -> CleanupQueueLeaseExpiryResult: 

433 policy = self._policy() 

434 async with self._lock: 

435 result = self._expire_due_leases_locked( 

436 current_time=now("UTC"), 

437 policy=policy, 

438 limit=limit, 

439 work_pool_id=work_pool_id, 

440 ) 

441 

442 for message in result.redelivered: 442 ↛ 443line 442 didn't jump to line 443 because the loop on line 442 never started

443 await self.wake_dispatchers(message.work_pool_id) 

444 record_cleanup_queue_lease_expiry_result(result) 

445 

446 return result 

447 

448 async def read_message( 

449 self, *, work_pool_id: UUID, message_id: UUID 

450 ) -> CleanupQueueMessage | None: 

451 async with self._lock: 

452 message = self._messages.get(message_id) 

453 if message is not None and message.work_pool_id != work_pool_id: 

454 return None 

455 return _copy_model(message) if message is not None else None 

456 

457 async def read_dead_letter( 

458 self, *, work_pool_id: UUID, message_id: UUID 

459 ) -> CleanupQueueDeadLetter | None: 

460 async with self._lock: 

461 dead_letter = self._dead_letters.get(message_id) 

462 if ( 

463 dead_letter is not None 

464 and dead_letter.message.work_pool_id != work_pool_id 

465 ): 

466 return None 

467 return _copy_model(dead_letter) if dead_letter is not None else None 

468 

469 async def wake_dispatchers(self, work_pool_id: UUID) -> CleanupQueueWakeup: 

470 async with self._condition: 

471 sequence = self._wakeup_sequences.get(work_pool_id, 0) + 1 

472 self._wakeup_sequences[work_pool_id] = sequence 

473 wakeup = CleanupQueueWakeup( 

474 work_pool_id=work_pool_id, 

475 sequence=sequence, 

476 ) 

477 self._condition.notify_all() 

478 return wakeup 

479 

480 async def read_wakeup_sequence(self, work_pool_id: UUID) -> int: 

481 async with self._condition: 

482 return self._wakeup_sequences.get(work_pool_id, 0) 

483 

484 async def wait_for_wakeup( 

485 self, 

486 work_pool_id: UUID, 

487 *, 

488 after: int = 0, 

489 timeout: float | None = None, 

490 ) -> CleanupQueueWakeup | None: 

491 async with self._condition: 

492 try: 

493 if timeout is None: 

494 await self._condition.wait_for( 

495 lambda: self._wakeup_sequences.get(work_pool_id, 0) > after 

496 ) 

497 else: 

498 await asyncio.wait_for( 

499 self._condition.wait_for( 

500 lambda: self._wakeup_sequences.get(work_pool_id, 0) > after 

501 ), 

502 timeout=timeout, 

503 ) 

504 except (TimeoutError, asyncio.TimeoutError): 

505 return None 

506 

507 return CleanupQueueWakeup( 

508 work_pool_id=work_pool_id, 

509 sequence=self._wakeup_sequences[work_pool_id], 

510 ) 

511 

512 def _read_existing_message_locked( 

513 self, 

514 *, 

515 message_id: UUID, 

516 idempotency_key: str, 

517 work_pool_id: UUID, 

518 ) -> CleanupQueueMessage | None: 

519 existing = self._messages.get(message_id) 

520 if existing is None and (dead_letter := self._dead_letters.get(message_id)): 

521 existing = dead_letter.message 

522 if existing is None and (acked_message := self._acked_messages.get(message_id)): 

523 existing = acked_message.message 

524 

525 if existing is not None: 

526 if ( 

527 existing.idempotency_key != idempotency_key 

528 or existing.work_pool_id != work_pool_id 

529 ): 

530 raise ValueError( 

531 "message_id is already associated with a different cleanup message" 

532 ) 

533 return existing 

534 

535 existing_message_id = self._idempotency_keys.get( 

536 (work_pool_id, idempotency_key) 

537 ) 

538 if existing_message_id is None: 

539 return None 

540 

541 if existing := self._messages.get(existing_message_id): 

542 return existing 

543 if dead_letter := self._dead_letters.get(existing_message_id): 

544 return dead_letter.message 

545 if acked_message := self._acked_messages.get(existing_message_id): 

546 return acked_message.message 

547 

548 self._idempotency_keys.pop((work_pool_id, idempotency_key), None) 

549 return None 

550 

551 def _validate_current_reservation_locked( 

552 self, 

553 *, 

554 operation: CleanupQueueOperation, 

555 work_pool_id: UUID, 

556 message_id: UUID, 

557 reservation_token: str, 

558 current_time: DateTime, 

559 policy: _QueuePolicy, 

560 ) -> CleanupQueueOperationResult | None: 

561 message = self._messages.get(message_id) 

562 if message is None: 

563 return CleanupQueueOperationResult( 

564 message_id=message_id, 

565 operation=operation, 

566 status="not_found", 

567 reason="message_not_found", 

568 ) 

569 

570 if message.work_pool_id != work_pool_id: 

571 return CleanupQueueOperationResult( 

572 message_id=message_id, 

573 operation=operation, 

574 status="unauthorized", 

575 reason="work_pool_mismatch", 

576 ) 

577 

578 reservation = self._reservations.get(message_id) 

579 if reservation is None: 

580 return CleanupQueueOperationResult( 

581 message_id=message_id, 

582 operation=operation, 

583 status="not_current", 

584 reason="no_active_reservation", 

585 ) 

586 

587 if reservation.token != reservation_token: 

588 return CleanupQueueOperationResult( 

589 message_id=message_id, 

590 operation=operation, 

591 status="invalid_token", 

592 reason="reservation_token_mismatch", 

593 ) 

594 

595 if reservation.lease_expires_at <= current_time: 

596 dead_letter = self._expire_message_lease_locked( 

597 message_id=message_id, 

598 current_time=current_time, 

599 policy=policy, 

600 ) 

601 if dead_letter is not None: 

602 return CleanupQueueOperationResult( 

603 message_id=message_id, 

604 operation=operation, 

605 status="dead_lettered", 

606 reason="max_delivery_attempts_reached", 

607 dead_letter=dead_letter, 

608 ) 

609 return CleanupQueueOperationResult( 

610 message_id=message_id, 

611 operation=operation, 

612 status="expired", 

613 reason="lease_expired", 

614 ) 

615 

616 return None 

617 

618 def _expire_due_leases_locked( 

619 self, 

620 *, 

621 current_time: DateTime, 

622 policy: _QueuePolicy, 

623 limit: int, 

624 work_pool_id: UUID | None, 

625 ) -> CleanupQueueLeaseExpiryResult: 

626 if limit < 1: 626 ↛ 627line 626 didn't jump to line 627 because the condition on line 626 was never true

627 raise ValueError("limit must be at least 1") 

628 

629 redelivered: list[CleanupQueueMessage] = [] 

630 dead_lettered: list[CleanupQueueDeadLetter] = [] 

631 expired_count = 0 

632 

633 for message_id, reservation in tuple(self._reservations.items()): 633 ↛ 634line 633 didn't jump to line 634 because the loop on line 633 never started

634 if reservation.lease_expires_at > current_time: 

635 continue 

636 message = self._messages.get(message_id) 

637 if work_pool_id is not None and ( 

638 message is None or message.work_pool_id != work_pool_id 

639 ): 

640 continue 

641 

642 dead_letter = self._expire_message_lease_locked( 

643 message_id=message_id, 

644 current_time=current_time, 

645 policy=policy, 

646 ) 

647 expired_count += 1 

648 if dead_letter is not None: 

649 dead_lettered.append(dead_letter) 

650 elif message := self._messages.get(message_id): 

651 redelivered.append(_copy_model(message)) 

652 

653 if expired_count >= limit: 

654 break 

655 

656 return CleanupQueueLeaseExpiryResult( 

657 redelivered=redelivered, 

658 dead_lettered=dead_lettered, 

659 ) 

660 

661 def _expire_message_lease_locked( 

662 self, 

663 *, 

664 message_id: UUID, 

665 current_time: DateTime, 

666 policy: _QueuePolicy, 

667 ) -> CleanupQueueDeadLetter | None: 

668 message = self._messages.get(message_id) 

669 if message is None: 

670 self._reservations.pop(message_id, None) 

671 return None 

672 

673 if message.delivery_count >= policy.max_delivery_attempts: 

674 return self._move_to_dead_letter_locked( 

675 message_id=message_id, 

676 reason="max_delivery_attempts_reached", 

677 current_time=current_time, 

678 ) 

679 

680 self._reservations.pop(message_id, None) 

681 self._messages[message_id] = message.model_copy( 

682 update={"updated_at": current_time} 

683 ) 

684 return None 

685 

686 def _move_to_dead_letter_locked( 

687 self, 

688 *, 

689 message_id: UUID, 

690 reason: str, 

691 current_time: DateTime, 

692 release_reason: str | None = None, 

693 ) -> CleanupQueueDeadLetter: 

694 message = self._messages.pop(message_id) 

695 reservation = self._reservations.pop(message_id, None) 

696 dead_letter = CleanupQueueDeadLetter( 

697 message=message, 

698 reason=reason, 

699 final_delivery_count=message.delivery_count, 

700 moved_at=current_time, 

701 reservation_token=reservation.token if reservation is not None else None, 

702 lease_expires_at=( 

703 reservation.lease_expires_at if reservation is not None else None 

704 ), 

705 release_reason=release_reason, 

706 ) 

707 self._dead_letters[message_id] = dead_letter 

708 record_cleanup_queue_dead_letter(dead_letter, source="memory_cleanup_queue") 

709 return _copy_model(dead_letter) 

710 

711 def _expired_result_wakeup_work_pool_id_locked( 

712 self, *, message_id: UUID, result: CleanupQueueOperationResult 

713 ) -> UUID | None: 

714 if result.status != "expired": 

715 return None 

716 message = self._messages.get(message_id) 

717 return message.work_pool_id if message is not None else None 

718 

719 @staticmethod 

720 def _queue_preference_passes( 

721 *, 

722 preferred_work_queue_ids: Iterable[UUID] | None, 

723 allow_fallback_to_any_queue: bool, 

724 ) -> tuple[set[UUID] | None, ...]: 

725 if preferred_work_queue_ids is None: 

726 return (None,) 

727 

728 preferred_queue_filter = set(preferred_work_queue_ids) 

729 if allow_fallback_to_any_queue: 

730 return (preferred_queue_filter, None) 

731 return (preferred_queue_filter,) 

732 

733 @staticmethod 

734 def _policy() -> _QueuePolicy: 

735 worker_channel_settings = get_current_settings().server.worker_channel 

736 retention_seconds = ( 

737 worker_channel_settings.cleanup_completed_idempotency_retention_seconds 

738 ) 

739 return _QueuePolicy( 

740 lease_duration=timedelta( 

741 seconds=worker_channel_settings.cleanup_lease_seconds 

742 ), 

743 max_delivery_attempts=worker_channel_settings.cleanup_max_delivery_attempts, 

744 completed_idempotency_retention=( 

745 None 

746 if retention_seconds is None 

747 else timedelta(seconds=retention_seconds) 

748 ), 

749 ) 

750 

751 def _prune_completed_idempotency_locked( 

752 self, *, current_time: DateTime, policy: _QueuePolicy 

753 ) -> None: 

754 retention = policy.completed_idempotency_retention 

755 if retention is None: 

756 return 

757 

758 expired_message_ids = [ 

759 message_id 

760 for message_id, acked_message in self._acked_messages.items() 

761 if acked_message.completed_at + retention <= current_time 

762 ] 

763 for message_id in expired_message_ids: 

764 acked_message = self._acked_messages.pop(message_id) 

765 idempotency_key = ( 

766 acked_message.message.work_pool_id, 

767 acked_message.message.idempotency_key, 

768 ) 

769 if self._idempotency_keys.get(idempotency_key) == message_id: 

770 self._idempotency_keys.pop(idempotency_key, None)