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

Allow deletion of ephemeral objects before commit #7064

Merged
merged 6 commits into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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 @@ -6,7 +6,7 @@

### Fixed
* <How do the end-user experience this issue? what was the impact?> ([#????](https://github.com/realm/realm-core/issues/????), since v?.?.?)
* None.
* Deleting an object in an asymmetric table would cause a crash. Likely to solve [#1537](https://github.com/realm/realm-kotlin/issues/1537), since v12.1.0.

### Breaking changes
* None.
Expand Down
2 changes: 1 addition & 1 deletion src/realm/cluster_tree.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1086,7 +1086,7 @@ void ClusterTree::verify() const
#endif
}

void ClusterTree::nullify_links(ObjKey obj_key, CascadeState& state)
void ClusterTree::nullify_incoming_links(ObjKey obj_key, CascadeState& state)
{
REALM_ASSERT(state.m_group);
m_root->nullify_incoming_links(obj_key, state);
Expand Down
2 changes: 1 addition & 1 deletion src/realm/cluster_tree.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ class ClusterTree {
{
m_root->destroy_deep();
}
void nullify_links(ObjKey, CascadeState&);
void nullify_incoming_links(ObjKey, CascadeState&);
bool is_empty() const noexcept
{
return size() == 0;
Expand Down
7 changes: 6 additions & 1 deletion src/realm/db.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2412,10 +2412,15 @@ Replication::version_type DB::do_commit(Transaction& transaction, bool commit_to
version_type new_version = current_version + 1;

if (!transaction.m_objects_to_delete.empty()) {
std::map<TableKey, std::vector<ObjKey>> table_object_map;
for (auto it : transaction.m_objects_to_delete) {
transaction.get_table(it.table_key)->remove_object(it.obj_key);
// Checking ttl would go here
Copy link
Collaborator

Choose a reason for hiding this comment

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

this would be lazy checking

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But I guess if we ever get this implemented, this would be the place to check which objects were due to be deleted. BTW if we skip recording the time the object was created, we can make this even more efficient. @danieltabacaru should we do that?

Copy link
Collaborator

@danieltabacaru danieltabacaru Oct 19, 2023

Choose a reason for hiding this comment

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

I am not saying it's a bad place to check, it's just that we would be relying on other things being committed. I am not sure if it's the same idea, but we don't really need the object keys, the table keys is enough (we can then clear the tables). we can optimize it further by also skipping storing the table keys by iterating through the tables and clearing the asymmetric ones.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Of course - the table keys are sufficient. Even better!

table_object_map[it.table_key].push_back(it.obj_key);
}
transaction.m_objects_to_delete.clear();
for (auto& item : table_object_map) {
transaction.get_table_unchecked(item.first)->batch_erase_objects(item.second);
}
}
if (Replication* repl = get_replication()) {
// If Replication::prepare_commit() fails, then the entire transaction
Expand Down
10 changes: 8 additions & 2 deletions src/realm/group.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,7 @@ class Group : public ArrayParent {
std::optional<size_t> read_lock_file_size = util::none,
std::optional<uint_fast64_t> read_lock_version = util::none);

Table* get_table_unchecked(TableKey);
size_t find_table_index(StringData name) const noexcept;
TableKey ndx2key(size_t ndx) const;
size_t key2ndx(TableKey key) const;
Expand Down Expand Up @@ -946,11 +947,16 @@ inline TableKey Group::find_table(StringData name) const noexcept
return (ndx != npos) ? ndx2key(ndx) : TableKey{};
}

inline Table* Group::get_table_unchecked(TableKey key)
{
auto ndx = key2ndx_checked(key);
return do_get_table(ndx); // Throws
}

inline TableRef Group::get_table(TableKey key)
{
check_attached();
auto ndx = key2ndx_checked(key);
Table* table = do_get_table(ndx); // Throws
Table* table = get_table_unchecked(key);
return TableRef(table, table ? table->m_alloc.get_instance_version() : 0);
}

Expand Down
16 changes: 10 additions & 6 deletions src/realm/sync/subscriptions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -883,8 +883,11 @@ MutableSubscriptionSet SubscriptionStore::get_mutable_by_version(int64_t version
{
auto tr = m_db->start_write();
auto sub_sets = tr->get_table(m_sub_set_table);
return MutableSubscriptionSet(weak_from_this(), std::move(tr),
sub_sets->get_object_with_primary_key(Mixed{version_id}));
auto obj = sub_sets->get_object_with_primary_key(Mixed{version_id});
if (!obj) {
throw KeyNotFound("Subscription set not found");
Copy link
Collaborator

Choose a reason for hiding this comment

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

we should include the version_id in the message

}
return MutableSubscriptionSet(weak_from_this(), std::move(tr), obj);
}

SubscriptionSet SubscriptionStore::get_by_version(int64_t version_id) const
Expand All @@ -897,15 +900,16 @@ SubscriptionSet SubscriptionStore::get_by_version_impl(int64_t version_id,
{
auto tr = m_db->start_frozen(db_version.value_or(VersionID{}));
auto sub_sets = tr->get_table(m_sub_set_table);
try {
return SubscriptionSet(weak_from_this(), *tr, sub_sets->get_object_with_primary_key(Mixed{version_id}));
auto obj = sub_sets->get_object_with_primary_key(Mixed{version_id});
if (obj) {
return SubscriptionSet(weak_from_this(), *tr, obj);
}
catch (const KeyNotFound&) {
else {
std::lock_guard<std::mutex> lk(m_pending_notifications_mutex);
if (version_id < m_min_outstanding_version) {
return SubscriptionSet(weak_from_this(), version_id, SubscriptionSet::SupersededTag{});
}
throw;
throw KeyNotFound("Subscription set not found");
Copy link
Collaborator

Choose a reason for hiding this comment

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

same here

}
}

Expand Down
39 changes: 27 additions & 12 deletions src/realm/table.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,7 @@ void Table::remove_recursive(CascadeState& cascade_state)
cascade_state.send_notifications();

for (auto& l : cascade_state.m_to_be_nullified) {
Obj obj = group->get_table(l.origin_table)->try_get_object(l.origin_key);
Obj obj = group->get_table_unchecked(l.origin_table)->try_get_object(l.origin_key);
REALM_ASSERT_DEBUG(obj);
if (obj) {
std::move(obj).nullify_link(l.origin_col_key, l.old_target_link);
Expand All @@ -558,7 +558,7 @@ void Table::remove_recursive(CascadeState& cascade_state)

auto to_delete = std::move(cascade_state.m_to_be_deleted);
for (auto obj : to_delete) {
auto table = group->get_table(obj.first);
auto table = obj.first == m_key ? this : group->get_table_unchecked(obj.first);
// This might add to the list of objects that should be deleted
REALM_ASSERT(!obj.second.is_unresolved());
table->m_clusters.erase(obj.second, cascade_state);
Expand All @@ -572,8 +572,9 @@ void Table::nullify_links(CascadeState& cascade_state)
Group* group = get_parent_group();
REALM_ASSERT(group);
for (auto& to_delete : cascade_state.m_to_be_deleted) {
auto table = group->get_table(to_delete.first);
table->m_clusters.nullify_links(to_delete.second, cascade_state);
auto table = to_delete.first == m_key ? this : group->get_table_unchecked(to_delete.first);
if (!table->is_asymmetric())
table->m_clusters.nullify_incoming_links(to_delete.second, cascade_state);
}
}

Expand Down Expand Up @@ -2178,20 +2179,22 @@ void Table::batch_erase_rows(const KeyColumn& keys)
void Table::batch_erase_objects(std::vector<ObjKey>& keys)
{
Group* g = get_parent_group();
bool maybe_has_incoming_links = g && !is_asymmetric();

if (has_any_embedded_objects() || (g && g->has_cascade_notification_handler())) {
CascadeState state(CascadeState::Mode::Strong, g);
std::for_each(keys.begin(), keys.end(), [this, &state](ObjKey k) {
state.m_to_be_deleted.emplace_back(m_key, k);
});
nullify_links(state);
if (maybe_has_incoming_links)
nullify_links(state);
remove_recursive(state);
}
else {
CascadeState state(CascadeState::Mode::None, g);
for (auto k : keys) {
if (g) {
m_clusters.nullify_links(k, state);
if (maybe_has_incoming_links) {
m_clusters.nullify_incoming_links(k, state);
}
m_clusters.erase(k, state);
}
Expand Down Expand Up @@ -3147,7 +3150,8 @@ Obj Table::get_object_with_primary_key(Mixed primary_key) const
DataType type = DataType(primary_key_col.get_type());
REALM_ASSERT((primary_key.is_null() && primary_key_col.get_attrs().test(col_attr_Nullable)) ||
primary_key.get_type() == type);
return m_clusters.get(m_index_accessors[primary_key_col.get_index().val]->find_first(primary_key));
ObjKey k = m_index_accessors[primary_key_col.get_index().val]->find_first(primary_key);
return k ? m_clusters.get(k) : Obj{};
}

Mixed Table::get_primary_key(ObjKey key) const
Expand Down Expand Up @@ -3385,16 +3389,27 @@ void Table::remove_object(ObjKey key)
{
Group* g = get_parent_group();

if (is_asymmetric()) {
REALM_ASSERT(g);
auto it = std::find_if(g->m_objects_to_delete.begin(), g->m_objects_to_delete.end(),
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: auto& objects_to_delete = g->m_objects_to_delete;

[&](const Group::ToDeleteRef& item) {
return item.table_key == m_key && item.obj_key == key;
});
if (it != g->m_objects_to_delete.end()) {
g->m_objects_to_delete.erase(it);
}
}

if (has_any_embedded_objects() || (g && g->has_cascade_notification_handler())) {
CascadeState state(CascadeState::Mode::Strong, g);
state.m_to_be_deleted.emplace_back(m_key, key);
m_clusters.nullify_links(key, state);
m_clusters.nullify_incoming_links(key, state);
remove_recursive(state);
}
else {
CascadeState state(CascadeState::Mode::None, g);
if (g) {
m_clusters.nullify_links(key, state);
m_clusters.nullify_incoming_links(key, state);
}
m_clusters.erase(key, state);
}
Expand Down Expand Up @@ -3946,7 +3961,7 @@ bool Table::has_any_embedded_objects()
for_each_public_column([&](ColKey col_key) {
auto target_table_key = get_opposite_table_key(col_key);
if (target_table_key && is_link_type(col_key.get_type())) {
auto target_table = get_parent_group()->get_table(target_table_key);
auto target_table = get_parent_group()->get_table_unchecked(target_table_key);
if (target_table->is_embedded()) {
m_has_any_embedded_objects = true;
return IteratorControl::Stop; // early out
Expand Down Expand Up @@ -3984,7 +3999,7 @@ ColKey Table::find_or_add_backlink_column(ColKey origin_col_key, TableKey origin
set_opposite_column(backlink_col_key, origin_table, origin_col_key);

if (Replication* repl = get_repl())
repl->typed_link_change(get_parent_group()->get_table(origin_table).unchecked_ptr(), origin_col_key,
repl->typed_link_change(get_parent_group()->get_table_unchecked(origin_table), origin_col_key,
m_key); // Throws
}

Expand Down
3 changes: 2 additions & 1 deletion src/realm/table.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,8 @@ class Table {
// - turns the object into a tombstone if links exist
// - otherwise works just as remove_object()
ObjKey invalidate_object(ObjKey key);
// Remove several objects
void batch_erase_objects(std::vector<ObjKey>& keys);
Obj try_get_tombstone(ObjKey key) const
{
REALM_ASSERT(key.is_unresolved());
Expand Down Expand Up @@ -703,7 +705,6 @@ class Table {
TableRef m_own_ref;

void batch_erase_rows(const KeyColumn& keys);
void batch_erase_objects(std::vector<ObjKey>& keys);
size_t do_set_link(ColKey col_key, size_t row_ndx, size_t target_row_ndx);

void populate_search_index(ColKey col_key);
Expand Down
9 changes: 9 additions & 0 deletions test/object-store/object.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2390,6 +2390,15 @@ TEST_CASE("Asymmetric Object") {
REQUIRE(realm->is_empty());
}

SECTION("Delete ephemeral object before comitting") {
realm->begin_transaction();
auto obj = realm->read_group().get_table("class_asymmetric")->create_object_with_primary_key(1);
obj.remove();
realm->commit_transaction();
REQUIRE(!obj.is_valid());
REQUIRE(realm->is_empty());
}

SECTION("Outgoing link not allowed") {
auto obj = create(AnyDict{{"_id", INT64_C(1)}}, "table");
auto table = realm->read_group().get_table("class_table");
Expand Down
2 changes: 2 additions & 0 deletions test/object-store/sync/flx_sync.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2340,6 +2340,8 @@ TEST_CASE("flx: writes work without waiting for sync", "[sync][flx][baas]") {
realm->refresh();
Results results(realm, table);
CHECK(results.size() == 1);
Obj obj = results.get(0);
CHECK(obj.get_primary_key().get_object_id() == bar_obj_id);
CHECK(table->get_object_with_primary_key({bar_obj_id}).is_valid());
});
}
Expand Down