Skip to content

AsyncAlchemicalQueue

Bases: Generic[T]

Async equivalent of AlchemicalQueue. Not intended to be initialized by a user, go through AsyncAlchemicalQueues.get instead.

Source code in src/alchemical_queues/aio.py
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
class AsyncAlchemicalQueue(Generic[T]):
    """Async equivalent of [AlchemicalQueue][alchemical_queues.AlchemicalQueue].
    Not intended to be initialized by a user, go through
    [AsyncAlchemicalQueues.get][alchemical_queues.aio.AsyncAlchemicalQueues.get]
    instead.
    """

    def __init__(
        self,
        engine: AsyncEngine,
        model,
        name: str,
        serializer: Serializer[T] = DEFAULT_SERIALIZER,
    ):
        self._engine = engine
        self._model = model
        self._name = name
        self._serializer = serializer
        self._session = async_sessionmaker(
            engine,
            autoflush=False,
            expire_on_commit=False,
        )

    @property
    def name(self) -> str:
        """The name of the queue"""
        return self._name

    @_retry_on_deadlock
    async def put(
        self,
        item: T,
        *,
        schedule_at: Union[datetime, None] = None,
        priority: int = 0,
    ) -> "AlchemicalEntry[T]":
        """Put an entry into the queue. See
        [AlchemicalQueue.put][alchemical_queues.AlchemicalQueue.put] for
        details.

        Returns:
            AlchemicalEntry[T]: The resultant queue entry.
        """
        entry = self._model(
            enqueued_at=datetime.now(),
            schedule_at=schedule_at,
            priority=priority,
            queue_name=self._name,
            data=self._serializer.dumps(item),
        )

        async with self._session() as session:
            session.add(entry)
            await session.commit()

            return AlchemicalEntry(entry, item)

    @_retry_on_deadlock
    async def get(self) -> Union["AlchemicalEntry[T]", None]:
        """Get the highest priority entry out from the queue, removing it.
        See [AlchemicalQueue.get][alchemical_queues.AlchemicalQueue.get] for
        details.

        Returns:
            (AlchemicalEntry | None)
        """
        timestamp = datetime.now()

        async with self._session() as session:
            query = (
                select(self._model)
                .filter(
                    self._model.queue_name == self._name,
                    or_(
                        self._model.schedule_at == None,  # noqa: E711
                        self._model.schedule_at <= timestamp,  # type: ignore
                    ),
                )
                .order_by(self._model.priority.desc(), self._model.entry_id.asc())  # type: ignore
                .limit(1)
            )

            if self._engine.dialect.name in _SKIP_LOCKED_DIALECTS:
                query = query.with_for_update(skip_locked=True)

            if _supports_returning(self._engine, "delete"):
                candidate = query.with_only_columns(self._model.entry_id)
                item = (
                    await session.execute(
                        delete(self._model)
                        .where(self._model.entry_id == candidate.scalar_subquery())
                        .returning(self._model)
                    )
                ).scalar_one_or_none()
            else:
                # MySQL has no RETURNING support at all: lock and fetch the
                # full candidate row first instead, then delete it by id in
                # the same transaction. The row stays locked throughout, so
                # the same atomicity guarantee holds.
                item = (await session.execute(query)).scalar_one_or_none()
                if item is not None:
                    await session.delete(item)

            if item is None:
                await session.rollback()
                return None

            entry = AlchemicalEntry(item, self._serializer.loads(item.data))
            await session.commit()

        return entry

    async def qsize(self) -> int:
        """Return the approximate size of this queue."""
        async with self._session() as session:
            return (
                await session.scalar(
                    select(func.count())  # pylint: disable=not-callable
                    .select_from(self._model)
                    .where(self._model.queue_name == self._name)
                )
                or 0
            )

    async def empty(self) -> bool:
        """Return `True` if the Queue is empty, `False` otherwise."""
        async with self._session() as session:
            return (
                await session.scalar(
                    select(self._model.entry_id)
                    .where(self._model.queue_name == self._name)
                    .limit(1)
                )
            ) is None

    @_retry_on_deadlock
    async def clear(self) -> None:
        """Clear all entries from this queue. Might fail-silent an update call."""
        async with self._session() as session:
            await session.execute(
                delete(self._model).where(self._model.queue_name == self._name)
            )
            await session.commit()

name property

The name of the queue

put(item, *, schedule_at=None, priority=0) async

Put an entry into the queue. See AlchemicalQueue.put for details.

Returns:

Type Description
AlchemicalEntry[T]

AlchemicalEntry[T]: The resultant queue entry.

Source code in src/alchemical_queues/aio.py
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
@_retry_on_deadlock
async def put(
    self,
    item: T,
    *,
    schedule_at: Union[datetime, None] = None,
    priority: int = 0,
) -> "AlchemicalEntry[T]":
    """Put an entry into the queue. See
    [AlchemicalQueue.put][alchemical_queues.AlchemicalQueue.put] for
    details.

    Returns:
        AlchemicalEntry[T]: The resultant queue entry.
    """
    entry = self._model(
        enqueued_at=datetime.now(),
        schedule_at=schedule_at,
        priority=priority,
        queue_name=self._name,
        data=self._serializer.dumps(item),
    )

    async with self._session() as session:
        session.add(entry)
        await session.commit()

        return AlchemicalEntry(entry, item)

get() async

Get the highest priority entry out from the queue, removing it. See AlchemicalQueue.get for details.

Returns:

Type Description
Union[AlchemicalEntry[T], None]

(AlchemicalEntry | None)

Source code in src/alchemical_queues/aio.py
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
@_retry_on_deadlock
async def get(self) -> Union["AlchemicalEntry[T]", None]:
    """Get the highest priority entry out from the queue, removing it.
    See [AlchemicalQueue.get][alchemical_queues.AlchemicalQueue.get] for
    details.

    Returns:
        (AlchemicalEntry | None)
    """
    timestamp = datetime.now()

    async with self._session() as session:
        query = (
            select(self._model)
            .filter(
                self._model.queue_name == self._name,
                or_(
                    self._model.schedule_at == None,  # noqa: E711
                    self._model.schedule_at <= timestamp,  # type: ignore
                ),
            )
            .order_by(self._model.priority.desc(), self._model.entry_id.asc())  # type: ignore
            .limit(1)
        )

        if self._engine.dialect.name in _SKIP_LOCKED_DIALECTS:
            query = query.with_for_update(skip_locked=True)

        if _supports_returning(self._engine, "delete"):
            candidate = query.with_only_columns(self._model.entry_id)
            item = (
                await session.execute(
                    delete(self._model)
                    .where(self._model.entry_id == candidate.scalar_subquery())
                    .returning(self._model)
                )
            ).scalar_one_or_none()
        else:
            # MySQL has no RETURNING support at all: lock and fetch the
            # full candidate row first instead, then delete it by id in
            # the same transaction. The row stays locked throughout, so
            # the same atomicity guarantee holds.
            item = (await session.execute(query)).scalar_one_or_none()
            if item is not None:
                await session.delete(item)

        if item is None:
            await session.rollback()
            return None

        entry = AlchemicalEntry(item, self._serializer.loads(item.data))
        await session.commit()

    return entry

qsize() async

Return the approximate size of this queue.

Source code in src/alchemical_queues/aio.py
391
392
393
394
395
396
397
398
399
400
401
async def qsize(self) -> int:
    """Return the approximate size of this queue."""
    async with self._session() as session:
        return (
            await session.scalar(
                select(func.count())  # pylint: disable=not-callable
                .select_from(self._model)
                .where(self._model.queue_name == self._name)
            )
            or 0
        )

empty() async

Return True if the Queue is empty, False otherwise.

Source code in src/alchemical_queues/aio.py
403
404
405
406
407
408
409
410
411
412
async def empty(self) -> bool:
    """Return `True` if the Queue is empty, `False` otherwise."""
    async with self._session() as session:
        return (
            await session.scalar(
                select(self._model.entry_id)
                .where(self._model.queue_name == self._name)
                .limit(1)
            )
        ) is None

clear() async

Clear all entries from this queue. Might fail-silent an update call.

Source code in src/alchemical_queues/aio.py
414
415
416
417
418
419
420
421
@_retry_on_deadlock
async def clear(self) -> None:
    """Clear all entries from this queue. Might fail-silent an update call."""
    async with self._session() as session:
        await session.execute(
            delete(self._model).where(self._model.queue_name == self._name)
        )
        await session.commit()