Skip to content
TidesDB

Replication and High Availability

Replication and High Availability

TideSQL stores data on one node. Replication and clustering come from the layers above the engine, and within those layers TideSQL is a first-class participant rather than a generic pass-through. It carries the binlog capabilities MariaDB replication needs, it commits through the two-phase protocol that keeps a replica crash-safe, and it participates directly in Galera certification, including the cross-node conflict resolution that keeps a multi-master cluster from diverging.

MariaDB replication

A TideSQL table replicates like any other. The engine is both row and statement binlog capable, so binlog_format of ROW, STATEMENT, or MIXED all work, with row-based replication the natural fit for an optimistic engine because it ships the resulting row images rather than re-running a statement whose outcome depends on commit ordering.

Commits go through the binlog two-phase protocol. The engine prepares durably before the binlog writes, and it commits in binlog order through the group-commit hook, so a crash between the binlog and the engine recovers to a consistent point and a replica built from that binlog is consistent with the primary. The prepare phase runs the same conflict checks as a commit, so with the binlog enabled a write-write conflict is reported when the transaction prepares. Commit behavior is covered in Transactions and Isolation.

Nothing engine-specific has to be enabled. A table created with ENGINE=TIDESDB on the primary replicates to a replica running the same engine through the standard replication configuration.

Galera cluster

TideSQL advertises the Galera replication capability, so a table created with ENGINE=TIDESDB in a wsrep-enabled server is replicated to every node in the cluster and certified in the global order the provider assigns. Galera certifies only write sets, it never fails two write sets from the same node against each other, and it certifies a transaction before the engine commits it, so the engine has to settle every conflict Galera cannot see before the transaction replicates. The sections below cover what it does for the cluster.

Certification keys

The engine appends a certification key for every row a transaction changes, built from the primary key and from each unique index, so a write to the same row or the same unique value on another node is caught as a conflict rather than silently diverging the replicas. A key with a NULL part constrains nothing and is skipped. A table with no unique key certifies each of its ordinary indexes instead, and a table with no certifiable key at all appends a table-level exclusive key, which serializes its writes across the cluster. When an update changes a key, the keys of both the before and the after image are appended, so the row’s old identity stays covered. Inserts and deletes append their keys as exclusive keys. An update appends an update key for a key that stays the same and an exclusive key for a unique key it changes, as InnoDB does, so a parent row updated in place on one node orders itself after a child row referencing it written on another, rather than conflicting with it.

A child row written under a foreign key also certifies a reference key on the parent row it depends on, the same key the parent’s delete or key change certifies as exclusive, so a parent deleted on one node and a child of it inserted on another conflict in certification. A transaction that cascades is applied on the other nodes on its own rather than in parallel with other write sets.

Conflicts between nodes

A cross-node conflict is resolved the way InnoDB resolves it, without a pessimistic lock. When an applier replays a cluster write that collides with a local transaction that has not committed, the engine brute-force aborts that local transaction through the server before it certifies, so the losing side rolls back cleanly instead of committing a write the other nodes cannot apply. A lock-free store parks no row lock for the applier to block on, so the engine keeps a small in-memory record of each uncommitted write, keyed by the row’s primary key and by each non-NULL unique value it writes, and uses it to find and abort the local loser. The applier holds those keys while it applies, so a local write to one of them meanwhile is refused. The same record covers a local transaction holding the other end of a foreign key, and a local transaction at REPEATABLE READ or SERIALIZABLE that read a key the applier writes or scanned a range holding it. An applier waits for the transactions it aborted to finish rolling back before it commits, since until then their reads would refuse its writes. A transaction at those levels that reads a row an applier has written but not yet committed is not aborted, it waits for that applier before it prepares, so the prepare finds the row newer than what it read and refuses the transaction as it would any stale read. An aborted transaction fails its next operation, its commit included, and the application on the losing side sees a deadlock error and retries, the same outcome InnoDB produces for the same race. An applier is never refused and never gives up on transient contention inside the store.

Conflicts on the same node

Two transactions on the same node never conflict in certification, so the engine settles them before either replicates. Under Galera a local write transaction is prepared before Galera certifies it, and the prepare is the engine’s own conflict check together with a durable record of the writes, so a transaction that loses to another on this node is refused before anything reaches the other nodes, and a certified transaction commits without checking again. At SNAPSHOT, which a session at the default REPEATABLE READ runs as, and at SERIALIZABLE, that check covers every write-write conflict, and the losing transaction fails at COMMIT exactly as it does without Galera. At the levels below SNAPSHOT a commit does not check its writes against other writers, so the engine refuses the losing write itself while its statement runs, as InnoDB’s row lock would, when another live transaction on the node wrote the same row or unique value, or when that row or value was committed after what the writer could have read. Without this, two writers of one row would each commit here and then replay on the other nodes as a duplicate insert or as an update of a row that is gone.

SERIALIZABLE

At SERIALIZABLE the engine validates that nothing the transaction read, and no range it scanned, changed after its snapshot. Under Galera that validation runs when the transaction prepares, before certification, so a transaction whose read went stale is refused at COMMIT exactly as without Galera, and a certified one cannot fail it afterwards. A prepared transaction’s reads refuse any write to what it read until it ends, which would refuse an applier, so the engine records the keys a local REPEATABLE READ or SERIALIZABLE transaction reads and the ranges it scans, and a cluster write to any of them aborts it and waits for it to end. Galera itself certifies only write sets, so SERIALIZABLE holds on each node, and across nodes the cluster gives snapshot isolation.

Transactions across engines

A transaction can write TidesDB and InnoDB tables together under Galera. The server certifies a transaction once for every engine taking part in its commit that carries Galera’s replication flag, and a transaction certified twice stops the node. So the engine takes part in commits without the flag, while its tables keep it, which is what makes their rows replicate. A transaction that writes only TidesDB is certified as it commits, and one that also writes InnoDB is certified once, with the keys and rows of both engines in one write set, and both engines commit it or neither does.

Commits after certification

A transaction Galera has certified is committed by every other node, so it must commit here too. The conflicts above are all refused before certification, so only a hard failure inside the store, such as an I/O error, can still fail such a commit. The node then stops with an error rather than fall out of step with the cluster or stop committing, and it rejoins through a state transfer. A large statement that runs as a bulk operation commits as a whole under Galera rather than in batches, since a batch committed before the statement’s write set was certified would stay on this node alone if the statement then failed.

Cluster position and state transfer

The cluster position is kept in a dedicated internal column family, __tidesdb_wsrep_meta, and recorded with every commit, as InnoDB records it in each commit. A transaction that already has its position when the engine commits it, an applier above all, records it in that same commit. A local transaction is prepared before it is certified and so before it has a position, so it writes a pending marker in its commit and records its position right after, and a marker left by a crash between the two makes the position unknown. The server’s own checkpoints at view changes, DDL and state transfers are recorded the same way. Every other engine reports only the position of its own last commit, and the server keeps whichever engine answers last, so when a node starts the engine works out the node’s real position and hands it to every engine through the server, and the answer is the same whichever speaks last. A node that shut down cleanly, or crashed and recovered its position through the server’s --wsrep-recover step as InnoDB does, rejoins by an incremental transfer while the write sets it is missing are still in the donor’s cache. A node whose position is unknown rejoins by a full state transfer, since replaying write sets it already holds would fail. An engine with no stored position leaves the others untouched.

A full state transfer has to carry the store, which lives outside the server’s data directory where the transfer methods do not look. While the donor holds its tables flushed and locked for the transfer, the engine writes a consistent copy of the store into #tidesdb_sst inside the data directory, which the rsync method carries with everything else, and removes it when the donor resumes. Every commit records its position, so a copy taken while the cluster is busy carries the exact position of the rows it holds. The joiner puts the copy in place as its store before the store opens. The store directory and the data directory must be on one filesystem for that last step. A transfer that arrived without a complete copy leaves TidesDB refusing to start, with an error naming the directory, rather than running without its rows. Use wsrep_sst_method=rsync, the one method that carries the store. mysqldump also works, since it recreates every table and row through SQL, given an account for the transfer in wsrep_sst_auth as with any engine. mariabackup copies only the files it knows and not the store, so the engine records an identity both in the store and in a file in the data directory, and a node whose data directory arrived without its store refuses to start TidesDB rather than run with missing or stale rows.

Auto-increment

The engine generates auto-increment values on the grid auto_increment_offset and auto_increment_increment describe, so with wsrep_auto_increment_control every node generates values no other node does.

What an application sees under conflict

On a single node, an optimistic write-write conflict surfaces at COMMIT as error 1180 wrapping handler error 149 (deadlock), or mid-statement as a deadlock (1213), and the application retries, described in Limitations. Under Galera a conflict between two transactions on the same node surfaces the same way at SNAPSHOT and SERIALIZABLE, and at the levels below SNAPSHOT as a deadlock (1213) on the losing write. Across a cluster the losing transaction of a cross-node conflict is aborted and reaches the client as a deadlock, which an application should handle by retrying the transaction. Autocommit statements are retried by the server automatically, up to wsrep_retry_autocommit times. The winner’s row stands identically on every node, so the replicas stay converged.