Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/18887.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix `run_as_background_process` messing with the caller logging context.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

No fix has been added yet. Only tests to reproduce the problem so far

101 changes: 99 additions & 2 deletions tests/util/test_logcontext.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@
SENTINEL_CONTEXT,
LoggingContext,
PreserveLoggingContext,
_Sentinel,
current_context,
make_deferred_yieldable,
nested_logging_context,
run_in_background,
)
from synapse.metrics.background_process_metrics import run_as_background_process
from synapse.types import ISynapseReactor
from synapse.util import Clock

Expand All @@ -44,8 +46,12 @@
class LoggingContextTestCase(unittest.TestCase):
def _check_test_key(self, value: str) -> None:
context = current_context()
assert isinstance(context, LoggingContext)
self.assertEqual(context.name, value)
assert isinstance(context, LoggingContext) or isinstance(context, _Sentinel), (
f"Expected LoggingContext({value}) but saw {context}"
)
self.assertEqual(
str(context), value, f"Expected LoggingContext({value}) but saw {context}"
)

def test_with_context(self) -> None:
with LoggingContext("test"):
Expand Down Expand Up @@ -187,6 +193,97 @@ def test_nested_logging_context(self) -> None:
nested_context = nested_logging_context(suffix="bar")
self.assertEqual(nested_context.name, "foo-bar")

async def test_run_as_background_process(self) -> None:
"""
Test that using `run_as_background_process` doesn't mess with the logging
context of the caller.
"""
Comment on lines +196 to +200
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 sanity check myself, would you also expect these tests to pass in an ideal world?

We currently fallback to the sentinel context at the end within the main block.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

I'd expect the logcontexts to be messed up by await bg_process_d.

run_as_background_process takes an awaitable that follows the logcontext rules and makes it so that you can safely not await on them. This means that the returned deferred doesn't actually follow the logcontext rules itself, and so if you then want to await on the deferred again you must use make_deferred_awaitable (or similar) to make it follow the rules again

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ahh, totally makes sense why it's broken in this test case! I jumped the gun on thinking I reproduced the problem here.

In terms of where this PR is spawning from with the problems in #18870; from a pairing session with @erikjohnston, the main thing I was banging my head against was that I removed PreserveLoggingContext at the following spot in order to maintain the main logging context further into Synapse starting up and running. But this also meant leaking the main logging context into the Twisted reactor event loop and apply to the next task which appeared like the desired result because the next task was start but also meant it got messed up later down the line.

with PreserveLoggingContext():
if daemonize:
assert pid_file is not None
if print_pidfile:
print(pid_file)
daemonize_process(pid_file, logger)
run()

While I haven't explored this thread to its' end, I think this is the key insight into why I was chasing ghosts.

See #18870 (comment) for the other discussion around this.

Thank you @erikjohnston for hopping on a call 🙇


async def test_func() -> None:
# We should see the context from the background process throughout this
# entire function
self._check_test_key("bg_process-0")
# Yield to the reactor to ensure that the context is preserved
await Clock(reactor).sleep(0)
# Context should still be the same
self._check_test_key("bg_process-0")

# Check that we start in the sentinel context
self._check_test_key("sentinel")

with LoggingContext("main"):
# We're in the main context block now
self._check_test_key("main")

# Kick off the background process
bg_process_d = run_as_background_process(
"bg_process",
server_name="test_server",
func=test_func,
)

# Doing more work here in the mean-time should be in the main context
self._check_test_key("main")

# Wait for callback_completed
await bg_process_d

# We should still be in the main context
self._check_test_key("main")

async def test_run_as_background_process_make_deferred_yieldable(
self,
) -> None:
"""
Test that using `run_as_background_process` with a background task that utilizes
`make_deferred_yieldable` doesn't mess with the logging context of the caller.
"""

async def test_func() -> None:
# We should see the context from the background process throughout this
# entire function
self._check_test_key("bg_process-0")

# A function which returns an incomplete deferred, but doesn't follow the
# synapse rules.
def blocking_function() -> defer.Deferred:
# This was run in the background process, so we should still see
# the background process context here.
self._check_test_key("bg_process-0")

d: defer.Deferred = defer.Deferred()
reactor.callLater(0, d.callback, None)
return d

# Use `make_deferred_yieldable` to make the deferred follow the Synapse rules.
await make_deferred_yieldable(blocking_function())

# Context should still be the same
self._check_test_key("bg_process-0")

# Check that we start in the sentinel context
self._check_test_key("sentinel")

with LoggingContext("main"):
# We're in the main context block now
self._check_test_key("main")

# Kick off the background process
bg_process_d = run_as_background_process(
"bg_process",
server_name="test_server",
func=test_func,
)

# Doing more work here in the mean-time should be in the main context
self._check_test_key("main")

# Wait for callback_completed
await bg_process_d

# We should still be in the main context
self._check_test_key("main")


# a function which returns a deferred which has been "called", but
# which had a function which returned another incomplete deferred on
Expand Down
Loading