Как красиво завершить работу с NIO при прерывании потока?

Здравствуйте.
Я делаю сервер на неблокирующих сокетах. К нему будут подключаться всякие разные клиенты и общаться с сервером по протоколу. В протоколе определены пакеты типа "начало соединения" и "конец соединения". В норме, клиент устанавливает TCP соединение с сервером, посылает пакет "начало соединения", сервер проверяет валидность логина с паролем из этого пакета и отвечает "соединение - ок". Далее клиент асинхронно отправляет большим потоком кучу сообщений, которые обрабатываются сервером в ThreadPoolExecutor.
В какой-то момент одна из сторон хочет завершить соединение и отправляет пакет "конец соединения". Другая сторона отвечает "конец соединения - ок" и после этого сокет закрывается.

Всё работает хорошо, пока администратор не решает перезапустить сервер. Для этого jvm процессу посылается сигнал SIGTERM, который обрабатывается достаточно прямолинейно - устанавливается interrupt статус всем потокам.
При этом я хочу корректно завершить все клиентские соединения, обработав до конца всё ещё выполняющиеся запросы, отправив каждому клиенту по пакету "завершение соединения", дождаться от каждого "завершение соединения - ок", закрыть сокет и спокойно завершить поток.

Проблема в том, что сокет закрывается с исключением ClosedByInterruptException на первом же вызове read()/write() совершенным после установки статуса прерывания. И это закрывает для меня возможности красиво отсоединить клиентов.

Вопрос в том, как сделать так, чтобы сокет не закрывался при поступлении запроса на прерывание?
Код, иллюстрирующий ситуацию, можно созерцать по ссылке: pastebin.com/nVsVvWXq

Варианты, которые меня не устраивают:
1. Как-то ещё оповестить процесс о том, что нужно завершиться.
Тут придётся везде понатыкать флаги-индикаторы завершения работы. При этом я лишаюсь возможности прервать другие блокирующие операции (типа Thread.sleep()), которые могут выполняться потенциально долго и получаю лишний код. Ещё администратору придётся помнить, что данный конкретный процесс особенный и его нельзя завершить как любой другой процесс в системе.

2. Сделать всё на традиционных блокирующих сокетах.
Это не годится, потому что у меня слишком много клиентов, чтобы каждому выделять по потоку. Собственно, из-за этого я и переделываю сетевую часть на NIO.

3. Натыкать перед каждым вызовом read()/write() проверку interrupt статуса. Что-то типа:
if ( Thread.interrupted() ) {
  // ...
  // завершающие действия
  // ...
  socketChannel.write(byeBye);
  // ...
  socketChannel.read(byeByeOk);
  socketChannel.close();
  Thread.currentThread().interrupt();
} else {
  socketChannel.read(inputBuffer);
}

Но это проблемы не решает, так как статус прерывания может быть установлен ровно между вызовами Thread.interrupted() и socketChannel.read().
Хотя и уменьшает вероятность её возникновения.

4. Переделать на NIO.2.
Я проверил, при работе с AsynchronousSocketChannel принудительного закрытия сокета не происходит и проблемы просто не существует. Но я ограничен java 6.

5. Так же я не хочу рассматривать варианты с модификацией протокола, клиентов, привлечении сторонних библиотек и софта. То есть вопрос принципиально про NIO и поведение Channel'ов при установке потоку interrupted статуса. Мне кажется, что не могли архитекторы NIO не предусмотреть подобный сценарий. Должен же быть обходной путь, но я его не вижу или просто задаю неправильные вопросы гуглу.
  • Вопрос задан
  • 601 просмотр
Решения вопроса 1
Vamp
@Vamp Автор вопроса
Проблема оказалась в изначально неверном предположении.

Во всех книгах написано, что jvm завершает свою работу только после завершения всех не daemon потоков. Но нигде не уточняется при каких условиях поток получает interrupted status. Мне всегда казалось, что при закрытии приложения внешним образом (Crtl+C, SIGTERM, рестарт системы), jvm сначала установит interrupted статус всем потокам, дождётся завершения последнего не daemon потока, после чего закроется. И достаточно просто продумать адекватную реакцию на прерывание потока.

Как оказалось, единственный способ установить interrupted status - вызвать Thread.interrupt() из пользовательского кода. А все остальные способы завершения тупо останавливают потоки jvm без возможности как-то отреагировать на ситуацию. И единственный способ отследить это - установить shutdown hook.
В принципе, это решение меня вполне устраивает.
Ответ написан
Комментировать
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы