Skip to content

Commit 90c72b6

Browse files
author
Michael Wilkerson-Barker
committed
RCORE-2060 Enabling 'cancel_waits_on_nonfatal_error' does not cancel waits during location update while offline (#7528)
* added tests using cancel_waits_on_nonfatal_error and fix operation during location update * Updated changelog and updated comments/debug statements * fix swift build and test and tsan errors * Added test to replicate swift autoopen feature * Fixed swift build issue * removed an unused function * Updates from review
1 parent 4ecb28f commit 90c72b6

File tree

7 files changed

+328
-13
lines changed

7 files changed

+328
-13
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
* 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)).
2121
* 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).
2222
* Queries using query paths on Mixed values returns inconsistent results ([#7587](https://github.com/realm/realm-core/issues/7587), since v14.0.0)
23+
* 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)
2324

2425
### Breaking changes
2526
* The following things have been renamed or moved as part of moving all of the App Services functionality to the app namespace:

src/realm/object-store/sync/app.cpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -842,6 +842,7 @@ void App::log_out(const std::shared_ptr<User>& user, SyncUser::State new_state,
842842
return;
843843
}
844844

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

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

1287-
log_debug("App: refresh_access_token: email: %1 %2", user->user_profile().email(),
1288-
update_location ? "(updating location)" : "");
1288+
log_debug("App: refresh_access_token: user_id: %1%2", user->user_id(),
1289+
update_location ? " (updating location)" : "");
12891290

12901291
// If update_location is set, force the location info to be updated before sending the request
12911292
do_request(
12921293
make_request(HttpMethod::post, url_for_path("/auth/session"), user, RequestTokenType::RefreshToken, ""),
12931294
[completion = std::move(completion), self = shared_from_this(), user](auto&&, const Response& response) {
12941295
if (auto error = AppUtils::check_for_errors(response)) {
1296+
self->log_error("App: refresh_access_token: %1 -> %2 ERROR: %3", user->user_id(),
1297+
response.http_status_code, error->what());
1298+
12951299
return completion(std::move(error));
12961300
}
12971301

src/realm/object-store/sync/sync_session.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,9 +340,15 @@ SyncSession::handle_refresh(const std::shared_ptr<SyncSession>& session, bool re
340340
// internal backoff timer which will happen automatically so nothing needs to
341341
// happen here.
342342
util::CheckedUniqueLock lock(session->m_state_mutex);
343+
// If updating access token while opening realm, just become active at this point
344+
// and try to use the current access token.
343345
if (session->m_state == State::WaitingForAccessToken) {
344346
session->become_active();
345347
}
348+
// If `cancel_waits_on_nonfatal_error` is true, then cancel the waiters and pass along the error
349+
else if (session->config(&SyncConfig::cancel_waits_on_nonfatal_error)) {
350+
session->cancel_pending_waits(std::move(lock), error->to_status()); // unlocks the mutex
351+
}
346352
}
347353
}
348354
else {

test/object-store/realm.cpp

Lines changed: 252 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,19 @@
4545

4646
#if REALM_ENABLE_SYNC
4747
#include <util/sync/flx_sync_harness.hpp>
48+
#include <util/sync/sync_test_utils.hpp>
49+
#include <util/test_file.hpp>
50+
#ifdef REALM_ENABLE_AUTH_TESTS
51+
#include <util/sync/baas_admin_api.hpp>
52+
#endif // REALM_ENABLE_AUTH_TESTS
4853

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

5358
#include <realm/sync/noinst/client_history_impl.hpp>
5459
#include <realm/sync/subscriptions.hpp>
55-
#endif
60+
#endif // REALM_ENABLE_SYNC
5661

5762
#include <catch2/catch_all.hpp>
5863
#include <catch2/matchers/catch_matchers_string.hpp>
@@ -62,7 +67,7 @@
6267
#include <array>
6368
#if REALM_HAVE_UV
6469
#include <uv.h>
65-
#endif
70+
#endif // REALM_HAVE_UV
6671

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

1223+
#if REALM_APP_SERVICES
1224+
1225+
SECTION("waiters are cancelled if cancel_waits_on_nonfatal_error") {
1226+
auto logger = util::Logger::get_default_logger();
1227+
auto transport = std::make_shared<HookedTransport<UnitTestTransport>>();
1228+
auto socket_provider = std::make_shared<HookedSocketProvider>(logger, "some user agent");
1229+
enum TestMode { expired_at_start, expired_by_websocket, websocket_fails };
1230+
enum FailureMode { location_fails, token_fails, token_not_authorized };
1231+
auto txt_test_mode = [](TestMode mode) {
1232+
switch (mode) {
1233+
case TestMode::expired_at_start:
1234+
return "access token expired when realm is opened";
1235+
case TestMode::expired_by_websocket:
1236+
return "access token expired by websocket";
1237+
case TestMode::websocket_fails:
1238+
return "websocket returns connection failed";
1239+
default:
1240+
return "Unknown TestMode";
1241+
}
1242+
};
1243+
auto txt_failure_mode = [](FailureMode mode) {
1244+
switch (mode) {
1245+
case FailureMode::location_fails:
1246+
return "location update fails";
1247+
case FailureMode::token_fails:
1248+
return "access token refresh fails";
1249+
case FailureMode::token_not_authorized:
1250+
return "websocket connect not authorized";
1251+
default:
1252+
return "Unknown FailureMode";
1253+
}
1254+
};
1255+
1256+
app::AppConfig app_config;
1257+
set_app_config_defaults(app_config, transport);
1258+
app_config.sync_client_config.socket_provider = socket_provider;
1259+
app_config.base_file_path = util::make_temp_dir();
1260+
app_config.metadata_mode = app::AppConfig::MetadataMode::NoEncryption;
1261+
1262+
auto the_app = app::App::get_app(app::App::CacheMode::Disabled, app_config);
1263+
create_user_and_log_in(the_app);
1264+
auto user = the_app->current_user();
1265+
// User should be logged in at this point
1266+
REQUIRE(user->is_logged_in());
1267+
1268+
bool not_authorized = false;
1269+
bool token_refresh_called = false;
1270+
bool location_refresh_called = false;
1271+
1272+
TestMode test_mode = GENERATE(expired_at_start, expired_by_websocket, websocket_fails);
1273+
FailureMode failure = GENERATE(location_fails, token_fails, token_not_authorized);
1274+
1275+
DYNAMIC_SECTION(txt_test_mode(test_mode) << " - " << txt_failure_mode(failure)) {
1276+
logger->info("TEST: %1 - %2", txt_test_mode(test_mode), txt_failure_mode(failure));
1277+
if (test_mode == TestMode::expired_at_start) {
1278+
// invalidate the user's cached access token
1279+
auto app_user = the_app->current_user();
1280+
app_user->update_data_for_testing([&](app::UserData& data) {
1281+
data.access_token = RealmJWT(expired_token);
1282+
});
1283+
}
1284+
else if (test_mode == TestMode::expired_by_websocket) {
1285+
// tell websocket to return not authorized to refresh access token
1286+
not_authorized = true;
1287+
}
1288+
}
1289+
1290+
the_app.reset();
1291+
1292+
auto err_handler = [](std::shared_ptr<SyncSession> session, SyncError error) {
1293+
auto logger = util::Logger::get_default_logger();
1294+
logger->debug("The sync error handler caught an error: '%1' for '%2'", error.status, session->path());
1295+
// Ignore connection failed non-fatal errors and check for access token refresh unauthorized fatal errors
1296+
if (error.status.code() == ErrorCodes::SyncConnectFailed) {
1297+
REQUIRE_FALSE(error.is_fatal);
1298+
return;
1299+
}
1300+
// If it's not SyncConnectFailed, then it should be AuthError
1301+
REQUIRE(error.status.code() == ErrorCodes::AuthError);
1302+
REQUIRE(error.is_fatal);
1303+
};
1304+
1305+
transport->request_hook = [&](const app::Request& req) -> std::optional<app::Response> {
1306+
static constexpr int CURLE_OPERATION_TIMEDOUT = 28;
1307+
std::lock_guard<std::mutex> lock(mutex);
1308+
if (req.url.find("/auth/session") != std::string::npos) {
1309+
token_refresh_called = true;
1310+
if (failure == FailureMode::token_not_authorized) {
1311+
return app::Response{403, 0, {}, "403 not authorized"};
1312+
}
1313+
if (failure == FailureMode::token_fails) {
1314+
return app::Response{0, CURLE_OPERATION_TIMEDOUT, {}, "Operation timed out"};
1315+
}
1316+
}
1317+
else if (req.url.find("/location") != std::string::npos) {
1318+
location_refresh_called = true;
1319+
if (failure == FailureMode::location_fails) {
1320+
// Fake "offline/request timed out" custom error response
1321+
return app::Response{0, CURLE_OPERATION_TIMEDOUT, {}, "Operation timed out"};
1322+
}
1323+
}
1324+
return std::nullopt;
1325+
};
1326+
1327+
socket_provider->websocket_connect_func = [&]() -> std::optional<SocketProviderError> {
1328+
if (not_authorized) {
1329+
not_authorized = false; // one shot
1330+
return SocketProviderError(sync::websocket::WebSocketError::websocket_unauthorized,
1331+
"403 not authorized");
1332+
}
1333+
return SocketProviderError(sync::websocket::WebSocketError::websocket_connection_failed,
1334+
"Operation timed out");
1335+
};
1336+
1337+
the_app = app::App::get_app(app::App::CacheMode::Disabled, app_config);
1338+
SyncTestFile config(the_app->current_user(), "realm");
1339+
config.sync_config->cancel_waits_on_nonfatal_error = true;
1340+
config.sync_config->error_handler = err_handler;
1341+
1342+
// User should be logged in at this point
1343+
REQUIRE(config.sync_config->user->is_logged_in());
1344+
1345+
auto task = Realm::get_synchronized_realm(config);
1346+
auto pf = util::make_promise_future<std::exception_ptr>();
1347+
task->start([&pf](auto ref, auto error) mutable {
1348+
REQUIRE(!ref);
1349+
REQUIRE(error);
1350+
pf.promise.emplace_value(error);
1351+
});
1352+
1353+
auto result = pf.future.get_no_throw();
1354+
REQUIRE(result.is_ok());
1355+
REQUIRE(result.get_value());
1356+
std::lock_guard<std::mutex> lock(mutex);
1357+
REQUIRE(location_refresh_called);
1358+
if (failure != FailureMode::location_fails) {
1359+
REQUIRE(token_refresh_called);
1360+
}
1361+
}
1362+
1363+
#endif // REALM_APP_SERVICES
1364+
12181365
SECTION("read-only mode sets the schema version") {
12191366
{
12201367
SharedRealm realm = Realm::get_shared_realm(config);
@@ -1348,6 +1495,109 @@ TEST_CASE("Get Realm using Async Open", "[sync][pbs][async open]") {
13481495
}
13491496
}
13501497

1498+
#if REALM_ENABLE_AUTH_TESTS
1499+
1500+
TEST_CASE("Syhcnronized realm: AutoOpen", "[sync][baas][pbs][async open]") {
1501+
const auto partition = random_string(100);
1502+
auto schema = get_default_schema();
1503+
enum TestMode { expired_at_start, expired_by_websocket, websocket_fails };
1504+
enum FailureMode { location_fails, token_fails, token_not_authorized };
1505+
1506+
auto logger = util::Logger::get_default_logger();
1507+
auto transport = std::make_shared<HookedTransport<>>();
1508+
auto socket_provider = std::make_shared<HookedSocketProvider>(logger, "some user agent");
1509+
std::mutex mutex;
1510+
1511+
// Create the app session and get the logged in user identity
1512+
auto server_app_config = minimal_app_config("autoopen-realm", schema);
1513+
TestAppSession session(create_app(server_app_config), transport, DeleteApp{true}, realm::ReconnectMode::normal,
1514+
socket_provider);
1515+
auto user = session.app()->current_user();
1516+
std::string identity = user->user_id();
1517+
REQUIRE(user->is_logged_in());
1518+
REQUIRE(!identity.empty());
1519+
// Reopen the App instance and retrieve the cached user
1520+
session.reopen(false);
1521+
user = session.app()->get_existing_logged_in_user(identity);
1522+
1523+
SyncTestFile config(user, partition, schema);
1524+
config.sync_config->cancel_waits_on_nonfatal_error = true;
1525+
config.sync_config->error_handler = [&logger](std::shared_ptr<SyncSession> session, SyncError error) {
1526+
logger->debug("The sync error handler caught an error: '%1' for '%2'", error.status, session->path());
1527+
// Ignore connection failed non-fatal errors and check for access token refresh unauthorized fatal errors
1528+
if (error.status.code() == ErrorCodes::SyncConnectFailed) {
1529+
REQUIRE_FALSE(error.is_fatal);
1530+
return;
1531+
}
1532+
// If it's not SyncConnectFailed, then it should be AuthError
1533+
REQUIRE(error.status.code() == ErrorCodes::AuthError);
1534+
REQUIRE(error.is_fatal);
1535+
};
1536+
1537+
bool not_authorized = false;
1538+
bool token_refresh_called = false;
1539+
bool location_refresh_called = false;
1540+
1541+
FailureMode failure = FailureMode::location_fails;
1542+
1543+
transport->request_hook = [&](const app::Request& req) -> std::optional<app::Response> {
1544+
static constexpr int CURLE_OPERATION_TIMEDOUT = 28;
1545+
std::lock_guard<std::mutex> lock(mutex);
1546+
if (req.url.find("/auth/session") != std::string::npos) {
1547+
token_refresh_called = true;
1548+
if (failure == FailureMode::token_not_authorized) {
1549+
return app::Response{403, 0, {}, "403 not authorized"};
1550+
}
1551+
if (failure == FailureMode::token_fails) {
1552+
return app::Response{0, CURLE_OPERATION_TIMEDOUT, {}, "Operation timed out"};
1553+
}
1554+
}
1555+
else if (req.url.find("/location") != std::string::npos) {
1556+
location_refresh_called = true;
1557+
if (failure == FailureMode::location_fails) {
1558+
// Fake "offline/request timed out" custom error response
1559+
return app::Response{0, CURLE_OPERATION_TIMEDOUT, {}, "Operation timed out"};
1560+
}
1561+
}
1562+
return std::nullopt;
1563+
};
1564+
1565+
socket_provider->websocket_connect_func = [&]() -> std::optional<SocketProviderError> {
1566+
if (not_authorized) {
1567+
not_authorized = false; // one shot
1568+
return SocketProviderError(sync::websocket::WebSocketError::websocket_unauthorized, "403 not authorized");
1569+
}
1570+
return SocketProviderError(sync::websocket::WebSocketError::websocket_connection_failed,
1571+
"Operation timed out");
1572+
};
1573+
1574+
auto task = Realm::get_synchronized_realm(config);
1575+
auto pf = util::make_promise_future<std::exception_ptr>();
1576+
task->start([&pf](auto ref, auto error) mutable {
1577+
REQUIRE(!ref);
1578+
REQUIRE(error);
1579+
pf.promise.emplace_value(error);
1580+
});
1581+
1582+
auto result = pf.future.get_no_throw();
1583+
REQUIRE(result.is_ok());
1584+
REQUIRE(result.get_value());
1585+
{
1586+
std::lock_guard<std::mutex> lock(mutex);
1587+
REQUIRE(location_refresh_called);
1588+
if (failure != FailureMode::location_fails) {
1589+
REQUIRE(token_refresh_called);
1590+
}
1591+
}
1592+
1593+
transport->request_hook = nullptr;
1594+
socket_provider->websocket_connect_func = nullptr;
1595+
auto r = Realm::get_shared_realm(config);
1596+
wait_for_download(*r);
1597+
}
1598+
1599+
#endif // REALM_ENABLE_AUTH_TESTS
1600+
13511601
TEST_CASE("SharedRealm: convert", "[sync][pbs][convert]") {
13521602
TestSyncManager tsm;
13531603
ObjectSchema object_schema = {"object",

test/object-store/util/sync/baas_admin_api.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,8 +346,11 @@ app::Response do_http_request(const app::Request& request)
346346

347347
auto logger = util::Logger::get_default_logger();
348348
if (response_code != CURLE_OK) {
349+
std::string message = curl_easy_strerror(response_code);
349350
logger->error("curl_easy_perform() failed when sending request to '%1' with body '%2': %3", request.url,
350-
request.body, curl_easy_strerror(response_code));
351+
request.body, message);
352+
// Return a failing response with the CURL error as the custom code
353+
return {0, response_code, {}, message};
351354
}
352355
if (logger->would_log(util::Logger::Level::trace)) {
353356
std::string coid = [&] {

0 commit comments

Comments
 (0)