Skip to content

Conversation

@luxscious
Copy link
Contributor

@luxscious luxscious commented Jan 27, 2026

feat: add exponential backoff strategy for SSE reconnection
fix: wrap client.start() in try catch

to prevent instances where we try to reconnect forever and spam the server.
fix: bug where client crashes when http error code received.
chore: lint errors
@luxscious luxscious changed the title fix: attempt to reconnect to sse server when response ends prematurely. fix: sse error handling Jan 27, 2026
@luxscious luxscious marked this pull request as ready for review January 27, 2026 20:20
@luxscious luxscious requested a review from a team as a code owner January 27, 2026 20:20
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Improves SSE robustness by propagating read-loop failures to the error handler and introducing an exponential backoff strategy for SSE reconnection.

Changes:

  • Wraps SSEClient.start() and the read loop in try/except/finally, forwarding failures to the configured error handler.
  • Adds exponential backoff + delayed reconnection thread orchestration in EnvironmentConfigManager.sse_error.
  • Fixes use_new_config boolean precedence for correct SSE URL change detection.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
devcycle_python_sdk/managers/sse_manager.py Wraps SSE read loop in error handling and forwards exceptions to the error callback; fixes URL-change condition precedence.
devcycle_python_sdk/managers/config_manager.py Adds exponential backoff reconnection logic and SSE connection recreation/teardown handling.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

Copilot AI commented Jan 27, 2026

@JamieSinn I've opened a new pull request, #108, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 246 to +249
def close(self):
self._polling_enabled = False
if self._sse_manager is not None and self._sse_manager.client is not None:
self._sse_manager.client.close()
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

close() only closes the current SSE client, but any already-scheduled reconnect thread (spawned from sse_error) can still wake up later and call _recreate_sse_connection(), re-opening SSE after the manager is supposedly closed. Add a shutdown/closed guard (or check _polling_enabled) in sse_error / _delayed_sse_reconnect / _recreate_sse_connection, and set it in close() so reconnection is suppressed during/after shutdown.

Copilot uses AI. Check for mistakes.
Comment on lines +199 to +204
logger.debug(f"DevCyle: SSE connection error: {error.error}")
current_time = time.time()

if self._sse_reconnecting:
logger.debug("DevCyle: Reconnection already in progress, skipping")
return
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in log prefix: "DevCyle" should be "DevCycle" for consistency/searchability.

Copilot uses AI. Check for mistakes.
Comment on lines +218 to +220
logger.debug(
f"DevCyle: Within backoff period, scheduling reconnection in {delay_seconds:.1f}s"
)
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in log prefix: "DevCyle" should be "DevCycle" for consistency/searchability.

Copilot uses AI. Check for mistakes.
Comment on lines +225 to +228
logger.debug(
f"DevCyle: Attempting SSE reconnection (attempt #{self._sse_reconnect_attempts}, "
f"backoff: {delay_seconds:.1f}s)"
)
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in log prefix: "DevCyle" should be "DevCycle" for consistency/searchability.

Copilot uses AI. Check for mistakes.
self._sse_reconnecting = False
self._last_reconnect_attempt_time = None
else:
logger.debug("DevCyle: SSE keepalive received")
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in log prefix: "DevCyle" should be "DevCycle" for consistency/searchability.

Copilot uses AI. Check for mistakes.
Comment on lines 197 to +233
def sse_error(self, error: ld_eventsource.actions.Fault):
self._sse_connected = False
logger.debug(f"DevCycle: Received SSE error: {error}")
logger.debug(f"DevCyle: SSE connection error: {error.error}")
current_time = time.time()

if self._sse_reconnecting:
logger.debug("DevCyle: Reconnection already in progress, skipping")
return

# Calculate exponential backoff interval (capped at max)
backoff_interval = min(
self._min_reconnect_interval * (2**self._sse_reconnect_attempts),
self._max_reconnect_interval,
)

# Check if we need to wait for remaining backoff time
delay_seconds = backoff_interval
if self._last_reconnect_attempt_time is not None:
time_since_last_attempt = current_time - self._last_reconnect_attempt_time
if time_since_last_attempt < backoff_interval:
delay_seconds = backoff_interval - time_since_last_attempt
logger.debug(
f"DevCyle: Within backoff period, scheduling reconnection in {delay_seconds:.1f}s"
)

self._sse_reconnecting = True
self._sse_reconnect_attempts += 1

logger.debug(
f"DevCyle: Attempting SSE reconnection (attempt #{self._sse_reconnect_attempts}, "
f"backoff: {delay_seconds:.1f}s)"
)

reconnect_thread = threading.Thread(
target=self._delayed_sse_reconnect, args=(delay_seconds,), daemon=True
)
reconnect_thread.start()
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New SSE exponential-backoff reconnection behavior (attempt counting, delay calculation, and suppression of concurrent reconnects) isn’t covered by tests. Add unit tests for sse_error that validate the computed delay/attempt increments (mocking time.time()/threading.Thread/time.sleep) and that successful reconnect resets attempts.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants