View Javadoc
1   /*
2    * Copyright (C) 2024 B3Partners B.V.
3    *
4    * SPDX-License-Identifier: MIT
5    */
6   package org.tailormap.api.repository.events;
7   
8   import java.lang.invoke.MethodHandles;
9   import java.util.Map;
10  import java.util.Objects;
11  import java.util.Optional;
12  import java.util.UUID;
13  import org.quartz.JobDataMap;
14  import org.quartz.JobKey;
15  import org.quartz.Scheduler;
16  import org.quartz.SchedulerException;
17  import org.quartz.impl.matchers.GroupMatcher;
18  import org.slf4j.Logger;
19  import org.slf4j.LoggerFactory;
20  import org.springframework.beans.factory.annotation.Autowired;
21  import org.springframework.data.rest.core.annotation.HandleAfterDelete;
22  import org.springframework.data.rest.core.annotation.HandleBeforeSave;
23  import org.springframework.data.rest.core.annotation.RepositoryEventHandler;
24  import org.springframework.stereotype.Component;
25  import org.tailormap.api.persistence.SearchIndex;
26  import org.tailormap.api.scheduling.IndexTask;
27  import org.tailormap.api.scheduling.TMJobDataMap;
28  import org.tailormap.api.scheduling.Task;
29  import org.tailormap.api.scheduling.TaskManagerService;
30  import org.tailormap.api.scheduling.TaskType;
31  
32  /**
33   * Event handler for Solr indexes; when a {@code SearchIndex} is created, updated or deleted a {@code Task} is
34   * associated.
35   */
36  @Component
37  @RepositoryEventHandler
38  public class SearchIndexEventHandler {
39    private static final Logger logger =
40        LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
41  
42    private final Scheduler scheduler;
43    private final TaskManagerService taskManagerService;
44  
45    public SearchIndexEventHandler(@Autowired Scheduler scheduler, @Autowired TaskManagerService taskManagerService) {
46      this.scheduler = scheduler;
47      this.taskManagerService = taskManagerService;
48    }
49  
50    /**
51     * Handle after delete. Delete any associated task.
52     *
53     * @param searchIndex the search index that was deleted
54     */
55    @HandleAfterDelete
56    public void afterDeleteSearchIndexEventHandler(SearchIndex searchIndex) throws SchedulerException {
57      if (null != searchIndex.getSchedule()) {
58        JobKey jobKey = taskManagerService.getJobKey(
59            TaskType.INDEX, searchIndex.getSchedule().getUuid());
60  
61        if (null != jobKey && scheduler.checkExists(jobKey)) {
62          logger.info(
63              "Deleting index task {} associated with search index: {}",
64              searchIndex.getSchedule().getUuid(),
65              searchIndex.getName());
66          boolean succes = scheduler.deleteJob(jobKey);
67          logger.info(
68              "Task {}:{} deletion {}",
69              jobKey.getGroup(),
70              jobKey.getName(),
71              (succes ? "succeeded" : "failed"));
72        }
73      }
74    }
75  
76    /**
77     * Handle before save. Create or update the associated task.
78     *
79     * @param searchIndex the search index that was saved
80     * @throws SchedulerException if the task could not be created or updated
81     */
82    @HandleBeforeSave
83    public void beforeSaveSearchIndexEventHandler(SearchIndex searchIndex) throws SchedulerException {
84      // TODO we don't handle the case where the schedule is null here; we would need to determine if
85      //   a task exists that was associated with the search index before;
86      //   this case can already be handled requesting a delete of the scheduled task instead
87      if (null != searchIndex.getSchedule()) {
88        if (null == searchIndex.getSchedule().getUuid()) {
89          validateNoTaskExistsForIndex(searchIndex);
90          // no task exists yet, create one
91          logger.info("Creating new task associated with search index: {}", searchIndex.getName());
92          TMJobDataMap jobDataMap = new TMJobDataMap(Map.of(
93              Task.TYPE_KEY,
94              TaskType.INDEX,
95              Task.DESCRIPTION_KEY,
96              searchIndex.getSchedule().getDescription(),
97              IndexTask.INDEX_KEY,
98              searchIndex.getId().toString()));
99          if (null != searchIndex.getSchedule().getPriority()
100             && searchIndex.getSchedule().getPriority() > 0) {
101           jobDataMap.put(Task.PRIORITY_KEY, searchIndex.getSchedule().getPriority());
102         }
103         final UUID uuid = taskManagerService.createTask(
104             IndexTask.class, jobDataMap, searchIndex.getSchedule().getCronExpression());
105         searchIndex.getSchedule().setUuid(uuid);
106       } else {
107         // UUID given, task should exist; update it
108         logger.info(
109             "Updating task {} associated with search index: {}",
110             searchIndex.getSchedule().getUuid(),
111             searchIndex.getName());
112 
113         JobKey jobKey = taskManagerService.getJobKey(
114             TaskType.INDEX, searchIndex.getSchedule().getUuid());
115         if (null != jobKey && scheduler.checkExists(jobKey)) {
116           // the only things that may have changed are the cron expression, priority and description
117           JobDataMap jobDataMap = scheduler.getJobDetail(jobKey).getJobDataMap();
118           jobDataMap.put(
119               Task.DESCRIPTION_KEY, searchIndex.getSchedule().getDescription());
120           jobDataMap.put(
121               Task.CRON_EXPRESSION_KEY, searchIndex.getSchedule().getCronExpression());
122           if (null != searchIndex.getSchedule().getPriority()
123               && searchIndex.getSchedule().getPriority() > 0) {
124             jobDataMap.put(
125                 Task.PRIORITY_KEY, searchIndex.getSchedule().getPriority());
126           }
127 
128           taskManagerService.updateTask(jobKey, new TMJobDataMap(jobDataMap));
129         }
130       }
131     }
132   }
133 
134   /**
135    * Validate that no scheduled task exists for the given index.
136    *
137    * @param searchIndex the search index to validate for scheduling a task
138    * @throws SchedulerException if there is a task that is already associated with the given index
139    */
140   private void validateNoTaskExistsForIndex(SearchIndex searchIndex) throws SchedulerException {
141     Optional<JobDataMap> jobDataMapOptional =
142         scheduler.getJobKeys(GroupMatcher.groupEquals(TaskType.INDEX.getValue())).stream()
143             .map(jobKey -> {
144               try {
145                 return scheduler.getJobDetail(jobKey).getJobDataMap();
146               } catch (SchedulerException e) {
147                 logger.error("Error getting task detail", e);
148                 return null;
149               }
150             })
151             .filter(Objects::nonNull)
152             .filter(jobDataMap -> searchIndex.getId().equals(jobDataMap.getLongValue(IndexTask.INDEX_KEY)))
153             .findFirst();
154 
155     if (jobDataMapOptional.isPresent()) {
156       logger.warn("A scheduled task already exists for search index: {}", searchIndex.getName());
157       throw new SchedulerException(
158           "A scheduled task already exists for search index: '%s'".formatted(searchIndex.getName()));
159     }
160   }
161 }