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

com.crobox.clickhouse.ClickhouseClient.scala Maven / Gradle / Ivy

The newest version!
package com.crobox.clickhouse

import akka.NotUsed
import akka.actor.ActorSystem
import akka.http.scaladsl.model._
import akka.stream.StreamTcpException
import akka.stream.scaladsl.{Framing, Source}
import akka.util.ByteString
import com.crobox.clickhouse.balancing.HostBalancer
import com.crobox.clickhouse.internal.{ClickHouseExecutor, ClickhouseQueryBuilder, ClickhouseResponseParser}
import com.typesafe.config.Config

import scala.concurrent.{ExecutionContext, Future}

/**
 * Async clickhouse client using Akka Http and Streams
 * TODO remove the implicit ActorSystem and  use own internal actor system + configuration
 *
 * @author Sjoerd Mulder
 * @since 31-03-17
 */
class ClickhouseClient(override val config: Config, val database: String = "default")(
    override implicit val system: ActorSystem = ActorSystem("clickhouseClient")
) extends ClickHouseExecutor
    with ClickhouseResponseParser
    with ClickhouseQueryBuilder {

  private val hostBalancer = HostBalancer(config)

  private val MaximumFrameLength: Int = 1024 * 1024 // 1 MB

  logger.info(s"Starting Clickhouse Client connecting to $hostBalancer, database $database")

  /**
   * Resolves the table name relative to the current clickhouse client database.
   *
   * @param name name of the table
   */
  def table(name: String): String = s"$database.$name"

  /**
   * Execute a read-only query on Clickhouse
   *
   * @param sql a valid Clickhouse SQL string
   * @return Future with the result that clickhouse returns
   */
  def query(sql: String): Future[String] =
    executeWithRetries {
      executeRequest(_, sql)
    }

  /**
   * Execute a query that is modifying the state of the database. e.g. INSERT, SET, CREATE TABLE.
   * For security purposes SELECT and SHOW queries are not allowed, use the .query() method for those.
   *
   * @param sql a valid Clickhouse SQL string
   * @return Future with the result that clickhouse returns
   */
  def execute(sql: String): Future[String] =
    Future {
      require(
        !(sql.toUpperCase.startsWith("SELECT") || sql.toUpperCase.startsWith("SHOW")),
        ".execute() is not allowed for SELECT or SHOW statements, use .query() instead"
      )
    }.flatMap(
      _ =>
        executeWithRetries {
          executeRequest(_, sql, readOnly = false)
      }
    )

  def execute(sql: String, entity: String): Future[String] =
    executeWithRetries {
      executeRequest(_, sql, readOnly = false, Option(entity))
    }

  /**
   * Creates a stream of the SQL query that will delimit the result from Clickhouse on new-line
   *
   * @param sql a valid Clickhouse SQL string
   */
  def source(sql: String): Source[String, NotUsed] =
    Source
      .fromFuture(hostBalancer.nextHost.flatMap { host =>
        singleRequest(toRequest(host, sql))
      })
      .flatMapConcat(response => response.entity.withoutSizeLimit().dataBytes)
      .via(Framing.delimiter(ByteString("\n"), MaximumFrameLength))
      .map(_.utf8String)

  /**
   * Accepts a source of Strings that it will stream to Clickhouse
   *
   * @param sql    a valid Clickhouse SQL INSERT statement
   * @param source the Source with strings
   * @return Future with the result that clickhouse returns
   */
  def sink(sql: String, source: Source[ByteString, Any]): Future[String] = {
    val entity = HttpEntity.apply(ContentTypes.`text/plain(UTF-8)`, source)
    executeWithRetries {
      executeRequest(_, sql, readOnly = false, Option(entity))
    }
  }

  private def executeWithRetries(request: Future[Uri] => Future[String], retries: Int = 5): Future[String] =
    request(hostBalancer.nextHost).recoverWith {
      // The http server closed the connection unexpectedly before delivering responses for 1 outstanding requests
      case e: StreamTcpException if retries > 0 =>
        logger.warn(s"Stream exception, retries left: $retries", e)
        executeWithRetries(request, retries - 1)
      case e: RuntimeException
          if e.getMessage.contains("The http server closed the connection unexpectedly") && retries > 0 =>
        logger.warn(s"Unexpected connection closure, retries left: $retries", e)
        //Retry the request with 1 less retry
        executeWithRetries(request, retries - 1)
    }

  override protected implicit val executionContext: ExecutionContext =
    system.dispatcher
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy