Самый важный вопрос - хотите ли вы синхронный или асинхронный API?
При синхронном, пользователь отправляет запрос и ожидает ответа в этом же HTTP вызове.
Прогресс можно писать прямо в output stream сервлета, но нужно договориться о протоколе.
Например, писать строки вида STATUS REPORT: 80%, а клиент должен уметь различать такие строки от настоящих данных.
Если запрос разорвется, то должен быть механизм подключения к той же задаче. Это довольно сложно, но реально. Например, можно первым делом генерить некий ID и выдавать его в первых строках, и если соединение рвется, клиент может переподключиться с этим же ID.
При асинхронном API генерируется ID, задача ставится в очередь, а клиент получает этот ID и HTTP вызов завершается. Потом клиент должен периодически опрашивать уже другой API (например, getResult(id) ), и если результата еще нет, этот API может возвращать что-то типа "NOT READY, STATUS 85%" в хедерах.
Подходы можно комбинировать - запуск задачи может быть асинхронным, а getResult - синхронным.
Конкретная реализация зависит от многих факторов. Можно просто в Executor добавлять задачу, а статус писать в какую-нибудь очередь. Можно на JMS делать. Можно вообще другой процесс запускать и общаться с ними через std in/out.