/** * Copyright 2016 Yahoo Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.yahoo.pulsar.broker.namespace; import static com.yahoo.pulsar.broker.cache.LocalZooKeeperCacheService.LOCAL_POLICIES_ROOT; import static com.yahoo.pulsar.broker.web.PulsarWebResource.joinPath; import static org.mockito.Matchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertNull; import static org.testng.Assert.assertTrue; import static org.testng.Assert.fail; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.net.URI; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.CompletableFuture; import org.apache.bookkeeper.mledger.ManagedLedger; import org.apache.bookkeeper.util.ZkUtils; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.tuple.Pair; import org.apache.zookeeper.CreateMode; import org.apache.zookeeper.KeeperException; import org.apache.zookeeper.ZooDefs; import org.apache.zookeeper.data.Stat; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testng.Assert; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import com.fasterxml.jackson.core.JsonProcessingException; import com.github.benmanes.caffeine.cache.AsyncLoadingCache; import com.google.common.collect.Lists; import com.google.common.hash.Hashing; import com.yahoo.pulsar.broker.LocalBrokerData; import com.yahoo.pulsar.broker.PulsarServerException; import com.yahoo.pulsar.broker.PulsarService; import com.yahoo.pulsar.broker.loadbalance.LoadManager; import com.yahoo.pulsar.broker.loadbalance.ModularLoadManager; import com.yahoo.pulsar.broker.loadbalance.impl.ModularLoadManagerImpl; import com.yahoo.pulsar.broker.loadbalance.impl.ModularLoadManagerWrapper; import com.yahoo.pulsar.broker.lookup.LookupResult; import com.yahoo.pulsar.broker.service.BrokerTestBase; import com.yahoo.pulsar.broker.service.Topic; import com.yahoo.pulsar.broker.service.persistent.PersistentTopic; import com.yahoo.pulsar.client.api.Consumer; import com.yahoo.pulsar.client.api.ConsumerConfiguration; import com.yahoo.pulsar.common.naming.DestinationName; import com.yahoo.pulsar.common.naming.NamespaceBundle; import com.yahoo.pulsar.common.naming.NamespaceBundleFactory; import com.yahoo.pulsar.common.naming.NamespaceBundles; import com.yahoo.pulsar.common.naming.NamespaceName; import com.yahoo.pulsar.common.naming.ServiceUnitId; import com.yahoo.pulsar.common.policies.data.Policies; import com.yahoo.pulsar.common.policies.data.loadbalancer.LoadReport; import com.yahoo.pulsar.common.policies.data.loadbalancer.ServiceLookupData; import com.yahoo.pulsar.common.util.ObjectMapperFactory; import com.yahoo.pulsar.common.util.collections.ConcurrentOpenHashMap; import com.yahoo.pulsar.zookeeper.ZooKeeperCache.Deserializer; public class NamespaceServiceTest extends BrokerTestBase { @BeforeMethod @Override protected void setup() throws Exception { super.baseSetup(); } @AfterMethod @Override protected void cleanup() throws Exception { super.internalCleanup(); } @Test public void testSplitAndOwnBundles() throws Exception { OwnershipCache MockOwnershipCache = spy(pulsar.getNamespaceService().getOwnershipCache()); doNothing().when(MockOwnershipCache).disableOwnership(any(NamespaceBundle.class)); Field ownership = NamespaceService.class.getDeclaredField("ownershipCache"); ownership.setAccessible(true); ownership.set(pulsar.getNamespaceService(), MockOwnershipCache); NamespaceService namespaceService = pulsar.getNamespaceService(); NamespaceName nsname = new NamespaceName("pulsar/global/ns1"); DestinationName dn = DestinationName.get("persistent://pulsar/global/ns1/topic-1"); NamespaceBundles bundles = namespaceService.getNamespaceBundleFactory().getBundles(nsname); NamespaceBundle originalBundle = bundles.findBundle(dn); // Split bundle and take ownership of split bundles CompletableFuture<Void> result = namespaceService.splitAndOwnBundle(originalBundle); try { result.get(); } catch (Exception e) { // make sure: no failure fail("split bundle faild", e); } NamespaceBundleFactory bundleFactory = this.pulsar.getNamespaceService().getNamespaceBundleFactory(); NamespaceBundles updatedNsBundles = bundleFactory.getBundles(nsname); // new updated bundles shouldn't be null assertNotNull(updatedNsBundles); List<NamespaceBundle> bundleList = updatedNsBundles.getBundles(); assertNotNull(bundles); NamespaceBundleFactory utilityFactory = NamespaceBundleFactory.createFactory(Hashing.crc32()); // (1) validate bundleFactory-cache has newly split bundles and removed old parent bundle Pair<NamespaceBundles, List<NamespaceBundle>> splitBundles = splitBundles(utilityFactory, nsname, bundles, originalBundle); assertNotNull(splitBundles); Set<NamespaceBundle> splitBundleSet = new HashSet<>(splitBundles.getRight()); splitBundleSet.removeAll(bundleList); assertTrue(splitBundleSet.isEmpty()); // (2) validate LocalZookeeper policies updated with newly created split // bundles String path = joinPath(LOCAL_POLICIES_ROOT, nsname.toString()); byte[] content = this.pulsar.getLocalZkCache().getZooKeeper().getData(path, null, new Stat()); Policies policies = ObjectMapperFactory.getThreadLocal().readValue(content, Policies.class); NamespaceBundles localZkBundles = bundleFactory.getBundles(nsname, policies.bundles); assertTrue(updatedNsBundles.equals(localZkBundles)); log.info("Policies: {}", policies); // (3) validate ownership of new split bundles by local owner bundleList.stream().forEach(b -> { try { byte[] data = this.pulsar.getLocalZkCache().getZooKeeper().getData(ServiceUnitZkUtils.path(b), null, new Stat()); NamespaceEphemeralData node = ObjectMapperFactory.getThreadLocal().readValue(data, NamespaceEphemeralData.class); Assert.assertEquals(node.getNativeUrl(), this.pulsar.getBrokerServiceUrl()); } catch (Exception e) { fail("failed to setup ownership", e); } }); } @Test public void testSplitMapWithRefreshedStatMap() throws Exception { OwnershipCache MockOwnershipCache = spy(pulsar.getNamespaceService().getOwnershipCache()); ManagedLedger ledger = mock(ManagedLedger.class); when(ledger.getCursors()).thenReturn(Lists.newArrayList()); doNothing().when(MockOwnershipCache).disableOwnership(any(NamespaceBundle.class)); Field ownership = NamespaceService.class.getDeclaredField("ownershipCache"); ownership.setAccessible(true); ownership.set(pulsar.getNamespaceService(), MockOwnershipCache); NamespaceService namespaceService = pulsar.getNamespaceService(); NamespaceName nsname = new NamespaceName("pulsar/global/ns1"); DestinationName dn = DestinationName.get("persistent://pulsar/global/ns1/topic-1"); NamespaceBundles bundles = namespaceService.getNamespaceBundleFactory().getBundles(nsname); NamespaceBundle originalBundle = bundles.findBundle(dn); PersistentTopic topic = new PersistentTopic(dn.toString(), ledger, pulsar.getBrokerService()); Method method = pulsar.getBrokerService().getClass().getDeclaredMethod("addTopicToStatsMaps", DestinationName.class, PersistentTopic.class); method.setAccessible(true); method.invoke(pulsar.getBrokerService(), dn, topic); String nspace = originalBundle.getNamespaceObject().toString(); List<PersistentTopic> list = this.pulsar.getBrokerService().getAllTopicsFromNamespaceBundle(nspace, originalBundle.toString()); assertNotNull(list); // Split bundle and take ownership of split bundles CompletableFuture<Void> result = namespaceService.splitAndOwnBundle(originalBundle); try { result.get(); } catch (Exception e) { // make sure: no failure fail("split bundle faild", e); } try { // old bundle should be removed from status-map list = this.pulsar.getBrokerService().getAllTopicsFromNamespaceBundle(nspace, originalBundle.toString()); fail(); } catch (NullPointerException ne) { // OK } // status-map should be updated with new split bundles NamespaceBundle splitBundle = pulsar.getNamespaceService().getBundle(dn); assertTrue(!CollectionUtils.isEmpty( this.pulsar.getBrokerService().getAllTopicsFromNamespaceBundle(nspace, splitBundle.toString()))); } @Test public void testIsServiceUnitDisabled() throws Exception { OwnershipCache MockOwnershipCache = spy(pulsar.getNamespaceService().getOwnershipCache()); ManagedLedger ledger = mock(ManagedLedger.class); when(ledger.getCursors()).thenReturn(Lists.newArrayList()); doNothing().when(MockOwnershipCache).disableOwnership(any(NamespaceBundle.class)); Field ownership = NamespaceService.class.getDeclaredField("ownershipCache"); ownership.setAccessible(true); ownership.set(pulsar.getNamespaceService(), MockOwnershipCache); NamespaceService namespaceService = pulsar.getNamespaceService(); NamespaceName nsname = new NamespaceName("pulsar/global/ns1"); DestinationName dn = DestinationName.get("persistent://pulsar/global/ns1/topic-1"); NamespaceBundles bundles = namespaceService.getNamespaceBundleFactory().getBundles(nsname); NamespaceBundle originalBundle = bundles.findBundle(dn); assertFalse(namespaceService.isNamespaceBundleDisabled(originalBundle)); } @Test public void testremoveOwnershipNamespaceBundle() throws Exception { OwnershipCache ownershipCache = spy(pulsar.getNamespaceService().getOwnershipCache()); ManagedLedger ledger = mock(ManagedLedger.class); when(ledger.getCursors()).thenReturn(Lists.newArrayList()); doNothing().when(ownershipCache).disableOwnership(any(NamespaceBundle.class)); Field ownership = NamespaceService.class.getDeclaredField("ownershipCache"); ownership.setAccessible(true); ownership.set(pulsar.getNamespaceService(), ownershipCache); NamespaceService namespaceService = pulsar.getNamespaceService(); NamespaceName nsname = new NamespaceName("prop/use/ns1"); NamespaceBundles bundles = namespaceService.getNamespaceBundleFactory().getBundles(nsname); NamespaceBundle bundle = bundles.getBundles().get(0); assertNotNull(ownershipCache.tryAcquiringOwnership(bundle)); assertNotNull(ownershipCache.getOwnedBundle(bundle)); ownershipCache.removeOwnership(bundles).get(); assertNull(ownershipCache.getOwnedBundle(bundle)); } @Test public void testUnloadNamespaceBundleFailure() throws Exception { final String topicName = "persistent://my-property/use/my-ns/my-topic1"; ConsumerConfiguration conf = new ConsumerConfiguration(); Consumer consumer = pulsarClient.subscribe(topicName, "my-subscriber-name", conf); ConcurrentOpenHashMap<String, CompletableFuture<Topic>> topics = pulsar.getBrokerService().getTopics(); Topic spyTopic = spy(topics.get(topicName).get()); topics.clear(); CompletableFuture<Topic> topicFuture = CompletableFuture.completedFuture(spyTopic); // add mock topic topics.put(topicName, topicFuture); doAnswer(new Answer<CompletableFuture<Void>>() { @Override public CompletableFuture<Void> answer(InvocationOnMock invocation) throws Throwable { CompletableFuture<Void> result = new CompletableFuture<>(); result.completeExceptionally(new RuntimeException("first time failed")); return result; } }).when(spyTopic).close(); NamespaceBundle bundle = pulsar.getNamespaceService().getBundle(DestinationName.get(topicName)); try { pulsar.getNamespaceService().unloadNamespaceBundle(bundle); } catch (Exception e) { // fail fail(e.getMessage()); } try { pulsar.getLocalZkCache().getZooKeeper().getData(ServiceUnitZkUtils.path(bundle), null, null); fail("it should fail as node is not present"); } catch (org.apache.zookeeper.KeeperException.NoNodeException e) { // ok } } /** * <pre> * It verifies that namespace service deserialize the load-report based on load-manager which active. * 1. write candidate1- load report using {@link LoadReport} which is used by SimpleLoadManagerImpl * 2. Write candidate2- load report using {@link LocalBrokerData} which is used by ModularLoadManagerImpl * 3. try to get Lookup Result based on active load-manager * </pre> * @throws Exception */ @Test public void testLoadReportDeserialize() throws Exception { final String candidateBroker1 = "http://localhost:8000"; final String candidateBroker2 = "http://localhost:3000"; LoadReport lr = new LoadReport(null, null, candidateBroker1, null); LocalBrokerData ld = new LocalBrokerData(null, null, candidateBroker2, null); URI uri1 = new URI(candidateBroker1); URI uri2 = new URI(candidateBroker2); String path1 = String.format("%s/%s:%s", LoadManager.LOADBALANCE_BROKERS_ROOT, uri1.getHost(), uri1.getPort()); String path2 = String.format("%s/%s:%s", LoadManager.LOADBALANCE_BROKERS_ROOT, uri2.getHost(), uri2.getPort()); ZkUtils.createFullPathOptimistic(pulsar.getZkClient(), path1, ObjectMapperFactory.getThreadLocal().writeValueAsBytes(lr), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL); ZkUtils.createFullPathOptimistic(pulsar.getZkClient(), path2, ObjectMapperFactory.getThreadLocal().writeValueAsBytes(ld), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL); LookupResult result1 = pulsar.getNamespaceService().createLookupResult(candidateBroker1).get(); // update to new load mananger pulsar.getLoadManager().set(new ModularLoadManagerWrapper(new ModularLoadManagerImpl())); LookupResult result2 = pulsar.getNamespaceService().createLookupResult(candidateBroker2).get(); Assert.assertEquals(result1.getLookupData().getBrokerUrl(), candidateBroker1); Assert.assertEquals(result2.getLookupData().getBrokerUrl(), candidateBroker2); System.out.println(result2); } @SuppressWarnings("unchecked") private Pair<NamespaceBundles, List<NamespaceBundle>> splitBundles(NamespaceBundleFactory utilityFactory, NamespaceName nsname, NamespaceBundles bundles, NamespaceBundle targetBundle) throws Exception { Field bCacheField = NamespaceBundleFactory.class.getDeclaredField("bundlesCache"); bCacheField.setAccessible(true); ((AsyncLoadingCache<NamespaceName, NamespaceBundles>) bCacheField.get(utilityFactory)).put(nsname, CompletableFuture.completedFuture(bundles)); return utilityFactory.splitBundles(targetBundle, 2); } private static final Logger log = LoggerFactory.getLogger(NamespaceServiceTest.class); }