All Downloads are FREE. Search and download functionalities are using the official Maven repository.

cz.o2.proxima.server.IngestService Maven / Gradle / Ivy

/**
 * Copyright 2017-2021 O2 Czech Republic, a.s.
 *
 * 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 cz.o2.proxima.server;

import static cz.o2.proxima.server.IngestServer.ingestRequest;
import static cz.o2.proxima.server.IngestServer.notFound;
import static cz.o2.proxima.server.IngestServer.status;

import com.google.common.base.Strings;
import com.google.protobuf.TextFormat;
import cz.o2.proxima.direct.core.DirectDataOperator;
import cz.o2.proxima.proto.service.IngestServiceGrpc;
import cz.o2.proxima.proto.service.Rpc;
import cz.o2.proxima.repository.AttributeDescriptor;
import cz.o2.proxima.repository.EntityDescriptor;
import cz.o2.proxima.repository.Repository;
import cz.o2.proxima.server.metrics.Metrics;
import cz.o2.proxima.storage.StreamElement;
import io.grpc.stub.StreamObserver;
import java.util.Optional;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import lombok.extern.slf4j.Slf4j;

/** The ingestion service. */
@Slf4j
public class IngestService extends IngestServiceGrpc.IngestServiceImplBase {

  private final Repository repo;
  private final DirectDataOperator direct;
  private final ScheduledExecutorService scheduler;

  public IngestService(
      Repository repo, DirectDataOperator direct, ScheduledExecutorService scheduler) {

    this.repo = repo;
    this.direct = direct;
    this.scheduler = scheduler;
  }

  private class IngestObserver implements StreamObserver {

    final StreamObserver responseObserver;
    final Object inflightRequestsLock = new Object();
    final AtomicInteger inflightRequests = new AtomicInteger(0);
    final Object responseObserverLock = new Object();

    IngestObserver(StreamObserver responseObserver) {
      this.responseObserver = responseObserver;
    }

    @Override
    public void onNext(Rpc.Ingest request) {
      Metrics.INGEST_SINGLE.increment();
      inflightRequests.incrementAndGet();
      processSingleIngest(
          request,
          status -> {
            synchronized (responseObserverLock) {
              responseObserver.onNext(status);
            }
            if (inflightRequests.decrementAndGet() == 0) {
              synchronized (inflightRequestsLock) {
                inflightRequestsLock.notifyAll();
              }
            }
          });
    }

    @Override
    public void onError(Throwable thrwbl) {
      log.error("Error on channel", thrwbl);
      synchronized (responseObserverLock) {
        responseObserver.onError(thrwbl);
      }
    }

    @Override
    public void onCompleted() {
      inflightRequests.accumulateAndGet(
          0,
          (a, b) -> {
            int res = a + b;
            if (res > 0) {
              synchronized (inflightRequestsLock) {
                try {
                  while (inflightRequests.get() > 0) {
                    inflightRequestsLock.wait();
                  }
                } catch (InterruptedException ex) {
                  Thread.currentThread().interrupt();
                }
              }
            }
            synchronized (responseObserverLock) {
              responseObserver.onCompleted();
            }
            return res;
          });
    }
  }

  private class IngestBulkObserver implements StreamObserver {

    final StreamObserver responseObserver;
    final Queue statusQueue = new ConcurrentLinkedQueue<>();
    final AtomicBoolean completed = new AtomicBoolean(false);
    final Object inflightRequestsLock = new Object();
    final AtomicInteger inflightRequests = new AtomicInteger();
    final AtomicLong lastFlushNanos = new AtomicLong(System.nanoTime());
    final Rpc.StatusBulk.Builder builder = Rpc.StatusBulk.newBuilder();
    static final long MAX_SLEEP_NANOS = 100000000L;
    static final int MAX_QUEUED_STATUSES = 500;

    Runnable flushTask = createFlushTask();

    // schedule the flush periodically
    ScheduledFuture flushFuture =
        scheduler.scheduleAtFixedRate(
            flushTask, MAX_SLEEP_NANOS, MAX_SLEEP_NANOS, TimeUnit.NANOSECONDS);

    IngestBulkObserver(StreamObserver responseObserver) {
      this.responseObserver = responseObserver;
    }

    private Runnable createFlushTask() {
      return () -> {
        try {
          synchronized (builder) {
            while (statusQueue.size() > MAX_QUEUED_STATUSES) {
              peekQueueToBuilderAndFlush();
            }
            long now = System.nanoTime();
            if (now - lastFlushNanos.get() >= MAX_SLEEP_NANOS) {
              while (!statusQueue.isEmpty()) {
                peekQueueToBuilderAndFlush();
              }
            }
            if (builder.getStatusCount() > 0) {
              responseObserver.onNext(builder.build());
              builder.clear();
            }
            if (completed.get() && inflightRequests.get() == 0 && statusQueue.isEmpty()) {

              responseObserver.onCompleted();
            }
          }
        } catch (Exception ex) {
          log.error("Failed to send bulk status", ex);
        }
      };
    }

    private void peekQueueToBuilderAndFlush() {
      synchronized (builder) {
        builder.addStatus(statusQueue.poll());
        if (builder.getStatusCount() >= 1000) {
          flush();
        }
      }
    }

    /** Flush response(s) to the observer. */
    private void flush() {
      synchronized (builder) {
        lastFlushNanos.set(System.nanoTime());
        Rpc.StatusBulk bulk = builder.build();
        if (bulk.getStatusCount() > 0) {
          responseObserver.onNext(bulk);
        }
        builder.clear();
      }
    }

    @Override
    public void onNext(Rpc.IngestBulk bulk) {
      Metrics.INGEST_BULK.increment();
      Metrics.BULK_SIZE.increment(bulk.getIngestCount());
      inflightRequests.addAndGet(bulk.getIngestCount());
      bulk.getIngestList()
          .stream()
          .forEach(
              r ->
                  processSingleIngest(
                      r,
                      status -> {
                        statusQueue.add(status);
                        if (statusQueue.size() >= MAX_QUEUED_STATUSES) {
                          // enqueue flush
                          scheduler.execute(flushTask);
                        }
                        if (inflightRequests.decrementAndGet() == 0) {
                          // there is no more inflight requests
                          synchronized (inflightRequestsLock) {
                            inflightRequestsLock.notifyAll();
                          }
                        }
                      }));
    }

    @Override
    public void onError(Throwable error) {
      log.error("Error from client", error);
      // close the connection
      responseObserver.onError(error);
      flushFuture.cancel(true);
    }

    @Override
    public void onCompleted() {
      completed.set(true);
      flushFuture.cancel(true);
      // flush all responses to the observer
      synchronized (inflightRequests) {
        while (inflightRequests.get() > 0) {
          try {
            inflightRequests.wait(100);
          } catch (InterruptedException ex) {
            log.warn("Interrupted while waiting to send responses to client", ex);
            Thread.currentThread().interrupt();
          }
        }
      }
      while (!statusQueue.isEmpty()) {
        peekQueueToBuilderAndFlush();
      }
      flush();
      responseObserver.onCompleted();
    }
  }

  private void processSingleIngest(Rpc.Ingest request, Consumer consumer) {

    if (log.isDebugEnabled()) {
      log.debug("Processing input ingest {}", TextFormat.shortDebugString(request));
    }
    Consumer loggingConsumer =
        rpc -> {
          log.info(
              "Input ingest {}: {}, {}",
              TextFormat.shortDebugString(request),
              rpc.getStatus(),
              rpc.getStatus() == 200 ? "OK" : rpc.getStatusMessage());
          consumer.accept(rpc);
        };
    Metrics.INGESTS.increment();
    try {
      if (!writeRequest(request, loggingConsumer)) {
        Metrics.INVALID_REQUEST.increment();
      }
    } catch (Exception err) {
      log.error("Error processing user request {}", request, err);
      loggingConsumer.accept(status(request.getUuid(), 500, err.getMessage()));
    }
  }

  /**
   * Ingest the given request and return {@code true} if successfully ingested and {@code false} if
   * the request is invalid.
   */
  private boolean writeRequest(Rpc.Ingest request, Consumer consumer) {

    if (Strings.isNullOrEmpty(request.getKey())
        || Strings.isNullOrEmpty(request.getEntity())
        || Strings.isNullOrEmpty(request.getAttribute())) {
      consumer.accept(status(request.getUuid(), 400, "Missing required fields in input message"));
      return false;
    }
    Optional entity = repo.findEntity(request.getEntity());

    if (!entity.isPresent()) {
      consumer.accept(notFound(request.getUuid(), "Entity " + request.getEntity() + " not found"));
      return false;
    }
    Optional> attr = entity.get().findAttribute(request.getAttribute());
    if (!attr.isPresent()) {
      consumer.accept(
          notFound(
              request.getUuid(),
              "Attribute "
                  + request.getAttribute()
                  + " of entity "
                  + entity.get().getName()
                  + " not found"));
      return false;
    }
    return ingestRequest(
        direct, toStreamElement(request, entity.get(), attr.get()), request.getUuid(), consumer);
  }

  @Override
  public void ingest(Rpc.Ingest request, StreamObserver responseObserver) {

    Metrics.INGEST_SINGLE.increment();
    processSingleIngest(
        request,
        status -> {
          responseObserver.onNext(status);
          responseObserver.onCompleted();
        });
  }

  @Override
  public StreamObserver ingestSingle(StreamObserver responseObserver) {

    return new IngestObserver(responseObserver);
  }

  @Override
  public StreamObserver ingestBulk(
      StreamObserver responseObserver) {

    // the responseObserver doesn't have to be synchronized in this
    // case, because the communication with the observer is done
    // in single flush thread

    return new IngestBulkObserver(responseObserver);
  }

  private static StreamElement toStreamElement(
      Rpc.Ingest request, EntityDescriptor entity, AttributeDescriptor attr) {

    long stamp = request.getStamp() == 0 ? System.currentTimeMillis() : request.getStamp();

    if (request.getDelete()) {
      return attr.isWildcard() && attr.getName().equals(request.getAttribute())
          ? StreamElement.deleteWildcard(entity, attr, request.getUuid(), request.getKey(), stamp)
          : StreamElement.delete(
              entity, attr, request.getUuid(), request.getKey(), request.getAttribute(), stamp);
    }
    return StreamElement.upsert(
        entity,
        attr,
        request.getUuid(),
        request.getKey(),
        request.getAttribute(),
        stamp,
        request.getValue().toByteArray());
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy