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

quasar.physical.mongodb.util.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2014–2017 SlamData Inc.
 *
 * 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 quasar.physical.mongodb

import slamdata.Predef._
import quasar.connector.{EnvErr, EnvironmentError}
import quasar.config._
import quasar.effect.Failure
import quasar.fp.free
import quasar.fs.mount.ConnectionUri

import java.util.concurrent.TimeoutException

import com.mongodb._
import com.mongodb.async.client.{MongoClient => AMongoClient, MongoClients, MongoClientSettings}
import scalaz._
import scalaz.concurrent.{Strategy, Task}
import scalaz.syntax.applicative._

object util {
  import ConfigError._, EnvironmentError._

  /** Returns an async `MongoClient` for the given `ConnectionUri`. Will fail
    * with a `ConfigError` if the uri is invalid and with an `EnvironmentError`
    * if there is a problem connecting to the server.
    *
    * NB: The connection is tested during creation and creation will fail if
    *     connecting to the server times out.
    */
  def createAsyncMongoClient[S[_]](
    uri: ConnectionUri
  )(implicit
    S0: Task :<: S,
    S1: EnvErr :<: S,
    S2: CfgErr :<: S
  ): Free[S, AMongoClient] = {
    val cfgErr = Failure.Ops[ConfigError, S]
    val envErr = Failure.Ops[EnvironmentError, S]

    val disableLogging =
      free.lift(disableMongoLogging).into[S]

    val connString =
      liftAndHandle(Task.delay(new ConnectionString(uri.value)))(t =>
        cfgErr.fail(malformedConfig(uri.value, t.getMessage)))

    /** Attempts a benign operation (reading the server version) using the
      * given client in order to test whether the connection was successful,
      * necessary as otherwise, given a bad connection URI, the driver will
      * retry indefinitely, on a separate monitor thread, to test the connection
      * while the next operation on the returned `MongoClient` will just block
      * indefinitely (i.e. somewhere in userland).
      *
      * TODO: Is there a better way to achieve this? Or somewhere we can set
      *       a timeout for the driver to consider an operation that takes
      *       too long an error?
      */
    def testConnection(aclient: AMongoClient): Task[Unit] =
      MongoDbIO.serverVersion.run(aclient)
        .timed(defaultTimeoutMillis.toLong)(Strategy.DefaultTimeoutScheduler)
        .attempt flatMap {
          case -\/(tout: TimeoutException) =>
            // NB: This is a java List of mongo objects – never going to have Show.
            @SuppressWarnings(Array("org.wartremover.warts.ToString"))
            val hosts = aclient.getSettings.getClusterSettings.getHosts.toString
            Task.fail(new TimeoutException(s"Timed out attempting to connect to: $hosts"))
          case -\/(t) =>
            Task.fail(t)
          case \/-(_) =>
            Task.now(())
        }

    val InvalidHostNameAllowedProp = "invalidHostNameAllowed"

    @SuppressWarnings(Array("org.wartremover.warts.NonUnitStatements"))
    def settings(cs: ConnectionString, invalidHostNameAllowed: Boolean): Task[MongoClientSettings] = Task.delay {
      import com.mongodb.connection._

      // NB: this is apparently the only way to get from a ConnectionString to a
      // MongoClient while also inspecting/modifying anything in the settings.
      // This is following `MongoClients.create(ConnectionString)`, and will have
      // to be revisited if a driver release adds additional settings objects.
      val settings = MongoClientSettings.builder

      settings.clusterSettings(ClusterSettings.builder
        .applyConnectionString(cs)
        .build)

      settings.connectionPoolSettings(ConnectionPoolSettings.builder
        .applyConnectionString(cs)
        .build)

      settings.credentialList(cs.getCredentialList)

      settings.serverSettings(ServerSettings.builder
        .build)

      settings.socketSettings(SocketSettings.builder
        .applyConnectionString(cs)
        .build)

      val sslSettings = SslSettings.builder
        .applyConnectionString(cs)
        .invalidHostNameAllowed(invalidHostNameAllowed)
        .build
      settings.sslSettings(sslSettings)

      // NB: Netty _must_ be used if SSL is required, but we do not use it by default
      // mostly because it seems to cause the REPL to fail to exit cleanly. If necessary,
      // it can also be forced using a system property (see MongoDB docs).
      if (sslSettings.isEnabled) {
        settings.streamFactoryFactory(com.mongodb.connection.netty.NettyStreamFactoryFactory.builder.build())
      }

      settings.build
    }

    def createClient(cs: ConnectionString) = {
      import quasar.console.booleanProp

      liftAndHandle(for {
        invalidHostNameAllowed <- booleanProp(InvalidHostNameAllowedProp)
        stngs  <- settings(cs, invalidHostNameAllowed)
        client <- Task.delay(MongoClients.create(stngs))
        _      <- testConnection(client) onFinish {
                    case Some(_) => Task.delay(client.close())
                    case None    => Task.now(())
                  }
      } yield client)(t => envErr.fail(connectionFailed(t)))
    }

    disableLogging *> connString >>= createClient
  }

  ////

  // TODO: Externalize
  private val defaultTimeoutMillis: Int = 10000

  private def disableMongoLogging: Task[Unit] = {
    import java.util.logging._
    Task.delay { Logger.getLogger("org.mongodb").setLevel(Level.WARNING) }
  }

  private def liftAndHandle[S[_], A]
              (ta: Task[A])(f: Throwable => Free[S, A])
              (implicit S0: Task :<: S)
              : Free[S, A] =
    free.lift(ta.attempt).into[S].flatMap(_.fold(f, _.point[Free[S, ?]]))
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy