/* * 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.addthis.hydra.data.query.op; import java.io.IOException; import java.util.HashMap; import java.util.Map; import com.addthis.basis.util.MemoryCounter; import com.addthis.bundle.channel.DataChannelError; import com.addthis.bundle.core.Bundle; import com.addthis.bundle.core.list.ListBundleFormat; import com.addthis.hydra.data.query.AbstractQueryOp; import com.addthis.hydra.data.query.QueryMemTracker; import com.addthis.hydra.data.query.QueryOp; import com.addthis.hydra.data.query.QueryOpProcessor; import com.addthis.hydra.data.query.op.merge.MergeConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelProgressivePromise; import io.netty.channel.DefaultChannelProgressivePromise; import io.netty.util.concurrent.ImmediateEventExecutor; /** * <p>This query operation <span class="hydra-summary">groups rows together</span>. * <p/> * <p>Example:</p> * <pre> * 0 A 3 * 1 A 1 * 1 B 2 * 0 A 5 * * groupby=k:limit=1 * * A 3 * B 2 * </pre> * * @user-reference * @hydra-name groupby */ public class OpGroupBy extends AbstractQueryOp { private static final Logger log = LoggerFactory.getLogger(OpGroupBy.class); private final Map<String, QueryOp> resultTable = new HashMap<>(); private final ListBundleFormat format = new ListBundleFormat(); private final long memTip; private final long rowTip; private long memTotal; private final String queryDeclaration; private final MergeConfig mergeConfig; @MemoryCounter.Mem(estimate = false) private final QueryOpProcessor processor; private final OpForward forwardingOp; @MemoryCounter.Mem(estimate = false) private final ChannelFutureListener errorForwarder; public OpGroupBy(QueryOpProcessor processor, String args, ChannelProgressivePromise opPromise) { super(opPromise); this.memTotal = 0; this.processor = processor; this.memTip = processor.memTip(); this.rowTip = processor.rowTip(); if (!args.contains(":")) { throw new IllegalStateException("groupby query argument missing ':'"); } String[] components = args.split(":", 2); this.mergeConfig = new MergeConfig(components[0]); this.queryDeclaration = components[1]; this.forwardingOp = new OpForward(opPromise, getNext()); // ops don't usually fail promises themselves, so this logic is unlikely to activate, but might as well this.errorForwarder = channelFuture -> { if (!channelFuture.isSuccess()) { opPromise.tryFailure(channelFuture.cause()); } }; opPromise.addListener(channelFuture -> { if (channelFuture.isSuccess()) { for (QueryOp queryOp : resultTable.values()) { queryOp.getOpPromise().trySuccess(); } } else { Throwable failureCause = channelFuture.cause(); for (QueryOp queryOp : resultTable.values()) { queryOp.getOpPromise().tryFailure(failureCause); } } }); } @Override public void setNext(QueryMemTracker memTracker, QueryOp next) { super.setNext(memTracker, next); forwardingOp.setForwardingTarget(next); } /** * Generate new promise for the child operation. * * @param opPromise promise of the 'groupby' query operation * * @return generated promise */ private ChannelProgressivePromise generateNewPromise(ChannelProgressivePromise opPromise) { final ChannelProgressivePromise result; if (opPromise.channel() == null) { result = new DefaultChannelProgressivePromise(null, ImmediateEventExecutor.INSTANCE); } else { result = opPromise.channel().newProgressivePromise(); } result.addListener(errorForwarder); return result; } @Override public void send(Bundle row) throws DataChannelError { if (opPromise.isDone()) { return; } String key = mergeConfig.handleBindAndGetKey(row, format); QueryOp queryOp = resultTable.computeIfAbsent(key, mapKey -> { ChannelProgressivePromise newPromise = generateNewPromise(opPromise); QueryOp newQueryOp = QueryOpProcessor.generateOps(processor, newPromise, forwardingOp, queryDeclaration); memTotal += MemoryCounter.estimateSize(newQueryOp); return newQueryOp; }); memTotal -= MemoryCounter.estimateSize(queryOp); queryOp.send(row); memTotal += MemoryCounter.estimateSize(queryOp); // If we're not tipping to disk, and the tips are set, then we will issue errors if we pass them if ((memTip > 0) && (memTotal > memTip)) { throw new DataChannelError("Memory usage of gathered objects exceeds allowed " + memTip); } if ((rowTip > 0) && (resultTable.size() > rowTip)) { throw new DataChannelError("Number of gathered rows exceeds allowed " + rowTip); } } @Override public void sendComplete() { for (QueryOp queryOp : resultTable.values()) { if (opPromise.isDone()) { break; } else { if (!queryOp.getOpPromise().isDone()) { queryOp.sendComplete(); queryOp.getOpPromise().trySuccess(); } } } QueryOp next = getNext(); next.sendComplete(); } @Override public void close() throws IOException { for (QueryOp queryOp : resultTable.values()) { QueryOp currentOp = queryOp; while (currentOp != null) { try { currentOp.close(); } catch (Throwable ex) { // hopefully an "out of off heap/direct memory" error if not an exception log.error("unexpected exception or error while closing query op", ex); } currentOp = currentOp.getNext(); } } } }