diff --git a/opendj-server-legacy/src/main/java/org/opends/admin/ads/util/ConnectionUtils.java b/opendj-server-legacy/src/main/java/org/opends/admin/ads/util/ConnectionUtils.java index 3501effc1c..3036dd1f38 100644 --- a/opendj-server-legacy/src/main/java/org/opends/admin/ads/util/ConnectionUtils.java +++ b/opendj-server-legacy/src/main/java/org/opends/admin/ads/util/ConnectionUtils.java @@ -13,10 +13,16 @@ * * Copyright 2008-2010 Sun Microsystems, Inc. * Portions Copyright 2012-2016 ForgeRock AS. + * Portions Copyright 2026 3A Systems, LLC. */ package org.opends.admin.ads.util; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + import org.forgerock.opendj.ldap.Attribute; +import org.forgerock.opendj.ldap.ByteString; import org.forgerock.opendj.ldap.Entry; /** @@ -44,4 +50,30 @@ public static String firstValueAsString(Entry entry, String attrDesc) Attribute attr = entry.getAttribute(attrDesc); return (attr != null && !attr.isEmpty()) ? attr.firstValueAsString() : null; } + + /** + * Returns all the values of this attribute decoded as UTF-8 strings, in the order they were + * returned by the server. + * + * @param entry + * the entry + * @param attrDesc + * the attribute description + * @return all the values of this attribute decoded as UTF-8 strings, an empty list if the + * attribute is not present. + */ + public static List allValuesAsStrings(Entry entry, String attrDesc) + { + Attribute attr = entry.getAttribute(attrDesc); + if (attr == null || attr.isEmpty()) + { + return Collections.emptyList(); + } + List values = new ArrayList<>(attr.size()); + for (ByteString value : attr) + { + values.add(value.toString()); + } + return values; + } } diff --git a/opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/Installer.java b/opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/Installer.java index 661af28dd3..ac312fc4ba 100644 --- a/opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/Installer.java +++ b/opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/Installer.java @@ -4356,7 +4356,8 @@ public void initializeSuffix(ConnectionWrapper conn, int replicaServerId, DN suf } } - String logMsg = firstValueAsString(sr, "ds-task-log-message"); + List logMsgs = InstallerHelper.getTaskLogMessages(sr); + String logMsg = InstallerHelper.getRelevantLogMessage(logMsgs); if (logMsg != null && !logMsg.equals(lastLogMsg)) { logger.info(LocalizableMessage.raw(logMsg)); @@ -4399,7 +4400,7 @@ public void initializeSuffix(ConnectionWrapper conn, int replicaServerId, DN suf else if (!TaskState.isSuccessful(taskState) || taskState == STOPPED_BY_ERROR) { ApplicationException ae = new ApplicationException(ReturnCode.APPLICATION_ERROR, errorMsg, null); - if (lastLogMsg == null || helper.isPeersNotFoundError(lastLogMsg)) + if (helper.isPeersNotFoundError(logMsgs)) { logger.warn(LocalizableMessage.raw("Throwing peer not found error. " + "Last Log Msg: " + lastLogMsg)); // Assume that this is a peer not found error. @@ -4555,7 +4556,7 @@ private void resetGenerationId(ConnectionWrapper conn, DN suffixDn, HostPort sou newSearchRequest(dn, BASE_OBJECT, "(objectclass=*)", "ds-task-log-message", "ds-task-state"); SearchResultEntry sr = conn.getConnection().searchSingleEntry(searchRequest); - String logMsg = firstValueAsString(sr, "ds-task-log-message"); + String logMsg = InstallerHelper.getRelevantLogMessage(InstallerHelper.getTaskLogMessages(sr)); if (logMsg != null && !logMsg.equals(lastLogMsg)) { logger.info(LocalizableMessage.raw(logMsg)); diff --git a/opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/InstallerHelper.java b/opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/InstallerHelper.java index 919ff3d427..3c5e499dc5 100644 --- a/opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/InstallerHelper.java +++ b/opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/InstallerHelper.java @@ -20,8 +20,10 @@ import static com.forgerock.opendj.cli.Utils.*; import static com.forgerock.opendj.util.OperatingSystem.*; +import static org.opends.admin.ads.util.ConnectionUtils.allValuesAsStrings; import static org.opends.messages.QuickSetupMessages.*; import static org.opends.quicksetup.Installation.*; +import static org.opends.server.config.ConfigConstants.ATTR_TASK_LOG_MESSAGES; import static org.opends.server.types.ExistingFileBehavior.*; import static org.opends.server.types.HostPort.*; @@ -36,6 +38,7 @@ import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; @@ -53,6 +56,7 @@ import org.forgerock.opendj.config.PropertyException; import org.forgerock.opendj.config.server.ConfigException; import org.forgerock.opendj.ldap.DN; +import org.forgerock.opendj.ldap.Entry; import org.forgerock.opendj.server.config.client.BackendCfgClient; import org.forgerock.opendj.server.config.client.CryptoManagerCfgClient; import org.forgerock.opendj.server.config.client.LocalBackendCfgClient; @@ -70,6 +74,7 @@ import org.opends.messages.BackendMessages; import org.opends.messages.CoreMessages; import org.opends.messages.ReplicationMessages; +import org.opends.messages.Severity; import org.opends.quicksetup.Application; import org.opends.quicksetup.ApplicationException; import org.opends.quicksetup.JavaArguments; @@ -105,6 +110,23 @@ public class InstallerHelper { private static final int MAX_ID_VALUE = Short.MAX_VALUE; private static final long ONE_MEGABYTE = 1024L * 1024; + /** + * The severity field of a message logged by a task with an error severity, as rendered by + * {@code org.opends.server.backends.task.Task#addLogMessage}: it follows the timestamp and + * precedes the message count, so it cannot be mistaken for the text of the message. + */ + private static final String ERROR_SEVERITY_FIELD = + "] severity=\"" + Severity.ERROR.name() + "\" msgCount="; + /** + * The message id field of the peers not found error, as rendered by + * {@code org.opends.server.backends.task.Task#addLogMessage}: a message is identified in a task + * log by its resource name and its ordinal, not by its ordinal alone, and the field ends where + * the message text starts, so the ordinal is matched whole. + */ + private static final String PEERS_NOT_FOUND_MSG_ID_FIELD = "msgID=" + + ReplicationMessages.ERR_NO_REACHABLE_PEER_IN_THE_DOMAIN.resourceName() + "-" + + ReplicationMessages.ERR_NO_REACHABLE_PEER_IN_THE_DOMAIN.ordinal() + " message=\""; + /** * Invokes the method ConfigureDS.configMain with the provided parameters. * @param args the arguments to be passed to ConfigureDS.configMain. @@ -674,18 +696,87 @@ else if (sync.hasReplicationServer()) } } + /** + * Returns the messages logged by the task described by the provided entry, in the order the + * server logged them. + * + * @param taskEntry + * the entry of the task. + * @return the messages logged by the task, an empty list if it logged none. + */ + public static List getTaskLogMessages(Entry taskEntry) + { + return allValuesAsStrings(taskEntry, ATTR_TASK_LOG_MESSAGES); + } + + /** + * Returns the message that best describes the outcome of a task among the messages it logged. + *

+ * This is the last message logged with the {@link Severity#ERROR} severity when there is one: + * a task keeps logging after it failed - the task scheduler itself appends a completion notice + * once the task is over - and those trailing messages hide the cause of the failure. It is the + * last message logged otherwise. + * + * @param logMsgs + * the messages logged by the task, as returned by {@link #getTaskLogMessages(Entry)}. + * @return the most relevant message, {@code null} if the task logged none. + */ + public static String getRelevantLogMessage(List logMsgs) + { + String lastErrorMsg = null; + for (String logMsg : logMsgs) + { + if (logMsg.contains(ERROR_SEVERITY_FIELD)) + { + lastErrorMsg = logMsg; + } + } + if (lastErrorMsg != null) + { + return lastErrorMsg; + } + return !logMsgs.isEmpty() ? logMsgs.get(logMsgs.size() - 1) : null; + } + /** * Tells whether the provided log message corresponds to a peers not found * error during the initialization of a replica or not. + *

+ * The message is recognized by the id a task logs it with, which is made of the name of the + * resource the message belongs to and of its ordinal within that resource. * * @param logMsg - * the log message. + * the log message, may be {@code null}. * @return {@code true} if the log message corresponds to a peers not * found error during initialization, {@code false} otherwise. */ public boolean isPeersNotFoundError(String logMsg) { - return logMsg.contains("=" + ReplicationMessages.ERR_NO_REACHABLE_PEER_IN_THE_DOMAIN.ordinal()); + return logMsg != null && logMsg.contains(PEERS_NOT_FOUND_MSG_ID_FIELD); + } + + /** + * Tells whether one of the provided log messages corresponds to a peers not found error during + * the initialization of a replica or not. + *

+ * All the messages must be tested: the message reporting the failure is neither the first one + * logged by the task nor, since the task scheduler appends a completion notice, the last one. + * + * @param logMsgs + * the log messages of the task, as returned by {@link #getTaskLogMessages(Entry)}. + * @return {@code true} if one of the log messages corresponds to a peers not + * found error during initialization, {@code false} otherwise. + */ + public boolean isPeersNotFoundError(Collection logMsgs) + { + for (String logMsg : logMsgs) + { + if (isPeersNotFoundError(logMsg)) + { + return true; + } + } + return false; } /** diff --git a/opendj-server-legacy/src/main/java/org/opends/server/tools/dsreplication/ReplicationCliMain.java b/opendj-server-legacy/src/main/java/org/opends/server/tools/dsreplication/ReplicationCliMain.java index bfed2aa6a4..4dbb2396c7 100644 --- a/opendj-server-legacy/src/main/java/org/opends/server/tools/dsreplication/ReplicationCliMain.java +++ b/opendj-server-legacy/src/main/java/org/opends/server/tools/dsreplication/ReplicationCliMain.java @@ -1420,7 +1420,7 @@ private void waitUntilResetChangeNumberTaskEnds(ConnectionWrapper conn, String t try { SearchResultEntry sr = getLastSearchResult(conn, taskDN, "ds-task-log-message", "ds-task-state"); - String logMsg = firstValueAsString(sr, "ds-task-log-message"); + String logMsg = InstallerHelper.getRelevantLogMessage(InstallerHelper.getTaskLogMessages(sr)); if (logMsg != null && !logMsg.equals(lastLogMsg)) { logger.info(LocalizableMessage.raw(logMsg)); @@ -1574,7 +1574,7 @@ private ReplicationCliReturnCode purgeHistoricalRemoteTask(ConnectionWrapper con "ds-task-purge-conflicts-historical-purge-completed-in-time", "ds-task-purge-conflicts-historical-purge-completed-in-time", "ds-task-purge-conflicts-historical-last-purged-changenumber"); - String logMsg = firstValueAsString(sr, "ds-task-log-message"); + String logMsg = InstallerHelper.getRelevantLogMessage(InstallerHelper.getTaskLogMessages(sr)); if (logMsg != null && !logMsg.equals(lastLogMsg)) { logger.info(LocalizableMessage.raw(logMsg)); @@ -6734,7 +6734,7 @@ private void postPreExternalInitialization(DN baseDN, try { SearchResultEntry sr = getLastSearchResult(conn, dn, "ds-task-log-message", "ds-task-state"); - String logMsg = firstValueAsString(sr, "ds-task-log-message"); + String logMsg = InstallerHelper.getRelevantLogMessage(InstallerHelper.getTaskLogMessages(sr)); if (logMsg != null && !logMsg.equals(lastLogMsg)) { logger.info(LocalizableMessage.raw(logMsg)); @@ -6870,7 +6870,8 @@ private void initializeAllSuffixTry(DN baseDN, ConnectionWrapper conn, boolean d } } - String logMsg = firstValueAsString(sr, "ds-task-log-message"); + List logMsgs = InstallerHelper.getTaskLogMessages(sr); + String logMsg = InstallerHelper.getRelevantLogMessage(logMsgs); if (logMsg != null && !logMsg.equals(lastLogMsg)) { logger.info(LocalizableMessage.raw(logMsg)); @@ -6906,8 +6907,7 @@ else if (!TaskState.isSuccessful(taskState) || taskState == STOPPED_BY_ERROR) ClientException ce = new ClientException( ReturnCode.APPLICATION_ERROR, errorMsg, null); - if (lastLogMsg == null - || helper.isPeersNotFoundError(lastLogMsg)) + if (helper.isPeersNotFoundError(logMsgs)) { logger.warn(LocalizableMessage.raw("Throwing peer not found error. "+ "Last Log Msg: "+lastLogMsg)); diff --git a/opendj-server-legacy/src/test/java/org/opends/quicksetup/installer/InstallerHelperTest.java b/opendj-server-legacy/src/test/java/org/opends/quicksetup/installer/InstallerHelperTest.java new file mode 100644 index 0000000000..c4d28abce6 --- /dev/null +++ b/opendj-server-legacy/src/test/java/org/opends/quicksetup/installer/InstallerHelperTest.java @@ -0,0 +1,182 @@ +/* + * The contents of this file are subject to the terms of the Common Development and + * Distribution License (the License). You may not use this file except in compliance with the + * License. + * + * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the + * specific language governing permission and limitations under the License. + * + * When distributing Covered Software, include this CDDL Header Notice in each file and include + * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL + * Header, with the fields enclosed by brackets [] replaced by your own identifying + * information: "Portions copyright [year] [name of copyright owner]". + * + * Copyright 2026 3A Systems, LLC. + */ +package org.opends.quicksetup.installer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.opends.messages.ReplicationMessages.ERR_NO_REACHABLE_PEER_IN_THE_DOMAIN; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.forgerock.opendj.ldap.Entry; +import org.forgerock.opendj.ldap.LinkedAttribute; +import org.forgerock.opendj.ldap.LinkedHashMapEntry; +import org.opends.server.DirectoryServerTestCase; +import org.testng.annotations.Test; + +/** + * Tests the classification of the messages a task logs into its entry, as read by + * {@code dsreplication} and by the quick setup while they wait for a task to complete. + *

+ * The messages below are the ones a real initialization task logs, in the form rendered by + * {@code org.opends.server.backends.task.Task#addLogMessage}: a start notice from the task + * scheduler, then whatever the task logged, then the completion notice the scheduler appends + * once the task is over - all of them before the terminal task state becomes visible. + */ +@SuppressWarnings("javadoc") +public class InstallerHelperTest extends DirectoryServerTestCase +{ + private static final String TASK_STARTED = + "[09/Sep/2026:12:06:48 +0000] severity=\"NOTICE\" msgCount=0 msgID=org.opends.messages.backend-413" + + " message=\"Initialize From Replica task quicksetup-initialize3 started execution\""; + private static final String PEERS_NOT_FOUND = + "[09/Sep/2026:12:08:48 +0000] severity=\"ERROR\" msgCount=1 msgID=org.opends.messages.replication-47" + + " message=\"Domain dc=example,dc=com: the server with serverId=12345 is unreachable\""; + private static final String IMPORT_NOT_SUPPORTED = + "[09/Sep/2026:12:08:48 +0000] severity=\"ERROR\" msgCount=2 msgID=org.opends.messages.replication-82" + + " message=\" Initialization cannot be done because import is not supported by the backend userRoot\""; + private static final String TASK_FINISHED = + "[09/Sep/2026:12:08:48 +0000] severity=\"NOTICE\" msgCount=3 msgID=org.opends.messages.backend-414" + + " message=\"Initialize From Replica task quicksetup-initialize3 finished execution in the state" + + " Stopped by error\""; + + /** + * The message id of the peers not found error, as a task renders it. The predicate under test is + * built from the message descriptor: this pins the descriptor to the form the other tests use. + */ + @Test + public void peersNotFoundErrorIsIdentifiedByResourceNameAndOrdinal() + { + assertThat(ERR_NO_REACHABLE_PEER_IN_THE_DOMAIN.resourceName() + "-" + + ERR_NO_REACHABLE_PEER_IN_THE_DOMAIN.ordinal()).isEqualTo("org.opends.messages.replication-47"); + } + + @Test + public void peersNotFoundErrorIsRecognized() + { + assertThat(new InstallerHelper().isPeersNotFoundError(PEERS_NOT_FOUND)).isTrue(); + } + + /** + * The ordinal alone identifies no message: it is unique within a message file only, and the + * count of the messages a task logged carries the same digits. + */ + @Test + public void anotherMessageCarryingTheOrdinalOfThePeersNotFoundErrorIsNotRecognized() + { + final String otherError = + "[09/Sep/2026:12:08:48 +0000] severity=\"ERROR\" msgCount=47 msgID=org.opends.messages.replication-45" + + " message=\"On domain dc=example,dc=com, initialization of server with serverId:12345 has been" + + " requested from a server with an invalid serverId:0. \""; + assertThat(new InstallerHelper().isPeersNotFoundError(otherError)).isFalse(); + } + + /** + * The ordinal is unique within a message file only: message 47 of another file is not this error. + * Nor is a message of the same file whose ordinal merely starts with the same digits. + */ + @Test + public void aMessageOfAnotherResourceOrWithALongerOrdinalIsNotRecognized() + { + final String waitingOnStartTime = + "[09/Sep/2026:12:08:48 +0000] severity=\"INFORMATION\" msgCount=1 msgID=org.opends.messages.task-47" + + " message=\"Waiting on start time\""; + final String longerOrdinal = + "[09/Sep/2026:12:08:48 +0000] severity=\"ERROR\" msgCount=1 msgID=org.opends.messages.replication-470" + + " message=\"Domain dc=example,dc=com: the server with serverId=12345 is unreachable\""; + assertThat(new InstallerHelper().isPeersNotFoundError(waitingOnStartTime)).isFalse(); + assertThat(new InstallerHelper().isPeersNotFoundError(longerOrdinal)).isFalse(); + } + + @Test + public void aNoticeIsNotAPeersNotFoundError() + { + assertThat(new InstallerHelper().isPeersNotFoundError(TASK_STARTED)).isFalse(); + assertThat(new InstallerHelper().isPeersNotFoundError((String) null)).isFalse(); + } + + /** + * The failure of a task is reported neither by the first message it logs nor by the last one, so + * all of them have to be tested. This is the sequence of the task of issue #995. + */ + @Test + public void peersNotFoundErrorIsFoundAmongAllTheMessagesOfTheTask() + { + final InstallerHelper helper = new InstallerHelper(); + assertThat(helper.isPeersNotFoundError(Arrays.asList(TASK_STARTED, PEERS_NOT_FOUND, TASK_FINISHED))).isTrue(); + assertThat(helper.isPeersNotFoundError(Arrays.asList(TASK_STARTED, IMPORT_NOT_SUPPORTED, TASK_FINISHED))) + .isFalse(); + assertThat(helper.isPeersNotFoundError(Collections. emptyList())).isFalse(); + } + + /** The completion notice appended by the task scheduler must not hide the cause of the failure. */ + @Test + public void relevantLogMessageIsTheLastErrorRatherThanTheLastMessage() + { + assertThat(InstallerHelper.getRelevantLogMessage( + Arrays.asList(TASK_STARTED, PEERS_NOT_FOUND, TASK_FINISHED))).isEqualTo(PEERS_NOT_FOUND); + assertThat(InstallerHelper.getRelevantLogMessage( + Arrays.asList(TASK_STARTED, PEERS_NOT_FOUND, IMPORT_NOT_SUPPORTED, TASK_FINISHED))) + .isEqualTo(IMPORT_NOT_SUPPORTED); + } + + /** + * The severity of a message is a field of its own, not a word of its text: a notice whose text + * names the state of a failed task is not the error. + */ + @Test + public void aNoticeMentioningAnErrorIsNotTheError() + { + final String notice = + "[09/Sep/2026:12:08:48 +0000] severity=\"NOTICE\" msgCount=3 msgID=org.opends.messages.backend-414" + + " message=\"Initialize From Replica task quicksetup-initialize3 finished execution in the state" + + " STOPPED_BY_ERROR\""; + assertThat(InstallerHelper.getRelevantLogMessage(Arrays.asList(TASK_STARTED, PEERS_NOT_FOUND, notice))) + .isEqualTo(PEERS_NOT_FOUND); + } + + @Test + public void relevantLogMessageIsTheLastMessageWhenTheTaskLoggedNoError() + { + assertThat(InstallerHelper.getRelevantLogMessage(Arrays.asList(TASK_STARTED, TASK_FINISHED))) + .isEqualTo(TASK_FINISHED); + assertThat(InstallerHelper.getRelevantLogMessage(Collections. emptyList())).isNull(); + } + + @Test + public void taskLogMessagesAreReadInTheOrderTheyWereLogged() + { + final Entry taskEntry = new LinkedHashMapEntry("ds-task-id=quicksetup-initialize3,cn=Scheduled Tasks,cn=Tasks"); + taskEntry.addAttribute(new LinkedAttribute("ds-task-log-message", + TASK_STARTED, PEERS_NOT_FOUND, TASK_FINISHED)); + + final List logMsgs = InstallerHelper.getTaskLogMessages(taskEntry); + + assertThat(logMsgs).containsExactly(TASK_STARTED, PEERS_NOT_FOUND, TASK_FINISHED); + assertThat(new InstallerHelper().isPeersNotFoundError(logMsgs)).isTrue(); + assertThat(InstallerHelper.getRelevantLogMessage(logMsgs)).isEqualTo(PEERS_NOT_FOUND); + } + + @Test + public void taskWithoutLogMessagesReadsAsAnEmptyLog() + { + final Entry taskEntry = new LinkedHashMapEntry("ds-task-id=quicksetup-initialize3,cn=Scheduled Tasks,cn=Tasks"); + + assertThat(InstallerHelper.getTaskLogMessages(taskEntry)).isEmpty(); + assertThat(InstallerHelper.getRelevantLogMessage(InstallerHelper.getTaskLogMessages(taskEntry))).isNull(); + } +} diff --git a/opendj-server-legacy/src/test/java/org/opends/server/tasks/TaskLogMessagesTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/tasks/TaskLogMessagesTestCase.java new file mode 100644 index 0000000000..c0e14db763 --- /dev/null +++ b/opendj-server-legacy/src/test/java/org/opends/server/tasks/TaskLogMessagesTestCase.java @@ -0,0 +1,117 @@ +/* + * The contents of this file are subject to the terms of the Common Development and + * Distribution License (the License). You may not use this file except in compliance with the + * License. + * + * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the + * specific language governing permission and limitations under the License. + * + * When distributing Covered Software, include this CDDL Header Notice in each file and include + * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL + * Header, with the fields enclosed by brackets [] replaced by your own identifying + * information: "Portions copyright [year] [name of copyright owner]". + * + * Copyright 2026 3A Systems, LLC. + */ +package org.opends.server.tasks; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.opends.messages.BackendMessages.NOTE_TASK_FINISHED; +import static org.opends.messages.BackendMessages.NOTE_TASK_STARTED; +import static org.opends.messages.ToolMessages.ERR_LDIFEXPORT_NO_BACKENDS_FOR_ID; +import static org.opends.server.config.ConfigConstants.ATTR_TASK_LOG_MESSAGES; +import static org.opends.server.protocols.internal.InternalClientConnection.getRootConnection; +import static org.opends.server.protocols.internal.Requests.newSearchRequest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import org.forgerock.opendj.ldap.ByteString; +import org.forgerock.opendj.ldap.DN; +import org.forgerock.opendj.ldap.SearchScope; +import org.opends.quicksetup.installer.InstallerHelper; +import org.opends.server.TestCaseUtils; +import org.opends.server.backends.task.TaskState; +import org.opends.server.protocols.internal.InternalSearchOperation; +import org.opends.server.types.Attribute; +import org.opends.server.types.Entry; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +/** + * Tests what the messages a task logs into its entry look like once the task is over, since the + * quick setup and {@code dsreplication} read them to report and to classify the failure of the + * tasks they run - see {@code InstallerHelper} and issue #995. + */ +@SuppressWarnings("javadoc") +public class TaskLogMessagesTestCase extends TasksTestCase +{ + @BeforeClass + public void startServer() throws Exception + { + TestCaseUtils.startServer(); + } + + /** + * The failure of a task is reported by neither the first nor the last message it logged: the + * task scheduler frames the execution with a start and a completion notice, and both of them are + * in the entry before the terminal task state is, so a reader that waits for that state always + * sees them. The cause has to be looked up among all the messages. + */ + @Test + public void failureOfATaskIsReportedBetweenTheNoticesOfTheScheduler() throws Exception + { + final String taskDN = "ds-task-id=" + UUID.randomUUID() + ",cn=Scheduled Tasks,cn=Tasks"; + // Exporting an unknown backend fails the task without touching any data. + final Entry taskEntry = TestCaseUtils.makeEntry( + "dn: " + taskDN, + "objectclass: top", + "objectclass: ds-task", + "objectclass: ds-task-export", + "ds-task-class-name: org.opends.server.tasks.ExportTask", + "ds-task-export-backend-id: no-such-backend", + "ds-task-export-ldif-file: " + TestCaseUtils.createTempFile()); + testTask(taskEntry, TaskState.STOPPED_BY_ERROR, 60); + + final List logMsgs = getLogMessages(DN.valueOf(taskDN)); + assertThat(logMsgs.size()).isGreaterThanOrEqualTo(3); + assertThat(logMsgs.get(0)).contains(msgIdField(NOTE_TASK_STARTED.resourceName(), NOTE_TASK_STARTED.ordinal())); + assertThat(logMsgs.get(logMsgs.size() - 1)) + .contains(msgIdField(NOTE_TASK_FINISHED.resourceName(), NOTE_TASK_FINISHED.ordinal())); + + final String failure = InstallerHelper.getRelevantLogMessage(logMsgs); + assertThat(failure).contains("severity=\"ERROR\"") + .contains(msgIdField(ERR_LDIFEXPORT_NO_BACKENDS_FOR_ID.resourceName(), + ERR_LDIFEXPORT_NO_BACKENDS_FOR_ID.ordinal())); + assertThat(logMsgs.indexOf(failure)).isGreaterThan(0).isLessThan(logMsgs.size() - 1); + assertThat(new InstallerHelper().isPeersNotFoundError(logMsgs)).isFalse(); + } + + /** + * The id a message is logged with, in the form rendered by + * {@code org.opends.server.backends.task.Task#addLogMessage}. This is what the classification of + * a task failure matches on, so the two must agree. + */ + private String msgIdField(String resourceName, int ordinal) + { + return "msgID=" + resourceName + "-" + ordinal; + } + + private List getLogMessages(DN taskDN) + { + final InternalSearchOperation searchOperation = + getRootConnection().processSearch(newSearchRequest(taskDN, SearchScope.BASE_OBJECT)); + final Entry taskEntry = searchOperation.getSearchEntries().getFirst(); + + final List logMsgs = new ArrayList<>(); + for (Attribute attribute : taskEntry.getAllAttributes(ATTR_TASK_LOG_MESSAGES)) + { + for (ByteString value : attribute) + { + logMsgs.add(value.toString()); + } + } + return logMsgs; + } +}