Skip to content

Retry JsonDrb and API requests in Python#2676

Merged
jmthomas merged 3 commits intomainfrom
py_reconnect
Dec 29, 2025
Merged

Retry JsonDrb and API requests in Python#2676
jmthomas merged 3 commits intomainfrom
py_reconnect

Conversation

@jmthomas
Copy link
Copy Markdown
Member

closes #2233

@codecov
Copy link
Copy Markdown

codecov bot commented Dec 23, 2025

Codecov Report

❌ Patch coverage is 4.25532% with 45 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.30%. Comparing base (a6bff9a) to head (d08beea).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
openc3/lib/openc3/io/json_api_object.rb 7.40% 25 Missing ⚠️
openc3/lib/openc3/io/json_drb_object.rb 0.00% 20 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2676      +/-   ##
==========================================
- Coverage   79.37%   79.30%   -0.07%     
==========================================
  Files         662      662              
  Lines       52526    52590      +64     
  Branches      734      734              
==========================================
+ Hits        41690    41707      +17     
- Misses      10756    10803      +47     
  Partials       80       80              
Flag Coverage Δ
python 81.38% <ø> (-0.08%) ⬇️
ruby-api 84.76% <ø> (ø)
ruby-backend 82.15% <4.25%> (-0.08%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mcosgriff
Copy link
Copy Markdown
Contributor

What there a reason you didn't use Retry from urllib3?

import requests
from requests.adapters import HTTPAdapter
from urllib3.util import Retry

session = requests.Session()
retries = Retry(total=5, backoff_factor=1, status_forcelist=[429, 500, 502, 503])
session.mount('http://', HTTPAdapter(max_retries=retries))

response = session.get('http://example.com')

@ryanmelt
Copy link
Copy Markdown
Member

What there a reason you didn't use Retry from urllib3?

import requests
from requests.adapters import HTTPAdapter
from urllib3.util import Retry

session = requests.Session()
retries = Retry(total=5, backoff_factor=1, status_forcelist=[429, 500, 502, 503])
session.mount('http://', HTTPAdapter(max_retries=retries))

response = session.get('http://example.com')

Retrying regular 500 is super dangerous, because it could duplicate actions (don't know where in the code it crashed). 502, 503 is probably ok.

@ryanmelt
Copy link
Copy Markdown
Member

What about Ruby?

Copy link
Copy Markdown
Member

@ryanmelt ryanmelt left a comment

Choose a reason for hiding this comment

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

Ruby and python behavior should be the same.

@jmthomas jmthomas requested a review from ryanmelt December 23, 2025 21:58
Copy link
Copy Markdown
Contributor

@clayandgen clayandgen left a comment

Choose a reason for hiding this comment

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

Could we use the Faraday retry in Ruby and urllib3 Retry in Python to simplify the logic? I think that's more standard, unless this was intentional to keep the python and ruby code more in-line

@jmthomas
Copy link
Copy Markdown
Member Author

jmthomas commented Dec 29, 2025

Based on claude's analysis, faraday-retry cannot directly replace the current custom retry logic. Here's why:

The custom retry logic does something faraday-retry cannot:

  # Current logic between retries (lines 224-227 in json_api_object.rb):
  disconnect()       # Closes and nils @http
  sleep(RETRY_DELAY)
  connect()          # Creates a brand new Faraday.new() instance

Why faraday-retry Won't Work

  1. faraday-retry is middleware - It operates within an existing Faraday connection and retries requests on the same connection. It cannot tear down and rebuild the connection object itself.
  2. Connection state - When you get Errno::ECONNRESET, Errno::EPIPE, or IOError, the underlying TCP connection is likely in a bad state. The current code handles this by completely destroying and recreating the Faraday instance. faraday-retry would just retry on the potentially broken connection.
  3. Custom hooks not supported - faraday-retry has retry_block callbacks, but these are for logging/monitoring, not for executing disconnect()/connect() to reset connection state.

We could use faraday-retry if we changed the architecture to not persist the @http connection - i.e., create a new Faraday instance for each request. But that would:

  • Remove connection reuse (performance impact)
  • Require significant refactoring

The same thing applies for the Python code and urllib3:

  1. urllib3 Retry operates at the connection pool level - It's configured via requests.adapters.HTTPAdapter and manages retries within the underlying connection pool. It cannot tear down and recreate the requests.Session() object.
  2. No callback/hook mechanism - urllib3's Retry class is purely declarative configuration (total, connect, read, backoff_factor, etc.). There's no way to inject disconnect()/connect() calls between retries.
  3. Different scope - urllib3 Retry handles low-level connection pool retries. The current code needs application-level session management - completely destroying and recreating the Session object to get a fresh state.
  4. Exception handling - The code catches ChunkedEncodingError, RequestsConnectionError, ConnectionResetError, BrokenPipeError, OSError. While urllib3 Retry handles some connection errors, it wouldn't help with the session recreation requirement.

@jmthomas jmthomas merged commit 39a3bb7 into main Dec 29, 2025
28 checks passed
@jmthomas jmthomas deleted the py_reconnect branch December 29, 2025 23:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Intermittent ConnectionResetError in script runner script that runs in the background

4 participants