Documentation
tomodachi

Scheduled functions and cron

Scheduled functions / cron / triggered on time interval

📘

See the example service after the decorator descriptions for a reference implementation of how the invoked functions may be used.

Invoker functions to schedule tasks

@tomodachi.schedule(interval=None, timestamp=None, timezone=None, immediately=False)

A scheduled function invoked on either a specified interval (you may use common cron notation as a str for a fine-tuned interval or specify an integer value of seconds) or a specific timestamp. The timezone will default to the server's local time unless explicitly stated.

When using an integer interval you may also specify whether the function should be called immediately on service start or wait the full interval seconds before its first invokation.


@tomodachi.heartbeat

A function which will be invoked every second.


@tomodachi.minutely
@tomodachi.hourly
@tomodachi.daily
@tomodachi.monthly

A scheduled function which will be called once every minute / hour / day / month.


Scheduled tasks when running multiple service replicas

🚧

Things to consider when used in a multi-replica context

What is your use-case for scheduling function triggers or functions that trigger on an interval?

These types of scheduling may not be optimal in clusters with many pods in the same replication set, as all the services running the same code will very likely execute at the same timestamp / interval (which in same cases may correlated with exactly when they were last deployed).

These functions are quite naive and should in distributed contexts only be used where applicable. Make sure that triggering the functions several times doesn't incur unnecessary costs or cause unwanted surprises if the functions aren't completely idempotent.

To perform a task on a specific timestamp or on an interval where only one of the available services of the same type in a cluster should trigger is a common thing to solve and there are several solutions to pick from., some kind of distributed consensus needs to be reached. Tooling exists, but what you need may differ depending on your use-case. There's algorithms for distributed consensus and leader election, Paxos or Raft, that luckily have already been implemented to solutions like the strongly consistent and distributed key-value stores etcd and TiKV. Even primitive solutions such as Redis SETNX commands or using DynamoDB conditional updates would work, but could be costly or hard to manage access levels around. If you're on k8s there's even a simple "leader election" API available that just creates a 15 seconds lease. Solutions are many and if you are in need, go hunting and find one that suits your use-case, there's probably tooling and libraries available to call it from your service functions.

Implementing proper consensus mechanisms and in turn leader election can be complicated. In distributed environments the architecture around these solutions needs to account for leases, decision making when consensus was not reached, how to handle crashed executors, quick recovery on master node(s) disruptions, etc.

Example implementation (scheduling)

import tomodachi


class Service(tomodachi.Service):
    name = "scheduler-example"

    @tomodachi.hourly  
    async def every_hour(self) -> None:
        print("Called every hour")

    # Using cron notation
    @tomodachi.schedule(interval="1/2 8-18 * * mon-fri")  
    async def work_hours(self) -> None:
        print("Every odd minute between 8-18 on weekdays")

    # The last Tuesday of January and March at 05:30 AM
    @tomodachi.schedule(interval="30 5 * jan,mar Ltue")  
    async def advanced_cron_notation_scheduling(self) -> None:
        print("The last Tuesday of January and March at 05:30 AM")

		# Executed at 22:15:30 every day (server timezone)
    @tomodachi.schedule(timestamp="22:15:30")
    async def as_timestamp(self) -> None:
        print("Local server time is now 22:15:30")

    # Midnight in Sweden ("Europe/Stockholm" timezone)
    @tomodachi.schedule(timestamp="00:00:00", timezone="Europe/Stockholm")
    async def midnight_in_sweden(self) -> None:
        print("It's midnight in Sweden")

    # Called every (interval) seconds
    @schedule(interval=20)
    async def every_twenty_seconds(self) -> None:
        print("Every 20 seconds")

    # Called every (interval) seconds and directly on service start
    @schedule(interval=20, immediately=True)
    async def every_twenty_seconds_and_immediately(self) -> None:
        print("Every 20 seconds and immediately")