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
34   * {@code Task} is 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(
46        @Autowired Scheduler scheduler, @Autowired TaskManagerService taskManagerService) {
47      this.scheduler = scheduler;
48      this.taskManagerService = taskManagerService;
49    }
50  
51    /**
52     * Handle after delete. Delete any associated task.
53     *
54     * @param searchIndex the search index that was deleted
55     */
56    @HandleAfterDelete
57    public void afterDeleteSearchIndexEventHandler(SearchIndex searchIndex)
58        throws SchedulerException {
59      if (null != searchIndex.getSchedule()) {
60        JobKey jobKey =
61            taskManagerService.getJobKey(TaskType.INDEX, searchIndex.getSchedule().getUuid());
62  
63        if (null != jobKey && scheduler.checkExists(jobKey)) {
64          logger.info(
65              "Deleting index task {} associated with search index: {}",
66              searchIndex.getSchedule().getUuid(),
67              searchIndex.getName());
68          boolean succes = scheduler.deleteJob(jobKey);
69          logger.info(
70              "Task {}:{} deletion {}",
71              jobKey.getGroup(),
72              jobKey.getName(),
73              (succes ? "succeeded" : "failed"));
74        }
75      }
76    }
77  
78    /**
79     * Handle before save. Create or update the associated task.
80     *
81     * @param searchIndex the search index that was saved
82     * @throws SchedulerException if the task could not be created or updated
83     */
84    @HandleBeforeSave
85    public void beforeSaveSearchIndexEventHandler(SearchIndex searchIndex) throws SchedulerException {
86      // TODO we don't handle the case where the schedule is null here; we would need to determine if
87      //   a task exists that was associated with the search index before;
88      //   this case can already be handled requesting a delete of the scheduled task instead
89      if (null != searchIndex.getSchedule()) {
90        if (null == searchIndex.getSchedule().getUuid()) {
91          validateNoTaskExistsForIndex(searchIndex);
92          // no task exists yet, create one
93          logger.info("Creating new task associated with search index: {}", searchIndex.getName());
94          TMJobDataMap jobDataMap =
95              new TMJobDataMap(
96                  Map.of(
97                      Task.TYPE_KEY,
98                      TaskType.INDEX,
99                      Task.DESCRIPTION_KEY,
100                     searchIndex.getSchedule().getDescription(),
101                     IndexTask.INDEX_KEY,
102                     searchIndex.getId().toString()));
103         if (null != searchIndex.getSchedule().getPriority()
104             && searchIndex.getSchedule().getPriority() > 0) {
105           jobDataMap.put(Task.PRIORITY_KEY, searchIndex.getSchedule().getPriority());
106         }
107         final UUID uuid =
108             taskManagerService.createTask(
109                 IndexTask.class, jobDataMap, searchIndex.getSchedule().getCronExpression());
110         searchIndex.getSchedule().setUuid(uuid);
111       } else {
112         // UUID given, task should exist; update it
113         logger.info(
114             "Updating task {} associated with search index: {}",
115             searchIndex.getSchedule().getUuid(),
116             searchIndex.getName());
117 
118         JobKey jobKey =
119             taskManagerService.getJobKey(TaskType.INDEX, searchIndex.getSchedule().getUuid());
120         if (null != jobKey && scheduler.checkExists(jobKey)) {
121           // the only things that may have changed are the cron expression, priority and description
122           JobDataMap jobDataMap = scheduler.getJobDetail(jobKey).getJobDataMap();
123           jobDataMap.put(Task.DESCRIPTION_KEY, searchIndex.getSchedule().getDescription());
124           jobDataMap.put(Task.CRON_EXPRESSION_KEY, searchIndex.getSchedule().getCronExpression());
125           if (null != searchIndex.getSchedule().getPriority()
126               && searchIndex.getSchedule().getPriority() > 0) {
127             jobDataMap.put(Task.PRIORITY_KEY, searchIndex.getSchedule().getPriority());
128           }
129 
130           taskManagerService.updateTask(jobKey, new TMJobDataMap(jobDataMap));
131         }
132       }
133     }
134   }
135 
136   /**
137    * Validate that no scheduled task exists for the given index.
138    *
139    * @param searchIndex the search index to validate for scheduling a task
140    * @throws SchedulerException if there is a task that is already associated with the given index
141    */
142   private void validateNoTaskExistsForIndex(SearchIndex searchIndex) throws SchedulerException {
143     Optional<JobDataMap> jobDataMapOptional =
144         scheduler.getJobKeys(GroupMatcher.groupEquals(TaskType.INDEX.getValue())).stream()
145             .map(
146                 jobKey -> {
147                   try {
148                     return scheduler.getJobDetail(jobKey).getJobDataMap();
149                   } catch (SchedulerException e) {
150                     logger.error("Error getting task detail", e);
151                     return null;
152                   }
153                 })
154             .filter(Objects::nonNull)
155             .filter(
156                 jobDataMap ->
157                     searchIndex.getId().equals(jobDataMap.getLongValue(IndexTask.INDEX_KEY)))
158             .findFirst();
159 
160     if (jobDataMapOptional.isPresent()) {
161       logger.warn("A scheduled task already exists for search index: {}", searchIndex.getName());
162       throw new SchedulerException(
163           "A scheduled task already exists for search index: '%s'"
164               .formatted(searchIndex.getName()));
165     }
166   }
167 }