Skip to content

Conversation

MadLittleMods
Copy link
Contributor

@MadLittleMods MadLittleMods commented Sep 9, 2025

Wrap the Rust HTTP client with make_deferred_yieldable so downstream usage doesn't need to use PreserveLoggingContext() or make_deferred_yieldable.

it seems like we should have some wrapper around it that uses make_deferred_yieldable(...) to make things right so we don't have to do this in the downstream code.

-- @MadLittleMods, #18357 (comment)

Spawning from wanting to remove PreserveLoggingContext() from the codebase and thinking that we shouldn't have to pollute all downstream usage with PreserveLoggingContext() or make_deferred_yieldable

Part of #18905 (Remove sentinel logcontext where we log in Synapse)

Dev notes

Pull Request Checklist

  • Pull request is based on the develop branch
  • Pull request includes a changelog file. The entry should:
    • Be a short description of your change which makes sense to users. "Fixed a bug that prevented receiving messages from other servers." instead of "Moved X method from EventStore to EventWorkerStore.".
    • Use markdown where necessary, mostly for code blocks.
    • End with either a period (.) or an exclamation mark (!).
    • Start with a capital letter.
    • Feel free to credit yourself, by adding a sentence "Contributed by @github_username." or "Contributed by [Your Name]." to the end of the entry.
  • Code style is correct (run the linters)

So downstream usage doesn't need to use
`PreserveLoggingContext()` or `make_deferred_yieldable`

Spawning from #18870
and #18357 (comment)
from synapse.types import ISynapseReactor


class HttpClient:
Copy link
Contributor Author

@MadLittleMods MadLittleMods Sep 9, 2025

Choose a reason for hiding this comment

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

I think this solution is awful.

What's a better way we can wrap the Rust code to do the right thing?

Call Python from the Rust code?

Write an equivalent wrapper in Rust? (not sure how to approach)

Better way to organize all of this? I initially tried to rename http_client.pyi to internal_http_client.pyi and add a new http_client.py with this wrapper all within synapse/synapse_rust/ but the native Python doesn't get picked up.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think calling make_deferred_yieldable from the Rust side (and therefore not having the wrapper) would be a reasonable alternative, but I also don't think this wrapper approach is too bad — at least it's extremely clear. (Maybe a bit easy to forget about using it?)

If you want a sketch of calling Python code from Rust, here's how I called _get_size_of for my caching fun, whilst only importing it once:

static GETSIZEOF: OnceLock<pyo3::Py<pyo3::PyAny>> = OnceLock::new();

fn get_size_of(py: Python<'_>, obj: &Bound<'_, PyAny>) -> u64 {
    let getsizeof = GETSIZEOF.get_or_init(|| {
        let sys = PyModule::import(py, "synapse.util.caches.lrucache").unwrap();
        let func = sys.getattr("_get_size_of").unwrap().unbind();
        func
    });

    let size: u64 = getsizeof.call1(py, (obj,)).unwrap().extract(py).unwrap();
    size
}

Copy link
Member

Choose a reason for hiding this comment

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

One thing I did when I did the pyo3-twisted bridge POC was to store and restore the contextvars during the future execution. If we switched to storing the logging context in contextvars, it would solve the logcontext being lost, but maybe not the resource usage metering. You can still probably take inspiration from that?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think calling make_deferred_yieldable from the Rust side (and therefore not having the wrapper) would be a reasonable alternative

This would be my choice if we can get it working.

(haven't tried yet since I'm trying to write a test to exercise this part of the code easily, #18903 (comment))

@MadLittleMods MadLittleMods marked this pull request as ready for review September 9, 2025 04:04
@MadLittleMods MadLittleMods requested a review from a team as a code owner September 9, 2025 04:04
Copy link
Contributor

@reivilibre reivilibre left a comment

Choose a reason for hiding this comment

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

OK to merge or otherwise would be happy to see the wrapperless approach if you have the energy for it

from synapse.types import ISynapseReactor


class HttpClient:
Copy link
Contributor

Choose a reason for hiding this comment

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

I think calling make_deferred_yieldable from the Rust side (and therefore not having the wrapper) would be a reasonable alternative, but I also don't think this wrapper approach is too bad — at least it's extremely clear. (Maybe a bit easy to forget about using it?)

If you want a sketch of calling Python code from Rust, here's how I called _get_size_of for my caching fun, whilst only importing it once:

static GETSIZEOF: OnceLock<pyo3::Py<pyo3::PyAny>> = OnceLock::new();

fn get_size_of(py: Python<'_>, obj: &Bound<'_, PyAny>) -> u64 {
    let getsizeof = GETSIZEOF.get_or_init(|| {
        let sys = PyModule::import(py, "synapse.util.caches.lrucache").unwrap();
        let func = sys.getattr("_get_size_of").unwrap().unbind();
        func
    });

    let size: u64 = getsizeof.call1(py, (obj,)).unwrap().extract(py).unwrap();
    size
}

Comment on lines +32 to +33
# This triggers the server startup hooks, which starts the Tokio thread pool
reactor.run()
Copy link
Contributor Author

@MadLittleMods MadLittleMods Sep 9, 2025

Choose a reason for hiding this comment

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

I'm trying to add a test for this but running into Tokio runtime is not running

$ SYNAPSE_TEST_LOG_LEVEL=INFO poetry run trial tests.synapse_rust.test_http_client
The "poetry.dev-dependencies" section is deprecated and will be removed in a future version. Use "poetry.group.dev.dependencies" instead.
tests.synapse_rust.test_http_client
  HttpClientTestCase
    test_logging_context ...                                             [FAIL]

===============================================================================
[FAIL]
Traceback (most recent call last):
  File "synapse/tests/synapse_rust/test_http_client.py", line 87, in test_logging_context
    self.get_success(self.till_deferred_has_result(asdf()))
  File "synapse/tests/unittest.py", line 693, in get_success
    return self.successResultOf(deferred)
  File "virtualenvs/matrix-synapse-xCtC9ulO-py3.13/lib/python3.13/site-packages/twisted/trial/_synctest.py", line 732, in successResultOf
    self.fail(
twisted.trial.unittest.FailTest: Success result expected on <Deferred at 0x7f928e6f5010 current result: None>, found failure result instead:
Traceback (most recent call last):
  File "virtualenvs/matrix-synapse-xCtC9ulO-py3.13/lib/python3.13/site-packages/twisted/internet/defer.py", line 1857, in _inlineCallbacks
    result = context.run(gen.send, result)
  File "synapse/tests/synapse_rust/test_http_client.py", line 82, in asdf
    await self._rust_http_client.get(
builtins.RuntimeError: Tokio runtime is not running


tests.synapse_rust.test_http_client.HttpClientTestCase.test_logging_context
-------------------------------------------------------------------------------
Ran 1 tests in 0.146s

FAILED (failures=1)

I can see that the tokio runtime is supposed to start when the Twisted reactor starts:

// Attach the runtime to the reactor, starting it when the reactor is
// running, stopping it when the reactor is shutting down
reactor.call_method1("callWhenRunning", (runtime.getattr("start")?,))?;

(introduced in #18691)

If I add in my own hook in the test itself hs.get_reactor().callWhenRunning(print, "asdf"), it prints.

My test is based off of the existing MasAuthDelegation tests but those actually seem to work. Perhaps because the Rust action is happening within the Synapse code instead of in the test code where there might be enough indirection delay for tokio to startup fully.

I can't figure out how to wait that little bit longer for tokio to startup.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@sandhose Any insight? 🙇

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants