| | |
| | | import org.forgerock.opendj.ldap.controls.PersistentSearchRequestControl; |
| | | import org.forgerock.opendj.ldap.requests.Requests; |
| | | import org.forgerock.opendj.ldap.requests.SearchRequest; |
| | | import org.forgerock.opendj.ldap.responses.Result; |
| | | import org.forgerock.opendj.ldap.responses.SearchResultEntry; |
| | | import org.forgerock.opendj.ldap.responses.SearchResultReference; |
| | | import org.forgerock.opendj.ldif.ConnectionEntryReader; |
| | |
| | | import org.opends.server.api.LocalBackend; |
| | | import org.opends.server.backends.MemoryBackend; |
| | | import org.opends.server.core.DirectoryServer; |
| | | import org.opends.server.core.PersistentSearch; |
| | | import org.opends.server.protocols.internal.InternalClientConnection; |
| | | import org.opends.server.protocols.internal.InternalSearchOperation; |
| | | import org.opends.server.types.AcceptRejectWarn; |
| | | import org.opends.server.types.Entry; |
| | | import org.testng.annotations.AfterClass; |
| | |
| | | psearch.searchAsync(request, new SearchResultHandler() { |
| | | @Override |
| | | public boolean handleEntry(SearchResultEntry entry) { |
| | | notified.add(entry.getName().toString()); |
| | | // Every notification carries the same DN, so the DN alone cannot tell a lost |
| | | // notification from a duplicated one: record which change is being reported. |
| | | notified.add(entry.getName() + " " + entry.parseAttribute("description").asString()); |
| | | return true; |
| | | } |
| | | |
| | |
| | | |
| | | // searchAsync returns before the server has registered the persistent search, so wait |
| | | // until the backend reports it; otherwise the first modification below can be notified |
| | | // before the search is listening and be missed. |
| | | // before the search is listening and be missed. A failed test is rerun in the same JVM |
| | | // (rerunFailingTestsCount), which can leave the persistent search of the previous run |
| | | // behind, hence the wait for a persistent search on our own base DN. |
| | | final LocalBackend<?> backend = TestCaseUtils.getServerContext() |
| | | .getBackendConfigManager().getLocalBackendById(TestCaseUtils.TEST_BACKEND_ID); |
| | | for (int i = 0; backend.getPersistentSearches().isEmpty() && i < 500; i++) { |
| | | for (int i = 0; !isPersistentSearchRegistered(backend, "ou=psearch,o=test") && i < 500; i++) { |
| | | Thread.sleep(10); |
| | | } |
| | | assertThat(backend.getPersistentSearches()).isNotEmpty(); |
| | | assertThat(isPersistentSearchRegistered(backend, "ou=psearch,o=test")) |
| | | .as("the persistent search was never registered with backend %s", backend.getBackendID()) |
| | | .isTrue(); |
| | | |
| | | // The same entry is modified repeatedly: each change must reach the persistent search. |
| | | for (int i = 1; i <= 3; i++) { |
| | | connection.modify(Requests.newModifyRequest("cn=changing,ou=psearch,o=test") |
| | | .addModification(ModificationType.REPLACE, "description", "change " + i)); |
| | | assertThat(notified.poll(30, TimeUnit.SECONDS)) |
| | | .as("notification for change " + i) |
| | | .isEqualTo("cn=changing,ou=psearch,o=test"); |
| | | .as("notification for change %d, persistent search still registered: %s, " |
| | | + "notifications received afterwards: %s", |
| | | i, isPersistentSearchRegistered(backend, "ou=psearch,o=test"), notified) |
| | | .isEqualTo("cn=changing,ou=psearch,o=test change " + i); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // A persistent search notification is not a search result: it must be reported whether or not |
| | | // the entry was returned before, and it is not bound by the size and time limits of the search. |
| | | // In a real persistent search the search phase is only open for a few instructions after the |
| | | // search is registered with the backend, so the notification path is driven directly here. |
| | | @Test |
| | | public void test_persistent_search_notification_ignores_search_phase_state() throws Exception { |
| | | TestCaseUtils.addEntries( |
| | | "dn: ou=psearch-notify,o=test", |
| | | "objectClass: top", |
| | | "objectClass: organizationalUnit", |
| | | "ou: psearch-notify", |
| | | "" |
| | | ); |
| | | final Entry entry = DirectoryServer.getEntry(DN.valueOf("ou=psearch-notify,o=test")); |
| | | |
| | | final InternalSearchOperation search = new InternalSearchOperation( |
| | | InternalClientConnection.getRootConnection(), |
| | | InternalClientConnection.nextOperationID(), |
| | | InternalClientConnection.nextMessageID(), |
| | | org.opends.server.protocols.internal.Requests |
| | | .newSearchRequest(DN.valueOf("o=test"), SearchScope.WHOLE_SUBTREE) |
| | | .setDereferenceAliasesPolicy(DereferenceAliasesPolicy.ALWAYS)); |
| | | |
| | | // The search phase returns the entry once, and drops it when it reaches it a second time |
| | | // through an alias ... |
| | | assertThat(search.returnEntry(entry, null)).isTrue(); |
| | | assertThat(search.returnEntry(entry, null)).isTrue(); |
| | | assertThat(search.getSearchEntries()).hasSize(1); |
| | | |
| | | // ... but a change reported to a persistent search is never a duplicate, whether the search |
| | | // phase is still open (the entry was just returned by it) or already over. |
| | | assertThat(search.returnPersistentSearchEntry(entry, null)).isTrue(); |
| | | search.endSearchPhase(); |
| | | assertThat(search.returnPersistentSearchEntry(entry, null)).isTrue(); |
| | | assertThat(search.getSearchEntries()).hasSize(3); |
| | | |
| | | // The size limit of the search does not bound a notification: it only bounds the search |
| | | // phase, and is lifted for the rest of a persistent search once that phase is over. |
| | | search.setSizeLimit(1); |
| | | assertThat(search.returnPersistentSearchEntry(entry, null)).isTrue(); |
| | | assertThat(search.getSearchEntries()).hasSize(4); |
| | | // The search phase itself is still bound by it. |
| | | assertThat(search.returnEntry(entry, null)).isFalse(); |
| | | assertThat(search.getResultCode()).isEqualTo(ResultCode.SIZE_LIMIT_EXCEEDED); |
| | | |
| | | // Same for the time limit, checked on its own: with a size limit left in the way the search |
| | | // phase would stop on that one and the time limit would never be reached. |
| | | search.setSizeLimit(0); |
| | | search.setTimeLimit(1); |
| | | search.setTimeLimitExpiration(0); |
| | | assertThat(search.returnPersistentSearchEntry(entry, null)).isTrue(); |
| | | assertThat(search.getSearchEntries()).hasSize(5); |
| | | assertThat(search.returnEntry(entry, null)).isFalse(); |
| | | assertThat(search.getResultCode()).isEqualTo(ResultCode.TIME_LIMIT_EXCEEDED); |
| | | } |
| | | |
| | | // A persistent search which is cancelled because its backend goes away, as happens when the |
| | | // backend is disabled or re-initialized, must be told so: without a search result done the |
| | | // client waits forever for changes on a search which no longer exists. |
| | | @Test |
| | | public void test_persistent_search_is_told_when_its_backend_goes_away() throws Exception { |
| | | final String backendID = "psearchUnavailable"; |
| | | final String baseDN = "o=psearch-unavailable"; |
| | | TestCaseUtils.initializeMemoryBackend(backendID, baseDN, true); |
| | | final MemoryBackend backend = (MemoryBackend) TestCaseUtils.getServerContext() |
| | | .getBackendConfigManager().getLocalBackendById(backendID); |
| | | |
| | | final SearchRequest request = |
| | | Requests.newSearchRequest(baseDN, SearchScope.WHOLE_SUBTREE, "(objectclass=*)") |
| | | .addControl(PersistentSearchRequestControl.newControl( |
| | | true, true, false, PersistentSearchChangeType.MODIFY)); |
| | | |
| | | final LDAPConnectionFactory factory = |
| | | new LDAPConnectionFactory("localhost", TestCaseUtils.getServerLdapPort()); |
| | | try (Connection psearch = factory.getConnection()) { |
| | | psearch.bind("cn=Directory Manager", "password".toCharArray()); |
| | | final LdapPromise<Result> searchDone = psearch.searchAsync(request, new SearchResultHandler() { |
| | | @Override |
| | | public boolean handleEntry(SearchResultEntry entry) { |
| | | return true; |
| | | } |
| | | |
| | | @Override |
| | | public boolean handleReference(SearchResultReference reference) { |
| | | return true; |
| | | } |
| | | }); |
| | | |
| | | for (int i = 0; !isPersistentSearchRegistered(backend, baseDN) && i < 500; i++) { |
| | | Thread.sleep(10); |
| | | } |
| | | assertThat(isPersistentSearchRegistered(backend, baseDN)) |
| | | .as("the persistent search was never registered with backend %s", backendID) |
| | | .isTrue(); |
| | | |
| | | backend.finalizeBackend(); |
| | | |
| | | try { |
| | | final Result result = searchDone.getOrThrow(30, TimeUnit.SECONDS); |
| | | fail("the persistent search should have been terminated, it returned " + result); |
| | | } catch (LdapException e) { |
| | | assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.UNAVAILABLE); |
| | | assertThat(e.getResult().getDiagnosticMessage()).contains(backendID); |
| | | } |
| | | } finally { |
| | | TestCaseUtils.getServerContext().getBackendConfigManager().deregisterLocalBackend(backend); |
| | | } |
| | | } |
| | | |
| | | /** Whether the provided backend has a persistent search registered for the provided base DN. */ |
| | | private static boolean isPersistentSearchRegistered(LocalBackend<?> backend, String baseDN) { |
| | | for (PersistentSearch psearch : backend.getPersistentSearches()) { |
| | | if (psearch.getSearchOperation().getBaseDN().equals(DN.valueOf(baseDN))) { |
| | | return true; |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | // An alias is dereferenced before its target is reached on its own: the target must still be |
| | | // returned, and exactly once. The original regression was order-sensitive, dropping the target |
| | | // when the alias reached it first, so this pins the alias-before-target order specifically. The |