Skip to content

refactor: Rework client timeout #384

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

Merged
merged 5 commits into from
Apr 22, 2025
Merged

refactor: Rework client timeout #384

merged 5 commits into from
Apr 22, 2025

Conversation

Pijukatel
Copy link
Contributor

@Pijukatel Pijukatel commented Apr 9, 2025

Add optional timeout argument to the individual client calls. Timeout of each call is either this timeout or the default client timeout.
In case of failed, but retry-able request, the timeout is multiplied by 2^attempt in each consecutive attempt, up to the value of client default timeout.

Several storage related endpoints now have reduced timeout value. Mainly the endpoints that are not expected to take long or are considered safe to run twice or idempotent.

Timeouts for the storage related endpoints are all summarized in the new tests where it is all in one place.

Addresses issues found in: apify/crawlee-python#1132

Updated some clients endpoints with spefici timeouts.
Added tests.
TODO: Add async tests. Make some deal with Mypy
@github-actions github-actions bot added this to the 112nd sprint - Tooling team milestone Apr 9, 2025
@github-actions github-actions bot added t-tooling Issues with this label are in the ownership of the tooling team. tested Temporary label used only programatically for some analytics. labels Apr 9, 2025
@Pijukatel Pijukatel requested a review from Copilot April 9, 2025 11:24
Copy link

@Copilot 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.

Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.

Comments suppressed due to low confidence (1)

src/apify_client/_http_client.py:176

  • [nitpick] The exponential backoff calculation would benefit from a clarifying comment or helper function to ensure its intent is immediately clear to future maintainers.
timeout = min(self.timeout_secs, (timeout_secs or self.timeout_secs) * 2 ** (attempt - 1))

@Pijukatel Pijukatel added adhoc Ad-hoc unplanned task added during the sprint. debt Code quality improvement or decrease of technical debt. labels Apr 9, 2025
@Pijukatel Pijukatel requested review from janbuchar and vdusek April 9, 2025 12:36
@Pijukatel Pijukatel marked this pull request as ready for review April 9, 2025 12:36
Copy link
Contributor

@vdusek vdusek left a comment

Choose a reason for hiding this comment

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

I guess you don't use timedelta because there are already integers utilized, correct?

@Pijukatel
Copy link
Contributor Author

I guess you don't use timedelta because there are already integers utilized, correct?

Yes, I used the same format that was already being used in the rest of the repository.

Copy link
Contributor

@vdusek vdusek left a comment

Choose a reason for hiding this comment

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

Ok, LGTM then

@@ -68,7 +69,7 @@ def __init__(
api_url: str | None = None,
max_retries: int | None = 8,
min_delay_between_retries_millis: int | None = 500,
timeout_secs: int | None = 360,
timeout_secs: int | None = DEFAULT_TIMEOUT,
Copy link
Contributor

Choose a reason for hiding this comment

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

So if you pass None, it will still use the default timeout?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, this is same as the previous implementation.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hm. I understand it's BC and BC is sacred, but I would expect None to mean "no timeout"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To limit the scope of this change, I will avoid adding unnecessary breaking change to this PR, but it might be added in own PR if needed.

@@ -11,12 +11,13 @@
class ResourceClient(BaseClient):
"""Base class for sub-clients manipulating a single resource."""

def _get(self) -> dict | None:
def _get(self, timeout_secs: int | None = None) -> dict | None:
Copy link
Contributor

Choose a reason for hiding this comment

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

This could have the default timeout constant as the default as well, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It could. I guess this makes only difference if someone decides to use the client directly and not through ApifyClient(A)sync. I am not strongly against this, but I lean towards not doing it as having same timeout constant used on several levels, can create some confusion about which one is the one being applied.

Currently it set on ApifyClient(A)sync and it falls through all the way down through the HTTPClient(Async)

@@ -34,7 +37,7 @@ def get(self) -> dict | None:
Returns:
The retrieved dataset, or None, if it does not exist.
"""
return self._get()
return self._get(timeout_secs=_SMALL_TIMEOUT)
Copy link
Contributor

Choose a reason for hiding this comment

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

It's strange to have no way to override this... Could we have a timeout parameter here as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is there any use-case for this?

_timeout_params,
)
@respx.mock
def test_specific_timeouts_for_specific_endpoints_sync(
Copy link
Contributor

Choose a reason for hiding this comment

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

So here we just test that the timeouts are set to the correct value, correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I think such test can keep the sync and async clients synchronized and it gives nice overview of the timeouts.

@Pijukatel
Copy link
Contributor Author

Pijukatel commented Apr 22, 2025

Comparison of this change to master when used in crawlee based BeautifulsoupCrawler:

Without the change, the timeout issue caused 2 out of 10 crawler runs to be delayed for several minutes. On average runtime was 110 s
Benchmark data

 2025-04-22 06:47:07,141 - benchmark_logger - INFO - Benchmark of run uHUeOuhjYGKTcfULc, benchmark=Actor: beautifulsoup-crawler-benchmark, Valid results: 160, Runtime: 55.079s,
2025-04-22 06:47:08,457 - benchmark_logger - INFO - Benchmark of run DRYRXo0DwjCc9cxil, benchmark=Actor: beautifulsoup-crawler-benchmark, Valid results: 160, Runtime: 52.431s, 
2025-04-22 06:47:09,719 - benchmark_logger - INFO - Benchmark of run kZKfpEZrwKSIHcf1X, benchmark=Actor: beautifulsoup-crawler-benchmark, Valid results: 160, Runtime: 48.069s, 
2025-04-22 06:47:11,015 - benchmark_logger - INFO - Benchmark of run TldmdpnCzs7jZtbBX, benchmark=Actor: beautifulsoup-crawler-benchmark, Valid results: 160, Runtime: 364.947s, 
2025-04-22 06:47:12,337 - benchmark_logger - INFO - Benchmark of run SHSTq8RB83VS9eACy, benchmark=Actor: beautifulsoup-crawler-benchmark, Valid results: 160, Runtime: 48.108s, 
2025-04-22 06:47:13,703 - benchmark_logger - INFO - Benchmark of run 2t9bybamUDI5A0gvp, benchmark=Actor: beautifulsoup-crawler-benchmark, Valid results: 160, Runtime: 42.485s, 
2025-04-22 06:47:14,971 - benchmark_logger - INFO - Benchmark of run 3RmzOyDKrGeIt3phf, benchmark=Actor: beautifulsoup-crawler-benchmark, Valid results: 160, Runtime: 355.603s, 
2025-04-22 06:47:16,172 - benchmark_logger - INFO - Benchmark of run 2w84fA5giuVZtq3Ig, benchmark=Actor: beautifulsoup-crawler-benchmark, Valid results: 160, Runtime: 49.454s, 
2025-04-22 06:47:17,398 - benchmark_logger - INFO - Benchmark of run TgdXHgEu7VrWWdjxs, benchmark=Actor: beautifulsoup-crawler-benchmark, Valid results: 160, Runtime: 45.008s, 
2025-04-22 06:47:18,597 - benchmark_logger - INFO - Benchmark of run ZfUB4J2PFsi0eKIrq, benchmark=Actor: beautifulsoup-crawler-benchmark, Valid results: 160, Runtime: 44.959s, 

2025-04-22 06:47:18,598 - benchmark_logger - INFO - Overall benchmark of all runs, :aggregated_benchmark=Actor: beautifulsoup-crawler-benchmark, Valid results: 160.0, Runtime: 110.6143s,

With the change, the timeout issue caused 1 out of 10 crawler runs to be delayed by reduced timeout only for several seconds. On average runtime was 47 s
Benchmark data

2025-04-22 10:33:04,939 - benchmark_logger - INFO - Benchmark of run O4sCXyUh4Bq123K4A, benchmark=Actor: beautifulsoup-crawler-benchmark, Valid results: 160, Runtime: 49.955s, 
2025-04-22 10:33:05,799 - benchmark_logger - INFO - Benchmark of run l6thld9FAQ3NZGYbU, benchmark=Actor: beautifulsoup-crawler-benchmark, Valid results: 160, Runtime: 46.766s, 
2025-04-22 10:33:06,666 - benchmark_logger - INFO - Benchmark of run 0fVrzImRHkmhlp1PU, benchmark=Actor: beautifulsoup-crawler-benchmark, Valid results: 160, Runtime: 44.235s, 
2025-04-22 10:33:07,448 - benchmark_logger - INFO - Benchmark of run wsKJuPOJwRh50q05I, benchmark=Actor: beautifulsoup-crawler-benchmark, Valid results: 160, Runtime: 42.443s, 
2025-04-22 10:33:08,338 - benchmark_logger - INFO - Benchmark of run 2lVzPb8qgVgv5b3h7, benchmark=Actor: beautifulsoup-crawler-benchmark, Valid results: 160, Runtime: 47.248s, 
2025-04-22 10:33:09,194 - benchmark_logger - INFO - Benchmark of run cTwxscer7IbmnrFJC, benchmark=Actor: beautifulsoup-crawler-benchmark, Valid results: 160, Runtime: 44.459s, 
2025-04-22 10:33:10,099 - benchmark_logger - INFO - Benchmark of run dtm1AvHdqQbKCtgrS, benchmark=Actor: beautifulsoup-crawler-benchmark, Valid results: 160, Runtime: 66.829s, 
2025-04-22 10:33:10,915 - benchmark_logger - INFO - Benchmark of run 8htGLQh32E4RdxeyN, benchmark=Actor: beautifulsoup-crawler-benchmark, Valid results: 160, Runtime: 42.451s, 
2025-04-22 10:33:11,789 - benchmark_logger - INFO - Benchmark of run ZCWnhLjHD6lcaarnm, benchmark=Actor: beautifulsoup-crawler-benchmark, Valid results: 160, Runtime: 43.601s, 
2025-04-22 10:33:12,564 - benchmark_logger - INFO - Benchmark of run Xo6dQd2zwkG3kOoQE, benchmark=Actor: beautifulsoup-crawler-benchmark, Valid results: 160, Runtime: 42.236s, 
2025-04-22 10:33:12,564 - benchmark_logger - INFO - Overall benchmark of all runs, :aggregated_benchmark=Actor: beautifulsoup-crawler-benchmark, Valid results: 160.0, Runtime: 47.0223s

@Pijukatel Pijukatel merged commit 6356d0c into master Apr 22, 2025
28 checks passed
@Pijukatel Pijukatel deleted the timeout-rework branch April 22, 2025 11:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
adhoc Ad-hoc unplanned task added during the sprint. debt Code quality improvement or decrease of technical debt. t-tooling Issues with this label are in the ownership of the tooling team. tested Temporary label used only programatically for some analytics.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants