/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.apache.flume.sink; import junit.framework.Assert; import org.apache.flume.Channel; import org.apache.flume.ChannelException; import org.apache.flume.Context; import org.apache.flume.Event; import org.apache.flume.EventDeliveryException; import org.apache.flume.Sink; import org.apache.flume.Sink.Status; import org.apache.flume.Transaction; import org.apache.flume.channel.AbstractChannel; import org.junit.Test; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; public class TestLoadBalancingSinkProcessor { private Context getContext(String selectorType, boolean backoff) { Map<String, String> p = new HashMap<String, String>(); p.put("selector", selectorType); p.put("backoff", String.valueOf(backoff)); Context ctx = new Context(p); return ctx; } private Context getContext(String selectorType) { Map<String, String> p = new HashMap<String, String>(); p.put("selector", selectorType); Context ctx = new Context(p); return ctx; } private LoadBalancingSinkProcessor getProcessor( String selectorType, List<Sink> sinks, boolean backoff) { return getProcessor(sinks, getContext(selectorType, backoff)); } private LoadBalancingSinkProcessor getProcessor(List<Sink> sinks, Context ctx) { LoadBalancingSinkProcessor lbsp = new LoadBalancingSinkProcessor(); lbsp.setSinks(sinks); lbsp.configure(ctx); lbsp.start(); return lbsp; } @Test public void testDefaultConfiguration() throws Exception { // If no selector is specified, the round-robin selector should be used Channel ch = new MockChannel(); int n = 100; int numEvents = 3 * n; for (int i = 0; i < numEvents; i++) { ch.put(new MockEvent("test" + i)); } MockSink s1 = new MockSink(1); s1.setChannel(ch); MockSink s2 = new MockSink(2); s2.setChannel(ch); MockSink s3 = new MockSink(3); s3.setChannel(ch); List<Sink> sinks = new ArrayList<Sink>(); sinks.add(s1); sinks.add(s2); sinks.add(s3); LoadBalancingSinkProcessor lbsp = getProcessor(sinks, new Context()); Status s = Status.READY; while (s != Status.BACKOFF) { s = lbsp.process(); } Assert.assertTrue(s1.getEvents().size() == n); Assert.assertTrue(s2.getEvents().size() == n); Assert.assertTrue(s3.getEvents().size() == n); } @Test public void testRandomOneActiveSink() throws Exception { Channel ch = new MockChannel(); int n = 10; int numEvents = n; for (int i = 0; i < numEvents; i++) { ch.put(new MockEvent("test" + i)); } MockSink s1 = new MockSink(1); s1.setChannel(ch); // s1 always fails s1.setFail(true); MockSink s2 = new MockSink(2); s2.setChannel(ch); MockSink s3 = new MockSink(3); s3.setChannel(ch); // s3 always fails s3.setFail(true); List<Sink> sinks = new ArrayList<Sink>(); sinks.add(s1); sinks.add(s2); sinks.add(s3); LoadBalancingSinkProcessor lbsp = getProcessor("random", sinks, false); Sink.Status s = Sink.Status.READY; while (s != Sink.Status.BACKOFF) { s = lbsp.process(); } Assert.assertTrue(s1.getEvents().size() == 0); Assert.assertTrue(s2.getEvents().size() == n); Assert.assertTrue(s3.getEvents().size() == 0); } @Test public void testRandomBackoff() throws Exception { Channel ch = new MockChannel(); int n = 100; int numEvents = n; for (int i = 0; i < numEvents; i++) { ch.put(new MockEvent("test" + i)); } MockSink s1 = new MockSink(1); s1.setChannel(ch); // s1 always fails s1.setFail(true); MockSink s2 = new MockSink(2); s2.setChannel(ch); MockSink s3 = new MockSink(3); s3.setChannel(ch); // s3 always fails s3.setFail(true); List<Sink> sinks = new ArrayList<Sink>(); sinks.add(s1); sinks.add(s2); sinks.add(s3); LoadBalancingSinkProcessor lbsp = getProcessor("random", sinks, true); // TODO: there is a remote possibility that s0 or s2 // never get hit by the random assignment // and thus not backoffed, causing the test to fail for (int i = 0; i < 50; i++) { // a well behaved runner would always check the return. lbsp.process(); } Assert.assertEquals(50, s2.getEvents().size()); s2.setFail(true); s1.setFail(false); // s1 should still be backed off try { lbsp.process(); // nothing should be able to process right now Assert.fail("Expected EventDeliveryException"); } catch (EventDeliveryException e) { // this is expected } Thread.sleep(2100); // wait for s1 to no longer be backed off Sink.Status s = Sink.Status.READY; while (s != Sink.Status.BACKOFF) { s = lbsp.process(); } Assert.assertEquals(50, s1.getEvents().size()); Assert.assertEquals(50, s2.getEvents().size()); Assert.assertEquals(0, s3.getEvents().size()); } @Test public void testRandomPersistentFailure() throws Exception { Channel ch = new MockChannel(); int n = 100; int numEvents = 3 * n; for (int i = 0; i < numEvents; i++) { ch.put(new MockEvent("test" + i)); } MockSink s1 = new MockSink(1); s1.setChannel(ch); MockSink s2 = new MockSink(2); s2.setChannel(ch); // s2 always fails s2.setFail(true); MockSink s3 = new MockSink(3); s3.setChannel(ch); List<Sink> sinks = new ArrayList<Sink>(); sinks.add(s1); sinks.add(s2); sinks.add(s3); LoadBalancingSinkProcessor lbsp = getProcessor("random",sinks, false); Status s = Status.READY; while (s != Status.BACKOFF) { s = lbsp.process(); } Assert.assertTrue(s2.getEvents().size() == 0); Assert.assertTrue(s1.getEvents().size() + s3.getEvents().size() == 3 * n); } @Test public void testRandomNoFailure() throws Exception { Channel ch = new MockChannel(); int n = 10000; int numEvents = n; for (int i = 0; i < numEvents; i++) { ch.put(new MockEvent("test" + i)); } MockSink s1 = new MockSink(1); s1.setChannel(ch); MockSink s2 = new MockSink(2); s2.setChannel(ch); MockSink s3 = new MockSink(3); s3.setChannel(ch); MockSink s4 = new MockSink(4); s4.setChannel(ch); MockSink s5 = new MockSink(5); s5.setChannel(ch); MockSink s6 = new MockSink(6); s6.setChannel(ch); MockSink s7 = new MockSink(7); s7.setChannel(ch); MockSink s8 = new MockSink(8); s8.setChannel(ch); MockSink s9 = new MockSink(9); s9.setChannel(ch); MockSink s0 = new MockSink(0); s0.setChannel(ch); List<Sink> sinks = new ArrayList<Sink>(); sinks.add(s1); sinks.add(s2); sinks.add(s3); sinks.add(s4); sinks.add(s5); sinks.add(s6); sinks.add(s7); sinks.add(s8); sinks.add(s9); sinks.add(s0); LoadBalancingSinkProcessor lbsp = getProcessor("random",sinks, false); Status s = Status.READY; while (s != Status.BACKOFF) { s = lbsp.process(); } Set<Integer> sizeSet = new HashSet<Integer>(); int sum = 0; for (Sink ms : sinks) { int count = ((MockSink) ms).getEvents().size(); sum += count; sizeSet.add(count); } // Assert that all the events were accounted for Assert.assertEquals(n, sum); // Assert that at least two sinks came with different event sizes. // This makes sense if the total number of events is evenly divisible by // the total number of sinks. In which case the round-robin policy will // end up causing all sinks to get the same number of events where as // the random policy will have very low probability of doing that. Assert.assertTrue("Miraculous distribution", sizeSet.size() > 1); } @Test public void testRoundRobinOneActiveSink() throws Exception { Channel ch = new MockChannel(); int n = 10; int numEvents = n; for (int i = 0; i < numEvents; i++) { ch.put(new MockEvent("test" + i)); } MockSink s1 = new MockSink(1); s1.setChannel(ch); // s1 always fails s1.setFail(true); MockSink s2 = new MockSink(2); s2.setChannel(ch); MockSink s3 = new MockSink(3); s3.setChannel(ch); // s3 always fails s3.setFail(true); List<Sink> sinks = new ArrayList<Sink>(); sinks.add(s1); sinks.add(s2); sinks.add(s3); LoadBalancingSinkProcessor lbsp = getProcessor("round_robin", sinks, false); Sink.Status s = Sink.Status.READY; while (s != Sink.Status.BACKOFF) { s = lbsp.process(); } Assert.assertTrue(s1.getEvents().size() == 0); Assert.assertTrue(s2.getEvents().size() == n); Assert.assertTrue(s3.getEvents().size() == 0); } @Test public void testRoundRobinPersistentFailure() throws Exception { Channel ch = new MockChannel(); int n = 100; int numEvents = 3 * n; for (int i = 0; i < numEvents; i++) { ch.put(new MockEvent("test" + i)); } MockSink s1 = new MockSink(1); s1.setChannel(ch); MockSink s2 = new MockSink(2); s2.setChannel(ch); // s2 always fails s2.setFail(true); MockSink s3 = new MockSink(3); s3.setChannel(ch); List<Sink> sinks = new ArrayList<Sink>(); sinks.add(s1); sinks.add(s2); sinks.add(s3); LoadBalancingSinkProcessor lbsp = getProcessor("round_robin",sinks, false); Status s = Status.READY; while (s != Status.BACKOFF) { s = lbsp.process(); } Assert.assertTrue(s1.getEvents().size() == n); Assert.assertTrue(s2.getEvents().size() == 0); Assert.assertTrue(s3.getEvents().size() == 2 * n); } // test that even if the sink recovers immediately that it is kept out of commission briefly // test also verifies that when a sink fails, events are balanced over remaining sinks @Test public void testRoundRobinBackoffInitialFailure() throws EventDeliveryException { Channel ch = new MockChannel(); int n = 100; int numEvents = 3 * n; for (int i = 0; i < numEvents; i++) { ch.put(new MockEvent("test" + i)); } MockSink s1 = new MockSink(1); s1.setChannel(ch); MockSink s2 = new MockSink(2); s2.setChannel(ch); MockSink s3 = new MockSink(3); s3.setChannel(ch); List<Sink> sinks = new ArrayList<Sink>(); sinks.add(s1); sinks.add(s2); sinks.add(s3); LoadBalancingSinkProcessor lbsp = getProcessor("round_robin",sinks, true); Status s = Status.READY; for (int i = 0; i < 3 && s != Status.BACKOFF; i++) { s = lbsp.process(); } s2.setFail(true); for (int i = 0; i < 3 && s != Status.BACKOFF; i++) { s = lbsp.process(); } s2.setFail(false); while (s != Status.BACKOFF) { s = lbsp.process(); } Assert.assertEquals((3 * n) / 2, s1.getEvents().size()); Assert.assertEquals(1, s2.getEvents().size()); Assert.assertEquals((3 * n) / 2 - 1, s3.getEvents().size()); } @Test public void testRoundRobinBackoffIncreasingBackoffs() throws EventDeliveryException, InterruptedException { Channel ch = new MockChannel(); int n = 100; int numEvents = 3 * n; for (int i = 0; i < numEvents; i++) { ch.put(new MockEvent("test" + i)); } MockSink s1 = new MockSink(1); s1.setChannel(ch); MockSink s2 = new MockSink(2); s2.setChannel(ch); s2.setFail(true); MockSink s3 = new MockSink(3); s3.setChannel(ch); List<Sink> sinks = new ArrayList<Sink>(); sinks.add(s1); sinks.add(s2); sinks.add(s3); LoadBalancingSinkProcessor lbsp = getProcessor("round_robin",sinks, true); Status s = Status.READY; for (int i = 0; i < 3 && s != Status.BACKOFF; i++) { s = lbsp.process(); } Assert.assertEquals(0, s2.getEvents().size()); Thread.sleep(2100); // this should let the sink come out of backoff and get backed off for a longer time for (int i = 0; i < 3 && s != Status.BACKOFF; i++) { s = lbsp.process(); } Assert.assertEquals(0, s2.getEvents().size()); s2.setFail(false); Thread.sleep(2100); // this time it shouldn't come out of backoff yet as the timeout isn't over for (int i = 0; i < 3 && s != Status.BACKOFF; i++) { s = lbsp.process(); } Assert.assertEquals(0, s2.getEvents().size()); // after this s2 should be receiving events agains Thread.sleep(2100); while (s != Status.BACKOFF) { s = lbsp.process(); } Assert.assertEquals( n + 2, s1.getEvents().size()); Assert.assertEquals( n - 3, s2.getEvents().size()); Assert.assertEquals( n + 1, s3.getEvents().size()); } @Test public void testRoundRobinBackoffFailureRecovery() throws EventDeliveryException, InterruptedException { Channel ch = new MockChannel(); int n = 100; int numEvents = 3 * n; for (int i = 0; i < numEvents; i++) { ch.put(new MockEvent("test" + i)); } MockSink s1 = new MockSink(1); s1.setChannel(ch); MockSink s2 = new MockSink(2); s2.setChannel(ch); s2.setFail(true); MockSink s3 = new MockSink(3); s3.setChannel(ch); List<Sink> sinks = new ArrayList<Sink>(); sinks.add(s1); sinks.add(s2); sinks.add(s3); LoadBalancingSinkProcessor lbsp = getProcessor("round_robin",sinks, true); Status s = Status.READY; for (int i = 0; i < 3 && s != Status.BACKOFF; i++) { s = lbsp.process(); } s2.setFail(false); Thread.sleep(2001); while (s != Status.BACKOFF) { s = lbsp.process(); } Assert.assertEquals(n + 1, s1.getEvents().size()); Assert.assertEquals(n - 1, s2.getEvents().size()); Assert.assertEquals(n, s3.getEvents().size()); } @Test public void testRoundRobinNoFailure() throws Exception { Channel ch = new MockChannel(); int n = 100; int numEvents = 3 * n; for (int i = 0; i < numEvents; i++) { ch.put(new MockEvent("test" + i)); } MockSink s1 = new MockSink(1); s1.setChannel(ch); MockSink s2 = new MockSink(2); s2.setChannel(ch); MockSink s3 = new MockSink(3); s3.setChannel(ch); List<Sink> sinks = new ArrayList<Sink>(); sinks.add(s1); sinks.add(s2); sinks.add(s3); LoadBalancingSinkProcessor lbsp = getProcessor("round_robin",sinks, false); Status s = Status.READY; while (s != Status.BACKOFF) { s = lbsp.process(); } Assert.assertTrue(s1.getEvents().size() == n); Assert.assertTrue(s2.getEvents().size() == n); Assert.assertTrue(s3.getEvents().size() == n); } @Test public void testCustomSelector() throws Exception { Channel ch = new MockChannel(); int n = 10; int numEvents = n; for (int i = 0; i < numEvents; i++) { ch.put(new MockEvent("test" + i)); } MockSink s1 = new MockSink(1); s1.setChannel(ch); // s1 always fails s1.setFail(true); MockSink s2 = new MockSink(2); s2.setChannel(ch); MockSink s3 = new MockSink(3); s3.setChannel(ch); List<Sink> sinks = new ArrayList<Sink>(); sinks.add(s1); sinks.add(s2); sinks.add(s3); // This selector will result in all events going to s2 Context ctx = getContext(FixedOrderSelector.class.getCanonicalName()); ctx.put("selector." + FixedOrderSelector.SET_ME, "foo"); LoadBalancingSinkProcessor lbsp = getProcessor(sinks, ctx); Sink.Status s = Sink.Status.READY; while (s != Sink.Status.BACKOFF) { s = lbsp.process(); } Assert.assertTrue(s1.getEvents().size() == 0); Assert.assertTrue(s2.getEvents().size() == n); Assert.assertTrue(s3.getEvents().size() == 0); } private static class MockSink extends AbstractSink { private final int id; private List<Event> events = new ArrayList(); private boolean fail = false; private MockSink(int id) { this.id = id; } List<Event> getEvents() { return events; } int getId() { return id; } void setFail(boolean bFail) { fail = bFail; } @Override public Status process() throws EventDeliveryException { if (fail) { throw new EventDeliveryException("failed"); } Event e = this.getChannel().take(); if (e == null) { return Status.BACKOFF; } events.add(e); return Status.READY; } } private static class MockChannel extends AbstractChannel { private List<Event> events = new ArrayList<Event>(); @Override public void put(Event event) throws ChannelException { events.add(event); } @Override public Event take() throws ChannelException { if (events.size() > 0) { return events.remove(0); } return null; } @Override public Transaction getTransaction() { return null; } } private static class MockEvent implements Event { private static final Map<String, String> EMPTY_HEADERS = Collections.unmodifiableMap(new HashMap<String, String>()); private byte[] body; MockEvent(String str) { this.body = str.getBytes(); } @Override public Map<String, String> getHeaders() { return EMPTY_HEADERS; } @Override public void setHeaders(Map<String, String> headers) { throw new UnsupportedOperationException(); } @Override public byte[] getBody() { return body; } @Override public void setBody(byte[] body) { this.body = body; } } }