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

org.yupana.pekko.TsdbTcp.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2019 Rusexpertiza LLC
 *
 * 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 org.yupana.pekko

import com.typesafe.scalalogging.StrictLogging
import org.apache.pekko.actor.ActorSystem
import org.apache.pekko.stream.Attributes.CancellationStrategy
import org.apache.pekko.stream.{ ActorAttributes, Attributes, Supervision }
import org.apache.pekko.stream.scaladsl.{ Flow, Source, Tcp }
import org.apache.pekko.util.{ ByteString, ByteStringBuilder }
import org.yupana.proto.{ Request, Response }

import java.nio.ByteOrder
import java.util.concurrent.atomic.AtomicInteger
import scala.concurrent.Future
import scala.concurrent.duration._
import scala.util.{ Failure, Success, Try }

class TsdbTcp(
    requestHandler: RequestHandler,
    host: String,
    port: Int,
    majorVersion: Int,
    minorVersion: Int,
    version: String
)(implicit val system: ActorSystem)
    extends StrictLogging {

  import system.dispatcher

  private val HEART_BEAT_INTERVAL = 10
  private val FRAME_SIZE = 1024 * 100
  private val REQUEST_SIZE_LIMIT = FRAME_SIZE * 50

  val decider: Supervision.Decider = { e =>
    logger.error("Exception:", e)
    Supervision.Stop
  }

  private val connections = Tcp()
    .bind(host, port, idleTimeout = 60.seconds)
    .withAttributes(ActorAttributes.supervisionStrategy(decider))

  connections runForeach { conn =>
    val sentSize = new AtomicInteger(0)
    val sentChunks = new AtomicInteger(0)

    logger.info(s"Get TCP connection from ${conn.remoteAddress}")

    val heartbeat =
      Source
        .tick(HEART_BEAT_INTERVAL.seconds, HEART_BEAT_INTERVAL.seconds, HEART_BEAT_INTERVAL)
        .scan(0)(_ + _)
        .drop(1)
        .map { time =>
          logger.debug(s"Heartbeat($time), connection: ${conn.remoteAddress}")
          Iterator(Response(Response.Resp.Heartbeat(time.toString)))
        }

    val requestFlow = Flow[ByteString]
      .addAttributes(
        Attributes(CancellationStrategy(CancellationStrategy.AfterDelay(1.second, CancellationStrategy.FailStage)))
      )
      .scan((ByteString.empty, Option.empty[Request])) {
        case ((acc, _), part) =>
          val b = acc.concat(part)
          if (b.length > REQUEST_SIZE_LIMIT) {
            throw new IllegalArgumentException(s"Request is too big")
          }

          unpackRequest(b) match {
            case Some(r) => ByteString.empty -> Some(r)
            case None    => b -> None
          }
      }
      .collect {
        case (_, Some(r)) =>
          logger.debug("Received request" + r)
          r
      }
      .mapAsync(1) {
        case Request(Request.Req.Ping(ping)) =>
          Future.successful(requestHandler.handlePingProto(ping, majorVersion, minorVersion, version))

        case Request(Request.Req.SqlQuery(sqlQuery)) =>
          requestHandler.handleQuery(sqlQuery)

        case Request(Request.Req.BatchSqlQuery(batchSqlQuery)) =>
          requestHandler.handleBatchQuery(batchSqlQuery)

        case Request(Request.Req.Empty) =>
          val error = "Got empty request"
          logger.error(error)
          throw new Exception(error)
      }
      .collect {
        case Right(xs) =>
          xs
        case Left(s) =>
          logger.error(s)
          throw new Exception(s)
      }
      .merge(heartbeat, eagerComplete = true)
      .flatMapConcat { rs =>
        val it = rs.map { resp =>
          packResponse(resp)
        }
        val repacked = new RepackIterator(it, 32768)
        Source.fromIterator(() => repacked)
      }
      .recover {
        case e: Throwable =>
          logger.error("Message was not handled", e)
          val resp = Response(Response.Resp.Error(e.getMessage))
          packResponse(resp)
      }

    val connHandler = requestFlow
      .watchTermination() { (_, done) =>
        done.onComplete {
          case Success(_) =>
            logger.debug(
              s"Connection closed: ${conn.remoteAddress}, bytes sent ${humanReadableByteSize(sentSize.get)}, chunks ${sentChunks.get()} "
            )
          case Failure(ex) =>
            logger.error(s"Error : $ex")
        }
      }

    conn.handleWith(connHandler.withAttributes(ActorAttributes.supervisionStrategy(decider)))
  }

  private def packResponse(response: Response): ByteString = {
    implicit val byteOrder: ByteOrder = ByteOrder.BIG_ENDIAN

    val b = new ByteStringBuilder
    val bytes = response.toByteArray
    b.putInt(bytes.length)
    b.putBytes(bytes)
    b.result()
  }

  private def unpackRequest(bs: ByteString): Option[Request] = {
    implicit val byteOrder: ByteOrder = ByteOrder.BIG_ENDIAN

    val it = bs.iterator

    val size = Try {
      it.getInt
    }

    size.toOption.foreach { s =>
      if (s > REQUEST_SIZE_LIMIT) throw new IllegalArgumentException(s"Request size is too big")
    }

    size.map { s =>
      val bytes = it.getBytes(s)
      Request.parseFrom(bytes)
    }.toOption
  }

  private def humanReadableByteSize(fileSize: Long): String = {
    if (fileSize <= 0) return "0 B"
    // kilo, Mega, Giga, Tera, Peta, Exa, Zetta, Yotta
    val units: Array[String] = Array("B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
    val digitGroup: Int = (Math.log10(fileSize.toDouble) / Math.log10(1024)).toInt
    if (digitGroup == 0) {
      f"${fileSize / Math.pow(1024, digitGroup)}%3.0f ${units(digitGroup)}"
    } else {
      f"${fileSize / Math.pow(1024, digitGroup)}%3.1f ${units(digitGroup)}"
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy