namalzi

← All posts

Digging into MariaDB's Source 3: Startup and Plugins — Until mariadbd Takes Its First Connection

MariaDB · Published

What happens between starting mariadbd and its first connection? We follow mysqld_main() in sql/mysqld.cc — how a line in my.cnf becomes a system variable, how plugin_init() registers and initializes everything as a plugin (even InnoDB and Aria), where ha_initialize_handlerton() lets an engine fill its handlerton, the signal thread and the poll() loop, and how shutdown runs startup in reverse — checked with the error log and gdb.

What this post covers

This is the third post in the "Digging into MariaDB's Source" series. In part 2 we drew the map: the server layer (sql/) and the storage engines (storage/) are separated by the handler/handlerton boundary, and one query travels connection → THD (the object holding all the state of one connection) → parsing → preparation → optimization → execution → handler → engine. This post zooms into the very front of the part 2 map — the area before any query arrives: from starting mariadbd until ready for connections. shows up in the error log, and from mariadb-admin shutdown until the process exits. We also look at the moment the bottom box of the map, the engine, gets attached to the server (the moment its handlerton is filled in).

The boxes of the part 2 map this post covers. The startup and plugins box at the top (main to mysqld_main; options, plugin_init(), the poll() loop) is blue, and an arrow labeled "waits in poll() for the first connection" leads from it to (1) Connection (part 4). Below come (2) THD and command (parts 4 and 5), (3) Parsing (part 6), (4) Preparation (part 7), (5) Optimization (part 8) and (6) Execution (part 9) in gray; from (6) one branch goes through (7) the handler boundary (part 9) to (8) Engine, the other to (9) Sending results (part 5). The (8) Engine box is blue too: the moment its handlerton is filled is in this post, the inside of InnoDB in parts 10, 11 and 12. A dotted line from the startup box to (8) Engine says "registers engines as plugins and fills each handlerton". The blue boxes are this post's scope. The startup and plugins box, gray on the part 2 map, is the one we zoom into this time. The gray ones are zoomed into one by one from part 4 on.

There are four questions to answer.

Item Value
Project MariaDB Server
Version 13.2.0 (main, alpha)
Commit c894d97781 (2026-10-19)
Main files sql/main.cc, sql/mysqld.cc, sql/sql_plugin.cc, sql/handler.cc, sql/set_var.cc, sql/sys_vars.cc, include/mysql/plugin.h, storage/innobase/handler/ha_innodb.cc

All line numbers in this post refer to this commit. How to get the same source and build the Docker image for the examples is in part 1. sql/mysqld.cc is 10,607 lines and sql/sql_plugin.cc is 4,646. This post follows only the main path of startup and plugins. Everything left out is listed by name and location in "What this post leaves out" at the end.

Terms to know first

mysqld_main() is the server's real main. The main() in sql/main.cc does nothing but call it, because the server body (almost every file in sql/) is built into a library and only main.cc becomes the executable on its own. The executable is called mariadbd, but the source files and function names keep the old name mysqld.

Option groups and load_defaults. my.cnf is split into bracketed groups such as [mysqld], [mariadbd] and [client-server]. load_defaults is a mysys/ function that collects the lines of the groups the server reads, turns them into command-line arguments (argv) of the form --socket=/tmp/mariadb.sock, and inserts them before the real command line. That is why, inside the server, the config file and the command line are read by the same code (handle_options()).

System variables (sys_var) are the things SHOW VARIABLES lists, such as max_connections and innodb_buffer_pool_size. The server's own ones are declared one by one in sql/sys_vars.cc as global objects of classes like Sys_var_ulong and Sys_var_enum; a plugin's ones are created when the plugin is loaded. One variable is one command-line option (section 2).

A plugin is a part you can plug in without changing the server executable. Storage engines, information_schema tables, authentication methods, audit logs, data types and SQL functions are all plugins. A plugin introduces itself with a single struct, st_maria_plugin. There are built-in (static) plugins linked into mariadbd, and dynamic plugins that live in .so files until INSTALL SONAME or --plugin-load-add loads them.

handlerton, as we saw in part 2, is a struct with one instance per storage engine — a table of function pointers such as the engine's commit, rollback and create (which makes the handler object that opens a table). In this post we look at who creates that table and who fills it. Since an engine is a plugin too, "initializing an engine plugin" means "creating a handlerton and letting the engine fill it".

The error log is the file set with log-error (/tmp/mariadbd.err in this series' examples). Despite the name, it records not only errors but also startup and shutdown progress as [Note] lines. In this post we connect its lines, one by one, to places in the source.

1. From main() to the first connection — the order at a glance

main() really is a one-liner.

int main(int argc, char **argv)
{
#ifdef _WIN32
  return mysqld_win_main(argc, argv);
#else
  return mysqld_main(argc, argv);
#endif
}

sql/main.cc lines 29–36

mysqld_main() is lines 6181–6634 of sql/mysqld.cc, about 450 lines. Let's see the order as a picture first.

The order of mysqld_main(). main() calls mysqld_main(); my_init() and load_defaults read my.cnf and prepend it to argv. sys_var_init() and handle_early_options() read the PARSE_EARLY options first, and get_options() in init_common_variables() reads the server options. init_signals() blocks signals and ignores SIGPIPE. init_server_components() opens the error log, starts the engines with plugin_init(), checks leftover options and sets the default engine. init_ssl() and network_init() prepare the TLS certificate and bind and listen on the sockets. start_signal_handler() starts the signal_hand thread, followed by acl_init(), grant_init(), udf_init() and Events::init(). "ready for connections." is logged and run_main_loop() waits in poll() inside handle_connections_sockets(). At the end, close_connections(), clean_up() and plugin_shutdown() clean up. Blue boxes are preparation steps; the main thread stays in the yellow box for the whole life of the server.

Here is the middle section in the source, starting where the engines are switched on, after init_common_variables() (line 6354) and init_signals() (line 6357).

  if (init_server_components())
    unireg_abort(1);

  init_ssl();
  network_init();
  // ... (WSREP recovery block omitted)
  /*
    init signals
    After this we can't quit by a simple unireg_abort
  */
  start_signal_handler();                               // Creates pidfile

  if (mysql_rm_tmp_tables() || acl_init(opt_noacl) ||
      my_tz_init((THD *)0, default_tz_name, opt_bootstrap))
    unireg_abort(1);

  if (!opt_noacl)
    (void) grant_init();

  udf_init();

sql/mysqld.cc lines 6425–6456

unireg_abort(1) means "give up on startup and exit" (where [ERROR] Aborting appears in the error log). The order we can read here:

  1. init_server_components() opens the error log file (lines 5266–5299), creates server parts such as metadata locking and the table definition cache, and initializes every plugin with plugin_init(). InnoDB's buffer pool allocation and crash recovery finish in here (sections 4 and 5).
  2. init_ssl(), network_init() prepare TLS and bind()/listen() on port 3306 and the Unix socket.
  3. start_signal_handler() starts a dedicated thread that receives SIGTERM and SIGHUP (section 6). As the comment says, from here on you can no longer simply quit with unireg_abort().
  4. acl_init(), grant_init() read the privilege tables (mysql.global_priv and others) into memory, which part 7 uses. Then udf_init() and Events::init() (line 6477) run.

After that, lines 6543–6560 log ready for connections., and run_main_loop() at line 6590 enters poll() in handle_connections_sockets(). When a stop request makes that function return, cleanup starts at line 6605 (section 7).

Example 1: watching the startup order in the error log

All examples in this post were run with MariaDB 13.2.0 (13.2.0-MariaDB-debug), a Debug build of the base commit (c894d97781) as is, in a Docker container (Ubuntu 24.04). We create an empty data directory, start the server and print the error log as it is. The helpers lab-init, lab-start and lab-stop inside the container are explained in part 1.

# Example written for this post (not part of the MariaDB source)
set -e
lab-init
lab-start
echo "===== 1. error log right after startup ====="
cat /tmp/mariadbd.err
echo "===== 2. TLS is on, with a certificate made at startup ====="
mariadb -t -e "SELECT @@have_ssl, @@ssl_cert, @@ssl_key;
               SHOW GLOBAL STATUS WHERE Variable_name IN ('Ssl_server_not_before','Ssl_server_not_after')"
echo "===== 3. shutdown with mariadb-admin (COM_SHUTDOWN) ====="
lab-stop
sed -n '/Normal shutdown/,$p' /tmp/mariadbd.err
echo "===== 4. shutdown with kill -TERM (signal_hand thread) ====="
lab-start
kill -TERM "$(cat /tmp/mariadbd.pid)"
while [ -e /tmp/mariadbd.pid ] && kill -0 "$(cat /tmp/mariadbd.pid)" 2>/dev/null; do sleep 0.2; done
sed -n '/Normal shutdown/,$p' /tmp/mariadbd.err

[Example ex/u03-startup-log/run.sh]

The output of the first group. A few [Note] lines, mostly InnoDB ones, are shortened to ....

===== 1. error log right after startup =====
2026-11-07  3:43:21 0 [Note] Starting MariaDB 13.2.0-MariaDB-debug source revision  server_uid vBnYwkGsBSVPH/FWuKqBU0vfwIc= as process 68
2026-11-07  3:43:21 0 [Note] Help others discover MariaDB. Star it on GitHub: https://github.com/MariaDB/server
...
2026-11-07  3:43:21 0 [Note] InnoDB: innodb_buffer_pool_size_max=8388608m, innodb_buffer_pool_size=128m
2026-11-07  3:43:21 0 [Note] InnoDB: Completed initialization of buffer pool
2026-11-07  3:43:21 0 [Note] InnoDB: Buffered log writes (block size=512 bytes)
2026-11-07  3:43:21 0 [Note] InnoDB: End of log at LSN=52261
...
2026-11-07  3:43:21 0 [Note] InnoDB: log sequence number 52261; transaction id 14
2026-11-07  3:43:21 0 [Note] InnoDB: Loading buffer pool(s) from /var/lib/mariadb-lab/ib_buffer_pool
2026-11-07  3:43:21 0 [Note] Plugin 'FEEDBACK' is disabled.
2026-11-07  3:43:21 0 [Note] InnoDB: Buffer pool(s) load completed at 261107  3:43:21
2026-11-07  3:43:26 0 [Note] Server socket created on IP: '0.0.0.0', port: '3306'.
2026-11-07  3:43:26 0 [Note] Server socket created on IP: '::', port: '3306'.
2026-11-07  3:43:26 0 [Note] mariadbd: Event Scheduler: Loaded 0 events
2026-11-07  3:43:26 0 [Note] mariadbd: ready for connections.
Version: '13.2.0-MariaDB-debug'  socket: '/tmp/mariadb.sock'  port: 3306  Source distribution

The times, server_uid, process number and LSN (52261 — a number marking a position in the redo log, part 12) can differ from run to run. Matching each line to its place in the source:

Error log Printed by Step in section 1
Starting MariaDB 13.2.0-MariaDB-debug … init_server_components(), right after opening the error log (lines 5313–5316) 1
Help others discover MariaDB … Lines 5317–5320 just below, only when SERVER_MATURITY_LEVEL is below stable — printed because this commit is alpha 1
Several InnoDB: … lines, ending with log sequence number 52261; transaction id 14 plugin_init() → InnoDB's innodb_init() → srv_start(). The last line is the end of srv_start() (srv0start.cc lines 2010–2017) 1
Plugin 'FEEDBACK' is disabled. test_plugin_options(), which reads plugin options (sql_plugin.cc lines 4393–4398) 1
Server socket created on IP: … network_init() → activate_tcp_port() (line 2591) 2
Event Scheduler: Loaded 0 events Events::load_events_from_db(), called by Events::init() (events.cc line 1288) 4
ready for connections. mysqld_main() lines 6543–6560; the message template is ER_STARTUP end

The order matches the picture in section 1 exactly. In particular, every InnoDB line comes before Server socket created.

Easy to get wrong: during crash recovery there is no port yet

network_init(), which creates the sockets, is called after init_server_components() returns. InnoDB's crash recovery (part 12) runs inside srv_start(), that is, inside init_server_components(), so while a long recovery is running, port 3306 is not open. A client that connects then is not told to wait — the connection is simply refused (there is no port). Conversely, from listen() until ready for connections. (reading privilege tables and so on) the port is open but the accept() loop is not running yet, so connections made in that window wait in the kernel's queue (back_log). If monitoring treats "the port is open" as "the server is ready", this small gap appears.

Easy to get wrong: the few-second gap in the log is TLS key generation

In the log above there is a 5-second gap between Buffer pool(s) load completed (3:43:21) and Server socket created (3:43:26). The length of the gap varies per run (when I ran it again it was 2 seconds, then 4). At first I assumed the Debug build was just slow, but starting the same image with --skip-ssl, which turns TLS off, printed both lines in the same second and the gap disappeared. In this version opt_use_ssl defaults to 1 (mysqld.cc line 1672), and if you give no certificate or key file, the server generates a new 4096-bit RSA key and a self-signed certificate on every start.

  if (!cert_file && !key_file)
  {
    if (!is_client)
    {
      EVP_PKEY *pkey;
      X509 *x509;
      if (!(pkey= vio_keygen()) || SSL_CTX_use_PrivateKey(ctx, pkey) < 1)
      {
        *error= SSL_INITERR_KEY;
        fprintf(stderr, "SSL error: %s\n", sslGetErrString(*error));
        DBUG_RETURN(1);
      }

      if (!(x509= vio_gencert(pkey)) || SSL_CTX_use_certificate(ctx, x509) < 1)

vio/viosslfactories.c lines 115–128

vio_keygen() (lines 50–70) makes the RSA key and vio_gencert() (lines 72–105) makes a certificate with CN=MariaDB Server, valid for 10 years (60*60*24*365*10 seconds). RSA key generation means searching for primes, so its time varies. The second group of example 1 confirms it.

===== 2. TLS is on, with a certificate made at startup =====
+------------+------------+-----------+
 @@have_ssl | @@ssl_cert | @@ssl_key |
+------------+------------+-----------+
 YES        | NULL       | NULL      |
+------------+------------+-----------+
+-----------------------+--------------------------+
 Variable_name         | Value                    |
+-----------------------+--------------------------+
 Ssl_server_not_after  | Nov  4 03:43:26 2036 GMT |
 Ssl_server_not_before | Nov  7 03:43:26 2026 GMT |
+-----------------------+--------------------------+

We gave no ssl_cert or ssl_key, yet have_ssl is YES, the certificate starts exactly at that startup time and ends exactly 10 years (3,650 days) later. If you point to certificate files in production, this time shrinks to reading the files.

2. Options and system variables — how a line in my.cnf becomes a global variable

load_defaults — turning the config file into a command line

The very beginning of mysqld_main():

  orig_argc= argc;
  orig_argv= argv;
  my_defaults_mark_files= TRUE;
  load_defaults_or_exit(MYSQL_CONFIG_NAME, load_default_groups, &argc, &argv);
  // ...
  sys_var_init();
  // ...
  int ho_error= handle_early_options();

sql/mysqld.cc lines 6209–6244

load_defaults_or_exit() looks up the groups in load_default_groups — "mysqld", "server", "mariadb", "mariadbd", "client-server" plus version-numbered groups from include/mysqld_default_groups.h — in my.cnf and prepends them to argv. my_defaults_mark_files= TRUE asks it to "insert markers saying which line came from which file". To check, we stopped at sys_var_init() with gdb and printed argv (part of example 5; the whole example is in section 5).

Breakpoint 1, sys_var_init () at /usr/src/mariadb/sql/set_var.cc:64
64	  DBUG_ENTER("sys_var_init");
#1  0x0000555555f9633d in mysqld_main (argc=11, argv=0x5555589d3f60) at /usr/src/mariadb/sql/mysqld.cc:6224
6224	  sys_var_init();
$1 = 11
$2 = {0x7fffffffef06 "/opt/mariadb/bin/mariadbd", 0x55555773cad0 "----file-marker----", 0x5555589d3e70 "/etc/my.cnf", 0x5555589d3e80 "--socket=/tmp/mariadb.sock", 0x5555589d3ea0 "--port=3306", 0x5555589d3eb0 "--user=root", 0x5555589d3ec0 "--datadir=/var/lib/mariadb-lab", 0x5555589d3ee0 "--log-error=/tmp/mariadbd.err", 0x5555589d3f00 "--pid-file=/tmp/mariadbd.pid", 0x55555773cad0 "----file-marker----", 0x55555773cb3f ""}

We started main() with no arguments (argc=1), yet argc in mysqld_main() became 11. The six lines of /etc/my.cnf came in as --socket=… and friends, wrapped by ----file-marker---- and the file name. The empty string ("") after the last ----file-marker---- means "the real command line starts here" (my_default.c lines 48–53, lines 470–474). Thanks to these markers, the server can later tell that "this value came from my.cnf".

Options are read in four passes

The resulting argv is not read in one go.

The four passes of option reading. Groups in my.cnf and the command line become argv through load_defaults. (1) handle_early_options() reads only PARSE_EARLY variables and leaves unknown ones in argv. (2) get_options() reads my_long_options and every server system variable and leaves unknown ones. (3) test_plugin_options() inside plugin_init() lets each plugin take its own options such as --innodb-buffer-pool-size or --feedback=ON. (4) init_server_components() runs handle_options() once more if remaining_argc is greater than 1. If nothing is left, startup continues; an option starting with loose- leaves just one Warning line; anything else is "unknown variable" (exit 7) or "unknown option" (exit 2) and Aborting. Each pass keeps unknown options in argv instead of dropping them, and hands them to the next pass. Only what is left at the very end is an error.

  1. handle_early_options() (line 6981) reads only variables marked PARSE_EARLY. To size performance_schema's memory, values such as max_connections must be known before anything else (lines 6252–6270).
  2. get_options() (line 9188, called from line 4331 in init_common_variables()) reads the my_long_options[] table and every server system variable. The comment at lines 9203–9204 gives the reason: "Skip unknown options so that they may be processed later by plugins".
  3. test_plugin_options() is called per plugin inside plugin_init() (section 4). The --innodb-… options are read here, right before InnoDB is initialized.
  4. Finally, init_server_components() checks what is left.
  /* we do want to exit if there are any other unknown options */
  if (remaining_argc > 1)
  {
    int ho_error;
    struct my_option removed_opts[]=
    // ... (removed_opts[] — table of options removed in older versions omitted)
    if ((ho_error= handle_options(&remaining_argc, &remaining_argv, removed_opts,
                                  mysqld_get_one_option))) {
#ifdef WITH_WSREP
      Wsrep_server_state::instance().disable_node_reset();
#endif

      unireg_abort(ho_error);
    }

sql/mysqld.cc lines 5645–5777

removed_opts[] is a table of options that existed in MySQL 5.6 or older MariaDB and were removed (innodb-file-format, secure-auth removed in 13.0, and so on). Options in that table pass with a warning instead of an error; if something not in the table is left over, handle_options() reports unknown variable (my_getopt.c lines 412–421). An option with a value (one containing =) ends with EXIT_UNKNOWN_VARIABLE (7); one without a value ends with unknown option and EXIT_UNKNOWN_OPTION (2). With a loose- prefix, the same place leaves only a warning instead of an error (lines 415–419).

Example 2: a mistyped option name — InnoDB is already up by then

We compared three cases: innodb-buffer-pool-size mistyped as sise; the same typo with a loose- prefix plus --validate-config (check the configuration and exit); and --skip-innodb, which turns InnoDB off.

# Example written for this post (not part of the MariaDB source)
lab-init
try() {
  echo "===== mariadbd $* ====="
  rm -f /tmp/mariadbd.err
  timeout 120 mariadbd "$@" > /tmp/out.txt 2>&1
  echo "exit code: $?"
  cat /tmp/mariadbd.err /tmp/out.txt 2>/dev/null | sed -E 's/^[0-9-]+ +[0-9:]+ [0-9]+ //' | grep -v "^\[Note\] InnoDB: \(!!!\|Compressed\|Number of\|Using\|Setting file\|File './ibtmp1'\|Buffer pool(s) load\|Loading buffer\|Opened 3\|128 rollback\|Completed init\|Buffered log\|End of log\)\|O_TMPFILE\|Help others" | head -20
}
try --innodb-buffer-pool-sise=64M
try --loose-innodb-buffer-pool-sise=64M --validate-config
try --skip-innodb

[Example ex/u03-options/run.sh]

===== mariadbd --innodb-buffer-pool-sise=64M =====
exit code: 7
[Note] Starting MariaDB 13.2.0-MariaDB-debug source revision  server_uid Qpcwx0GWB78W6kgk4K2JAgHnRoI= as process 75
[Note] InnoDB: innodb_buffer_pool_size_max=8388608m, innodb_buffer_pool_size=128m
[Note] InnoDB: log sequence number 52261; transaction id 14
[Note] Plugin 'FEEDBACK' is disabled.
[ERROR] mariadbd: unknown variable 'innodb-buffer-pool-sise=64M'
[ERROR] Aborting
===== mariadbd --loose-innodb-buffer-pool-sise=64M --validate-config =====
exit code: 0
[Warning] mariadbd: unknown variable 'loose-innodb-buffer-pool-sise=64M'
Configuration is valid, but warnings were reported; see above.
===== mariadbd --skip-innodb =====
exit code: 1
[Note] Starting MariaDB 13.2.0-MariaDB-debug source revision  server_uid Qpcwx0GWB78W6kgk4K2JAgHnRoI= as process 100
[Note] Plugin 'InnoDB' is disabled.
[Note] Plugin 'FEEDBACK' is disabled.
[ERROR] Unknown/unsupported storage engine: InnoDB
[ERROR] Aborting

The noisy InnoDB [Note] lines were removed with grep -v. The server_uid, process numbers and LSN can differ per run.

One system variable object = one option = one global variable

The declaration of max_connections in sql/sys_vars.cc:

// Default max_connections of 151 is larger than Apache's default max
// children, to avoid "too many connections" error in a common setup
static Sys_var_on_access_global<Sys_var_ulong,
                                PRIV_SET_SYSTEM_GLOBAL_VAR_MAX_CONNECTIONS>
Sys_max_connections(
       "max_connections", "The number of simultaneous clients allowed",
       PARSED_EARLY GLOBAL_VAR(max_connections), CMD_LINE(REQUIRED_ARG),
       VALID_RANGE(10, 100000),
       DEFAULT(MAX_CONNECTIONS_DEFAULT), BLOCK_SIZE(1), NO_MUTEX_GUARD,
       NOT_IN_BINLOG, ON_CHECK(0), ON_UPDATE(fix_max_connections));

sql/sys_vars.cc lines 1922–1931

It states the variable's name, description, range (10 to 100000), default (MAX_CONNECTIONS_DEFAULT = 151, sql/sql_const.h line 168) and GLOBAL_VAR(max_connections) — the C++ global variable that holds the value — all at once. The comment above explains the default of 151: it is larger than Apache httpd's default number of child processes, so a common setup does not hit "too many connections". httpd's number of child processes is decided by the MPM we saw in httpd part 8.

What the constructor of this object (the constructor of the parent class sys_var) does is the key.

  bzero(&option, sizeof(option));
  option.name= name_arg;
  option.id= getopt_id;
  option.comment= comment;
  option.arg_type= getopt_arg_type;
  option.value= (uchar **)global_var_ptr();
  option.def_value= def_val;
  option.app_type= this;
  option.var_type= flags & AUTO_SET ? GET_AUTO : 0;
  option.deprecation_substitute= substitute;
  // ...
  if (chain->last)
    chain->last->next= this;
  else
    chain->first= this;
  chain->last= this;

sql/set_var.cc lines 178–197

  1. It fills in one command-line option (my_option) itself. option.value points to global_var_ptr() — that very global variable max_connections. When handle_options() reads --max-connections=200, it writes 200 to that address.
  2. It appends itself to the global linked list all_sys_vars (lines 193–197). sys_var_init() puts this list into a hash, and sys_var_add_options() in get_options() walks the list to build the option table (set_var.cc lines 89–107).

SET GLOBAL max_connections=300 ends up in sys_var::update() → global_update() and changes the same global variable, and SELECT @@max_connections reads the same place. The config file, the command line and SQL all use the same single slot of memory.

Easy to get wrong: these objects are created before main()

Sys_max_connections is a static global object, so its constructor runs before main() even starts, when the C++ runtime initializes global objects. That is why the all_sys_vars list is already full when sys_var_init() is called at line 6224 of mysqld_main(). If you look for a function call that "registers this variable", you won't find one. Plugin variables (innodb_buffer_pool_size and so on) are different: test_plugin_options() in section 4 creates sys_var_pluginvar objects at run time when it loads a plugin, naming them <plugin name>_<variable name> (sql_plugin.cc lines 4318–4331). Two option names are created for each: --innodb-buffer-pool-size and --plugin-innodb-buffer-pool-size (the comment at line 4281).

The server remembers where each value came from

mysqld_get_one_option(), called for every option that is read:

  if (opt->app_type)
  {
    sys_var *var= (sys_var*) opt->app_type;
    // ...
    if (*filename)
    {
      var->origin_filename= filename;
      var->value_origin= sys_var::CONFIG;
    }
    else
      var->value_origin= sys_var::COMMAND_LINE;

sql/mysqld.cc lines 8591–8605

If filename is not empty — thanks to the file markers we saw — it records CONFIG; if empty, COMMAND_LINE. Values the server sizes on its own are AUTO, and a SET GLOBAL makes sys_var::update() change it to SQL (set_var.cc line 244). This is the GLOBAL_VALUE_ORIGIN column of information_schema.SYSTEM_VARIABLES.

Example 3: value origins and SET GLOBAL

We started the server with --max-connections=200 on the command line and asked where a few variables came from.

-- Example written for this post (not part of the MariaDB source)
-- 1. Where did each value come from: compile-time default / my.cnf / command line / auto-sized
SELECT '--- 1. where did each value come from' AS step;
SELECT VARIABLE_NAME, GLOBAL_VALUE, GLOBAL_VALUE_ORIGIN, DEFAULT_VALUE, READ_ONLY
  FROM information_schema.SYSTEM_VARIABLES
 WHERE VARIABLE_NAME IN ('MAX_CONNECTIONS','SOCKET','PORT','THREAD_HANDLING',
                         'INNODB_BUFFER_POOL_SIZE','TABLE_DEFINITION_CACHE','DEFAULT_STORAGE_ENGINE')
 ORDER BY VARIABLE_NAME;
SELECT VARIABLE_NAME, GLOBAL_VALUE_PATH
  FROM information_schema.SYSTEM_VARIABLES WHERE GLOBAL_VALUE_ORIGIN='CONFIG' ORDER BY VARIABLE_NAME;

-- 2. SET GLOBAL changes the same global variable, and the origin becomes SQL
SELECT '--- 2. SET GLOBAL' AS step;
SET GLOBAL max_connections = 300;
SELECT VARIABLE_NAME, GLOBAL_VALUE, GLOBAL_VALUE_ORIGIN
  FROM information_schema.SYSTEM_VARIABLES WHERE VARIABLE_NAME='MAX_CONNECTIONS';

[Example ex/u03-sysvars/demo.sql — run.sh starts the server with lab-start --max-connections=200, runs this, and finally tries SET GLOBAL thread_handling=… once]

Part of the output:

+-------------------------+---------------------------+---------------------+---------------------------+-----------+
 VARIABLE_NAME           | GLOBAL_VALUE              | GLOBAL_VALUE_ORIGIN | DEFAULT_VALUE             | READ_ONLY |
+-------------------------+---------------------------+---------------------+---------------------------+-----------+
 DEFAULT_STORAGE_ENGINE  | InnoDB                    | COMPILE-TIME        | InnoDB                    | NO        |
 INNODB_BUFFER_POOL_SIZE | 134217728                 | COMPILE-TIME        | 134217728                 | NO        |
 MAX_CONNECTIONS         | 200                       | COMMAND-LINE        | 151                       | NO        |
 PORT                    | 3306                      | CONFIG              | 0                         | YES       |
 SOCKET                  | /tmp/mariadb.sock         | CONFIG              |                           | YES       |
 TABLE_DEFINITION_CACHE  | 400                       | AUTO                | 400                       | NO        |
 THREAD_HANDLING         | one-thread-per-connection | COMPILE-TIME        | one-thread-per-connection | YES       |
+-------------------------+---------------------------+---------------------+---------------------------+-----------+
...
 DATADIR       | /etc/my.cnf       |
 LOG_ERROR     | /etc/my.cnf       |
 PID_FILE      | /etc/my.cnf       |
 PORT          | /etc/my.cnf       |
 SOCKET        | /etc/my.cnf       |
...
 MAX_CONNECTIONS | 300          | SQL                 |
...
ERROR 1238 (HY000) at line 1: Variable 'thread_handling' is a read only variable

3. What a plugin is — one declaration, two ways to build it

There are four plugin-related structs.

Four plugin-related structs. st_plugin_int is the server's record, one per plugin, with name, state, load_option, data and ref_count. Its plugin field points to st_maria_plugin, the read-only declaration the plugin exports (type, info, name, system_vars, init, deinit). Its plugin_dl field points to st_plugin_dl, one dlopen-ed .so (dl, handle, plugins array), only for dynamic plugins and 0 if built in. Its data field points to a handlerton (db_type, slot, create, commit, panic) for engines; the server mallocs it and the engine fills it. The plugin writes only the st_maria_plugin at the bottom. The server creates the other three.

maria_declare_plugin — the same source compiles two ways

InnoDB's declaration, near the very end of the file:

maria_declare_plugin(innobase)
{
  MYSQL_STORAGE_ENGINE_PLUGIN,
  &innobase_storage_engine,
  innobase_hton_name,
  plugin_author,
  "Supports transactions, row-level locking, foreign keys and encryption for tables",
  PLUGIN_LICENSE_GPL,
  innodb_init, /* Plugin Init */
  NULL, /* Plugin Deinit */
  MYSQL_VERSION_MAJOR << 8 | MYSQL_VERSION_MINOR,
  innodb_status_variables_export,/* status variables             */
  innobase_system_variables, /* system variables */
  PACKAGE_VERSION,
  MariaDB_PLUGIN_MATURITY_STABLE /* maturity */
},
i_s_innodb_trx,
i_s_innodb_locks,
// ... (26 entries from i_s_innodb_lock_waits to i_s_innodb_sys_virtual omitted)
i_s_innodb_tablespaces_encryption
maria_declare_plugin_end;

storage/innobase/handler/ha_innodb.cc lines 20762–20807

One pair of braces is one st_maria_plugin. The first element is the storage engine InnoDB (innodb_init in the init slot), followed by 29 more from i_s_innodb_trx to i_s_innodb_tablespaces_encryption, so one array holds 30 plugins. The 29 are INFORMATION_SCHEMA plugins that create tables such as information_schema.INNODB_TRX, INNODB_LOCKS and INNODB_BUFFER_PAGE. The INNODB_TRX table we use in part 11 is also something InnoDB exports as a plugin.

The maria_declare_plugin macro expands differently depending on how it is built.

#ifndef MYSQL_DYNAMIC_PLUGIN
// ...
#define MARIA_DECLARE_PLUGIN__(NAME, VERSION, PSIZE, DECLS)                   \
MYSQL_PLUGIN_EXPORT int VERSION;                                              \
int VERSION= MARIA_PLUGIN_INTERFACE_VERSION;                                  \
MYSQL_PLUGIN_EXPORT int PSIZE;                                                \
int PSIZE= sizeof(struct st_maria_plugin);                                    \
MYSQL_PLUGIN_EXPORT struct st_maria_plugin DECLS[];                           \
struct st_maria_plugin DECLS[]= {
#else
// ... (mysql_ variant of the macro omitted)
#define MARIA_DECLARE_PLUGIN__(NAME, VERSION, PSIZE, DECLS)                    \
MYSQL_PLUGIN_EXPORT int _maria_plugin_interface_version_;                      \
int _maria_plugin_interface_version_= MARIA_PLUGIN_INTERFACE_VERSION;          \
MYSQL_PLUGIN_EXPORT int _maria_sizeof_struct_st_plugin_;                       \
int _maria_sizeof_struct_st_plugin_= sizeof(struct st_maria_plugin);           \
MYSQL_PLUGIN_EXPORT struct st_maria_plugin _maria_plugin_declarations_[];      \
struct st_maria_plugin _maria_plugin_declarations_[]= {

#endif

include/mysql/plugin.h lines 136–167

CMake decides which. MYSQL_ADD_PLUGIN() in cmake/plugin.cmake links a static plugin into the server without MYSQL_DYNAMIC_PLUGIN and adds its name to a list (lines 255–268); for a dynamic one it defines MYSQL_DYNAMIC_PLUGIN and builds a MODULE library (lines 269–280). The template that list goes into is sql/sql_builtin.cc.in. Lines 37–47 of that file hold two arrays, mysql_optional_plugins[] and mysql_mandatory_plugins[], and CMake fills the @mysql_optional_plugins@ placeholders with lines like builtin_maria_innobase_plugin, to produce sql_builtin.cc in the build directory. The first three of the mandatory array (binlog, mysql_password, mhnsw) are written into the template directly. Since this generated file is not in the repository (and the example image did not keep its build directory), we printed the arrays inside the executable with gdb — reading only the executable, without starting a process.

===== 0. built-in plugin arrays generated at build time (no process) =====
0x294daa0 <mysql_mandatory_plugins>:	0x29487a0 <builtin_maria_binlog_plugin>	0x291f020 <builtin_maria_mysql_password_plugin>
0x294dab0 <mysql_mandatory_plugins+16>:	0x2948c20 <builtin_maria_mhnsw_plugin>	0x2954220 <builtin_maria_csv_plugin>
...
0x294db20 <mysql_mandatory_plugins+128>:	0x294dca0 <builtin_maria_online_alter_log_plugin>	0x0
0x294da40 <mysql_optional_plugins>:	0x2956560 <builtin_maria_innobase_plugin>	0x2950860 <builtin_maria_perfschema_plugin>
0x294da50 <mysql_optional_plugins+16>:	0x2b635a0 <builtin_maria_sequence_plugin>	0x2b636c0 <builtin_maria_auth_socket_plugin>
...
0x294da70 <mysql_optional_plugins+48>:	0x2b652a0 <builtin_maria_thread_pool_info_plugin>	0x2950460 <builtin_maria_partition_plugin>
0x294da80 <mysql_optional_plugins+64>:	0x0

Addresses differ per build, and the middle lines are shortened to .... The mandatory array has 17 entries and the optional one 8, both ending in 0. Each entry is one of the "plugin arrays" above, so there are more plugins than entries (InnoDB's array alone has 30). Remember that InnoDB, perfschema and partition are on the optional side; it explains LOAD_OPTION in section 4. The ap_prelinked_modules[] we saw in httpd part 6 was also an array in a build-generated modules.c. The two servers put static modules into the executable in almost the same way.

4. plugin_init() — register, switch on type by type, read mysql.plugin

Let's first picture what plugin_init() (lines 1619–1890), called at line 5618 of init_server_components(), does.

The flow of plugin_init(). From the mysql_mandatory_plugins and mysql_optional_plugins arrays generated into sql_builtin.cc at build time, register_builtin() registers every built-in plugin in the UNINITIALIZED state — mandatory ones as FORCE, optional ones as ON. MyISAM alone is initialized first as the first value of table_plugin. plugin_load_list() dlopens the .so files of --plugin-load-add and only registers them. Walking the plugin types in order (DAEMON, encryption, data type, function, storage engine, INFORMATION_SCHEMA …), plugin_initialize() calls test_plugin_options() and plugin_do_initialize(), which runs the per-type init function (ha_initialize_handlerton() for engines). plugin_load() reads the mysql.plugin table and registers INSTALLed plugins, and one more pass initializes only the newly registered ones. Finally failures are cleaned up, and a failed FORCE plugin fails startup. In the yellow box the server reads one of its own tables (mysql.plugin). For that, the table's engine must already be running.

First, register everything — just register

  for (builtins= mysql_mandatory_plugins; *builtins || mandatory; builtins++)
  {
    if (!*builtins)
    {
      builtins= mysql_optional_plugins;
      mandatory= false;
      if (!*builtins)
        break;
    }
    for (plugin= *builtins; plugin->info; plugin++)
    {
      // ... (option that skips the built-in InnoDB omitted)
      bzero(&tmp, sizeof(tmp));
      tmp.plugin= plugin;
      tmp.name= tmp_plugin_name;
      tmp.state= 0;
      tmp.load_option= mandatory ? PLUGIN_FORCE : PLUGIN_ON;
      // ... (applying override_plugin_load_policy omitted)
      tmp.state= PLUGIN_IS_UNINITIALIZED;
      if (register_builtin(plugin, &tmp, &plugin_ptr))
        goto err_unlock;
    }
  }

sql/sql_plugin.cc lines 1685–1722

It walks the two arrays in turn and only puts each plugin into plugin_array and the per-type hash plugin_hash[type] with register_builtin() (lines 1893–1913). The state is PLUGIN_IS_UNINITIALIZED, and plugins from the mandatory array get PLUGIN_FORCE, those from the optional array PLUGIN_ON (line 1705). The omitted lines 1709–1716 apply an exception table, override_plugin_load_policy (lines 213–234), which changes performance_schema to FORCE and feedback to OFF. Example 1's Plugin 'FEEDBACK' is disabled. starts here.

Next, lines 1724–1744 initialize MyISAM alone first and make it the first value of global_system_variables.table_plugin (the default engine). The comment gives the reason: "that should almost always succeed". Switching to the real default engine, InnoDB, happens after plugin_init() returns, in init_default_storage_engine() (mysqld.cc line 5871). Lines 1747–1751 open the .so files given with --plugin-load-add through plugin_load_list() and only register them.

Initialize type by type, read mysql.plugin, go around once more

  for(;;)
  {
    int error;
    for (i=0; i < MYSQL_MAX_PLUGIN_TYPE_NUM; i++)
    {
      HASH *hash= plugin_hash + plugin_type_initialization_order[i];
      for (uint idx= 0; idx < hash->records; idx++)
      {
        plugin_ptr= (struct st_plugin_int *) my_hash_element(hash, idx);
        if (plugin_ptr->state == PLUGIN_IS_UNINITIALIZED)
        {
          bool plugin_table_engine= lex_string_eq(&plugin_table_engine_name,
                                                  &plugin_ptr->name);
          bool opts_only= flags & PLUGIN_INIT_SKIP_INITIALIZATION &&
                         (flags & PLUGIN_INIT_SKIP_PLUGIN_TABLE ||
                          !plugin_table_engine);
          error= plugin_initialize(&tmp_root, plugin_ptr, argc, argv,
                                   opts_only);
    // ... (failures go to the reap list, HA_ERR_RETRY_INIT to the retry list — omitted)
    /* load and init plugins from the plugin table (unless done already) */
    if (flags & PLUGIN_INIT_SKIP_PLUGIN_TABLE ||
        plugin_table_engine_name.length == 0)
      break;

    mysql_mutex_unlock(&LOCK_plugin);
    plugin_load(&tmp_root);
    flags|= PLUGIN_INIT_SKIP_PLUGIN_TABLE;
    mysql_mutex_lock(&LOCK_plugin);
  }

sql/sql_plugin.cc lines 1774–1860

  1. Inside the outer for(;;), it follows the type order table plugin_type_initialization_order[], walks each type's hash and runs plugin_initialize() on everything still UNINITIALIZED. The order table (lines 142–156) is DAEMON → encryption → data type → function → storage engine → INFORMATION_SCHEMA → full-text parser → authentication → password validation → audit → replication → UDF. Encryption key plugins (MariaDB_ENCRYPTION_PLUGIN) must come before engines so InnoDB can open encrypted tablespaces, and engines must come before INFORMATION_SCHEMA so tables like INNODB_TRX can rely on their engine.
  2. Failed plugins are collected in the reap list. Those that returned HA_ERR_RETRY_INIT because a plugin they depend on is not there yet are collected separately and retried (lines 1805–1849).
  3. When a pass ends, plugin_load() reads the mysql.plugin table and registers the plugins loaded with INSTALL SONAME, sets the PLUGIN_INIT_SKIP_PLUGIN_TABLE flag and goes around once more. In the second pass only the newly registered ones are UNINITIALIZED, so only they get initialized.
  4. At the end it cleans up the reap list. If any of them is FORCE, plugin_init() fails and startup stops (lines 1865–1879).

mysql.plugin is read after the first pass because it is an ordinary server table. plugin_load() creates a connection-less THD with new THD(0), opens the table with open_and_lock_tables() and reads it row by row with READ_RECORD (lines 1933–2042). Opening a table needs its engine to be running, so the built-in engines have to be switched on in the first pass. That is also why lines 1753–1760 read the engine name from mysql/plugin.frm in advance. As the example below shows, this table's engine is Aria.

plugin_initialize() and plugin_do_initialize()

plugin_initialize() (lines 1523–1555) first reads that plugin's options with test_plugin_options() (pass 3 in section 2), and if the plugin is not disabled, calls plugin_do_initialize().

static int plugin_do_initialize(struct st_plugin_int *plugin, uint &state)
{
  DBUG_ENTER("plugin_do_initialize");
  mysql_mutex_assert_not_owner(&LOCK_plugin);
  plugin_type_init init= plugin_type_initialize[plugin->plugin->type];
  if (!init)
    init= plugin->plugin->init;
  if (init)
  {
    if (int ret= init(plugin))
    {
      /* Plugin init failed and did not requested a retry */
      if (ret != HA_ERR_RETRY_INIT)
        print_init_failed_error(plugin);
      DBUG_RETURN(ret);
    }
  }
  state= PLUGIN_IS_READY; // plugin->init() succeeded

sql/sql_plugin.cc lines 1483–1500

Line 1487 is the key. If the table has a per-type init function, that is called; otherwise the plugin's own init. The table:

plugin_type_init plugin_type_initialize[MYSQL_MAX_PLUGIN_TYPE_NUM]=
{
  0, ha_initialize_handlerton, 0, 0,initialize_schema_table,
  initialize_audit_plugin, 0, 0, 0, initialize_encryption_plugin,
  initialize_data_type_plugin, 0
};

sql/sql_plugin.cc lines 123–128

The slot number is the type number in include/mysql/plugin.h. Slot 1 (MYSQL_STORAGE_ENGINE_PLUGIN) is ha_initialize_handlerton, slot 4 (MYSQL_INFORMATION_SCHEMA_PLUGIN) is initialize_schema_table, and slots 5 (audit), 9 (encryption) and 10 (data type) have functions too. For the zero slots (daemon, authentication and so on) the plugin's init is called directly. An engine plugin's init (InnoDB's innodb_init) is not called by the server directly; ha_initialize_handlerton() calls it after preparing a handlerton — the topic of section 5. On success the state becomes PLUGIN_IS_READY and the plugin's status variables (the Innodb_… entries in SHOW STATUS) are registered (lines 1500–1519).

Example 4: the plugin list, INSTALL SONAME, and a restart

-- Example written for this post (not part of the MariaDB source)
-- 1. Right after startup: plugins per type (PLUGIN_LIBRARY IS NULL means built into mariadbd)
SELECT '--- 1. plugins right after startup' AS step;
SELECT PLUGIN_TYPE, COUNT(*) AS n, SUM(PLUGIN_LIBRARY IS NULL) AS builtin, SUM(PLUGIN_STATUS='ACTIVE') AS active
  FROM information_schema.PLUGINS GROUP BY PLUGIN_TYPE ORDER BY n DESC, PLUGIN_TYPE;
SELECT PLUGIN_NAME, PLUGIN_STATUS, PLUGIN_TYPE, LOAD_OPTION
  FROM information_schema.PLUGINS
 WHERE PLUGIN_TYPE='STORAGE ENGINE' OR PLUGIN_STATUS<>'ACTIVE' ORDER BY PLUGIN_TYPE, PLUGIN_NAME;
-- Which engine holds the mysql.plugin table itself?
SELECT TABLE_NAME, ENGINE FROM information_schema.TABLES
 WHERE TABLE_SCHEMA='mysql' AND TABLE_NAME='plugin';

-- 2. Load a dynamic plugin: the server dlopen()s a .so from plugin_dir
SELECT '--- 2. INSTALL SONAME' AS step;
SELECT @@plugin_dir;
INSTALL SONAME 'metadata_lock_info';
SELECT PLUGIN_NAME, PLUGIN_STATUS, PLUGIN_TYPE, PLUGIN_LIBRARY, LOAD_OPTION
  FROM information_schema.PLUGINS WHERE PLUGIN_LIBRARY IS NOT NULL;
SELECT * FROM mysql.plugin;
-- A new information_schema table now exists
SELECT COUNT(*) AS locks_held FROM information_schema.METADATA_LOCK_INFO;

[Example ex/u03-install-soname/demo1.sql]

After running this, run.sh restarts the server and prints step 3 (asking again only for PLUGIN_LIBRARY IS NOT NULL) and step 4 (UNINSTALL PLUGIN InnoDB), then restarts with lab-start --plugin-load-add=ha_blackhole and prints step 5. Only part of the output is shown (at the ... marks we removed the step title tables and table headers, and for steps 2, 3 and 5 kept only the result rows).

+--------------------+----+---------+--------+
 PLUGIN_TYPE        | n  | builtin | active |
+--------------------+----+---------+--------+
 INFORMATION SCHEMA | 42 |      42 |     41 |
 FUNCTION           | 13 |      13 |     13 |
 STORAGE ENGINE     | 10 |      10 |     10 |
 DATA TYPE          |  6 |       6 |      6 |
 AUTHENTICATION     |  3 |       3 |      3 |
 DAEMON             |  3 |       3 |      3 |
+--------------------+----+---------+--------+
+--------------------+---------------+--------------------+-------------+
 PLUGIN_NAME        | PLUGIN_STATUS | PLUGIN_TYPE        | LOAD_OPTION |
+--------------------+---------------+--------------------+-------------+
 FEEDBACK           | DISABLED      | INFORMATION SCHEMA | OFF         |
 Aria               | ACTIVE        | STORAGE ENGINE     | FORCE       |
 CSV                | ACTIVE        | STORAGE ENGINE     | FORCE       |
 InnoDB             | ACTIVE        | STORAGE ENGINE     | ON          |
 MEMORY             | ACTIVE        | STORAGE ENGINE     | FORCE       |
 MRG_MyISAM         | ACTIVE        | STORAGE ENGINE     | FORCE       |
 MyISAM             | ACTIVE        | STORAGE ENGINE     | FORCE       |
 partition          | ACTIVE        | STORAGE ENGINE     | ON          |
 PERFORMANCE_SCHEMA | ACTIVE        | STORAGE ENGINE     | FORCE       |
 SEQUENCE           | ACTIVE        | STORAGE ENGINE     | ON          |
 SQL_SEQUENCE       | ACTIVE        | STORAGE ENGINE     | FORCE       |
+--------------------+---------------+--------------------+-------------+
+------------+--------+
 TABLE_NAME | ENGINE |
+------------+--------+
 plugin     | Aria   |
+------------+--------+
...
 METADATA_LOCK_INFO | ACTIVE        | INFORMATION SCHEMA | metadata_lock_info.so | ON          |
...
 METADATA_LOCK_INFO | ACTIVE        | metadata_lock_info.so | ON          |
...
ERROR 1619 (HY000) at line 1: Built-in plugins cannot be deleted
...
 BLACKHOLE          | ACTIVE        | ha_blackhole.so       | ON          |
 METADATA_LOCK_INFO | ACTIVE        | metadata_lock_info.so | ON          |

Easy to get wrong: FORCE and ON — why --skip-innodb works but --skip-aria doesn't

test_plugin_options() creates one more option per plugin, named after the plugin (--innodb, --feedback …), which decides whether to switch it on. But the code that uses this value is wrapped in if (!plugin_is_forced(tmp)).

    if (!plugin_is_forced(tmp))
    {
      plugin_load_option= (enum_plugin_load_option) *(ulong*) opts[0].value;
      disable_plugin= (plugin_load_option == PLUGIN_OFF);
      tmp->load_option= plugin_load_option;
    }
  // ...
  if (disable_plugin)
  {
    if (global_system_variables.log_warnings && !opt_help)
      sql_print_information("Plugin '%s' is disabled.",
                            tmp->name.str);
    goto err;

sql/sql_plugin.cc lines 4373–4398

A FORCE plugin is not affected by this option, so --skip-aria cannot turn Aria off. InnoDB is ON, so --skip-innodb turns it off and, as in the third group of example 2, Plugin 'InnoDB' is disabled. is printed. But since the default of the default engine (default_storage_engine) is "InnoDB" (mysqld.cc lines 4285–4286), init_default_storage_engine() after plugin_init() cannot find the engine and stops startup with Unknown/unsupported storage engine: InnoDB (lines 5091–5096). To turn InnoDB off, you also have to change --default-storage-engine.

5. ha_initialize_handlerton() — where an engine fills its handlerton

Drawn as a sequence, the initialization of one engine plugin looks like this.

The order in which the InnoDB engine plugin is initialized. On the storage engine type's turn, plugin_init() calls plugin_do_initialize(), which calls ha_initialize_handlerton(), the entry plugin_type_initialize[1]. ha_initialize_handlerton() mallocs a zero-filled handlerton, stores it in plugin->data and calls plugin->plugin->init(hton), that is, innodb_init(). innodb_init() fills function pointers such as hton's commit, rollback, create and panic, and calls srv_start(). srv_start() prepares the buffer pool, redo log and undo, runs crash recovery if needed, and returns DB_SUCCESS. ha_initialize_handlerton() checks db_type, gets a slot and stores hton in installed_htons[db_type]. plugin_do_initialize() sets the state to PLUGIN_IS_READY and registers the status variables. The server mallocs the handlerton; the engine fills it.

Server side: create an empty table and hand it over

  hton= (handlerton *)my_malloc(key_memory_handlerton, sizeof(handlerton),
                                MYF(MY_WME | MY_ZEROFILL));
  // ... (out-of-memory handling omitted)
  hton->tablefile_extensions= hton_no_exts;
  hton->discover_table_names= hton_ext_based_table_discovery;
  hton->drop_table= hton_drop_table;

  hton->slot= HA_SLOT_UNDEF;
  /* Historical Requirement */
  plugin->data= hton; // shortcut for the future
  if (plugin->plugin->init && (ret= plugin->plugin->init(hton)))
    goto err;
  // ... (discovery defaults and db_type conflict check omitted)
  if ((ret= setup_transaction_participant(plugin)))
    goto err_deinit;

  installed_htons[hton->db_type]= hton;

sql/handler.cc lines 725–792

  1. It mallocs a zero-filled handlerton and puts defaults in a few slots (lines 725–739). Zero-filling matters: function pointers the engine does not fill stay NULL, and the server checks for NULL before using them (for example if (hton->panic) in ha_finalize_handlerton()).
  2. It hangs it on the st_plugin_int with plugin->data= hton and calls plugin->plugin->init(hton) — the engine's init (lines 741–743). init takes a void *, but an engine plugin receives a handlerton *.
  3. On return it checks that the db_type the engine chose does not clash, and assigns a free number if it clashes or is missing (lines 767–787). db_type is the old engine number written into .frm files.
  4. setup_transaction_participant() (lines 674–715) assigns a slot. The slot is the index of the per-engine cell inside THD (thd->ha_data[slot]); InnoDB keeps each connection's transaction object trx_t in that cell (ha_innodb.cc line 1810, thd_get_ha_data(thd, innodb_hton_ptr)). If the engine has prepare, it also increments the number of engines taking part in two-phase commit (total_ha_2pc) — which connects to the XA story in part 12.
  5. installed_htons[db_type]= hton puts it on the numbered list.

Engine side: the smallest example, and InnoDB

How simple the engine's job is shows in the init of SQL_SEQUENCE, the internal engine behind CREATE SEQUENCE.

static int sequence_initialize(void *p)
{
  handlerton *local_sequence_hton= (handlerton *)p;
  DBUG_ENTER("sequence_initialize");

  local_sequence_hton->db_type= DB_TYPE_SEQUENCE;
  local_sequence_hton->create= sequence_create_handler;
  local_sequence_hton->panic= sequence_end;
  local_sequence_hton->flags= (HTON_NOT_USER_SELECTABLE |
                               // ... (remaining HTON_ flags omitted)
                               HTON_NO_PARTITION);
  DBUG_RETURN(0);
}

sql/ha_sequence.cc lines 544–561

It puts a number, two function pointers (create, panic) and property flags (flags) into the handlerton it received, and that's it. Because of HTON_HIDDEN and HTON_NOT_USER_SELECTABLE, users cannot pick it with ENGINE=SQL_SEQUENCE.

InnoDB's innodb_init() does the same job, much more of it.

static int innodb_init(void* p)
{
        DBUG_ENTER("innodb_init");
        handlerton* innobase_hton= static_cast<handlerton*>(p);
        innodb_hton_ptr = innobase_hton;

        innobase_hton->db_type = DB_TYPE_INNODB;
        // ...
        innobase_hton->commit = innobase_commit;
        innobase_hton->rollback = innobase_rollback;
        innobase_hton->prepare = innobase_xa_prepare;
        // ... (… dozens of function pointers, innodb_init_params(), performance_schema registration omitted)
        bool    create_new_db = false;

        /* Check whether the data files exist. */
        dberr_t err = srv_sys_space.check_file_spec(&create_new_db, 5U << 20);
        // ...
        err = srv_start(create_new_db);

        if (err != DB_SUCCESS) {
                if (!recv_sys.rpo) {
                        recv_sys.rpo = srv_read_only_mode;
                }
                innodb_shutdown();
                goto init_abort;
        }

        srv_was_started = true;

storage/innobase/handler/ha_innodb.cc lines 4295–4475

The first part (lines 4301–4390) is all innobase_hton->… = …: innobase_commit for commit, innobase_rollback for rollback, innobase_xa_prepare for prepare, innobase_create_handler for create (which makes the handler that opens a table), and innobase_end for panic (called at shutdown, section 7). The function pointers of a recently added feature that keeps the binlog inside InnoDB (binlog_storage_engine) are lined up here too (lines 4365–4390, part 12).

The second part tidies up the innodb_… variables with innodb_init_params() (line 4416), checks whether the data files exist (line 4451) and calls srv_start() (line 4465). srv_start() (srv0start.cc line 1303) creates the buffer pool (buf_pool.create() at line 1383), prepares the redo log (line 1390), reads the redo log from the checkpoint and re-applies it if needed (recv_recovery_from_checkpoint_start() at line 1570). Example 1's InnoDB: Completed initialization of buffer pool and End of log at LSN=52261 were printed in there. The buffer pool is covered in part 10, redo and recovery in part 12. If srv_start() fails, innodb_shutdown() cleans up and an error is returned; since InnoDB is ON rather than FORCE, the server tries to go on without InnoDB and — as we saw — stops because it cannot find the default engine.

Example 5: stopping at ha_initialize_handlerton with gdb and printing engine names

We started mariadbd under gdb, put a dprintf (print a line without stopping) on ha_initialize_handlerton to print engine names, and stopped once at srv_start to see the backtrace. Then we stopped at handle_connections_sockets to print the thread list (section 6), and in a second run stopped at innodb_shutdown during shutdown (section 7).

# Example written for this post (not part of the MariaDB source)
set pagination off
set print thread-events off
break sys_var_init
dprintf ha_initialize_handlerton,"ha_initialize_handlerton: %s\n", ((st_plugin_int*)plugin_)->name.str
break srv_start
break handle_connections_sockets
run
up
print argc
print *argv@argc
continue
bt
continue
info threads
kill

[Example ex/u03-gdb-handlerton/gdb-start.cmds — run.sh runs gdb -batch -x gdb-start.cmds --args /opt/mariadb/bin/mariadbd after lab-init, and removes the [New Thread …] lines printed whenever a thread starts with grep -v. The shutdown file gdb-stop.cmds is a short one that sets break innodb_shutdown and does run, bt, continue; while that gdb runs in the background, the script sends mariadb-admin shutdown]

The engine names and the backtrace:

ha_initialize_handlerton: MyISAM
ha_initialize_handlerton: MEMORY
ha_initialize_handlerton: CSV
ha_initialize_handlerton: PERFORMANCE_SCHEMA
ha_initialize_handlerton: Aria
ha_initialize_handlerton: MRG_MyISAM
ha_initialize_handlerton: partition
ha_initialize_handlerton: InnoDB

Thread 1 "mariadbd" hit Breakpoint 3, srv_start (create_new_db=false) at /usr/src/mariadb/storage/innobase/srv/srv0start.cc:1304
1304	{
#0  srv_start (create_new_db=false) at /usr/src/mariadb/storage/innobase/srv/srv0start.cc:1304
#1  0x0000555556a6a9c8 in innodb_init (p=0x5555589d7230) at /usr/src/mariadb/storage/innobase/handler/ha_innodb.cc:4465
#2  0x000055555661ec05 in ha_initialize_handlerton (plugin_=0x555558c6d3b0) at /usr/src/mariadb/sql/handler.cc:742
#3  0x00005555561b7762 in plugin_do_initialize (plugin=0x555558c6d3b0, state=@0x7fffffffb7a8: 4) at /usr/src/mariadb/sql/sql_plugin.cc:1492
#4  0x00005555561b7b09 in plugin_initialize (tmp_root=0x7fffffffbe00, plugin=0x555558c6d3b0, argc=0x5555580c26d0 <remaining_argc>, argv=0x5555589d3f60, options_only=false) at /usr/src/mariadb/sql/sql_plugin.cc:1546
#5  0x00005555561b892a in plugin_init (argc=0x5555580c26d0 <remaining_argc>, argv=0x5555589d3f60, flags=0) at /usr/src/mariadb/sql/sql_plugin.cc:1790
#6  0x0000555555f94dbd in init_server_components () at /usr/src/mariadb/sql/mysqld.cc:5618
#7  0x0000555555f967e1 in mysqld_main (argc=11, argv=0x5555589d3f60) at /usr/src/mariadb/sql/mysqld.cc:6425
#8  0x0000555555f88d2d in main (argc=1, argv=0x7fffffffed38) at /usr/src/mariadb/sql/main.cc:34
ha_initialize_handlerton: SQL_SEQUENCE
ha_initialize_handlerton: SEQUENCE

Addresses and argument values depend on the environment (gdb starts the program with address randomization turned off, so rerunning on the same image gave the same values).

Easy to get wrong: InnoDB's deinit is NULL

In InnoDB's declaration (lines 20762–20777), the deinit slot is NULL. So where is InnoDB shut down? innodb_init() set innobase_hton->panic = innobase_end (line 4329), and the engines' per-type cleanup function ha_finalize_handlerton() calls hton->panic(hton, HA_PANIC_CLOSE). An engine's way out goes through a handlerton function pointer, not the plugin's deinit. Section 7's backtrace confirms it.

6. Waiting for the first connection — the signal thread and the poll() loop

Signals are received by one dedicated thread

init_signals() (lines 3349–3419) puts SIGTERM, SIGHUP, SIGQUIT and SIGPIPE — plus SIGINT and SIGTSTP unless started with --gdb — into a set and blocks them with sigprocmask() and pthread_sigmask() (lines 3385–3417). Every thread created after this inherits the mask, so no connection thread is interrupted by these signals. Then the single signal_hand thread started by start_signal_handler() waits for and receives them with sigwait.

    while ((error= my_sigwait(&set, &sig, &origin)) == EINTR) /* no-op */;

    if (abort_loop)
      break;

    switch (sig) {
    case SIGTERM:
    case SIGQUIT:
#ifdef EXTRA_DEBUG
      sql_print_information("Got signal %d to shutdown server",sig);
#endif
      /* switch to the old log message processing */
      logger.set_handlers(global_system_variables.sql_log_slow ? LOG_FILE:LOG_NONE,
                          opt_log ? LOG_FILE:LOG_NONE);

      break_connect_loop();
      DBUG_ASSERT(abort_loop);
      break;
    case SIGHUP:
      // ... (SIGHUP: reload_acl_and_cache() and reopening logs omitted)
      break;

sql/mysqld.cc lines 3498–3539

On SIGTERM or SIGQUIT it wakes the main thread's loop with break_connect_loop() (section 7); on SIGHUP it reopens the logs and flushes the privilege and table caches (reload_acl_and_cache(), similar to FLUSH LOGS). A signal handler can do very little safely, but receiving signals with sigwait in an ordinary thread lets you take locks and do heavy work. This thread also writes the pid file (create_pid_file() at lines 3474–3476), so /tmp/mariadbd.pid appears only after start_signal_handler(). httpd in httpd part 5 went around main()'s loop once more on a signal to re-read its configuration, but MariaDB's SIGHUP does not re-read the config file; the flags passed to reload_acl_and_cache() above are only the log, table, privilege, thread and host caches.

The poll() loop

handle_connections_sockets(), called by run_main_loop():

  int termination_fds[2];
  if (pipe(termination_fds))
  // ... (pipe creation failure handling omitted)
  struct pollfd event_fd;
  event_fd.fd= termination_fds[0];
  event_fd.events= POLLIN;
  fds.push(event_fd);

  sd_notify(0, "READY=1\n"
            "STATUS=Taking your SQL requests now...\n");

  DBUG_PRINT("general",("Waiting for connections."));
  while (!abort_loop)
  {
    retval= poll(fds.get_pos(0), fds.size(), -1);
      // ... (poll() error handling and finding which socket omitted)
      new_sock= mysql_socket_accept(key_socket_client_connection, sock,
                                    (struct sockaddr *)(&cAddr),
                                    &length);
      if (mysql_socket_getfd(new_sock) != INVALID_SOCKET)
        handle_accepted_socket(new_sock, sock);

sql/mysqld.cc lines 6865–6930

  1. It puts the listening sockets created by network_init() (TCP IPv4 and IPv6, the Unix socket) into a pollfd array (lines 6856–6864).
  2. It creates one more pipe and puts its read end into the same array (lines 6865–6882). When a stop request comes, someone writes to this pipe, and poll() wakes up (section 7).
  3. It tells systemd READY=1 (lines 6884–6885). This is the moment systemctl start mariadb returns.
  4. poll(…, -1) waits with no timeout. When a new connection arrives on a socket, it accept()s it and passes it to handle_accepted_socket(). Attaching a thread to each connection after that is covered in part 4.

The two threads that wait for the first connection. At the top, the signal_hand thread (started by start_signal_handler()) waits in my_sigwait() for SIGTERM, SIGQUIT, SIGHUP and so on; every other thread blocks signals. On SIGHUP it calls reload_acl_and_cache() to reopen logs and flush caches; on SIGTERM or SIGQUIT, break_connect_loop() sets abort_loop to 1 and writes 8 bytes into the termination pipe. At the bottom, the main thread's handle_connections_sockets() puts the listening sockets made by network_init() (TCP IPv4, IPv6, Unix socket) and the termination pipe's read end termination_fds[0] into a pollfd array and waits in poll(…, -1) with no time limit. If a socket wakes it, it calls mysql_socket_accept() and then handle_accepted_socket() (Part 4); if the pipe wakes it, it sees abort_loop, leaves the loop and returns to mysqld_main() (Section 7) The thread that receives signals and the thread that waits for connections are separate, and a single pipe links them. The list of awaited signals was checked against signal_hand's sigaddset calls (mysqld.cc lines 3466–3472).

The main thread stays in this loop for the server's whole life. The thread list when example 5 stopped at handle_connections_sockets:

Thread 1 "mariadbd" hit Breakpoint 4, handle_connections_sockets () at /usr/src/mariadb/sql/mysqld.cc:6846
6846	{
  Id   Target Id                                         Frame
* 1    Thread 0x7ffff6fe2a00 (LWP 94) "mariadbd"         handle_connections_sockets () at /usr/src/mariadb/sql/mysqld.cc:6846
  2    Thread 0x7ffff63e86c0 (LWP 97) "statement_timer"  0x00007ffff730ae51 in ?? () from /lib/x86_64-linux-gnu/libc.so.6
  3    Thread 0x7fffe1d8e6c0 (LWP 98) "checkpoint_bg"    0x00007ffff730ae51 in ?? () from /lib/x86_64-linux-gnu/libc.so.6
  4    Thread 0x7fffe118c6c0 (LWP 99) "io_uring_wait"    0x00007ffff7edfab0 in ?? () from /lib/x86_64-linux-gnu/liburing.so.2
  5    Thread 0x7fffe098b6c0 (LWP 100) "ib_tpool_worker" 0x00007ffff730ae51 in ?? () from /lib/x86_64-linux-gnu/libc.so.6
  6    Thread 0x77ffdbfff6c0 (LWP 101) "page_cleaner"    0x00007ffff730ae51 in ?? () from /lib/x86_64-linux-gnu/libc.so.6
...
  11   Thread 0x7ffff41aa6c0 (LWP 106) "manager"         0x00007ffff730ae51 in ?? () from /lib/x86_64-linux-gnu/libc.so.6
  12   Thread 0x7ffff41596c0 (LWP 107) "signal_handler"  0x00007ffff72b7fc8 in sigtimedwait () from /lib/x86_64-linux-gnu/libc.so.6
  13   Thread 0x7ffff410e6c0 (LWP 108) "mariadbd"        0x00007ffff730ae51 in ?? () from /lib/x86_64-linux-gnu/libc.so.6

LWP numbers and some thread addresses differ per run, and repeated names are shortened to .... Even before the first connection there are 13 threads. Number 1 is the main thread (now in handle_connections_sockets), and number 12, signal_handler, is the signal_hand above, waiting in sigtimedwait. my_sigwait() (include/my_pthread.h lines 176–187) calls sigwaitinfo() when HAVE_SIGWAITINFO is set; seeing sigtimedwait in the backtrace suggests the C library (glibc) implements it that way (I did not check this in the glibc source). page_cleaner, ib_tpool_worker, io_uring_wait and checkpoint_bg are InnoDB threads started by srv_start(), and manager is the server's manager thread. There is not a single connection thread yet. In part 4 we compare this list with one where each connection gets a thread.

7. Shutdown — startup in reverse

The way to stop comes in through two doors and meets in one function.

The shutdown flow. mariadb-admin shutdown or a SHUTDOWN statement comes in on a connection thread through COM_SHUTDOWN to kill_mysql(); kill -TERM comes in when sigwait in the signal_hand thread wakes up. Both reach break_connect_loop(), which sets abort_loop to 1 and writes 8 bytes to the termination pipe. poll() in the main thread wakes up and handle_connections_sockets() returns; with the Normal shutdown log line, close_connections() closes the sockets and ends the connections. Through ha_pre_shutdown() and clean_up(), plugin_shutdown() marks READY plugins as DELETED and calls reap_plugins(); ha_finalize_handlerton() calls hton->panic(hton, HA_PANIC_CLOSE), and innobase_end() calls innodb_shutdown(), which dumps the buffer pool and makes a checkpoint. Finally Shutdown complete is logged. The two doors (gray) meet at break_connect_loop(). The actual cleanup is always done by the main thread.

break_connect_loop() — one flag and 8 bytes

static void break_connect_loop()
{
  abort_loop= 1;
  // ... (Windows version omitted)
  mysql_mutex_lock(&LOCK_start_thread);
  if (termination_event_fd >= 0)
  {
    uint64_t u= 1;
    if(write(termination_event_fd, &u, sizeof(uint64_t)) < 0)
    {
      sql_print_error("Couldn't send event to terminate listen loop");
      abort();
    }
  }
  mysql_mutex_unlock(&LOCK_start_thread);
#endif /* _WIN32 */
}

sql/mysqld.cc lines 1850–1870

It sets abort_loop= 1 and writes 8 bytes (one uint64_t) to section 6's pipe. That's all it does, and the comment above the function (lines 1845–1847) is firm about it: "Shutdown as such is supposed to be performed by main thread itself". Three places call it: SIGTERM/SIGQUIT in signal_hand (line 3513), kill_mysql() (line 1914), and end_mysqld_signal() (line 3066), which handles ^C when started with --gdb. kill_mysql() is called by the COM_SHUTDOWN command that mariadb-admin shutdown sends (sql_parse.cc lines 2241–2268) and by the SQL statement SHUTDOWN (lines 5517–5523); both check the SHUTDOWN privilege first. kill_mysql() also records who shut the server down (shutdown_user, lines 1886–1895).

The bytes written to the pipe wake poll() in the main thread, which sees abort_loop, leaves the loop and returns to mysqld_main().

  run_main_loop();

  /* Shutdown requested */
  {
    char *user= shutdown_user.load(std::memory_order_relaxed);
    sql_print_information(ER_DEFAULT(ER_NORMAL_SHUTDOWN), my_progname,
                          user ? user : "unknown");
  }
  // ... (WSREP block omitted)
  close_connections();

termination:
  my_free(shutdown_user.load(std::memory_order_relaxed));
  /*
    The signal thread can use server resources, e.g. when processing SIGHUP,
    and it must end gracefully before clean_up()
  */
  wait_for_signal_thread_to_end();

  ha_pre_shutdown();
  clean_up(1, true);

sql/mysqld.cc lines 6590–6616

plugin_shutdown() — unused ones first

    while (reap_needed && (count= plugin_array.elements))
    {
      reap_plugins();
      for (i= 0; i < count; i++)
      {
        plugin= *dynamic_element(&plugin_array, i, struct st_plugin_int **);
        if (plugin->state == PLUGIN_IS_READY)
        {
          plugin->state= PLUGIN_IS_DELETED;
          reap_needed= true;
        }
      }
      if (!reap_needed)
      {
        /*
          release any plugin references held.
        */
        unlock_variables(NULL, &global_system_variables);
        unlock_variables(NULL, &max_system_variables);
      }
    }

sql/sql_plugin.cc lines 2160–2180

It repeats marking every READY plugin as DELETED and calling reap_plugins(). reap_plugins() (lines 1348–1390) picks only those with ref_count 0 and runs plugin_deinitialize() on them. A plugin someone still holds is pushed to the next round. The typical one is the default engine: global_system_variables.table_plugin keeps InnoDB locked, so it is cleaned up in the round after lines 2177–2178 release it with unlock_variables(), once everything else is gone. That is the "reasonable order" in the comment (lines 2153–2158).

plugin_deinitialize() mirrors plugin_do_initialize(). If there is a per-type cleanup function (plugin_type_deinitialize[], lines 130–135), it calls that — ha_finalize_handlerton() for engines (lines 1307–1311). And, as we saw in section 5, ha_finalize_handlerton() calls hton->panic.

  if (installed_htons[hton->db_type] == hton)
    installed_htons[hton->db_type]= NULL;

  if (hton->panic)
    hton->panic(hton, HA_PANIC_CLOSE);

  if (plugin->plugin->deinit)
    deinit_status= plugin->plugin->deinit(NULL);

sql/handler.cc lines 599–606

Example 6: two kinds of shutdown, and a backtrace

Groups 3 and 4 of example 1 stopped the same server once with mariadb-admin shutdown and once with kill -TERM. The identical lines in the middle are shortened to ....

===== 3. shutdown with mariadb-admin (COM_SHUTDOWN) =====
2026-11-07  3:43:27 0 [Note] mariadbd (initiated by: root[root] @ localhost []): Normal shutdown
2026-11-07  3:43:27 0 [Note] InnoDB: FTS optimize thread exiting.
2026-11-07  3:43:27 0 [Note] InnoDB: Starting shutdown...
...
2026-11-07  3:43:27 0 [Note] Shutdown completed; log sequence number 52261; transaction id 15
2026-11-07  3:43:27 0 [Note] mariadbd: Shutdown complete
===== 4. shutdown with kill -TERM (signal_hand thread) =====
2026-11-07  3:43:29 0 [Note] mariadbd (initiated by: unknown): Normal shutdown
...
2026-11-07  3:43:29 0 [Note] Shutdown completed; log sequence number 52261; transaction id 15
2026-11-07  3:43:29 0 [Note] mariadbd: Shutdown complete

The two logs differ only in the initiated by: of the first line: COM_SHUTDOWN leaves root[root] @ localhost [], the signal leaves unknown. The rest is the same, because after the two doors meet at break_connect_loop() the path is one. InnoDB: Starting shutdown... is printed in logs_empty_and_mark_files_at_shutdown(), called by innodb_shutdown() (log0log.cc lines 2106–2112), which also kicks off the buffer pool dump with buf_dump_start() at line 2129 (the dump messages themselves come from buf_dump() in buf/buf0dump.cc). The function's comment reads "Make a checkpoint at the latest lsn on shutdown". That is why there is nothing to recover at the next start after a clean shutdown; what happens when kill -9 skips this step is covered in part 12.

The backtrace from the second run of example 5, stopped at innodb_shutdown:

Thread 1 "mariadbd" hit Breakpoint 1, innodb_shutdown () at /usr/src/mariadb/storage/innobase/srv/srv0start.cc:2105
2105		ut_ad(!srv_read_only_mode || recv_sys.rpo);
#0  innodb_shutdown () at /usr/src/mariadb/storage/innobase/srv/srv0start.cc:2105
#1  0x0000555556a6ab83 in innobase_end () at /usr/src/mariadb/storage/innobase/handler/ha_innodb.cc:4542
#2  0x000055555661e4a2 in ha_finalize_handlerton (plugin_=0x555558c6d3b0) at /usr/src/mariadb/sql/handler.cc:603
#3  0x00005555561b6a07 in plugin_deinitialize (plugin=0x555558c6d3b0, ref_check=true) at /usr/src/mariadb/sql/sql_plugin.cc:1311
#4  0x00005555561b6f4b in reap_plugins () at /usr/src/mariadb/sql/sql_plugin.cc:1382
#5  0x00005555561b9d68 in plugin_shutdown () at /usr/src/mariadb/sql/sql_plugin.cc:2162
#6  0x0000555555f8cb02 in clean_up (print_message=true, use_dummy_thd=true) at /usr/src/mariadb/sql/mysqld.cc:2183
#7  0x0000555555f96e20 in mysqld_main (argc=11, argv=0x5555589d3f60) at /usr/src/mariadb/sql/mysqld.cc:6616
#8  0x0000555555f88d2d in main (argc=1, argv=0x7fffffffed38) at /usr/src/mariadb/sql/main.cc:34

From bottom to top: mysqld_main (line 6616, clean_up()) → clean_up (line 2183, plugin_shutdown()) → plugin_shutdown (line 2162, reap_plugins()) → reap_plugins → plugin_deinitialize (line 1311) → ha_finalize_handlerton (line 603, hton->panic) → innobase_end → innodb_shutdown. It pairs up with section 5's startup backtrace (plugin_init → plugin_do_initialize → ha_initialize_handlerton → innodb_init).

What this post leaves out

Summary

Concept In one line
main() Just calls mysqld_main(); the server body is a library
Startup order options → block signals → init_server_components() (error log, plugin_init(), leftover option check) → TLS and sockets → signal thread → privileges → ready for connections. → poll()
load_defaults Inserts the my.cnf groups as --x=y before argv, with file markers recording the origin
Four option passes early → get_options() → test_plugin_options() per plugin → leftover check. Unknown options are passed along to the end
The price of a typo The leftover check comes after plugin_init(), so unknown variable stops startup only after InnoDB is fully up. --validate-config is faster
sys_var One global object = one option = one global variable. my.cnf, the command line and SET GLOBAL use the same memory
st_maria_plugin A plugin's declaration. maria_declare_plugin expands to builtin_maria_<name>_plugin when static, _maria_plugin_declarations_ when dynamic
plugin_init() register all → MyISAM first → initialize type by type → read mysql.plugin → one more pass → clean up failures
FORCE/ON The mandatory array is FORCE (cannot be turned off), the optional array ON (turned off with --skip-<name>)
ha_initialize_handlerton() The server creates a zero-filled handlerton and the engine's init(hton) fills the function pointers. The server assigns slot and db_type
innodb_init() Fills hton and calls srv_start() — buffer pool, redo and recovery happen here, before the sockets open
Signals Blocked in every thread; one signal_hand thread receives them with sigwait
Shutdown break_connect_loop() wakes poll() through a pipe, then the main thread cleans up in reverse: connections → plugin_shutdown() → hton->panic

What's next

At the end of this post the main thread was waiting in poll() for the first connection. When one arrives, the accept()ed socket goes through handle_accepted_socket() and either gets a thread of its own (the default, one-thread-per-connection) or joins a thread pool group. A THD is created for each connection, and an authentication plugin checks the password. It continues in part 4: Connections and Threads — One Thread per Connection, and the Thread Pool.

Compared with the earlier series: httpd in httpd part 5 read its configuration twice inside main() and handled a restart as "one more round of a loop that re-reads the configuration from scratch". MariaDB reads its configuration once and, with no notion of a restart, changes only what can be changed through SET GLOBAL. Compared with modules in httpd part 6, there is a lot in common: a build-generated static list (ap_prelinked_modules[] ↔ mysql_mandatory_plugins[]), a way to open a .so later (LoadModule ↔ INSTALL SONAME/--plugin-load-add), and a module attaching functions to hook arrays in register_hooks() versus an engine filling the function pointers of its handlerton in init(). The difference is that an httpd hook is an array where many modules line up at one spot, while a handlerton is a table, one per engine. In Tomcat part 3 every component followed the Lifecycle state machine and the shutdown command came over port 8005; MariaDB's plugin states (UNINITIALIZED → READY → DELETED → DYING) are much simpler, and the shutdown command comes over an SQL connection (COM_SHUTDOWN) or as a Unix signal.

More posts