简体   繁体   中英

Run scheduled task once within Spring Boot app deployed to Azure

In my Spring Boot app I have a scheduled task that runs every minute. It runs some queries to find any notifications that are due, and then sends them by email

@Service
public class EmailSender {

    @Scheduled(cron = "0 * * * * *")
    public void runTask() {
      // send some emails
    }
}

It works fine when I test it locally, but when I deploy it to Azure, 2 copies of each email are sent. This is because in Azure we run (at least) 2 containers, both of which have the scheduling enabled.

I looked for a per-container environment variable, which the scheduler would check, and only run the job if this variable is set to "true", but couldn't find any reliable way to achieve this.

I'm now considering the following instead

  • Removing the @Scheduled annotation
  • Adding a HTTP endpoint (controller method) that exposes the functionality of the task
  • Scheduling this endpoint to be called every minute using Azure Logic Apps (I also considered using WebJobs , but this is not yet supported for App Service on Linux).

This seems like a lot of additional complexity, and am wondering if there's a simpler solution?

Spring, by default, cannot handle scheduler synchronization over multiple instances – it executes the jobs simultaneously on every node instead. So there are two possible solutions:

Using an external library

You can use Spring ShedLock to handle this though. Here there is a guide you can use: Spring ShedLock . You can find all the code need to implement this lock there.

It works by using a parameter in a database, that gives the information to the other nodes about the execution status of that task. In this way ShedLock prevents your scheduled tasks from being executed more than once at the same time. If a task is being executed on one node, it acquires a lock which prevents the execution of the same task from another node.

You can find more info in their Github Page about how to handle it with different databases.

Deploying the active schedulers in a different instance

You can create two different profiles, for example, one is prod and the other is cron . Then you can put the annotation @Profile("cron") on scheduler classes, so that they will be ran only on the second profile.

Then you have to build the application twice:

  1. The first time you have to build it with -Dspring.profiles.active=prod and you deploy it normally.
  2. The second time you build it with -Dspring.profiles.active=prod,cron , and you have to deploy this in an environment that is not autoscaling so that you can be sure there will be only one instance of this.

Option 4

(Option 3 being the one in the comment by @MDeinum)

Factor your code so the executable job runs against a REST request, protected by proper authentication (a GUID parameter is sufficient for the purpose, feel free to use more robust schemes). Disable scheduling offered by @Scheduled annotation. Use an Azure Automation account to periodically invoke the REST API.

The idea is that the REST invocation is routed to exactly one node, so only one node is awakened.

The drawback is in security, as you must expose an API that just runs a job , ie. can be run repeatedly, and can be potentially abused. Even if it's not exposed to the internet, someone may repeatedly curl to that API using the valid developer's token.

I am comfortable with this approach because at some point I will be required to implement a "force run scheduled task" feature to be used when a previous execution of the job has failed.

Personally, I had to deliver this kind of request on every single project I participated.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM