schedule icon indicating copy to clipboard operation
schedule copied to clipboard

run only once - : schedule.at('22:30').do(any_job)

Open helarsen opened this issue 3 years ago • 9 comments

To run a job only once the manual says:

def job_that_executes_once():
    # Do some work ...
    return schedule.CancelJob

schedule.every().day.at('22:30').do(job_that_executes_once)

The problem with this implementation is that in case the job_that_executes_once is missing the return schedule.CancelJob, you will get the job run every day at "22:30" where you did not expect this. This can be one of the very hard-to-find bugs which pops "out of the blue"

The term ".every().day." is misleading in this context but there is no way around it as of now.

The naive may try: schedule.at('22:30').do(any_job) AttributeError: module 'schedule' has no attribute 'at' so it does not work.

I would advocate for a method to schedule any job to run only once. But I don't know how this best fits the existing interface.

Maybe something like schedule.once().at('22:30').do(any_job) ?

helarsen avatar Jan 03 '21 17:01 helarsen

Hi @helarsen,

This has been proposed before in #39 and addressed in this comment specifically. It was also implemented in #184 and declined for the same reason.

Your strong reasoning about hard-to-find bugs made me reconsider declining again. The fact that this has been proposed over and over throughout the years, and the relatively small impact of this change, it might be worth it to add...

And also I like the looks of this :heart_eyes:

schedule.once().at('22:30').do(any_job)

Keeping this open and marking it as enhancement, no promises though ;)

SijmenHuizenga avatar Jan 04 '21 22:01 SijmenHuizenga

I would extend my suggestion:

  • schedule.once().at('HH:MM:SS').do(any_job) -> first coming time instance HH:MM:SS, which may be up to 24h away from now.
  • schedule.once().at(':MM:SS').do(any_job) -> first coming time instance :MM:SS, which may be up to 1h away from now.
  • schedule.once().at(':SS').do(any_job) -> first coming time instance :SS, which may be up to 1min away from now.

This provides appealing symmetry.

There cannot be any schedule.once().at(':MM').do(any_job) or schedule.once().at('MM').do(any_job)and good so, i would say.

By the way - thumbs up to how professional the management of this library is!

helarsen avatar Jan 05 '21 17:01 helarsen

Thanks for the compliment :blush:

In regards to when the first coming invocation should be; Could we just keep the scheduling system as is and cancel the job after the first invocation? Is there a need to re-define when the the 'next' job is?

SijmenHuizenga avatar Jan 05 '21 19:01 SijmenHuizenga

Sorry, I miss your point here:

in regards to when the first coming invocation should be; Could we just keep the scheduling system as is and cancel the job after the first invocation? Is there a need to re-define when the the 'next' job is?

What I suggest is that: schedule.once().at(':MM:SS').do(any_job) will run at time MM:SS, but because the HH part is not specified the scheduler must define a default HH value. I suggest that the obvious choice is: "the next coming time matching MM:SS" i.e. "xx:MM:SS". As I write in worst case this may be up to an hour from "now" At first this may sound a bit silly but the "HH:MM:SS" does exactly the same just on day basis. Missing the deadline by 1 second and it will have to wait until tomorrow - but this is logically fitting the rest of the interface philosophy. Does this clarify what I mean?

helarsen avatar Jan 05 '21 23:01 helarsen

Could we just keep the scheduling system as is and cancel the job after the first invocation?

Yes

Is there a need to re-define when the the 'next' job is?

No

would be the short answers - hopefully just supplementing what I previously wrote

helarsen avatar Jan 06 '21 19:01 helarsen

Ah, I now understand your point;

schedule.once().at(':MM:SS').do(any_job) will run at time MM:SS, but because the HH part is not specified the scheduler must define a default HH value.

This is how the scheduler currently works, so we can just re-use the current next-run calculations. Which makes this so much easier :smile:

SijmenHuizenga avatar Jan 06 '21 22:01 SijmenHuizenga

so we can just re-use the current next-run calculations

Yes the main difference to every() is that the scheduler should somehow ensure it only runs one time. Note that the semantics calls for that it will always run exactly one time irrespective of what time was requested, because there is no defined notion of being late and missing a deadline when the timing is using modulus (MM, HH or 24h) arithmetic - just to re-state what I wrote 3 posts ago.

Further to the support of this enhancement: Having to add return schedule.CancelJob to Job() means:

  • The Job() will have to import schedule library.
  • The Job() will have to know who called it in order to determine if it should cancel or not.
  • This affects portability and reuse of the code

helarsen avatar Jan 08 '21 10:01 helarsen

Maybe when scheduale.once() is called, a tag is added to the job (e.g. run_once). When schedual._run_job() is called, we check for the tag and handle accordingly.

nadavgolden avatar Jan 30 '21 08:01 nadavgolden

A workaround I'm using for this is adding a boolean (named once) as argument of the job function + adding "-once" after the job id in the tag:

Then at the start of the job function i have:

if once is True:
    schedule.clear(str(job.id) + '-once')

Which prevents the job from executing another time after this first run.

ph3ne avatar Jan 17 '22 14:01 ph3ne