-
Notifications
You must be signed in to change notification settings - Fork 3
Recover state after RpcConnection reconnection #47
Comments
@alexlapa , cc |
Немного отредактировал текст в шапке дабы лучше обьяснить ситуацию. В целом, с предложенной реализацией согласен. По мелочам:
Будут замечания/предложения по предлоежнному решению? |
Согласен, но есть одно но. Я еще раз посмотрел на все это уже в коде, и есть ощущение, что
Добавит гибкости, и выглядит чуть более логично. Но из неприятных сторон, на практике из-за
Да, выглядит неплохо.
На мой взгляд |
Сценарий 1
Восстановление соединения пользователя Bob в случае буфферизации
Восстановление соединения пользователя Bob в случае snapshoting'а
alice:
peers:
- id: 1
state: WaitRemoteSdp
sdp_offer: "..."
sdp_answer: null
receivers:
- ...
senders:
- ...
bob:
peers:
- id: 2
state: WaitLocalHaveRemote
sdp_offer: "..."
sdp_answer: null
receivers:
- ...
senders:
- ...
struct Room {
peers: PeerRepository,
/* ... */
}
impl Room {
fn on_handshake(&mut self, snapshot: Snaphost) {
let me = snapshot.members.get("bob");
for peer in me.peers {
if let Some(my_peer) = self.peers.get(&peer.id) {
if peer.state > my_peer.state {
/* ... */
} else if peer.state < my_peer.state {
/* ... */
}
} else {
self.peers.create(peer);
}
}
}
fn create_peer(&mut self, new_peer: PeerStateMachine) {
match new_peer {
PeerStateMachine::WaitLocalHaveRemote(peer: Peer<WaitLocalHaveRemote>) => {
let peer_connection = peer.create_connection();
self.peers.push(peer_connection);
self.rpc.send_command(Command::MakeSdpAnswer {
peer_id: peer_connection.id,
sdp_answer: peer_connection.sdp_answer(),
});
},
/* ... */
}
}
}
impl Peer<WaitLocalHaveRemote> {
pub fn create_connection(self, room: &Room) -> PeerConnection<WaitLocalHaveRemote> {
let connection = PeerConnection<WaitLocalHaveRemote>::new();
room.create_connection_from_tracks(self.context.tracks);
connection.proccess_offer(self.sdp_offer);
connection.create_and_set_answer();
connection
}
} Сценарий 2
Восстановление соединения пользователя Bob в случае буфферизации
Восстановление соединения пользователя Bob в случае snapshoting'а
impl Room {
fn on_handshake(&mut self, snapshot: Snapshot) {
/* ... */
in (id, peer) in self.peers {
if let None = snapshot.peers.get(peer) {
self.peers.remove(id);
}
}
}
} Сценарий 3
Восстановление соединения пользователя Alice в случае буфферизации
Восстановление соединения пользователя Alice в случае snapshoting'а
alice:
peers:
- id: 1
state: New
sdp_offer: null
sdp_answer: null
struct Room {
peers: PeerRepository,
/* ... */
}
impl Room {
fn on_handshake(&mut self, snapshot: Snaphost) {
let me = snapshot.members.get("bob");
for peer in me.peers {
if let Some(my_peer) = self.peers.get(&peer.id) {
if peer.state > my_peer.state {
my_peer.upgrade(peer); // Here it would be something similar to the logic of the server upgrade.
} else if peer.state < my_peer.state {
my_peer.upgrade_server_peer(peer);
}
} else {
self.peers.create(peer);
}
}
}
}
impl PeerConnection<WaitLocalHaveRemote> {
pub fn upgrade_server_peer(&self, server_peer: PeerStateMachine, room: &Room) {
match server_peer {
PeerStateMachine::New(server_peer: Peer<New>) => {
room.rpc.send_command(Command::MakeSdpAnswer {
/* ... */
})
}
}
}
} |
- impl RpcClient reconnection - add ServerMsg::RpcSettings and its sending/processing - add Room.on_connection_loss callback which fires when RpcClient loses connection - add ReconnectHandle for JS side to start reconnection - impl reconnection with backoff and simple reconnection with a constant retry delay - add connection loss notification with button for manual reconnection to 'e2e-demo' app - add WebSocket connection state indicator to 'demo' app - remove RpcTransport::on_close and use RpcTransport::on_state_change instead Additionally: - rewrite 'satisfies_by_device_id!' and 'console_error!' macros as functions - remove 'weak_map!' macro and add 'upgrade_or_detached!' instead - add 'new_js_error!' macro for creating JasonError with 'tracerr' information for JS side - reverse ping/pong mechanism: server sends Ping and expects Pong from client - add 'rpc.ping_interval' configuration option for Medea - use 'fakerator' instead of 'faker' for generating random male usernames in demos
…, #47) - add Component and State for Sender, Receiver, Peer and PeerRepository - rewrite EventHandler implementation to update Component's States instead of real objects Additionally: - upgrade Firefox to 84.0.2 version for E2E tests - fix 'opt-level' being ignored when compiling 'medea-jason' crate in Cargo manifest - globally set 'codegen-units = 1' in Cargo manifest
- add states for client and sever synchronization to 'medea-client-api-proto' crate - add Event::StateSynchronized and Command::SynchronizeMe to 'medea-client-api-proto` crate - implement state synchronization on reconnection in 'medea-jason' and 'medea' crates Additionally: - upgrade Firefox to 85.0 version for E2E tests
Resolved in #167 |
Part of #27
Background
При попытке отправить событие пользователю в закрытый
RpcConnection
,сообщения не будут отправлены или сохранены. В текущей реализации ошибки отправки обрабатываются закрытием комнаты.
Problem to solve
Проблема возникнет при следующем сценарии:
PeerCreated
пользователю A.RpcConnectionClosed(ClosedReason::Lost)
)MakeSdpOffer
.SdpOfferMade
происходит ошибка, приводящая к закрытию комнаты.В текущей реализации,
ClosedReason::Lost
будет отправлен в случае отсутствия ping'ов от клиента в теченииRpc.idle_timeout
.В случае поетри соединения будет отправлен
ClosedReason::Closed
независимо от статуса.Possible solutions
Изменения в отправке сообщений клиентам находящимся в состоянии Lost (могут вернутся):
Предлагается сделать буфферизацию сообщений в случае отсутствия подключения
с клиентом.
В случае перепоключения клиента будет совершена попытка зафлашить буфер в новое подкючение.
В случае
ClosedReason::Closed
сообщения будут дискардится.Предлагаемая реализация
Для начала, предлагается создать обертку над
Box<dyn RpcConnection>
с возможностьюбуфферизации
EventMessage
. Выглядеть это будет примерно так:В
BufferedRpcConnection::send_event
мы считаем, чтоRpcConnection
оборван, еслиRpcConnection::send_event
возвращает ошибку (на данный момент это()
, но при надобности можно будет добавить enum). На данный момент дляAddr<WsSession>::send_event
это будетMailboxError::Closed
.В документации это не описано, но опытным путем определено, что это так.
BufferedRpcConnection::swap_connection_and_flush
будет вызываться вParticipantService::connection_established
, если будет найденBufferedRpcConnection
для подключившегося пользователя.Ну и соответственно
ParticipantService
теперь будет вместоBox<dyn RpcConnection
хранитьBufferedRpcConnection
в полеconnections
.Переподключение пользователя по истечению
reconnect_timeout
(ClosedReason::Closed
)В этом случае сервер будет воспринимать пользователя как нового и будет пытатся соединить его с другими пользователями заново.
Тут основной проблемой является то, что сервер ничего не знает про его текущее состояние, клиент еще может держать обьекты, ассоциированные с более не существующими обьектами сервера, удаленными по причине отключения пользователя.
Варианты решения:
В дальнейшем будут обсуждатся способы реализации второго варианта.
Первый вариант
Можно добавить поле
closed_members
вParticipantService
в которое мыбудем добавлять пользователей, которые были отключены по таймауту. Когда
такой пользователь будет повторно подключаться, мы будем отправлять ему
EventMessage::ResetState
. По-идее, на такое сообщение клиент должен будетсообщать об этом приложению и пересоздавать
Room
. Пытаться восстанавливатьв таком случае состояние - бесполезно, поскольку остальные пользователи
уже будут считать переподключившегося пользователя "отвалившемся" и удалят
его у себя.
Второй вариант
Также можно сообщать свежеподключившемуся пользователю о том, что его подключение новое. Таким образом на такие сообщения, если клиент считает, что он переподключался, он должен будет уничтожить обьекты ассоциированые с этим подключением и ожидать дальнейших комманд.
Такой вариант выглядит предпочтительнее из-за отсутствия необходимости
в сохранении "отвалившихся" клиентов.
Переподключение пользователя до наступления таймаута на переподключение (
ClosedReason::Lost
)Тут все сложнее, поскольку на стороне клиента, в момент потерянного
RpcConnection
тоже могут происходить
Command
ы, которые он будет пытаться отправить. Соответсвенно их он должен будет буфферизировать таким же образом, как и сервер, но тут уже вылазят проблемы синхронизации сервера и клиента. Можно проставлять UNIX timestamp в буффере для каждогоEvent
иCommand
. Но останутся проблемы с конфликтами.Для примера можно взять ситуацию с получением сервером
Command::RemovePeers
, но при этом на сервере эти пиры уже удалены. В таком случае сервер должен будет проигнорировать такой невалидныйEvent
. Поскольку клиенту в тот же момент должен будет придти буфферизированныйEvent::PeersRemoved
с этими удаленными пирами. Для большей надежности можно добавить вCommand
иEvent
флаг, сообщающий о том, что данное сообщение было буфферизированно, и может иметь конфликты, которые нужно игнорировать.На данный момент, у нас нет как таковых конфликтующий
Command
/Event
, которые могли бы произойти без соединения с сервером. Но с первого взгляда, предложенный мною подход должен быть рабочим во всех случаях.Все что описано в данном разделе - это лишь размышления о решении будущих проблем с синхронизацией клиента и сервера. На данный момент у нас мало ситуаций в которых могут происходить конфликты, и поэтому в будущем описанный вариант решения проблемы может оказаться не рабочим, но может послужить отправной точкой.
Изменения в логике отправки
RpcConnectionClosed
:ClosedReason::Closed
если ws-коннект был закрыт со статусом 1000.ClosedReason::Lost
по истечению idle_timeout либо при закрытии ws-коннекта с любым статусом отличным от 1000 либо при отсутствии статуса.The text was updated successfully, but these errors were encountered: