/** * Licensed to Cloudera, Inc. under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. Cloudera, Inc. 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 com.cloudera.flume.handlers.rolling; import java.io.IOException; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicLong; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.cloudera.flume.conf.Context; import com.cloudera.flume.conf.FlumeBuilder; import com.cloudera.flume.conf.FlumeSpecException; import com.cloudera.flume.conf.SinkFactory.SinkBuilder; import com.cloudera.flume.core.CompositeSink; import com.cloudera.flume.core.Event; import com.cloudera.flume.core.EventSink; import com.cloudera.flume.reporter.ReportEvent; import com.cloudera.util.Clock; import com.google.common.base.Preconditions; /** * This rolling configurations based on a trigger such as a period of time or a * certain size of file. When a roll happens, the current instance of the * subordinate configuration is closed and a new instance of the sink is opened. */ public class RollSink extends EventSink.Base { static final Logger LOG = LoggerFactory.getLogger(RollSink.class); final String fspec; EventSink curSink; final RollTrigger trigger; protected TriggerThread triggerThread = null; private static int threadInitNumber = 0; final long checkLatencyMs; // default 4x a second private Context ctx; // roll context // reporting attributes and counters public final static String A_ROLLS = "rolls"; public final static String A_ROLLFAILS = "rollfails"; public final String A_ROLLSPEC = "rollspec"; public final String A_ROLL_TAG; // TODO (jon) parameterize this. public final static String DEFAULT_ROLL_TAG = "rolltag"; final AtomicLong rolls = new AtomicLong(); final AtomicLong rollfails = new AtomicLong(); public RollSink(Context ctx, String spec, long maxAge, long checkMs) { this.ctx = ctx; A_ROLL_TAG = DEFAULT_ROLL_TAG; this.fspec = spec; this.trigger = new TimeTrigger(new ProcessTagger(), maxAge); this.checkLatencyMs = checkMs; LOG.info("Created RollSink: maxAge=" + maxAge + "ms trigger=[" + trigger + "] checkPeriodMs = " + checkLatencyMs + " spec='" + fspec + "'"); } public RollSink(Context ctx, String spec, RollTrigger trigger, long checkMs) { this.ctx = ctx; A_ROLL_TAG = DEFAULT_ROLL_TAG; this.fspec = spec; this.trigger = trigger; this.checkLatencyMs = checkMs; LOG.info("Created RollSink: trigger=[" + trigger + "] checkPeriodMs = " + checkLatencyMs + " spec='" + fspec + "'"); } private static synchronized int nextThreadNum() { return threadInitNumber++; } /** * This thread wakes up to check if a batch should be committed, no matter how * small it is. */ class TriggerThread extends Thread { final CountDownLatch doneLatch = new CountDownLatch(1); final CountDownLatch startedLatch = new CountDownLatch(1); TriggerThread() { super("Roll-TriggerThread-" + nextThreadNum()); } void doStart() { this.start(); try { startedLatch.await(); } catch (InterruptedException e) { LOG.warn("Interrupted while waiting for batch timeout thread to start"); } } public void run() { startedLatch.countDown(); while (!isInterrupted()) { // TODO there should probably be a lcok on Roll sink but until we handle // interruptions throughout the code, we cannot because this causes a // deadlock if (trigger.isTriggered()) { trigger.reset(); LOG.info("Rotate started by triggerthread... "); rotate(); LOG.info("Rotate stopped by triggerthread... "); continue; } try { Clock.sleep(checkLatencyMs); } catch (InterruptedException e) { LOG.warn("TriggerThread interrupted"); doneLatch.countDown(); return; } } LOG.info("TriggerThread shutdown"); doneLatch.countDown(); } }; protected EventSink newSink(Context ctx) throws IOException { try { // TODO (jon) add roll-specific context information. return new CompositeSink(ctx, fspec); } catch (FlumeSpecException e) { // check done prior to construction. throw new IllegalArgumentException("This should never happen:" + e.getMessage()); } }; // This is a large synchronized section. Won't fix until it becomes a problem. @Override public void append(Event e) throws IOException { Preconditions.checkState(curSink != null, "Attempted to append when rollsink not open"); if (trigger.isTriggered()) { trigger.reset(); LOG.debug("Rotate started by append... "); rotate(); LOG.debug("... rotate completed by append."); } String tag = trigger.getTagger().getTag(); e.set(A_ROLL_TAG, tag.getBytes()); synchronized (this) { curSink.append(e); trigger.append(e); super.append(e); } } synchronized public boolean rotate() { try { rolls.incrementAndGet(); if (curSink == null) { // wtf, was closed or never opened LOG.error("Attempting to rotate an already closed roller"); return false; } LOG.info("CURRENT curSink value: " + rolls.get()); LOG.info("Closing curSink"); curSink.close(); LOG.info("Closed curSink, calling newSink"); curSink = newSink(ctx); LOG.info("Called newSink, calling open"); curSink.open(); LOG.info("rotated sink "); } catch (IOException e1) { // TODO This is an error condition that needs to be handled -- could be // due to resource exhaustion. LOG.error("Failure when attempting to rotate and open new sink: " + e1.getMessage()); rollfails.incrementAndGet(); return false; } return true; } @Override public void close() throws IOException { LOG.info("closing RollSink '" + fspec + "'"); // TODO triggerThread can race with an open call, but we really need to // avoid having the await while locked! if (triggerThread != null) { try { triggerThread.interrupt(); triggerThread.doneLatch.await(); } catch (InterruptedException e) { LOG .warn("Interrupted while waiting for batch timeout thread to finish"); } } synchronized (this) { if (curSink == null) { LOG.info("double close '" + fspec + "'"); return; } curSink.close(); curSink = null; } } @Override synchronized public void open() throws IOException { Preconditions.checkState(curSink == null, "Attempting to open already open RollSink '" + fspec + "'"); LOG.info("opening RollSink '" + fspec + "'"); trigger.getTagger().newTag(); triggerThread = new TriggerThread(); triggerThread.doStart(); try { curSink = newSink(ctx); curSink.open(); } catch (IOException e1) { LOG.warn("Failure when attempting to open initial sink", e1); } } @Override public String getName() { return "Roll"; } @Override synchronized public ReportEvent getReport() { ReportEvent rpt = super.getReport(); rpt.setLongMetric(A_ROLLS, rolls.get()); rpt.setLongMetric(A_ROLLFAILS, rollfails.get()); rpt.setStringMetric(A_ROLLSPEC, fspec); return rpt; } @Override public void getReports(String namePrefix, Map<String, ReportEvent> reports) { super.getReports(namePrefix, reports); if (curSink != null) { curSink.getReports(namePrefix + getName() + ".", reports); } } /** * This is currently only used in tests. */ @Deprecated public String getCurrentTag() { return trigger.getTagger().getTag(); } /** * Builder for a spec based rolling sink. (most general version, does not * necessarily output to files!). */ public static SinkBuilder builder() { return new SinkBuilder() { @Override public EventSink build(Context ctx, String... argv) { Preconditions.checkArgument(argv.length >= 2 && argv.length <= 3, "roll(rollmillis[, checkmillis]) { sink }"); String spec = argv[0]; long rollmillis = Long.parseLong(argv[1]); long checkmillis = 250; // TODO (jon) parameterize 250 argument. if (argv.length >= 3) { checkmillis = Long.parseLong(argv[2]); } try { // check sub spec to make sure it works. FlumeBuilder.buildSink(ctx, spec); // ok it worked, instantiate the roller return new RollSink(ctx, spec, rollmillis, checkmillis); } catch (FlumeSpecException e) { throw new IllegalArgumentException("Failed to parse/build " + spec, e); } } }; } }