/* * Copyright 2016 JBoss 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 io.apiman.plugins.auth3scale.util.report.batchedreporter; import io.apiman.gateway.engine.async.AsyncResultImpl; import io.apiman.gateway.engine.async.IAsyncHandler; import io.apiman.gateway.engine.async.IAsyncResult; import io.apiman.gateway.engine.async.IAsyncResultHandler; import io.apiman.plugins.auth3scale.Auth3ScaleConstants; import io.apiman.plugins.auth3scale.util.ParameterMap; import io.apiman.plugins.auth3scale.util.report.ReportResponseHandler.ReportResponse; import java.net.URI; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ConcurrentHashMap; /** * @author Marc Savy {@literal <[email protected]>} * @param <T> extends ReportData */ public class ReporterImpl<T extends BatchedReportData> implements Reporter { private final ReporterOptions options; private IAsyncHandler<Void> fullHandler; private IAsyncResultHandler<List<BatchedReportData>> flushHandler; private final Map<Integer, ArrayBlockingQueue<T>> reportBuckets = new ConcurrentHashMap<>(); public ReporterImpl(ReporterOptions options) { this.options = options; } @Override public List<EncodedReport> encode() { List<EncodedReport> encodedReports = new ArrayList<>(reportBuckets.size()); // For each bucket for (ArrayBlockingQueue<T> bucket : reportBuckets.values()) { if (bucket.isEmpty()) { continue; } // Drain TODO Small chance of brief blocking; can rework easily if this becomes a problem. List<BatchedReportData> reports = new ArrayList<>(bucket.size()); bucket.drainTo(reports); encodedReports.add(new ReportToSendImpl(options.getReportEndpoint(), reports, flushHandler)); } return encodedReports; } public ReporterImpl<T> addRecord(T record) { ArrayBlockingQueue<T> reportGroup = reportBuckets.computeIfAbsent(record.bucketId(), k -> new ArrayBlockingQueue<>(options.getInitialBucketCapacity())); reportGroup.add(record); // This is just approximate, we don't care whether it's somewhat out. if (reportGroup.size() >= options.getBucketFullTriggerSize()) { full(); } return this; } public ReporterImpl<T> flushHandler(IAsyncResultHandler<List<BatchedReportData>> flushHandler) { this.flushHandler = flushHandler; return this; } @Override public ReporterImpl<T> setFullHandler(IAsyncHandler<Void> fullHandler) { this.fullHandler = fullHandler; return this; } protected void full() { fullHandler.handle((Void) null); } private static final class ReportToSendImpl implements EncodedReport { private final URI endpoint; private final List<BatchedReportData> reports; private IAsyncResultHandler<List<BatchedReportData>> flushHandler; public ReportToSendImpl(URI endpoint, List<BatchedReportData> reports, IAsyncResultHandler<List<BatchedReportData>> flushHandler) { this.endpoint = endpoint; this.reports = reports; this.flushHandler = flushHandler; } @Override public String getData() { // 2 mandatory top-level items ParameterMap data = new ParameterMap(); data.add(Auth3ScaleConstants.SERVICE_TOKEN, reports.get(0).getServiceToken()); data.add(Auth3ScaleConstants.SERVICE_ID, reports.get(0).getServiceId()); List<ParameterMap> transactions = new ArrayList<>(); // Build transactions list. reports.stream().forEach(report -> transactions.add(report.toParameterMap())); // Get array representation. data.add(Auth3ScaleConstants.TRANSACTIONS, transactions.toArray(new ParameterMap[0])); return data.encode(); } @Override public String getContentType() { return "application/x-www-form-urlencoded"; //$NON-NLS-1$ } @Override public URI getEndpoint() { return endpoint; } @Override public void flush(IAsyncResult<ReportResponse> reportResponse) { if (reportResponse.isSuccess()) { flushHandler.handle(AsyncResultImpl.create(reports)); } else { // Flushing failed! Likely same result -- want to flush from cache somewhere. flushHandler.handle(new IAsyncResult<List<BatchedReportData>>() { @Override public boolean isSuccess() { return false; } @Override public boolean isError() { return true; } @Override public List<BatchedReportData> getResult() { return reports; } @Override public Throwable getError() { return new RuntimeException("Reporting failed; see #getResult for failed entries."); //$NON-NLS-1$ } }); } } @Override public String toString() { final int maxLen = 10; return String.format("Report [endpoint=%s, reports=%s]", endpoint, //$NON-NLS-1$ reports != null ? reports.subList(0, Math.min(reports.size(), maxLen)) : null); } } }