Skip to content

feat: add multi-url failover to Python bindings#126

Open
nwoolmer wants to merge 5 commits intomainfrom
feat/multi-url-support
Open

feat: add multi-url failover to Python bindings#126
nwoolmer wants to merge 5 commits intomainfrom
feat/multi-url-support

Conversation

@nwoolmer
Copy link
Copy Markdown
Contributor

@nwoolmer nwoolmer commented Mar 12, 2026

Summary

  • Expose multi-url failover in Python via config strings with multiple addr keys
  • Add addresses keyword argument to Sender.__init__ for programmatic use: addresses=[('host2', 9000), ('host3', 9001)]
  • Update .pyi type stubs and .pxd Cython declarations
  • 18 Python tests covering basic usage, failover behavior, error handling, IPv6 parsing, body content verification, and edge cases

Depends on

  • questdb/c-questdb-client#TBD (multi-url failover core)
  • questdb/questdb-confstr-rs#TBD (duplicate key support)

Test plan

  • 523 Python tests passed (12 skipped), including 18 new multi-url tests
  • TestMultiUrl covers: basic kwarg, int/str/list ports, None/empty backward compat, bad types, TCP/TCPS rejection, config string, failover on 500, non-retriable 400, IPv6 parsing, programmatic failover, 3-server chain, all-servers-fail, body content verification

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Multi-URL failover for Sender: new addresses parameter enabling round‑robin HTTP/HTTPS failover and retry behavior.
  • Tests

    • Added comprehensive multi‑URL failover tests (retries, validation, IPv6, payload integrity).
    • Added a test for timestamp-only sends and bumped test environment to QuestDB 9.3.0.

nwoolmer and others added 2 commits March 12, 2026 16:10
Expose multi-url failover in Python via config strings with multiple
`addr` keys and a new `addresses` keyword argument on Sender.__init__
for programmatic use. Update type stubs and add 14 tests covering
basic usage, failover behavior, error handling, and IPv6 parsing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix resource leak in test_multi_url_ipv6_from_conf (add finally/close)
- Add error code assertion to test_addresses_tcp_rejected
- Add tests: tcps rejection, all-servers-fail, failover body content,
  list entries in addresses param
- Pre-load sufficient 500 responses for all-servers-fail test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 12, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c4b416fa-96d1-4796-ab3b-23d071865eb8

📥 Commits

Reviewing files that changed from the base of the PR and between 5ed6564 and 8289d73.

📒 Files selected for processing (4)
  • c-questdb-client
  • src/questdb/ingress.pyi
  • src/questdb/ingress.pyx
  • test/test.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • test/test.py
  • c-questdb-client
  • src/questdb/ingress.pyi

📝 Walkthrough

Walkthrough

Adds multi-address (multi-URL) failover support: Sender accepts an addresses parameter, configuration parsing collects multiple addr entries, a C API binding line_sender_opts_address is exposed, and tests for multi-URL failover and timestamp-only sends are added.

Changes

Cohort / File(s) Summary
Type Stubs & C Bindings
src/questdb/ingress.pyi, src/questdb/line_sender.pxd
Added Sequence/Tuple imports; updated Sender.__init__ signature to include addresses; declared line_sender_opts_address in the Cython pxd for host/port registration.
Core Implementation
src/questdb/ingress.pyx
parse_conf_str now collects multiple addr entries into _addrs; Sender.__init__ accepts addresses, validates host/port pairs, forwards each to C API via line_sender_opts_address, and raises on C API errors.
Tests
test/test.py, test/system_test.py
Added TestMultiUrl with extensive multi-URL/failover tests; added test_sending_just_timestamps; bumped QUESTDB_VERSION to 9.3.0.
Submodule Reference
c-questdb-client
Updated submodule commit reference only (no functional code changes).

Sequence Diagram

sequenceDiagram
    participant Client
    participant Sender
    participant ConfParser
    participant CAPI

    Client->>Sender: init(..., addresses=[('host1', 8080), ('host2', 8080)])
    Sender->>CAPI: line_sender_opts_new()
    alt Addresses provided programmatically
        loop for each (host,port)
            Sender->>CAPI: line_sender_opts_address(host, port)
            CAPI-->>Sender: status / err
        end
    else Addresses from config
        Sender->>ConfParser: parse_conf_str(conf_string)
        ConfParser-->>Sender: params (includes _addrs)
        loop for each addr in _addrs
            Sender->>CAPI: line_sender_opts_address(host, port)
            CAPI-->>Sender: status / err
        end
    end
    CAPI-->>Sender: configured opts or error
    Sender-->>Client: ready or raise error
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped through confs with nose so keen,
Collected each host in a tidy routine.
If one gate closes, I'll bound to the next door,
Bouncing the payload till it's safely ashore.
Fluffy failover — hop, retry, explore.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main feature added: multi-url failover support for the Python bindings, which is the primary objective of this PR.
Docstring Coverage ✅ Passed Docstring coverage is 92.31% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/multi-url-support
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can use your project's `ruff` configuration to improve the quality of Python code reviews.

Add a Ruff configuration file to your project to customize how CodeRabbit runs ruff.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/questdb/ingress.pyi (1)

834-861: ⚠️ Potential issue | 🟡 Minor

Align the stub with the accepted addresses entry shape.

src/questdb/ingress.pyx accepts both tuple and list entries in addresses, and test/test.py now asserts that addresses=[['127.0.0.1', port]] works. This stub only allows List[Tuple[...]], so type-checked callers will get a false error for a supported input. Either widen the annotation here or stop accepting list entries at runtime.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/questdb/ingress.pyi` around lines 834 - 861, The stub's addresses
parameter only allows List[Tuple[..., ...]] but runtime accepts lists as well;
update the signature in src/questdb/ingress.pyi to widen addresses to accept
both tuples and lists (e.g. change addresses: Optional[List[Tuple[str,
Union[int, str]]]] to addresses: Optional[List[Sequence[Union[str, int, str]]]]
or addresses: Optional[List[Sequence[Union[str, int]]]]), and add Sequence to
the typing imports so type-checkers won't flag callers using
addresses=[['127.0.0.1', port]].
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/system_test.py`:
- Around line 331-334: The test_sending_just_timestamps test is currently a
no-op because sender.row(..., at=TimestampNanos.now()) doesn't write unless at
least one column/symbol is present; update the test to either assert the no-op
explicitly (e.g., check that no rows were created) or send a real field/symbol
and then verify persistence. Modify the call to sender.row in
test_sending_just_timestamps to include a concrete column or symbol (e.g., add a
field name/value) and after sender.flush() query the table to assert the row
exists and has the expected timestamp, or alternatively add an explicit
assertion that no rows were written to confirm the no-op behavior; refer to
sender.row, TimestampNanos.now(), and the ingestion behavior in
src/questdb/ingress.pyx when making the change.

---

Outside diff comments:
In `@src/questdb/ingress.pyi`:
- Around line 834-861: The stub's addresses parameter only allows
List[Tuple[..., ...]] but runtime accepts lists as well; update the signature in
src/questdb/ingress.pyi to widen addresses to accept both tuples and lists (e.g.
change addresses: Optional[List[Tuple[str, Union[int, str]]]] to addresses:
Optional[List[Sequence[Union[str, int, str]]]] or addresses:
Optional[List[Sequence[Union[str, int]]]]), and add Sequence to the typing
imports so type-checkers won't flag callers using addresses=[['127.0.0.1',
port]].

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cc03a00f-2552-4b8b-a53d-d8bf8a59e199

📥 Commits

Reviewing files that changed from the base of the PR and between 437382b and ce13b3a.

📒 Files selected for processing (6)
  • c-questdb-client
  • src/questdb/ingress.pyi
  • src/questdb/ingress.pyx
  • src/questdb/line_sender.pxd
  • test/system_test.py
  • test/test.py

Comment thread test/system_test.py
Comment on lines +331 to +334
def test_sending_just_timestamps(self):
with qi.Sender('http', 'localhost', self.qdb_plain.http_server_port) as sender:
sender.row(table_name="just_timestamp_test", at=TimestampNanos.now())
sender.flush()
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Mar 12, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

This test is currently a no-op.

sender.row(..., at=TimestampNanos.now()) does not write anything unless at least one symbol or column is present, so this path just rewinds the buffer in src/questdb/ingress.pyx. With no assertion afterwards, the test stays green even if timestamp-only writes are unsupported or broken. Please either assert the no-op explicitly or add a real field and verify the row was persisted.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/system_test.py` around lines 331 - 334, The test_sending_just_timestamps
test is currently a no-op because sender.row(..., at=TimestampNanos.now())
doesn't write unless at least one column/symbol is present; update the test to
either assert the no-op explicitly (e.g., check that no rows were created) or
send a real field/symbol and then verify persistence. Modify the call to
sender.row in test_sending_just_timestamps to include a concrete column or
symbol (e.g., add a field name/value) and after sender.flush() query the table
to assert the row exists and has the expected timestamp, or alternatively add an
explicit assertion that no rows were written to confirm the no-op behavior;
refer to sender.row, TimestampNanos.now(), and the ingestion behavior in
src/questdb/ingress.pyx when making the change.

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.

This is pre-existing code, not introduced by this PR — out of scope.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@nwoolmer, the diff actually shows this test was added in this PR — all four lines carry + markers, indicating they are new additions. The AI-generated summary also confirms it: "Added new test method test_sending_just_timestamps to send a row with only a timestamp."

Given that, the concern stands: as written the test is effectively a no-op (no column/symbol → nothing is written → no assertion). Would you like to either:

  1. Assert the no-op explicitly — e.g., verify no rows were persisted, or
  2. Add a real field and then query the table to confirm the row exists (matching the pattern of other tests in the file)?

Accept both tuples and lists in type annotations, matching runtime
behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/questdb/ingress.pyi (1)

39-39: Unused Tuple import.

Tuple is imported but not used anywhere in this file. Consider removing it to keep imports clean.

🧹 Suggested fix
-from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
+from typing import Any, Dict, List, Optional, Sequence, Union
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/questdb/ingress.pyi` at line 39, Remove the unused Tuple symbol from the
typing import list in the module (the line that currently imports "Any, Dict,
List, Optional, Sequence, Tuple, Union"); update the import to only include the
used names (e.g., "Any, Dict, List, Optional, Sequence, Union") so the unused
Tuple is no longer imported and the file passes linting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/questdb/ingress.pyi`:
- Line 39: Remove the unused Tuple symbol from the typing import list in the
module (the line that currently imports "Any, Dict, List, Optional, Sequence,
Tuple, Union"); update the import to only include the used names (e.g., "Any,
Dict, List, Optional, Sequence, Union") so the unused Tuple is no longer
imported and the file passes linting.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f773f770-4ff7-4ec7-b1c4-5f4b8fb3b260

📥 Commits

Reviewing files that changed from the base of the PR and between ce13b3a and 5ed6564.

📒 Files selected for processing (1)
  • src/questdb/ingress.pyi

@nwoolmer
Copy link
Copy Markdown
Contributor Author

Type stub fix for addresses parameter: widened from List[Tuple[str, Union[int, str]]] to Sequence[Sequence[Union[str, int]]] in 5ed6564, matching runtime acceptance of both tuples and lists.

nwoolmer and others added 2 commits March 13, 2026 16:02
- Host must be str (reject None, int, etc. with TypeError)
- Error messages include entry index: "addresses[0] must be..."
- Type stub narrowed to Sequence[Tuple[str, Union[str, int]]]
- 4 new tests for bad host types and wrong-length tuples

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

1 participant