-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Fix #8518 (sync requests being cached wrongly on timeout) #9358
Changes from all commits
406c8f1
bbad98c
e8296ff
3b08294
b239033
04c399d
ed39223
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Added a fix that invalidates cache for empty timed-out sync responses. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -277,8 +277,9 @@ async def wait_for_sync_for_user( | |
| user_id = sync_config.user.to_string() | ||
| await self.auth.check_auth_blocking(requester=requester) | ||
|
|
||
| res = await self.response_cache.wrap( | ||
| res = await self.response_cache.wrap_conditional( | ||
| sync_config.request_key, | ||
| lambda result: since_token != result.next_batch, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this should have had a comment. Why does doing this give the right behaviour? (I shouldn't have to go and find the PR that added it, and even having done so it's hard to correlate the description to the code.)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It was described in the PR description, but i'll take it in another PR to add a comment here, i.e. "small fixes for
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
yeah as I said: having to go and find the PR and grok it is a pain.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alright, i'll add a comment describing what that conditional does 👍 |
||
| self._wait_for_sync_for_user, | ||
| sync_config, | ||
| since_token, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,7 +13,7 @@ | |
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
| import logging | ||
| from typing import TYPE_CHECKING, Any, Callable, Dict, Generic, Optional, TypeVar | ||
| from typing import TYPE_CHECKING, Any, Callable, Dict, Generic, Optional, Set, TypeVar | ||
|
|
||
| from twisted.internet import defer | ||
|
|
||
|
|
@@ -40,6 +40,7 @@ class ResponseCache(Generic[T]): | |
| def __init__(self, hs: "HomeServer", name: str, timeout_ms: float = 0): | ||
| # Requests that haven't finished yet. | ||
| self.pending_result_cache = {} # type: Dict[T, ObservableDeferred] | ||
| self.pending_conditionals = {} # type: Dict[T, Set[Callable[[Any], bool]]] | ||
|
|
||
| self.clock = hs.get_clock() | ||
| self.timeout_sec = timeout_ms / 1000.0 | ||
|
|
@@ -101,7 +102,11 @@ def set(self, key: T, deferred: defer.Deferred) -> defer.Deferred: | |
| self.pending_result_cache[key] = result | ||
|
|
||
| def remove(r): | ||
| if self.timeout_sec: | ||
| should_cache = all( | ||
| func(r) for func in self.pending_conditionals.pop(key, []) | ||
| ) | ||
|
|
||
| if self.timeout_sec and should_cache: | ||
| self.clock.call_later( | ||
| self.timeout_sec, self.pending_result_cache.pop, key, None | ||
| ) | ||
|
|
@@ -112,6 +117,31 @@ def remove(r): | |
| result.addBoth(remove) | ||
| return result.observe() | ||
|
|
||
| def add_conditional(self, key: T, conditional: Callable[[Any], bool]): | ||
| self.pending_conditionals.setdefault(key, set()).add(conditional) | ||
|
Comment on lines
+120
to
+121
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this seems like a dangerous thing to add to the public interface. It seems like it would be very easy to use in racy way.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should i signal that
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. since it's only called in one place, I'd just inline it. |
||
|
|
||
| def wrap_conditional( | ||
| self, | ||
| key: T, | ||
| should_cache: Callable[[Any], bool], | ||
| callback: "Callable[..., Any]", | ||
| *args: Any, | ||
| **kwargs: Any | ||
| ) -> defer.Deferred: | ||
| """The same as wrap(), but adds a conditional to the final execution. | ||
|
|
||
| When the final execution completes, *all* conditionals need to return True for it to properly cache, | ||
| else it'll not be cached in a timed fashion. | ||
| """ | ||
|
|
||
| # See if there's already a result on this key that hasn't yet completed. Due to the single-threaded nature of | ||
| # python, adding a key immediately in the same execution thread will not cause a race condition. | ||
| result = self.get(key) | ||
| if not result or isinstance(result, defer.Deferred) and not result.called: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is correct, but I had to go and look up the precedence of |
||
| self.add_conditional(key, should_cache) | ||
|
Comment on lines
+140
to
+141
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is it correct that the conditional is added when
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. operator precedence is (not result) or (isinstance(result, defer.Deferred) and (not result.called))I had to check after your previous comment, it could actually be Yes, the conditional is added only to be executed after the result is called, i do agree that now i realise that any future calls can be like "hey, this shouldn't be cached anyways because the returned value doesn't agree with what i already got from somewhere else", in which case I need to rewrite this to execute the conditional locally whenever it's called, and evict the cache when it's not valid according to the new conditional. (the (this last approach (letting
Oh, this might actually be a problem, if the function (with the same key) hasn't returned yet on the second
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. as discussed at considerable length in the room: I don't agree with your logic here. I think that supporting multiple
This comment was marked as outdated.
Sorry, something went wrong.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alright, i'll just make it a simple
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, more discussion in #9522 on this.
Comment on lines
+139
to
+141
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel like this is duplicating half the logic of
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I initially did that, but that became a bit of a mess, and you'd had to shift around the positional arguments on any I also didn't wanna make it a keyword argument, because that'd shadow any potential keyword arguments to the inner call. While i know it is trivial to "just add it", i didn't want to because it is non-explicit to anyone not aware of this change to
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I'm not suggesting changing the signature of Wrap would be: def wrap(
self, key: T, callback: "Callable[..., Any]", *args: Any, **kwargs: Any
) -> defer.Deferred:
return self.wrap_conditional(key, None, callback, *args, **kwargs)why is that a mess?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmmm, i'll change it to that in that PR, then 👍 |
||
|
|
||
| return self.wrap(key, callback, *args, **kwargs) | ||
|
|
||
| def wrap( | ||
| self, key: T, callback: "Callable[..., Any]", *args: Any, **kwargs: Any | ||
| ) -> defer.Deferred: | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.