The problem background

In some business scenarios, we need to perform scheduled operations to complete some periodic tasks, such as deleting some historical data from the previous week and performing a scheduled detection task every other week. An easy way to do this in daily development is to use Spring’s @Scheduled annotations. But the Spring framework’s built-in annotations are flawed. When the server time is changed, scheduled tasks may not be executed. To avoid this problem, restart the service after the server time is changed. This article will focus on how the @scheduled annotations fail due to server time changes, and how Scheduled tasks can still work without restarting the service after server time changes.

@Scheduled failure cause analysis

Before we can analyze the @scheduled failure, we need to understand how it actually works before we can piece together the true root cause. After SpringBoot started, there were two main things you did with the @Scheduled section. One was to scan all @Scheduled annotation methods and add the corresponding Scheduled tasks to the global task queue. The other is to start the scheduled task thread pool, the start time is calculated with the period of the scheduled task.

1.@Scheduledparsing

First, take a look at how Spring interprets @Scheduled annotations. Here’s a tip for looking at the source code. Typically, annotations and their parsing classes are placed under a package, as follows:

By above knowable, ScheduledAnnotationBeanPostProcessor is parsed @ Scheduled class notes. We all know that classes that end with BeanPostProcessor are extensions of the Spring framework’s capabilities, which are enhanced during bean initialization by calling the implementation’s before and after methods. The Spring framework calls all of the BeanPostProcessors involved in the pre – and post-enhancement methods. PostProcessAfterInitialization is invoked after the bean is initialized. Shown below for SpringBoot starts, roughly ScheduledAnnotationBeanPostProcessor call process.

ScheduledAnnotationBeanPostProcessor corresponding source as shown below:

public class ScheduledAnnotationBeanPostProcessor implements MergedBeanDefinitionPostProcessor.DestructionAwareBeanPostProcessor.Ordered.EmbeddedValueResolverAware.BeanNameAware.BeanFactoryAware.ApplicationContextAware.SmartInitializingSingleton.ApplicationListener<ContextRefreshedEvent>, DisposableBean {...public Object postProcessAfterInitialization(Object bean, String beanName) {
        if (bean instanceof AopInfrastructureBean) {
            return bean;
        } else {
            // (key-1) Parse all Scheduled annotation classesClass<? > targetClass = AopProxyUtils.ultimateTargetClass(bean);if (!this.nonAnnotatedClasses.contains(targetClass)) {
                Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetClass, new MetadataLookup<Set<Scheduled>>() {
                    public Set<Scheduled> inspect(Method method) {
                        Set<Scheduled> scheduledMethods = AnnotatedElementUtils.getMergedRepeatableAnnotations(method, Scheduled.class, Schedules.class);
                        return! scheduledMethods.isEmpty() ? scheduledMethods :null; }});if (annotatedMethods.isEmpty()) {
                    this.nonAnnotatedClasses.add(targetClass);
                    if (this.logger.isTraceEnabled()) {
                        this.logger.trace("No @Scheduled annotations found on bean class: "+ targetClass); }}else {
                    Iterator var5 = annotatedMethods.entrySet().iterator();

                    while(var5.hasNext()) {
                        Entry<Method, Set<Scheduled>> entry = (Entry)var5.next();
                        Method method = (Method)entry.getKey();
                        Iterator var8 = ((Set)entry.getValue()).iterator();

                        while(var8.hasNext()) {
                            Scheduled scheduled = (Scheduled)var8.next();
                            // (key-2) handles classes modified by annotations
                            this.processScheduled(scheduled, method, bean); }}if (this.logger.isDebugEnabled()) {
                        this.logger.debug(annotatedMethods.size() + " @Scheduled methods processed on bean '" + beanName + "'."+ annotatedMethods); }}}returnbean; }}... }Copy the code

It does two main things:

The key – 1:

Parse all methods modified with @scheduled and @schedules annotations, and store the methods and their Schedules collections into a map. Note that the value corresponding to the method as a key is a collection, illustrating that a method can be modified by multiple @scheduled and @schedules annotations.

The key – 2:

Take the key-1 map method out and process it, calling the processScheduled method. As follows:

protected void processScheduled(Scheduled scheduled, Method method, Object bean) {
        try {
            Runnable runnable = this.createRunnable(bean, method);
            boolean processedSchedule = false;
            String errorMessage = "Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required";
            Set<ScheduledTask> tasks = new LinkedHashSet(4);
            longinitialDelay = scheduled.initialDelay(); .// (key-1) Get the corresponding scron parameter in the annotation
            String cron = scheduled.cron();
            if (StringUtils.hasText(cron)) {
                String zone = scheduled.zone();
                if (this.embeddedValueResolver ! =null) {
                    cron = this.embeddedValueResolver.resolveStringValue(cron);
                    zone = this.embeddedValueResolver.resolveStringValue(zone);
                }

                if (StringUtils.hasLength(cron)) {
                    Assert.isTrue(initialDelay == -1L."'initialDelay' not supported for cron triggers");
                    processedSchedule = true;
                    if (!"-".equals(cron)) {
                        TimeZone timeZone;
                        if (StringUtils.hasText(zone)) {
                            timeZone = StringUtils.parseTimeZoneString(zone);
                        } else {
                            timeZone = TimeZone.getDefault();
                        }
                        // (key-2) Add to the task list
                        tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, newCronTrigger(cron, timeZone)))); }}}... Assert.isTrue(processedSchedule, errorMessage); Map var18 =this.scheduledTasks;
            synchronized(this.scheduledTasks) {
                Set<ScheduledTask> regTasks = (Set)this.scheduledTasks.computeIfAbsent(bean, (key) -> {
                    return new LinkedHashSet(4); }); regTasks.addAll(tasks); }}catch (IllegalArgumentException var25) {
            throw new IllegalStateException("Encountered invalid @Scheduled method '" + method.getName() + "'."+ var25.getMessage()); }}Copy the code

This article mainly describes scRON scheduled task parsing.

The key – 1:

Time parameters in annotations are retrieved and parsed.

The key – 2:

Wrap the task as a CronTask to add to the global scheduled task.

2. Start scheduled tasks

After SpringBoot is started, scheduled tasks are started by listening to events.

public ScheduledTask scheduleCronTask(CronTask task) {
		ScheduledTask scheduledTask = this.unresolvedTasks.remove(task);
		boolean newTask = false;
		if (scheduledTask == null) {
			scheduledTask = new ScheduledTask();
			newTask = true;
		}
		if (this.taskScheduler ! =null) {
			scheduledTask.future = this.taskScheduler.schedule(task.getRunnable(), task.getTrigger());
		}
		else {
			addCronTask(task);
			this.unresolvedTasks.put(task, scheduledTask);
		}
		return (newTask ? scheduledTask : null);
	}

Copy the code

The root cause is that the JVM records system time after startup, and then the JVM calculates its own time based on CPU ticks, which is the baseline time of the scheduled task. If the system time is changed, Spring will invalidate the internal timing task when it compares the previously obtained base time with the current obtained system time. Because the system time changes, scheduled tasks are not triggered.

publicScheduledFuture<? > schedule() {synchronized (this.triggerContextMonitor) {
			this.scheduledExecutionTime = this.trigger.nextExecutionTime(this.triggerContext);
			if (this.scheduledExecutionTime == null) {
				return null;
			}
			// Get the time difference
			long initialDelay = this.scheduledExecutionTime.getTime() - System.currentTimeMillis();
			this.currentFuture = this.executor.schedule(this, initialDelay, TimeUnit.MILLISECONDS);
			return this; }}Copy the code

The Scheduled failure issue has been resolved

1. Crude solutions

Restart the service and calculate the task execution time again according to the time adjusted by the server. In this way, the scheduled task will not be executed. However, this solution is too unfriendly.

2. An elegant solution

To avoid using the @scheduled annotation, Scheduled tasks may not be executed when changing server time. In the project need to use the timing scenario missions, make alternative ScheduledThreadPoolExecutor, its task scheduling is based on the relative time, The reason is that it stores in the task internal the time that the task will need before the next scheduling (using the relative time based on system.nanotime implementation, will not change because the System time changes, such as 10 seconds before the next execution, will not change to 4 seconds after the System time is set 6 seconds earlier).