Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RCORE-2060 Enabling 'cancel_waits_on_nonfatal_error' does not cancel waits during location update while offline #7528

Merged
merged 25 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2990263
added tests using cancel_waits_on_nonfatal_error and fix operation du…
Mar 27, 2024
b5d2dc5
Merge branch 'master' of github.com:realm/realm-core into mwb/fix-can…
Mar 27, 2024
c060daf
Updated changelog and updated comments/debug statements
Mar 27, 2024
91a54f1
fix swift build and test and tsan errors
Mar 27, 2024
5650107
Update from review
Mar 27, 2024
ea76f65
Fixed TSAN failures
Mar 27, 2024
0776dd8
Merge branch 'master' of github.com:realm/realm-core into mwb/fix-can…
Mar 27, 2024
7298787
Updates from review
Mar 27, 2024
5ac829b
Fixed lint failure
Mar 27, 2024
b3bb528
Updates from review
Mar 27, 2024
3bad95e
Merge branch 'master' of github.com:realm/realm-core into mwb/fix-can…
Mar 27, 2024
3c158a9
More updates from review
Mar 27, 2024
6e5f0a4
Additional updates from review
Mar 27, 2024
3f29c30
Added test to replicate swift autoopen feature
Apr 3, 2024
a6d55e9
Merge branch 'master' of github.com:realm/realm-core into mwb/fix-can…
Apr 3, 2024
1ef3639
Updated test
Apr 3, 2024
52a1d54
Merge branch 'master' of github.com:realm/realm-core into mwb/fix-can…
Apr 24, 2024
669d09c
Fixed swift build issue
Apr 25, 2024
b83a346
Merge branch 'master' of github.com:realm/realm-core into mwb/fix-can…
Apr 25, 2024
b2e7406
Fixed test failures
Apr 26, 2024
5830b46
Merge branch 'master' of github.com:realm/realm-core into mwb/fix-can…
Apr 26, 2024
c1a25c0
removed an unused function
Apr 26, 2024
dce8409
Merge branch 'master' of github.com:realm/realm-core into mwb/fix-can…
Apr 26, 2024
a34d5f6
Fixed debug message
Apr 26, 2024
2e0a68f
Updates from review
Apr 26, 2024
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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

### Fixed
* <How do the end-user experience this issue? what was the impact?> ([#????](https://github.com/realm/realm-core/issues/????), since v?.?.?)
* None.
* Enabling 'cancel_waits_on_nonfatal_error' does not cancel waits during location update while offline ([#7527](https://github.com/realm/realm-core/issues/7527), since v13.26.0)

### Breaking changes
* None.
Expand Down
5 changes: 4 additions & 1 deletion src/realm/exceptions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,12 @@ Exception::Exception(Status status)
{
}

Status exception_to_status() noexcept
Status exception_to_status(std::exception_ptr exc_ptr) noexcept
{
try {
if (exc_ptr) {
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't appear to be used anywhere.

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 was previously using this to get the exception thrown in the Async open start callback, but not anymore. Removed this unused code.

std::rethrow_exception(exc_ptr);
}
throw;
}
catch (const Exception& e) {
Expand Down
3 changes: 2 additions & 1 deletion src/realm/exceptions.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

#include <realm/status.hpp>

#include <exception>
#include <stdexcept>
#include <system_error>

Expand Down Expand Up @@ -49,7 +50,7 @@ class Exception : public std::exception {
*
* Currently this works for exceptions that derive from std::exception or Exception only.
*/
Status exception_to_status() noexcept;
Status exception_to_status(std::exception_ptr exc_ptr = nullptr) noexcept;


/// The UnsupportedFileFormatVersion exception is thrown by DB::open()
Expand Down
9 changes: 6 additions & 3 deletions src/realm/object-store/sync/app.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -823,7 +823,7 @@ void App::log_out(const std::shared_ptr<SyncUser>& user, UniqueFunction<void(Opt
return completion(util::none);
}

log_debug("App: log_out(%1)", user->user_profile().name());
log_debug("App: log_out(%1)", user->identity());
auto refresh_token = user->refresh_token();
user->log_out();

Expand Down Expand Up @@ -1255,15 +1255,18 @@ void App::refresh_access_token(const std::shared_ptr<SyncUser>& sync_user, bool
return;
}

log_debug("App: refresh_access_token: email: %1 %2", sync_user->user_profile().email(),
log_debug("App: refresh_access_token: email: %1 %2", sync_user->identity(),
update_location ? "(updating location)" : "");

// If update_location is set, force the location info to be updated before sending the request
do_request(
{HttpMethod::post, url_for_path("/auth/session"), m_request_timeout_ms,
get_request_headers(sync_user, RequestTokenType::RefreshToken)},
[completion = std::move(completion), sync_user](const Response& response) {
[self = shared_from_this(), completion = std::move(completion), sync_user](const Response& response) {
if (auto error = AppUtils::check_for_errors(response)) {
self->log_error("App: refresh_access_token: %1 -> %2 ERROR: %3", sync_user->identity(),
response.http_status_code, error->what());

return completion(std::move(error));
}

Expand Down
6 changes: 6 additions & 0 deletions src/realm/object-store/sync/sync_session.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -343,9 +343,15 @@ SyncSession::handle_refresh(const std::shared_ptr<SyncSession>& session, bool re
// internal backoff timer which will happen automatically so nothing needs to
// happen here.
util::CheckedUniqueLock lock(session->m_state_mutex);
// If updating access token while opening realm, just become active at this point
// and try to use the current access token.
if (session->m_state == State::WaitingForAccessToken) {
session->become_active();
}
// If `cancel_waits_on_nonfatal_error` is true, then cancel the waiters and pass along the error
else if (session->config(&SyncConfig::cancel_waits_on_nonfatal_error)) {
session->cancel_pending_waits(std::move(lock), error->to_status()); // unlocks the mutex
}
}
}
else {
Expand Down
96 changes: 96 additions & 0 deletions test/object-store/realm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@

#if REALM_ENABLE_SYNC
#include <util/sync/flx_sync_harness.hpp>
#include <util/sync/sync_test_utils.hpp>

#include <realm/object-store/sync/async_open_task.hpp>
#include <realm/object-store/sync/impl/sync_metadata.hpp>
Expand Down Expand Up @@ -1206,6 +1207,101 @@ TEST_CASE("Get Realm using Async Open", "[sync][pbs][async open]") {
REQUIRE(got_error);
}

SECTION("waiters are cancelled if cancel_waits_on_nonfatal_error") {
auto logger = util::Logger::get_default_logger();
auto transport = std::make_shared<HookedTransport<UnitTestTransport>>();
auto socket_provider = std::make_shared<HookedSocketProvider>(logger, "some user agent");
enum TestMode { location_fails, token_fails, token_not_authorized };

OfflineAppSession::Config oas_config;
oas_config.transport = transport;
oas_config.socket_provider = socket_provider;
OfflineAppSession oas(oas_config);

SyncTestFile config(oas, "realm");
auto user = config.sync_config->user;
config.sync_config->cancel_waits_on_nonfatal_error = true;
Copy link
Collaborator

Choose a reason for hiding this comment

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

should we add a test (maybe already have one?) when this is false?

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 don't think it's needed...
When this is false, the session will continue with retrying to connect to the server, so the test case expectations will be different. In addition, all the other aynsc open tests (as well as many others), validate the sync client operation with this flag set to false (the default).

Copy link
Collaborator

@danieltabacaru danieltabacaru Mar 27, 2024

Choose a reason for hiding this comment

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

yeah, what I meant is to test that behavior and expectations too (in a separate test not as a section). But I think you added a test in the previous PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, the tests added in the previous PR should test the normal cases; although I could add an additional test specifically for async open if you think that is necessary.

config.sync_config->error_handler = [&logger](std::shared_ptr<SyncSession> session, SyncError error) {
logger->debug("The sync error handler caught an error: '%1' for '%2'", error.status, session->path());
// Ignore connection failed non-fatal errors and check for access token refresh unauthorized fatal errors
if (error.status.code() == ErrorCodes::SyncConnectFailed) {
REQUIRE_FALSE(error.is_fatal);
return;
}
if (error.status.code() == ErrorCodes::AuthError) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

looking at the websocket replies, these are the only two errors allowed. you could just:

REQUIRE(error.status.code() == ErrorCodes::AuthError);
REQUIRE(error.is_fatal);

REQUIRE(error.is_fatal);
}
};

auto valid_token = user->access_token();
// User should be logged in at this point
michael-wb marked this conversation as resolved.
Show resolved Hide resolved
bool not_authorized = false;
bool token_refresh_called = false;
bool location_refresh_called = false;

TestMode mode = GENERATE(location_fails, token_fails, token_not_authorized);

SECTION("access token expired when realm is opened") {
michael-wb marked this conversation as resolved.
Show resolved Hide resolved
logger->trace(">>> access token expired at start - mode: %1", mode);
user->update_access_token(std::move(expired_token));
}
SECTION("access token expired when websocket connects") {
michael-wb marked this conversation as resolved.
Show resolved Hide resolved
logger->trace(">>> access token expired by websocket - mode: %1", mode);
not_authorized = true;
}
SECTION("access token expired when websocket connects") {
logger->trace(">>> websocket returns connection failed - mode: %1", mode);
}

transport->request_hook = [&](const app::Request& req) -> std::optional<app::Response> {
std::lock_guard<std::mutex> lock(mutex);
if (req.url.find("/auth/session") != std::string::npos) {
token_refresh_called = true;
if (mode == token_not_authorized) {
return app::Response{403, 0, {}, "403 not authorized"};
}
if (mode == token_fails) {
return app::Response{0, 28, {}, "Operation timed out"};
}
}
else if (req.url.find("/location") != std::string::npos) {
location_refresh_called = true;
if (mode == location_fails) {
// Fake "offline/request timed out" custom error response
return app::Response{0, 28, {}, "Operation timed out"};
Copy link
Contributor

Choose a reason for hiding this comment

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

Does the 28 have some special significance?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not really - it just corresponded with CURLE_OPERATION_TIMEDOUT (28), since there really isn't a default error returned when the operation times out and is more transport implementation specific.
https://curl.se/libcurl/c/libcurl-errors.html#:~:text=CURLE_OPERATION_TIMEDOUT

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 added a define for the value to try to explain why the value is being used.

}
}
return std::nullopt;
};

socket_provider->websocket_connect_func = [&]() -> std::optional<SocketProviderError> {
if (not_authorized) {
not_authorized = false; // one shot
return SocketProviderError(sync::websocket::WebSocketError::websocket_unauthorized,
"403 not authorized");
}
return SocketProviderError(sync::websocket::WebSocketError::websocket_connection_failed,
"Operation timed out");
};

auto task = Realm::get_synchronized_realm(config);
auto pf = util::make_promise_future<std::exception_ptr>();
task->start([&pf](auto ref, auto error) mutable {
REQUIRE(!ref);
REQUIRE(error);
pf.promise.emplace_value(error);
});

auto result = pf.future.get_no_throw();
REQUIRE(result.is_ok());
REQUIRE(result.get_value());
std::lock_guard<std::mutex> lock(mutex);
REQUIRE(location_refresh_called);
if (mode != TestMode::location_fails) {
REQUIRE(token_refresh_called);
}
}

SECTION("read-only mode sets the schema version") {
{
SharedRealm realm = Realm::get_shared_realm(config);
Expand Down
12 changes: 6 additions & 6 deletions test/object-store/sync/app.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2685,7 +2685,7 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") {
}
}
{
auto redir_transport = std::make_shared<HookedTransport>();
auto redir_transport = std::make_shared<HookedTransport<SynchronousTestTransport>>();
danieltabacaru marked this conversation as resolved.
Show resolved Hide resolved
AutoVerifiedEmailCredentials creds;

auto app_config = get_config(redir_transport, session.app_session());
Expand Down Expand Up @@ -2852,7 +2852,7 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") {
}
}
SECTION("Test app redirect with no metadata") {
auto redir_transport = std::make_shared<HookedTransport>();
auto redir_transport = std::make_shared<HookedTransport<SynchronousTestTransport>>();
AutoVerifiedEmailCredentials creds, creds2;

auto app_config = get_config(redir_transport, session.app_session());
Expand Down Expand Up @@ -2943,7 +2943,7 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") {
const std::string redirect_host = "fakerealm.example.com:9090";
const std::string redirect_url = "http://fakerealm.example.com:9090";

auto redir_transport = std::make_shared<HookedTransport>();
auto redir_transport = std::make_shared<HookedTransport<SynchronousTestTransport>>();
auto redir_provider = std::make_shared<HookedSocketProvider>(logger, "");
redir_provider->websocket_endpoint_resolver = [&](sync::WebSocketEndpoint&& ep) {
ep.address = original_address;
Expand Down Expand Up @@ -3164,7 +3164,7 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") {
original_address = original_host.substr(0, port_pos);
}

auto redir_transport = std::make_shared<HookedTransport>();
auto redir_transport = std::make_shared<HookedTransport<SynchronousTestTransport>>();
auto redir_provider = std::make_shared<HookedSocketProvider>(logger, "");
redir_provider->websocket_endpoint_resolver = [&](sync::WebSocketEndpoint&& ep) {
ep.address = original_address;
Expand Down Expand Up @@ -3262,7 +3262,7 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") {
REQUIRE(get_dogs(r).size() == 1);
}

auto transport = std::make_shared<HookedTransport>();
auto transport = std::make_shared<HookedTransport<SynchronousTestTransport>>();
TestAppSession hooked_session(session.app_session(), transport, DeleteApp{false});
auto app = hooked_session.app();
std::shared_ptr<SyncUser> user = app->current_user();
Expand Down Expand Up @@ -3321,7 +3321,7 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") {
REQUIRE(token.expired(now));
}

auto transport = std::make_shared<HookedTransport>();
auto transport = std::make_shared<HookedTransport<SynchronousTestTransport>>();
TestAppSession hooked_session(session.app_session(), transport, DeleteApp{false});
auto app = hooked_session.app();
std::shared_ptr<SyncUser> user = app->current_user();
Expand Down
2 changes: 1 addition & 1 deletion test/object-store/sync/flx_sync.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2678,7 +2678,7 @@ TEST_CASE("flx: connect to PBS as FLX returns an error", "[sync][flx][protocol][
}

TEST_CASE("flx: commit subscription while refreshing the access token", "[sync][flx][token][baas]") {
auto transport = std::make_shared<HookedTransport>();
auto transport = std::make_shared<HookedTransport<SynchronousTestTransport>>();
FLXSyncTestHarness harness("flx_wait_access_token2", FLXSyncTestHarness::default_server_schema(), transport);
auto app = harness.app();
std::shared_ptr<SyncUser> user = app->current_user();
Expand Down
5 changes: 4 additions & 1 deletion test/object-store/util/sync/baas_admin_api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -346,8 +346,11 @@ app::Response do_http_request(const app::Request& request)

auto logger = util::Logger::get_default_logger();
if (response_code != CURLE_OK) {
std::string message = curl_easy_strerror(response_code);
logger->error("curl_easy_perform() failed when sending request to '%1' with body '%2': %3", request.url,
request.body, curl_easy_strerror(response_code));
request.body, message);
// Return a failing response with the CURL error as the custom code
return {0, response_code, {}, message};
}
if (logger->would_log(util::Logger::Level::trace)) {
std::string coid = [&] {
Expand Down
10 changes: 8 additions & 2 deletions test/object-store/util/sync/sync_test_utils.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,14 @@ class SynchronousTestTransport : public app::GenericNetworkTransport {
};


class HookedTransport : public SynchronousTestTransport {
// Converted to a templated class to allow creating against the UnitTestTransport or
// SynchronousTestTransport (or other custom) GenericNetworkTransport base class.
template <typename Parent>
class HookedTransport : public Parent {
public:
static_assert(std::is_base_of<app::GenericNetworkTransport, Parent>::value,
"HookedTransport must be derived from a class whose parent is app::GenericNetworkTransport");

void send_request_to_server(const app::Request& request,
util::UniqueFunction<void(const app::Response&)>&& completion) override
{
Expand All @@ -204,7 +210,7 @@ class HookedTransport : public SynchronousTestTransport {
return completion(*simulated_response);
}
}
SynchronousTestTransport::send_request_to_server(request, [&](const app::Response& response) mutable {
Parent::send_request_to_server(request, [&](const app::Response& response) mutable {
if (response_hook) {
response_hook(request, response);
}
Expand Down
Loading