-
Notifications
You must be signed in to change notification settings - Fork 929
fix: prevent race conditions when starting concurrent sessions (Node.js & Python) #690
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -232,6 +232,8 @@ export class CopilotClient { | |
| * Parse CLI URL into host and port | ||
| * Supports formats: "host:port", "http://host:port", "https://host:port", or just "port" | ||
| */ | ||
| private startPromise: Promise<void> | null = null; | ||
|
|
||
| private parseCliUrl(url: string): { host: string; port: number } { | ||
| // Remove protocol if present | ||
| let cleanUrl = url.replace(/^https?:\/\//, ""); | ||
|
|
@@ -282,25 +284,34 @@ export class CopilotClient { | |
| return; | ||
| } | ||
|
|
||
| this.state = "connecting"; | ||
| if (this.startPromise) { | ||
| return this.startPromise; | ||
| } | ||
|
|
||
| try { | ||
| // Only start CLI server process if not connecting to external server | ||
| if (!this.isExternalServer) { | ||
| await this.startCLIServer(); | ||
| } | ||
| this.startPromise = (async () => { | ||
| this.state = "connecting"; | ||
|
|
||
| try { | ||
|
Comment on lines
+291
to
+294
|
||
| // Only start CLI server process if not connecting to external server | ||
| if (!this.isExternalServer) { | ||
| await this.startCLIServer(); | ||
| } | ||
|
|
||
| // Connect to the server | ||
| await this.connectToServer(); | ||
| // Connect to the server | ||
| await this.connectToServer(); | ||
|
|
||
| // Verify protocol version compatibility | ||
| await this.verifyProtocolVersion(); | ||
| // Verify protocol version compatibility | ||
| await this.verifyProtocolVersion(); | ||
|
|
||
| this.state = "connected"; | ||
| } catch (error) { | ||
| this.state = "error"; | ||
| throw error; | ||
| } | ||
| this.state = "connected"; | ||
| } catch (error) { | ||
| this.state = "error"; | ||
| this.startPromise = null; | ||
| throw error; | ||
| } | ||
| })(); | ||
|
|
||
| return this.startPromise; | ||
|
Comment on lines
+306
to
+314
|
||
| } | ||
|
|
||
| /** | ||
|
|
@@ -403,6 +414,7 @@ export class CopilotClient { | |
| } | ||
|
|
||
| this.state = "disconnected"; | ||
| this.startPromise = null; | ||
| this.actualPort = null; | ||
| this.stderrBuffer = ""; | ||
| this.processExitPromise = null; | ||
|
|
@@ -475,6 +487,7 @@ export class CopilotClient { | |
| } | ||
|
|
||
| this.state = "disconnected"; | ||
| this.startPromise = null; | ||
| this.actualPort = null; | ||
| this.stderrBuffer = ""; | ||
| this.processExitPromise = null; | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -201,6 +201,7 @@ def __init__(self, options: CopilotClientOptions | None = None): | |||||||||||||||||||||||
| self._process: subprocess.Popen | None = None | ||||||||||||||||||||||||
| self._client: JsonRpcClient | None = None | ||||||||||||||||||||||||
| self._state: ConnectionState = "disconnected" | ||||||||||||||||||||||||
| self._start_lock = asyncio.Lock() | ||||||||||||||||||||||||
| self._sessions: dict[str, CopilotSession] = {} | ||||||||||||||||||||||||
| self._sessions_lock = threading.Lock() | ||||||||||||||||||||||||
| self._models_cache: list[ModelInfo] | None = None | ||||||||||||||||||||||||
|
|
@@ -281,39 +282,40 @@ async def start(self) -> None: | |||||||||||||||||||||||
| >>> await client.start() | ||||||||||||||||||||||||
| >>> # Now ready to create sessions | ||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||
| if self._state == "connected": | ||||||||||||||||||||||||
| return | ||||||||||||||||||||||||
| async with self._start_lock: | ||||||||||||||||||||||||
|
Comment on lines
284
to
+285
|
||||||||||||||||||||||||
| """ | |
| if self._state == "connected": | |
| return | |
| async with self._start_lock: | |
| """ | |
| # Fast-path: avoid acquiring the start lock if we're already connected. | |
| if self._state == "connected": | |
| return | |
| async with self._start_lock: | |
| # Double-check under the lock to ensure correctness with concurrent callers. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
start()now returns aPromisein some branches but returns nothing in the early-return path shown here. Ifstart()is no longer declaredasync, callers doingawait client.start()may not reliably await anything. Consider ensuringstart()is declaredasync(soreturn;becomesPromise<void>), or return a resolved promise in the early-return path (e.g.,return this.startPromise ?? Promise.resolve();) to keep the return type consistent.