/** * Copyright 2019, Futurewei Technologies * <p> * 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 * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <p> * 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.bluemarlin.ims.imsservice.service; import com.bluemarlin.ims.imsservice.exceptions.ESConnectionException; import com.bluemarlin.ims.imsservice.dao.booking.BookingDaoESImp; import com.bluemarlin.ims.imsservice.dao.inventory.InventoryEstimateDaoESImp; import com.bluemarlin.ims.imsservice.dao.tbr.TBRDao; import com.bluemarlin.ims.imsservice.model.Booking; import com.bluemarlin.ims.imsservice.model.BookingBucket; import com.bluemarlin.ims.imsservice.model.Day; import com.bluemarlin.ims.imsservice.model.DayImpression; import com.bluemarlin.ims.imsservice.model.Impression; import com.bluemarlin.ims.imsservice.model.InventoryResult; import com.bluemarlin.ims.imsservice.model.Range; import com.bluemarlin.ims.imsservice.model.TargetingChannel; import com.bluemarlin.ims.imsservice.util.CommonUtil; import com.bluemarlin.ims.imsservice.util.TargetingChannelUtil; import javafx.util.Pair; import org.json.JSONException; import org.springframework.stereotype.Service; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; @Service public class InventoryEstimateService extends BaseService { private static final int MAX_TIME_SIZE = 100; private static ExecutorService executor = Executors.newCachedThreadPool(); public InventoryEstimateService() { } /** * This method filters out the booking buckets that do not have intersection with targeting channel. * * @param targetingChannel * @param bookingBuckets * @param bookingsMapForDay * @return */ private List<BookingBucket> filterBookingBuckets(TargetingChannel targetingChannel, List<BookingBucket> bookingBuckets, Map<String, Booking> bookingsMapForDay) { List<BookingBucket> filteredBookingBuckets = new ArrayList<>(); if (!CommonUtil.isEmpty(bookingBuckets)) { for (BookingBucket bookingBucket : bookingBuckets) { if (bookingBucket.getSumOfAllocatedAmounts() != 0 || hasIntersection(bookingBucket, targetingChannel, bookingsMapForDay)) { filteredBookingBuckets.add(bookingBucket); } } } return filteredBookingBuckets; } /** * This method returns AVAILABLE inventory for a specific targeting channel in a period of time through the following steps: * <p> * 1. Get the total inventory for the targeting channel in period of time; this value does not consider bookings. * 2. Calculate how much of the inventory is taken by each booking-bucket for each day. * 3. Subtract the total amount booked from total inventory. * <p> * This method in multi-thread function. * * @param targetingChannel * @param ranges * @return * @throws JSONException * @throws ESConnectionException * @throws IOException * @throws ExecutionException * @throws InterruptedException */ private Pair<Impression, Long> aggregateInventory(TargetingChannel targetingChannel, List<Range> ranges) throws JSONException, ESConnectionException, IOException, ExecutionException, InterruptedException { Impression result = new Impression(); if (ranges.size() == 0 || ranges.size() > MAX_TIME_SIZE) { LOGGER.error("ranges size", ranges.size()); return new Pair(result, 0L); } List<Day> sortedDays = Day.buildSortedDays(ranges); Map<Day, Impression> potentialInventory = this.getPotentialInventoryForDays(targetingChannel, new HashSet<>(sortedDays)); Map<Day, List<BookingBucket>> bookingBucketMap = this.bookingDao.getBookingBuckets(new HashSet<>(sortedDays)); Map<Day, List<Booking>> bookings = this.bookingDao.getBookings(new HashSet<>(sortedDays)); Map<Day, Future<Long>> futureMap = new HashMap<>(); for (Day day : sortedDays) { /** * It holds the amount that is booked for a day. */ Future bookedForDayFuture = executor.submit((Callable<Object>) () -> { Map<String, Booking> bookingsMapForDay = Booking.buildBookingMap(bookings.get(day)); List<BookingBucket> bookingBuckets = bookingBucketMap.get(day); /** * Filtering the bookingBuckets. */ List<BookingBucket> filteredBookingBuckets = filterBookingBuckets(targetingChannel, bookingBuckets, bookingsMapForDay); long consideredBookedForDay = 0; List<Future<Long>> bookedOnBookingBucketFutures = new ArrayList<>(); for (BookingBucket bookingBucket : filteredBookingBuckets) { Future futureForBookingBucket = executor.submit((Callable<Object>) () -> { long consideredBookedFor1Day1BB = 0; Impression outside = getInventoryForBookingBucketMinusQuery(bookingBucket, targetingChannel, day, bookingsMapForDay); long outsideOverflow = Math.round(bookingBucket.getSumOfAllocatedAmounts()) - outside.getTotal(); /** * If outside is less than allocated then outside-overflow is considered booked. * In other words consider outsideOverflow if it is more than 0. */ consideredBookedFor1Day1BB += Math.max(outsideOverflow, 0); return consideredBookedFor1Day1BB; }); bookedOnBookingBucketFutures.add(futureForBookingBucket); } for (Future<Long> _future : bookedOnBookingBucketFutures) { long count = _future.get(); consideredBookedForDay += count; } return consideredBookedForDay; }); futureMap.put(day, bookedForDayFuture); } long consideredBooked = 0; for (Day day : sortedDays) { long count = futureMap.get(day).get(); consideredBooked += count; } Impression totalInventory = new Impression(); for (Impression impression : potentialInventory.values()) { totalInventory = Impression.add(totalInventory, impression); } return new Pair(totalInventory, consideredBooked); } /** * This method returns total inventory (ignoring bookings) for a collection of targeting channels. * * @param tcs * @param day * @return * @throws JSONException * @throws IOException * @throws ESConnectionException */ private Impression getInventoryForDay_PI_TCs(List<TargetingChannel> tcs, Day day) throws JSONException, IOException, ESConnectionException { Impression impression = inventoryEstimateDao.getPredictions_PI_TCs(day, tcs); double tbr = tbrDao.getTBRRatio_PI_TCs(tcs); return Impression.multiply(impression, tbr); } /** * This method return total inventory ignoring bookings. * It considers TBR and region distribution. * * @param targetingChannel * @param days * @return * @throws JSONException * @throws ESConnectionException * @throws IOException */ private Map<Day, Impression> getPotentialInventoryForDays(TargetingChannel targetingChannel, Set<Day> days) throws JSONException, ESConnectionException, IOException { Map<Day, Impression> predictions = this.inventoryEstimateDao.aggregatePredictionsFullDays(targetingChannel, days); Map<Day, Impression> result = new HashMap<>(); if ((targetingChannel.hasMultiValues())) { LOGGER.debug("targeting channel has multi values"); double ratio = tbrDao.getTBRRatio(targetingChannel); for (Map.Entry<Day, Impression> entry : predictions.entrySet()) { Day day = entry.getKey(); Impression impression = entry.getValue(); impression = Impression.multiply(impression, ratio); result.put(day, impression); } } else { result = predictions; } return result; } public InventoryEstimateService(InventoryEstimateDaoESImp inventoryEstimateDao, TBRDao tbrDao, BookingDaoESImp bookingDao) { this.inventoryEstimateDao = inventoryEstimateDao; this.tbrDao = tbrDao; this.bookingDao = bookingDao; LOGGER.debug("process InventoryEstimateService"); } /** * This service returns available inventory for a specific targeting channel in a period of time. * <p> * THIS SERVICE DOES NOT CONSIDER REGION RATIOS. Refer to ERD for details. * * @param targetingChannel * @param ranges * @param price * @return * @throws JSONException * @throws ESConnectionException * @throws IOException */ public InventoryResult aggregateInventory(TargetingChannel targetingChannel, List<Range> ranges, double price) throws JSONException, ESConnectionException, IOException, ExecutionException, InterruptedException { InventoryResult result = new InventoryResult(); Pair<Impression, Long> inventoryValue = aggregateInventory(targetingChannel, ranges); long value = inventoryValue.getKey().countImpressions(price, targetingChannel.getPm()); long booked = inventoryValue.getValue(); value -= booked; if (value < 0) { LOGGER.info("Estimate impressions < 0"); value = 0; } result.setAvailCount(value); LOGGER.info("inventory estimate:{}", value); return result; } /** * The method returns a list of targeting channels associated with booking ids. It reflects the order of the bookings ids. * * @return */ public List<TargetingChannel> extractTargetingChannelsFromBookingsByBookingIds(Map<String, Booking> bookingsMap, List<String> bookingIds) { List<TargetingChannel> result = new ArrayList<>(); for (String bookingId : bookingIds) { if (bookingsMap.containsKey(bookingId)) { Booking booking = bookingsMap.get(bookingId); result.add(booking.getQuery()); } } return result; } /** * This method returns true if booking-bucket and targeting channel have intersection. * * @param bookingBucket * @param targetingChannel * @param bookingsMap * @return */ public boolean hasIntersection(BookingBucket bookingBucket, TargetingChannel targetingChannel, Map<String, Booking> bookingsMap) { List<TargetingChannel> bns = extractTargetingChannelsFromBookingsByBookingIds(bookingsMap, bookingBucket.getAndBookingsIds()); bns.add(targetingChannel); return TargetingChannelUtil.hasIntersectionsForSingleAttributes(bns); } /** * This method returns total inventory ignoring bookings for the following complex targeting channel collections : * <p> * (Bn1 and Bn2 and ...) - (Bm1 or Bm2 or ... or query) * <p> * Each Bn and Bm is a targeting channel. * * @param day * @param bns * @param bms * @param query * @param avgTbr : This is avg TBR of Bms * @param maxImpression : This is the maximum value of [(PBm1+PBm2+Pq)&(PBn)].avrTbr * @return * @throws ESConnectionException * @throws IOException * @throws JSONException */ public Impression getInventoryFor_BNs_Minus_BMs_Minus_q(Day day, List<TargetingChannel> bns, List<TargetingChannel> bms, TargetingChannel query, double avgTbr, Impression maxImpression) throws ESConnectionException, IOException, JSONException { /** * result = (PBns.TBns - [(PBm1+PBm2+Pq)&(PBn)].avrTbr */ Impression impression1 = getInventoryForDay_PI_TCs(bns, day); List<TargetingChannel> Bms_plus_q = new ArrayList<>(bms); Bms_plus_q.add(query); Impression PBms_plus_Pq_and_Bns = inventoryEstimateDao.getPredictions_SIGMA_TCs_PI_BNs(day, Bms_plus_q, bns); Impression impression2 = Impression.multiply(PBms_plus_Pq_and_Bns, avgTbr); /** * [(PBm1+PBm2+Pq)&(PBn)].avrTbr has the min value of * max of [(PBm(i))&(PBn)].Tbr(Bm(i), Bn) */ if (impression2.getTotal() < maxImpression.getTotal()) { impression2 = maxImpression; } Impression result = Impression.subtract(impression1, impression2); result.adjustPositive(); return result; } /** * This method returns total inventory ignoring bookings for the following complex targeting channel collections : * <p> * (Bn1 and Bn2 and ...) and query - (Bm1 or Bm2 or ... ) * <p> * Each Bn and Bm is a targeting channel. * * @param day * @param bns * @param query * @param bms * @param avgTbr * @param maxImpression * @return * @throws ESConnectionException * @throws IOException * @throws JSONException */ public Impression getInventoryFor_BNs_DOT_q_Minus_BMs(Day day, List<TargetingChannel> bns, TargetingChannel query, List<TargetingChannel> bms, double avgTbr, Impression maxImpression) throws ESConnectionException, IOException, JSONException { /** * result = (PBns.TBns - [(PBm1+PBm2+~Pq)&(PBns)].avrTbr - (Pq&(Bns-Bms))*(Tn^TqBar) * * The output of this method might be negative because of avgTbr. The result is floored at 0. */ Impression impression1 = getInventoryForDay_PI_TCs(bns, day); Impression impression2 = inventoryEstimateDao.getPredictions_SIGMA_BMs_PLUS_qBAR_DOT_BNs(day, bms, query, bns); impression2 = Impression.multiply(impression2, avgTbr); /** * [(PBm1+PBm2+~Pq)&(PBns)].avrTbr has the min value of * max of [(PBm(i))&(PBn)].Tbr(Bm(i), Bn) */ if (impression2.getTotal() < maxImpression.getTotal()) { impression2 = maxImpression; } Impression impression3 = inventoryEstimateDao.getPredictions_PI_BNs_MINUS_SIGMA_BMs_DOT_q(day, bns, bms, query); double tbr = tbrDao.getTBRRatio_PI_BNs_DOT_qBAR(bns, query); impression3 = Impression.multiply(impression3, tbr); Impression result = Impression.subtract(impression1, impression2); result = Impression.subtract(result, impression3); result.adjustPositive(); return result; } /** * This method returns total inventory (ignoring bookings) that is associated with * one booking-bucket minus a targeting channel. * * @param bookingBucket * @param query * @param day * @param bookingsMap * @return * @throws IOException * @throws JSONException * @throws ESConnectionException */ public Impression getInventoryForBookingBucketMinusQuery(BookingBucket bookingBucket, TargetingChannel query, Day day, Map<String, Booking> bookingsMap) throws IOException, JSONException, ESConnectionException { List<TargetingChannel> Bns = extractTargetingChannelsFromBookingsByBookingIds(bookingsMap, bookingBucket.getAndBookingsIds()); List<TargetingChannel> Bms = extractTargetingChannelsFromBookingsByBookingIds(bookingsMap, bookingBucket.getMinusBookingsIds()); if (CommonUtil.isEmpty(Bns)) { LOGGER.info("Bookings of BB have been removed"); return new Impression(); } Pair<Double, Impression> result = getAverageTBRForBookingBucketMinusQ(day, bookingBucket, Bns, query, bookingsMap.keySet()); double avgTbr = result.getKey(); Impression maxImpression = result.getValue(); Impression impression = getInventoryFor_BNs_Minus_BMs_Minus_q(day, Bns, Bms, query, avgTbr, maxImpression); return impression; } /** * This method returns total inventory (ignoring bookings) that is associated with intersection of * one booking-bucket and a targeting channel. * * @param bookingBucket * @param query * @param day * @param bookingsMap * @return * @throws IOException * @throws JSONException * @throws ESConnectionException */ public Impression getInventoryForBookingBucketCrossQuery(BookingBucket bookingBucket, TargetingChannel query, Day day, Map<String, Booking> bookingsMap) throws IOException, JSONException, ESConnectionException { List<TargetingChannel> Bns = extractTargetingChannelsFromBookingsByBookingIds(bookingsMap, bookingBucket.getAndBookingsIds()); List<TargetingChannel> Bms = extractTargetingChannelsFromBookingsByBookingIds(bookingsMap, bookingBucket.getMinusBookingsIds()); if (CommonUtil.isEmpty(Bns)) { LOGGER.info("Bookings of BB have been removed"); return new Impression(); } Pair<Double, Impression> result = getAverageTBRForBookingBucketMinusQBar(day, bookingBucket, Bns, query, bookingsMap.keySet()); double avgTbr = result.getKey(); Impression maxImpression = result.getValue(); Impression impression = getInventoryFor_BNs_DOT_q_Minus_BMs(day, Bns, query, Bms, avgTbr, maxImpression); return impression; } /** * This method returns average TBR of a booking-bucket minus targeting channel with its possible maximum value (max of ims/inv). * * @param day * @param bb * @param Bns * @param query * @param validBookingIds * @return * @throws IOException * @throws JSONException */ public Pair<Double, Impression> getAverageTBRForBookingBucketMinusQ(Day day, BookingBucket bb, List<TargetingChannel> Bns, TargetingChannel query, Set<String> validBookingIds) throws IOException, JSONException { BookingBucket.AvrTBRInsight avrTBRInsight = bb.getAvrTBRInsight(validBookingIds); double nominator = avrTBRInsight.getNominator(); double denominator = avrTBRInsight.getDenominator(); Impression maxImpression = avrTBRInsight.getMaxImpression(); List<TargetingChannel> _tcs = new ArrayList<>(); _tcs.addAll(Bns); _tcs.add(query); Impression impression = inventoryEstimateDao.getPredictions_PI_TCs(day, _tcs); double tbr = tbrDao.getTBRRatio_PI_TCs(_tcs); double impressionValue = impression.getTotal(); if (impressionValue * tbr > maxImpression.getTotal()) { maxImpression = Impression.multiply(impression, tbr); } nominator += impressionValue * tbr; denominator += impressionValue; return buildTBRResponse(nominator, denominator, maxImpression); } /** * This method returns average TBR of a booking-bucket minus inverse of a targeting channel with its possible maximum value (max of ims/inv). * * @param day * @param bb * @param Bns * @param query * @param validBookingIds * @return * @throws IOException * @throws JSONException * @throws ESConnectionException */ public Pair<Double, Impression> getAverageTBRForBookingBucketMinusQBar(Day day, BookingBucket bb, List<TargetingChannel> Bns, TargetingChannel query, Set<String> validBookingIds) throws IOException, JSONException, ESConnectionException { BookingBucket.AvrTBRInsight avrTBRInsight = bb.getAvrTBRInsight(validBookingIds); double nominator = avrTBRInsight.getNominator(); double denominator = avrTBRInsight.getDenominator(); Impression maxImpression = avrTBRInsight.getMaxImpression(); Impression impression = inventoryEstimateDao.getPredictions_PI_TCs_DOT_qBAR(day, Bns, query); /** * Q bar does not have tbr part */ double tbr = tbrDao.getTBRRatio_PI_TCs(Bns); double impressionValue = impression.getTotal(); if (impressionValue * tbr > maxImpression.getTotal()) { maxImpression = Impression.multiply(impression, tbr); } nominator += impressionValue * tbr; denominator += impressionValue; return buildTBRResponse(nominator, denominator, maxImpression); } /** * This service returns the last predicted total hourly impressions for a targeting channel ignoring bookings. * This method is used for chart and publishing purposes. * * @param targetingChannel * @return * @throws JSONException * @throws ESConnectionException */ public DayImpression getInventoryDateEstimate( TargetingChannel targetingChannel) throws JSONException, ESConnectionException, IOException { double tbrRatio = 1; if (!(targetingChannel.hasMultiValues())) { LOGGER.debug("targeting channel has no multi values"); } else { LOGGER.debug("targeting channel has multi values"); tbrRatio = tbrDao.getTBRRatio(targetingChannel); LOGGER.info("tbrRatio=", tbrRatio); } return inventoryEstimateDao.getHourlyPredictions(targetingChannel, tbrRatio); } private Pair<Double, Impression> buildTBRResponse(double nominator, double denominator, Impression maxImpression) { if (CommonUtil.equalNumbers(nominator, denominator)) { return new Pair(1.0, maxImpression); } if (nominator == 0) { return new Pair(0.0, maxImpression); } return new Pair(nominator / denominator, maxImpression); } }