Skip to content

Commit

Permalink
PS-8879: OOM when alter column to compression (percona#5164) (percona…
Browse files Browse the repository at this point in the history
…#5168)

https://jira.percona.com/browse/PS-8879

Post-push fix.

Keep uncompressed data on blob_heap.
compress_heap is used only for compressed data.
Q: Why do we need separate heaps for compression and decompression?
A: There are below scenarios when we use one or both heaps and we need
to manage them separately.

Case 1:
Consider the following table:
CREATE TABLE t1 (a INT, b BLOB COLUMN_FORMAT COMPRESSED);
If we update the row in the following way:
UPDATE t1 SET a=1 WHERE a = 0;
the flow is:
For every row do:
  1. Read row
    1.1. Read row with compressed BLOB from the storage into blob_heap
             (allocates mem from blob_heap)
    1.2. Decompress BLOB. Put decompressed version on heap_x and link
             record with BLOB pointer (allocates memory from heap_x)
    1.3. Construct the new row. As BLOB didn't change, reuse the pointer
             to it.
  2. Write row
    2.1. Compress BLOB. Put compressed version on compress_heap
             (allocates memory from compress_heap)
    2.2. Store row with compressed BLOB to the storage.
  3. Write row event to binlog
    3.1. Use decompressed blob pointer (from heap_x)
Note that in the above flow we have 1 table involved, so one 'prebuilt'
object.
So we need to keep decompressed BLOB buffer valid until we write the row
into the binlog.
Every loop allocates memory from heaps, so we need to free it when not
needed anymore to avoid OOM.
That's why blob_heap is cleared before row read and compress_heap is
cleared after row write (it could be cleared before row write as well as
it was in the original implementation).
We can not use compress_heap to keep decompressed version of the BLOB,
because it has to retain up to binlogging. The last moment we are able
to deallocate it is at the end of row write, but it is too early.
That's why we do it before next row read (and additionally after the
query).

Note:
Decompression relates not only to BLOB columns, but also text and
varchar. In case of such columns (and no blob in row), there is no
blob_heap allocated, that's why we allocate it during decompression.
Logically, we could use dedicated decompress_heap (heap_x from above) to
keep decompressed version of column, but that would lead to bigger
changes (also in partitioned tables). Both blob_heap and decompress_heap
(heap_x) would serve the same purpose: keep decompressed version of
column, so we can use blob_heap for this purpose.

Case 2:
Consider the following table:
CREATE TABLE t1 (b1 BLOB, b2 BLOB COLUMN_FORMAT COMPRESSED);
If we alter this table in the following way:
ALTER TABLE t1 MODIFY b1 BLOB COLUMN_FORMAT COMPRESSED;
the flow is:
  1. Create temp table with compressed BLOB b1 column (target table)
  2. For every row do:
  3. Read row from original table
    3.1  Read row with BLOBs from the storage into blob_heap
    3.2. Decompress BLOB b2. Put decompressed version on heap_x and link
         record with BLOB b2 pointer
  2. Write row to target table
    2.1. Compress BLOBs. Put compressed version on compress_heap
    2.2. Store row with compressed BLOBs to the storage.

Note that in the above flow we have 2 different tables, so 2 different
'prebuilt' objects.
Again, when we are looping over rows, we are allocating memory from
heaps, but we need to keep decompressed version of the row until it is
stored back, so we could free decompressed version after writing row.
But it has to be kept longer because of Case 1. This is why we free it
before next row read (or after the query).

Considering two above cases we get to the following conclusions:
1. We need to keep decompressed version of row until it is binlogged
   => we can free it before next row read and after the query is
   completed
2. We need to free compressed version of row to avoid OOM
   => we can free it before or after row write and after the query is
   completed
  • Loading branch information
kamil-holubicki authored Dec 7, 2023
1 parent ee91c81 commit 88c81fc
Show file tree
Hide file tree
Showing 6 changed files with 27 additions and 17 deletions.
6 changes: 3 additions & 3 deletions storage/innobase/row/row0ins.cc
Original file line number Diff line number Diff line change
Expand Up @@ -880,7 +880,7 @@ static void row_ins_foreign_fill_virtual(upd_node_t *cascade, const rec_t *rec,

dfield_t *vfield = innobase_get_computed_value(
update->old_vrow, col, index, &v_heap, update->heap, nullptr, thd,
nullptr, nullptr, nullptr, nullptr, &prebuilt->compress_heap);
nullptr, nullptr, nullptr, nullptr, &prebuilt->blob_heap);

if (vfield == nullptr) {
*err = DB_COMPUTE_VALUE_FAILED;
Expand Down Expand Up @@ -918,15 +918,15 @@ static void row_ins_foreign_fill_virtual(upd_node_t *cascade, const rec_t *rec,
}
dfield_t *new_vfield = innobase_get_computed_value(
update->old_vrow, col, index, &v_heap, update->heap, nullptr, thd,
nullptr, nullptr, node->update, foreign, &prebuilt->compress_heap);
nullptr, nullptr, node->update, foreign, &prebuilt->blob_heap);
dfield_copy(&(upd_field->new_val), new_vfield);
}
}

if (!node->is_delete && (foreign->type & DICT_FOREIGN_ON_UPDATE_CASCADE)) {
dfield_t *new_vfield = innobase_get_computed_value(
update->old_vrow, col, index, &v_heap, update->heap, nullptr, thd,
nullptr, nullptr, node->update, foreign, &prebuilt->compress_heap);
nullptr, nullptr, node->update, foreign, &prebuilt->blob_heap);

if (new_vfield == nullptr) {
*err = DB_COMPUTE_VALUE_FAILED;
Expand Down
9 changes: 7 additions & 2 deletions storage/innobase/row/row0purge.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1249,8 +1249,13 @@ que_thr_t *row_purge_step(que_thr_t *thr) {
row_purge_end(thr);
}

if (thr->prebuilt != nullptr && thr->prebuilt->compress_heap != nullptr)
mem_heap_empty(thr->prebuilt->compress_heap);
/* Most probably this is not needed at all, because purge for virtual columns
is disabled in 8.0 (see #ifdef INNODB_DD_VC_SUPPORT) */
if (thr->prebuilt != nullptr && thr->prebuilt->blob_heap != nullptr)
mem_heap_empty(thr->prebuilt->blob_heap);

/* compress_heap was not used */
ut_ad(thr->prebuilt == 0 || thr->prebuilt->compress_heap == 0);

return (thr);
}
Expand Down
8 changes: 4 additions & 4 deletions storage/innobase/row/row0sel.cc
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ static dberr_t row_sel_sec_rec_is_for_clust_rec(
vfield = innobase_get_computed_value(
row, v_col, clust_index, &heap, heap, nullptr,
thr_get_trx(thr)->mysql_thd, thr->prebuilt->m_mysql_table, nullptr,
nullptr, nullptr, &thr->prebuilt->compress_heap);
nullptr, nullptr, &thr->prebuilt->blob_heap);

if (vfield == nullptr) {
/* This may happen e.g. when this statement is executed in
Expand Down Expand Up @@ -2830,7 +2830,7 @@ void row_sel_field_store_in_mysql_format_func(

row_sel_field_store_in_mysql_format(mysql_rec + templ->mysql_col_offset,
templ, rec_index, field_no, data, len,
&prebuilt->compress_heap, ULINT_UNDEFINED);
&prebuilt->blob_heap, ULINT_UNDEFINED);

if (heap != blob_heap) {
mem_heap_free(heap);
Expand Down Expand Up @@ -2886,7 +2886,7 @@ void row_sel_field_store_in_mysql_format_func(

row_sel_field_store_in_mysql_format(mysql_rec + templ->mysql_col_offset,
templ, rec_index, field_no, data, len,
&prebuilt->compress_heap, sec_field_no);
&prebuilt->blob_heap, sec_field_no);
}

ut_ad(rec_field_not_null_not_add_col_def(len));
Expand Down Expand Up @@ -3004,7 +3004,7 @@ bool row_sel_store_mysql_rec(byte *mysql_rec, row_prebuilt_t *prebuilt,
row_sel_field_store_in_mysql_format(
mysql_rec + templ->mysql_col_offset, templ, rec_index,
templ->clust_rec_field_no, (const byte *)dfield->data, dfield->len,
&prebuilt->compress_heap, ULINT_UNDEFINED);
&prebuilt->blob_heap, ULINT_UNDEFINED);
if (templ->mysql_null_bit_mask) {
mysql_rec[templ->mysql_null_byte_offset] &=
~(byte)templ->mysql_null_bit_mask;
Expand Down
10 changes: 5 additions & 5 deletions storage/innobase/row/row0upd.cc
Original file line number Diff line number Diff line change
Expand Up @@ -937,7 +937,7 @@ upd_t *row_upd_build_difference_binary(

dfield_t *vfield = innobase_get_computed_value(
update->old_vrow, col, index, &v_heap, heap, nullptr, thd,
mysql_table, nullptr, nullptr, nullptr, &prebuilt->compress_heap);
mysql_table, nullptr, nullptr, nullptr, &prebuilt->blob_heap);

if (vfield == nullptr) {
*error = DB_COMPUTE_VALUE_FAILED;
Expand Down Expand Up @@ -1899,17 +1899,17 @@ static void row_upd_store_v_row(upd_node_t *node, const upd_t *update, THD *thd,
row_upd_dup_v_new_vals(update);
new_val_v_cols_dup = true;
}
innobase_get_computed_value(node->row, col, index, &heap,
node->heap, nullptr, thd, mysql_table,
nullptr, nullptr, nullptr, &prebuilt->compress_heap);
innobase_get_computed_value(
node->row, col, index, &heap, node->heap, nullptr, thd,
mysql_table, nullptr, nullptr, nullptr, &prebuilt->blob_heap);
}
}
} else {
/* Need to compute, this happens when
deleting row */
innobase_get_computed_value(node->row, col, index, &heap, node->heap,
nullptr, thd, mysql_table, nullptr,
nullptr, nullptr, &prebuilt->compress_heap);
nullptr, nullptr, &prebuilt->blob_heap);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion storage/innobase/row/row0vers.cc
Original file line number Diff line number Diff line change
Expand Up @@ -652,7 +652,7 @@ static void row_vers_build_clust_v_col(dtuple_t *row, dict_index_t *clust_index,

innobase_get_computed_value(row, col, clust_index, &local_heap, heap,
nullptr, current_thd, nullptr, nullptr,
nullptr, nullptr, &prebuilt->compress_heap);
nullptr, nullptr, &prebuilt->blob_heap);
}
}

Expand Down
9 changes: 7 additions & 2 deletions storage/innobase/trx/trx0purge.cc
Original file line number Diff line number Diff line change
Expand Up @@ -277,11 +277,16 @@ Frees the global purge system control structure. */
void trx_purge_sys_close() {
if (!purge_sys) return;

/* Most probably this is not needed at all, because purge for virtual columns
is disabled in 8.0 (see #ifdef INNODB_DD_VC_SUPPORT) */
for (que_thr_t *thr = UT_LIST_GET_FIRST(purge_sys->query->thrs);
thr != nullptr; thr = UT_LIST_GET_NEXT(thrs, thr)) {
if (thr->prebuilt != nullptr && thr->prebuilt->compress_heap != nullptr) {
row_mysql_prebuilt_free_compress_heap(thr->prebuilt);
if (thr->prebuilt != nullptr && thr->prebuilt->blob_heap != nullptr) {
row_mysql_prebuilt_free_blob_heap(thr->prebuilt);
}

/* compress_heap was not used */
ut_ad(thr->prebuilt == 0 || thr->prebuilt->compress_heap == 0);
}

que_graph_free(purge_sys->query);
Expand Down

0 comments on commit 88c81fc

Please sign in to comment.