Skip to content

fix: bound request timeouts - #285

Draft
utkarash2991 wants to merge 2 commits into
masterfrom
fix/bound-request-timeout
Draft

utkarash2991 wants to merge 2 commits into
masterfrom
fix/bound-request-timeout

Conversation

@utkarash2991

@utkarash2991 utkarash2991 commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

We pass timeout_milliseconds straight to CURLOPT_TIMEOUT_MS and libcurl treats 0 as no timeout. So a request with 0 timeout to a server which accepts the connection but never responds blocks the whole batch loop and the worker never comes out of it. net.worker_restart() also doesn't help here as the restart flag is only checked between batches.

What this PR does:

  • Adds a GUC pg_net.max_timeout_ms (default 600000, PGC_SUSET).
  • The worker rejects any request whose timeout is outside 1..pg_net.max_timeout_ms: it is not sent and gets an ERROR response with the message timeout_milliseconds must be between 1 and <max> (pg_net.max_timeout_ms), got <value>. Same shape as any other per-request failure (bad URL, curl error). Done in C only, no SQL changes and no migration, and it covers requests inserted directly into the queue as well.
  • README documents the GUC.

Behaviour change: a 0 or negative or oversized timeout used to hang the worker (negative ones made curl_easy_setopt fail and crash it). Now the request fails with an error response and the batch carries on. Everything in range is untouched.

Tests (psycopg): 0, negative and oversized timeouts get the error response with the exact message; lowering pg_net.max_timeout_ms via alter system + reload changes the bound in the message; non-superusers can't set it.

@utkarash2991
utkarash2991 requested review from AndrewJackson2020, imor and steve-chavez and removed request for imor September 15, 2026 11:03
@utkarash2991
utkarash2991 force-pushed the fix/bound-request-timeout branch from 6e47ab2 to b465078 Compare September 15, 2026 11:05
Comment thread test/test_http_timeout.py Outdated
Comment thread test/test_http_timeout.py Outdated
Comment thread test/test_http_timeout.py Outdated
assert execinfo.value.orig.diag.message_primary == CHECK_VIOLATION


def test_worker_clamps_timeout_of_rows_queued_before_the_bound(sess):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In future we could also think of having CI tests in which we run upgrades and assert on properties we want upgrades to uphold.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I agree with this. I would much rather the tests do something like:

  1. install older version of extension
  2. setup (insert rows, whatever)
  3. upgrade to latest or target version
  4. run tests

as opposed to cherry pick aspects of the migration script in the pytest code.

Comment thread test/test_http_timeout.py Outdated


@pytest.mark.parametrize("timeout", [0, -1, 600001])
def test_direct_insert_rejects_timeout_out_of_range(sess, timeout):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is it necessary to test inserting into table and using the convenience function? Seems to me like they test the same code paths and we can just test the function to test the entire execution.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

YEs they are testing the same code path. I just added it for sake of completeness purpose.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

IMO if they test the same code path it's not buying us anything other than slower testing times, especially since we can't run these tests in parallel (or at least its not documented).

Comment thread test/test_http_timeout.py Outdated
Comment thread src/core.c Outdated
handle->ez_handle = curl_easy_init();

handle->timeout_milliseconds = row.timeout_milliseconds;
if (handle->timeout_milliseconds <= 0 || handle->timeout_milliseconds > MAX_TIMEOUT_MS) {

@AndrewJackson2020 AndrewJackson2020 Sep 15, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm a bit confused about this. The upgrade scripts seems to make this impossible via the below statement, does it not? Personally I think I would prefer to handle this at the SQL level. Are we concerned about a mismatch between the c .so and the customer not upgrading the in database extension? IMO we should try to not expose the customer to breaking changes without them running upgrade on the extension.

update net.http_request_queue
  set timeout_milliseconds = net._max_timeout_ms()
  where timeout_milliseconds <= 0 or timeout_milliseconds > net._max_timeout_ms();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@AndrewJackson2020 we need this change as because the binary and the extension SQL don't move together. The .so is replaced whenever the image or package is upgraded but the constraint only appears when we run ALTER EXTENSION UPDATE . So there's a window where the constraint doesn't exist yet and the new worker still has to deal with out-of-range rows.
It also matters for the hung projects this PR is about. After a worker restart the 0-timeout row is back in the queue and the worker picks it up again, so the upgrade script can't even take its lock until that batch finishes. With this line the batch finishes, and then the upgrade can run.

Regarding the breaking changes, the condition here just tries to ensure that the batch finishes, which otherwise can go to hung state forever.

@AndrewJackson2020 AndrewJackson2020 Sep 15, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

That's fine. I did not realize upgrading the extension was not possible under those circumstances. I would just put a comment in. This logic presumably can and should be removed in the future (after we have migrated instances off of the old version).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The .so is replaced whenever the image or package is upgraded but the constraint only appears when we run ALTER EXTENSION UPDATE

The above would be a non-issue if we go ahead with a GUC as mentioned above.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a GUC in the latest commit.

Comment thread sql/pg_net.sql Outdated
@AndrewJackson2020

Copy link
Copy Markdown
Contributor

@utkarash2991 can you make sure this is rebased so that it is picking up the new CI checks?

Comment thread sql/pg_net.sql Outdated
Comment thread test/test_http_timeout.py Outdated
Comment thread test/test_http_timeout.py Outdated
Comment on lines +169 to +181
@pytest.mark.parametrize("timeout", [0, -1, 2147483647])
def test_worker_bounds_out_of_range_timeouts(conn, timeout):
"""A 0, negative or oversized timeout is clamped to pg_net.max_timeout_ms instead of hanging the worker"""

request_id = pg_http_request(
conn,
"select net.http_get(url := 'http://localhost:8080/pathological?status=200', timeout_milliseconds := %s)",
(timeout,),
)

response = pg_collect_response(conn, request_id)

assert response["status"] == "SUCCESS"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we should refine this. Two options:

  • Just fail when the timeout surpasses the GUC one. IMO the best.
  • Silently clamp it as it's done now, but log a warning.

@utkarash2991
utkarash2991 marked this pull request as draft September 18, 2026 20:47
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.

4 participants