agronholm / apscheduler

Task scheduling library for Python
MIT License
5.94k stars 690 forks source link

So confusing between sync and async. #894

Closed zawsq closed 1 month ago

zawsq commented 2 months ago

Things to check first

Feature description

Make AsyncIOScheduler awaitable

It's confusing to use sync and async when using APScheduler for the first time; here are some examples:

import asyncio
import datetime
import tzlocal
import time

from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.schedulers.background import BackgroundScheduler

import logging
from rich.logging import RichHandler
from rich.traceback import install
install(show_locals=True)

FORMAT = "%(message)s"
logging.basicConfig(level="INFO", format=FORMAT, datefmt="[%X]", handlers=[RichHandler()])

Synchronous way:

class SyncScheduler:
    def __init__(self):
        self.scheduler = BackgroundScheduler(
            timezone=tzlocal.get_localzone(),
            jobstore_retry_interval=1,
            misfire_grace_time=1)
        """those make sense to start because it's a sync"""
        self.scheduler.start()

    def add_job(self, func, trigger, **kwargs):
        self.scheduler.add_job(func, trigger, **kwargs)

def sync_job():
    print("Hello, world!")

scheduler = SyncScheduler()

def main():
    "this works"
    now = datetime.datetime.now()
    scheduler.add_job(sync_job, "date", run_date=now + datetime.timedelta(seconds=1))

    time.sleep(5)

main()

Asynchronous way: it's too confusing to use

class AsyncScheduler:
    def __init__(self):
        self.scheduler = AsyncIOScheduler(
            timezone=tzlocal.get_localzone(),
            jobstore_retry_interval=1,
            misfire_grace_time=1)
        """doesn't make sense to start because it's a async. but still allows it.

        it should raise a RuntimeWarning: 'Enable tracemalloc to get the object allocation traceback'"""
        #self.scheduler.start()

    async def add_job(self, func, trigger, **kwargs):
        """It doesn't execute anything unless start is called inside an async function"""
        self.scheduler.add_job(func, trigger, **kwargs)

async def async_job():
    print('Hello, world!')

scheduler = AsyncScheduler()

async def main():
    now = datetime.datetime.now()
    """Doesn't execute anything unless start is called inside an async function"""
    await scheduler.add_job(async_job, 'date', run_date=now + datetime.timedelta(seconds=1))

    """start needs to be called inside async function to work without it being awaited."""
    # await scheduler.scheduler.start()

    await asyncio.sleep(5)

asyncio.run(main())

Use case

I'm not familiar with how trio works however what is "asynchronous" should be initiated in asynchronous way.

it's confusing when it's your first time using APScheduler. it allows calling start() to be called without awaiting and processed to ignore all jobs if it's called outside async function.

agronholm commented 2 months ago

Why don't you try APScheduler 4.0 alpha? The vast majority of the code and the API has been refactored there. You might like that API better.

agronholm commented 1 month ago

Closing due to lack of responses, and the fact that 4.0 has proper async support and 3.x isn't getting any new features.