Skip to content

MDEV-37605 Extend mariadb-binlog to convert innodb based binary logs to legacy - #5276

Draft
tarunw07 wants to merge 15 commits into
MariaDB:mainfrom
tarunw07:MDEV-37605-convert-innodb-binlog-to-legacy
Draft

MDEV-37605 Extend mariadb-binlog to convert innodb based binary logs to legacy#5276
tarunw07 wants to merge 15 commits into
MariaDB:mainfrom
tarunw07:MDEV-37605-convert-innodb-binlog-to-legacy

Conversation

@tarunw07

@tarunw07 tarunw07 commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR implements MDEV-37605: Convert InnoDB Binary Logs to Legacy Format.

The goal of this work is to extend mariadb-binlog so that it can read InnoDB-based binary log files (.ibb) and generate legacy-format binary log files as output. This is intended to support compatibility use cases such as downgrades, migrations, and tooling that expects the legacy binlog format.

The current approach is to avoid fully deserializing and reserializing regular events. Since most event contents are already encoded in the .ibb file, the converter should mostly copy event bytes into the generated legacy binlog, while handling FORMAT_DESCRIPTION_EVENT, GTID_LIST_EVENT, BINLOG_CHECKPOINT_EVENT & ROTATE_EVENT.

Current status

This PR currently includes:

  • New option: --convert-engine-binlog
  • Initial legacy binlog writer/conversion path
  • FORMAT_DESCRIPTION_EVENT handling
  • Generate/write GTID_LIST_EVENT
  • Use the initial FSP_BINLOG_TYPE_GTID_STATE from the first page of the .ibb file to initialize the converter’s GTID state which is used for GTID_LIST_EVENT
  • Rotate the output binlog based on the size specified by --max-binlog-size.
  • Handle ROTATE_EVENT
  • Generate/write BINLOG_CHECKPOINT_EVENT
  • Update end_log_pos correctly in generated legacy binlog events
  • Test coverage for generating a .ibb file, converting it to legacy binlog format, and checking the converted output

Tests included

  • Basic test: Converts a basic InnoDB binlog and checks that the converted file can be replayed correctly.
  • Max-size test: Checks that the output binlog rotates when --max-binlog-size is provided.
  • GTID list test: Checks that the right GTID state is added to each converted binlog.
  • Replication test: Checks that a slave can replicate converted events.
  • Unsupported-options test: Checks that unsupported options and legacy binlog inputs are rejected.

Design notes

The current design direction is:

  • Copy regular events byte-by-byte where possible.
  • Add serializer/generation logic for events(FORMAT_DESCRIPTION_EVENT, GTID_LIST_EVENT, BINLOG_CHECKPOINT_EVENT & ROTATE_EVENT) that must be synthesized for legacy binlog output.
  • Use the initial FSP_BINLOG_TYPE_GTID_STATE from the first page of the .ibb file to initialize the converter’s GTID state.
  • Maintain GTID state during conversion, using rpl_binlog_state_base, so that GTID_LIST_EVENT can be generated correctly at the beginning of each output legacy binlog file.
  • Write a synthetic/dummy BINLOG_CHECKPOINT_EVENT near the beginning of each generated legacy binlog file.
  • Update end_log_pos for all the generated legacy binlog events.
  • Rotate output legacy binlogs when needed

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces the --convert-engine-binlog option to mariadb-binlog for converting InnoDB-based engine binlog files into legacy-format binary logs, along with helper functions for event serialization and new test suites. The code review identified several critical issues, including potential NULL pointer dereferences of gtid_state, fdev, gtid_set, and list, as well as a memory leak on write failures in write_event_to_legacy_binlog. Additionally, the feedback highlights return type mismatches, inconsistent boolean usage, and multiple coding standard violations regarding spacing after if keywords.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread client/mysqlbinlog.cc Outdated
Comment on lines +1951 to +1957
if (ev->get_type_code() == GTID_EVENT) {
rpl_gtid ev_gtid;
Gtid_log_event *gle= (Gtid_log_event*) ev;
ev_gtid= {gle->domain_id, gle->server_id, gle->seq_no};

gtid_state->update_nolock(&ev_gtid);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

If gtid_state is not initialized (e.g., if the conversion starts or encounters a GTID_EVENT before gtid_state is allocated), dereferencing it via gtid_state->update_nolock(&ev_gtid) will cause a crash. We should ensure gtid_state is initialized or add a defensive NULL check.

  if (ev->get_type_code() == GTID_EVENT) {
    rpl_gtid ev_gtid;
    Gtid_log_event *gle= (Gtid_log_event*) ev;
    ev_gtid= {gle->domain_id, gle->server_id, gle->seq_no};

    if (gtid_state)
      gtid_state->update_nolock(&ev_gtid);
  }

Comment thread client/mysqlbinlog.cc Outdated
Comment thread sql/log_event.cc
Comment on lines +2838 to +2866
bool
Gtid_list_log_event::to_packet(String *packet)
{
uint32 i;
uchar *p;
uint32 needed_length;

DBUG_ASSERT(count < 1<<28);

needed_length= packet->length() + get_data_size();
if (packet->reserve(needed_length))
return true;
p= (uchar *)packet->ptr() + packet->length();;
packet->length(needed_length);
int4store(p, (count & ((1<<28)-1)) | gl_flags);
p += 4;
/* Initialise the padding for empty Gtid_list. */
if (count == 0)
int2store(p, 0);
for (i= 0; i < count; ++i)
{
int4store(p, list[i].domain_id);
int4store(p+4, list[i].server_id);
int8store(p+8, list[i].seq_no);
p += 16;
}

return false;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

If list is NULL (which can happen if my_malloc failed during construction or if the event is invalid), dereferencing list[i] in the loop will cause a crash. We should check is_valid() or list != NULL before proceeding. This also cleans up a double semicolon typo on line 2850.

bool
Gtid_list_log_event::to_packet(String *packet)
{
  uint32 i;
  uchar *p;
  uint32 needed_length;

  if (!is_valid())
    return true;

  DBUG_ASSERT(count < 1<<28);

  needed_length= packet->length() + get_data_size();
  if (packet->reserve(needed_length))
    return true;
  p= (uchar *)packet->ptr() + packet->length();
  packet->length(needed_length);
  int4store(p, (count & ((1<<28)-1)) | gl_flags);
  p += 4;
  /* Initialise the padding for empty Gtid_list. */
  if (count == 0)
    int2store(p, 0);
  for (i= 0; i < count; ++i)
  {
    int4store(p, list[i].domain_id);
    int4store(p+4, list[i].server_id);
    int8store(p+8, list[i].seq_no);
    p += 16;
  }

  return false;
}

Comment thread sql/log_event_client.cc
Comment on lines +2362 to +2373
Gtid_list_log_event::Gtid_list_log_event(rpl_binlog_state_base *gtid_set)
: count(gtid_set->count_nolock()), gl_flags(0), list(0), sub_id_list(0)
{
cache_type= EVENT_NO_CACHE;
/* Failure to allocate memory will be caught by is_valid() returning false. */
if (count < (1<<28) &&
(list = (rpl_gtid *)my_malloc(PSI_INSTRUMENT_ME,
count * sizeof(*list) + (count == 0), MYF(MY_WME))))
{
gtid_set->get_gtid_list_nolock(list, count);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

If gtid_set is NULL, calling gtid_set->count_nolock() in the initializer list will result in a NULL pointer dereference and crash. We should add a defensive check to handle a NULL gtid_set gracefully.

Gtid_list_log_event::Gtid_list_log_event(rpl_binlog_state_base *gtid_set)
  : count(gtid_set ? gtid_set->count_nolock() : 0), gl_flags(0), list(0), sub_id_list(0)
{
  cache_type= EVENT_NO_CACHE;
  /* Failure to allocate memory will be caught by is_valid() returning false. */
  if (gtid_set && count < (1<<28) &&
      (list = (rpl_gtid *)my_malloc(PSI_INSTRUMENT_ME,
                          count * sizeof(*list) + (count == 0), MYF(MY_WME))))
  {
    gtid_set->get_gtid_list_nolock(list, count);
  }
}

Comment thread client/mysqlbinlog.cc Outdated
Comment thread client/mysqlbinlog.cc Outdated
Comment thread client/mysqlbinlog.cc Outdated
Comment thread client/mysqlbinlog.cc Outdated
Comment thread client/mysqlbinlog.cc Outdated
Comment thread client/mysqlbinlog.cc Outdated
@ParadoxV5
ParadoxV5 marked this pull request as draft June 25, 2026 05:22
@ParadoxV5 ParadoxV5 added GSoC External Contribution All PRs from entities outside of MariaDB Foundation, Corporation, Codership agreements. Replication Patches involved in replication labels Jun 25, 2026
@tarunw07
tarunw07 force-pushed the MDEV-37605-convert-innodb-binlog-to-legacy branch 3 times, most recently from 528c9e1 to da82682 Compare June 29, 2026 22:54
Comment thread client/mysqlbinlog.cc Outdated
Comment on lines +1940 to +1944
static bool init_output_legacy_binlog(FILE **out_file, char *out_name,
size_t out_name_len)
size_t out_name_len, bool rotate)
{
if (rotate)
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Having a separate rotate_output_legacy_binlog() that calls init_output_legacy_binlog() sounds better than shoving rotation inside “reïnitialization”, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, thanks for your input. Your suggestion seems like a much better design, so I’ll go ahead and make these changes. Thanks! :)

@tarunw07
tarunw07 force-pushed the MDEV-37605-convert-innodb-binlog-to-legacy branch 2 times, most recently from 2251204 to 937845a Compare July 16, 2026 10:04
@@ -1 +1,2 @@
--source include/not_embedded.inc

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@ParadoxV5 @bnestere Can you review/confirm this change? mysqlbinlog_convert_engine_binlog_basic test was failing because it was running on embedded server. I could've added this line in the test but this felt like a better option.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hm, it makes sense.
Compare with have_log_bin.inc, which literally has:

source include/not_embedded.inc;

In your tests, all but mysqlbinlog_convert_engine_binlog_basic calls have_binlog_format_row.inc, which in turn calls have_log_bin.inc.
I see that most have_innodb_binlog.inc callers aren’t “affected” either because they transitively call have_log_bin.inc.

The other tests I found call not_embedded.inc directly, like your alternative solution; they are:

  • binlog_in_engine.mariabackup_binlog_dir
  • binlog_in_engine.mariabackup_binlogs
  • mariabackup_slave_provision.inc callers

I wonder if it even makes sense to include the entire have_log_bin.inc in have_innodb_binlog.inc, not just --log-bin in have_innodb_binlog.opt?

Suggested change
--source include/not_embedded.inc
--source include/have_log_bin.inc

Though a shortcoming with have_log_bin.inc is that it will run the test in each of the three content formats (Mixed (default), Statement, Row).
This is unnecessary for tests that are more about the container than the contained contents, including yours, hence the calls to have_binlog_format_X.inc.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Could you elaborate a bit more on the shortcoming? I’m not sure I fully understood it.
My intention was for mysqlbinlog_convert_engine_binlog_basic to run with all three binlog formats: ROW, STATEMENT, and MIXED.

I did consider the change you suggested, but I was hesitant because have_log_bin.inc internally sources include/no_view_protocol.inc, and I wasn’t sure whether that could introduce any unexpected side effects. That said, logically, including have_log_bin.inc from have_innodb_binlog.inc does seem coherent.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

(answered in our fortnightly call)

@tarunw07
tarunw07 force-pushed the MDEV-37605-convert-innodb-binlog-to-legacy branch from 6e98f55 to 1557281 Compare July 26, 2026 20:38
@tarunw07
tarunw07 force-pushed the MDEV-37605-convert-innodb-binlog-to-legacy branch 2 times, most recently from 6748fdc to 842ff82 Compare August 6, 2026 20:45

@knielsen knielsen left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Some initial review comments on the draft patch in email comment.

@knielsen

knielsen commented Aug 11, 2026 via email

Copy link
Copy Markdown
Member

@knielsen

knielsen commented Aug 12, 2026 via email

Copy link
Copy Markdown
Member

@knielsen

knielsen commented Aug 12, 2026 via email

Copy link
Copy Markdown
Member

@knielsen

knielsen commented Aug 13, 2026 via email

Copy link
Copy Markdown
Member

@tarunw07
tarunw07 force-pushed the MDEV-37605-convert-innodb-binlog-to-legacy branch from 5091a7e to b6eae6e Compare August 16, 2026 05:27
@knielsen

knielsen commented Aug 16, 2026 via email

Copy link
Copy Markdown
Member

@knielsen

knielsen commented Aug 18, 2026 via email

Copy link
Copy Markdown
Member

@ParadoxV5

ParadoxV5 commented Aug 18, 2026 via email

Copy link
Copy Markdown
Contributor

@tarunw07

Copy link
Copy Markdown
Contributor Author

Hi Kristian,

tarun wadhwa @.***> writes:
While implementing your suggested changes for the PR <#5276>, I ran into a few questions that I wanted to discuss with you before proceeding.
Thanks for explaining these points, Tarun. At the root of these issues is the awkward way that code is being shared between mysqlbinlog and the server, with a mess of #include of .cc sources and sprinkled #ifdef. This is causing a number of difficulties when trying to share code, including these two issues that I hadn't realised when I first commented:

Here, it would be good to introduce Rotate_log_event::to_packet() and use it > in Rotate_log_event::write(), similar to how it's done for eg. > Gtid_list_log_event. Then we don't duplicate the logic for serialising the > data for Rotate_log_event. > Here, you recommended extracting Rotate_log_event::to_packet(). However, instead of creating a Rotate_log_event object, I am currently generating its serialized representation directly and writing it to the output binlog file.
Aha, right. Generally, we have the code shared for reading Log_event subclasses shared with the mysqlbinlog, but not the ability to create new Log_event objects. I'd forgotten that the to_packet() methods work off an existing object. Then I think your original code is better than trying to introduce Log_event constructors into the mysqlbinlog tool. The serialisation of the Rotate_log_event is very simple, it's not a huge issue to have it duplicated here, no reason to introduce the complexity of trying to import the Log_event constructors just for this. If we wanted to share the code for the serialisation of ROTATE_EVENT (and BINLOG_CHECKPOINT_EVENT?), the approach we could take is to introduce the counterparts to the static methods like Gtid_log_event::peek(), and reuse such methods both in mysqlbinlog and inside the server in Rotate_log_event::to_packet(). But for now it's probably simpler to just accept a bit of code duplication here as the best compromise.

Thanks for the explanations. I'm fine with your original code in these two cases then. - Kristian.

Thanks for the clarification. That makes sense.

I will go ahead with my original code and proceed with remaining review changes.

Thanks,
Tarun

@tarunw07
tarunw07 force-pushed the MDEV-37605-convert-innodb-binlog-to-legacy branch from b6eae6e to d664f90 Compare August 20, 2026 01:24

@bnestere bnestere left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hi @tarunw07 !

This is looking very nice, it is so close to the finish line :) Thanks for all your hard work!

I've left a few notes, though overall, I'd like @knielsen to verify a few of my questions before you actually fix any of the bigger points I raise.

Comment thread client/mysqlbinlog.cc
{
if (rotate_output_legacy_binlog(&output_legacy_binlog_file,
out_file_name, sizeof(out_file_name)))
goto err;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This will result in a double-free of the glob_description_event (i.e. at label err, ev is deleted; and then in cleanup(), glob_description_event is freed again). You should just be able to return ERROR_STOP here.

Comment thread client/mysqlbinlog.cc
}

/* Write the GTID_LIST_EVENT to the output legacy binlog file */
Gtid_list_log_event gle= Gtid_list_log_event(gtid_state);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

After instantiating this, it should be validated (i.e.

Suggested change
Gtid_list_log_event gle= Gtid_list_log_event(gtid_state);
Gtid_list_log_event gle= Gtid_list_log_event(gtid_state);
if (!gle.is_valid())
{
...

Comment thread client/mysqlbinlog.cc
error("The --convert-engine-binlog option does not support GTID values "
"for --start-position or --stop-position");
die(1);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should be comprehensive in this list, i.e. also deny: --database, --table, --start-position (you already deny --stop-position earlier), --start-datetime, --offset, --do-domain-ids, --ignore-domain-ids, --do-server-ids, --ignore-server-ids,

Comment thread client/mysqlbinlog.cc
OK_CONTINUE)
goto end;

if (opt_convert_engine_binlog) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

bracket should be on new line (and on the else at 4046)

Comment thread client/mysqlbinlog.cc Outdated
end:
if (output_legacy_binlog_file)
{
if (my_fclose(output_legacy_binlog_file, MYF(0)))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

hmm - something doesn't seem right here. dump_local_log_entries would convert one binlog-in-innodb file to legacy. Though IIRC, we decided we didn't want a 1-to-1 relationship between innodb-binlog files to legacy binlog files. So here you close and nullify the output_legacy_binlog_flie, though IIUC, that initializes a new legacy file (with new counter) when the next innodb binlog file is read (or around then)

So what is this hunk trying to do?

FOUND 1 /Gtid list/ in conv_listing.txt
FOUND 1 /Binlog checkpoint conv.000001/ in conv_listing.txt
FOUND 1 /Start: binlog v 4/ in conv_listing.txt
FOUND 1 /server v 13.1.0-MariaDB/ in conv_listing.txt

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We shouldn't output any hard server versions in result files, any time we have a new release, it would cause this to fail. I'd instead change this to use assert_grep.inc and assert the version is "correct" (there should be tons of examples of this in our existing suite).

--source include/reset_master.inc

# Fixed timestamp for deterministic event listings (with --timezone=GMT-3
# from the -master.opt file, matching binlog_in_engine.mysqlbinlog).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm there is no mysqlbinlog_ocnvet_engine_binlog_basic-master.opt file, did you miss uploading it?

Comment thread client/mysqlbinlog.cc
{

delete glob_description_event;
glob_description_event= (Format_description_log_event *) ev;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

My understanding is that if the innodb binlog writes an FDE, it was due to a restart, and will have some created timestamp. This will drop temporary tables. Which is what we want for this FDE, but if we overwrite the glob_description_event with this FDE, then that means any future legacy binlogs we open (say due to reaching --max-binlog-size) will be written with a created field filled out to this value. This will cause temporary tables to drop for each legacy log rotation.

I think we don't want to delete/overwrite the glob_description_event - I think all we want to do is simply print the new found FDE (or maybe actually rotate? I'm not sure it matters either way, but a simple print is cheaper). This would also override my just-below comment about a double-free on the glob_description_event.

Though I'd like to defer to @knielsen to confirm

@bnestere

Copy link
Copy Markdown
Contributor

@tarunw07 ah a couple more quick comments:

  • You have a lot of code comments like TODO: Tarun - these should be resolved now :) Please remove them
  • Please look through your full patch series on the command line with git show - you'll see a number of places with extraneous spaces at the end of lines. Please remove these.

@knielsen

knielsen commented Aug 24, 2026 via email

Copy link
Copy Markdown
Member

…n multiple innoDB binlog input files are provided
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

External Contribution All PRs from entities outside of MariaDB Foundation, Corporation, Codership agreements. GSoC Replication Patches involved in replication

Development

Successfully merging this pull request may close these issues.

4 participants