Как реализовать многопоточность в Jetty

Реализую сервис, которому в GET-Запросе передаются некоторые теги, а сервис в свою очередь обращается на StackOverFlow API, ищет указанные теги и в JSON формате возвращает нам количество существующих по данному тегу вопросов и кол-во вопросов на которые дали ответ. Выглядит это примерно следующим образом : Вывод в JSON формате

Однако существует некоторая проблема, при указывании пользователем нескольких тэгов сервис должен обрабатывать их параллельно и независимо друг от друга, а сейчас код реализован иначе. Я уже гуглил и пытался реализовывать через футуры и пока загадочные для меня CompletableFuture, однако сервис вообще перестает работать в этом случае.

Main класс:

public class Main {
public static void main(String[] args) {
    ServletContextHandler context = new ServletContextHandler(1);
    context.addServlet(new ServletHolder(new AsyncServer()), "/*");

    Server server = new Server(8080);

    server.setHandler(context);

    try {
        server.start();
        server.join();
    } catch (Exception e) {
        e.printStackTrace();
    }
}
}

Два сервлета: Первый вызывает второй. Первый:

public class AsyncServer extends HttpServlet {
    Gson gson = new Gson();
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        final AsyncContext ctxt = req.startAsync();
        String[] tags = req.getParameterValues("tag");
        for (String tag: tags) {
            ctxt.start(new Runnable() {
                           @Override
                           public void run() {
                               try {
                                   StackOverFlowServlet servlet = new StackOverFlowServlet();
                                   Root root = getStackOverFlow(tag);
                                   servlet.setParameter(tag, gson, root);
                                   servlet.doGet(req, resp);
                                   ctxt.complete();
                               } catch (ServletException | IOException e) {
                                   e.printStackTrace();
                               }
                           }
                       }
            );
        }
    }

    public Root getStackOverFlow(String tag) throws IOException {
        URL url = new URL("https://api.stackexchange.com/2.2/search?pagesize=100&order=desc&sort=creation&tagged=" + tag + "&site=stackoverflow");
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();

        connection.setRequestMethod("GET");
        connection.setRequestProperty("Content-Type", "application/json");

        GZIPInputStream gis = new GZIPInputStream(connection.getInputStream());
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(gis, StandardCharsets.UTF_8));

        String inputLine;

        StringBuilder stringBuilder = new StringBuilder();

        while ((inputLine = bufferedReader.readLine()) != null) {
            stringBuilder.append((inputLine));
        }
        bufferedReader.close();

        return gson.fromJson(stringBuilder.toString(), Root.class);
    }

}

Второй:

public class StackOverFlowServlet extends HttpServlet {
    private String tag;
    private Gson gson;
    private Root root;

    public void setParameter(String tag, Gson gson, Root root){
        this.tag = tag;
        this.gson = gson;
        this.root = root;
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("application/json");
        try {
            if (tag == null) {
                resp.setStatus(404);
            } else {
                List<Boolean> isAnswered = new ArrayList<>();
                for (Item root1 : root.items) {
                    if (root1.is_answered) {
                        isAnswered.add(true);
                    }
                }
                resp.getWriter().printf("\"%s\":{ \"total:\": %s, \"answered\": %s}\n", tag, gson.toJson(root.items.size()), isAnswered.size());
            }
        } catch (Exception e){
            e.printStackTrace();
            Error error = new Error(String.format("Kakaya-to huynya proizoshla: %s", e.getMessage()));
            resp.getWriter().print(gson.toJson(error));
            resp.setStatus(500);
        }
    }

}

Ну и два класса по котороым JSON парсит значения из API:

package json;

import java.util.List;

public class Root{
    public List<Item> items;
    public boolean has_more;
    public int quota_max;
    public int quota_remaining;
}

package json;

import java.util.List;

public class Item {
    public List<String> tags;
    public boolean is_answered;
    public int view_count;
    public int answer_count;
    public int score;
    public int last_activity_date;
    public int creation_date;
    public int question_id;
    public String content_license;
    public String link;
    public String title;
    public int closed_date;
    public String closed_reason;
    public int last_edit_date;
    public int accepted_answer_id;
}

Ответы (1 шт):

Автор решения: Roman Konoval

Неправильно используется API для асинхронной обработки запроса. Главная проблема, что AsyncContext.start и AsyncContext.complete при обработке запроса можно и нужно вызывать ровно один раз. То есть это API позволяет выполнить обработку запроса асинхронно в отдельном потоке, но не дает возможность распараллелить выполнение, это нужно делать самому.

Вторая вытекающая из этого проблема это запись в один поток вывода ответа (resp.getWriter().printf) из разных потоков выполнения. Нет никакой гарантии, что каждый из потоков выполнения полностью запишет свои данные до того как другой запишет свои. То есть данные в ответ могут попасть вперемешку от разных потоков, даже в середине строки, которую записывает один поток.

Еще одна проблема это то, что в асинхронном обработчике идет обращение к HttpServletRequest и HttpServletResponse, которые были переданы в doGet. Это неправильно, их нужно брать из AsyncContext.

То есть решение должно стартовать потоки для обработки отдельных тегов, а асинхронный обработчик должен ждать, когда каждый из запущенных потоков закончит выполнение и уже сам записать результат в ответ. Вот схема решения:

public class AsyncServer extends HttpServlet {

  private ExecutorService executorService = Executors.newFixedThreadPool(10);
  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
     
    ...
    ctxt.start(new Runnable() {
      @Override
      public void run() {
        try {
             List<Future<Root>> futures = new ArryaList<>();
             for (String tag: tags) {
                // добавляем задачу по обработке тега
                // это Callable возвращающий объект Root
                futures.add(
                  executorService.submit(() -> getStackOverFlow(tag));
                );
             } 
             Writer writer = ctxt.getResponse().getWriter();
             
             for (Future<Root> future : futures) {
               // future.get ждет пока поток обрабатывающий тег закончится
               // и возвращает результат
               Root root = future.get();
               // тут записываем кусок json соответствующий данным из root
               // используя writer.printf();
             }
             ctxt.complete();
        } catch (ServletException | IOException e) {
              e.printStackTrace();
        }
      }
  }
}
→ Ссылка