
com.couchbase.client.scala.manager.eventing.AsyncEventingFunctionManagerShared.scala Maven / Gradle / Ivy
/*
* Copyright 2024 Couchbase, 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 com.couchbase.client.scala.manager.eventing
import com.couchbase.client.core.Core
import com.couchbase.client.core.api.CoreCouchbaseOps
import com.couchbase.client.core.api.manager.CoreBucketAndScope
import com.couchbase.client.core.cnc.RequestSpan
import com.couchbase.client.core.endpoint.http.CoreCommonOptions
import com.couchbase.client.core.error.DecodingFailureException
import com.couchbase.client.core.manager.CoreEventingFunctionManager
import com.couchbase.client.core.protostellar.CoreProtostellarUtil
import com.couchbase.client.core.retry.RetryStrategy
import com.couchbase.client.scala.env.ClusterEnvironment
import com.couchbase.client.scala.json.{JsonArray, JsonArraySafe, JsonObject, JsonObjectSafe}
import com.couchbase.client.scala.query.QueryScanConsistency
import com.couchbase.client.scala.util.DurationConversions._
import com.couchbase.client.scala.util.{FunctionalUtil, FutureConversions}
import java.nio.charset.StandardCharsets
import java.util.concurrent.TimeUnit
import scala.concurrent.duration.Duration
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success, Try}
private[scala] class AsyncEventingFunctionManagerShared(
private val env: ClusterEnvironment,
private val couchbaseOps: CoreCouchbaseOps,
private val scope: Option[CoreBucketAndScope]
)(
implicit ec: ExecutionContext
) {
private[scala] val DefaultTimeout = env.timeoutConfig.managementTimeout
private[scala] val DefaultRetryStrategy = env.retryStrategy
private def coreManagerTry: Future[CoreEventingFunctionManager] = {
couchbaseOps match {
case core: Core => Future.successful(new CoreEventingFunctionManager(core, scope.orNull))
case _ =>
Future.failed(
CoreProtostellarUtil.unsupportedInProtostellar("eventing function management")
)
}
}
def upsertFunction(
function: EventingFunction,
timeout: Duration = DefaultTimeout,
retryStrategy: RetryStrategy = DefaultRetryStrategy,
parentSpan: Option[RequestSpan] = None
): Future[Unit] = {
coreManagerTry.flatMap(
coreManager =>
FutureConversions
.javaCFToScalaFutureMappingExceptions(
coreManager.upsertFunction(
function.name,
AsyncEventingFunctionManagerShared.encodeFunction(function),
makeOptions(timeout, retryStrategy, parentSpan)
)
)
.map(_ => ())
)
}
def getFunction(
name: String,
timeout: Duration = DefaultTimeout,
retryStrategy: RetryStrategy = DefaultRetryStrategy,
parentSpan: Option[RequestSpan] = None
): Future[EventingFunction] = {
coreManagerTry.flatMap(
coreManager =>
FutureConversions
.javaCFToScalaFutureMappingExceptions(
coreManager.getFunction(name, makeOptions(timeout, retryStrategy, parentSpan))
)
.flatMap(
v =>
AsyncEventingFunctionManagerShared.decodeFunction(v) match {
case Success(x) => Future.successful(x)
case Failure(err) => Future.failed(err)
}
)
)
}
def dropFunction(
name: String,
timeout: Duration = DefaultTimeout,
retryStrategy: RetryStrategy = DefaultRetryStrategy,
parentSpan: Option[RequestSpan] = None
): Future[Unit] = {
coreManagerTry.flatMap(
coreManager =>
FutureConversions
.javaCFToScalaFutureMappingExceptions(
coreManager.dropFunction(name, makeOptions(timeout, retryStrategy, parentSpan))
)
.map(_ => ())
)
}
private def makeOptions(
timeout: Duration,
retryStrategy: RetryStrategy,
parentSpan: Option[RequestSpan]
): CoreCommonOptions = {
CoreCommonOptions.of(timeout, retryStrategy, parentSpan.orNull)
}
def deployFunction(
name: String,
timeout: Duration = DefaultTimeout,
retryStrategy: RetryStrategy = DefaultRetryStrategy,
parentSpan: Option[RequestSpan] = None
): Future[Unit] = {
coreManagerTry.flatMap(
coreManager =>
FutureConversions
.javaCFToScalaFutureMappingExceptions(
coreManager.deployFunction(name, makeOptions(timeout, retryStrategy, parentSpan))
)
.map(_ => ())
)
}
def undeployFunction(
name: String,
timeout: Duration = DefaultTimeout,
retryStrategy: RetryStrategy = DefaultRetryStrategy,
parentSpan: Option[RequestSpan] = None
): Future[Unit] = {
coreManagerTry.flatMap(
coreManager =>
FutureConversions
.javaCFToScalaFutureMappingExceptions(
coreManager.undeployFunction(name, makeOptions(timeout, retryStrategy, parentSpan))
)
.map(_ => ())
)
}
def getAllFunctions(
timeout: Duration = DefaultTimeout,
retryStrategy: RetryStrategy = DefaultRetryStrategy,
parentSpan: Option[RequestSpan] = None
): Future[Seq[EventingFunction]] = {
coreManagerTry.flatMap(
coreManager =>
FutureConversions
.javaCFToScalaFutureMappingExceptions(
coreManager
.getAllFunctions(makeOptions(timeout, retryStrategy, parentSpan))
)
.flatMap(
v =>
AsyncEventingFunctionManagerShared.decodeFunctions(v) match {
case Success(x) => Future.successful(x)
case Failure(err) => Future.failed(err)
}
)
)
}
def pauseFunction(
name: String,
timeout: Duration = DefaultTimeout,
retryStrategy: RetryStrategy = DefaultRetryStrategy,
parentSpan: Option[RequestSpan] = None
): Future[Unit] = {
coreManagerTry.flatMap(
coreManager =>
FutureConversions
.javaCFToScalaFutureMappingExceptions(
coreManager.pauseFunction(name, makeOptions(timeout, retryStrategy, parentSpan))
)
.map(_ => ())
)
}
def resumeFunction(
name: String,
timeout: Duration = DefaultTimeout,
retryStrategy: RetryStrategy = DefaultRetryStrategy,
parentSpan: Option[RequestSpan] = None
): Future[Unit] = {
coreManagerTry.flatMap(
coreManager =>
FutureConversions
.javaCFToScalaFutureMappingExceptions(
coreManager.resumeFunction(name, makeOptions(timeout, retryStrategy, parentSpan))
)
.map(_ => ())
)
}
def functionsStatus(
timeout: Duration = DefaultTimeout,
retryStrategy: RetryStrategy = DefaultRetryStrategy,
parentSpan: Option[RequestSpan] = None
): Future[EventingStatus] = {
coreManagerTry.flatMap(
coreManager =>
FutureConversions
.javaCFToScalaFutureMappingExceptions(
coreManager
.functionsStatus(makeOptions(timeout, retryStrategy, parentSpan))
)
.flatMap(
bytes =>
JsonObjectSafe
.fromJsonSafe(new String(bytes, StandardCharsets.UTF_8))
.flatMap(json => AsyncEventingFunctionManagerShared.decodeStatus(json)) match {
case Success(x) => Future.successful(x)
case Failure(err) => Future.failed(err)
}
)
)
}
}
private[scala] object AsyncEventingFunctionManagerShared {
private def encodeFunction(function: EventingFunction): Array[Byte] = {
val func = JsonObject.create
func.put("appname", function.name)
func.put("appcode", function.code)
function.version.foreach(v => func.put("version", v))
function.enforceSchema.foreach(v => func.put("enforce_schema", v))
function.handlerUuid.foreach(v => func.put("handleruuid", v))
function.functionInstanceId.foreach(v => func.put("function_instance_id", v))
val depcfg = JsonObject.create
depcfg.put("source_bucket", function.sourceKeyspace.bucket)
function.sourceKeyspace.scope.foreach(v => depcfg.put("source_scope", v))
function.sourceKeyspace.collection.foreach(v => depcfg.put("source_collection", v))
depcfg.put("metadata_bucket", function.metadataKeyspace.bucket)
function.metadataKeyspace.scope.foreach(v => depcfg.put("metadata_scope", v))
function.metadataKeyspace.collection.foreach(v => depcfg.put("metadata_collection", v))
function.constantBindings match {
case Some(cb) if cb.nonEmpty =>
val constants = JsonArray.create
cb.foreach(v => {
constants.add(
JsonObject.create
.put("alias", v.alias)
.put("literal", v.literal)
)
})
depcfg.put("constants", constants)
case _ =>
}
function.urlBindings match {
case Some(bindings) if bindings.nonEmpty =>
val urls = JsonArray.create
bindings.foreach(c => {
val map = JsonObject.create
map.put("alias", c.alias)
map.put("hostname", c.hostname)
map.put("allow_cookies", c.allowCookies)
map.put("validate_ssl_certificates", c.validateSslCertificate)
c.auth match {
case EventingFunctionUrlAuth.None =>
map.put("auth_type", "no-auth")
case v: EventingFunctionUrlAuth.Basic =>
map.put("auth_type", "basic")
map.put("username", v.username)
v.password.foreach(x => map.put("password", x))
case _: EventingFunctionUrlAuth.Digest =>
map.put("auth_type", "digest")
case _: EventingFunctionUrlAuth.Bearer =>
map.put("auth_type", "bearer")
}
urls.add(map)
})
depcfg.put("curl", urls)
case _ =>
}
function.bucketBindings match {
case Some(bindings) if bindings.nonEmpty =>
val buckets = JsonArray.create
bindings.foreach(c => {
val map = JsonObject.create
map.put("alias", c.alias)
map.put("bucket_name", c.name.bucket)
map.put("scope_name", c.name.scope)
map.put("collection_name", c.name.collection)
val access = c.access match {
case EventingFunctionBucketAccess.ReadOnly => "r"
case EventingFunctionBucketAccess.ReadWrite => "rw"
}
map.put("access", access)
buckets.add(map)
})
depcfg.put("buckets", buckets)
case _ =>
}
val settings = JsonObject.create
function.settings match {
case Some(efs) =>
efs.processingStatus match {
case Some(EventingFunctionProcessingStatus.Running) =>
settings.put("processing_status", true)
case _ => settings.put("processing_status", false)
}
efs.deploymentStatus match {
case Some(EventingFunctionDeploymentStatus.Deployed) =>
settings.put("deployment_status", true)
case _ => settings.put("deployment_status", false)
}
efs.cppWorkerThreadCount.foreach(v => settings.put("cpp_worker_thread_count", v))
efs.dcpStreamBoundary.foreach(
v =>
settings.put("dcp_stream_boundary", v match {
case EventingFunctionDcpBoundary.Everything => "everything"
case EventingFunctionDcpBoundary.FromNow => "from_now"
})
)
efs.description.foreach(v => settings.put("description", v))
efs.logLevel.foreach(
v =>
settings.put(
"log_level",
v match {
case EventingFunctionLogLevel.Info => "INFO"
case EventingFunctionLogLevel.Error => "ERROR"
case EventingFunctionLogLevel.Warning => "WARN"
case EventingFunctionLogLevel.Debug => "DEBUG"
case EventingFunctionLogLevel.Trace => "TRACE"
}
)
)
efs.languageCompatibility.foreach(
v =>
settings.put(
"language_compatibility",
v match {
case EventingFunctionLanguageCompatibility.Version_6_0_0 => "6.0.0"
case EventingFunctionLanguageCompatibility.Version_6_5_0 => "6.5.0"
case EventingFunctionLanguageCompatibility.Version_6_6_2 => "6.6.2"
case EventingFunctionLanguageCompatibility.Version_7_2_0 => "7.2.0"
}
)
)
efs.executionTimeout.foreach(v => settings.put("execution_timeout", v.toSeconds))
efs.lcbTimeout.foreach(v => settings.put("lcb_timeout", v.toSeconds))
efs.lcbInstCapacity.foreach(v => settings.put("lcb_inst_capacity", v))
efs.lcbRetryCount.foreach(v => settings.put("lcb_retry_count", v))
efs.numTimerPartitions.foreach(v => settings.put("num_timer_partitions", v))
efs.sockBatchSize.foreach(v => settings.put("sock_batch_size", v))
efs.tickDuration.foreach(v => settings.put("tick_duration", v.toMillis))
efs.timerContextSize.foreach(v => settings.put("timer_context_size", v))
efs.bucketCacheSize.foreach(v => settings.put("bucket_cache_size", v))
efs.bucketCacheAge.foreach(v => settings.put("bucket_cache_age", v))
efs.curlMaxAllowedRespSize.foreach(v => settings.put("curl_max_allowed_resp_size", v))
efs.workerCount.foreach(v => settings.put("worker_count", v))
efs.appLogMaxSize.foreach(v => settings.put("app_log_max_size", v))
efs.appLogMaxFiles.foreach(v => settings.put("app_log_max_files", v))
efs.checkpointInterval.foreach(v => settings.put("checkpoint_interval", v.toSeconds))
efs.handlerHeaders match {
case Some(v) if v.nonEmpty => settings.put("handler_headers", JsonArray.fromSeq(v))
case _ =>
}
efs.handlerFooters match {
case Some(v) if v.nonEmpty => settings.put("handler_fotters", JsonArray.fromSeq(v))
case _ =>
}
efs.queryPrepareAll.foreach(v => settings.put("n1ql_prepare_all", v))
efs.enableAppLogRotation.foreach(v => settings.put("enable_applog_rotation", v))
efs.userPrefix.foreach(v => settings.put("user_prefix", v))
efs.appLogDir.foreach(v => settings.put("app_log_dir", v))
efs.queryConsistency match {
case Some(_: QueryScanConsistency.RequestPlus) =>
settings.put("n1ql_consistency", "request")
case _ => settings.put("n1ql_consistency", "none")
}
case _ =>
settings.put("processing_status", false)
settings.put("deployment_status", false)
}
func.put("depcfg", depcfg)
func.put("settings", settings)
func.toString.getBytes(StandardCharsets.UTF_8)
}
def decodeStatus(in: JsonObjectSafe): Try[EventingStatus] = {
in.num("num_eventing_nodes")
.flatMap(numEventingNodes => {
in.arr("apps")
.flatMap(apps => {
val functions = apps.iterator.map {
case app: JsonObjectSafe =>
for {
name <- app.str("name")
status <- app.str("composite_status")
statusMapped <- status match {
case "undeployed" => Success(EventingFunctionStatus.Undeployed)
case "deploying" => Success(EventingFunctionStatus.Deploying)
case "deployed" => Success(EventingFunctionStatus.Deployed)
case "undeploying" => Success(EventingFunctionStatus.Deploying)
case "paused" => Success(EventingFunctionStatus.Paused)
case "pausing" => Success(EventingFunctionStatus.Pausing)
case _ =>
Failure(new DecodingFailureException(s"Unknown composite_status $status"))
}
numBootstrappingNodes <- app.num("num_bootstrapping_nodes")
numDeployedNodes <- app.num("num_deployed_nodes")
deploymentStatus <- app.bool("deployment_status") match {
case Success(true) => Success(EventingFunctionDeploymentStatus.Deployed)
case _ => Success(EventingFunctionDeploymentStatus.Undeployed)
}
processingStatus <- app.bool("processing_status") match {
case Success(true) => Success(EventingFunctionProcessingStatus.Running)
case _ => Success(EventingFunctionProcessingStatus.Paused)
}
} yield EventingFunctionState(
name,
statusMapped,
numBootstrappingNodes,
numDeployedNodes,
deploymentStatus,
processingStatus
)
}.toSeq
FunctionalUtil.traverse(functions).map(v => EventingStatus(numEventingNodes, v))
})
})
}
def decodeKeyspace(in: JsonObjectSafe, prefix: String): Try[EventingFunctionKeyspace] = {
val bucket = in.str(prefix + "bucket")
val scope = in.str(prefix + "scope").toOption
val collection = in.str(prefix + "collection").toOption
val x = bucket.map(b => EventingFunctionKeyspace(b, scope, collection))
x
}
def decodeFunction(encoded: Array[Byte]): Try[EventingFunction] = {
val s = new String(encoded, StandardCharsets.UTF_8)
JsonObjectSafe
.fromJsonSafe(s)
.flatMap(func => {
for {
depcfg <- func.obj("depcfg")
settings <- func.obj("settings")
appname <- func.str("appname")
appcode <- func.str("appcode")
sourceKeyspace <- decodeKeyspace(depcfg, "source_")
metadataKeyspace <- decodeKeyspace(depcfg, "metadata_")
} yield {
val s = EventingFunctionSettings(
settings.numLong("cpp_worker_thread_count").toOption,
settings.str("dcp_stream_boundary") match {
case Success("everything") => Some(EventingFunctionDcpBoundary.Everything)
case Success("from_now") => Some(EventingFunctionDcpBoundary.FromNow)
case _ => None
},
settings.str("description").toOption,
settings.str("log_level") match {
case Success("ERROR") => Some(EventingFunctionLogLevel.Error)
case Success("WARNING") => Some(EventingFunctionLogLevel.Warning)
case Success("INFO") => Some(EventingFunctionLogLevel.Info)
case Success("DEBUG") => Some(EventingFunctionLogLevel.Debug)
case Success("TRACE") => Some(EventingFunctionLogLevel.Trace)
case _ => None
},
settings.str("language_compatibility") match {
case Success("6.0.0") => Some(EventingFunctionLanguageCompatibility.Version_6_0_0)
case Success("6.5.0") => Some(EventingFunctionLanguageCompatibility.Version_6_5_0)
case Success("6.6.2") => Some(EventingFunctionLanguageCompatibility.Version_6_6_2)
case Success("7.2.0") => Some(EventingFunctionLanguageCompatibility.Version_7_2_0)
case _ => None
},
settings
.numLong("execution_timeout")
.toOption
.map(v => Duration.create(v, TimeUnit.SECONDS)),
settings.numLong("lcb_inst_capacity").toOption,
settings.numLong("lcb_retry_count").toOption,
settings.numLong("lcb_timeout").toOption.map(v => Duration.create(v, TimeUnit.SECONDS)),
settings.str("n1ql_consistency") match {
case Success("request") => Some(QueryScanConsistency.RequestPlus())
case _ => None
},
settings.numLong("num_timer_partitions").toOption,
settings.numLong("sock_batch_size").toOption,
settings
.numLong("tick_duration")
.toOption
.map(v => Duration.create(v, TimeUnit.MILLISECONDS)),
settings.numLong("timer_context_size").toOption,
settings.str("user_prefix").toOption,
settings.numLong("bucket_cache_size").toOption,
settings.numLong("bucket_cache_age").toOption,
settings.numLong("curl_max_allowed_resp_size").toOption,
settings.numLong("worker_count").toOption,
settings.bool("n1ql_prepare_all").toOption,
settings.arr("handler_headers").toOption.map(v => v.toSeq.map(x => x.toString)),
settings.arr("handler_footers").toOption.map(v => v.toSeq.map(x => x.toString)),
settings.bool("enable_applog_rotation").toOption,
settings.str("app_log_dir").toOption,
settings.numLong("app_log_max_size").toOption,
settings.numLong("app_log_max_files").toOption,
settings
.numLong("checkpoint_interval")
.toOption
.map(v => Duration.create(v, TimeUnit.SECONDS)),
settings.bool("processing_status") match {
case Success(true) => Some(EventingFunctionProcessingStatus.Running)
case Success(false) => Some(EventingFunctionProcessingStatus.Paused)
case _ => None
},
settings.bool("deployment_status") match {
case Success(true) => Some(EventingFunctionDeploymentStatus.Deployed)
case Success(false) => Some(EventingFunctionDeploymentStatus.Undeployed)
case _ => None
}
)
val bucketBindings: Option[Seq[EventingFunctionBucketBinding]] =
depcfg.arr("buckets") match {
case Success(ja) =>
Some(ja.iterator.map {
case v: JsonObjectSafe =>
EventingFunctionBucketBinding(
v.str("alias").get,
EventingFunctionKeyspace(
v.str("bucket_name").get,
v.str("scope_name").toOption,
v.str("collection_name").toOption
),
v.str("access") match {
case Success("rw") => EventingFunctionBucketAccess.ReadWrite
case _ => EventingFunctionBucketAccess.ReadOnly
}
)
}.toSeq)
case _ => None
}
val constantBindings: Option[Seq[EventingFunctionConstantBinding]] =
depcfg.arr("constants") match {
case Success(ja) =>
Some(ja.iterator.map {
case v: JsonObjectSafe =>
EventingFunctionConstantBinding(v.str("value").get, v.str("literal").get)
}.toSeq)
case _ => None
}
val urlBindings: Option[Seq[EventingFunctionUrlBinding]] = depcfg.arr("curl") match {
case Success(ja) =>
Some(ja.iterator.map {
case v: JsonObjectSafe =>
EventingFunctionUrlBinding(
v.str("hostname").get,
v.str("alias").get,
v.str("auth_type") match {
case Success("basic") =>
EventingFunctionUrlAuth.Basic(v.str("username").get, None)
case Success("digest") =>
EventingFunctionUrlAuth.Digest(v.str("username").get, None)
case Success("bearer") =>
EventingFunctionUrlAuth.Bearer(v.str("bearer_key").get)
case _ => EventingFunctionUrlAuth.None
},
v.bool("allow_cookies").getOrElse(false),
v.bool("validate_ssl_certificate").getOrElse(false)
)
}.toSeq)
case _ => None
}
EventingFunction(
appname,
appcode,
sourceKeyspace,
metadataKeyspace,
Some(s),
func.str("version").toOption,
func.bool("enforce_schema").toOption,
func.numLong("handleruuid").toOption,
func.str("function_instance_id").toOption,
bucketBindings,
urlBindings,
constantBindings
)
}
})
}
def decodeFunctions(encoded: Array[Byte]): Try[Seq[EventingFunction]] = {
val s = new String(encoded, StandardCharsets.UTF_8)
JsonArraySafe.fromJsonSafe(s) match {
case Success(j) =>
val x = j.iterator.map {
case v: JsonObjectSafe =>
val reencoded = v.toString
decodeFunction(reencoded.getBytes(StandardCharsets.UTF_8))
}.toSeq
FunctionalUtil.traverse(x)
case Failure(err) => Failure(err)
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy