Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package com.dotmarketing.startup.runonce;

import com.dotmarketing.common.db.DotConnect;
import com.dotmarketing.exception.DotDataException;
import com.dotmarketing.startup.StartupTask;
import com.dotmarketing.util.Logger;

import java.sql.SQLException;
import java.util.List;
import java.util.Map;

/**
* Sets LZ4 compression on every {@code text}, {@code bytea}, {@code jsonb}, and {@code json}
* column in the {@code public} schema that uses TOAST storage.
*
* <h3>Why LZ4?</h3>
* PostgreSQL defaults to pglz for TOAST compression. LZ4 decompresses roughly 3–5× faster
* than pglz while achieving comparable (often slightly better) ratios on typical dotCMS data
* (HTML, JSON, workflow payloads). Read-heavy workloads — content delivery, page rendering,
* workflow evaluation — pay the decompression cost on every fetch of a TOASTed column, so
* faster decompression directly reduces latency.
*
* <h3>Scope</h3>
* Only columns with TOAST-eligible storage are targeted (extended / external / main).
* Plain-storage columns (e.g. short {@code varchar}) are excluded automatically via
* the {@code pg_attribute.attstorage} filter. Columns already using LZ4
* ({@code attcompression = 'l'}) are skipped — the task is fully idempotent.
*
* <h3>Effect on existing data</h3>
* {@code SET COMPRESSION lz4} changes the compression method for <em>future</em> writes only.
* Existing TOASTed values are not rewritten immediately — they retain their original encoding
* until the row is next updated. This makes the migration instant and lock-free.
*
* @since Apr 3rd, 2026
*/
public class Task260403SetLz4CompressionOnTextColumns implements StartupTask {

/**
* Finds all TOAST-eligible text/bytea/jsonb/json columns in the public schema that do not
* yet use LZ4 compression.
*
* <p>Storage codes: 'x' = extended (TOAST, compressed), 'e' = external (TOAST,
* uncompressed), 'm' = main (inline compressed). Compression code 'l' = lz4.
*/
private static final String FIND_COLUMNS_SQL =
"SELECT c.relname AS tbl, a.attname AS col " +
" FROM pg_attribute a " +
" JOIN pg_class c ON c.oid = a.attrelid " +
" JOIN pg_namespace n ON n.oid = c.relnamespace " +
" JOIN pg_type t ON t.oid = a.atttypid " +
" WHERE n.nspname = 'public' " +
" AND c.relkind = 'r' " +
" AND a.attnum > 0 " +
" AND NOT a.attisdropped " +
" AND t.typname IN ('text', 'bytea', 'jsonb', 'json') " +
" AND a.attstorage IN ('x', 'e', 'm') " +
" AND a.attcompression != 'l' " +
" ORDER BY c.relname, a.attname";

@Override
public boolean forceRun() {
return true;
}

@Override
public void executeUpgrade() throws DotDataException {
final List<Map<String, Object>> columns = new DotConnect()
.setSQL(FIND_COLUMNS_SQL)
.loadObjectResults();

if (columns.isEmpty()) {
Logger.info(this, "All eligible columns already use LZ4 compression — nothing to do");
return;
}

Logger.info(this, "Setting LZ4 compression on " + columns.size() + " column(s)");
int updated = 0;
int failed = 0;

for (final Map<String, Object> row : columns) {
final String table = (String) row.get("tbl");
final String column = (String) row.get("col");
try {
new DotConnect().executeStatement(
"ALTER TABLE " + table + " ALTER COLUMN " + column + " SET COMPRESSION lz4");
updated++;
} catch (final SQLException e) {
Logger.warn(this, "Failed to set LZ4 on " + table + "." + column
+ ": " + e.getMessage());
failed++;
}
}

Logger.info(this, "LZ4 compression migration complete — "
+ updated + " column(s) updated, " + failed + " skipped due to errors");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.dotmarketing.startup.runonce;

import com.dotmarketing.common.db.DotConnect;
import com.dotmarketing.exception.DotDataException;
import com.dotmarketing.startup.StartupTask;
import com.dotmarketing.util.Logger;

import java.sql.SQLException;

/**
* Converts the {@code permission_reference} table to UNLOGGED.
*
* <p>UNLOGGED tables bypass WAL (Write-Ahead Logging), which eliminates the
* per-row WAL write cost on every INSERT/UPDATE/DELETE. This table is a pure
* denormalized cache — rows are rebuilt automatically by the permission system
* whenever they are invalidated. Losing this data on an unexpected crash is
* completely safe because the permission system rebuilds it on demand.
*
* <p>Expected benefits:
* <ul>
* <li>~2–3× faster INSERT/DELETE throughput on permission rebuilds</li>
* <li>Reduced WAL volume, lowering I/O pressure and replication lag</li>
* <li>Smaller checkpoints during mass permission recalculation</li>
* </ul>
*
* <p>{@code ALTER TABLE ... SET UNLOGGED} rewrites the table (brief exclusive
* lock) and truncates the unlogged table on replica nodes — expected behaviour
* since permission_reference is never read-from replicas directly.
*
* @since Apr 3rd, 2026
*/
public class Task260403SetPermissionReferenceUnlogged implements StartupTask {

private static final String TABLE_NAME = "permission_reference";

@Override
public boolean forceRun() {
return true;
}

@Override
public void executeUpgrade() throws DotDataException {
try {
Logger.info(this, "Converting " + TABLE_NAME + " to UNLOGGED");
new DotConnect().executeStatement(
"ALTER TABLE " + TABLE_NAME + " SET UNLOGGED");
Logger.info(this, "Successfully converted " + TABLE_NAME + " to UNLOGGED");
} catch (final SQLException e) {
throw new DotDataException(
"Failed to set " + TABLE_NAME + " UNLOGGED: " + e.getMessage(), e);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,8 @@
import com.dotmarketing.startup.runonce.Task260206AddUsagePortletToMenu;
import com.dotmarketing.startup.runonce.Task260320AddPluginsPortletToMenu;
import com.dotmarketing.startup.runonce.Task260324AddIdentifierPathTriggerIndex;
import com.dotmarketing.startup.runonce.Task260403SetLz4CompressionOnTextColumns;
import com.dotmarketing.startup.runonce.Task260403SetPermissionReferenceUnlogged;
import com.google.common.collect.ImmutableList;

import java.util.ArrayList;
Expand Down Expand Up @@ -600,6 +602,8 @@ public static List<Class<?>> getStartupRunOnceTaskClasses() {
.add(Task260206AddUsagePortletToMenu.class)
.add(Task260320AddPluginsPortletToMenu.class)
.add(Task260324AddIdentifierPathTriggerIndex.class)
.add(Task260403SetLz4CompressionOnTextColumns.class)
.add(Task260403SetPermissionReferenceUnlogged.class)
.build();

return ret.stream().sorted(classNameComparator).collect(Collectors.toList());
Expand Down
Loading
Loading