Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for scheduling global callbacks #2661

Merged
merged 13 commits into from Mar 8, 2022
Merged

Conversation

philippjfr
Copy link
Member

@philippjfr philippjfr commented Aug 24, 2021

Often times you want to run a task periodically independently of the apps being served, e.g. to refresh cached data at certain intervals or for any other number of reasons. This PR introduces pn.state.schedule that allows scheduling tasks at periodic intervals either as declared by a period or a cron expression.

One important caveat here is that the callback to be scheduled may not be defined in the same module as the app being run so it must either be imported from an external module OR it may be declared as part of a setup script. A setup script may now be provided using the new --setup argument to panel serve which may configure the Panel apps in any number of ways, e.g. by changing the pn.config defaults, populating the pn.state.cache or to schedule tasks with pn.state.schedule.

Implements #2657

        Schedule a callback periodically at a specific
        time. Scheduling is idempotent, i.e. if a callback has already
        been scheduled under the same name subsequent calls will have
        no effect. By default the starting time is immediate but may
        be overridden with the `at` keyword argument. The period may
        be declared using the `period` argument or a cron expression
        (which requires the `croniter` library).

        Arguments
        ---------
        name: str
          Name of the scheduled task
        callback: callable
          Callback to schedule
        at: datetime.datetime
          Datetime to schedule the task at
        period: str or datetime.timedelta
          The period between executions, may be expressed as a timedelta
          or a string:

            - Week:   '1w'
            - Day:    '1d'
            - Hour:   '1h'
            - Minute: '1m'
            - Second: '1s'

        cron: str
          A cron expression (requires croniter to parse)

@philippjfr
Copy link
Member Author

@MarcSkovMadsen Suggestions on the API and functionality here would be appreciated.

@codecov
Copy link

codecov bot commented Aug 24, 2021

Codecov Report

Merging #2661 (b5c9e1b) into master (e87a0e5) will increase coverage by 0.00%.
The diff coverage is 86.34%.

Impacted file tree graph

@@           Coverage Diff            @@
##           master    #2661    +/-   ##
========================================
  Coverage   83.09%   83.10%            
========================================
  Files         193      193            
  Lines       25753    25931   +178     
========================================
+ Hits        21400    21550   +150     
- Misses       4353     4381    +28     
Impacted Files Coverage Δ
panel/command/serve.py 36.98% <16.66%> (-1.18%) ⬇️
panel/io/state.py 69.80% <76.05%> (+1.14%) ⬆️
panel/util.py 84.86% <90.90%> (+0.27%) ⬆️
panel/tests/conftest.py 94.92% <100.00%> (+0.03%) ⬆️
panel/tests/test_server.py 100.00% <100.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update e87a0e5...b5c9e1b. Read the comment docs.

@MarcSkovMadsen
Copy link
Collaborator

One thing the above will not support is a physicist wanting to schedule a job at the next solar eclipse. Or maybe in trading the next trading day etc. For that a custom function next_schedule(nowutc: datetime) -> datetime that given the current utc datetime returns the next utc datetime would be "nice to have". Providing nowutc is just nice because that makes it easy to test the function.

But the above api is nice and covers most of my use cases.

@MarcSkovMadsen
Copy link
Collaborator

This is something I'm currently testing out

class JobScheduler(param.Parameterized):
    """The JobScheduler enables scheduling jobs in a global or session context"""

    job = param.ClassSelector(class_=Job, doc="The Job to schedule")
    next_run_utc_func = param.Parameter(
        precedence=-1,
        doc="""
        A function taking a datetime (utcnow) as argument and returning the next utc datetime to
        schedule the job.
    """,
    )
    context = param.ObjectSelector(
        default="global",
        objects=["global"],
        constant=True,
        doc="""
        Either global or session. If session the JobScheduler will stop running when the session
        is deleted.
    """,
    )

    next_run_utc = param.Date(
        constant=True, doc="""The next utc datetime the job is scheduled for"""
    )
    logger = param.ClassSelector(class_=Logger)
    view = param.Parameter()

    def __init__(self, **params):
        super().__init__(**params)

        self.view = pn.Param(self, parameters=["job", "next_run_utc"])
        self._schedule_next_run()

    def _run_and_reschedule(self):
        self.job.run()  # pylint: disable=no-member
        self._schedule_next_run()

    def _schedule_next_run(self):
        with param.edit_constant(self):
            self._add_callback()

    def _add_callback(self):
        IOLoop.current().add_callback(self._add_timeout)

    def _add_timeout(self):
        utcnow=datetime.datetime.utcnow()
        try:
            self.next_run_utc = self.next_run_utc_func(utcnow)
            deadline=self.next_run_utc-utcnow
            IOLoop.current().add_timeout(
                deadline=deadline, callback=self._run_and_reschedule  # pylint: disable=no-member
            )
            self._log_info(f"Next run of {self.job.name} was scheduled to {self.next_run_utc}")
        except Exception as ex:
            self._log_error(f"Could not add timeout {ex}")


    @staticmethod
    def _get_miliseconds_to_next_run(now, next_run):
        return (next_run - now).seconds * 1000

    def _log_info(self, message):
        if self.logger:
            self.logger.info(self.name + " - " + message)  # pylint: disable=no-member

    def _log_error(self, message):
        if self.logger:
            self.logger.error(self.name + " - " + message)  # pylint: disable=no-member

@MarcSkovMadsen
Copy link
Collaborator

Note for my self. When this PR is implemented and released I should rewrite https://discourse.holoviz.org/t/panel-starting-a-stream-of-data/2709/4?u=marc.

@jbednar
Copy link
Member

jbednar commented Feb 11, 2022

Since jobs are named, it seems like previously scheduled jobs could be canceled with schedule(thename, remove=True), so that users of schedule can easily discover that capability; cancel_scheduled isn't easily found if looking at the docs for schedule.

The --setup option could also be --startup or --bootstrap; not sure which is clearer.

examples/user_guide/Deploy_and_Export.ipynb Outdated Show resolved Hide resolved
examples/user_guide/Deploy_and_Export.ipynb Outdated Show resolved Hide resolved
examples/user_guide/Deploy_and_Export.ipynb Outdated Show resolved Hide resolved
examples/user_guide/Deploy_and_Export.ipynb Show resolved Hide resolved
examples/user_guide/Performance_and_Debugging.ipynb Outdated Show resolved Hide resolved
Copy link
Member

@maximlt maximlt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My general feedback would be to document this a little more by:

  • giving examples of tasks a user may want to register
  • making it clearer what kind of callbacks should be used and what they allow, between schedule and add_next_tick_callback and add_next_tick_callback and onload and --warm and --setup...

examples/user_guide/Deploy_and_Export.ipynb Outdated Show resolved Hide resolved
examples/user_guide/Deploy_and_Export.ipynb Outdated Show resolved Hide resolved
examples/user_guide/Deploy_and_Export.ipynb Outdated Show resolved Hide resolved
examples/user_guide/Overview.ipynb Show resolved Hide resolved
examples/user_guide/Overview.ipynb Show resolved Hide resolved
panel/io/state.py Outdated Show resolved Hide resolved
panel/io/state.py Outdated Show resolved Hide resolved
panel/io/state.py Outdated Show resolved Hide resolved
panel/io/state.py Outdated Show resolved Hide resolved
panel/util.py Show resolved Hide resolved
@philippjfr philippjfr merged commit 7d6044f into master Mar 8, 2022
@philippjfr philippjfr deleted the schedule_callbacks branch March 8, 2022 23:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants