Fix move-assigning to a `Results` that has a notifier to not leak the
`Realm`, and moving from a `Results` to not result in a use-after-free. The compiler generated move-assignment operator resulted in `m_notifier` being assigned to without first calling `CollectionNotifier::unregister`. This left a retain cycle in place, causing the `Realm` and other objects to leak. `ResultsNotifier` keeps track of which `Results` it should update when a new `TableView` becomes available. When `Results` move-assignment operator and move-constructor transfer ownership of the `ResultsNotifier` to a new instance they also need to update its target so it won't attempt to update the moved-from `Results`.
This commit is contained in:
parent
78c4f30ee9
commit
65a748de0c
|
@ -150,6 +150,9 @@ public:
|
|||
void prepare_handover();
|
||||
bool deliver(Realm&, SharedGroup&, std::exception_ptr);
|
||||
|
||||
template <typename T>
|
||||
class Handle;
|
||||
|
||||
protected:
|
||||
bool have_callbacks() const noexcept { return m_have_callbacks; }
|
||||
void add_changes(CollectionChangeBuilder change) { m_accumulated_changes.merge(std::move(change)); }
|
||||
|
@ -201,6 +204,43 @@ private:
|
|||
CollectionChangeCallback next_callback();
|
||||
};
|
||||
|
||||
// A smart pointer to a CollectionNotifier that unregisters the notifier when
|
||||
// the pointer is destroyed. Movable. Copying will produce a null Handle.
|
||||
template <typename T>
|
||||
class CollectionNotifier::Handle : public std::shared_ptr<T> {
|
||||
public:
|
||||
using std::shared_ptr<T>::shared_ptr;
|
||||
|
||||
Handle() = default;
|
||||
~Handle() { reset(); }
|
||||
|
||||
// Copying a Handle produces a null Handle.
|
||||
Handle(const Handle&) : Handle() { }
|
||||
Handle& operator=(const Handle& other)
|
||||
{
|
||||
if (this != &other) {
|
||||
reset();
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
Handle(Handle&&) = default;
|
||||
Handle& operator=(Handle&& other)
|
||||
{
|
||||
reset();
|
||||
std::shared_ptr<T>::shared_ptr::operator=(std::move(other));
|
||||
return *this;
|
||||
}
|
||||
|
||||
void reset()
|
||||
{
|
||||
if (*this) {
|
||||
this->get()->unregister();
|
||||
std::shared_ptr<T>::reset();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace _impl
|
||||
} // namespace realm
|
||||
|
||||
|
|
|
@ -18,8 +18,6 @@
|
|||
|
||||
#include "impl/results_notifier.hpp"
|
||||
|
||||
#include "results.hpp"
|
||||
|
||||
using namespace realm;
|
||||
using namespace realm::_impl;
|
||||
|
||||
|
@ -34,6 +32,14 @@ ResultsNotifier::ResultsNotifier(Results& target)
|
|||
m_query_handover = Realm::Internal::get_shared_group(*get_realm()).export_for_handover(q, MutableSourcePayload::Move);
|
||||
}
|
||||
|
||||
void ResultsNotifier::target_results_moved(Results& old_target, Results& new_target)
|
||||
{
|
||||
auto lock = lock_target();
|
||||
|
||||
REALM_ASSERT(m_target_results == &old_target);
|
||||
m_target_results = &new_target;
|
||||
}
|
||||
|
||||
void ResultsNotifier::release_data() noexcept
|
||||
{
|
||||
m_query = nullptr;
|
||||
|
|
|
@ -30,6 +30,8 @@ class ResultsNotifier : public CollectionNotifier {
|
|||
public:
|
||||
ResultsNotifier(Results& target);
|
||||
|
||||
void target_results_moved(Results& old_target, Results& new_target);
|
||||
|
||||
private:
|
||||
// Target Results to update
|
||||
// Can only be used with lock_target() held
|
||||
|
|
|
@ -30,12 +30,7 @@ using namespace realm;
|
|||
using namespace realm::_impl;
|
||||
|
||||
List::List() noexcept = default;
|
||||
List::~List()
|
||||
{
|
||||
if (m_notifier) {
|
||||
m_notifier->unregister();
|
||||
}
|
||||
}
|
||||
List::~List() = default;
|
||||
|
||||
List::List(std::shared_ptr<Realm> r, const ObjectSchema& s, LinkViewRef l) noexcept
|
||||
: m_realm(std::move(r))
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
#define REALM_LIST_HPP
|
||||
|
||||
#include "collection_notifications.hpp"
|
||||
#include "impl/collection_notifier.hpp"
|
||||
|
||||
#include <realm/link_view_fwd.hpp>
|
||||
#include <realm/row.hpp>
|
||||
|
@ -46,6 +47,11 @@ public:
|
|||
List(std::shared_ptr<Realm> r, const ObjectSchema& s, LinkViewRef l) noexcept;
|
||||
~List();
|
||||
|
||||
List(const List&) = default;
|
||||
List& operator=(const List&) = default;
|
||||
List(List&&) = default;
|
||||
List& operator=(List&&) = default;
|
||||
|
||||
const std::shared_ptr<Realm>& get_realm() const { return m_realm; }
|
||||
Query get_query() const;
|
||||
const ObjectSchema& get_object_schema() const { return *m_object_schema; }
|
||||
|
@ -89,7 +95,7 @@ private:
|
|||
std::shared_ptr<Realm> m_realm;
|
||||
const ObjectSchema* m_object_schema;
|
||||
LinkViewRef m_link_view;
|
||||
std::shared_ptr<_impl::CollectionNotifier> m_notifier;
|
||||
_impl::CollectionNotifier::Handle<_impl::CollectionNotifier> m_notifier;
|
||||
|
||||
void verify_valid_row(size_t row_ndx, bool insertion = false) const;
|
||||
|
||||
|
|
|
@ -38,6 +38,9 @@ using namespace realm;
|
|||
#define REALM_FALLTHROUGH
|
||||
#endif
|
||||
|
||||
Results::Results() = default;
|
||||
Results::~Results() = default;
|
||||
|
||||
Results::Results(SharedRealm r, const ObjectSchema &o, Query q, SortOrder s)
|
||||
: m_realm(std::move(r))
|
||||
, m_object_schema(&o)
|
||||
|
@ -83,13 +86,88 @@ Results::Results(SharedRealm r, const ObjectSchema& o, TableView tv, SortOrder s
|
|||
REALM_ASSERT(m_sort.column_indices.size() == m_sort.ascending.size());
|
||||
}
|
||||
|
||||
Results::~Results()
|
||||
Results::Results(const Results& other)
|
||||
: m_realm(other.m_realm)
|
||||
, m_object_schema(other.m_object_schema)
|
||||
, m_query(other.m_query)
|
||||
, m_table_view(other.m_table_view)
|
||||
, m_link_view(other.m_link_view)
|
||||
, m_table(other.m_table)
|
||||
, m_sort(other.m_sort)
|
||||
, m_live(other.m_live)
|
||||
, m_notifier()
|
||||
, m_mode(other.m_mode)
|
||||
, m_has_used_table_view(false)
|
||||
, m_wants_background_updates(true)
|
||||
{
|
||||
}
|
||||
|
||||
#if 0
|
||||
// FIXME: TableViewBase::operator= is missing from the core static library.
|
||||
Results& Results::operator=(const Results& other)
|
||||
{
|
||||
if (this == &other) {
|
||||
return *this;
|
||||
}
|
||||
|
||||
m_realm = other.m_realm;
|
||||
m_object_schema = other.m_object_schema;
|
||||
m_query = other.m_query;
|
||||
m_table_view = other.m_table_view;
|
||||
m_link_view = other.m_link_view;
|
||||
m_table = other.m_table;
|
||||
m_sort = other.m_sort;
|
||||
m_live = other.m_live;
|
||||
m_notifier.reset();
|
||||
m_mode = other.m_mode;
|
||||
m_has_used_table_view = false;
|
||||
m_wants_background_updates = true;
|
||||
|
||||
return *this;
|
||||
}
|
||||
#endif
|
||||
|
||||
Results::Results(Results&& other)
|
||||
: m_realm(std::move(other.m_realm))
|
||||
, m_object_schema(std::move(other.m_object_schema))
|
||||
, m_query(std::move(other.m_query))
|
||||
, m_table_view(std::move(other.m_table_view))
|
||||
, m_link_view(std::move(other.m_link_view))
|
||||
, m_table(other.m_table)
|
||||
, m_sort(std::move(other.m_sort))
|
||||
, m_live(other.m_live)
|
||||
, m_notifier(std::move(other.m_notifier))
|
||||
, m_mode(other.m_mode)
|
||||
, m_has_used_table_view(other.m_has_used_table_view)
|
||||
, m_wants_background_updates(other.m_wants_background_updates)
|
||||
{
|
||||
if (m_notifier) {
|
||||
m_notifier->unregister();
|
||||
m_notifier->target_results_moved(other, *this);
|
||||
}
|
||||
}
|
||||
|
||||
Results& Results::operator=(Results&& other)
|
||||
{
|
||||
m_realm = std::move(other.m_realm);
|
||||
m_object_schema = std::move(other.m_object_schema);
|
||||
m_query = std::move(other.m_query);
|
||||
m_table_view = std::move(other.m_table_view);
|
||||
m_link_view = std::move(other.m_link_view);
|
||||
m_table = other.m_table;
|
||||
m_sort = std::move(other.m_sort);
|
||||
m_live = other.m_live;
|
||||
m_notifier = std::move(other.m_notifier);
|
||||
m_mode = other.m_mode;
|
||||
m_has_used_table_view = other.m_has_used_table_view;
|
||||
m_wants_background_updates = other.m_wants_background_updates;
|
||||
|
||||
if (m_notifier) {
|
||||
m_notifier->target_results_moved(other, *this);
|
||||
}
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
void Results::validate_read() const
|
||||
{
|
||||
if (m_realm)
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
|
||||
#include "collection_notifications.hpp"
|
||||
#include "shared_realm.hpp"
|
||||
#include "impl/collection_notifier.hpp"
|
||||
|
||||
#include <realm/table_view.hpp>
|
||||
#include <realm/util/optional.hpp>
|
||||
|
@ -48,7 +49,7 @@ public:
|
|||
// Results can be either be backed by nothing, a thin wrapper around a table,
|
||||
// or a wrapper around a query and a sort order which creates and updates
|
||||
// the tableview as needed
|
||||
Results() = default;
|
||||
Results();
|
||||
Results(SharedRealm r, const ObjectSchema& o, Table& table);
|
||||
Results(SharedRealm r, const ObjectSchema& o, Query q, SortOrder s = {});
|
||||
Results(SharedRealm r, const ObjectSchema& o, TableView tv, SortOrder s);
|
||||
|
@ -56,10 +57,10 @@ public:
|
|||
~Results();
|
||||
|
||||
// Results is copyable and moveable
|
||||
Results(Results const&) = default;
|
||||
Results(Results&&) = default;
|
||||
Results& operator=(Results&&) = default;
|
||||
Results& operator=(Results const&) = default;
|
||||
Results(Results&&);
|
||||
Results& operator=(Results&&);
|
||||
Results(const Results&);
|
||||
Results& operator=(const Results&);
|
||||
|
||||
// Get the Realm
|
||||
SharedRealm get_realm() const { return m_realm; }
|
||||
|
@ -203,7 +204,7 @@ private:
|
|||
SortOrder m_sort;
|
||||
bool m_live = true;
|
||||
|
||||
std::shared_ptr<_impl::ResultsNotifier> m_notifier;
|
||||
_impl::CollectionNotifier::Handle<_impl::ResultsNotifier> m_notifier;
|
||||
|
||||
Mode m_mode = Mode::Empty;
|
||||
bool m_has_used_table_view = false;
|
||||
|
|
|
@ -533,3 +533,54 @@ TEST_CASE("Async Results error handling") {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Notifications on moved Results") {
|
||||
InMemoryTestFile config;
|
||||
config.cache = false;
|
||||
config.automatic_change_notifications = false;
|
||||
config.schema = std::make_unique<Schema>(Schema{
|
||||
{"object", "", {
|
||||
{"value", PropertyType::Int},
|
||||
}},
|
||||
});
|
||||
|
||||
auto r = Realm::get_shared_realm(config);
|
||||
auto table = r->read_group()->get_table("class_object");
|
||||
auto results = std::make_unique<Results>(r, *config.schema->find("object"), *table);
|
||||
|
||||
int notification_calls = 0;
|
||||
auto token = results->add_notification_callback([&](CollectionChangeSet c, std::exception_ptr err) {
|
||||
REQUIRE_FALSE(err);
|
||||
++notification_calls;
|
||||
});
|
||||
|
||||
advance_and_notify(*r);
|
||||
|
||||
auto write = [&](auto&& f) {
|
||||
r->begin_transaction();
|
||||
f();
|
||||
r->commit_transaction();
|
||||
advance_and_notify(*r);
|
||||
};
|
||||
|
||||
SECTION("notifications continue to work after Results is moved (move-constructor)") {
|
||||
Results r(std::move(*results));
|
||||
results.reset();
|
||||
|
||||
write([&] {
|
||||
table->set_int(0, table->add_empty_row(), 1);
|
||||
});
|
||||
REQUIRE(notification_calls == 2);
|
||||
}
|
||||
|
||||
SECTION("notifications continue to work after Results is moved (move-assignment)") {
|
||||
Results r;
|
||||
r = std::move(*results);
|
||||
results.reset();
|
||||
|
||||
write([&] {
|
||||
table->set_int(0, table->add_empty_row(), 1);
|
||||
});
|
||||
REQUIRE(notification_calls == 2);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue