replit-river 0.1.16.dev2__tar.gz → 0.1.16.dev3__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.dev3}/PKG-INFO +1 -1
  2. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev3}/pyproject.toml +1 -1
  3. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev3}/replit_river/client_transport.py +70 -106
  4. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev3}/replit_river/session.py +14 -14
  5. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev3}/replit_river/transport.py +25 -14
  6. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev3}/LICENSE +0 -0
  7. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev3}/README.md +0 -0
  8. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev3}/replit_river/__init__.py +0 -0
  9. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev3}/replit_river/client.py +0 -0
  10. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev3}/replit_river/client_session.py +0 -0
  11. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev3}/replit_river/codegen/__init__.py +0 -0
  12. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev3}/replit_river/codegen/__main__.py +0 -0
  13. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev3}/replit_river/codegen/client.py +0 -0
  14. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev3}/replit_river/codegen/run.py +0 -0
  15. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev3}/replit_river/codegen/schema.py +0 -0
  16. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev3}/replit_river/codegen/server.py +0 -0
  17. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev3}/replit_river/error_schema.py +0 -0
  18. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev3}/replit_river/message_buffer.py +0 -0
  19. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev3}/replit_river/messages.py +0 -0
  20. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev3}/replit_river/py.typed +0 -0
  21. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev3}/replit_river/rate_limiter.py +0 -0
  22. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev3}/replit_river/rpc.py +0 -0
  23. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev3}/replit_river/seq_manager.py +0 -0
  24. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev3}/replit_river/server.py +0 -0
  25. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev3}/replit_river/server_transport.py +0 -0
  26. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev3}/replit_river/task_manager.py +0 -0
  27. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev3}/replit_river/transport_options.py +0 -0
  28. {replit_river-0.1.16.dev2 → replit_river-0.1.16.dev3}/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.dev3
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.dev3"
8
8
  description="Replit river toolkit for Python"
9
9
  authors = ["Replit <eng@replit.com>"]
10
10
  license = "LICENSE"
@@ -57,35 +57,26 @@ class ClientTransport(Transport):
57
57
  self._rate_limiter = LeakyBucketRateLimit(
58
58
  transport_options.connection_retry_options
59
59
  )
60
- # We want to make sure there's only one session creation at a time
61
- 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
60
 
69
61
  async def close(self) -> None:
70
62
  self._rate_limiter.close()
71
63
  await self._close_all_sessions()
72
64
 
73
65
  async def _get_existing_session(self) -> Optional[ClientSession]:
74
- async with self._session_lock:
75
- if not self._sessions:
76
- return None
77
- if len(self._sessions) > 1:
78
- raise RiverException(
79
- "session_error",
80
- "More than one session found in client, should only be one",
81
- )
82
- session = list(self._sessions.values())[0]
83
- if isinstance(session, ClientSession):
84
- return session
85
- else:
86
- raise RiverException(
87
- "session_error", f"Client session type wrong, got {type(session)}"
88
- )
66
+ if not self._sessions:
67
+ return None
68
+ if len(self._sessions) > 1:
69
+ raise RiverException(
70
+ "session_error",
71
+ "More than one session found in client, should only be one",
72
+ )
73
+ session = list(self._sessions.values())[0]
74
+ if isinstance(session, ClientSession):
75
+ return session
76
+ else:
77
+ raise RiverException(
78
+ "session_error", f"Client session type wrong, got {type(session)}"
79
+ )
89
80
 
90
81
  async def _establish_new_connection(
91
82
  self,
@@ -112,7 +103,9 @@ class ClientTransport(Transport):
112
103
  if not existing_session
113
104
  else existing_session.session_id
114
105
  )
115
- logging.error(f"##### existing session : {existing_session}")
106
+ logging.error(
107
+ f"##### _establish_new_connection: existing session : {existing_session}"
108
+ )
116
109
  rate_limit.consume_budget(client_id)
117
110
  handshake_request, handshake_response = await self._establish_handshake(
118
111
  self._transport_id, self._server_id, session_id, ws
@@ -130,94 +123,65 @@ class ClientTransport(Transport):
130
123
  "Failed to create session after retrying max number of times",
131
124
  )
132
125
 
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
126
+ async def _create_new_session(
127
+ self,
128
+ ) -> ClientSession:
129
+ new_ws, hs_request, hs_response = await self._establish_new_connection()
130
+ advertised_session_id = hs_response.status.sessionId
131
+ if not advertised_session_id:
132
+ raise RiverException(
133
+ ERROR_SESSION,
134
+ "Server did not return a sessionId in successful handshake",
135
+ )
136
+ new_session = ClientSession(
137
+ transport_id=self._transport_id,
138
+ to_id=self._server_id,
139
+ session_id=hs_request.sessionId,
140
+ advertised_session_id=advertised_session_id,
141
+ websocket=new_ws,
142
+ transport_options=self._transport_options,
143
+ is_server=False,
144
+ handlers={},
145
+ close_session_callback=self._delete_session,
146
+ retry_connection_callback=lambda x: self._get_or_create_session(),
147
+ )
175
148
 
176
- async def _get_or_create_session(self) -> ClientSession:
177
- logging.error(f"####### _get_or_create_session")
149
+ await self._set_session(new_session, acquire_lock=False)
150
+ await new_session.start_serve_responses()
151
+ return new_session
178
152
 
179
- async with self._create_session_lock:
153
+ async def _get_or_create_session(self) -> ClientSession:
154
+ async with self._session_lock:
180
155
  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")
156
+ if not existing_session:
157
+ logging.error(f"##### No existing session")
158
+ return await self._create_new_session()
159
+ is_session_open = await existing_session.is_session_open()
160
+ if not is_session_open:
161
+ logging.error(f"##### session open, creating new session")
162
+ await existing_session.close(
163
+ is_unexpected_close=False, acquire_transport_lock=False
164
+ )
165
+ return await self._create_new_session()
166
+ is_ws_open = existing_session.is_websocket_open()
167
+ if is_ws_open:
168
+ logging.error(f"##### Reuse existing session")
169
+ return existing_session
170
+ else:
171
+ new_ws, _, hs_response = await self._establish_new_connection()
172
+ if (
173
+ hs_response.status.sessionId
174
+ == existing_session.advertised_session_id
175
+ ):
176
+ logging.error(f"##### session open, replacing websocket")
177
+ await existing_session.replace_with_new_websocket(new_ws)
184
178
  return existing_session
185
179
  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
195
- 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",
180
+ logging.error(f"##### session open, not same session id, reuse")
181
+ await existing_session.close(
182
+ is_unexpected_close=False, acquire_transport_lock=False
203
183
  )
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
- ),
217
- )
218
- await self._set_session(new_session)
219
- await new_session.start_serve_responses()
220
- return new_session
184
+ return await self._create_new_session()
221
185
 
222
186
  async def _send_handshake_request(
223
187
  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,13 +104,6 @@ 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.
@@ -119,6 +112,7 @@ class Session(object):
119
112
  if self._close_session_after_time_secs is not None:
120
113
  # already in grace period, no need to set again
121
114
  return
115
+ await self._ws_wrapper.close()
122
116
  logging.debug(
123
117
  "websocket closed from %s to %s begin grace period",
124
118
  self._transport_id,
@@ -137,7 +131,7 @@ class Session(object):
137
131
  try:
138
132
  await self._handle_messages_from_ws(tg)
139
133
  except ConnectionClosed as e:
140
- await self._on_websocket_unexpected_close()
134
+ await self._begin_close_session_countdown()
141
135
  logging.debug("ConnectionClosed while serving: %r", e)
142
136
  except FailedSendingMessageException as e:
143
137
  # Expected error if the connection is closed.
@@ -292,7 +286,7 @@ class Session(object):
292
286
  "%r closing websocket because of heartbeat misses",
293
287
  self.session_id,
294
288
  )
295
- await self._on_websocket_unexpected_close()
289
+ await self._begin_close_session_countdown()
296
290
  await self.close_websocket(
297
291
  self._ws_wrapper, should_retry=not self._is_server
298
292
  )
@@ -327,7 +321,7 @@ class Session(object):
327
321
  ) -> None:
328
322
  try:
329
323
  await send_transport_message(
330
- msg, websocket, self._on_websocket_unexpected_close, prefix_bytes
324
+ msg, websocket, self._begin_close_session_countdown, prefix_bytes
331
325
  )
332
326
  except WebsocketClosedException as e:
333
327
  raise e
@@ -416,6 +410,9 @@ class Session(object):
416
410
  ) -> None:
417
411
  """Mark the websocket as closed, close the websocket, and retry if needed."""
418
412
  async with self._ws_lock:
413
+ # Already closed.
414
+ if not await ws_wrapper.is_open():
415
+ return
419
416
  await ws_wrapper.close()
420
417
  if should_retry and self._retry_connection_callback:
421
418
  await self._retry_connection_callback(self)
@@ -495,7 +492,9 @@ class Session(object):
495
492
  async def start_serve_responses(self) -> None:
496
493
  await self._task_manager.create_task(self.serve())
497
494
 
498
- async def close(self, is_unexpected_close: bool) -> None:
495
+ async def close(
496
+ self, is_unexpected_close: bool, acquire_transport_lock: bool = True
497
+ ) -> None:
499
498
  """Close the session and all associated streams."""
500
499
  logging.info(
501
500
  f"{self._transport_id} closing session "
@@ -508,9 +507,10 @@ class Session(object):
508
507
  return
509
508
  self._state = SessionState.CLOSING
510
509
  self._reset_session_close_countdown()
511
- await self.close_websocket(self._ws_wrapper, should_retry=False)
510
+ async with self._ws_lock:
511
+ await self._ws_wrapper.close()
512
512
  # Clear the session in transports
513
- await self._close_session_callback(self)
513
+ await self._close_session_callback(self, acquire_transport_lock)
514
514
  await self._task_manager.cancel_all_tasks()
515
515
  # TODO: unexpected_close should close stream differently here to
516
516
  # throw exception correctly.
@@ -45,14 +45,24 @@ 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:
55
- self._sessions[session._to_id] = session
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:
64
+ if session._to_id in self._sessions:
65
+ del self._sessions[session._to_id]
56
66
 
57
67
  def generate_nanoid(self) -> str:
58
68
  return str(nanoid.generate())
@@ -87,6 +97,7 @@ class Transport:
87
97
  ) -> Session:
88
98
  session_to_close: Optional[Session] = None
89
99
  new_session: Optional[Session] = None
100
+ logging.error(f"## get_or_create_session, {to_id}")
90
101
  async with self._session_lock:
91
102
  if to_id not in self._sessions:
92
103
  logging.debug(
@@ -138,13 +149,13 @@ class Transport:
138
149
  new_session = old_session
139
150
  except FailedSendingMessageException as e:
140
151
  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)
152
+
153
+ if session_to_close:
154
+ logging.debug(
155
+ "Closing stale session %s", session_to_close.advertised_session_id
156
+ )
157
+ await session_to_close.close(
158
+ is_unexpected_close=False, acquire_transport_lock=False
159
+ )
160
+ await self._set_session(new_session, acquire_lock=False)
150
161
  return new_session