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 23 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
* Non-streaming download sync progress notification is fixed for flexible sync Realms where before it was sometimes stopping to emit values right after the registration of the callback (PR [#7561](https://github.com/realm/realm-core/issues/7561)).
* Schema initialization could hit an assertion failure if the sync client applied a downloaded changeset while the Realm file was in the process of being opened ([#7041](https://github.com/realm/realm-core/issues/7041), since v11.4.0).
* Queries using query paths on Mixed values returns inconsistent results ([#7587](https://github.com/realm/realm-core/issues/7587), since v14.0.0)
* 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
* The following things have been renamed or moved as part of moving all of the App Services functionality to the app namespace:
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
8 changes: 6 additions & 2 deletions src/realm/object-store/sync/app.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,7 @@ void App::log_out(const std::shared_ptr<User>& user, SyncUser::State new_state,
return;
}

log_debug("App: log_out(%1)", user->user_id());
auto request =
make_request(HttpMethod::del, url_for_path("/auth/session"), user, RequestTokenType::RefreshToken, "");

Expand Down Expand Up @@ -1284,14 +1285,17 @@ void App::refresh_access_token(const std::shared_ptr<User>& user, bool update_lo
return;
}

log_debug("App: refresh_access_token: email: %1 %2", user->user_profile().email(),
update_location ? "(updating location)" : "");
log_debug("App: refresh_access_token: user-id: %1%2", user->user_id(),
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: user_id

update_location ? " (updating location)" : "");

// If update_location is set, force the location info to be updated before sending the request
do_request(
make_request(HttpMethod::post, url_for_path("/auth/session"), user, RequestTokenType::RefreshToken, ""),
[completion = std::move(completion), self = shared_from_this(), user](auto&&, const Response& response) {
if (auto error = AppUtils::check_for_errors(response)) {
self->log_error("App: refresh_access_token: %1 -> %2 ERROR: %3", user->user_id(),
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 @@ -340,9 +340,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
256 changes: 254 additions & 2 deletions test/object-store/realm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,19 @@

#if REALM_ENABLE_SYNC
#include <util/sync/flx_sync_harness.hpp>
#include <util/sync/sync_test_utils.hpp>
#include <util/test_file.hpp>
#ifdef REALM_ENABLE_AUTH_TESTS
#include <util/sync/baas_admin_api.hpp>
#endif // REALM_ENABLE_AUTH_TESTS

#include <realm/object-store/sync/async_open_task.hpp>
#include <realm/object-store/sync/impl/app_metadata.hpp>
#include <realm/object-store/sync/sync_session.hpp>

#include <realm/sync/noinst/client_history_impl.hpp>
#include <realm/sync/subscriptions.hpp>
#endif
#endif // REALM_ENABLE_SYNC

#include <catch2/catch_all.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
Expand All @@ -62,7 +67,7 @@
#include <array>
#if REALM_HAVE_UV
#include <uv.h>
#endif
#endif // REALM_HAVE_UV

namespace realm {
class TestHelper {
Expand Down Expand Up @@ -1215,6 +1220,148 @@ TEST_CASE("Get Realm using Async Open", "[sync][pbs][async open]") {
REQUIRE(got_error);
}

#if REALM_APP_SERVICES

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 { expired_at_start, expired_by_websocket, websocket_fails };
enum FailureMode { location_fails, token_fails, token_not_authorized };
auto txt_test_mode = [](TestMode mode) {
switch (mode) {
case TestMode::expired_at_start:
return "access token expired when realm is opened";
case TestMode::expired_by_websocket:
return "access token expired by websocket";
case TestMode::websocket_fails:
return "websocket returns connection failed";
default:
return "Unknown TestMode";
}
};
auto txt_failure_mode = [](FailureMode mode) {
switch (mode) {
case FailureMode::location_fails:
return "location update fails";
case FailureMode::token_fails:
return "access token refresh fails";
case FailureMode::token_not_authorized:
return "websocket connect not authorized";
default:
return "Unknown FailureMode";
}
};

app::AppConfig app_config;
set_app_config_defaults(app_config, transport);
app_config.sync_client_config.socket_provider = socket_provider;
app_config.base_file_path = util::make_temp_dir();
app_config.metadata_mode = app::AppConfig::MetadataMode::NoEncryption;

auto the_app = app::App::get_app(app::App::CacheMode::Disabled, app_config);
create_user_and_log_in(the_app);
auto user = the_app->current_user();
// User should be logged in at this point
michael-wb marked this conversation as resolved.
Show resolved Hide resolved
REQUIRE(user->is_logged_in());

bool not_authorized = false;
bool token_refresh_called = false;
bool location_refresh_called = false;

TestMode test_mode = GENERATE(expired_at_start, expired_by_websocket, websocket_fails);
FailureMode failure = GENERATE(location_fails, token_fails, token_not_authorized);

DYNAMIC_SECTION(txt_test_mode(test_mode) << " - " << txt_failure_mode(failure)) {
logger->info("TEST: %1 - %2", txt_test_mode(test_mode), txt_failure_mode(failure));
if (test_mode == TestMode::expired_at_start) {
// invalidate the user's cached access token
auto app_user = the_app->current_user();
app_user->update_data_for_testing([&](app::UserData& data) {
data.access_token = RealmJWT(expired_token);
});
}
else if (test_mode == TestMode::expired_by_websocket) {
// tell websocket to return not authorized to refresh access token
not_authorized = true;
}
}

the_app.reset();

auto err_handler = [](std::shared_ptr<SyncSession> session, SyncError error) {
auto logger = util::Logger::get_default_logger();
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 it's not SyncConnectFailed, then it should be AuthError
REQUIRE(error.status.code() == ErrorCodes::AuthError);
REQUIRE(error.is_fatal);
};

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

the_app = app::App::get_app(app::App::CacheMode::Disabled, app_config);
SyncTestFile config(the_app->current_user(), "realm");
config.sync_config->cancel_waits_on_nonfatal_error = true;
config.sync_config->error_handler = err_handler;

// User should be logged in at this point
REQUIRE(config.sync_config->user->is_logged_in());

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 (failure != FailureMode::location_fails) {
REQUIRE(token_refresh_called);
}
}

#endif // REALM_APP_SERVICES

SECTION("read-only mode sets the schema version") {
{
SharedRealm realm = Realm::get_shared_realm(config);
Expand Down Expand Up @@ -1348,6 +1495,111 @@ TEST_CASE("Get Realm using Async Open", "[sync][pbs][async open]") {
}
}

#if REALM_ENABLE_AUTH_TESTS
#if REALM_APP_SERVICES
Copy link
Member

Choose a reason for hiding this comment

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

REALM_ENABLE_AUTH_TESTS implies REALM_APP_SERVICES so they don't both need to be checked.


TEST_CASE("Syhcnronized realm: AutoOpen", "[sync][baas][pbs][async open]") {
const auto partition = random_string(100);
auto schema = get_default_schema();
enum TestMode { expired_at_start, expired_by_websocket, websocket_fails };
enum FailureMode { location_fails, token_fails, token_not_authorized };

auto logger = util::Logger::get_default_logger();
auto transport = std::make_shared<HookedTransport<>>();
auto socket_provider = std::make_shared<HookedSocketProvider>(logger, "some user agent");
std::mutex mutex;

// Create the app session and get the logged in user identity
auto server_app_config = minimal_app_config("autoopen-realm", schema);
TestAppSession session(create_app(server_app_config), transport, DeleteApp{true}, realm::ReconnectMode::normal,
socket_provider);
auto user = session.app()->current_user();
std::string identity = user->user_id();
REQUIRE(user->is_logged_in());
REQUIRE(!identity.empty());
// Reopen the App instance and retrieve the cached user
session.reopen(false);
user = session.app()->get_existing_logged_in_user(identity);

SyncTestFile config(user, partition, schema);
config.sync_config->cancel_waits_on_nonfatal_error = true;
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 it's not SyncConnectFailed, then it should be AuthError
REQUIRE(error.status.code() == ErrorCodes::AuthError);
REQUIRE(error.is_fatal);
};

bool not_authorized = false;
bool token_refresh_called = false;
bool location_refresh_called = false;

FailureMode failure = FailureMode::location_fails;

transport->request_hook = [&](const app::Request& req) -> std::optional<app::Response> {
static constexpr int CURLE_OPERATION_TIMEDOUT = 28;
std::lock_guard<std::mutex> lock(mutex);
if (req.url.find("/auth/session") != std::string::npos) {
token_refresh_called = true;
if (failure == FailureMode::token_not_authorized) {
return app::Response{403, 0, {}, "403 not authorized"};
}
if (failure == FailureMode::token_fails) {
return app::Response{0, CURLE_OPERATION_TIMEDOUT, {}, "Operation timed out"};
}
}
else if (req.url.find("/location") != std::string::npos) {
location_refresh_called = true;
if (failure == FailureMode::location_fails) {
// Fake "offline/request timed out" custom error response
return app::Response{0, CURLE_OPERATION_TIMEDOUT, {}, "Operation timed out"};
}
}
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 (failure != FailureMode::location_fails) {
REQUIRE(token_refresh_called);
}
}

transport->request_hook = nullptr;
socket_provider->websocket_connect_func = nullptr;
auto r = Realm::get_shared_realm(config);
wait_for_download(*r);
}

#endif // REALM_APP_SERVICES
#endif // REALM_ENABLE_AUTH_TESTS

TEST_CASE("SharedRealm: convert", "[sync][pbs][convert]") {
TestSyncManager tsm;
ObjectSchema object_schema = {"object",
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
Loading
Loading