agronholm / apscheduler

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

`'CronTrigger' object has no attribute 'year'` when try to pause schedule #923

Open krosenmann opened 1 month ago

krosenmann commented 1 month ago

Things to check first

Version

4.0.0a5

What happened?

Hi! When I try to pause schedule of a task, created with cron trigger, I'm getting following error. Do you have any advice or workaround maybe for this kind of event? Thanks!

Environment:

python = "3.11.4"
apscheduler = "4.0.0a5"
sqlalchemy = "2.0.30"
asyncpg = "0.29.0"

The error itself:

Traceback (most recent call last):
  File "/home/krosenmann/sources/apscheduler-minimal-example/executor_test.py", line 26, in main
    await scheduler.pause_schedule(id='this-is-the-end')
  File "/home/krosenmann/.cache/pypoetry/virtualenvs/apscheduler-minimal-example-RlZethiB-py3.11/lib/python3.11/site-packages/apscheduler/_schedulers/async_.py", line 538, in pause_schedule
    await self.data_store.add_schedule(
  File "/home/krosenmann/.cache/pypoetry/virtualenvs/apscheduler-minimal-example-RlZethiB-py3.11/lib/python3.11/site-packages/apscheduler/datastores/sqlalchemy.py", line 504, in add_schedule
    schedule.marshal(self.serializer)
  File "/home/krosenmann/.cache/pypoetry/virtualenvs/apscheduler-minimal-example-RlZethiB-py3.11/lib/python3.11/site-packages/apscheduler/_structures.py", line 143, in marshal
    marshalled = attrs.asdict(self, value_serializer=serialize)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/krosenmann/.cache/pypoetry/virtualenvs/apscheduler-minimal-example-RlZethiB-py3.11/lib/python3.11/site-packages/attr/_next_gen.py", line 211, in asdict
    return _asdict(
           ^^^^^^^^
  File "/home/krosenmann/.cache/pypoetry/virtualenvs/apscheduler-minimal-example-RlZethiB-py3.11/lib/python3.11/site-packages/attr/_funcs.py", line 65, in asdict
    rv[a.name] = asdict(
                 ^^^^^^^
  File "/home/krosenmann/.cache/pypoetry/virtualenvs/apscheduler-minimal-example-RlZethiB-py3.11/lib/python3.11/site-packages/attr/_funcs.py", line 56, in asdict
    v = getattr(inst, a.name)
        ^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'CronTrigger' object has no attribute 'year'

How can we reproduce the bug?

Following code reproduces bug

import asyncio

from apscheduler.datastores.sqlalchemy import SQLAlchemyDataStore
from apscheduler.serializers.json import JSONSerializer

from apscheduler import AsyncScheduler
from apscheduler.triggers.cron import CronTrigger

async def dummy_task():
    await asyncio.sleep(1)
    print("Banana!")

async def main():
    data_store = SQLAlchemyDataStore(
        engine_or_url="postgresql+asyncpg://<user>:<password>@127.0.0.1:15433/task_scheduler_data_store",
        serializer=JSONSerializer()
    )
    async with AsyncScheduler(data_store=data_store) as scheduler:
        # Add schedules, configure tasks here
        task = await scheduler.configure_task(dummy_task)

        schedule = await scheduler.add_schedule(task, id='this-is-the-end',
                                                trigger=CronTrigger.from_crontab('* * * * *'))
        await scheduler.pause_schedule(id='this-is-the-end')

asyncio.run(main())
WillDaSilva commented 1 month ago

Thank you for reporting this issue. I'll look into this later today.

WillDaSilva commented 1 month ago

JSONSerializer() is not necessary to reproduce this, nor is Postgres:

>>> data_store = SQLAlchemyDataStore(engine_or_url="sqlite:///example.db")
>>> scheduler = await AsyncScheduler(data_store=data_store).__aenter__()
>>> schedule = await scheduler.add_schedule(task, id='this-is-the-end', trigger=CronTrigger.from_crontab('* * * * *'))
>>> await scheduler.pause_schedule(id='this-is-the-end')
Traceback (most recent call last):
[ truncated ]
AttributeError: year

Pausing the schedule is not necessary either:

>>> data_store = SQLAlchemyDataStore(engine_or_url="sqlite:///example.db")
>>> scheduler = await AsyncScheduler(data_store=data_store).__aenter__()
>>> schedule_id = await scheduler.add_schedule(task, id='this-is-the-end', trigger=CronTrigger.from_crontab('* * * * *'))
>>> schedule = await scheduler.get_schedule(schedule_id)
>>> attrs.asdict(schedule)
Traceback (most recent call last):
[ truncated ]
AttributeError: year
>>> attrs.asdict(schedule.trigger)
Traceback (most recent call last):
[ truncated ]
AttributeError: year
>>> attrs.asdict(CronTrigger.from_crontab("* * * * *"))
{'year': None, 'month': '*', 'day': '*', [ truncated ] }
>>> schedule.trigger
CronTrigger(year='*', month='*', day='*', week='*', day_of_week='*', hour='*', minute='*', second='0', start_time='2024-06-07T10:33:22.438244-04:00', timezone='America/Toronto')
>>> CronTrigger.from_crontab("* * * * *")
CronTrigger(year='*', month='*', day='*', week='*', day_of_week='*', hour='*', minute='*', second='0', start_time='2024-06-07T10:58:40.501026-04:00', timezone='America/Toronto')

It's not clear why attrs can serialize CronTrigger.from_crontab("* * * * *") but not schedule.trigger

agronholm commented 1 month ago

It's not clear why attrs can serialize CronTrigger.from_crontab(" *") but not schedule.trigger

Try serializing ,deserializing and then serializing it again.

WillDaSilva commented 1 month ago

@agronholm Good idea

>>> s = PickleSerializer()
>>> attrs.asdict(s.deserialize(s.serialize(CronTrigger.from_crontab("* * * * *"))))
Traceback (most recent call last):
[ truncated ]
AttributeError: year
>>> attrs.asdict(CronTrigger.from_crontab("* * * * *"))
{'year': None, 'month': '*', 'day': '*', [ truncated ] }
agronholm commented 1 month ago

The crux of the problem here is that these virtual fields are only used for initialization, and are not separately saved during serialization, or restored during deserialization.

agronholm commented 1 month ago

@WillDaSilva would like to take a shot at fixing the issue, or should I?

WillDaSilva commented 4 weeks ago

@agronholm I could probably address this within a week or two, but if you'd like this resolved promptly it'd be best if you take care of it. This issue isn't affecting me personally. I only jumped in initially because of the possibility that this had something to do with the pause schedule feature.

agronholm commented 4 weeks ago

I only jumped in initially because of the possibility that this had something to do with the pause schedule feature.

Yeah, it certainly doesn't. I'm busy for the next couple of days but I can take care of this later this week.