Handle network errors

- Move HTTP connect/disconnect logic to networkManager
- If a talk fails due to socket error, HTTP transport error or
  nio bad response that might change, retry every 2s until success
- Clean up some leftover debug prints
This commit is contained in:
miruka 2019-04-19 16:15:21 -04:00
parent 1f04fa07cb
commit 0d7728665f
5 changed files with 51 additions and 42 deletions

View File

@ -7,8 +7,6 @@
it should be the peer's display name instead. it should be the peer's display name instead.
- Support "Empty room (was ...)" after peer left - Support "Empty room (was ...)" after peer left
- Catch network errors in socket operations
- Proper logoff when closing client - Proper logoff when closing client
- Handle cases where an avatar char is # or @ (#alias room, @user\_id) - Handle cases where an avatar char is # or @ (#alias room, @user\_id)

View File

@ -79,10 +79,7 @@ class Client(QObject):
@pyqtSlot(str, str) @pyqtSlot(str, str)
@futurize @futurize
def login(self, password: str, device_name: str = "") -> None: def login(self, password: str, device_name: str = "") -> None:
self.net.write(self.nio.connect())
response = self.net.talk(self.nio.login, password, device_name) response = self.net.talk(self.nio.login, password, device_name)
self.net_sync.write(self.nio_sync.connect())
self.nio_sync.receive_response(response) self.nio_sync.receive_response(response)
@ -90,11 +87,8 @@ class Client(QObject):
@futurize @futurize
def resumeSession(self, user_id: str, token: str, device_id: str def resumeSession(self, user_id: str, token: str, device_id: str
) -> None: ) -> None:
self.net.write(self.nio.connect())
response = nr.LoginResponse(user_id, device_id, token) response = nr.LoginResponse(user_id, device_id, token)
self.nio.receive_response(response) self.nio.receive_response(response)
self.net_sync.write(self.nio_sync.connect())
self.nio_sync.receive_response(response) self.nio_sync.receive_response(response)
@ -102,8 +96,8 @@ class Client(QObject):
@futurize @futurize
def logout(self) -> None: def logout(self) -> None:
self._stop_sync.set() self._stop_sync.set()
self.net.write(self.nio.disconnect()) self.net.http_disconnect()
self.net_sync.write(self.nio_sync.disconnect()) self.net_sync.http_disconnect()
@pyqtSlot() @pyqtSlot()
@ -181,24 +175,19 @@ class Client(QObject):
set_for_secs = 5 set_for_secs = 5
last_set, last_time = self._last_typing_set[room_id] last_set, last_time = self._last_typing_set[room_id]
print(last_set, last_time)
if not typing and last_set is False: if not typing and last_set is False:
print("ignore 1")
return return
if typing and time.time() - last_time < set_for_secs - 1: if typing and time.time() - last_time < set_for_secs - 1:
print("ignore 2")
return return
print("SET", typing)
self._last_typing_set[room_id] = (typing, time.time()) self._last_typing_set[room_id] = (typing, time.time())
self.net.talk( self.net.talk(
self.nio.room_typing, self.nio.room_typing,
room_id = room_id, room_id = room_id,
typing_state = typing, typing_state = typing,
timeout = set_for_secs * 1000, timeout = set_for_secs * 1000,
) )

View File

@ -3,11 +3,12 @@
import re import re
import html_sanitizer.sanitizer as sanitizer
import mistune import mistune
from lxml.html import HtmlElement, etree from lxml.html import HtmlElement, etree
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSlot from PyQt5.QtCore import QObject, pyqtProperty, pyqtSlot
import html_sanitizer.sanitizer as sanitizer
class HtmlFilter(QObject): class HtmlFilter(QObject):
link_regexes = [re.compile(r, re.IGNORECASE) for r in [ link_regexes = [re.compile(r, re.IGNORECASE) for r in [

View File

@ -11,6 +11,7 @@ from uuid import UUID
import nio import nio
import nio.responses as nr import nio.responses as nr
from nio.exceptions import RemoteTransportError
OptSock = Optional[ssl.SSLSocket] OptSock = Optional[ssl.SSLSocket]
NioRequestFunc = Callable[..., Tuple[UUID, bytes]] NioRequestFunc = Callable[..., Tuple[UUID, bytes]]
@ -48,7 +49,10 @@ class NetworkManager:
@staticmethod @staticmethod
def _close_socket(sock: socket.socket) -> None: def _close_socket(sock: Optional[socket.socket]) -> None:
if not sock:
return
try: try:
sock.shutdown(how=socket.SHUT_RDWR) sock.shutdown(how=socket.SHUT_RDWR)
except OSError: # Already closer by server except OSError: # Already closer by server
@ -56,6 +60,14 @@ class NetworkManager:
sock.close() sock.close()
def http_disconnect(self) -> None:
data = self.nio.disconnect()
try:
self.write(data)
except (OSError, RemoteTransportError):
pass
def read(self, with_sock: OptSock = None) -> nr.Response: def read(self, with_sock: OptSock = None) -> nr.Response:
sock = with_sock or self._get_socket() sock = with_sock or self._get_socket()
@ -78,9 +90,6 @@ class NetworkManager:
def write(self, data: bytes, with_sock: OptSock = None) -> None: def write(self, data: bytes, with_sock: OptSock = None) -> None:
if not data:
return
sock = with_sock or self._get_socket() sock = with_sock or self._get_socket()
sock.sendall(data) sock.sendall(data)
@ -88,34 +97,47 @@ class NetworkManager:
self._close_socket(sock) self._close_socket(sock)
def talk(self, nio_func: NioRequestFunc, *args, **kwargs) -> nr.Response: def talk(self,
nio_func: NioRequestFunc,
*args,
**kwargs) -> nr.Response:
with self._lock: with self._lock:
while True: while True:
to_send = nio_func(*args, **kwargs)[1] sock = None
sock = self._get_socket()
try: try:
sock = self._get_socket()
if not self.nio.connection:
# Establish HTTP protocol connection:
self.write(self.nio.connect(), sock)
to_send = nio_func(*args, **kwargs)[1]
self.write(to_send, sock) self.write(to_send, sock)
response = self.read(sock) response = self.read(sock)
except OSError as err:
logging.error("Socket error for %s: %s",
nio_func.__name__, err.strerror)
self._close_socket(sock)
time.sleep(2)
except RemoteTransportError as err:
logging.error("HTTP transport error for %s: %s",
nio_func.__name__, err)
self._close_socket(sock)
self.http_disconnect()
time.sleep(2)
except NioErrorResponse as err: except NioErrorResponse as err:
logging.error("bad read for %s: %s", nio_func, err) logging.error("Nio response error for %s: %s",
nio_func.__name__, err)
self._close_socket(sock) self._close_socket(sock)
if self._should_abort_talk(err): if err.response.status_code in self.http_retry_codes:
logging.error("aborting talk") return response
break
time.sleep(10) time.sleep(2)
else: else:
break return response
self._close_socket(sock)
return response
def _should_abort_talk(self, err: NioErrorResponse) -> bool:
if err.response.status_code in self.http_retry_codes:
return False
return True

View File

@ -62,7 +62,6 @@ Rectangle {
} }
if (textArea.text === "") { return } if (textArea.text === "") { return }
Backend.clientManager.clients[chatPage.user_id] Backend.clientManager.clients[chatPage.user_id]
.sendMarkdown(chatPage.room.room_id, textArea.text) .sendMarkdown(chatPage.room.room_id, textArea.text)
textArea.clear() textArea.clear()