class ApiTaskManager {

    private static final Logger LOGGER = LoggerFactory.getLogger(ApiTaskManager.class);

    private final Map<String, ExecutorService> queues = new ConcurrentHashMap<>(); // queue name -> ExecutorService
    private final Map<UUID, ApiTask> apiTasks = new ConcurrentHashMap<>(); // UUID -> ApiTask

    private static final ApiTaskManager apiTaskManager = new ApiTaskManager();

    private String contextPath;

    private ApiTaskManager() {
        // singleton
    }

    public static ApiTaskManager getInstance() {
        return apiTaskManager;
    }

    /**
     * @param uuid UUID
     * @return ApiTask object or null
     */
    public ApiTask getApiTask(String uuid) {
        return apiTasks.get(UUID.fromString(uuid));
    }

    /**
     * Set context path. This is used to construct status URLs in {@link #submitApiTask(String, ApiTask)}
     * @param path context path
     */
    public void setContextPath(String path) {
        this.contextPath = path;
    }

    static String getQueueName(String name) {
        String suffix = name.replaceAll("^/", "").replace("/", "-");
        if (!suffix.startsWith("-")) {
             suffix = "-" + suffix;
        }
        return "api_task" + suffix;
    }

    /**
     * Submit an API task for processing.
     * @param name name of the API endpoint
     * @param apiTask ApiTask object
     * @return Response status
     */
    public Response submitApiTask(String name, ApiTask apiTask) {
        String queueName = getQueueName(name);
        if (queues.get(queueName) == null) {
            LOGGER.log(Level.WARNING, "cannot find queue ''{0}''", queueName);
            return Response.status(Response.Status.BAD_REQUEST).build();
        }

        apiTask.setFuture(queues.get(queueName).submit(apiTask.getCallable()));
        apiTasks.put(apiTask.getUuid(), apiTask);

        // This is useful for testing where there is no context path.
        var uriStringBuilder = Optional.ofNullable(contextPath)
                .map(StringBuilder::new)
                .map(stringBuilder -> stringBuilder.append("/api/v1"))
                .orElseGet(StringBuilder::new);
        uriStringBuilder.append("/");
        uriStringBuilder.append(StatusController.PATH);
        uriStringBuilder.append("/");
        uriStringBuilder.append(apiTask.getUuid());

        return Response.status(Response.Status.ACCEPTED).
                location(URI.create(uriStringBuilder.toString())).build();
    }

    /**
     * Delete API task.
     * @param uuid task UUID
     */
    public void deleteApiTask(String uuid) {
        apiTasks.remove(UUID.fromString(uuid));
    }

    /**
     * Add new thread pool.
     * @param name name of the thread pool
     * @param threadCount thread count
     */
    public void addPool(String name, int threadCount) {
        String queueName = getQueueName(name);

        if (queues.get(queueName) != null) {
            throw new IllegalStateException(String.format("queue %s already present", queueName));
        }

        queues.put(queueName, Executors.newFixedThreadPool(threadCount,
                new OpenGrokThreadFactory(queueName)));
    }

    /**
     * Shutdown all executor services and wait 60 seconds for pending tasks.
     * @throws InterruptedException on termination
     */
    public synchronized void shutdown() throws InterruptedException {<FILL_FUNCTION_BODY>}
}

        for (ExecutorService executorService : queues.values()) {
            executorService.shutdown();
        }

        for (Map.Entry<String, ExecutorService> entry : queues.entrySet()) {
            boolean shutdownResult = entry.getValue().awaitTermination(60, TimeUnit.SECONDS);
            if (!shutdownResult) {
                LOGGER.log(Level.WARNING, "abnormal termination for executor service for queue ''{0}''",
                        entry.getKey());
            }
        }
    
<no_super_class>