TTL Service#
TTL stands for Time To Live. This feature makes it possible to automatically remove rows from the database that have lived beyond their expiration date. The TTL is implemented in the RonDB data nodes, thereby the expiration is exact. TTL also is transaction safe, this means that if you have locked a row with TTL, time will stop from this transactions point of view. Other transactions waiting for the lock or reading using a Read Committed will not see the row after expiration.
This ensures that we don’t mess up the transaction logic since an expiration would in principle be a delete. It also means that a row that expires can be expired immediately after commit of the transaction unless the expiration date was changed.
Expired rows are still present in the database and are still part of recovery logic. The rows are removed by the TTL purge service that executes in all the REST API servers. This service will delete all rows that have expired in a timely fashion.
There are special flags available to read rows that have expired, this is required for checkpoint logic, backup logic, but can also be used by an application that so desires.
1. Create table#
Creating a TTL (Time-to-Live) table is straightforward using the
CREATE TABLE statement with a specific COMMENT:
"TTL=ttl_seconds@ttl_column"
-
ttl_seconds: Specifies how long (in seconds) a row will remain in the table. [0 - 4294967039] -
ttl_column: Refers to aDATETIMEorTIMESTAMPcolumn used to determine the TTL. It must be a stored column: it cannot be a hidden column, a virtual generated column, or a column stored on disk.
A row whose TTL column is NULL never expires.
Example
The following example creates a TTL table where rows expire 10 seconds
after the time specified in the event_time column:
CREATE TABLE tbl (
credit_id INT,
event_time DATETIME NOT NULL,
amount int,
PRIMARY KEY(credit_id, event_time)
) ENGINE=NDB, COMMENT="NDB_TABLE=TTL=10@event_time";
A TTL table can also be a ring buffer table, which keeps at most a fixed number of rows per key prefix, see Ring Buffer Tables.
2. Alter table#
By using the ALTER TABLE statement to modify the table comment, you
can:
-
Change the TTL Seconds for a Specific TTL Table or enable TTL functionality for a table
"TTL=new_ttl_seconds@new_ttl_column"-
new_ttl_seconds: Must adhere to the same rules as inCREATE TABLE.-
new_ttl_column: The column type must still beDATETIMEorTIMESTAMP. -
Disable TTL Functionality for a Specific Table
"TTL=OFF"
Notes
1. If TTL is disabled or modified, expired rows that have not yet been purged may become visible again.
2. Dropping or changing the data type of the TTL column is not allowed unless TTL is first disabled on the table. ALTER TABLE statement will return failure if you do that
Example
The following example disables the TTL functionality on table “tbl”
ALTER TABLE tbl COMMENT="NDB_TABLE=TTL=OFF";
3. TTL Purging program#
The TTL purging program is integrated into rdrs2, which automatically
removes expired rows. Additionally, you can perform extra configuration
to optimize or control the purging process. The REST endpoints that show
and change what the purging program does are described in TTL Purge
REST API.
1. Add an Index on the TTL Column to Improve Purging Efficiency#
The purging program performs a table scan (with an adaptive batch size)
to locate and delete expired rows. You can enhance this process by
adding an index with the name "ttl_index" on the TTL column. This
enables index-based scanning, which is more efficient and incurs a lower
cost. The TTL column must be the first column of the index; otherwise
the purging program does not use the index and scans the table.
Example:
ALTER TABLE tbl ADD INDEX ttl_index(event_time);
2. Create a Configuration Table to Build an “rdrs2 Purging Cluster”#
To distribute purging tasks across multiple rdrs2 nodes in a RonDB
cluster, you can create a configuration table mysql.ttl_purge_nodes.
By default, all rdrs2 nodes handle purging for all TTL tables.
However, this configuration allows you to divide the purging workload
among specific nodes.
Create the configuration table:
USE mysql;
CREATE TABLE ttl_purge_nodes (
node_id INT NOT NULL,
last_active DATETIME DEFAULT NULL,
PRIMARY KEY (node_id)
) ENGINE = NDB;
Insert the node IDs for the rdrs2 nodes you want to participate in
purging:
INSERT INTO ttl_purge_nodes VALUES (5, DEFAULT), (6, DEFAULT);
Behavior and Features
-
Task Partitioning:
-
rdrs2nodes (e.g., node 5 and node 6) split the partitions of every TTL table between them: with n purge nodes, each node purges about one n-th of the partitions of each table, and all nodes purge in parallel. When purge nodes join or leave, the partitions are redistributed automatically. -
High Availability:
- If one
rdrs2node (e.g., node 5) goes down, the other nodes take over its partitions until it is back online.*Note: A node that has not refreshed its entry in `ttl_purge_nodes` for 20 seconds is considered gone.*
3. Control When Rows Are Purged#
By default the purging program runs around the clock and deletes rows as soon as they have expired. You can delay purging with a purge lag, and restrict purging to a daily active window, for example to keep the delete load in off-peak hours. The purge lag and the window only control when expired rows are deleted and their memory is freed. Reads stop seeing a row as soon as its TTL has passed.
The control table
An optional NDB table, mysql.ttl_purge_ctrl, holds cluster-wide
settings:
USE mysql;
CREATE TABLE ttl_purge_ctrl (
ctrl_id INT NOT NULL PRIMARY KEY,
value INT
) ENGINE = NDB;
-- Purge only between 03:00 and 05:00 UTC (minutes since midnight)
INSERT INTO ttl_purge_ctrl VALUES (2, 180), (3, 300);
-- Purge a row only when it has been expired for at least 600 seconds
INSERT INTO ttl_purge_ctrl VALUES (1, 600);
-
ctrl_id1: the purge lag in seconds. An expired row is purged only once it has been expired at least this long. A negative value is used as 0. -
ctrl_id2 and 3: the start and end of a daily active window, in minutes since midnight UTC (0 to 1439). Purging runs only inside the window, which wraps past midnight when the start is larger than the end. Both rows must be present, and the start must differ from the end.
A missing row or a NULL value means no lag and no window. To convert a
time of day to minutes, multiply the hours by 60 and add the minutes:
03:00 is 180 and 23:30 is 1410.
Every purge worker reads the three rows in one transaction at the start
of every round, so a change takes effect within about one round on every
rdrs2 node. The rdrs2 log shows each change, for example
purge window size changed from 0 to 600 seconds,
purge active window set to [03:00-05:00) UTC or
purge active window cleared, purging around the clock.
Create the table exactly as shown. If a purge worker cannot read the
table, for example because a column is missing or has another name, it
does not purge in that round and logs
Failed to get purge control settings. Retry.... Since all rdrs2
nodes read the same table, every round fails on every node and no rows
are purged. After more than 10 failures in a row, the worker logs
Pre-round errors exceeded 10 times and is restarted, which repeats
about every 15 seconds. GET /0.1.0/ttl-purge/status does not show this
reliably: it shows running or paused most of the time and error
only for a few seconds per restart, so alert on the log lines instead.
To fix it, drop the table and create it again exactly as shown. Purging
resumes within seconds, without a restart of rdrs2.
Three ways to set an active window
| Method | Applies to | Takes effect | After an rdrs2 restart |
|---|---|---|---|
Rows 2 and 3 in mysql.ttl_purge_ctrl |
All rdrs2 nodes |
Within about one round | Kept |
TTLPurge.ActiveWindow in the configuration file |
One rdrs2 node |
When rdrs2 starts |
Kept |
active_window in PUT /0.1.0/ttl-purge/config |
One rdrs2 node |
In the next round | Lost |
The configuration file and the REST API take the window as
"HH:MM-HH:MM" in UTC, for example "03:00-05:00" or "22:00-02:00".
The REST API changes the window of the node until the next restart, see
TTL Purge REST API.
Use ttl_purge_ctrl when all nodes should purge in the same window,
including nodes that are started later. Use the configuration file when
nodes need different windows, or when the configuration of rdrs2 is
managed by deployment tooling. Use the REST API for a temporary change
on one node.
The window that a node uses is:
-
the window in
ttl_purge_ctrl, if both rows are present and valid; -
otherwise the window of the node itself, from the configuration file or the last REST API change;
-
otherwise none, and purging runs around the clock.
An invalid window in ttl_purge_ctrl, for example only one of the two
rows, a value outside 0 to 1439, or a start equal to the end, is
ignored, and the log shows the warning
Invalid purge active window (start=..., end=...). An invalid
TTLPurge.ActiveWindow stops rdrs2 at startup with an error, and an
invalid window in the REST API is rejected with HTTP status 400.
To check the window that a node uses, read active_window and
active_window_source in GET /0.1.0/ttl-purge/status on a purge node
with purging enabled. On a disabled node, and on a node that is not in
ttl_purge_nodes, these fields keep old values. A node that is not in
ttl_purge_nodes logs
Not the configured purging node, skip purging....
GET /0.1.0/ttl-purge/config shows only the window of the node itself,
and SELECT * FROM mysql.ttl_purge_ctrl shows the cluster-wide window.
With several purge nodes, a node outside its window stops refreshing its
entry in ttl_purge_nodes, and after 20 seconds its partitions move to
the nodes that are inside their windows. When a node enters its window
again, it skips one round so that the partitions are divided among the
active nodes before it purges.
Changing the window
Change rows 2 and 3 in one statement:
UPDATE mysql.ttl_purge_ctrl SET value = IF(ctrl_id = 2, 60, 240)
WHERE ctrl_id IN (2, 3);
With two separate statements, a purge worker can read the table between them and use a window with the new start and the old end for one round. To rule out any round with an unintended window, disable purging while you change the rows:
-
Send
{"enabled": false}toPUT /0.1.0/ttl-purge/configon every purge node, and wait untilGET /0.1.0/ttl-purge/statusshowsdisabled. -
Change the rows in
mysql.ttl_purge_ctrl. -
Send
{"enabled": true}on every purge node. The first round after that reads the new window before it purges.
Behavior at the edges of the window
-
Purging starts in the first round after the window opens. The log shows
Entered the purge active window [03:00-05:00) UTC (from ttl_purge_ctrl), purging resumed. -
When the window closes, the worker does not start another batch, also in the middle of a round. The batch that is already running is completed. If that batch fails, the worker retries it up to 10 times, one second apart, so while the cluster returns errors, purging can continue for some tens of seconds after the end of the window. A batch that fails with a transaction timeout is not retried. If a period must be kept free of purging, end the window a minute before it starts. The log shows
Outside the purge active window [03:00-05:00) UTC (from ttl_purge_ctrl), purging paused. -
Expired rows that are left when the window closes stay in the table, hidden from reads and using memory, until the next window. A backlog does not extend purging past the end of the window.
-
The log shows the warning
The purge active window closed while purging was still saturated; an expired-row backlog likely remains. Consider a wider window or more purge nodes.when the worker was still deleting full batches in its last round, or when the window closed while tables were left in the round. The second case happens without any backlog, and more often with many TTL tables. A real backlog shows as this warning at almost every window close, day after day, and as acurrent_batch_sizeequal tomax_batch_sizefor the table near the end of the window, see TTL Purge REST API. -
If a worker cannot read
ttl_purge_ctrl, it skips the round instead of purging with settings it could not read. -
A node with purging disabled does not purge, whatever the window.
Recommended setup
-
Create
mysql.ttl_purge_ctrlexactly as shown above. -
Insert rows 2 and 3 in one statement, for example
INSERT INTO mysql.ttl_purge_ctrl VALUES (2, 180), (3, 300); -
Check that the log of each purge node shows
purge active window set to [03:00-05:00) UTC. -
Check that
GET /0.1.0/ttl-purge/statuson every purge node shows"active_window":"03:00-05:00"and"active_window_source":"ttl_purge_ctrl". -
Set up alerts on the log lines below.
Log lines to watch
All lines start with [TTL PWorker].
-
Invalid purge active window: the window inttl_purge_ctrlis ignored. -
The purge active window closed while purging was still saturated: a backlog of expired rows likely remains. -
Failed to get purge control settings. Retry..., repeated: the worker cannot readttl_purge_ctrland does not purge. -
Pre-round errors exceeded 10 times: the worker failed repeatedly before it could start purging, and is restarted. Errors are counted since the last completed round, and no round completes outside the window. With a short window, a few transient errors over many hours can add up and cause one restart, which is harmless. Restarts every 15 seconds or so mean a persistent error. -
purge active window set to,purge active window cleared,Entered the purge active windowandOutside the purge active window: confirm a change of the window and the start and end of each window.
4. Monitor and Control Purging through REST#
Each rdrs2 node reports on and controls its own purge worker through
the REST endpoints below /0.1.0/ttl-purge, which exist only under API
version 0.1.0. GET requests return the configuration, the status of
the worker, and metrics for all TTL tables and for each table.
PUT /0.1.0/ttl-purge/config changes the configuration of the node, for
example {"enabled": false} or {"active_window": "03:00-05:00"}.
A change made with PUT applies to this node only and lasts until the
node restarts. At startup, enabled and active_window come from
TTLPurge.Enable and TTLPurge.ActiveWindow in the configuration file
of the node. The chapter TTL Purge REST API
describes every endpoint, its fields and how to use them to monitor
purging.
5. Reading Expired Rows#
Expired rows stay in the table until the purging program deletes them. An NDB API application can still read them by bypassing the TTL check:
-
Single-Key Operations: set the
OO_TTL_IGNOREflag in the operation options:NdbOperation::OperationOptions options; options.optionsPresent |= NdbOperation::OperationOptions::OO_TTL_IGNORE; -
Scan Operations: set the
SO_TTL_IGNOREflag in the scan options:NdbScanOperation::ScanOptions options; options.optionsPresent = NdbScanOperation::ScanOptions::SO_TTL_IGNORE;
In a MySQL Server, the session variable
ttl_expired_rows_visible_in_delete makes expired rows visible to
DELETE statements, so that a DELETE also removes rows that have expired
but are not purged yet. RonSQL always skips expired rows.
6. Restrictions#
-
Triggers on DELETE: Triggers on DELETE will not be fired when a row expires due to TTL.
-
Foreign Key Constraints: A TTL table cannot take part in a foreign key relationship, neither as the parent nor as the child table. Since rows in a TTL table expire without a DELETE, expiry cannot fire cascading actions to child tables. RonDB rejects adding TTL to a table with foreign keys, and adding a foreign key to a TTL table.
-
Timezone Requirement: RonDB uses UTC-0 timezone to determine row expiration. Therefore, the value in the TTL column must be specified in UTC-0 time to ensure correct expiration behavior.
7. Replication and Backup#
-
The purging program removes expired rows with delete operations. A MySQL Server that writes a binary log for the cluster logs these deletes, and they are replicated like other deletes.
-
A backup contains the rows that have expired but are not purged yet, and
ndb_restorerestores them as they are. After the restore they are hidden from reads and the purging program deletes them. -
The purging program purges every TTL table. There is no setting that excludes a single TTL table from purging.