replit-river 0.1.16.dev2__tar.gz → 0.1.16.dev4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev4}/PKG-INFO +1 -1
  2. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev4}/pyproject.toml +1 -1
  3. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev4}/replit_river/client_transport.py +68 -94
  4. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev4}/replit_river/session.py +31 -20
  5. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev4}/replit_river/transport.py +23 -13
  6. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev4}/replit_river/transport_options.py +1 -1
  7. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev4}/LICENSE +0 -0
  8. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev4}/README.md +0 -0
  9. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev4}/replit_river/__init__.py +0 -0
  10. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev4}/replit_river/client.py +0 -0
  11. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev4}/replit_river/client_session.py +0 -0
  12. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev4}/replit_river/codegen/__init__.py +0 -0
  13. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev4}/replit_river/codegen/__main__.py +0 -0
  14. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev4}/replit_river/codegen/client.py +0 -0
  15. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev4}/replit_river/codegen/run.py +0 -0
  16. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev4}/replit_river/codegen/schema.py +0 -0
  17. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev4}/replit_river/codegen/server.py +0 -0
  18. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev4}/replit_river/error_schema.py +0 -0
  19. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev4}/replit_river/message_buffer.py +0 -0
  20. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev4}/replit_river/messages.py +0 -0
  21. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev4}/replit_river/py.typed +0 -0
  22. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev4}/replit_river/rate_limiter.py +0 -0
  23. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev4}/replit_river/rpc.py +0 -0
  24. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev4}/replit_river/seq_manager.py +0 -0
  25. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev4}/replit_river/server.py +0 -0
  26. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev4}/replit_river/server_transport.py +0 -0
  27. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev4}/replit_river/task_manager.py +0 -0
  28. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev4}/replit_river/websocket_wrapper.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: replit-river
3
- Version: 0.1.16.dev2
3
+ Version: 0.1.16.dev4
4
4
  Summary: Replit river toolkit for Python
5
5
  License: LICENSE
6
6
  Keywords: rpc,websockets
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name="replit-river"
7
- version="0.1.16.dev2"
7
+ version="0.1.16.dev4"
8
8
  description="Replit river toolkit for Python"
9
9
  authors = ["Replit <eng@replit.com>"]
10
10
  license = "LICENSE"
@@ -33,7 +33,6 @@ from replit_river.seq_manager import (
33
33
  IgnoreMessageException,
34
34
  InvalidMessageException,
35
35
  )
36
- from replit_river.session import Session
37
36
  from replit_river.transport import Transport
38
37
  from replit_river.transport_options import TransportOptions
39
38
 
@@ -59,12 +58,6 @@ class ClientTransport(Transport):
59
58
  )
60
59
  # We want to make sure there's only one session creation at a time
61
60
  self._create_session_lock = asyncio.Lock()
62
- # Only one retry should happen at a time
63
- self._retry_ws_lock = asyncio.Lock()
64
-
65
- async def _on_session_closed(self, session: Session) -> None:
66
- logging.info(f"Client session {session.advertised_session_id} closed")
67
- await self._delete_session(session)
68
61
 
69
62
  async def close(self) -> None:
70
63
  self._rate_limiter.close()
@@ -89,6 +82,7 @@ class ClientTransport(Transport):
89
82
 
90
83
  async def _establish_new_connection(
91
84
  self,
85
+ old_session: Optional[ClientSession] = None,
92
86
  ) -> Tuple[
93
87
  WebSocketCommonProtocol,
94
88
  ControlMessageHandshakeRequest,
@@ -101,18 +95,25 @@ class ClientTransport(Transport):
101
95
  for i in range(max_retry):
102
96
  if i > 0:
103
97
  logging.info(f"Retrying build handshake number {i} times")
98
+ logging.info(
99
+ f"old_session: {old_session}, old_session.is_session_open(): {await old_session.is_session_open() if old_session else None}"
100
+ )
104
101
  if not rate_limit.has_budget(client_id):
105
102
  logging.debug("No retry budget for %s.", client_id)
106
103
  break
107
104
  try:
105
+ logging.error(
106
+ f"##### _establish_new_connection: old session : {old_session}"
107
+ )
108
108
  ws = await websockets.connect(self._websocket_uri)
109
- existing_session = await self._get_existing_session()
110
109
  session_id = (
111
110
  self.generate_session_id()
112
- if not existing_session
113
- else existing_session.session_id
111
+ if not old_session
112
+ else old_session.session_id
113
+ )
114
+ logging.error(
115
+ f"##### _establish_new_connection: existing session : {old_session}"
114
116
  )
115
- logging.error(f"##### existing session : {existing_session}")
116
117
  rate_limit.consume_budget(client_id)
117
118
  handshake_request, handshake_response = await self._establish_handshake(
118
119
  self._transport_id, self._server_id, session_id, ws
@@ -127,97 +128,70 @@ class ClientTransport(Transport):
127
128
  await asyncio.sleep(backoff_time / 1000)
128
129
  raise RiverException(
129
130
  ERROR_HANDSHAKE,
130
- "Failed to create session after retrying max number of times",
131
+ "Failed to create ws after retrying max number of times",
131
132
  )
132
133
 
133
- async def _retry_session_connection(
134
- self, session_to_replace_ws: Session
135
- ) -> Session:
136
- async with self._retry_ws_lock:
137
- if await session_to_replace_ws.is_websocket_open():
138
- # other retry successfully replaced the websocket,
139
- return session_to_replace_ws
140
- if not await session_to_replace_ws.is_session_open():
141
- # If the session is already closing we don't retry connection
142
- return session_to_replace_ws
143
- new_ws, hs_request, hs_response = await self._establish_new_connection()
144
- # If the server session id different, we create a new session.
145
- if (
146
- hs_response.status.sessionId
147
- != session_to_replace_ws.advertised_session_id
148
- ):
149
- server_session_id = hs_response.status.sessionId
150
- if not server_session_id:
151
- raise RiverException(
152
- ERROR_SESSION,
153
- "Server did not return a sessionId in successful handshake",
154
- )
155
- new_session = ClientSession(
156
- transport_id=self._transport_id,
157
- to_id=self._server_id,
158
- session_id=hs_request.sessionId,
159
- advertised_session_id=server_session_id,
160
- websocket=new_ws,
161
- transport_options=self._transport_options,
162
- is_server=False,
163
- handlers={},
164
- close_session_callback=self._on_session_closed,
165
- retry_connection_callback=lambda x: self._retry_session_connection(
166
- x
167
- ),
168
- )
169
- return new_session
170
- else:
171
- # If the session is still active and aligns with the server session
172
- # we replace the websocket in it.
173
- await session_to_replace_ws.replace_with_new_websocket(new_ws)
174
- return session_to_replace_ws
134
+ async def _create_new_session(
135
+ self,
136
+ ) -> ClientSession:
137
+ new_ws, hs_request, hs_response = await self._establish_new_connection()
138
+ advertised_session_id = hs_response.status.sessionId
139
+ if not advertised_session_id:
140
+ raise RiverException(
141
+ ERROR_SESSION,
142
+ "Server did not return a sessionId in successful handshake",
143
+ )
144
+ new_session = ClientSession(
145
+ transport_id=self._transport_id,
146
+ to_id=self._server_id,
147
+ session_id=hs_request.sessionId,
148
+ advertised_session_id=advertised_session_id,
149
+ websocket=new_ws,
150
+ transport_options=self._transport_options,
151
+ is_server=False,
152
+ handlers={},
153
+ close_session_callback=self._delete_session,
154
+ retry_connection_callback=lambda x: self._get_or_create_session(),
155
+ )
175
156
 
176
- async def _get_or_create_session(self) -> ClientSession:
177
- logging.error(f"####### _get_or_create_session")
157
+ await self._set_session(new_session, acquire_lock=False)
158
+ await new_session.start_serve_responses()
159
+ return new_session
178
160
 
161
+ async def _get_or_create_session(self) -> ClientSession:
162
+ logging.error(f"####### start get or create session")
179
163
  async with self._create_session_lock:
180
164
  existing_session = await self._get_existing_session()
181
- if existing_session:
182
- if await existing_session.is_websocket_open():
183
- logging.error(f"####### use existing session")
184
- return existing_session
185
- else:
186
- logging.error(f"####### retry session connection")
187
- session = await self._retry_session_connection(existing_session)
188
- # This should never happen, adding here to make mypy happy
189
- if not isinstance(session, ClientSession):
190
- raise RiverException(
191
- ERROR_SESSION,
192
- f"Session type is not ClientSession, got {type(session)}",
193
- )
194
- return session
165
+ if not existing_session:
166
+ logging.error(f"##### _get_or_create_session No existing session")
167
+ return await self._create_new_session()
168
+ is_session_open = await existing_session.is_session_open()
169
+ if not is_session_open:
170
+ logging.error(
171
+ f"##### _get_or_create_session session open, creating new session"
172
+ )
173
+ return await self._create_new_session()
174
+ is_ws_open = await existing_session.is_websocket_open()
175
+ if is_ws_open:
176
+ logging.error(f"##### _get_or_create_session Reuse existing session")
177
+ return existing_session
195
178
  else:
196
- logging.error(f"####### establish new connection")
197
- new_ws, hs_request, hs_response = await self._establish_new_connection()
198
- advertised_session_id = hs_response.status.sessionId
199
- if not advertised_session_id:
200
- raise RiverException(
201
- ERROR_SESSION,
202
- "Server did not return a sessionId in successful handshake",
203
- )
204
- new_session = ClientSession(
205
- transport_id=self._transport_id,
206
- to_id=self._server_id,
207
- session_id=hs_request.sessionId,
208
- advertised_session_id=advertised_session_id,
209
- websocket=new_ws,
210
- transport_options=self._transport_options,
211
- is_server=False,
212
- handlers={},
213
- close_session_callback=self._on_session_closed,
214
- retry_connection_callback=lambda x: self._retry_session_connection(
215
- x
216
- ),
179
+ new_ws, _, hs_response = await self._establish_new_connection(
180
+ existing_session
217
181
  )
218
- await self._set_session(new_session)
219
- await new_session.start_serve_responses()
220
- return new_session
182
+ if (
183
+ hs_response.status.sessionId
184
+ == existing_session.advertised_session_id
185
+ ):
186
+ logging.error(
187
+ f"##### _get_or_create_session session open, replacing websocket"
188
+ )
189
+ await existing_session.replace_with_new_websocket(new_ws)
190
+ return existing_session
191
+ else:
192
+ logging.error(f"##### session open, not same session id, reuse")
193
+ await existing_session.close(is_unexpected_close=False)
194
+ return await self._create_new_session()
221
195
 
222
196
  async def _send_handshake_request(
223
197
  self,
@@ -52,7 +52,7 @@ class Session(object):
52
52
  transport_options: TransportOptions,
53
53
  is_server: bool,
54
54
  handlers: Dict[Tuple[str, str], Tuple[str, GenericRpcHandler]],
55
- close_session_callback: Callable[["Session"], Coroutine[Any, Any, None]],
55
+ close_session_callback: Callable[["Session", bool], Coroutine[Any, Any, None]],
56
56
  retry_connection_callback: Optional[
57
57
  Callable[
58
58
  ["Session"],
@@ -104,18 +104,10 @@ class Session(object):
104
104
  async with self._ws_lock:
105
105
  return await self._ws_wrapper.is_open()
106
106
 
107
- async def _on_websocket_unexpected_close(self) -> None:
108
- """Handle unexpected websocket close."""
109
- logging.debug(
110
- "Unexpected websocket close from %s to %s", self._transport_id, self._to_id
111
- )
112
- await self._begin_close_session_countdown()
113
-
114
107
  async def _begin_close_session_countdown(self) -> None:
115
108
  """Begin the countdown to close session, this should be called when
116
109
  websocket is closed.
117
110
  """
118
- logging.debug("begin_close_session_countdown")
119
111
  if self._close_session_after_time_secs is not None:
120
112
  # already in grace period, no need to set again
121
113
  return
@@ -128,6 +120,7 @@ class Session(object):
128
120
  self._close_session_after_time_secs = (
129
121
  await self._get_current_time() + grace_period_ms / 1000
130
122
  )
123
+ await self.close_websocket(self._ws_wrapper, should_retry=not self._is_server)
131
124
 
132
125
  async def serve(self) -> None:
133
126
  """Serve messages from the websocket."""
@@ -137,7 +130,7 @@ class Session(object):
137
130
  try:
138
131
  await self._handle_messages_from_ws(tg)
139
132
  except ConnectionClosed as e:
140
- await self._on_websocket_unexpected_close()
133
+ await self._begin_close_session_countdown()
141
134
  logging.debug("ConnectionClosed while serving: %r", e)
142
135
  except FailedSendingMessageException as e:
143
136
  # Expected error if the connection is closed.
@@ -170,8 +163,15 @@ class Session(object):
170
163
  self._ws_wrapper.id,
171
164
  )
172
165
  try:
173
- async for message in self._ws_wrapper.ws:
166
+ ws_wrapper = self._ws_wrapper
167
+ async for message in ws_wrapper.ws:
174
168
  try:
169
+ logging.error(
170
+ f" await ws_wrapper.is_open(): : { await ws_wrapper.is_open()}"
171
+ )
172
+ if not await ws_wrapper.is_open():
173
+ # We should not process messages if the websocket is closed.
174
+ break
175
175
  msg = parse_transport_msg(message, self._transport_options)
176
176
 
177
177
  logging.debug(f"{self._transport_id} got a message %r", msg)
@@ -241,9 +241,13 @@ class Session(object):
241
241
  await asyncio.sleep(
242
242
  self._transport_options.close_session_check_interval_ms / 1000
243
243
  )
244
+ logging.error("#### _check_to_close_session")
244
245
  if not self._close_session_after_time_secs:
245
246
  continue
246
247
  current_time = await self._get_current_time()
248
+ logging.error(
249
+ f"#### _check_to_close_session : current_time: {current_time} self._close_session_after_time_secs: {self._close_session_after_time_secs}, {current_time > self._close_session_after_time_secs}"
250
+ )
247
251
  if current_time > self._close_session_after_time_secs:
248
252
  logging.debug(
249
253
  "Grace period ended for %s, closing session", self._transport_id
@@ -288,14 +292,14 @@ class Session(object):
288
292
  self._heartbeat_misses
289
293
  >= self._transport_options.heartbeats_until_dead
290
294
  ):
295
+ if self._close_session_after_time_secs is not None:
296
+ # already in grace period, no need to set again
297
+ continue
291
298
  logging.debug(
292
299
  "%r closing websocket because of heartbeat misses",
293
300
  self.session_id,
294
301
  )
295
- await self._on_websocket_unexpected_close()
296
- await self.close_websocket(
297
- self._ws_wrapper, should_retry=not self._is_server
298
- )
302
+ await self._begin_close_session_countdown()
299
303
  continue
300
304
  except FailedSendingMessageException:
301
305
  # this is expected during websocket closed period
@@ -327,7 +331,7 @@ class Session(object):
327
331
  ) -> None:
328
332
  try:
329
333
  await send_transport_message(
330
- msg, websocket, self._on_websocket_unexpected_close, prefix_bytes
334
+ msg, websocket, self._begin_close_session_countdown, prefix_bytes
331
335
  )
332
336
  except WebsocketClosedException as e:
333
337
  raise e
@@ -416,9 +420,13 @@ class Session(object):
416
420
  ) -> None:
417
421
  """Mark the websocket as closed, close the websocket, and retry if needed."""
418
422
  async with self._ws_lock:
423
+ # Already closed.
424
+ if not await ws_wrapper.is_open():
425
+ return
419
426
  await ws_wrapper.close()
420
427
  if should_retry and self._retry_connection_callback:
421
- await self._retry_connection_callback(self)
428
+ logging.error("### running retry_connection_callback")
429
+ await self._task_manager.create_task(self._retry_connection_callback(self))
422
430
 
423
431
  async def _open_stream_and_call_handler(
424
432
  self,
@@ -495,7 +503,9 @@ class Session(object):
495
503
  async def start_serve_responses(self) -> None:
496
504
  await self._task_manager.create_task(self.serve())
497
505
 
498
- async def close(self, is_unexpected_close: bool) -> None:
506
+ async def close(
507
+ self, is_unexpected_close: bool, acquire_transport_lock: bool = True
508
+ ) -> None:
499
509
  """Close the session and all associated streams."""
500
510
  logging.info(
501
511
  f"{self._transport_id} closing session "
@@ -508,9 +518,10 @@ class Session(object):
508
518
  return
509
519
  self._state = SessionState.CLOSING
510
520
  self._reset_session_close_countdown()
511
- await self.close_websocket(self._ws_wrapper, should_retry=False)
521
+ async with self._ws_lock:
522
+ await self._ws_wrapper.close()
512
523
  # Clear the session in transports
513
- await self._close_session_callback(self)
524
+ await self._close_session_callback(self, acquire_transport_lock)
514
525
  await self._task_manager.cancel_all_tasks()
515
526
  # TODO: unexpected_close should close stream differently here to
516
527
  # throw exception correctly.
@@ -45,13 +45,22 @@ class Transport:
45
45
  await asyncio.gather(*tasks)
46
46
  logging.info(f"Transport closed {self._transport_id}")
47
47
 
48
- async def _delete_session(self, session: Session) -> None:
49
- async with self._session_lock:
48
+ async def _delete_session(
49
+ self, session: Session, acquire_lock: bool = True
50
+ ) -> None:
51
+ if acquire_lock:
52
+ async with self._session_lock:
53
+ if session._to_id in self._sessions:
54
+ del self._sessions[session._to_id]
55
+ else:
50
56
  if session._to_id in self._sessions:
51
57
  del self._sessions[session._to_id]
52
58
 
53
- async def _set_session(self, session: Session) -> None:
54
- async with self._session_lock:
59
+ async def _set_session(self, session: Session, acquire_lock: bool = True) -> None:
60
+ if acquire_lock:
61
+ async with self._session_lock:
62
+ self._sessions[session._to_id] = session
63
+ else:
55
64
  self._sessions[session._to_id] = session
56
65
 
57
66
  def generate_nanoid(self) -> str:
@@ -87,6 +96,7 @@ class Transport:
87
96
  ) -> Session:
88
97
  session_to_close: Optional[Session] = None
89
98
  new_session: Optional[Session] = None
99
+ logging.error(f"## get_or_create_session, {to_id}")
90
100
  async with self._session_lock:
91
101
  if to_id not in self._sessions:
92
102
  logging.debug(
@@ -138,13 +148,13 @@ class Transport:
138
148
  new_session = old_session
139
149
  except FailedSendingMessageException as e:
140
150
  raise e
141
- if session_to_close:
142
- logging.debug(
143
- "Closing stale session %s", session_to_close.advertised_session_id
144
- )
145
- await session_to_close.close(False)
146
- logging.info(
147
- f"Closed stale session {session_to_close.advertised_session_id}"
148
- )
149
- await self._set_session(new_session)
151
+
152
+ if session_to_close:
153
+ logging.debug(
154
+ "Closing stale session %s", session_to_close.advertised_session_id
155
+ )
156
+ await session_to_close.close(
157
+ is_unexpected_close=False, acquire_transport_lock=False
158
+ )
159
+ await self._set_session(new_session, acquire_lock=False)
150
160
  return new_session
@@ -13,7 +13,7 @@ class ConnectionRetryOptions(BaseModel):
13
13
  max_backoff_ms: float = 32_000
14
14
  attempt_budget_capacity: float = 5
15
15
  budget_restore_interval_ms: float = 200
16
- max_retry: int = 10
16
+ max_retry: int = 100
17
17
 
18
18
 
19
19
  # setup in replit web can be found at