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

com.gilt.cavellc.batching.CaveBatchClient.scala Maven / Gradle / Ivy

package com.gilt.cavellc.batching

import java.util.{Timer, TimerTask}

import com.gilt.cavellc.models.Metric
import com.gilt.gfc.logging.Loggable
import com.gilt.gfc.concurrent.ScalaFutures._

import scala.collection.mutable
import scala.concurrent.forkjoin.ForkJoinPool
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success}

trait MetricsBatchClient {
  /**
   * Create metric
   *
   * @param metric Metrics to send.
   */
  def createMetric(metric: Metric): Unit

  /**
   * Explicitly shutdown the batching client.
   *
   * Specifically, this cancels internal timers, etc.
   *
   * Note, internal timers, etc., will be created as daemons, so should not block normal terminations if this
   * method is not called.
   */
  def shutdown(): Unit
}

/**
 * Simple batching of metrics for CAVE.
 *
 * This class gathers raw metrics together, and sends to CAVE, as batches, in a timely fashion.
 *
 * The approach in this class is to collect metrics into collections, and when full, or a timer (based on the age of the first
 * metric in the collection) expires, that batch is sent to CAVE.
 *
 * A minimal runtime impact is achieved by using a single Timer, and a single thread - the timer is used simply to schedule tasks
 * to be executed on the thread, with the thread being used for all the work (interacting with CAVE, etc.).
 *
 * Note, each instance of this class builds its own batches, and delivers to CAVE individually - multiple instances will result
 * in multiple batches, if this is so desired.
 */
class CaveBatchClient(batchConfig: CaveBatchConfiguration, publishMetrics: Seq[Metric] => Future[Unit]) extends MetricsBatchClient with Loggable {

  private var timerTask: Option[TimerTask] = None
  private val metrics = mutable.ArrayBuffer.empty[Metric]

  /**
    * Mutex to ensure correct handling around the growing collection representing the batch, control of timers,
    * and initiation of delivering the batches, etc.
    * This will not be used to coordinate communication with CAVE, or any other long/blocking operation.
    */
  private val mutex = new Object

  def createMetric(metric: Metric): Unit = {
    mutex.synchronized {
      if (metrics.isEmpty) {
        startBatchTimer()
      }

      metrics += metric

      if (complete) {
        cancelBatchTimer()
        send()
      }
    }
  }

  private def complete: Boolean = metrics.size >= batchConfig.sendBatchSize

  private def send(attemptNumber: Int = 1, sendBatchOpt: Option[List[Metric]] = None): Unit = {
    // re-entrant, but ensure mutual exclusion, as there are multiple paths to call this method
    mutex.synchronized {
      val sendBatch = sendBatchOpt.getOrElse {
        val batch = metrics.toList
        metrics.clear()
        batch
      }

      publishMetrics(sendBatch).withTimeout(batchConfig.publishTimeout, Some("Failed to send metrics batch")).onComplete {
        case Success(_) =>
          debug(s"batch of ${sendBatch.size} metrics successfully sent")

        case Failure(ex) =>
          if (attemptNumber < batchConfig.maxAttempts) {
            warn(s"Failed to send metrics to CAVE, retrying shortly", ex)
            timer.schedule(new FutureLaunchingTimerTask(send(attemptNumber + 1, Some(sendBatch))), batchConfig.retryTimeout.toMillis)
          } else {
            warn(s"Batch of metrics failed to be sent to CAVE $attemptNumber times - dumping ${sendBatch.size} metrics", ex)
          }
      }
    }
  }

  private def cancelBatchTimer(): Unit = mutex.synchronized {
    timerTask.foreach(_.cancel)
    timerTask = None
  }

  private def startBatchTimer(): Unit = {
    val task = new FutureLaunchingTimerTask(
      mutex.synchronized {
        timerTask = None
        send(1)
      }
    )
    timer.schedule(task, batchConfig.batchTimeout.toMillis)
    timerTask = Some(task)
  }

  // Only a single task is allowed to run concurrently
  private implicit val executionContext = ExecutionContext.fromExecutor(new ForkJoinPool(1))

  private val timer = new Timer("cave-batching-timer", true)

  private class FutureLaunchingTimerTask(func: => Unit)(implicit ec: ExecutionContext) extends TimerTask {
    override def run(): Unit = func
  }

  def shutdown(): Unit = {
    timer.cancel()
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy