com.addthis.hydra.data.query.op.OpGroupBy.java Source code

Java tutorial

Introduction

Here is the source code for com.addthis.hydra.data.query.op.OpGroupBy.java

Source

/*
 * 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();
            }
        }
    }
}