Saltar al contenido principal

Capítulo 4: Tareas Programadas y Asíncronas

Objetivos del Capítulo​

  • Configurar tareas programadas con @Scheduled
  • Dominar las expresiones Cron
  • Implementar métodos asíncronos con @Async
  • Configurar pools de hilos
  • Gestionar errores y monitoreo

1. Configuración Básica​

1.1 Activar el Scheduling​

package com.example.scheduling;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableScheduling
@EnableAsync
public class SchedulingApplication {

public static void main(String[] args) {
SpringApplication.run(SchedulingApplication.class, args);
}
}

1.2 Configuración del Thread Pool​

package com.example.scheduling.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

@Configuration
public class SchedulingConfig implements SchedulingConfigurer {

@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setTaskScheduler(taskScheduler());
}

@Bean
public ThreadPoolTaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(10);
scheduler.setThreadNamePrefix("scheduled-");
scheduler.setAwaitTerminationSeconds(60);
scheduler.setWaitForTasksToCompleteOnShutdown(true);
scheduler.setErrorHandler(t -> {
// Log de errores
System.err.println("Scheduled task error: " + t.getMessage());
});
return scheduler;
}
}

2. @Scheduled - Tareas Programadas​

2.1 Fixed Rate​

Ejecuta a intervalo fijo desde el inicio de la última ejecución.

@Component
public class ScheduledTasks {

private static final Logger log = LoggerFactory.getLogger(ScheduledTasks.class);

// Cada 5 segundos
@Scheduled(fixedRate = 5000)
public void reportCurrentTime() {
log.info("Current time: {}", LocalDateTime.now());
}

// Con retardo inicial de 10 segundos
@Scheduled(fixedRate = 5000, initialDelay = 10000)
public void taskWithInitialDelay() {
log.info("Task with initial delay");
}

// Usar propiedades
@Scheduled(fixedRateString = "${task.fixed-rate:5000}")
public void configurableTask() {
log.info("Configurable task");
}
}

2.2 Fixed Delay​

Ejecuta a intervalo fijo desde el final de la última ejecución.

@Component
public class ScheduledTasks {

// 3 segundos después del final de la ejecución anterior
@Scheduled(fixedDelay = 3000)
public void processQueue() {
log.info("Processing queue...");
// Simulación de procesamiento
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
log.info("Queue processed");
}
}

2.3 Expresiones Cron​

@Component
public class ScheduledTasks {

// Todos los días a medianoche
@Scheduled(cron = "0 0 0 * * *")
public void dailyCleanup() {
log.info("Running daily cleanup");
}

// Cada hora, en el minuto 0
@Scheduled(cron = "0 0 * * * *")
public void hourlyReport() {
log.info("Generating hourly report");
}

// Todos los lunes a las 9h
@Scheduled(cron = "0 0 9 * * MON")
public void weeklyReport() {
log.info("Generating weekly report");
}

// El primer día de cada mes a las 6h
@Scheduled(cron = "0 0 6 1 * *")
public void monthlyReport() {
log.info("Generating monthly report");
}

// Cada 15 minutos durante el horario laboral
@Scheduled(cron = "0 */15 9-17 * * MON-FRI")
public void businessHoursTask() {
log.info("Business hours task");
}

// Con zona horaria
@Scheduled(cron = "0 0 9 * * *", zone = "America/Montreal")
public void taskWithTimezone() {
log.info("Task at 9 AM Montreal time");
}
}

2.4 Sintaxis Cron​

┌───────────── segundo (0-59)
│ ┌───────────── minuto (0-59)
│ │ ┌───────────── hora (0-23)
│ │ │ ┌───────────── día del mes (1-31)
│ │ │ │ ┌───────────── mes (1-12 o JAN-DEC)
│ │ │ │ │ ┌───────────── día de la semana (0-7 o SUN-SAT)
│ │ │ │ │ │
* * * * * *
ExpresiónDescripción
*Todos los valores
*/15Cada 15
0-23Rango de valores
1,15Lista de valores
?No especificado (día)
LÚltimo (día del mes)
WDía laborable más cercano
#Enésimo día de la semana (2#1 = primer lunes)

2.5 Ejemplos Cron Comunes​

ExpresiónDescripción
0 0 * * * *Cada hora
0 */10 * * * *Cada 10 minutos
0 0 8-18 * * *Cada hora de 8h a 18h
0 0 0 * * *Todos los días a medianoche
0 0 0 * * SUNTodos los domingos a medianoche
0 0 0 1 * *Primer día del mes
0 0 0 L * *Último día del mes
0 0 0 ? * 6LÚltimo viernes del mes

3. @Async - Métodos Asíncronos​

3.1 Configuración​

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

@Override
@Bean(name = "taskExecutor")
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
executor.initialize();
return executor;
}

@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) -> {
System.err.println("Async error in method " + method.getName() + ": " + ex.getMessage());
};
}
}

3.2 Métodos Asíncronos​

@Service
public class EmailService {

private static final Logger log = LoggerFactory.getLogger(EmailService.class);

// Fire and forget
@Async
public void sendEmailAsync(String to, String subject, String body) {
log.info("Sending email to {} on thread {}", to, Thread.currentThread().getName());

// Simulación de envío
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}

log.info("Email sent to {}", to);
}

// Con valor de retorno
@Async
public CompletableFuture<Boolean> sendEmailWithResult(String to, String subject) {
log.info("Sending email to {}", to);

try {
// Simulación
Thread.sleep(2000);
log.info("Email sent successfully to {}", to);
return CompletableFuture.completedFuture(true);
} catch (Exception e) {
log.error("Failed to send email to {}", to, e);
return CompletableFuture.completedFuture(false);
}
}

// Con executor específico
@Async("emailExecutor")
public void sendWithSpecificExecutor(String to) {
// Utiliza el bean "emailExecutor"
}
}

3.3 Uso​

@RestController
@RequestMapping("/api/orders")
public class OrderController {

private final OrderService orderService;
private final EmailService emailService;

@PostMapping
public ResponseEntity<Order> createOrder(@RequestBody OrderRequest request) {
// Crear la orden (sincrónico)
Order order = orderService.createOrder(request);

// Enviar el email (asíncrono - no bloquea)
emailService.sendEmailAsync(
request.getEmail(),
"Order Confirmation",
"Your order #" + order.getId() + " has been created"
);

// Retorna inmediatamente
return ResponseEntity.ok(order);
}

@PostMapping("/bulk-email")
public ResponseEntity<String> sendBulkEmails(@RequestBody List<String> emails) {
// Enviar varios emails en paralelo
List<CompletableFuture<Boolean>> futures = emails.stream()
.map(email -> emailService.sendEmailWithResult(email, "Newsletter"))
.collect(Collectors.toList());

// Esperar todos los resultados
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenRun(() -> {
long successCount = futures.stream()
.map(CompletableFuture::join)
.filter(Boolean::booleanValue)
.count();
log.info("Sent {} emails successfully", successCount);
});

return ResponseEntity.accepted().body("Emails are being sent");
}
}

4. Casos de Uso Comunes​

4.1 Limpieza de Datos Expirados​

@Component
public class CleanupTasks {

private final SessionRepository sessionRepository;
private final TempFileService tempFileService;

// Limpiar sesiones expiradas cada hora
@Scheduled(cron = "0 0 * * * *")
public void cleanupExpiredSessions() {
log.info("Starting session cleanup");

LocalDateTime expirationThreshold = LocalDateTime.now().minusHours(24);
int deleted = sessionRepository.deleteByLastAccessBefore(expirationThreshold);

log.info("Cleaned up {} expired sessions", deleted);
}

// Limpiar archivos temporales todos los días a las 3h
@Scheduled(cron = "0 0 3 * * *")
public void cleanupTempFiles() {
log.info("Starting temp files cleanup");

int deleted = tempFileService.deleteOlderThan(Duration.ofDays(7));

log.info("Deleted {} temp files", deleted);
}
}

4.2 Generación de Reportes​

@Component
public class ReportTasks {

private final ReportService reportService;
private final EmailService emailService;

// Reporte diario a las 6h
@Scheduled(cron = "0 0 6 * * *")
public void generateDailyReport() {
log.info("Generating daily report");

DailyReport report = reportService.generateDailyReport(LocalDate.now().minusDays(1));
emailService.sendReportAsync("admin@company.com", report);

log.info("Daily report generated and sent");
}

// Reporte semanal el lunes a las 7h
@Scheduled(cron = "0 0 7 * * MON")
public void generateWeeklyReport() {
log.info("Generating weekly report");

WeeklyReport report = reportService.generateWeeklyReport();
emailService.sendReportAsync("management@company.com", report);
}
}

4.3 Sincronización de Datos​

@Component
public class SyncTasks {

private final ExternalApiService externalApi;
private final ProductRepository productRepository;

// Sincronizar precios cada 15 minutos
@Scheduled(fixedRate = 900000) // 15 minutos
public void syncPrices() {
log.info("Starting price sync");

try {
List<PriceUpdate> updates = externalApi.fetchPriceUpdates();

updates.forEach(update -> {
productRepository.updatePrice(update.getProductId(), update.getNewPrice());
});

log.info("Synced {} price updates", updates.size());
} catch (Exception e) {
log.error("Price sync failed", e);
}
}
}

4.4 Health Check​

@Component
public class HealthCheckTasks {

private final List<ExternalService> externalServices;
private final AlertService alertService;

// Verificar los servicios externos cada minuto
@Scheduled(fixedRate = 60000)
public void checkExternalServices() {
externalServices.forEach(service -> {
boolean healthy = service.healthCheck();

if (!healthy) {
log.warn("Service {} is unhealthy", service.getName());
alertService.sendAlert("Service " + service.getName() + " is down");
}
});
}
}

5. Gestión de Errores​

5.1 Try-Catch en las Tareas​

@Component
public class RobustScheduledTask {

@Scheduled(fixedRate = 60000)
public void robustTask() {
try {
performTask();
} catch (Exception e) {
log.error("Task failed, will retry on next schedule", e);
// No relanzar para evitar detener el scheduling
}
}
}

5.2 Error Handler Global​

@Configuration
public class SchedulingConfig implements SchedulingConfigurer {

@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(5);
scheduler.setErrorHandler(t -> {
log.error("Scheduled task error", t);
// Enviar alerta, métricas, etc.
alertService.sendAlert("Scheduled task failed: " + t.getMessage());
});
scheduler.initialize();
taskRegistrar.setTaskScheduler(scheduler);
}
}

6. Control Condicional​

6.1 Activar/Desactivar por Configuración​

@Component
@ConditionalOnProperty(name = "app.scheduling.enabled", havingValue = "true")
public class ScheduledTasks {
// Estas tareas solo se ejecutan si app.scheduling.enabled=true
}

6.2 Control por Perfil​

@Component
@Profile("!test") // No en entorno de test
public class ProductionScheduledTasks {

@Scheduled(cron = "0 0 0 * * *")
public void productionOnlyTask() {
// Se ejecuta solo en producción
}
}

6.3 Leader Election (Cluster)​

@Component
public class ClusterAwareTask {

private final LockProvider lockProvider;

@Scheduled(cron = "0 0 * * * *")
@SchedulerLock(name = "hourlyTask", lockAtLeastFor = "PT5M", lockAtMostFor = "PT30M")
public void hourlyTask() {
// Solo una instancia en el cluster ejecuta esta tarea
log.info("Running hourly task on this instance");
}
}

7. Resumen​

Anotaciones​

AnotaciónUso
@EnableSchedulingActiva el scheduling
@EnableAsyncActiva los métodos async
@ScheduledMarca un método programado
@AsyncMarca un método asíncrono

Opciones @Scheduled​

OpciónDescripción
fixedRateIntervalo desde el inicio
fixedDelayIntervalo desde el final
cronExpresión Cron
initialDelayRetardo inicial
zoneZona horaria para Cron

Buenas Prácticas​

  1. Siempre gestionar las excepciones en las tareas
  2. Utilizar pools de hilos dedicados
  3. Monitorear la ejecución de las tareas
  4. Evitar tareas demasiado largas
  5. Usar locking en entorno cluster
  6. Registrar el inicio y fin de las tareas

Capítulo Siguiente: WebSockets

Volver al Módulo 14