Will inconsistencies between the base table and the GSI table be detected?

I tried deleting some SSTables files of the base table, then restarting scylla and performing repair on only the base table. At this time, I found that new data will be generated in the memtable, and the data is written to the GSI table. When the base table data changes, will a copy of the data be sent to the GSI synchronously?

This behavior looks identical to the call chain for put_item.

database::do_apply(schema_ptr s, → push_view_replica_updates - > do_push_view_replica_updates(schema_ptr s, mutation m, → generate_and_propagate_view_updates(const schema_ptr& base

How does nodetool repair relate to database::do_apply?

Repair does not use the regular write path, so data written by repair will not go through database::apply().
Repair writes data to sstables directly. If the repaired table has views or indexes attached to it, the sstable will be registered with the view builder.
See make_streaming_consumer() in streaming/consumer.cc, in particular the call to register_staging_sstable(). Sstables which are registered with the view builder, will be processed and the view builder will generate view updates for all the writes contained in them.

1 Like

get_missing_rows_from_follower_nodes → flush_rows_in_working_row_buf → flush_rows → repair_writer_impl::create_writer → streaming::make_streaming_consumer → view_update_generator::register_staging_sstable → ??? → database::do_apply(schema_ptr s,

@Botond_Denes
How is register_staging_sstable propagated to database::do_apply? It is known that when I perform repair on the base table, it will inevitably use the database::do_apply function, as shown in the figure above.

Thanks!

View updates generated by the view_builder, will be sent to the relevant view replicas, where they will go through database::apply().

But the repaired data of the repaired table (base table) will not go through database::apply().

There is a problem of non-key attributes being lost when the repair base table propagates data to GSI, see Major bug: Repair will cause index table data loss!. Can you help me take a look? The entire call chain logic is quite complicated, and I haven’t understood this code yet.

The call chain is as follows:

  • repair/streaming calls view_update_generator::register_streaming_sstable()
  • registered sstable’s content is eventually consumed by db::view::view_updating_consumer.
  • db::view::view_updating_consumer forwards individual rows to replica::table::stream_view_replica_updates()
  • replica::table::do_push_view_replica_updates()
  • db::view::view_update_generator::generate_and_propagate_view_updates()
  • db::view::view_update_generator::mutate_MV()

As you can see, the code-path joins-in with that of the normal write path. They all end up calling mutate_MV() in the end.

1 Like

Hi @denesb ,
I found that view_updates::update_entry(const partition_key& base_key, const clustering_or_static_row& update, const clustering_or_static_row& existing, gc_clock::time_point now) was called in function view_updates::generate_update. There is a line of statement auto diff = update.cells().difference(*_base, kind, existing.cells()); in generate_update.

I found through reproducing problem Major bug: Repair will cause index table data loss! that the diff (actually a row type) here is empty, which results in the cells of the generated GSI being empty.

If the data modified from the base table(update) is the same as the data existing in the node(existing), then diff is empty, resulting in the record only having a key and no cells in GSI.

Why use update_entry, can’t we just use create_entry directly?

void view_updates::update_entry(const partition_key& base_key, const clustering_or_static_row& update, const clustering_or_static_row& existing, gc_clock::time_point now) {
    // While we know update and existing correspond to the same view entry,
    // they may not match the view filter.
    if (!matches_view_filter(*_base, _view_info, base_key, existing, now)) {
        create_entry(base_key, update, now);
        return;
    }
    if (!matches_view_filter(*_base, _view_info, base_key, update, now)) {
        do_delete_old_entry(base_key, existing, update, now);
        return;
    }

    if (can_skip_view_updates(update, existing)) {
        return;
    }

    auto view_rows = get_view_rows(base_key, update, std::nullopt);
    auto update_marker = compute_row_marker(update);
    const auto kind = update.column_kind();
    for (const auto& [r, action] : view_rows) { // r deletablerow
        if (auto rm = std::get_if<row_marker>(&action)) {
            r->apply(*rm);
            vlogger.info("view_updates::update_entry bbb {}", row::printer(*_base, kind, r->cells()));
        } else {
            r->apply(update_marker);
        }
        r->apply(update.tomb());
        vlogger.info("view_updates::update_entry update {}", row::printer(*_base, kind, update.cells()));
        vlogger.info("view_updates::update_entry existing {}", row::printer(*_base, kind, existing.cells()));
   
        auto diff = update.cells().difference(*_base, kind, existing.cells()); // row
        vlogger.info("view_updates::update_entry diif {}", row::printer(*_base, kind, diff)); // diff is none
        add_cells_to_view(*_base, *_view, kind, std::move(diff), r->cells());
        vlogger.info("view_updates::update_entry r {}", row::printer(*_base, kind, r->cells())); // r is none
    }
    _op_count += view_rows.size();
}

I’m out of my depth here. We need help from @nyh or @wmitros.