Blob Blame Raw
From c79630de8012a893ed3d1c46b41bc7871a07a3e2 Mon Sep 17 00:00:00 2001
From: Mark Reynolds <mreynolds@redhat.com>
Date: Wed, 26 May 2021 13:32:13 -0400
Subject: [PATCH 11/12] Issue 4778 - RFE - Allow setting TOD for db compaction
 and add task

Description:  Since database compaction can be costly it should be allowed
              to set a time to execute it during offpeak hours.  Once the
              compaction interval has been met, it will wait for the configured
              time of day to do the compaction.  The default is just before
              midnight: 23:59

              A task was also created that can run compaction on demand,
              and can also just target the replication changelog.  This could
              be used in conjunction with a cronjob for more complex
              execution patterns.

ASAN tested and approved.

relates: https://github.com/389ds/389-ds-base/issues/4778

Reviewed by: spichugi(Thanks!)
---
 .../tests/suites/config/compact_test.py       |  81 ++++++
 ldap/schema/01core389.ldif                    |   3 +-
 ldap/servers/plugins/replication/cl5.h        |   1 +
 ldap/servers/plugins/replication/cl5_api.c    |  70 ++++-
 ldap/servers/plugins/replication/cl5_api.h    |   2 +-
 .../servers/plugins/replication/cl5_clcache.c |   3 -
 ldap/servers/plugins/replication/cl5_config.c | 102 ++++++-
 ldap/servers/plugins/replication/cl5_init.c   |   2 +-
 .../servers/plugins/replication/repl_shared.h |   2 +
 ldap/servers/plugins/retrocl/retrocl.c        |   1 -
 .../slapd/back-ldbm/db-bdb/bdb_config.c       |  79 ++++++
 .../slapd/back-ldbm/db-bdb/bdb_layer.c        | 258 ++++++++++++------
 .../slapd/back-ldbm/db-bdb/bdb_layer.h        |   4 +-
 ldap/servers/slapd/back-ldbm/init.c           |   2 +
 ldap/servers/slapd/back-ldbm/ldbm_config.h    |   1 +
 .../servers/slapd/back-ldbm/proto-back-ldbm.h |   1 +
 ldap/servers/slapd/filtercmp.c                |   5 +-
 ldap/servers/slapd/pblock.c                   |  17 +-
 ldap/servers/slapd/slap.h                     |   2 +
 ldap/servers/slapd/slapi-private.h            |   1 +
 ldap/servers/slapd/task.c                     | 102 ++++++-
 src/cockpit/389-console/src/database.jsx      |   1 +
 .../src/lib/database/databaseConfig.jsx       |  16 +-
 src/lib389/lib389/_constants.py               |   1 +
 src/lib389/lib389/backend.py                  |   1 +
 src/lib389/lib389/cli_conf/backend.py         |  24 +-
 src/lib389/lib389/cli_conf/replication.py     |   3 +
 src/lib389/lib389/tasks.py                    |  14 +-
 28 files changed, 689 insertions(+), 110 deletions(-)
 create mode 100644 dirsrvtests/tests/suites/config/compact_test.py

diff --git a/dirsrvtests/tests/suites/config/compact_test.py b/dirsrvtests/tests/suites/config/compact_test.py
new file mode 100644
index 000000000..1f1c097e4
--- /dev/null
+++ b/dirsrvtests/tests/suites/config/compact_test.py
@@ -0,0 +1,81 @@
+import logging
+import pytest
+import os
+import time
+from lib389.tasks import DBCompactTask
+from lib389.backend import DatabaseConfig
+from lib389.replica import Changelog5
+from lib389.topologies import topology_m1 as topo
+
+log = logging.getLogger(__name__)
+
+
+def test_compact_db_task(topo):
+    """Specify a test case purpose or name here
+
+    :id: 1b3222ef-a336-4259-be21-6a52f76e1859
+    :setup: Standalone Instance
+    :steps:
+        1. Create task
+        2. Check task was successful
+        3. Check errors log to show task was run
+        3. Create task just for replication
+    :expectedresults:
+        1. Success
+        2. Success
+        3. Success
+        4. Success
+    """
+    inst = topo.ms["supplier1"]
+
+    task = DBCompactTask(inst)
+    task.create()
+    task.wait()
+    assert task.get_exit_code() == 0
+
+    # Check errors log to make sure task actually compacted db
+    assert inst.searchErrorsLog("Compacting databases")
+    inst.deleteErrorLogs(restart=False)
+
+
+def test_compaction_interval_and_time(topo):
+    """Specify a test case purpose or name here
+
+    :id: f361bee9-d7e7-4569-9255-d7b60dd9d92e
+    :setup: Supplier Instance
+    :steps:
+        1. Configure compact interval and time for database and changelog
+        2. Check compaction occurs as expected
+    :expectedresults:
+        1. Success
+        2. Success
+    """
+
+    inst = topo.ms["supplier1"]
+
+    # Configure DB compaction
+    config = DatabaseConfig(inst)
+    config.set([('nsslapd-db-compactdb-interval', '2'), ('nsslapd-db-compactdb-time', '00:01')])
+
+    # Configure changelog compaction
+    cl5 = Changelog5(inst)
+    cl5.replace_many(
+        ('nsslapd-changelogcompactdb-interval', '2'),
+        ('nsslapd-changelogcompactdb-time', '00:01'),
+        ('nsslapd-changelogtrim-interval',  '2')
+    )
+    inst.deleteErrorLogs()
+
+    # Check is compaction occurred
+    time.sleep(6)
+    assert inst.searchErrorsLog("Compacting databases")
+    assert inst.searchErrorsLog("compacting replication changelogs")
+    inst.deleteErrorLogs(restart=False)
+
+
+if __name__ == '__main__':
+    # Run isolated
+    # -s for DEBUG mode
+    CURRENT_FILE = os.path.realpath(__file__)
+    pytest.main(["-s", CURRENT_FILE])
+
diff --git a/ldap/schema/01core389.ldif b/ldap/schema/01core389.ldif
index 9e9a26c21..0c73e5114 100644
--- a/ldap/schema/01core389.ldif
+++ b/ldap/schema/01core389.ldif
@@ -285,6 +285,7 @@ attributeTypes: ( 2.16.840.1.113730.3.1.2310 NAME 'nsds5ReplicaFlowControlWindow
 attributeTypes: ( 2.16.840.1.113730.3.1.2311 NAME 'nsds5ReplicaFlowControlPause' DESC 'Netscape defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'Netscape Directory Server' )
 attributeTypes: ( 2.16.840.1.113730.3.1.2313 NAME 'nsslapd-changelogtrim-interval' DESC 'Netscape defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'Netscape Directory Server' )
 attributeTypes: ( 2.16.840.1.113730.3.1.2314 NAME 'nsslapd-changelogcompactdb-interval' DESC 'Netscape defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'Netscape Directory Server' )
+attributeTypes: ( 2.16.840.1.113730.3.1.2385 NAME 'nsslapd-changelogcompactdb-time' DESC 'Netscape defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'Netscape Directory Server' )
 attributeTypes: ( 2.16.840.1.113730.3.1.2315 NAME 'nsDS5ReplicaWaitForAsyncResults' DESC 'Netscape defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'Netscape Directory Server' )
 attributeTypes: ( 2.16.840.1.113730.3.1.2316 NAME 'nsslapd-auditfaillog-maxlogsize' DESC 'Netscape defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'Netscape Directory Server' )
 attributeTypes: ( 2.16.840.1.113730.3.1.2317 NAME 'nsslapd-auditfaillog-logrotationsync-enabled' DESC 'Netscape defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'Netscape Directory Server' )
@@ -345,5 +346,5 @@ objectClasses: ( nsEncryptionConfig-oid NAME 'nsEncryptionConfig' DESC 'Netscape
 objectClasses: ( nsEncryptionModule-oid NAME 'nsEncryptionModule' DESC 'Netscape defined objectclass' SUP top MUST ( cn ) MAY ( nsSSLToken $ nsSSLPersonalityssl $ nsSSLActivation $ ServerKeyExtractFile $ ServerCertExtractFile ) X-ORIGIN 'Netscape' )
 objectClasses: ( 2.16.840.1.113730.3.2.327 NAME 'rootDNPluginConfig' DESC 'Netscape defined objectclass' SUP top MUST ( cn ) MAY ( rootdn-open-time $ rootdn-close-time $ rootdn-days-allowed $ rootdn-allow-host $ rootdn-deny-host $ rootdn-allow-ip $ rootdn-deny-ip ) X-ORIGIN 'Netscape' )
 objectClasses: ( 2.16.840.1.113730.3.2.328 NAME 'nsSchemaPolicy' DESC 'Netscape defined objectclass' SUP top  MAY ( cn $ schemaUpdateObjectclassAccept $ schemaUpdateObjectclassReject $ schemaUpdateAttributeAccept $ schemaUpdateAttributeReject) X-ORIGIN 'Netscape Directory Server' )
-objectClasses: ( 2.16.840.1.113730.3.2.332 NAME 'nsChangelogConfig' DESC 'Configuration of the changelog5 object' SUP top MUST ( cn $ nsslapd-changelogdir ) MAY ( nsslapd-changelogmaxage $ nsslapd-changelogtrim-interval $ nsslapd-changelogmaxentries $ nsslapd-changelogsuffix $ nsslapd-changelogcompactdb-interval $ nsslapd-encryptionalgorithm $ nsSymmetricKey ) X-ORIGIN '389 Directory Server' )
+objectClasses: ( 2.16.840.1.113730.3.2.332 NAME 'nsChangelogConfig' DESC 'Configuration of the changelog5 object' SUP top MUST ( cn $ nsslapd-changelogdir ) MAY ( nsslapd-changelogmaxage $ nsslapd-changelogtrim-interval $ nsslapd-changelogmaxentries $ nsslapd-changelogsuffix $ nsslapd-changelogcompactdb-interval $ nsslapd-changelogcompactdb-time $ nsslapd-encryptionalgorithm $ nsSymmetricKey ) X-ORIGIN '389 Directory Server' )
 objectClasses: ( 2.16.840.1.113730.3.2.337 NAME 'rewriterEntry' DESC '' SUP top MUST ( nsslapd-libPath ) MAY ( cn $ nsslapd-filterrewriter $ nsslapd-returnedAttrRewriter ) X-ORIGIN '389 Directory Server' )
diff --git a/ldap/servers/plugins/replication/cl5.h b/ldap/servers/plugins/replication/cl5.h
index 2af57e369..99ea1c6a2 100644
--- a/ldap/servers/plugins/replication/cl5.h
+++ b/ldap/servers/plugins/replication/cl5.h
@@ -29,6 +29,7 @@ typedef struct changelog5Config
     char *symmetricKey;
     long compactInterval;
     long trimInterval;
+    char *compactTime;
 } changelog5Config;
 
 /* initializes changelog*/
diff --git a/ldap/servers/plugins/replication/cl5_api.c b/ldap/servers/plugins/replication/cl5_api.c
index 403a6a666..75a2f46f5 100644
--- a/ldap/servers/plugins/replication/cl5_api.c
+++ b/ldap/servers/plugins/replication/cl5_api.c
@@ -158,6 +158,7 @@ typedef struct cl5trim
     time_t maxAge;       /* maximum entry age in seconds                            */
     int maxEntries;      /* maximum number of entries across all changelog files    */
     int compactInterval; /* interval to compact changelog db */
+    char *compactTime;   /* time to compact changelog db */
     int trimInterval;    /* trimming interval */
     PRLock *lock;        /* controls access to trimming configuration            */
 } CL5Trim;
@@ -184,6 +185,7 @@ typedef struct cl5desc
     PRLock *clLock;         /* Lock associated to clVar, used to notify threads on close */
     PRCondVar *clCvar;      /* Condition Variable used to notify threads on close */
     void *clcrypt_handle;   /* for cl encryption */
+    char *compact_time;     /* Time to execute changelog compaction */
 } CL5Desc;
 
 typedef void (*VFP)(void *);
@@ -1025,7 +1027,7 @@ cl5GetState()
                 CL5_BAD_STATE if changelog is not open
  */
 int
-cl5ConfigTrimming(int maxEntries, const char *maxAge, int compactInterval, int trimInterval)
+cl5ConfigTrimming(int maxEntries, const char *maxAge, int compactInterval, char *compactTime, int trimInterval)
 {
     if (s_cl5Desc.dbState == CL5_STATE_NONE) {
         slapi_log_err(SLAPI_LOG_ERR, repl_plugin_name_cl,
@@ -1061,6 +1063,10 @@ cl5ConfigTrimming(int maxEntries, const char *maxAge, int compactInterval, int t
         s_cl5Desc.dbTrim.compactInterval = compactInterval;
     }
 
+    if (strcmp(compactTime, CL5_STR_IGNORE) != 0) {
+        s_cl5Desc.dbTrim.compactTime = slapi_ch_strdup(compactTime);
+    }
+
     if (trimInterval != CL5_NUM_IGNORE) {
         s_cl5Desc.dbTrim.trimInterval = trimInterval;
     }
@@ -3077,16 +3083,48 @@ _cl5TrimCleanup(void)
 {
     if (s_cl5Desc.dbTrim.lock)
         PR_DestroyLock(s_cl5Desc.dbTrim.lock);
+    slapi_ch_free_string(&s_cl5Desc.dbTrim.compactTime);
 
     memset(&s_cl5Desc.dbTrim, 0, sizeof(s_cl5Desc.dbTrim));
 }
 
+static time_t
+_cl5_get_tod_expiration(char *expire_time)
+{
+    time_t start_time, todays_elapsed_time, now = time(NULL);
+    struct tm *tm_struct = localtime(&now);
+    char hour_str[3] = {0};
+    char min_str[3] = {0};
+    char *s = expire_time;
+    char *endp = NULL;
+    int32_t hour, min, expiring_time;
+
+    /* Get today's start time */
+    todays_elapsed_time = (tm_struct->tm_hour * 3600) + (tm_struct->tm_min * 60) + (tm_struct->tm_sec);
+    start_time = slapi_current_utc_time() - todays_elapsed_time;
+
+    /* Get the hour and minute and calculate the expiring time.  The time was
+     * already validated in bdb_config.c:  HH:MM */
+    hour_str[0] = *s++;
+    hour_str[1] = *s++;
+    s++;  /* skip colon */
+    min_str[0] = *s++;
+    min_str[1] = *s++;
+    hour = strtoll(hour_str, &endp, 10);
+    min = strtoll(min_str, &endp, 10);
+    expiring_time = (hour * 60 * 60) + (min * 60);
+
+    return start_time + expiring_time;
+}
+
 static int
 _cl5TrimMain(void *param __attribute__((unused)))
 {
     time_t timePrev = slapi_current_utc_time();
     time_t timeCompactPrev = slapi_current_utc_time();
     time_t timeNow;
+    PRBool compacting = PR_FALSE;
+    int32_t compactdb_time = 0;
 
     PR_AtomicIncrement(&s_cl5Desc.threadCount);
 
@@ -3097,11 +3135,26 @@ _cl5TrimMain(void *param __attribute__((unused)))
             timePrev = timeNow;
             _cl5DoTrimming();
         }
+
+        if (!compacting) {
+            /* Once we know we want to compact we need to stop refreshing the
+             * TOD expiration. Otherwise if the compact time is close to
+             * midnight we could roll over past midnight during the checkpoint
+             * sleep interval, and we'd never actually compact the databases.
+             * We also need to get this value before the sleep.
+            */
+            compactdb_time = _cl5_get_tod_expiration(s_cl5Desc.dbTrim.compactTime);
+        }
         if ((s_cl5Desc.dbTrim.compactInterval > 0) &&
-            (timeNow - timeCompactPrev >= s_cl5Desc.dbTrim.compactInterval)) {
-            /* time to trim */
-            timeCompactPrev = timeNow;
-            _cl5CompactDBs();
+            (timeNow - timeCompactPrev >= s_cl5Desc.dbTrim.compactInterval))
+        {
+            compacting = PR_TRUE;
+            if (slapi_current_utc_time() > compactdb_time) {
+				/* time to trim */
+				timeCompactPrev = timeNow;
+				_cl5CompactDBs();
+				compacting = PR_FALSE;
+            }
         }
         if (NULL == s_cl5Desc.clLock) {
             /* most likely, emergency */
@@ -3215,6 +3268,10 @@ _cl5CompactDBs(void)
                       rc, db_strerror(rc));
         goto bail;
     }
+
+
+    slapi_log_err(SLAPI_LOG_NOTICE, repl_plugin_name_cl,
+                  "_cl5CompactDBs - compacting replication changelogs...\n");
     for (fileObj = objset_first_obj(s_cl5Desc.dbFiles);
          fileObj;
          fileObj = objset_next_obj(s_cl5Desc.dbFiles, fileObj)) {
@@ -3235,6 +3292,9 @@ _cl5CompactDBs(void)
                       "_cl5CompactDBs - %s - %d pages freed\n",
                       dbFile->replName, c_data.compact_pages_free);
     }
+
+    slapi_log_err(SLAPI_LOG_NOTICE, repl_plugin_name_cl,
+                  "_cl5CompactDBs - compacting replication changelogs finished.\n");
 bail:
     if (fileObj) {
         object_release(fileObj);
diff --git a/ldap/servers/plugins/replication/cl5_api.h b/ldap/servers/plugins/replication/cl5_api.h
index 302af97a0..4b0949fb3 100644
--- a/ldap/servers/plugins/replication/cl5_api.h
+++ b/ldap/servers/plugins/replication/cl5_api.h
@@ -236,7 +236,7 @@ int cl5GetState(void);
    Return:        CL5_SUCCESS if successful;
                 CL5_BAD_STATE if changelog has not been open
  */
-int cl5ConfigTrimming(int maxEntries, const char *maxAge, int compactInterval, int trimInterval);
+int cl5ConfigTrimming(int maxEntries, const char *maxAge, int compactInterval, char *compactTime, int trimInterval);
 
 void cl5DestroyIterator(void *iterator);
 
diff --git a/ldap/servers/plugins/replication/cl5_clcache.c b/ldap/servers/plugins/replication/cl5_clcache.c
index 90dec4d54..e5a39c9c1 100644
--- a/ldap/servers/plugins/replication/cl5_clcache.c
+++ b/ldap/servers/plugins/replication/cl5_clcache.c
@@ -452,9 +452,6 @@ static int
 clcache_cursor_set(DBC *cursor, CLC_Buffer *buf)
 {
     int rc;
-    uint32_t ulen;
-    uint32_t dlen;
-    uint32_t size;
 
     rc = cursor->c_get(cursor, &buf->buf_key, &buf->buf_data, DB_SET);
     if (rc == DB_BUFFER_SMALL) {
diff --git a/ldap/servers/plugins/replication/cl5_config.c b/ldap/servers/plugins/replication/cl5_config.c
index e0530bed2..b32686788 100644
--- a/ldap/servers/plugins/replication/cl5_config.c
+++ b/ldap/servers/plugins/replication/cl5_config.c
@@ -131,6 +131,7 @@ changelog5_config_done(changelog5Config *config)
         /* slapi_ch_free_string accepts NULL pointer */
         slapi_ch_free_string(&config->maxAge);
         slapi_ch_free_string(&config->dir);
+        slapi_ch_free_string(&config->compactTime);
         slapi_ch_free_string(&config->symmetricKey);
         slapi_ch_free_string(&config->dbconfig.encryptionAlgorithm);
         slapi_ch_free_string(&config->dbconfig.symmetricKey);
@@ -211,7 +212,7 @@ changelog5_config_add(Slapi_PBlock *pb __attribute__((unused)),
     }
 
     /* set trimming parameters */
-    rc = cl5ConfigTrimming(config.maxEntries, config.maxAge, config.compactInterval, config.trimInterval);
+    rc = cl5ConfigTrimming(config.maxEntries, config.maxAge, config.compactInterval, config.compactTime, config.trimInterval);
     if (rc != CL5_SUCCESS) {
         *returncode = 1;
         if (returntext) {
@@ -302,6 +303,7 @@ changelog5_config_modify(Slapi_PBlock *pb,
     config.compactInterval = CL5_NUM_IGNORE;
     slapi_ch_free_string(&config.maxAge);
     config.maxAge = slapi_ch_strdup(CL5_STR_IGNORE);
+    config.compactTime = slapi_ch_strdup(CHANGELOGDB_COMPACT_TIME);
     config.trimInterval = CL5_NUM_IGNORE;
 
     slapi_pblock_get(pb, SLAPI_MODIFY_MODS, &mods);
@@ -375,6 +377,55 @@ changelog5_config_modify(Slapi_PBlock *pb,
                         *returncode = LDAP_UNWILLING_TO_PERFORM;
                         goto done;
                     }
+                } else if (strcasecmp(config_attr, CONFIG_CHANGELOG_COMPACTTIME_ATTRIBUTE) == 0) {
+                	if (config_attr_value && config_attr_value[0] != '\0') {
+                	    char *val = slapi_ch_strdup(config_attr_value);
+                        char *endp = NULL;
+                        char *hour_str = NULL;
+                        char *min_str = NULL;
+                        int32_t hour, min;
+                        errno = 0;
+
+                        slapi_ch_free_string(&config.compactTime);
+
+                      	if (strstr(val, ":")) {
+                            /* Get the hour and minute */
+                            hour_str = ldap_utf8strtok_r(val, ":", &min_str);
+                  	        /* Validate hour */
+                   	        hour = strtoll(hour_str, &endp, 10);
+                 	        if (*endp != '\0' || errno == ERANGE || hour < 0 || hour > 23 || strlen(hour_str) != 2) {
+          	       	            slapi_create_errormsg(returntext, SLAPI_DSE_RETURNTEXT_SIZE,
+                   	                    "Invalid hour set (%s), must be a two digit number between 00 and 23",
+                   	                    hour_str);
+                   	            slapi_log_err(SLAPI_LOG_ERR, "changelog5_extract_config",
+                                       "Invalid minute set (%s), must be a two digit number between 00 and 59.  "
+                	       	                    "Using default of 23:59\n", hour_str);
+                                *returncode = LDAP_UNWILLING_TO_PERFORM;
+                   	            goto done;
+           	       	        }
+       	        	        /* Validate minute */
+           	       	        min = strtoll(min_str, &endp, 10);
+           	      	        if (*endp != '\0' || errno == ERANGE || min < 0 || min > 59 || strlen(min_str) != 2) {
+                   	            slapi_create_errormsg(returntext, SLAPI_DSE_RETURNTEXT_SIZE,
+                  	                    "Invalid minute set (%s), must be a two digit number between 00 and 59",
+                   	                    hour_str);
+                   	            slapi_log_err(SLAPI_LOG_ERR, "changelog5_extract_config",
+                   	                    "Invalid minute set (%s), must be a two digit number between 00 and 59.  "
+                   	                    "Using default of 23:59\n", min_str);
+                                *returncode = LDAP_UNWILLING_TO_PERFORM;
+                   	            goto done;
+                   	        }
+                   	    } else {
+                   	        /* Wrong format */
+                   	        slapi_create_errormsg(returntext, SLAPI_DSE_RETURNTEXT_SIZE,
+                   	                "Invalid setting (%s), must have a time format of HH:MM", val);
+                   	        slapi_log_err(SLAPI_LOG_ERR, "changelog5_extract_config",
+                   	                "Invalid setting (%s), must have a time format of HH:MM\n", val);
+                            *returncode = LDAP_UNWILLING_TO_PERFORM;
+                   	        goto done;
+                   	    }
+                        config.compactTime = slapi_ch_strdup(config_attr_value);
+                    }
                 } else if (strcasecmp(config_attr, CONFIG_CHANGELOG_TRIM_ATTRIBUTE) == 0) {
                     if (slapi_is_duration_valid(config_attr_value)) {
                         config.trimInterval = (long)slapi_parse_duration(config_attr_value);
@@ -419,6 +470,11 @@ changelog5_config_modify(Slapi_PBlock *pb,
         if (originalConfig->maxAge)
             config.maxAge = slapi_ch_strdup(originalConfig->maxAge);
     }
+    if (strcmp(config.compactTime, CL5_STR_IGNORE) == 0) {
+        slapi_ch_free_string(&config.compactTime);
+        if (originalConfig->compactTime)
+            config.compactTime = slapi_ch_strdup(originalConfig->compactTime);
+    }
 
     /* attempt to change chagelog dir */
     if (config.dir) {
@@ -519,7 +575,7 @@ changelog5_config_modify(Slapi_PBlock *pb,
     if (config.maxEntries != CL5_NUM_IGNORE ||
         config.trimInterval != CL5_NUM_IGNORE ||
         strcmp(config.maxAge, CL5_STR_IGNORE) != 0) {
-        rc = cl5ConfigTrimming(config.maxEntries, config.maxAge, config.compactInterval, config.trimInterval);
+        rc = cl5ConfigTrimming(config.maxEntries, config.maxAge, config.compactInterval, config.compactTime, config.trimInterval);
         if (rc != CL5_SUCCESS) {
             *returncode = 1;
             if (returntext) {
@@ -689,6 +745,7 @@ changelog5_extract_config(Slapi_Entry *entry, changelog5Config *config)
 {
     const char *arg;
     char *max_age = NULL;
+    char *val = NULL;
 
     memset(config, 0, sizeof(*config));
     config->dir = slapi_entry_attr_get_charptr(entry, CONFIG_CHANGELOG_DIR_ATTRIBUTE);
@@ -711,6 +768,47 @@ changelog5_extract_config(Slapi_Entry *entry, changelog5Config *config)
         config->compactInterval = CHANGELOGDB_COMPACT_INTERVAL;
     }
 
+    arg = slapi_entry_attr_get_ref(entry, CONFIG_CHANGELOG_COMPACTTIME_ATTRIBUTE);
+    if (arg) {
+        char *endp = NULL;
+        char *hour_str = NULL;
+        char *min_str = NULL;
+        int32_t hour, min;
+        errno = 0;
+
+        val = slapi_ch_strdup((char *)arg);
+    	if (strstr(val, ":")) {
+            /* Get the hour and minute */
+            hour_str = ldap_utf8strtok_r(val, ":", &min_str);
+    	        /* Validate hour */
+   	        hour = strtoll(hour_str, &endp, 10);
+   	        if (*endp != '\0' || errno == ERANGE || hour < 0 || hour > 23 || strlen(hour_str) != 2) {
+   	            slapi_log_err(SLAPI_LOG_ERR, "changelog5_extract_config",
+   	                    "Invalid minute set (%s), must be a two digit number between 00 and 59.  "
+   	                    "Using default of 23:59\n", hour_str);
+   	            goto set_default;
+   	        }
+    	        /* Validate minute */
+   	        min = strtoll(min_str, &endp, 10);
+  	        if (*endp != '\0' || errno == ERANGE || min < 0 || min > 59 || strlen(min_str) != 2) {
+   	            slapi_log_err(SLAPI_LOG_ERR, "changelog5_extract_config",
+   	                    "Invalid minute set (%s), must be a two digit number between 00 and 59.  "
+   	                    "Using default of 23:59\n", min_str);
+   	            goto set_default;
+   	        }
+   	    } else {
+   	        /* Wrong format */
+   	        slapi_log_err(SLAPI_LOG_ERR, "changelog5_extract_config",
+   	                "Invalid setting (%s), must have a time format of HH:MM\n", val);
+   	        goto set_default;
+   	    }
+        config->compactTime = slapi_ch_strdup(arg);
+    } else {
+    	set_default:
+        config->compactTime = slapi_ch_strdup(CHANGELOGDB_COMPACT_TIME);
+    }
+    slapi_ch_free_string(&val);
+
     arg = slapi_entry_attr_get_ref(entry, CONFIG_CHANGELOG_TRIM_ATTRIBUTE);
     if (arg) {
         if (slapi_is_duration_valid(arg)) {
diff --git a/ldap/servers/plugins/replication/cl5_init.c b/ldap/servers/plugins/replication/cl5_init.c
index 112c4ece4..251859714 100644
--- a/ldap/servers/plugins/replication/cl5_init.c
+++ b/ldap/servers/plugins/replication/cl5_init.c
@@ -57,7 +57,7 @@ changelog5_init()
     }
 
     /* set trimming parameters */
-    rc = cl5ConfigTrimming(config.maxEntries, config.maxAge, config.compactInterval, config.trimInterval);
+    rc = cl5ConfigTrimming(config.maxEntries, config.maxAge, config.compactInterval, config.compactTime, config.trimInterval);
     if (rc != CL5_SUCCESS) {
         slapi_log_err(SLAPI_LOG_ERR, repl_plugin_name_cl,
                       "changelog5_init: failed to configure changelog trimming\n");
diff --git a/ldap/servers/plugins/replication/repl_shared.h b/ldap/servers/plugins/replication/repl_shared.h
index b1ed86934..6708e12f7 100644
--- a/ldap/servers/plugins/replication/repl_shared.h
+++ b/ldap/servers/plugins/replication/repl_shared.h
@@ -26,11 +26,13 @@
 
 #define CHANGELOGDB_TRIM_INTERVAL 300        /* 5 minutes */
 #define CHANGELOGDB_COMPACT_INTERVAL 2592000 /* 30 days */
+#define CHANGELOGDB_COMPACT_TIME "23:55" /* 30 days */
 
 #define CONFIG_CHANGELOG_DIR_ATTRIBUTE "nsslapd-changelogdir"
 #define CONFIG_CHANGELOG_MAXENTRIES_ATTRIBUTE "nsslapd-changelogmaxentries"
 #define CONFIG_CHANGELOG_MAXAGE_ATTRIBUTE "nsslapd-changelogmaxage"
 #define CONFIG_CHANGELOG_COMPACTDB_ATTRIBUTE "nsslapd-changelogcompactdb-interval"
+#define CONFIG_CHANGELOG_COMPACTTIME_ATTRIBUTE "nsslapd-changelogcompactdb-time"
 #define CONFIG_CHANGELOG_TRIM_ATTRIBUTE "nsslapd-changelogtrim-interval"
 /* Changelog Internal Configuration Parameters -> Changelog Cache related */
 #define CONFIG_CHANGELOG_ENCRYPTION_ALGORITHM "nsslapd-encryptionalgorithm"
diff --git a/ldap/servers/plugins/retrocl/retrocl.c b/ldap/servers/plugins/retrocl/retrocl.c
index 2a620301c..f73c81528 100644
--- a/ldap/servers/plugins/retrocl/retrocl.c
+++ b/ldap/servers/plugins/retrocl/retrocl.c
@@ -400,7 +400,6 @@ retrocl_start(Slapi_PBlock *pb)
 
         for (size_t i = 0; i < num_vals; i++) {
             char *value = values[i];
-            size_t length = strlen(value);
 
             char *pos = strchr(value, ':');
             if (pos == NULL) {
diff --git a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_config.c b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_config.c
index 167644943..4261c6ce2 100644
--- a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_config.c
+++ b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_config.c
@@ -678,6 +678,84 @@ bdb_config_db_compactdb_interval_set(void *arg,
     return retval;
 }
 
+static void *
+bdb_config_db_compactdb_time_get(void *arg)
+{
+    struct ldbminfo *li = (struct ldbminfo *)arg;
+    return (void *)slapi_ch_strdup(BDB_CONFIG(li)->bdb_compactdb_time);
+}
+
+static int
+bdb_config_db_compactdb_time_set(void *arg,
+                                 void *value,
+                                 char *errorbuf __attribute__((unused)),
+                                 int phase __attribute__((unused)),
+                                 int apply)
+{
+    struct ldbminfo *li = (struct ldbminfo *)arg;
+    char *val = slapi_ch_strdup((char *)value);
+    char *endp = NULL;
+    char *hour_str = NULL;
+    char *min_str = NULL;
+    char *default_time = "23:59";
+    int32_t hour, min;
+    int retval = LDAP_SUCCESS;
+    errno = 0;
+
+    if (strstr(val, ":")) {
+        /* Get the hour and minute */
+        hour_str = ldap_utf8strtok_r(val, ":", &min_str);
+
+        /* Validate hour */
+        hour = strtoll(hour_str, &endp, 10);
+        if (*endp != '\0' || errno == ERANGE || hour < 0 || hour > 23 || strlen(hour_str) != 2) {
+            slapi_create_errormsg(errorbuf, SLAPI_DSE_RETURNTEXT_SIZE,
+                    "Invalid hour set (%s), must be a two digit number between 00 and 23",
+                    hour_str);
+            slapi_log_err(SLAPI_LOG_ERR, "bdb_config_db_compactdb_interval_set",
+                    "Invalid minute set (%s), must be a two digit number between 00 and 59.  "
+                    "Using default of 23:59\n", hour_str);
+            retval = LDAP_OPERATIONS_ERROR;
+            goto done;
+        }
+
+        /* Validate minute */
+        min = strtoll(min_str, &endp, 10);
+        if (*endp != '\0' || errno == ERANGE || min < 0 || min > 59 || strlen(min_str) != 2) {
+            slapi_create_errormsg(errorbuf, SLAPI_DSE_RETURNTEXT_SIZE,
+                    "Invalid minute set (%s), must be a two digit number between 00 and 59",
+                    hour_str);
+            slapi_log_err(SLAPI_LOG_ERR, "bdb_config_db_compactdb_interval_set",
+                    "Invalid minute set (%s), must be a two digit number between 00 and 59.  "
+                    "Using default of 23:59\n", min_str);
+            retval = LDAP_OPERATIONS_ERROR;
+            goto done;
+        }
+    } else {
+        /* Wrong format */
+        slapi_create_errormsg(errorbuf, SLAPI_DSE_RETURNTEXT_SIZE,
+                "Invalid setting (%s), must have a time format of HH:MM", val);
+        slapi_log_err(SLAPI_LOG_ERR, "bdb_config_db_compactdb_interval_set",
+                "Invalid setting (%s), must have a time format of HH:MM\n", val);
+        retval = LDAP_OPERATIONS_ERROR;
+        goto done;
+    }
+
+done:
+    if (apply) {
+        slapi_ch_free((void **)&(BDB_CONFIG(li)->bdb_compactdb_time));
+        if (retval) {
+            /* Something went wrong, use the default */
+            BDB_CONFIG(li)->bdb_compactdb_time = slapi_ch_strdup(default_time);
+        } else {
+            BDB_CONFIG(li)->bdb_compactdb_time = slapi_ch_strdup((char *)value);
+        }
+    }
+    slapi_ch_free_string(&val);
+
+    return retval;
+}
+
 static void *
 bdb_config_db_page_size_get(void *arg)
 {
@@ -1473,6 +1551,7 @@ static config_info bdb_config_param[] = {
     {CONFIG_DB_TRANSACTION_WAIT, CONFIG_TYPE_ONOFF, "off", &bdb_config_db_transaction_wait_get, &bdb_config_db_transaction_wait_set, CONFIG_FLAG_ALWAYS_SHOW | CONFIG_FLAG_ALLOW_RUNNING_CHANGE},
     {CONFIG_DB_CHECKPOINT_INTERVAL, CONFIG_TYPE_INT, "60", &bdb_config_db_checkpoint_interval_get, &bdb_config_db_checkpoint_interval_set, CONFIG_FLAG_ALWAYS_SHOW | CONFIG_FLAG_ALLOW_RUNNING_CHANGE},
     {CONFIG_DB_COMPACTDB_INTERVAL, CONFIG_TYPE_INT, "2592000" /*30days*/, &bdb_config_db_compactdb_interval_get, &bdb_config_db_compactdb_interval_set, CONFIG_FLAG_ALWAYS_SHOW | CONFIG_FLAG_ALLOW_RUNNING_CHANGE},
+    {CONFIG_DB_COMPACTDB_TIME, CONFIG_TYPE_STRING, "23:59", &bdb_config_db_compactdb_time_get, &bdb_config_db_compactdb_time_set, CONFIG_FLAG_ALWAYS_SHOW | CONFIG_FLAG_ALLOW_RUNNING_CHANGE},
     {CONFIG_DB_TRANSACTION_BATCH, CONFIG_TYPE_INT, "0", &bdb_get_batch_transactions, &bdb_set_batch_transactions, CONFIG_FLAG_ALWAYS_SHOW | CONFIG_FLAG_ALLOW_RUNNING_CHANGE},
     {CONFIG_DB_TRANSACTION_BATCH_MIN_SLEEP, CONFIG_TYPE_INT, "50", &bdb_get_batch_txn_min_sleep, &bdb_set_batch_txn_min_sleep, CONFIG_FLAG_ALWAYS_SHOW | CONFIG_FLAG_ALLOW_RUNNING_CHANGE},
     {CONFIG_DB_TRANSACTION_BATCH_MAX_SLEEP, CONFIG_TYPE_INT, "50", &bdb_get_batch_txn_max_sleep, &bdb_set_batch_txn_max_sleep, CONFIG_FLAG_ALWAYS_SHOW | CONFIG_FLAG_ALLOW_RUNNING_CHANGE},
diff --git a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.c b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.c
index 2f25f67a2..ec1976d38 100644
--- a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.c
+++ b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.c
@@ -2126,6 +2126,7 @@ bdb_post_close(struct ldbminfo *li, int dbmode)
          */
         slapi_ch_free_string(&conf->bdb_dbhome_directory);
         slapi_ch_free_string(&conf->bdb_home_directory);
+        slapi_ch_free_string(&conf->bdb_compactdb_time);
     }
 
     return return_value;
@@ -3644,6 +3645,39 @@ log_flush_threadmain(void *param)
     return 0;
 }
 
+/*
+ * This refreshes the TOD expiration.  So live changes to the configuration
+ * will take effect immediately.
+ */
+static time_t
+bdb_get_tod_expiration(char *expire_time)
+{
+    time_t start_time, todays_elapsed_time, now = time(NULL);
+    struct tm *tm_struct = localtime(&now);
+    char hour_str[3] = {0};
+    char min_str[3] = {0};
+    char *s = expire_time;
+    char *endp = NULL;
+    int32_t hour, min, expiring_time;
+
+    /* Get today's start time */
+    todays_elapsed_time = (tm_struct->tm_hour * 3600) + (tm_struct->tm_min * 60) + (tm_struct->tm_sec);
+    start_time = slapi_current_utc_time() - todays_elapsed_time;
+
+    /* Get the hour and minute and calculate the expiring time.  The time was
+     * already validated in bdb_config.c:  HH:MM */
+    hour_str[0] = *s++;
+    hour_str[1] = *s++;
+    s++;  /* skip colon */
+    min_str[0] = *s++;
+    min_str[1] = *s++;
+    hour = strtoll(hour_str, &endp, 10);
+    min = strtoll(min_str, &endp, 10);
+    expiring_time = (hour * 60 * 60) + (min * 60);
+
+    return start_time + expiring_time;
+}
+
 /*
  * create a thread for checkpoint_threadmain
  */
@@ -3685,7 +3719,9 @@ checkpoint_threadmain(void *param)
     time_t checkpoint_interval_update = 0;
     time_t compactdb_interval = 0;
     time_t checkpoint_interval = 0;
-    back_txn txn;
+    int32_t compactdb_time = 0;
+    PRBool compacting = PR_FALSE;
+
 
     PR_ASSERT(NULL != param);
     li = (struct ldbminfo *)param;
@@ -3724,22 +3760,35 @@ checkpoint_threadmain(void *param)
     slapi_timespec_expire_at(checkpoint_interval, &checkpoint_expire);
 
     while (!BDB_CONFIG(li)->bdb_stop_threads) {
-        /* sleep for a while */
-        /* why aren't we sleeping exactly the right amount of time ? */
-        /* answer---because the interval might be changed after the server
-         * starts up */
+        PR_Lock(li->li_config_mutex);
+        checkpoint_interval_update = (time_t)BDB_CONFIG(li)->bdb_checkpoint_interval;
+        compactdb_interval_update = (time_t)BDB_CONFIG(li)->bdb_compactdb_interval;
+        if (!compacting) {
+            /* Once we know we want to compact we need to stop refreshing the
+             * TOD expiration. Otherwise if the compact time is close to
+             * midnight we could roll over past midnight during the checkpoint
+             * sleep interval, and we'd never actually compact the databases.
+             * We also need to get this value before the sleep.
+             */
+            compactdb_time = bdb_get_tod_expiration((char *)BDB_CONFIG(li)->bdb_compactdb_time);
+        }
+        PR_Unlock(li->li_config_mutex);
+
+        if (compactdb_interval_update != compactdb_interval) {
+            /* Compact interval was changed, so reset the timer */
+            slapi_timespec_expire_at(compactdb_interval_update, &compactdb_expire);
+        }
 
+        /* Sleep for a while ...
+         * Why aren't we sleeping exactly the right amount of time ?
+         * Answer---because the interval might be changed after the server
+         * starts up */
         DS_Sleep(interval);
 
         if (0 == BDB_CONFIG(li)->bdb_enable_transactions) {
             continue;
         }
 
-        PR_Lock(li->li_config_mutex);
-        checkpoint_interval_update = (time_t)BDB_CONFIG(li)->bdb_checkpoint_interval;
-        compactdb_interval_update = (time_t)BDB_CONFIG(li)->bdb_compactdb_interval;
-        PR_Unlock(li->li_config_mutex);
-
         /* If the checkpoint has been updated OR we have expired */
         if (checkpoint_interval != checkpoint_interval_update ||
             slapi_timespec_expire_check(&checkpoint_expire) == TIMER_EXPIRED) {
@@ -3807,94 +3856,37 @@ checkpoint_threadmain(void *param)
 
         /*
          * Remember that if compactdb_interval is 0, timer_expired can
-         * never occur unless the value in compctdb_interval changes.
+         * never occur unless the value in compactdb_interval changes.
          *
-         * this could have been a bug infact, where compactdb_interval
+         * this could have been a bug in fact, where compactdb_interval
          * was 0, if you change while running it would never take effect ....
          */
-        if (compactdb_interval_update != compactdb_interval ||
-            slapi_timespec_expire_check(&compactdb_expire) == TIMER_EXPIRED) {
-            int rc = 0;
-            Object *inst_obj;
-            ldbm_instance *inst;
-            DB *db = NULL;
-            DB_COMPACT c_data = {0};
-
-            for (inst_obj = objset_first_obj(li->li_instance_set);
-                 inst_obj;
-                 inst_obj = objset_next_obj(li->li_instance_set, inst_obj)) {
-                inst = (ldbm_instance *)object_get_data(inst_obj);
-                rc = dblayer_get_id2entry(inst->inst_be, &db);
-                if (!db || rc) {
-                    continue;
-                }
-                slapi_log_err(SLAPI_LOG_NOTICE, "checkpoint_threadmain", "Compacting DB start: %s\n",
-                              inst->inst_name);
-
-                /*
-                 * It's possible for this to heap us after free because when we access db
-                 * *just* as the server shut's down, we don't know it. So we should probably
-                 * do something like wrapping access to the db var in a rwlock, and have "read"
-                 * to access, and take writes to change the state. This would prevent the issue.
-                 */
-                DBTYPE type;
-                rc = db->get_type(db, &type);
-                if (rc) {
-                    slapi_log_err(SLAPI_LOG_ERR, "checkpoint_threadmain",
-                                  "compactdb: failed to determine db type for %s: db error - %d %s\n",
-                                  inst->inst_name, rc, db_strerror(rc));
-                    continue;
-                }
+        if (slapi_timespec_expire_check(&compactdb_expire) == TIMER_EXPIRED) {
+            compacting = PR_TRUE;
+            if (slapi_current_utc_time() < compactdb_time) {
+                /* We have passed the interval, but we need to wait for a
+                 * particular TOD to pass before compacting */
+                continue;
+            }
 
-                rc = dblayer_txn_begin(inst->inst_be, NULL, &txn);
-                if (rc) {
-                    slapi_log_err(SLAPI_LOG_ERR, "checkpoint_threadmain", "compactdb: transaction begin failed: %d\n", rc);
-                    break;
-                }
-                /*
-                 * https://docs.oracle.com/cd/E17275_01/html/api_reference/C/BDB-C_APIReference.pdf
-                 * "DB_FREELIST_ONLY
-                 * Do no page compaction, only returning pages to the filesystem that are already free and at the end
-                 * of the file. This flag must be set if the database is a Hash access method database."
-                 *
-                 */
+            /* Time to compact the DB's */
+            dblayer_force_checkpoint(li);
+            bdb_compact(li);
+            dblayer_force_checkpoint(li);
 
-                uint32_t compact_flags = DB_FREE_SPACE;
-                if (type == DB_HASH) {
-                    compact_flags |= DB_FREELIST_ONLY;
-                }
-                rc = db->compact(db, txn.back_txn_txn, NULL /*start*/, NULL /*stop*/,
-                                 &c_data, compact_flags, NULL /*end*/);
-                if (rc) {
-                    slapi_log_err(SLAPI_LOG_ERR, "checkpoint_threadmain",
-                                  "compactdb: failed to compact %s; db error - %d %s\n",
-                                  inst->inst_name, rc, db_strerror(rc));
-                    if ((rc = dblayer_txn_abort(inst->inst_be, &txn))) {
-                        slapi_log_err(SLAPI_LOG_ERR, "checkpoint_threadmain", "compactdb: failed to abort txn (%s) db error - %d %s\n",
-                                      inst->inst_name, rc, db_strerror(rc));
-                        break;
-                    }
-                } else {
-                    slapi_log_err(SLAPI_LOG_NOTICE, "checkpoint_threadmain",
-                                  "compactdb: compact %s - %d pages freed\n",
-                                  inst->inst_name, c_data.compact_pages_free);
-                    if ((rc = dblayer_txn_commit(inst->inst_be, &txn))) {
-                        slapi_log_err(SLAPI_LOG_ERR, "checkpoint_threadmain", "compactdb: failed to commit txn (%s) db error - %d %s\n",
-                                      inst->inst_name, rc, db_strerror(rc));
-                        break;
-                    }
-                }
-            }
+            /* Now reset the timer and compacting flag */
             compactdb_interval = compactdb_interval_update;
             slapi_timespec_expire_at(compactdb_interval, &compactdb_expire);
+            compacting = PR_FALSE;
         }
     }
-    slapi_log_err(SLAPI_LOG_TRACE, "checkpoint_threadmain", "Check point before leaving\n");
+    slapi_log_err(SLAPI_LOG_HOUSE, "checkpoint_threadmain", "Check point before leaving\n");
     rval = dblayer_force_checkpoint(li);
+
 error_return:
 
     DECR_THREAD_COUNT(pEnv);
-    slapi_log_err(SLAPI_LOG_TRACE, "checkpoint_threadmain", "Leaving checkpoint_threadmain\n");
+    slapi_log_err(SLAPI_LOG_HOUSE, "checkpoint_threadmain", "Leaving checkpoint_threadmain\n");
     return rval;
 }
 
@@ -6209,3 +6201,99 @@ bdb_back_ctrl(Slapi_Backend *be, int cmd, void *info)
 
     return rc;
 }
+
+int32_t
+ldbm_back_compact(Slapi_Backend *be)
+{
+    struct ldbminfo *li = NULL;
+    int32_t rc = -1;
+
+    li = (struct ldbminfo *)be->be_database->plg_private;
+    dblayer_force_checkpoint(li);
+    rc = bdb_compact(li);
+    dblayer_force_checkpoint(li);
+    return rc;
+}
+
+
+int32_t
+bdb_compact(struct ldbminfo *li)
+{
+    Object *inst_obj;
+    ldbm_instance *inst;
+    DB *db = NULL;
+    back_txn txn = {0};
+    int rc = 0;
+    DB_COMPACT c_data = {0};
+
+    slapi_log_err(SLAPI_LOG_NOTICE, "bdb_compact",
+                  "Compacting databases ...\n");
+    for (inst_obj = objset_first_obj(li->li_instance_set);
+        inst_obj;
+        inst_obj = objset_next_obj(li->li_instance_set, inst_obj))
+    {
+        inst = (ldbm_instance *)object_get_data(inst_obj);
+        rc = dblayer_get_id2entry(inst->inst_be, &db);
+        if (!db || rc) {
+            continue;
+        }
+        slapi_log_err(SLAPI_LOG_NOTICE, "bdb_compact", "Compacting DB start: %s\n",
+                      inst->inst_name);
+
+        /*
+         * It's possible for this to heap us after free because when we access db
+         * *just* as the server shut's down, we don't know it. So we should probably
+         * do something like wrapping access to the db var in a rwlock, and have "read"
+         * to access, and take writes to change the state. This would prevent the issue.
+         */
+        DBTYPE type;
+        rc = db->get_type(db, &type);
+        if (rc) {
+            slapi_log_err(SLAPI_LOG_ERR, "bdb_compact",
+                          "compactdb: failed to determine db type for %s: db error - %d %s\n",
+                          inst->inst_name, rc, db_strerror(rc));
+            continue;
+        }
+
+        rc = dblayer_txn_begin(inst->inst_be, NULL, &txn);
+        if (rc) {
+            slapi_log_err(SLAPI_LOG_ERR, "bdb_compact", "compactdb: transaction begin failed: %d\n", rc);
+            break;
+        }
+        /*
+         * https://docs.oracle.com/cd/E17275_01/html/api_reference/C/BDB-C_APIReference.pdf
+         * "DB_FREELIST_ONLY
+         * Do no page compaction, only returning pages to the filesystem that are already free and at the end
+         * of the file. This flag must be set if the database is a Hash access method database."
+         *
+         */
+        uint32_t compact_flags = DB_FREE_SPACE;
+        if (type == DB_HASH) {
+            compact_flags |= DB_FREELIST_ONLY;
+        }
+        rc = db->compact(db, txn.back_txn_txn, NULL /*start*/, NULL /*stop*/,
+                         &c_data, compact_flags, NULL /*end*/);
+        if (rc) {
+            slapi_log_err(SLAPI_LOG_ERR, "bdb_compact",
+                    "compactdb: failed to compact %s; db error - %d %s\n",
+                    inst->inst_name, rc, db_strerror(rc));
+            if ((rc = dblayer_txn_abort(inst->inst_be, &txn))) {
+                slapi_log_err(SLAPI_LOG_ERR, "bdb_compact", "compactdb: failed to abort txn (%s) db error - %d %s\n",
+                              inst->inst_name, rc, db_strerror(rc));
+                break;
+            }
+        } else {
+            slapi_log_err(SLAPI_LOG_NOTICE, "bdb_compact",
+                          "compactdb: compact %s - %d pages freed\n",
+                          inst->inst_name, c_data.compact_pages_free);
+            if ((rc = dblayer_txn_commit(inst->inst_be, &txn))) {
+                slapi_log_err(SLAPI_LOG_ERR, "bdb_compact", "compactdb: failed to commit txn (%s) db error - %d %s\n",
+                              inst->inst_name, rc, db_strerror(rc));
+                break;
+            }
+        }
+    }
+    slapi_log_err(SLAPI_LOG_NOTICE, "bdb_compact", "Compacting databases finished.\n");
+
+    return rc;
+}
diff --git a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.h b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.h
index 6bb04d21a..e3a49dbac 100644
--- a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.h
+++ b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.h
@@ -79,7 +79,8 @@ typedef struct bdb_config
     int bdb_previous_lock_config;  /* Max lock count when we last shut down--
                                       * used to determine if we delete the mpool */
     u_int32_t bdb_deadlock_policy; /* i.e. the atype to DB_ENV->lock_detect in deadlock_threadmain */
-    int bdb_compactdb_interval;    /* interval to execute compact id2entry dbs */
+    int32_t bdb_compactdb_interval; /* interval to execute compact id2entry dbs */
+    char *bdb_compactdb_time;       /* time of day to execute compact id2entry dbs */
 } bdb_config;
 
 int bdb_init(struct ldbminfo *li, config_info *config_array);
@@ -96,6 +97,7 @@ int bdb_db_size(Slapi_PBlock *pb);
 int bdb_upgradedb(Slapi_PBlock *pb);
 int bdb_upgradednformat(Slapi_PBlock *pb);
 int bdb_upgradeddformat(Slapi_PBlock *pb);
+int32_t bdb_compact(struct ldbminfo *li);
 int bdb_restore(struct ldbminfo *li, char *src_dir, Slapi_Task *task);
 int bdb_cleanup(struct ldbminfo *li);
 int bdb_txn_begin(struct ldbminfo *li, back_txnid parent_txn, back_txn *txn, PRBool use_lock);
diff --git a/ldap/servers/slapd/back-ldbm/init.c b/ldap/servers/slapd/back-ldbm/init.c
index 4165c8fad..42c9bd00a 100644
--- a/ldap/servers/slapd/back-ldbm/init.c
+++ b/ldap/servers/slapd/back-ldbm/init.c
@@ -180,6 +180,8 @@ ldbm_back_init(Slapi_PBlock *pb)
                            (void *)ldbm_back_set_info);
     rc |= slapi_pblock_set(pb, SLAPI_PLUGIN_DB_CTRL_INFO_FN,
                            (void *)ldbm_back_ctrl_info);
+    rc |= slapi_pblock_set(pb, SLAPI_PLUGIN_DB_COMPACT_FN,
+                           (void *)ldbm_back_compact);
 
     if (rc != 0) {
         slapi_log_err(SLAPI_LOG_CRIT, "ldbm_back_init", "Failed %d\n", rc);
diff --git a/ldap/servers/slapd/back-ldbm/ldbm_config.h b/ldap/servers/slapd/back-ldbm/ldbm_config.h
index 6fa8292eb..48446193e 100644
--- a/ldap/servers/slapd/back-ldbm/ldbm_config.h
+++ b/ldap/servers/slapd/back-ldbm/ldbm_config.h
@@ -84,6 +84,7 @@ struct config_info
 #define CONFIG_DB_TRANSACTION_WAIT "nsslapd-db-transaction-wait"
 #define CONFIG_DB_CHECKPOINT_INTERVAL "nsslapd-db-checkpoint-interval"
 #define CONFIG_DB_COMPACTDB_INTERVAL "nsslapd-db-compactdb-interval"
+#define CONFIG_DB_COMPACTDB_TIME "nsslapd-db-compactdb-time"
 #define CONFIG_DB_TRANSACTION_BATCH "nsslapd-db-transaction-batch-val"
 #define CONFIG_DB_TRANSACTION_BATCH_MIN_SLEEP "nsslapd-db-transaction-batch-min-wait"
 #define CONFIG_DB_TRANSACTION_BATCH_MAX_SLEEP "nsslapd-db-transaction-batch-max-wait"
diff --git a/ldap/servers/slapd/back-ldbm/proto-back-ldbm.h b/ldap/servers/slapd/back-ldbm/proto-back-ldbm.h
index 5d618a89c..30c9003bf 100644
--- a/ldap/servers/slapd/back-ldbm/proto-back-ldbm.h
+++ b/ldap/servers/slapd/back-ldbm/proto-back-ldbm.h
@@ -478,6 +478,7 @@ void ldbm_back_search_results_release(void **search_results);
 int ldbm_back_init(Slapi_PBlock *pb);
 void ldbm_back_prev_search_results(Slapi_PBlock *pb);
 int ldbm_back_isinitialized(void);
+int32_t ldbm_back_compact(Slapi_Backend *be);
 
 /*
  * vlv.c
diff --git a/ldap/servers/slapd/filtercmp.c b/ldap/servers/slapd/filtercmp.c
index f7e3ed4d5..c886267bd 100644
--- a/ldap/servers/slapd/filtercmp.c
+++ b/ldap/servers/slapd/filtercmp.c
@@ -344,7 +344,6 @@ slapi_filter_compare(struct slapi_filter *f1, struct slapi_filter *f2)
     struct berval *inval1[2], *inval2[2], **outval1, **outval2;
     int ret;
     Slapi_Attr sattr;
-    int cmplen;
 
     slapi_log_err(SLAPI_LOG_TRACE, "slapi_filter_compare", "=>\n");
 
@@ -379,11 +378,11 @@ slapi_filter_compare(struct slapi_filter *f1, struct slapi_filter *f2)
         if (key1 && key2) {
             struct berval bvkey1 = {
                 slapi_value_get_length(key1[0]),
-                slapi_value_get_string(key1[0])
+				(char *)slapi_value_get_string(key1[0])
             };
             struct berval bvkey2 = {
                 slapi_value_get_length(key2[0]),
-                slapi_value_get_string(key2[0])
+				(char *)slapi_value_get_string(key2[0])
             };
             ret = slapi_berval_cmp(&bvkey1, &bvkey2);
         }
diff --git a/ldap/servers/slapd/pblock.c b/ldap/servers/slapd/pblock.c
index f7d1f8885..fcac53839 100644
--- a/ldap/servers/slapd/pblock.c
+++ b/ldap/servers/slapd/pblock.c
@@ -925,6 +925,12 @@ slapi_pblock_get(Slapi_PBlock *pblock, int arg, void *value)
         }
         (*(IFP *)value) = pblock->pb_plugin->plg_db2ldif;
         break;
+    case SLAPI_PLUGIN_DB_COMPACT_FN:
+        if (pblock->pb_plugin->plg_type != SLAPI_PLUGIN_DATABASE) {
+            return (-1);
+        }
+        (*(IFP *)value) = pblock->pb_plugin->plg_dbcompact;
+        break;
     case SLAPI_PLUGIN_DB_DB2INDEX_FN:
         if (pblock->pb_plugin->plg_type != SLAPI_PLUGIN_DATABASE) {
             return (-1);
@@ -2925,7 +2931,12 @@ slapi_pblock_set(Slapi_PBlock *pblock, int arg, void *value)
         }
         pblock->pb_backend->be_noacl = *((int *)value);
         break;
-
+    case SLAPI_PLUGIN_DB_COMPACT_FN:
+        if (pblock->pb_plugin->plg_type != SLAPI_PLUGIN_DATABASE) {
+            return (-1);
+        }
+        pblock->pb_plugin->plg_dbcompact = (IFP)value;
+        break;
 
     /* extendedop plugin functions */
     case SLAPI_PLUGIN_EXT_OP_FN:
@@ -4137,8 +4148,8 @@ slapi_pblock_set(Slapi_PBlock *pblock, int arg, void *value)
         break;
 
     case SLAPI_URP_TOMBSTONE_CONFLICT_DN:
-	pblock->pb_intop->pb_urp_tombstone_conflict_dn = (char *)value;
-	break;
+        pblock->pb_intop->pb_urp_tombstone_conflict_dn = (char *)value;
+        break;
 
     case SLAPI_URP_TOMBSTONE_UNIQUEID:
         _pblock_assert_pb_intop(pblock);
diff --git a/ldap/servers/slapd/slap.h b/ldap/servers/slapd/slap.h
index 3126a65f3..c48516157 100644
--- a/ldap/servers/slapd/slap.h
+++ b/ldap/servers/slapd/slap.h
@@ -1041,6 +1041,7 @@ struct slapdplugin
             IFP plg_un_db_ldif2db;              /* ldif 2 database */
             IFP plg_un_db_db2ldif;              /* database 2 ldif */
             IFP plg_un_db_db2index;             /* database 2 index */
+            IFP plg_un_db_dbcompact;            /* compact database */
             IFP plg_un_db_archive2db;           /* ldif 2 database */
             IFP plg_un_db_db2archive;           /* database 2 ldif */
             IFP plg_un_db_upgradedb;            /* convert old idl to new */
@@ -1082,6 +1083,7 @@ struct slapdplugin
 #define plg_result plg_un.plg_un_db.plg_un_db_result
 #define plg_ldif2db plg_un.plg_un_db.plg_un_db_ldif2db
 #define plg_db2ldif plg_un.plg_un_db.plg_un_db_db2ldif
+#define plg_dbcompact plg_un.plg_un_db.plg_un_db_dbcompact
 #define plg_db2index plg_un.plg_un_db.plg_un_db_db2index
 #define plg_archive2db plg_un.plg_un_db.plg_un_db_archive2db
 #define plg_db2archive plg_un.plg_un_db.plg_un_db_db2archive
diff --git a/ldap/servers/slapd/slapi-private.h b/ldap/servers/slapd/slapi-private.h
index b956ebe63..570765e47 100644
--- a/ldap/servers/slapd/slapi-private.h
+++ b/ldap/servers/slapd/slapi-private.h
@@ -928,6 +928,7 @@ int proxyauth_get_dn(Slapi_PBlock *pb, char **proxydnp, char **errtextp);
 #define SLAPI_PLUGIN_DB_GET_INFO_FN               290
 #define SLAPI_PLUGIN_DB_SET_INFO_FN               291
 #define SLAPI_PLUGIN_DB_CTRL_INFO_FN              292
+#define SLAPI_PLUGIN_DB_COMPACT_FN                294
 
 /**** End of database plugin interface. **************************************/
 
diff --git a/ldap/servers/slapd/task.c b/ldap/servers/slapd/task.c
index 93d31b806..4c7262ab3 100644
--- a/ldap/servers/slapd/task.c
+++ b/ldap/servers/slapd/task.c
@@ -1,6 +1,6 @@
 /** BEGIN COPYRIGHT BLOCK
  * Copyright (C) 2001 Sun Microsystems, Inc. Used by permission.
- * Copyright (C) 2005 Red Hat, Inc.
+ * Copyright (C) 2021 Red Hat, Inc.
  * All rights reserved.
  *
  * License: GPL (version 3 or any later version).
@@ -2928,6 +2928,105 @@ des2aes_task_destructor(Slapi_Task *task)
                   "des2aes_task_destructor <--\n");
 }
 
+struct task_compact_data
+{
+    char *suffix;
+    Slapi_Task *task;
+};
+
+static void
+compact_db_task_destructor(Slapi_Task *task)
+{
+    slapi_log_err(SLAPI_LOG_PLUGIN, "compact db task",
+                  "compact_db_task_destructor -->\n");
+    if (task) {
+        struct task_compact_data *mydata = (struct task_compact_data *)slapi_task_get_data(task);
+        while (slapi_task_get_refcount(task) > 0) {
+            /* Yield to wait for the task to finish */
+            DS_Sleep(PR_MillisecondsToInterval(100));
+        }
+        if (mydata) {
+            slapi_ch_free((void **)&mydata);
+        }
+    }
+    slapi_log_err(SLAPI_LOG_PLUGIN, "compact db task",
+                  "compact_db_task_destructor <--\n");
+}
+
+static void
+task_compact_thread(void *arg)
+{
+    struct task_compact_data *task_data = arg;
+    Slapi_Task *task = task_data->task;
+    Slapi_Backend *be = NULL;
+    char *cookie = NULL;
+    int32_t rc = -1;
+
+    slapi_task_inc_refcount(task);
+    slapi_task_begin(task, 1);
+
+    be = slapi_get_first_backend(&cookie);
+    while (be) {
+        if (be->be_private == 0) {
+            /* Found a non-private backend, start compacting */
+            rc = (be->be_database->plg_dbcompact)(be);
+            break;
+        }
+        be = (backend *)slapi_get_next_backend(cookie);
+    }
+    slapi_ch_free_string(&cookie);
+
+    slapi_task_finish(task, rc);
+    slapi_task_dec_refcount(task);
+}
+
+/*
+ * compact the BDB database
+ *
+ *  dn: cn=compact_it,cn=compact db,cn=tasks,cn=config
+ *  objectclass: top
+ *  objectclass: extensibleObject
+ *  cn: compact_it
+ */
+static int
+task_compact_db_add(Slapi_PBlock *pb,
+                    Slapi_Entry *e,
+                    Slapi_Entry *eAfter __attribute__((unused)),
+                    int *returncode,
+                    char *returntext,
+                    void *arg __attribute__((unused)))
+{
+    Slapi_Task *task = slapi_new_task(slapi_entry_get_ndn(e));
+    struct task_compact_data *task_data = NULL;
+    PRThread *thread = NULL;
+
+    slapi_task_log_notice(task, "Beginning database compaction task...\n");
+
+    /* Register our destructor for cleaning up our private data */
+    slapi_task_set_destructor_fn(task, compact_db_task_destructor);
+
+    task_data = (struct task_compact_data *)slapi_ch_calloc(1, sizeof(struct task_compact_data));
+    task_data->task = task;
+    slapi_task_set_data(task, task_data);
+
+    /* Start the compaction as a separate thread */
+    thread = PR_CreateThread(PR_USER_THREAD, task_compact_thread,
+             (void *)task_data, PR_PRIORITY_NORMAL, PR_GLOBAL_THREAD,
+             PR_UNJOINABLE_THREAD, SLAPD_DEFAULT_THREAD_STACKSIZE);
+    if (thread == NULL) {
+        slapi_log_err(SLAPI_LOG_ERR, "task_compact_db_add", "Unable to create db compact thread!\n");
+        *returncode = LDAP_OPERATIONS_ERROR;
+        slapi_ch_free((void **)&task_data);
+    }
+
+    if (*returncode != LDAP_SUCCESS) {
+        slapi_task_finish(task, *returncode);
+        return SLAPI_DSE_CALLBACK_ERROR;
+    }
+
+    return SLAPI_DSE_CALLBACK_OK;
+}
+
 /* cleanup old tasks that may still be in the DSE from a previous session
  * (this can happen if the server crashes [no matter how unlikely we like
  * to think that is].)
@@ -3010,6 +3109,7 @@ task_init(void)
     slapi_task_register_handler("sysconfig reload", task_sysconfig_reload_add);
     slapi_task_register_handler("fixup tombstones", task_fixup_tombstones_add);
     slapi_task_register_handler("des2aes", task_des2aes);
+    slapi_task_register_handler("compact db", task_compact_db_add);
 }
 
 /* called when the server is shutting down -- abort all existing tasks */
diff --git a/src/cockpit/389-console/src/database.jsx b/src/cockpit/389-console/src/database.jsx
index 11cae972c..b73dc8460 100644
--- a/src/cockpit/389-console/src/database.jsx
+++ b/src/cockpit/389-console/src/database.jsx
@@ -196,6 +196,7 @@ export class Database extends React.Component {
                                     dblocksMonitoringPause: attrs['nsslapd-db-locks-monitoring-pause'],
                                     chxpoint: attrs['nsslapd-db-checkpoint-interval'],
                                     compactinterval: attrs['nsslapd-db-compactdb-interval'],
+                                    compacttime: attrs['nsslapd-db-compactdb-time'],
                                     importcacheauto: attrs['nsslapd-import-cache-autosize'],
                                     importcachesize: attrs['nsslapd-import-cachesize'],
                                 },
diff --git a/src/cockpit/389-console/src/lib/database/databaseConfig.jsx b/src/cockpit/389-console/src/lib/database/databaseConfig.jsx
index 6a71c138d..1fa9f2cc2 100644
--- a/src/cockpit/389-console/src/lib/database/databaseConfig.jsx
+++ b/src/cockpit/389-console/src/lib/database/databaseConfig.jsx
@@ -36,6 +36,7 @@ export class GlobalDatabaseConfig extends React.Component {
             dblocksMonitoringPause: this.props.data.dblocksMonitoringPause,
             chxpoint: this.props.data.chxpoint,
             compactinterval: this.props.data.compactinterval,
+            compacttime: this.props.data.compacttime,
             importcachesize: this.props.data.importcachesize,
             importcacheauto: this.props.data.importcacheauto,
             // These variables store the original value (used for saving config)
@@ -55,6 +56,7 @@ export class GlobalDatabaseConfig extends React.Component {
             _dblocksMonitoringPause: this.props.data.dblocksMonitoringPause,
             _chxpoint: this.props.data.chxpoint,
             _compactinterval: this.props.data.compactinterval,
+            _compacttime: this.props.data.compacttime,
             _importcachesize: this.props.data.importcachesize,
             _importcacheauto: this.props.data.importcacheauto,
             _db_cache_auto: this.props.data.db_cache_auto,
@@ -186,6 +188,10 @@ export class GlobalDatabaseConfig extends React.Component {
             cmd.push("--compactdb-interval=" + this.state.compactinterval);
             requireRestart = true;
         }
+        if (this.state._compacttime != this.state.compacttime) {
+            cmd.push("--compactdb-time=" + this.state.compacttime);
+            requireRestart = true;
+        }
         if (this.state.import_cache_auto) {
             // Auto cache is selected
             if (this.state._import_cache_auto != this.state.import_cache_auto) {
@@ -485,7 +491,15 @@ export class GlobalDatabaseConfig extends React.Component {
                                             Database Compact Interval
                                         </Col>
                                         <Col sm={8}>
-                                            <input id="compactinterval" value={this.state.compactinterval} onChange={this.handleChange} className="ds-input-auto" type="text" />
+                                            <input id="compactinterval" value={this.state.compactinterval} onChange={this.handleChange} className="ds-input-auto" type="number" />
+                                        </Col>
+                                    </Row>
+                                    <Row className="ds-margin-top" title="The Time Of Day to perform the database compaction after the compact interval has been met.  Uses the format: 'HH:MM' and defaults to '23:59'. (nsslapd-db-compactdb-time)">
+                                        <Col componentClass={ControlLabel} sm={4}>
+                                            Database Compact Time
+                                        </Col>
+                                        <Col sm={8}>
+                                            <input id="compacttime" value={this.state.compacttime} onChange={this.handleChange} className="ds-input-auto" type="number" />
                                         </Col>
                                     </Row>
                                     <Row className="ds-margin-top" title="The number of database locks (nsslapd-db-locks).">
diff --git a/src/lib389/lib389/_constants.py b/src/lib389/lib389/_constants.py
index c184c8d4f..d6161cebb 100644
--- a/src/lib389/lib389/_constants.py
+++ b/src/lib389/lib389/_constants.py
@@ -154,6 +154,7 @@ DN_EUUID_TASK = "cn=entryuuid task,%s" % DN_TASKS
 DN_TOMB_FIXUP_TASK = "cn=fixup tombstones,%s" % DN_TASKS
 DN_FIXUP_LINKED_ATTIBUTES = "cn=fixup linked attributes,%s" % DN_TASKS
 DN_AUTOMEMBER_REBUILD_TASK = "cn=automember rebuild membership,%s" % DN_TASKS
+DN_COMPACTDB_TASK = "cn=compact db,%s" % DN_TASKS
 
 # Script Constants
 LDIF2DB = 'ldif2db'
diff --git a/src/lib389/lib389/backend.py b/src/lib389/lib389/backend.py
index 13bb27842..ad78a6ffe 100644
--- a/src/lib389/lib389/backend.py
+++ b/src/lib389/lib389/backend.py
@@ -1005,6 +1005,7 @@ class DatabaseConfig(DSLdapObject):
                     'nsslapd-db-transaction-wait',
                     'nsslapd-db-checkpoint-interval',
                     'nsslapd-db-compactdb-interval',
+                    'nsslapd-db-compactdb-time',
                     'nsslapd-db-page-size',
                     'nsslapd-db-transaction-batch-val',
                     'nsslapd-db-transaction-batch-min-wait',
diff --git a/src/lib389/lib389/cli_conf/backend.py b/src/lib389/lib389/cli_conf/backend.py
index 722764d10..7b2f32c23 100644
--- a/src/lib389/lib389/cli_conf/backend.py
+++ b/src/lib389/lib389/cli_conf/backend.py
@@ -1,5 +1,5 @@
 # --- BEGIN COPYRIGHT BLOCK ---
-# Copyright (C) 2020 Red Hat, Inc.
+# Copyright (C) 2021 Red Hat, Inc.
 # Copyright (C) 2019 William Brown <william@blackhats.net.au>
 # All rights reserved.
 #
@@ -19,6 +19,7 @@ from lib389.chaining import (ChainingLinks)
 from lib389.monitor import MonitorLDBM
 from lib389.replica import Replicas
 from lib389.utils import ensure_str, is_a_dn, is_dn_parent
+from lib389.tasks import DBCompactTask
 from lib389._constants import *
 from lib389.cli_base import (
     _format_status,
@@ -41,6 +42,7 @@ arg_to_attr = {
         'txn_wait': 'nsslapd-db-transaction-wait',
         'checkpoint_interval': 'nsslapd-db-checkpoint-interval',
         'compactdb_interval': 'nsslapd-db-compactdb-interval',
+        'compactdb_time': 'nsslapd-db-compactdb-time',
         'txn_batch_val': 'nsslapd-db-transaction-batch-val',
         'txn_batch_min': 'nsslapd-db-transaction-batch-min-wait',
         'txn_batch_max': 'nsslapd-db-transaction-batch-max-wait',
@@ -789,6 +791,18 @@ def backend_reindex_vlv(inst, basedn, log, args):
     log.info("Successfully reindexed VLV indexes")
 
 
+def backend_compact(inst, basedn, log, args):
+    task = DBCompactTask(inst)
+    task_properties = {}
+    if args.only_changelog:
+        task_properties = {'justChangelog': 'yes'}
+    task.create(properties=task_properties)
+    task.wait()
+    if task.get_exit_code() != 0:
+        raise ValueError("Failed to create Database Compaction Task")
+    log.info("Successfully started Database Compaction Task")
+
+
 def create_parser(subparsers):
     backend_parser = subparsers.add_parser('backend', help="Manage database suffixes and backends")
     subcommands = backend_parser.add_subparsers(help="action")
@@ -994,6 +1008,7 @@ def create_parser(subparsers):
     set_db_config_parser.add_argument('--checkpoint-interval', help='Sets the amount of time in seconds after which the Directory Server sends a '
                                                                     'checkpoint entry to the database transaction log')
     set_db_config_parser.add_argument('--compactdb-interval', help='Sets the interval in seconds when the database is compacted')
+    set_db_config_parser.add_argument('--compactdb-time', help='Sets the Time Of Day to compact the database after the "compactdb interval" has been reached:  Use this format to set the hour and minute: HH:MM')
     set_db_config_parser.add_argument('--txn-batch-val', help='Specifies how many transactions will be batched before being committed')
     set_db_config_parser.add_argument('--txn-batch-min', help='Controls when transactions should be flushed earliest, independently of '
                                                               'the batch count (only works when txn-batch-val is set)')
@@ -1121,3 +1136,10 @@ def create_parser(subparsers):
     #######################################################
     get_tree_parser = subcommands.add_parser('get-tree', help='Get a representation of the suffix tree')
     get_tree_parser.set_defaults(func=backend_get_tree)
+
+    #######################################################
+    # Run the db compaction task
+    #######################################################
+    compact_parser = subcommands.add_parser('compact-db', help='Compact the database and the replication changelog')
+    compact_parser.set_defaults(func=backend_compact)
+    compact_parser.add_argument('--only-changelog', action='store_true', help='Only compact the Replication Change Log')
diff --git a/src/lib389/lib389/cli_conf/replication.py b/src/lib389/lib389/cli_conf/replication.py
index 04886f632..3478a0a1f 100644
--- a/src/lib389/lib389/cli_conf/replication.py
+++ b/src/lib389/lib389/cli_conf/replication.py
@@ -37,6 +37,7 @@ arg_to_attr = {
         'max_entries': 'nsslapd-changelogmaxentries',
         'max_age': 'nsslapd-changelogmaxage',
         'compact_interval': 'nsslapd-changelogcompactdb-interval',
+        'compact_time': 'nsslapd-changelogcompactdb-time',
         'trim_interval': 'nsslapd-changelogtrim-interval',
         'encrypt_algo': 'nsslapd-encryptionalgorithm',
         'encrypt_key': 'nssymmetrickey',
@@ -1216,6 +1217,8 @@ def create_parser(subparsers):
     repl_set_cl.add_argument('--max-entries', help="The maximum number of entries to get in the replication changelog")
     repl_set_cl.add_argument('--max-age', help="The maximum age of a replication changelog entry")
     repl_set_cl.add_argument('--compact-interval', help="The replication changelog compaction interval")
+    repl_set_cl.add_argument('--compact-time', help='Sets the Time Of Day to compact the database after the changelog "compact interval" '
+                                                    'has been reached:  Use this format to set the hour and minute: HH:MM')
     repl_set_cl.add_argument('--trim-interval', help="The interval to check if the replication changelog can be trimmed")
 
     repl_get_cl = repl_subcommands.add_parser('get-changelog', help='Display replication changelog attributes.')
diff --git a/src/lib389/lib389/tasks.py b/src/lib389/lib389/tasks.py
index 590c6ee79..b64bc6ce5 100644
--- a/src/lib389/lib389/tasks.py
+++ b/src/lib389/lib389/tasks.py
@@ -217,6 +217,19 @@ class EntryUUIDFixupTask(Task):
         self._must_attributes.extend(['basedn'])
 
 
+class DBCompactTask(Task):
+    """A single instance of compactdb task entry
+
+    :param instance: An instance
+    :type instance: lib389.DirSrv
+    """
+
+    def __init__(self, instance, dn=None):
+        self.cn = 'compact_db_' + Task._get_task_date()
+        dn = "cn=" + self.cn + "," + DN_COMPACTDB_TASK
+        super(DBCompactTask, self).__init__(instance, dn)
+
+
 class SchemaReloadTask(Task):
     """A single instance of schema reload task entry
 
@@ -227,7 +240,6 @@ class SchemaReloadTask(Task):
     def __init__(self, instance, dn=None):
         self.cn = 'schema_reload_' + Task._get_task_date()
         dn = "cn=" + self.cn + ",cn=schema reload task," + DN_TASKS
-
         super(SchemaReloadTask, self).__init__(instance, dn)
 
 
-- 
2.26.3