Compare commits

...
Author SHA1 Message Date
Cyrille BéraudandCopilot ce8744862a proxy_client: fetch stored values when retrying a failed subscription
restartListeners() retries a push subscription that failed as a refresh
("refresh": true), and the proxy answers a refresh with no values. But a
failed subscription never received the values stored on the key: they
are only delivered if a later push happens to announce them.

On Android this can drop incoming calls. The account restarts while the
device is in doze, so its subscriptions fail for lack of network; the
connection request of the call is then stored on the key, and the retry
sent on the next connectivity change does not retrieve it. The call only
rings if the proxy pushes again before the caller gives up (dhtnet waits
30 s for an answer): in one reproduction the request was processed 9 s
after the call push, in another run the call failed.

Retry a failed subscription as a full SUBSCRIBE, so the proxy answers
with the values it stores. They go through the listener value cache like
any other, so a value already received is not delivered twice.

Also fix the comment of fillBody(), which described the opposite.

Co-authored-by: Copilot (Claude Opus 5.5) <223556219+Copilot@users.noreply.github.com>
2026-09-26 13:07:17 -04:00
Cyrille BéraudandCopilot d14759eadf tests: cover values missed by a failed push subscription
A push subscription that fails (typically while the device has no
network) is retried by restartListeners() on the next connectivity
change. The values stored on the key meanwhile were never received, so
the retry must ask the proxy for them, i.e. not be sent as a refresh.

Co-authored-by: Copilot (Claude Opus 5.5) <223556219+Copilot@users.noreply.github.com>
2026-09-26 13:06:45 -04:00
4 changed files with 56 additions and 5 deletions

No files matched your search

+3 -1
View File
@@ -437,8 +437,10 @@ private:
/**
* Refresh a listen via a token
* @param token
* @param fetchValues ask the server to send the values it stores, for a listener
* that may have missed some (its previous subscription failed).
*/
void resubscribe(const InfoHash& key, const size_t token, Listener& listener);
void resubscribe(const InfoHash& key, const size_t token, Listener& listener, bool fetchValues = false);
/**
* If we want to use push notifications by default.
+7 -4
View File
@@ -1213,7 +1213,9 @@ DhtProxyClient::restartListeners(const asio::error_code& ec)
for (auto& search : searches_)
for (auto& listener : search.second.listeners)
if (!listener.second.opstate->ok)
resubscribe(search.first, listener.first, listener.second);
// The subscription failed: values stored on the key since then were never
// received (e.g. a connection request sent while the device had no network).
resubscribe(search.first, listener.first, listener.second, true);
return;
}
if (logger_)
@@ -1376,7 +1378,8 @@ DhtProxyClient::pushNotificationReceived([[maybe_unused]] const std::map<std::st
void
DhtProxyClient::resubscribe([[maybe_unused]] const InfoHash& key,
[[maybe_unused]] const size_t token,
[[maybe_unused]] Listener& listener)
[[maybe_unused]] Listener& listener,
[[maybe_unused]] bool fetchValues)
{
#ifdef OPENDHT_PUSH_NOTIFICATIONS
if (deviceKey_.empty())
@@ -1398,7 +1401,7 @@ DhtProxyClient::resubscribe([[maybe_unused]] const InfoHash& key,
listener.refreshSubscriberTimer->async_wait(
std::bind(&DhtProxyClient::handleResubscribe, this, std::placeholders::_1, key, token, opstate));
auto vcb = listener.cb;
sendListen(header, vcb, opstate, listener, ListenMethod::RESUBSCRIBE);
sendListen(header, vcb, opstate, listener, fetchValues ? ListenMethod::SUBSCRIBE : ListenMethod::RESUBSCRIBE);
#endif
}
@@ -1423,7 +1426,7 @@ DhtProxyClient::fillBody(bool resubscribe)
Json::Value body;
getPushRequest(body);
if (resubscribe) {
// This is the first listen, we want to retrieve previous values.
// Refreshing a subscription: the server does not send the values it stores again.
body["refresh"] = true;
}
auto content = Json::writeString(jsonBuilder_, body) + "\n";
+44
View File
@@ -90,4 +90,48 @@ DhtProxyClientTester::testSetPushNotificationTokenResubscribesWithNewToken()
#endif
}
void
DhtProxyClientTester::testRestartListenersFetchesValuesAfterFailedSubscribe()
{
#ifdef OPENDHT_PUSH_NOTIFICATIONS
DhtProxyClient client({}, {}, [] {}, "http://127.0.0.1:8080", "OpenDHT-Test", "client-id", "push-token");
auto addListener = [&](const InfoHash& key, bool ok) -> DhtProxyClient::Listener& {
auto& search = client.searches_[key];
auto [it, inserted] = search.listeners.emplace(std::piecewise_construct,
std::forward_as_tuple(1),
std::forward_as_tuple(ValueCallback(
[](const std::vector<Sp<Value>>&, bool) { return true; })));
CPPUNIT_ASSERT(inserted);
auto& listener = it->second;
listener.opstate = std::make_shared<DhtProxyClient::OperationState>();
listener.opstate->ok = ok;
listener.cb = [](const std::vector<Sp<Value>>&, bool, system_clock::time_point) {
return true;
};
return listener;
};
// The subscription of this listener failed (e.g. no network): values stored on the
// key since then were never received.
auto failedKey = InfoHash::get("proxy-client-failed-subscribe");
auto& failed = addListener(failedKey, false);
auto healthyKey = InfoHash::get("proxy-client-healthy-subscribe");
auto& healthy = addListener(healthyKey, true);
client.restartListeners({});
CPPUNIT_ASSERT(failed.request);
CPPUNIT_ASSERT_EQUAL(std::string("/key/") + failedKey.toString(),
std::string(failed.request->header_.request_target()));
CPPUNIT_ASSERT(failed.request->body_.find("push-token") != std::string::npos);
// Without "refresh", the server answers with the values it stores for the key.
CPPUNIT_ASSERT(failed.request->body_.find("\"refresh\"") == std::string::npos);
CPPUNIT_ASSERT(!healthy.request);
#else
CPPUNIT_ASSERT(true);
#endif
}
} // namespace test
+2
View File
@@ -12,11 +12,13 @@ class DhtProxyClientTester : public CppUnit::TestFixture
CPPUNIT_TEST_SUITE(DhtProxyClientTester);
CPPUNIT_TEST(testResubscribeUsesKeyRoute);
CPPUNIT_TEST(testSetPushNotificationTokenResubscribesWithNewToken);
CPPUNIT_TEST(testRestartListenersFetchesValuesAfterFailedSubscribe);
CPPUNIT_TEST_SUITE_END();
public:
void testResubscribeUsesKeyRoute();
void testSetPushNotificationTokenResubscribesWithNewToken();
void testRestartListenersFetchesValuesAfterFailedSubscribe();
};
} // namespace test